{
tauri::async_runtime::spawn_blocking(move || {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "conversation.read")?;
@@ -5967,12 +5986,12 @@ pub(crate) async fn read_direct_project_history_slice(
};
let (raw_items, has_more, recorded_at_ms, first_item_id) =
read_direct_project_history_items_slice_at(root, anchor, limit.unwrap_or(20))?;
- let items = direct_thread_items_from_history(root, &raw_items, |item| {
- direct_thread_item_identity(item)
+ let items = thread_items_from_history(root, &raw_items, |item| {
+ thread_item_identity(item)
.and_then(|identity| recorded_at_ms.get(&identity).copied())
.unwrap_or_default()
});
- Ok(DirectThreadHistorySlice {
+ Ok(HistorySlice {
items,
has_more,
first_item_id,
diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs
index 9f4fe3db3..31d9323e1 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/main.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs
@@ -4,6 +4,11 @@
#[path = "../build_support/godot_bundle.rs"]
mod godot_bundle;
+// 复用随包资源声明的既有单测(校验通过/拒绝用例),生产运行时只经 codex_bundle 使用布局。
+#[cfg(test)]
+#[path = "../build_support/package_layout.rs"]
+mod package_layout;
+
use std::collections::BTreeMap;
use std::fs;
use std::fs::{File, OpenOptions};
@@ -2565,7 +2570,7 @@ fn main() {
})?;
setup_log.append("startup.runner.start.begin");
set_game_creator_agent_runtime_update_app_handle(app.handle().clone());
- set_direct_thread_manager_app_handle(app.handle().clone());
+ set_thread_manager_app_handle(app.handle().clone());
platform_maintenance::set_platform_maintenance_app_handle(app.handle().clone());
auth_session::initialize_auth_session(app.handle());
set_asset_generation_task_app_handle(app.handle().clone());
@@ -2590,7 +2595,6 @@ fn main() {
})
.invoke_handler(tauri::generate_handler![
analytics::gui::capture_analytics_context,
- analytics::gui::settle_direct_run_analytics,
analytics::gui::record_analytics_project_open,
analytics::gui::record_analytics_project_leave,
analytics::gui::record_analytics_ui_save,
@@ -2635,7 +2639,8 @@ fn main() {
chat_with_game_creator_agent,
chat_with_game_creator_role_agent,
chat_with_game_creator_role_agent_stream,
- chat_with_game_creator_direct_codex,
+ enqueue_direct_codex_turn,
+ remove_direct_project_pending_turn,
preflight_web_game_creation,
cancel_direct_codex_turn,
select_game_creator_reasoning_effort,
diff --git a/apps/ai-game-creator-shell/src-tauri/tauri.conf.json b/apps/ai-game-creator-shell/src-tauri/tauri.conf.json
index bed37e341..cf24da0c5 100644
--- a/apps/ai-game-creator-shell/src-tauri/tauri.conf.json
+++ b/apps/ai-game-creator-shell/src-tauri/tauri.conf.json
@@ -39,19 +39,14 @@
"vendor/codex-utils-path-uri/LICENSE": "licenses/codex-utils-path-uri/LICENSE",
"vendor/codex-utils-path-uri/NOTICE": "licenses/codex-utils-path-uri/NOTICE",
"vendor/codex-utils-absolute-path/LICENSE": "licenses/codex-utils-absolute-path/LICENSE",
- "vendor/codex-utils-absolute-path/NOTICE": "licenses/codex-utils-absolute-path/NOTICE",
- "resources/claude-agent": "claude-agent"
+ "vendor/codex-utils-absolute-path/NOTICE": "licenses/codex-utils-absolute-path/NOTICE"
},
"linux": {
"deb": {
- "depends": [
- "bubblewrap"
- ]
+ "depends": ["bubblewrap"]
},
"rpm": {
- "depends": [
- "bubblewrap"
- ]
+ "depends": ["bubblewrap"]
}
},
"icon": [
diff --git a/apps/ai-game-creator-shell/src-tauri/tauri.macos.conf.json b/apps/ai-game-creator-shell/src-tauri/tauri.macos.conf.json
index 019a0f248..ef3f1170b 100644
--- a/apps/ai-game-creator-shell/src-tauri/tauri.macos.conf.json
+++ b/apps/ai-game-creator-shell/src-tauri/tauri.macos.conf.json
@@ -19,7 +19,8 @@
"resources/codex/mac-native/darwin-x64/codex-package.json": "coding-agent/mac-native/darwin-x64/codex-package.json",
"resources/codex/mac-native/darwin-x64/NOTICE.md": "coding-agent/mac-native/darwin-x64/NOTICE.md",
"resources/codex/mac-native/darwin-x64/manifest.json": "coding-agent/mac-native/darwin-x64/manifest.json",
- "resources/plugins": "plugins"
+ "resources/plugins": "plugins",
+ "resources/claude-agent": "claude-agent"
}
},
"plugins": {
diff --git a/apps/ai-game-creator-shell/src-tauri/tauri.windows.conf.json b/apps/ai-game-creator-shell/src-tauri/tauri.windows.conf.json
index 5fa3b1b35..9ef52cc2a 100644
--- a/apps/ai-game-creator-shell/src-tauri/tauri.windows.conf.json
+++ b/apps/ai-game-creator-shell/src-tauri/tauri.windows.conf.json
@@ -12,7 +12,8 @@
"resources/codex/win-x64/codex-package.json": "coding-agent/win-x64/codex-package.json",
"resources/codex/win-x64/NOTICE.md": "coding-agent/win-x64/NOTICE.md",
"resources/codex/win-x64/manifest.json": "coding-agent/win-x64/manifest.json",
- "resources/plugins": "plugins"
+ "resources/plugins": "plugins",
+ "resources/claude-agent": "claude-agent"
}
}
}
diff --git a/apps/ai-game-creator-shell/src-tauri/tests/prompt_source_boundaries.rs b/apps/ai-game-creator-shell/src-tauri/tests/prompt_source_boundaries.rs
index 453b77e00..5ed98b980 100644
--- a/apps/ai-game-creator-shell/src-tauri/tests/prompt_source_boundaries.rs
+++ b/apps/ai-game-creator-shell/src-tauri/tests/prompt_source_boundaries.rs
@@ -258,7 +258,7 @@ fn prompt_entry_points_load_their_prose_from_external_files() {
),
(
"src/agent/direct_codex_user_item/wire.rs",
- &["render_ui_design_code_context"],
+ &["ui_design_code_context_for"],
),
(
"src/commands.rs",
diff --git a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts
index 2d68e7929..ee7b515f6 100644
--- a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts
+++ b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts
@@ -954,7 +954,7 @@ function directCodexDiagnosticFailureParts(
return { stageLabel, summary, hint, retryable: retryable === 'true' };
}
-/** 收口文案的一句话:阶段标签只有"回合失败"才成立,拒单那边传 `null`(见下面的导出函数)。 */
+/** 收口文案的一句话:阶段标签只有"回合失败"才成立,请求被拒那边传 `null`(见下面的导出函数)。 */
function directDiagnosticSentence(
parts: DirectDiagnosticParts,
stageLabel: string | null,
@@ -970,10 +970,10 @@ function directCodexDiagnosticFailureDetail(message: string) {
}
/**
- * **拒单**的可见文案:宿主收口文案里那段已脱敏的摘要与建议。
+ * **请求被拒**的可见文案:宿主收口文案里那段已脱敏的摘要与建议。
*
* 与 [`projectRuntimeVisibleError`] 的差别只有一处——**不带阶段标签**:阶段说的是"失败发生在交付的
- * 哪一步",而拒单是"这一轮没有开始",阶段只会是默认值(`code-generation`),套上去会把没发生的
+ * 哪一步",而请求被拒是"这一轮没有开始",阶段只会是默认值(`code-generation`),套上去会把没发生的
* 事讲成发生了。宿主的文案不是收口形状时退回同一份运行错误映射。
*/
export function projectRuntimeVisibleRejectionError(
diff --git a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx
index c18a6da4f..1b4a52659 100644
--- a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx
+++ b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx
@@ -1113,11 +1113,6 @@ export function RuntimeConfigDialog({
{plugin.name}
内置 Plugin · 不可卸载
- {plugin.lastError ? (
-
- {plugin.lastError}
-
- ) : null}
@@ -1258,11 +1253,6 @@ export function RuntimeConfigDialog({
{typeLabel} · 来自 {item.sourceName}
- {item.lastError ? (
-
- {item.lastError}
-
- ) : null}
diff --git a/apps/ai-game-creator-shell/src/services/clientAnalytics.ts b/apps/ai-game-creator-shell/src/services/clientAnalytics.ts
index 7e5c928c6..453c7ce85 100644
--- a/apps/ai-game-creator-shell/src/services/clientAnalytics.ts
+++ b/apps/ai-game-creator-shell/src/services/clientAnalytics.ts
@@ -1,39 +1,5 @@
import type { TauriInvoke } from '../app/types';
-// 一个 run 可因自动认证刷新调用多次 native,只确认最后一次尝试。
-export function beginDirectRunAnalytics(
- invoke: TauriInvoke,
- currentGeneration: () => number,
-) {
- const generation = currentGeneration();
- let attemptId: string | undefined;
- let settled = false;
- return {
- nextAttempt() {
- attemptId = undefined;
- try {
- attemptId = crypto.randomUUID();
- } catch {
- // 最后一次 UUID 失败不能留下前一次失败候选的标识。
- }
- return attemptId;
- },
- settle() {
- if (settled) return;
- settled = true;
- if (!attemptId) return;
- try {
- void invoke('settle_direct_run_analytics', {
- attemptId,
- discard: currentGeneration() !== generation,
- }).catch(() => undefined);
- } catch {
- // 不等待埋点,不改变调用链的成功、失败和取消结果。
- }
- },
- };
-}
-
// 宿主签发的无凭据上下文:前端只传回原值,不从当前登录态补身份。
type AnalyticsContext = {
route: { user_id: string | null; destination_origin: string | null };
diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx b/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx
index becc0792a..e10829c5f 100644
--- a/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx
+++ b/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx
@@ -130,8 +130,9 @@ export function DirectProjectChatView({
appendLocalMessage,
attachmentNotice,
attachmentsImporting,
- cancelQueuedTurn,
+ cancelPendingTurn,
cancelTurn,
+ commandInFlight,
composerNotice,
directEntries,
directTurnRunning,
@@ -139,11 +140,10 @@ export function DirectProjectChatView({
historyHasMore,
loadEarlierHistory,
localMessages,
- queuedTurns,
+ pendingTurns,
startInitialTurn,
statusNotice,
submit,
- turnBusy,
turnCancelling,
uploadFiles,
} = chat;
@@ -161,14 +161,15 @@ export function DirectProjectChatView({
}),
[directEntries, localMessages, directTurnRunning, directTurnStartedAt],
);
- // 「这一轮在跑吗」只从这一个派生入口读:原生真相 / 本地命令在飞 / 最新一轮两态。
+ // 「这一轮在跑吗」只从这一个派生入口读:原生真相 / IPC 在飞 / 待发消息条数 / 最新一轮两态。
const turnStatus = useDirectProjectTurnStatus({
turnRunning: directTurnRunning,
- turnBusy,
+ commandInFlight,
+ pendingTurns,
turns: directTurns,
});
// 状态条的起点只认**未结束**的最新一轮:只有它才拿得到本轮的 `turn.started.at`(运行中读实时值)。
- // 只可能是最后一轮:原生 `turnRunning` 只赋给最新一轮。接单窗口里还没有这一轮的条目,
+ // 只可能是最后一轮:原生 `turnRunning` 只赋给最新一轮。入队窗口里还没有这一轮的条目,
// 于是这里是 0,卡片只报"正在处理"、不读秒(宿主开始事件一到就开始读秒)。
const latestTurn = directTurns.at(-1) ?? null;
const activeTurnStartedAt =
@@ -273,11 +274,14 @@ export function DirectProjectChatView({
projectPath={projectPath}
attachmentNotice={attachmentNotice}
attachmentsImporting={attachmentsImporting}
- queuedTurns={queuedTurns}
+ pendingTurns={pendingTurns}
composerNotice={composerNotice}
busy={turnStatus.displayBusy}
+ canStopTurn={turnStatus.canStopTurn}
turnCancelling={turnCancelling}
- onCancelQueuedTurn={cancelQueuedTurn}
+ onCancelPendingTurn={(clientTurnId) =>
+ void cancelPendingTurn(clientTurnId)
+ }
onCancelTurn={() => void cancelTurn()}
onReferencePickerOpen={() => {
// 素材标签、改名、删除这些写入发生在资源画布,聊天侧收不到失效事件:
diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/ComposerControls.tsx b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/ComposerControls.tsx
index dc4904ed0..3db8b8f3b 100644
--- a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/ComposerControls.tsx
+++ b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/ComposerControls.tsx
@@ -3,7 +3,7 @@
* 模型 + 麦克风 + 发送/终止,以及输入盒上方的待发附件与消息队列 chip。
*
* 这些组件只承载表现与交互;回合附件由宿主导入后插成正文芯片(正文是唯一事实源),
- * 队列由 `chatComposerQueue.ts` 的纯函数维护。
+ * 待发消息队列由宿主持有(这里只是一份事件投影,文案派生见 `pendingTurnChipLabel.ts`)。
*/
import {
Check,
@@ -30,8 +30,7 @@ import {
DEFAULT_COMPOSER_REASONING_EFFORT,
normalizeComposerReasoningEffort,
} from '../../../../../features/project-workspace/composerReasoningEffort';
-import type { QueuedChatTurn } from './chatComposerQueue';
-import { queuedChatTurnLabel } from './chatComposerQueue';
+import type { DirectPendingTurn } from '../../conversation/directPendingTurns';
import {
resolveSpeechRecognitionCtor,
speechEventTranscript,
@@ -41,6 +40,7 @@ import {
type SpeechRecognitionLike,
VOICE_INPUT_UNSUPPORTED_MESSAGE,
} from './chatComposerVoice';
+import { pendingTurnChipLabel } from './pendingTurnChipLabel';
type ComposerAttachmentMenuProps = {
disabled: boolean;
@@ -151,38 +151,46 @@ export function ComposerAttachmentMenu({
);
}
-/** 队列 chip:回合运行中入队的消息,按 FIFO 顺序展示,可单条取消。 */
+/**
+ * 待发消息 chip:还没放行的消息,按 FIFO 顺序展示,可单条取消。
+ *
+ * 列表就是宿主事件流折出来的那一份(`DirectPendingTurn`),顺序与宿主认领队首的顺序一致;
+ * 这里不排序、不重排,也不自己决定放行。
+ */
export function ComposerTurnQueue({
turns,
assets,
onCancel,
}: {
- turns: readonly QueuedChatTurn[];
+ turns: readonly DirectPendingTurn[];
/** 与聊天消息渲染同源的素材清单:chip 文案里的 `@` 引用按它展开成显示名。 */
assets: readonly GameCreationAppAssetManifestEntry[];
- onCancel: (id: string) => void;
+ onCancel: (clientTurnId: string) => void;
}) {
if (turns.length === 0) {
return null;
}
return (
- {turns.map((turn, index) => (
- -
- {index + 1}
-
- {queuedChatTurnLabel(turn, assets)}
-
-
-
- ))}
+ {turns.map((turn, index) => {
+ const label = pendingTurnChipLabel(turn, assets);
+ return (
+ -
+
+ {index + 1}
+
+ {label}
+
+
+ );
+ })}
);
}
diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx
index c92cd74d1..f151e8344 100644
--- a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx
+++ b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx
@@ -23,8 +23,8 @@ import {
} from '../../../../../features/project-workspace/ResourceReferencePicker';
import { assetsSignature } from '../../../../../features/project-workspace/resourceReferences';
import type { DirectCodexTurnAttachment } from '../../conversation/directCodexTurnAttachments';
+import type { DirectPendingTurn } from '../../conversation/directPendingTurns';
import type { DirectCodexUserContentPart } from '../../generated/DirectCodexUserContentPart';
-import type { QueuedChatTurn } from './chatComposerQueue';
import {
ComposerAttachmentMenu,
ComposerReasoningEffortSelect,
@@ -43,10 +43,10 @@ function draftAttachmentCount(draft: {
}
/**
- * DirectProject 输入区:队列、附件、`@` 引用输入框、模型/推理选择和发送/终止。
+ * DirectProject 输入区:待发消息 chip、附件、`@` 引用输入框、模型/推理选择和发送/终止。
*
- * 模型可用性校验、语音提示这类输入区自己的瞬时状态留在这里;回合、队列和附件的事实源
- * 仍然在聊天容器里,这里只负责把用户动作交回去。
+ * 模型可用性校验、语音提示这类输入区自己的瞬时状态留在这里;回合、待发消息队列和附件的事实源
+ * 仍然在聊天容器里(队列更上游是宿主),这里只负责把用户动作交回去。
*/
export function DirectProjectComposer({
assets,
@@ -54,11 +54,12 @@ export function DirectProjectComposer({
projectPath,
attachmentNotice,
attachmentsImporting,
- queuedTurns,
+ pendingTurns,
composerNotice,
busy,
+ canStopTurn,
turnCancelling,
- onCancelQueuedTurn,
+ onCancelPendingTurn,
onCancelTurn,
onReferencePickerOpen,
onSubmit,
@@ -70,11 +71,19 @@ export function DirectProjectComposer({
attachmentNotice: string;
/** 附件导入进行中:导入结果还没落成芯片,这一轮先不许发出去。 */
attachmentsImporting: boolean;
- queuedTurns: QueuedChatTurn[];
+ pendingTurns: readonly DirectPendingTurn[];
composerNotice: string;
+ /** 「界面忙不忙」:三者并集,用来锁住模型 / 附件 / 语音这些与当前这轮冲突的控件。 */
busy: boolean;
+ /**
+ * 「现在点终止有没有作用对象」(见 `useDirectProjectTurnStatus` 的 `canStopTurn`)。
+ *
+ * 与 `busy` 分开:队列非空也算忙,但待发消息不属于「终止」——按 `busy` 显示终止钮会得到一个
+ * 点了只会报「当前没有正在运行的回合」的按钮。发送钮 / 终止钮只按它换。
+ */
+ canStopTurn: boolean;
turnCancelling: boolean;
- onCancelQueuedTurn: (id: string) => void;
+ onCancelPendingTurn: (clientTurnId: string) => void;
onCancelTurn: () => void;
/**
* 打开 `@` 选择器前刷新一次项目清单。
@@ -84,7 +93,14 @@ export function DirectProjectComposer({
* 用户真正要看这份清单的时刻是打开选择器的这一刻,所以在这里补一次重读。
*/
onReferencePickerOpen?: () => void;
- onSubmit: (content: DirectCodexUserContentPart[]) => boolean;
+ /**
+ * 提交草稿:返回**是否把这条消息交出去了**。
+ *
+ * `true` 只代表"草稿可以清"——入队成立、或已经交给权限确认流程(确认后会重跑同一份内容);
+ * 入队失败里用户自己能改的那些返回 `false`,草稿原样留在输入盒里,不让一条被拒的消息从
+ * 界面上凭空消失。
+ */
+ onSubmit: (content: DirectCodexUserContentPart[]) => Promise;
/**
* 导入文件:`draftAttachmentCount` 是草稿里已有的附件芯片数(上限按它算)。
* 返回导入成功的附件——由这里插成正文芯片;失败的一条都不插入。
@@ -152,7 +168,8 @@ export function DirectProjectComposer({
},
[onUploadFiles],
);
- // 这个按钮只在非 busy 分支出现(busy 时由 ComposerStopButton 接管),所以不再带忙态逻辑。
+ // 这个按钮只在"没有可终止对象"的分支出现(有的话由 ComposerStopButton 接管),
+ // 所以不再带忙态逻辑:队列里有待发消息时它照样在,用户能继续排。
const submitButton = (