diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx index 0d7c44c42..af2336e9a 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx @@ -70,6 +70,16 @@ function directCodexTurnMessageId(turnId: string, role: 'user' | 'assistant') { return `direct-codex:${turnId}:${role}`; } +/** 从 `direct-codex::assistant` 反解回合 id;不是这个形状返回 `null`。 */ +function directCodexTurnIdFromAssistantMessageId(messageId: string) { + const prefix = 'direct-codex:'; + const suffix = ':assistant'; + if (!messageId.startsWith(prefix) || !messageId.endsWith(suffix)) { + return null; + } + return messageId.slice(prefix.length, -suffix.length); +} + type RuntimePanelProps = ComponentProps; function directStatusTitle(status: string | null | undefined) { @@ -233,6 +243,7 @@ export function ProjectSupervisorView({ const streamTurnId = activeTurnId?.trim() ?? ''; const toolCallsByAnchor = new Map(); const liveToolCalls: GameCreatorDirectToolCall[] = []; + let liveToolCallTurnId = ''; if (directCodex) { for (const call of toolCalls) { const expected = directCodexTurnMessageId(call.turnId, 'assistant'); @@ -249,10 +260,24 @@ export function ProjectSupervisorView({ continue; } if (streamTurnId && call.turnId === streamTurnId) { + if (!liveToolCallTurnId) { + liveToolCallTurnId = call.turnId; + } liveToolCalls.push(call); } } } + // 该回合用户消息的 `updatedAt`:拿得到就在块头显示「发送 → 结束」,拿不到只显示结束时间。 + const userMessageUpdatedAtForTurn = (turnId: string) => { + if (!turnId) { + return 0; + } + const userId = directCodexTurnMessageId(turnId, 'user'); + return ( + visibleMessages.find((message) => message.messageId === userId) + ?.updatedAt ?? 0 + ); + }; const emptyState = directCodex && visibleMessages.length === 0 && @@ -363,11 +388,17 @@ export function ProjectSupervisorView({ const anchoredToolCalls = message.messageId ? (toolCallsByAnchor.get(message.messageId) ?? []) : []; + const anchoredTurnId = message.messageId + ? directCodexTurnIdFromAssistantMessageId(message.messageId) + : null; return ( {anchoredToolCalls.length > 0 ? ( ) : null} @@ -383,6 +414,7 @@ export function ProjectSupervisorView({ {liveToolCalls.length > 0 ? ( ) : null} diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ToolCallGroup.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ToolCallGroup.tsx index 781c12f94..62fbf8f27 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ToolCallGroup.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ToolCallGroup.tsx @@ -10,26 +10,36 @@ import { useId, useState } from 'react'; import type { GameCreatorDirectToolCall } from '../../app/types'; import { + formatClockTime, + formatToolCallDuration, + formatTurnDuration, + toolCallDurationMs, toolCallGroupSummary, toolCallRowText, + turnToolCallDurationMs, + turnToolCallEndedAt, + turnToolCallTimeLabel, } from './toolCallGroupPresentation'; /** * 一回合的工具调用折叠块(Codex 风格): - * 块头一行汇总,展开态每行一条工具(行可二级展开看命令 / 文件明细 / 输出)。 + * 块头一行汇总 + 该回合总用时 + 结束时间,展开态每行一条工具(行可二级展开看命令 / 文件明细 / 输出)。 * 契约见 `docs/technical/【技术方案】GameAgent对话工具调用卡片-2026-09-14.md`。 * * 无障碍:块头与每一行都是 `