接通 DirectProject 中间阶段进度

订阅 game-creator-agent-progress 并按项目过滤

在执行过程卡显示真实阶段文案并补充回归测试
This commit is contained in:
kdletters
2026-10-06 12:05:23 +08:00
parent c0c9179d5c
commit 2c285fbd99
4 changed files with 71 additions and 2 deletions
@@ -13,6 +13,11 @@ import type { PendingUiConfirmation } from '../../../app/types';
import { projectNameFromPath } from '../../../features/agent-runtime';
import { registerActiveChatComposer } from '../../../features/project-workspace/activeChatComposer';
import { userItemFromContent } from '../../../features/project-workspace/resourceReferences';
import { visibleClientErrorMessage } from '../../../services/errorReporting';
import {
canSubscribeTauriEvents,
subscribeTauriEvent,
} from '../../../services/tauriEventSubscription';
import { type ApprovalMode, approvalModeLabel } from '../approvalMode';
import { ApprovalModeDialog } from '../ApprovalModeDialog';
import { DirectProjectChatHeader } from './components/DirectProjectChatHeader/DirectProjectChatHeader';
@@ -115,6 +120,7 @@ export function DirectProjectChatView({
} = useDirectProjectManifest(projectPath);
const composerHandleRef = useRef<DirectProjectComposerHandle | null>(null);
const [runtimeNotice, setRuntimeNotice] = useState('');
const [agentProgress, setAgentProgress] = useState('');
const [settingsOpen, setSettingsOpen] = useState(false);
const [approvalOpen, setApprovalOpen] = useState(false);
const [approvalMode, setApprovalMode] = useState<ApprovalMode>('strict');
@@ -123,6 +129,39 @@ export function DirectProjectChatView({
const handleRuntimeError = useCallback((message: string) => {
setRuntimeNotice(message);
}, []);
useEffect(() => {
setAgentProgress('');
if (!projectPath || !canSubscribeTauriEvents()) return undefined;
let disposed = false;
let unsubscribe: (() => void) | null = null;
void subscribeTauriEvent<{
project_path?: string;
projectPath?: string;
message?: string;
}>('game-creator-agent-progress', (event) => {
if (disposed) return;
const eventProjectPath =
event.payload.project_path ?? event.payload.projectPath ?? '';
if (eventProjectPath !== projectPath) return;
const message = event.payload.message?.trim();
if (message) setAgentProgress(message);
})
.then((release) => {
if (disposed) release();
else unsubscribe = release;
})
.catch((error) => {
if (!disposed) {
setRuntimeNotice(
`执行过程进度订阅失败:${visibleClientErrorMessage(error)}`,
);
}
});
return () => {
disposed = true;
unsubscribe?.();
};
}, [projectPath]);
const chat = useDirectProjectChatController({
enabled: Boolean(projectPath),
ensureConversationReadAllowed,
@@ -156,6 +195,13 @@ export function DirectProjectChatView({
turnCancelling,
uploadFiles,
} = chat;
useEffect(() => {
// 阶段文案属于当前回合;清掉上一轮的最后阶段,避免下一轮尚未收到首个进度事件时
// 继续显示旧的“正在验证/正在生成”。
if (commandInFlight || (!directTurnRunning && pendingTurns.length === 0)) {
setAgentProgress('');
}
}, [commandInFlight, directTurnRunning, pendingTurns.length]);
// 这份投影要参与下游 memo 的判据,必须自己先缓存:不缓存的话每次渲染都是一份新数组,
// `useDirectProjectTurnStatus` 里以 `turns` 为判据的 `useMemo` 永远命中不了,那层 memo
// 就成了死代码,读代码的人还会误以为 `turnStatus` 是引用稳定的。
@@ -265,6 +311,7 @@ export function DirectProjectChatView({
historyLoading={historyLoading}
historyError={historyError}
turnInFlight={turnStatus.displayBusy}
activeProgress={agentProgress}
activeTurnStartedAt={activeTurnStartedAt}
onLoadEarlierHistory={() => void loadEarlierHistory()}
onRetryEarlierHistory={() => void retryEarlierHistory()}
@@ -26,6 +26,7 @@ export function DirectProjectConversation({
historyLoading,
historyError,
turnInFlight,
activeProgress = '',
activeTurnStartedAt,
onLoadEarlierHistory,
onRetryEarlierHistory,
@@ -46,6 +47,8 @@ export function DirectProjectConversation({
* `activeTurnStartedAt` 还是 0,卡片只报"正在处理";宿主开始事件一到就开始读秒。
*/
turnInFlight: boolean;
/** Rust `game-creator-agent-progress` 的当前阶段;没有事件时保留通用忙态。 */
activeProgress?: string;
activeTurnStartedAt: number;
onLoadEarlierHistory: () => void;
onRetryEarlierHistory: () => void;
@@ -105,7 +108,7 @@ export function DirectProjectConversation({
>
<header>
<span aria-hidden="true" />
<strong>陶泥儿正在处理</strong>
<strong>{activeProgress || '陶泥儿正在处理'}</strong>
<DirectTurnElapsed startedAt={activeTurnStartedAt} />
</header>
</AgentMessageContent>
@@ -77,3 +77,22 @@ test('没有运行中的回合时不订阅时钟', () => {
vi.useRealTimers();
}
});
test('宿主阶段进度会显示在运行中过程卡,而不是只显示通用忙态', () => {
const view = render(
<DirectProjectConversation
conversationKey="direct-process-progress"
turns={[]}
historyHasMore={false}
historyLoading={false}
historyError={null}
turnInFlight
activeProgress="正在验证游戏"
activeTurnStartedAt={0}
onLoadEarlierHistory={() => undefined}
onRetryEarlierHistory={() => undefined}
/>,
);
expect(view.getByText('正在验证游戏')).toBeTruthy();
expect(view.queryByText('陶泥儿正在处理')).toBeNull();
});
@@ -5,7 +5,7 @@
## 2026-10-06 AGC 失败终态与 CC/MCP 边界不能吞掉协议、清理和 I/O 错误
- **现象**:DirectProject 的 `turn.completed(status=failed)` 若跨 IPC 丢失 `failure` 载荷,前端会只收口回合而不显示任何说明;Claude Code sidecar 的非 UTF-8 stderr、进程树清理失败、会话表锁损坏,以及 MCP loopback 服务退出、插件能力桥超时、损坏 manifest,会分别被压成空 detail、继续成功取消、伪造 hash projectId 或静默不广告插件。
- **现行口径**:失败载荷缺失显示明确的协议错误;app-server 只给 `failed` 状态却不带 `error` 对象时,正文保留 `status=failed error=missing` 协议事实;旧历史里的已知通用失败句明确标成“历史错误事件未记录具体错误正文”,不伪装成当前仍在吞错;CC 清理、stderr 解码、会话锁都保留原始正文;MCP server / capability bridge 失败写入统一应用日志,MCP manifest / journal / 时间 / metadata 读取失败返回具体错误;完成判定在把素材缺失归因于“未引用平台图”前先验证源码、manifest 和已登记图片的 I/O/PNG 错误;资源生成队列提交异常直接收口失败并恢复可重试草稿。切片文件是可选投影,完整 spritesheet 不因无切片而误失败。
- **现行口径**:失败载荷缺失显示明确的协议错误;app-server 只给 `failed` 状态却不带 `error` 对象时,正文保留 `status=failed error=missing` 协议事实;旧历史里的已知通用失败句明确标成“历史错误事件未记录具体错误正文”,不伪装成当前仍在吞错;CC 清理、stderr 解码、会话锁都保留原始正文;后端 `game-creator-agent-progress` 阶段事件由 DirectProject 按项目路径订阅并显示在执行过程卡,不再只显示空白忙态;MCP server / capability bridge 失败写入统一应用日志,MCP manifest / journal / 时间 / metadata 读取失败返回具体错误;完成判定在把素材缺失归因于“未引用平台图”前先验证源码、manifest 和已登记图片的 I/O/PNG 错误;资源生成队列提交异常直接收口失败并恢复可重试草稿。切片文件是可选投影,完整 spritesheet 不因无切片而误失败。
- **验证**:`cargo test agent::claude_code_cli::tests::` 14 passed;`cargo test agent::direct_tools_mcp::tests::` 34 passed;`cargo test agent::direct_runtime::tests::` 92 passed;`npx vitest run tests/directThreadChat.test.ts tests/directTurnFailure.test.ts` 64 passed;另有损坏 manifest 回归用例 1 passed。新失败正文仍经既有脱敏边界处理。
- **关联**:`agent/claude_code_cli.rs`、`agent/direct_tools_mcp.rs`、`agent/direct_runtime/mod.rs`、`agent/thread_manager/dispatch.rs`、`chat/conversation/directThreadChat.ts`。