diff --git a/apps/ai-game-creator-shell/src/components/WindowChrome.tsx b/apps/ai-game-creator-shell/src/components/WindowChrome.tsx index 19d87059d..2d456e33d 100644 --- a/apps/ai-game-creator-shell/src/components/WindowChrome.tsx +++ b/apps/ai-game-creator-shell/src/components/WindowChrome.tsx @@ -178,6 +178,7 @@ export function WindowChrome({ children }: WindowChromeProps) { activeTurns={activeProjectRuns.activeTurns} currentProjectPath={activeProjectRuns.currentProjectPath} readFailed={activeProjectRuns.readFailed} + readError={activeProjectRuns.readError} onOpenProject={activeProjectRuns.onOpenProject} placement="titlebar" /> diff --git a/apps/ai-game-creator-shell/src/components/windowChromeContext.ts b/apps/ai-game-creator-shell/src/components/windowChromeContext.ts index 5e9141404..283d6c0cf 100644 --- a/apps/ai-game-creator-shell/src/components/windowChromeContext.ts +++ b/apps/ai-game-creator-shell/src/components/windowChromeContext.ts @@ -19,6 +19,7 @@ export type WindowChromeActiveProjectRuns = { activeTurns: GameCreatorDirectActiveTurn[]; currentProjectPath?: string | null; readFailed?: boolean; + readError?: string; onOpenProject?: (projectPath: string) => void; }; diff --git a/apps/ai-game-creator-shell/src/features/agent-runtime/directActiveTurns.ts b/apps/ai-game-creator-shell/src/features/agent-runtime/directActiveTurns.ts index 926706f9d..be98e4981 100644 --- a/apps/ai-game-creator-shell/src/features/agent-runtime/directActiveTurns.ts +++ b/apps/ai-game-creator-shell/src/features/agent-runtime/directActiveTurns.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import type { GameCreatorDirectActiveTurn, TauriInvoke } from '../../app/types'; +import { visibleClientErrorMessage } from '../../services/errorReporting'; import { canSubscribeTauriEvents, subscribeTauriEvent, @@ -35,6 +36,7 @@ export function useDirectActiveTurns({ [], ); const [snapshotReadFailed, setSnapshotReadFailed] = useState(false); + const [snapshotReadError, setSnapshotReadError] = useState(''); const mountedRef = useRef(true); const inFlightRef = useRef | null>(null); /** @@ -93,10 +95,12 @@ export function useDirectActiveTurns({ setActiveTurns(nextTurns); } setSnapshotReadFailed(false); + setSnapshotReadError(''); inFlightRef.current = null; return; - } catch { + } catch (error) { if (!isCurrent()) return; + setSnapshotReadError(visibleClientErrorMessage(error)); if (attempt < DIRECT_ACTIVE_TURNS_READ_ATTEMPTS) { await new Promise((resolve) => { retryTimerRef.current = window.setTimeout(() => { @@ -132,6 +136,7 @@ export function useDirectActiveTurns({ // 空态也要保持引用稳定:已经空了就不要再换一个新数组。 setActiveTurns((current) => (current.length === 0 ? current : [])); setSnapshotReadFailed((current) => (current ? false : current)); + setSnapshotReadError((current) => (current ? '' : current)); return; } let disposed = false; @@ -172,5 +177,10 @@ export function useDirectActiveTurns({ }; }, [enabled, invoke, refreshActiveTurns]); - return { activeTurns, refreshActiveTurns, snapshotReadFailed }; + return { + activeTurns, + refreshActiveTurns, + snapshotReadFailed, + snapshotReadError, + }; } diff --git a/apps/ai-game-creator-shell/src/features/app-shell/ActiveProjectRunsPanel.tsx b/apps/ai-game-creator-shell/src/features/app-shell/ActiveProjectRunsPanel.tsx index 1df87f2bb..d3cb512c0 100644 --- a/apps/ai-game-creator-shell/src/features/app-shell/ActiveProjectRunsPanel.tsx +++ b/apps/ai-game-creator-shell/src/features/app-shell/ActiveProjectRunsPanel.tsx @@ -16,6 +16,7 @@ export type ActiveProjectRunsPanelProps = { activeTurns: GameCreatorDirectActiveTurn[]; currentProjectPath?: string | null; readFailed?: boolean; + readError?: string; onOpenProject?: (projectPath: string) => void; placement?: 'panel' | 'titlebar'; }; @@ -59,6 +60,7 @@ export function ActiveProjectRunsPanel({ activeTurns, currentProjectPath = null, readFailed = false, + readError = '', onOpenProject, placement = 'panel', }: ActiveProjectRunsPanelProps) { @@ -97,7 +99,7 @@ export function ActiveProjectRunsPanel({ className="launcher-runs-titlebar launcher-runs-titlebar--error" role="status" > - 正在运行的项目读取失败 + {readError || '正在运行的项目读取失败'} ); } @@ -105,7 +107,7 @@ export function ActiveProjectRunsPanel({ return ( ); @@ -166,10 +168,10 @@ export function ActiveProjectRunsPanel({ const elapsed = formatActiveTurnElapsed(turn.startedAt, now); const isCurrent = Boolean( currentProjectPath && - projectPathsMatchForInvalidation( - turn.projectPath, - currentProjectPath, - ), + projectPathsMatchForInvalidation( + turn.projectPath, + currentProjectPath, + ), ); return (
  • @@ -215,10 +217,10 @@ export function ActiveProjectRunsPanel({ const elapsed = formatActiveTurnElapsed(turn.startedAt, now); const isCurrent = Boolean( currentProjectPath && - projectPathsMatchForInvalidation( - turn.projectPath, - currentProjectPath, - ), + projectPathsMatchForInvalidation( + turn.projectPath, + currentProjectPath, + ), ); return (
  • diff --git a/apps/ai-game-creator-shell/src/features/app-shell/WorkspaceLauncher.tsx b/apps/ai-game-creator-shell/src/features/app-shell/WorkspaceLauncher.tsx index 2679b6061..fc59ca9e9 100644 --- a/apps/ai-game-creator-shell/src/features/app-shell/WorkspaceLauncher.tsx +++ b/apps/ai-game-creator-shell/src/features/app-shell/WorkspaceLauncher.tsx @@ -174,10 +174,11 @@ export function WorkspaceLauncherShell({ : null, ); const directInvoke = resolveTauriInvoke(); - const { activeTurns, snapshotReadFailed } = useDirectActiveTurns({ - invoke: directInvoke, - enabled: true, - }); + const { activeTurns, snapshotReadFailed, snapshotReadError } = + useDirectActiveTurns({ + invoke: directInvoke, + enabled: true, + }); const switchedToGameRuntime = gameRuntimeSwitch !== null && currentProjectContext !== null && @@ -304,6 +305,7 @@ export function WorkspaceLauncherShell({ activeTurns, currentProjectPath: currentProjectContext?.projectPath ?? null, readFailed: snapshotReadFailed, + readError: snapshotReadError, onOpenProject: (projectPath: string) => openActiveProjectRef.current(projectPath), }); @@ -312,6 +314,7 @@ export function WorkspaceLauncherShell({ currentProjectContext?.projectPath, setActiveProjectRuns, snapshotReadFailed, + snapshotReadError, ]); useEffect(() => () => setActiveProjectRuns(null), [setActiveProjectRuns]); diff --git a/apps/ai-game-creator-shell/tests/directActiveTurns.test.tsx b/apps/ai-game-creator-shell/tests/directActiveTurns.test.tsx index 52c52c362..26daa2e00 100644 --- a/apps/ai-game-creator-shell/tests/directActiveTurns.test.tsx +++ b/apps/ai-game-creator-shell/tests/directActiveTurns.test.tsx @@ -152,6 +152,9 @@ describe('useDirectActiveTurns', () => { }); expect(result.current.activeTurns).toBe(firstSnapshot); expect(result.current.snapshotReadFailed).toBe(true); + expect(result.current.snapshotReadError).toContain( + 'snapshot unavailable', + ); } finally { vi.useRealTimers(); } diff --git a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md index c6c592b15..9071bcc2a 100644 --- a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md +++ b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md @@ -135,6 +135,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 - 模板库权限探测和 Direct 线程订阅/consume/bootstrap 属于用户可见的前端状态边界:权限/IPC 失败可以关闭入口或结束订阅,但必须同时保留安全错误正文;禁止用空 catch、只 settle 锚点或清空状态来吞掉认证、网络和 IPC 原因。 - UI 编辑器剪贴板、文件选择、预览等原生 IPC 操作的失败提示也必须经过 `visibleClientErrorMessage`;操作指引可以保留,但不能覆盖原生错误正文。 - `agc_web_search` 的上游非 2xx 也属于用户可见的受控工具失败:MCP 结果必须保留 HTTP 状态与有界脱敏正文,不能只返回状态码。 +- 跨项目活动回合快照读取失败即使保留旧快照,也必须把最近一次安全正文一并呈现;“读不到不等于没有在跑”不能变成“读不到所以没有原因”。 ## 后果与边界 diff --git a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md index 471e52848..ec9a2096c 100644 --- a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md @@ -56,3 +56,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - 前端继续发现两个静默出口:模板库权限检查失败会直接清空入口,Direct 线程订阅/consume/bootstrap 的 IPC 失败会只 settle anchor 而不显示正文;现分别保留权限检查错误,并把订阅链错误正文送入项目状态栏/运行错误出口。认证失败回归已验证显示 `authentication-required`,不再变成泛化“执行失败,请稍后重试”。 - UI 编辑器复制路径的原生剪贴板 IPC 失败也曾只显示“复制失败,请手动复制路径”,现保留脱敏后的剪贴板错误正文并同时给出手动复制指引。 - 受控联网搜索的上游非 2xx 响应原来只返回 HTTP 状态;现保留有界、脱敏的服务端正文,错误码/限流原因不再丢失。 +- 窗口标题栏的 Direct 活动回合快照读取失败原来只给“读取失败”标志并保留旧快照;现额外保留脱敏后的 IPC/读取正文,面板不会把真实原因吞掉。