01e97d3a02
新增活动 Direct 回合只读快照与状态更新 接入重进项目的忙碌态恢复和运行中项目面板 补充生命周期规范、决策记录与定向测试
120 lines
3.9 KiB
TypeScript
120 lines
3.9 KiB
TypeScript
import type { GameCreatorDirectActiveTurn } from '../../app/types';
|
|
import { projectNameFromPath } from '../agent-runtime';
|
|
import { projectPathsMatchForInvalidation } from '../project-summary/projectPath';
|
|
|
|
/**
|
|
* 左上角的"正在运行的项目"面板。
|
|
*
|
|
* 数据来自 Rust 的活动回合注册表(同一个只读快照也用于重新进入项目时的进度重连),
|
|
* 面板只负责呈现:项目名、阶段、已运行时长,以及点击进入该项目。没有在跑回合时
|
|
* 整块不渲染,不留空白占位。
|
|
*/
|
|
export type ActiveProjectRunsPanelProps = {
|
|
activeTurns: GameCreatorDirectActiveTurn[];
|
|
currentProjectPath?: string | null;
|
|
readFailed?: boolean;
|
|
onOpenProject?: (projectPath: string) => void;
|
|
};
|
|
|
|
const ACTIVE_TURN_STATUS_LABELS: Record<string, string> = {
|
|
accepted: '已受理',
|
|
running: '创作中',
|
|
streaming: '生成中',
|
|
finalizing: '收尾中',
|
|
completed: '已完成',
|
|
failed: '已失败',
|
|
};
|
|
|
|
function activeTurnStatusLabel(status: string) {
|
|
return ACTIVE_TURN_STATUS_LABELS[status] ?? '创作中';
|
|
}
|
|
|
|
function formatActiveTurnElapsed(startedAt: number, now: number) {
|
|
const elapsedMs = now - startedAt;
|
|
if (!Number.isFinite(elapsedMs) || elapsedMs < 0) {
|
|
return '';
|
|
}
|
|
const totalMinutes = Math.floor(elapsedMs / 60_000);
|
|
if (totalMinutes < 1) {
|
|
return '不到 1 分钟';
|
|
}
|
|
if (totalMinutes < 60) {
|
|
return `${totalMinutes} 分钟`;
|
|
}
|
|
const hours = Math.floor(totalMinutes / 60);
|
|
const minutes = totalMinutes % 60;
|
|
return minutes === 0 ? `${hours} 小时` : `${hours} 小时 ${minutes} 分`;
|
|
}
|
|
|
|
function activeTurnDisplayName(turn: GameCreatorDirectActiveTurn) {
|
|
const snapshotName = turn.projectName?.trim();
|
|
return snapshotName || projectNameFromPath(turn.projectPath);
|
|
}
|
|
|
|
export function ActiveProjectRunsPanel({
|
|
activeTurns,
|
|
currentProjectPath = null,
|
|
readFailed = false,
|
|
onOpenProject,
|
|
}: ActiveProjectRunsPanelProps) {
|
|
if (activeTurns.length === 0) {
|
|
if (!readFailed) {
|
|
return null;
|
|
}
|
|
// 三次都没读到快照:只说"没读到",不改写成业务、权限或审批结论。
|
|
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,
|
|
);
|
|
return (
|
|
<aside className="launcher-runs-panel" aria-label="正在运行的项目">
|
|
<header className="launcher-runs-panel-header">
|
|
<span className="launcher-runs-panel-dot" aria-hidden="true" />
|
|
<strong>正在运行</strong>
|
|
</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(
|
|
turn.projectPath,
|
|
currentProjectPath,
|
|
),
|
|
);
|
|
return (
|
|
<li key={`${turn.projectPath}:${turn.turnId}`}>
|
|
<button
|
|
type="button"
|
|
className="launcher-runs-panel-item"
|
|
aria-current={isCurrent ? 'true' : undefined}
|
|
disabled={!onOpenProject}
|
|
onClick={() => onOpenProject?.(turn.projectPath)}
|
|
>
|
|
<span className="launcher-runs-panel-name" title={name}>
|
|
{name}
|
|
</span>
|
|
<span className="launcher-runs-panel-meta">
|
|
{[activeTurnStatusLabel(turn.status), elapsed]
|
|
.filter(Boolean)
|
|
.join(' · ')}
|
|
</span>
|
|
</button>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</aside>
|
|
);
|
|
}
|