From a8a4b053feb27393e656111928db54b3cb9d96a8 Mon Sep 17 00:00:00 2001
From: Suzumiya
Date: Wed, 7 Oct 2026 01:08:38 +0800
Subject: [PATCH] =?UTF-8?q?fix(agc):=20=E8=BF=90=E8=A1=8C=E4=B8=8D?=
=?UTF-8?q?=E5=8F=AF=E7=94=A8=E6=8F=90=E7=A4=BA=E6=8C=89=E6=B8=85=E5=8D=95?=
=?UTF-8?q?=E4=BA=8B=E5=AE=9E=E5=88=86=E5=8F=89=E6=96=87=E6=A1=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/view/project-development/index.tsx | 3 +-
.../project-development/runUnavailableHint.ts | 27 ++++
.../appSurface/project-development.suite.ts | 128 ++++++++++++++++++
.../tests/runUnavailableHint.test.ts | 45 ++++++
4 files changed, 202 insertions(+), 1 deletion(-)
create mode 100644 apps/ai-game-creator-shell/src/view/project-development/runUnavailableHint.ts
create mode 100644 apps/ai-game-creator-shell/tests/runUnavailableHint.test.ts
diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx
index 255249275..029bb5324 100644
--- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx
+++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx
@@ -443,6 +443,7 @@ import {
projectResourceSectionZoomFromWheel,
} from './resourceSectionHeightModel';
import { ResourceTypePanel } from './ResourceTypePanel';
+import { runUnavailableHintText } from './runUnavailableHint';
import {
describeProjectResourceCanvasLayoutRead,
type ProjectResourceCanvasLayoutReadReport,
@@ -10086,7 +10087,7 @@ export default function ProjectDevelopmentView({
className="game-run-unavailable"
role="status"
>
- 首个可运行原型尚未完成,运行视图暂不可用
+ {runUnavailableHintText(manifest)}
) : null}
diff --git a/apps/ai-game-creator-shell/src/view/project-development/runUnavailableHint.ts b/apps/ai-game-creator-shell/src/view/project-development/runUnavailableHint.ts
new file mode 100644
index 000000000..288b4af79
--- /dev/null
+++ b/apps/ai-game-creator-shell/src/view/project-development/runUnavailableHint.ts
@@ -0,0 +1,27 @@
+import type { GameCreationAppManifest } from '../../../../../packages/shared/src/contracts/gameCreationApp';
+
+/**
+ * 「运行」页签不可用(`run-unavailable-hint`)时给用户看的文案。
+ *
+ * 同一句话对两种情况并不都成立,所以按清单里已有的事实分叉,而不是套一句万能的空话:
+ *
+ * - **全新项目**(清单里还没有任何工程内部版本):确实什么都还没产出,
+ * 「首个可运行原型尚未完成,运行视图暂不可用」是准确的,保持原样。
+ * - **已经有工程内容、但本机还没有可运行的构建产物**:典型是**从平台作品 Fork 来的工程源包
+ * 项目**——包里就是作者的完整工程,但工程源包按规则不含构建产物(`project_bundle.rs` 排除
+ * `game/dist`),AGC 也不在 Fork 时自动构建(与「不自动安装外部工程依赖」的既有策略一致)。
+ * 这时原句会让人以为「什么都没有」,必须如实说清:内容已经在本地,只是还没有构建出可运行的
+ * 原型,让智能体完成可运行原型之后即可运行。
+ *
+ * 判据只用清单里已有的字段(`versions` 非空 = 这个项目已经有工程内部版本记录)。渲染层拿不到
+ * 「预览根有没有 index.html」这种文件事实(那是原生侧的门禁判据),所以这里不新造状态、也不
+ * 额外调 IPC——它只决定**措辞**,能不能运行仍由运行门禁与原生预览决定。
+ */
+export function runUnavailableHintText(
+ manifest: Pick,
+): string {
+ const hasProjectVersions = (manifest.versions?.length ?? 0) > 0;
+ return hasProjectVersions
+ ? '已有工程内容但还没有可运行的构建产物:让智能体完成可运行原型后即可运行'
+ : '首个可运行原型尚未完成,运行视图暂不可用';
+}
diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts
index 8004a2abf..41f047b48 100644
--- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts
+++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts
@@ -531,6 +531,134 @@ export function registerProjectWorkbenchFoundationTests() {
expect(screen.getByRole('article', { name: /发布 Agent/ })).not.toBeNull();
});
+ /**
+ * Fork 建项之后运行视图必须直接可用。
+ *
+ * 原生侧(`game_fork.rs` 的 `register_forked_project_state_at`)在建项收口时登记「可运行
+ * 原型」:成品包形态把发行产物铺进预览根并置 `code-prototype = completed`,同时写一条
+ * `initial-1` 工程内部版本。这里钉的是渲染层的投影结果——那句「首个可运行原型尚未完成」
+ * 必须消失,运行页签可点并真的发出播放请求。
+ */
+ it('renders a forked package project as directly runnable with an initial version', () => {
+ const manifest = createGameCreationAppManifest(
+ 'workbench-fork-package',
+ 'Fork 项目(成品包)',
+ );
+ const codePrototype = manifest.tasks.find(
+ (task) => task.id === 'code-prototype',
+ );
+ if (!codePrototype) {
+ throw new Error('fixture 必须包含 code-prototype seed 任务');
+ }
+ codePrototype.status = 'completed';
+ manifest.versions = [
+ {
+ versionId: 'initial-1',
+ parentVersionId: null,
+ projectRevision: 1,
+ resourceBindings: [],
+ createdReason: 'initial',
+ createdAt: 1_791_271_678,
+ },
+ ];
+
+ const onPlay = vi.fn();
+ render(
+ React.createElement(ProjectDevelopmentView, {
+ projectName: 'Fork 项目(成品包)',
+ projectPath: '/tmp/workbench-fork-package',
+ manifest,
+ attachments: [],
+ recentRunStatus: null,
+ recentRunStopReason: null,
+ chat: React.createElement(
+ 'div',
+ { 'aria-label': '测试项目总控' },
+ '项目总控对话内容',
+ ),
+ onHomeOpen: vi.fn(),
+ onProjectsOpen: vi.fn(),
+ onPlay,
+ }),
+ );
+
+ // 提示必须消失,页签必须不再是「不可用」态。
+ expect(
+ screen.queryByText('首个可运行原型尚未完成,运行视图暂不可用'),
+ ).toBeNull();
+ expect(document.querySelector('#run-unavailable-hint')).toBeNull();
+ const runTab = screen.getByRole('tab', {
+ name: '运行',
+ }) as HTMLButtonElement;
+ expect(runTab.getAttribute('data-unavailable')).toBeNull();
+ expect(runTab.getAttribute('aria-describedby')).toBeNull();
+
+ // 点运行 = 切视图 + 发起播放请求(就是 fork 之后用户看到的「直接能跑」)。
+ fireEvent.click(runTab);
+ expect(runTab.getAttribute('aria-selected')).toBe('true');
+ expect(onPlay).toHaveBeenCalledTimes(1);
+ });
+
+ /**
+ * 工程源包 Fork 的如实表述:包里是作者的完整工程,但工程源包不含构建产物
+ * (`project_bundle.rs` 排除 `game/dist`),AGC 也不在 Fork 时自动构建,所以本机还没有
+ * 可运行的构建产物。此时不能再说「首个可运行原型尚未完成」(听着像什么都没有),
+ * 也不能谎称已经可运行。
+ */
+ it('tells the truth for a source-only fork that still needs a build', () => {
+ const manifest = createGameCreationAppManifest(
+ 'workbench-fork-source',
+ 'Fork 项目(工程源包)',
+ );
+ manifest.versions = [
+ {
+ versionId: 'initial-1',
+ parentVersionId: null,
+ projectRevision: 1,
+ resourceBindings: [],
+ createdReason: 'initial',
+ createdAt: 1_791_271_678,
+ },
+ ];
+
+ const onPlay = vi.fn();
+ render(
+ React.createElement(ProjectDevelopmentView, {
+ projectName: 'Fork 项目(工程源包)',
+ projectPath: '/tmp/workbench-fork-source',
+ manifest,
+ attachments: [],
+ recentRunStatus: null,
+ recentRunStopReason: null,
+ chat: React.createElement(
+ 'div',
+ { 'aria-label': '测试项目总控' },
+ '项目总控对话内容',
+ ),
+ onHomeOpen: vi.fn(),
+ onProjectsOpen: vi.fn(),
+ onPlay,
+ }),
+ );
+
+ expect(
+ screen.getByText(
+ '已有工程内容但还没有可运行的构建产物:让智能体完成可运行原型后即可运行',
+ ),
+ ).not.toBeNull();
+ expect(
+ screen.queryByText('首个可运行原型尚未完成,运行视图暂不可用'),
+ ).toBeNull();
+
+ const runTab = screen.getByRole('tab', {
+ name: '运行',
+ }) as HTMLButtonElement;
+ expect(runTab.getAttribute('data-unavailable')).toBe('true');
+ fireEvent.click(runTab);
+ expect(runTab.getAttribute('aria-selected')).toBe('false');
+ expect(onPlay).not.toHaveBeenCalled();
+ });
+
it('keeps the approval-mode choices reachable in their own dialog surface', () => {
// 审批模式原来是长在会话列头部按钮里的对话框;面板顶部精简之后它由设置浮层里的
// 「操作权限」行打开,但选项与「不可用项给出说明」的语义必须完整保留。
diff --git a/apps/ai-game-creator-shell/tests/runUnavailableHint.test.ts b/apps/ai-game-creator-shell/tests/runUnavailableHint.test.ts
new file mode 100644
index 000000000..bd3d8714e
--- /dev/null
+++ b/apps/ai-game-creator-shell/tests/runUnavailableHint.test.ts
@@ -0,0 +1,45 @@
+import { describe, expect, it } from 'vitest';
+
+import { runUnavailableHintText } from '../src/view/project-development/runUnavailableHint';
+
+/**
+ * 「运行」页签不可用时的文案(`run-unavailable-hint`)。
+ *
+ * 为什么要有这条用例:同一句「首个可运行原型尚未完成」对**已有工程内容、只是本机还没构建出
+ * 可运行产物**的情况(典型是从平台作品 Fork 来的工程源包项目)是误导——内容明明已经在本地。
+ * 文案因此必须按清单事实分叉,而分叉判据只能是清单里已有的字段。
+ */
+describe('runUnavailableHintText', () => {
+ it('全新项目(清单里还没有工程内部版本)沿用原有措辞', () => {
+ expect(runUnavailableHintText({ versions: [] })).toBe(
+ '首个可运行原型尚未完成,运行视图暂不可用',
+ );
+ });
+
+ it('versions 字段整体缺失时按「没有版本」处理,不抛错', () => {
+ expect(runUnavailableHintText({} as { versions: [] })).toBe(
+ '首个可运行原型尚未完成,运行视图暂不可用',
+ );
+ });
+
+ it('已有工程内容但没有可运行的构建产物时如实说明需要先完成原型', () => {
+ const text = runUnavailableHintText({
+ versions: [
+ {
+ versionId: 'initial-1',
+ parentVersionId: null,
+ projectRevision: 1,
+ resourceBindings: [],
+ createdReason: 'initial',
+ createdAt: 1_791_271_678,
+ },
+ ],
+ });
+
+ expect(text).toBe(
+ '已有工程内容但还没有可运行的构建产物:让智能体完成可运行原型后即可运行',
+ );
+ // 不能再用「首个可运行原型尚未完成」那种"什么都还没有"的说法。
+ expect(text).not.toContain('首个可运行原型尚未完成');
+ });
+});