diff --git a/apps/ai-game-creator-shell/src/components/WindowChrome.tsx b/apps/ai-game-creator-shell/src/components/WindowChrome.tsx index 19d87059d..6801e0212 100644 --- a/apps/ai-game-creator-shell/src/components/WindowChrome.tsx +++ b/apps/ai-game-creator-shell/src/components/WindowChrome.tsx @@ -177,6 +177,7 @@ export function WindowChrome({ children }: WindowChromeProps) { void; }; 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..9d17d308b 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 @@ -6,15 +6,18 @@ import { projectNameFromPath } from '../agent-runtime'; import { projectPathsMatchForInvalidation } from '../project-summary/projectPath'; /** - * 窗口标题栏的"正在运行的项目"入口,也保留面板布局供独立组件测试和复用。 + * 窗口标题栏的项目入口,也保留面板布局供独立组件测试和复用。 * - * 数据来自 Rust 的活动回合注册表(同一个只读快照也用于重新进入项目时的进度重连), - * 面板只负责呈现:项目名、阶段、已运行时长,以及点击进入该项目。没有在跑回合时 - * 整块不渲染,不留空白占位。 + * 数据来自 Rust 的活动回合注册表(同一个只读快照也用于重新进入项目时的进度重连)。 + * 标题栏形态的主文案恒为**当前项目**:正在运行的项目只占圆点、数量徽标与展开菜单, + * 否则「我在哪个项目」会被后台别的项目顶掉;不在项目内(没有 `currentProjectName`)时 + * 才回落到最近启动的运行项目。没有在跑回合且没读失败时整块不渲染,不留空白占位。 */ export type ActiveProjectRunsPanelProps = { activeTurns: GameCreatorDirectActiveTurn[]; currentProjectPath?: string | null; + /** 当前工作台打开的项目名;有值即顶栏主文案,运行中的项目不再抢它。 */ + currentProjectName?: string | null; readFailed?: boolean; onOpenProject?: (projectPath: string) => void; placement?: 'panel' | 'titlebar'; @@ -58,6 +61,7 @@ function activeTurnDisplayName(turn: GameCreatorDirectActiveTurn) { export function ActiveProjectRunsPanel({ activeTurns, currentProjectPath = null, + currentProjectName = null, readFailed = false, onOpenProject, placement = 'panel', @@ -87,11 +91,26 @@ export function ActiveProjectRunsPanel({ }; }, [open, placement]); - if (activeTurns.length === 0) { - if (!readFailed) { + const now = Date.now(); + const orderedTurns = [...activeTurns].sort( + (left, right) => left.startedAt - right.startedAt, + ); + const latestTurn = orderedTurns[orderedTurns.length - 1] ?? null; + const runningCount = orderedTurns.length; + const currentName = currentProjectName?.trim() ?? ''; + /** + * 标签主文案只认当前项目:正在运行的项目不在标题栏抢主文案,只在徽标和展开菜单里表达。 + * 不在项目内(没有当前项目名)时才回落到最近启动的运行项目。 + */ + const primaryName = + currentName || (latestTurn ? activeTurnDisplayName(latestTurn) : ''); + + if (placement === 'titlebar') { + if (!latestTurn && !readFailed) { return null; } - if (placement === 'titlebar') { + // 读不到快照、又没有当前项目可显示:保持原来那条只读提示,不假装成可展开入口。 + if (!primaryName) { return ( ); } - // 三次都没读到快照:只说"没读到",不改写成业务、权限或审批结论。 - return ( - - ); - } - - const now = Date.now(); - const orderedTurns = [...activeTurns].sort( - (left, right) => left.startedAt - right.startedAt, - ); - if (placement === 'titlebar') { - const latestTurn = orderedTurns[orderedTurns.length - 1]; - if (!latestTurn) { - return null; - } - const latestName = activeTurnDisplayName(latestTurn); + /** + * 标签提示 = 作用域 + 运行状态,分成两段是为了不在一句话里重复「正在运行」: + * 回落态的主文案本身就是「最近启动的在跑项目」,状态位再写一遍会读成「正在运行的项目:X; + * 正在运行 N 个项目」。 + */ + const scopeLabel = currentName + ? `当前项目:${primaryName}` + : `最近运行的项目:${primaryName}`; + const runStateLabel = readFailed + ? '正在运行的项目读取失败' + : `共 ${runningCount} 个项目在运行`; + const triggerLabel = `${scopeLabel};${runStateLabel}`; const openProject = (projectPath: string) => { setOpen(false); onOpenProject?.(projectPath); @@ -129,23 +140,30 @@ export function ActiveProjectRunsPanel({
- - ); - })} - + + {name} + + + {[ + isCurrent ? '当前' : null, + activeTurnStatusLabel(turn.status), + elapsed, + ] + .filter(Boolean) + .join(' · ')} + + + + ); + })} + + ) : null}
) : null} ); } + if (runningCount === 0) { + // 快照读到了、只是没有在跑回合:整块不渲染,不能报成读取失败。 + if (!readFailed) { + return null; + } + // 三次都没读到快照:只说"没读到",不改写成业务、权限或审批结论。 + return ( + + ); + } + return (