修复:AGC 窗口标题栏项目标签改认当前项目(#618)
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m40s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m15s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m17s
Project CI / Frontend tests (pull_request) Successful in 2m51s
Project CI / Backend tests (pull_request) Successful in 7m38s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m58s
Project CI / Repository checks (pull_request) Successful in 6m18s
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m40s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m15s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m17s
Project CI / Frontend tests (pull_request) Successful in 2m51s
Project CI / Backend tests (pull_request) Successful in 7m38s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m58s
Project CI / Repository checks (pull_request) Successful in 6m18s
Project CI / Native shell tests (pull_request) Has been cancelled
- 窗口标题栏标签主文案恒为当前项目名:WindowChromeActiveProjectRuns 新增 currentProjectName,由 WorkspaceLauncher 发布当前项目清单名,WindowChrome 透传给标题栏入口 - 正在运行的项目降级为圆点、运行中数量徽标与展开菜单:ActiveProjectRunsPanel 主文案不再取开始时间最晚的在跑项目 - 展开菜单里当前项目那一行沿用清单项目名,meta 首位标「当前」,aria-current 保留 - 运行快照读取失败不再整块替换标签:圆点转告警色,结论落在菜单内一行;既无在跑项目又无当前项目名时保留原来的纯提示 - 新增 .launcher-runs-panel-dot.is-warning 与 .launcher-runs-titlebar-menu-note 样式 - 用例:directActiveTurns 新增三条回归(别的项目在跑时标签仍是当前项目、当前项目自己跑时列表带「当前」、读取失败不顶掉当前项目名),WindowChrome 新增当前项目优先级用例,并把「只显示最后开始的项目」用例改名为「不在项目内时…」 - 文档:修正主规范 2026-09-15 段的标题栏条目、里程碑与实施计划文末修订,decision-log 追加 2026-10-04 决策
This commit is contained in:
@@ -177,6 +177,7 @@ export function WindowChrome({ children }: WindowChromeProps) {
|
||||
<ActiveProjectRunsPanel
|
||||
activeTurns={activeProjectRuns.activeTurns}
|
||||
currentProjectPath={activeProjectRuns.currentProjectPath}
|
||||
currentProjectName={activeProjectRuns.currentProjectName}
|
||||
readFailed={activeProjectRuns.readFailed}
|
||||
onOpenProject={activeProjectRuns.onOpenProject}
|
||||
placement="titlebar"
|
||||
|
||||
@@ -18,6 +18,11 @@ export type WindowChromeContextValue = {
|
||||
export type WindowChromeActiveProjectRuns = {
|
||||
activeTurns: GameCreatorDirectActiveTurn[];
|
||||
currentProjectPath?: string | null;
|
||||
/**
|
||||
* 当前工作台打开的项目名(清单项目名)。标题栏标签的主文案认这一个:正在运行的项目只在
|
||||
* 徽标和展开菜单里表达,否则「我在哪个项目」会被后台别的项目顶掉。
|
||||
*/
|
||||
currentProjectName?: string | null;
|
||||
readFailed?: boolean;
|
||||
onOpenProject?: (projectPath: string) => void;
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<span
|
||||
className="launcher-runs-titlebar launcher-runs-titlebar--error"
|
||||
@@ -101,26 +120,14 @@ export function ActiveProjectRunsPanel({
|
||||
</span>
|
||||
);
|
||||
}
|
||||
// 三次都没读到快照:只说"没读到",不改写成业务、权限或审批结论。
|
||||
return (
|
||||
<aside className="launcher-runs-panel" aria-label="正在运行的项目">
|
||||
<span className="launcher-runs-panel-note" role="status">
|
||||
未能读取正在运行的项目
|
||||
</span>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
const runningLabel = readFailed
|
||||
? '正在运行的项目读取失败'
|
||||
: `正在运行 ${runningCount} 个项目`;
|
||||
const triggerLabel = `${
|
||||
currentName
|
||||
? `当前项目:${primaryName}`
|
||||
: `正在运行的项目:${primaryName}`
|
||||
};${runningLabel}`;
|
||||
const openProject = (projectPath: string) => {
|
||||
setOpen(false);
|
||||
onOpenProject?.(projectPath);
|
||||
@@ -129,24 +136,26 @@ export function ActiveProjectRunsPanel({
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="launcher-runs-titlebar"
|
||||
data-active-project-count={orderedTurns.length}
|
||||
data-active-project-count={runningCount}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="launcher-runs-titlebar-trigger"
|
||||
aria-label={`正在运行的项目:${latestName}`}
|
||||
aria-label={triggerLabel}
|
||||
title={triggerLabel}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="menu"
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
>
|
||||
<span className="launcher-runs-panel-dot" aria-hidden="true" />
|
||||
<span className="launcher-runs-titlebar-name" title={latestName}>
|
||||
{latestName}
|
||||
<span
|
||||
className={`launcher-runs-panel-dot${readFailed ? ' is-warning' : ''}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="launcher-runs-titlebar-name" title={primaryName}>
|
||||
{primaryName}
|
||||
</span>
|
||||
{orderedTurns.length > 1 ? (
|
||||
<span className="launcher-runs-titlebar-count">
|
||||
{orderedTurns.length}
|
||||
</span>
|
||||
{runningCount > 0 ? (
|
||||
<span className="launcher-runs-titlebar-count">{runningCount}</span>
|
||||
) : null}
|
||||
<ChevronDown
|
||||
className={`launcher-runs-titlebar-chevron${open ? ' is-open' : ''}`}
|
||||
@@ -158,51 +167,78 @@ export function ActiveProjectRunsPanel({
|
||||
<div className="launcher-runs-titlebar-menu" role="menu">
|
||||
<div className="launcher-runs-titlebar-menu-header">
|
||||
<strong>正在运行的项目</strong>
|
||||
<span>{orderedTurns.length} 个</span>
|
||||
<span>{runningLabel}</span>
|
||||
</div>
|
||||
<ul className="launcher-runs-titlebar-list">
|
||||
{orderedTurns.map((turn) => {
|
||||
const name = activeTurnDisplayName(turn);
|
||||
const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
|
||||
const isCurrent = Boolean(
|
||||
currentProjectPath &&
|
||||
projectPathsMatchForInvalidation(
|
||||
turn.projectPath,
|
||||
currentProjectPath,
|
||||
),
|
||||
);
|
||||
return (
|
||||
<li key={`${turn.projectPath}:${turn.turnId}`}>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className="launcher-runs-titlebar-item"
|
||||
aria-current={isCurrent ? 'true' : undefined}
|
||||
disabled={!onOpenProject}
|
||||
onClick={() => openProject(turn.projectPath)}
|
||||
>
|
||||
<span
|
||||
className="launcher-runs-titlebar-item-name"
|
||||
title={name}
|
||||
{readFailed ? (
|
||||
<p className="launcher-runs-titlebar-menu-note" role="status">
|
||||
正在运行的项目读取失败
|
||||
</p>
|
||||
) : null}
|
||||
{runningCount > 0 ? (
|
||||
<ul className="launcher-runs-titlebar-list">
|
||||
{orderedTurns.map((turn) => {
|
||||
const isCurrent = Boolean(
|
||||
currentProjectPath &&
|
||||
projectPathsMatchForInvalidation(
|
||||
turn.projectPath,
|
||||
currentProjectPath,
|
||||
),
|
||||
);
|
||||
// 当前项目这一行与标签主文案同名:同一个项目在不同的运行状态下
|
||||
// 显示两个名字,用户没法把"标签上那个"和"列表里这个"对上。
|
||||
const name =
|
||||
isCurrent && currentName
|
||||
? currentName
|
||||
: activeTurnDisplayName(turn);
|
||||
const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
|
||||
return (
|
||||
<li key={`${turn.projectPath}:${turn.turnId}`}>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className="launcher-runs-titlebar-item"
|
||||
aria-current={isCurrent ? 'true' : undefined}
|
||||
disabled={!onOpenProject}
|
||||
onClick={() => openProject(turn.projectPath)}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
<span className="launcher-runs-titlebar-item-meta">
|
||||
{[activeTurnStatusLabel(turn.status), elapsed]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<span
|
||||
className="launcher-runs-titlebar-item-name"
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
<span className="launcher-runs-titlebar-item-meta">
|
||||
{[
|
||||
isCurrent ? '当前' : null,
|
||||
activeTurnStatusLabel(turn.status),
|
||||
elapsed,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (runningCount === 0) {
|
||||
// 三次都没读到快照:只说"没读到",不改写成业务、权限或审批结论。
|
||||
return (
|
||||
<aside className="launcher-runs-panel" aria-label="正在运行的项目">
|
||||
<span className="launcher-runs-panel-note" role="status">
|
||||
未能读取正在运行的项目
|
||||
</span>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="launcher-runs-panel" aria-label="正在运行的项目">
|
||||
<header className="launcher-runs-panel-header">
|
||||
@@ -211,8 +247,6 @@ export function ActiveProjectRunsPanel({
|
||||
</header>
|
||||
<ul className="launcher-runs-panel-list">
|
||||
{orderedTurns.map((turn) => {
|
||||
const name = activeTurnDisplayName(turn);
|
||||
const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
|
||||
const isCurrent = Boolean(
|
||||
currentProjectPath &&
|
||||
projectPathsMatchForInvalidation(
|
||||
@@ -220,6 +254,11 @@ export function ActiveProjectRunsPanel({
|
||||
currentProjectPath,
|
||||
),
|
||||
);
|
||||
const name =
|
||||
isCurrent && currentName
|
||||
? currentName
|
||||
: activeTurnDisplayName(turn);
|
||||
const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
|
||||
return (
|
||||
<li key={`${turn.projectPath}:${turn.turnId}`}>
|
||||
<button
|
||||
@@ -233,7 +272,11 @@ export function ActiveProjectRunsPanel({
|
||||
{name}
|
||||
</span>
|
||||
<span className="launcher-runs-panel-meta">
|
||||
{[activeTurnStatusLabel(turn.status), elapsed]
|
||||
{[
|
||||
isCurrent ? '当前' : null,
|
||||
activeTurnStatusLabel(turn.status),
|
||||
elapsed,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</span>
|
||||
|
||||
@@ -303,12 +303,14 @@ export function WorkspaceLauncherShell({
|
||||
setActiveProjectRuns({
|
||||
activeTurns,
|
||||
currentProjectPath: currentProjectContext?.projectPath ?? null,
|
||||
currentProjectName: currentProjectContext?.projectName ?? null,
|
||||
readFailed: snapshotReadFailed,
|
||||
onOpenProject: (projectPath: string) =>
|
||||
openActiveProjectRef.current(projectPath),
|
||||
});
|
||||
}, [
|
||||
activeTurns,
|
||||
currentProjectContext?.projectName,
|
||||
currentProjectContext?.projectPath,
|
||||
setActiveProjectRuns,
|
||||
snapshotReadFailed,
|
||||
|
||||
@@ -507,6 +507,16 @@ body {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/*
|
||||
* 读取失败时菜单里的那一行结论:只说明"没读到",不顶掉标签上的当前项目名,
|
||||
* 也不改写成业务、权限或审批失败。
|
||||
*/
|
||||
.launcher-runs-titlebar-menu-note {
|
||||
margin: 8px 5px 2px;
|
||||
color: var(--platform-text-muted, #a38f80);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.launcher-runs-titlebar-list {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
@@ -1116,6 +1126,12 @@ textarea {
|
||||
box-shadow: 0 0 0 3px rgb(77 159 84 / 14%);
|
||||
}
|
||||
|
||||
/* 快照读取失败:圆点改成告警色,文字结论留在展开菜单里,不占用标题栏主文案。 */
|
||||
.launcher-runs-panel-dot.is-warning {
|
||||
background: #c7653d;
|
||||
box-shadow: 0 0 0 3px rgb(199 101 61 / 14%);
|
||||
}
|
||||
|
||||
.launcher-runs-panel-list {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
|
||||
@@ -33,15 +33,24 @@ function TitleSetter({ value }: { value: string }) {
|
||||
|
||||
function ActiveRunsSetter({
|
||||
activeTurns,
|
||||
currentProjectName,
|
||||
currentProjectPath,
|
||||
}: {
|
||||
activeTurns: GameCreatorDirectActiveTurn[];
|
||||
currentProjectName?: string;
|
||||
currentProjectPath?: string;
|
||||
}) {
|
||||
const { setActiveProjectRuns } = useWindowChrome();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setActiveProjectRuns({ activeTurns, onOpenProject: () => undefined })
|
||||
setActiveProjectRuns({
|
||||
activeTurns,
|
||||
...(currentProjectName ? { currentProjectName } : {}),
|
||||
...(currentProjectPath ? { currentProjectPath } : {}),
|
||||
onOpenProject: () => undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
显示运行项目
|
||||
@@ -164,6 +173,43 @@ describe('WindowChrome', () => {
|
||||
expect(screen.getAllByRole('menuitem')).toHaveLength(2);
|
||||
});
|
||||
|
||||
/**
|
||||
* 回归:进项目后当前项目没在跑、后台别的项目在跑时,标题栏曾经整块换成
|
||||
* 「正在运行的项目」,当前项目名在窗口上消失。
|
||||
*/
|
||||
it('keeps the current project in the title bar while another project runs', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<WindowChrome>
|
||||
<ActiveRunsSetter
|
||||
activeTurns={[
|
||||
{
|
||||
projectPath: 'C:/projects/other',
|
||||
projectName: '别的项目',
|
||||
turnId: 'turn-other',
|
||||
startedAt: 100,
|
||||
status: 'running',
|
||||
updatedAt: 120,
|
||||
sequence: 1,
|
||||
},
|
||||
]}
|
||||
currentProjectName="飞鸟 v2"
|
||||
currentProjectPath="C:/projects/bird"
|
||||
/>
|
||||
</WindowChrome>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '显示运行项目' }));
|
||||
|
||||
expect(screen.getByText('飞鸟 v2')).toBeTruthy();
|
||||
expect(screen.queryByText('别的项目')).toBeNull();
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: '当前项目:飞鸟 v2;正在运行 1 个项目',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
/**
|
||||
* 回归:发布面板等 ThemedModal 弹窗打开时,标题栏在模态之外,焦点陷阱曾把
|
||||
* 标题栏上的点击一起拦下 —— 三个窗口按钮看着正常但点不动。
|
||||
|
||||
@@ -308,7 +308,7 @@ describe('ActiveProjectRunsPanel', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('标题栏入口只显示最后开始的项目,展开后列出全部项目', () => {
|
||||
it('不在项目内时标题栏入口只显示最后开始的项目,展开后列出全部项目', () => {
|
||||
const onOpenProject = vi.fn();
|
||||
render(
|
||||
<ActiveProjectRunsPanel
|
||||
@@ -345,4 +345,92 @@ describe('ActiveProjectRunsPanel', () => {
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /先开始/ }));
|
||||
expect(onOpenProject).toHaveBeenCalledWith('C:/projects/first');
|
||||
});
|
||||
|
||||
/**
|
||||
* 回归:进入的项目没在跑、后台别的项目在跑时,旧实现在标题栏显示的是
|
||||
* 最后开始的那个项目(当前项目名整个消失),用户会以为自己开错了项目。
|
||||
*/
|
||||
it('进入项目后标签显示当前项目名,后台在跑的项目只进徽标与展开菜单', () => {
|
||||
render(
|
||||
<ActiveProjectRunsPanel
|
||||
placement="titlebar"
|
||||
activeTurns={[
|
||||
{
|
||||
projectPath: 'C:/projects/other',
|
||||
projectName: 'gameagent-other',
|
||||
turnId: 'turn-other',
|
||||
startedAt: 100,
|
||||
status: 'running',
|
||||
updatedAt: 120,
|
||||
sequence: 1,
|
||||
},
|
||||
]}
|
||||
currentProjectName="飞鸟 v2"
|
||||
currentProjectPath="C:/projects/bird"
|
||||
onOpenProject={() => undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
const trigger = screen.getByRole('button', {
|
||||
name: '当前项目:飞鸟 v2;正在运行 1 个项目',
|
||||
});
|
||||
expect(trigger.textContent).toContain('飞鸟 v2');
|
||||
expect(trigger.textContent).toContain('1');
|
||||
expect(trigger.textContent).not.toContain('gameagent-other');
|
||||
|
||||
fireEvent.click(trigger);
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: /gameagent-other/ }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('当前项目自己的回合在跑时列表标「当前」并沿用项目名', () => {
|
||||
render(
|
||||
<ActiveProjectRunsPanel
|
||||
placement="titlebar"
|
||||
activeTurns={[
|
||||
{
|
||||
projectPath: 'C:/projects/bird',
|
||||
projectName: 'gameagent-bird',
|
||||
turnId: 'turn-bird',
|
||||
startedAt: 100,
|
||||
status: 'streaming',
|
||||
updatedAt: 120,
|
||||
sequence: 1,
|
||||
},
|
||||
]}
|
||||
currentProjectName="飞鸟 v2"
|
||||
currentProjectPath="C:/projects/bird"
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /当前项目:飞鸟 v2/ }));
|
||||
const item = screen.getByRole('menuitem');
|
||||
expect(item.textContent).toContain('飞鸟 v2');
|
||||
expect(item.textContent).not.toContain('gameagent-bird');
|
||||
expect(item.textContent).toContain('当前');
|
||||
expect(item.getAttribute('aria-current')).toBe('true');
|
||||
});
|
||||
|
||||
it('读取失败时不顶掉当前项目名,读取结论留在展开菜单里', () => {
|
||||
render(
|
||||
<ActiveProjectRunsPanel
|
||||
placement="titlebar"
|
||||
activeTurns={[]}
|
||||
readFailed
|
||||
currentProjectName="飞鸟 v2"
|
||||
/>,
|
||||
);
|
||||
|
||||
const trigger = screen.getByRole('button', {
|
||||
name: /当前项目:飞鸟 v2;正在运行的项目读取失败/,
|
||||
});
|
||||
expect(trigger.textContent).toContain('飞鸟 v2');
|
||||
|
||||
fireEvent.click(trigger);
|
||||
expect(screen.getByRole('menu')).toBeTruthy();
|
||||
expect(screen.getByRole('status').textContent).toBe(
|
||||
'正在运行的项目读取失败',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ Milestone: `【里程碑】Direct回合跨页面生命周期与运行中项目
|
||||
|
||||
1. Rust:扩展活动回合注册表并暴露只读快照命令,配定向用例(进入 / 进度 / 终态移除 / 多项目并存)。
|
||||
2. 前端:接入快照读取,实现“重新进入项目 → 恢复忙碌态与进度 → 以快照 sequence 续接 → 阻止并发提交”。
|
||||
3. 前端:在窗口标题栏挂载“正在运行的项目”下拉入口;标题栏只显示最后开始的项目,展开后按开始时间列出全部项目,复用既有组件与设计 token。
|
||||
3. 前端:在窗口标题栏挂载项目入口;2026-10-04 修订为**主文案恒为当前项目名**,运行中项目只占圆点、数量徽标与展开菜单(展开后按开始时间列出全部在跑项目,当前项目带「当前」标记),复用既有组件与设计 token。原口径「标题栏只显示最后开始的项目」见里程碑文末修订记录。
|
||||
4. 报错归类:按审计结论修正会误导的映射,逐条加回归用例;真实权限拒绝保持原提示。
|
||||
5. 文档:主规范与共享记忆同步;里程碑验收后删除临时计划文件。
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Parent Spec: `docs/technical/【技术方案】AI游戏创作智能体App实施
|
||||
|
||||
## 目标
|
||||
|
||||
离开项目界面不再等于“回合消失”:后台继续跑的 Direct 回合必须能被前端重新发现并续接进度,同一项目在回合结束前不允许再发起第二条付费回合;壳层窗口标题栏提供“正在运行的项目”下拉入口,常态显示最后开始的项目,展开后列出当前确有在跑回合的全部项目并可点击进入。
|
||||
离开项目界面不再等于“回合消失”:后台继续跑的 Direct 回合必须能被前端重新发现并续接进度,同一项目在回合结束前不允许再发起第二条付费回合;壳层窗口标题栏提供项目入口,常态显示**当前打开的项目名**,展开后列出当前确有在跑回合的全部项目并可点击进入(2026-10-04 修订,见文末)。
|
||||
|
||||
## 边界
|
||||
|
||||
@@ -22,7 +22,7 @@ Parent Spec: `docs/technical/【技术方案】AI游戏创作智能体App实施
|
||||
- 重新进入有在跑回合的项目后:界面进入“正在处理”、显示最近一次进度、以快照 `sequence` 续接后续事件;回合结束前提交第二条需求不会真正发起第二条付费回合。
|
||||
- 回合结束(completed / failed)后:忙碌态解除、可以再次发送;不重复追加助手消息。
|
||||
- 无在跑回合的项目:行为与今天一致(可正常发送,不出现额外提示或阻塞)。
|
||||
- 窗口标题栏入口:常态只显示最后开始的项目,点击后按 `startedAt` 升序列出所有在跑项目,显示项目名(缺失时回退目录名)与状态/时长,点击进入对应项目;没有在跑回合时不渲染入口。
|
||||
- 窗口标题栏入口:常态显示当前项目名(不在项目内时回落到最后开始的在跑项目);正在运行的项目只占圆点与数量徽标,点击后按 `startedAt` 升序列出所有在跑项目,显示项目名(缺失时回退目录名)与状态/时长、当前项目带「当前」标记,点击进入对应项目;没有在跑回合且没有读取失败时不渲染入口;读取失败不顶掉当前项目名,结论在展开菜单里。
|
||||
- 快照读取失败不得阻断发送、不得显示成业务失败。
|
||||
- 已修的错误映射不回归:`direct-codex-turn-already-running:` 与历史同义中文正文都归一到“仍在处理这个项目的上一条需求”;真正的 `项目权限策略拒绝执行:<command>` 仍显示审批提示。
|
||||
|
||||
@@ -41,3 +41,12 @@ Parent Spec: `docs/technical/【技术方案】AI游戏创作智能体App实施
|
||||
- 仍未取得证据(需要真实运行):真机重新进入正在跑的项目后恢复忙碌态与进度、标题栏入口点击进入对应项目、回合结束后可再次发送。
|
||||
|
||||
因此本计划从 `in-progress` 改为 `implemented-awaiting-runtime-acceptance`:代码与定向测试已落地,缺的是真机运行时证据。
|
||||
|
||||
## 2026-10-04 修订:标题栏标签认「当前项目」,运行中项目降级为徽标
|
||||
|
||||
- 背景:issue #618。窗口标题栏那枚标签原来被「正在运行的项目」整块接管(`WindowChrome` 在 `activeTurns.length > 0 || readFailed` 时替换掉当前工作区标题),主文案取开始时间最晚的在跑项目。于是「当前项目没在跑、后台别的项目在跑」时标签显示的是别的项目,当前项目名在整个窗口上消失(现场:当前项目 `gameagent-ff7f3240`,标签显示 `gameagent-c3af9c7e`)。
|
||||
- 决策(主文案优先级):标签主文案恒为当前项目名(`currentProjectContext.projectName`,经 `WindowChromeActiveProjectRuns.currentProjectName` 由 `WorkspaceLauncher` 发布);不在项目内时才回落到最后开始的在跑项目。这与主规范既有条目「打开项目后居中标题切换为当前项目名」一致,运行中项目不再和它抢位置。
|
||||
- 决策(运行态表达):圆点(读取失败转告警色)+ 运行中数量徽标(≥1 即显示)+ 展开菜单;菜单里当前项目那一行用与标签同名的项目名并带「当前」标记(`aria-current` 保留)。
|
||||
- 决策(读取失败):不再整块替换标签,结论只留在圆点与菜单里;既没有在跑项目、又没有当前项目可显示时才保留原来的纯提示。
|
||||
- 有意未做:后台项目在菜单里仍是目录名。运行中项目名来自快照 `projectName`(Rust 侧取 `thread_id` 目录名),要改成清单项目名就得让 `ThreadManager` 在认领回合时读 `.agent/manifest.json`,给纯内存模块的 claim 路径加锁内文件 I/O;当前项目那一行已用清单项目名对齐。
|
||||
- 验证:`npx vitest run apps/ai-game-creator-shell/tests/directActiveTurns.test.tsx apps/ai-game-creator-shell/tests/WindowChrome.test.tsx apps/ai-game-creator-shell/tests/workspaceWindowSync.test.tsx` → 22 passed(含三条新回归:别的项目在跑时标签仍是当前项目、当前项目自己跑时列表带「当前」、读取失败不顶掉当前项目名);全量 AGC Web 用例与真实浏览器冒烟见 PR。
|
||||
|
||||
@@ -9580,3 +9580,13 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 验证:`npm run agc:bundled-resources:test`(18 passed,含 sidecar 归位、跳过规则、上游缺失、版本漂移、目录被占);`npm run agc:bundled-resources:check`、`check-config.mjs`、`cargo test --bin genarrative-ai-game-creator-shell package_layout::tests`、`cargo check --no-default-features`、`cargo fmt --check`、`check:encoding`、eslint/prettier 全部通过;Windows 真机准备步骤 staging 24 个文件(含 243MB `claude.exe`)后 `cargo check` 不再出现构建期写入。
|
||||
- 边界(未验证):macOS 真机的 sidecar 加载与 `check-macos-bundle.mjs` 包内容门禁未在本机验证;Linux 门禁按新配置不再要求 sidecar 资源,需 CI 实跑确认转绿。
|
||||
- 关联:issue #519、master `fb130d184`、`docs/technical/【技术方案】AGC随包资源staging归位-2026-09-26.md`、CI run 3083。
|
||||
|
||||
## 2026-10-04 AGC 窗口标题栏项目标签改认「当前项目」,运行中项目降级为圆点与徽标
|
||||
|
||||
- 背景:issue #618。窗口标题栏那枚标签原来被「正在运行的项目」整块接管——`WindowChrome` 只要存在活动回合或运行快照读取失败,就把当前工作区标题换成 `ActiveProjectRunsPanel`(titlebar 形态),主文案取开始时间最晚的在跑项目。于是「当前项目没在跑、后台别的项目在跑」时,标签显示的是别的项目(现场:当前项目 `gameagent-ff7f3240`,标签 `gameagent-c3af9c7e`),用户以为自己开错了项目;读取失败时标签还会变成「正在运行的项目读取失败」,同样顶掉当前项目名。
|
||||
- 决策(主文案):标签主文案恒为当前项目名,与当前项目是否在跑回合无关;不在项目内(首页 / 项目组 / 模板库)才回落到最后开始的在跑项目。名字由 `WorkspaceLauncher` 经 `WindowChromeActiveProjectRuns.currentProjectName` 发布(`currentProjectContext.projectName`,即清单项目名)。这同时消掉了同一项目「有回合在跑显示目录名、没在跑显示项目名」的两种口径。
|
||||
- 决策(运行态):圆点 + 运行中数量徽标(≥1 即显示)+ 展开菜单;菜单里当前项目那一行用与标签同名的项目名,meta 首位带「当前」标记,`aria-current` 保留。读取失败改成告警色圆点 + 菜单内一行结论;只有既没有在跑项目、又没有当前项目可显示时才保留原来的纯提示。
|
||||
- 有意未做:后台项目在菜单里仍是目录名(快照 `projectName` 由 Rust 取 `thread_id` 目录名)。改成清单项目名要在 `ThreadManager` 的 claim 路径里读 `.agent/manifest.json`,等于给纯内存模块加锁内文件 I/O,本轮不做。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src/components/WindowChrome.tsx`、`src/components/windowChromeContext.ts`、`src/features/app-shell/ActiveProjectRunsPanel.tsx`、`src/features/app-shell/WorkspaceLauncher.tsx`、`src/styles.css` 与两个定向用例文件;同步修正主规范 2026-09-15 段的标题栏条目、里程碑与实施计划文末修订。不改 Rust、快照命令字段、公共 API 与持久化。
|
||||
- 验证方式:`npx vitest run apps/ai-game-creator-shell/tests/directActiveTurns.test.tsx apps/ai-game-creator-shell/tests/WindowChrome.test.tsx apps/ai-game-creator-shell/tests/workspaceWindowSync.test.tsx` → 22 passed(三条新回归:别的项目在跑时标签仍是当前项目、当前项目自己跑时列表带「当前」、读取失败不顶掉当前项目名);全量 `npm run test -- apps/ai-game-creator-shell/tests` 与真实 Chromium 冒烟见 PR。
|
||||
- 关联:issue #618、`docs/project-memory/plans/【里程碑】Direct回合跨页面生命周期与运行中项目可见性-2026-09-15.md`(2026-10-04 修订)。
|
||||
|
||||
@@ -1715,7 +1715,7 @@ DirectProject、Agent Runtime、Provider、app-server、内置 MCP、命令执
|
||||
|
||||
Direct 回合的所有权属于进程内项目身份锁,不属于当前页面。离开工作台或切换到首页时,正在运行的回合继续执行;重新进入项目时,只读活动回合快照负责恢复 clientTurnId 与运行状态,Direct 回合事件负责更新进度。Thread Manager 的 Provider turnId 与事件序列不得当成 clientTurnId 或展示事件序列;其通知只触发快照和历史同步,回放的终态不能重新创建活动回合或“正在提交回复”。定期复核同一份活动快照以弥补页面切换时丢失的结束事件;失败读取保留已知状态,过期读取不能覆盖新回合。活动回合结束后移除快照并解除发送阻断;没有活动回合的项目保持原有发送行为。
|
||||
|
||||
壳层窗口标题栏的“正在运行”下拉入口只呈现活动 Direct 回合快照:常态只显示最后开始的项目,展开后按开始时间排序,显示全部项目的项目名、状态、活动时长并允许进入对应项目。快照读取失败只显示读取失败并保留上一份结果,不得改写成权限、审批或业务失败;入口不建立第二份运行真相。应用重启后的恢复、取消入口和非 Direct Agent 项目不在本合同内。
|
||||
壳层窗口标题栏的项目入口只呈现活动 Direct 回合快照:常态显示当前打开的项目名(不在项目内时回落到最后开始的在跑项目),正在运行的项目只占圆点、数量徽标与展开菜单;展开后按开始时间排序,显示全部在跑项目的项目名、状态、活动时长、当前项目标记并允许进入对应项目。快照读取失败保留上一份结果、不顶掉当前项目名,读取结论只在展开菜单里给出,不得改写成权限、审批或业务失败;入口不建立第二份运行真相。应用重启后的恢复、取消入口和非 Direct Agent 项目不在本合同内。
|
||||
|
||||
活动回合快照命令是进程内 Tauri 只读命令,不进入公共 API 或持久化协议;字段包含 `projectPath / projectName / turnId / status / activity / startedAt / updatedAt / sequence`,状态和序号与既有 Direct 回合进度事件一致。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user