运行/预览提示退出对话区,改 toast 与运行区域小字

- 删除播放、/preview、/open-preview 与生成后自动启动预览写进对话区的成功提示,对话区只保留对话内容
- 新增 onRunNotice 通道,工作台壳用 RunNoticeToast 弹 2.6 秒浮层,同一句连续触发会重新计时
- 运行表现层在预览框上方常驻小字「已载入客户端运行视图:URL」,没有活预览时不占位
- 更新 appSurface 四处用例,新增 onRunNotice、小字与 RunNoticeToast 用例
- 同步 check-config 与 check-native-shells 的字符串判据,并记录决策到 shared-memory
This commit is contained in:
2026-09-22 10:00:38 +08:00
parent cc8c03a40d
commit e6083050a8
14 changed files with 351 additions and 103 deletions
@@ -1876,7 +1876,7 @@ for (const snippet of [
'runtime_config.save',
"'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'",
"'activate_local_game_preview'",
'已切换到客户端运行视图',
'已载入客户端运行视图',
'async function executeRunLocal',
'function needsInitializedChatProject',
'function resolvePendingCommandProjectPath',
+30 -45
View File
@@ -497,6 +497,7 @@ type AppProps = {
metadata?: ProjectManifestSnapshotMetadata,
) => void;
onPreviewChange?: (preview: GameCreationAppPreviewState | null) => void;
onRunNotice?: ProjectSupervisorComponentProps['onRunNotice'];
onAgentRuntimeSummariesChange?: (
summaries: ProjectAgentRuntimeSummary[],
) => void;
@@ -543,6 +544,7 @@ export function App({
onPlayRequestHandled,
onManifestChange,
onPreviewChange,
onRunNotice,
onAgentRuntimeSummariesChange,
onAgentResultsChange,
}: AppProps = {}) {
@@ -6955,19 +6957,20 @@ export function App({
setPreviewStatus(`运行中:127.0.0.1:${previewResult.port}`);
setCommandLog((current) => [...current, 'preview.start']);
void refreshManifest(generatedProjectPath);
setMessages((current) => [
...current,
{
role: 'assistant',
text: [
`已保存并在客户端运行视图启动预览:${previewResult.url}`,
completionSummary?.text,
]
.filter(Boolean)
.join('\n\n'),
draftCommand: completionSummary?.draftCommand,
},
]);
// 启动预览的成功反馈走 toast,不再往对话区写一条「已保存并…启动预览:URL」;
// run trace 摘要本身是对话内容,继续按消息发出。
onRunNotice?.('运行通过,已载入客户端运行视图');
const completionText = completionSummary?.text;
if (completionText) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: completionText,
draftCommand: completionSummary.draftCommand,
},
]);
}
} catch (previewError) {
setMessages((current) => [
...current,
@@ -8812,15 +8815,8 @@ export function App({
setPreviewStatus(`运行中:127.0.0.1:${result.port}`);
void refreshManifest(nextProjectPath);
setCommandLog((current) => [...current, 'preview.start']);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: `预览已在客户端运行视图启动:${result.url}`,
},
]);
}
// 成功反馈走 toast + 运行区域上方的小字,不在对话区留行。
onRunNotice?.('运行通过,已载入客户端运行视图');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setPreviewStatus(message);
@@ -8875,30 +8871,18 @@ export function App({
'activate_local_game_preview',
nextProjectPath ? { projectPath: nextProjectPath } : undefined,
);
if (
result.status === 'running' &&
result.url &&
result.port &&
result.root
) {
updateClientPreview({
url: result.url,
port: result.port,
root: result.root,
});
const activatedPreview =
result.status === 'running' && result.url && result.port && result.root
? { url: result.url, port: result.port, root: result.root }
: null;
if (activatedPreview) {
updateClientPreview(activatedPreview);
setPreviewStatus(`运行中:127.0.0.1:${result.port}`);
}
setCommandLog((current) => [...current, 'preview.open']);
if (announceToChat) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: result.url
? `已切换到客户端运行视图:${result.url}`
: '已切换到客户端运行视图。',
},
]);
// 只有真的切到了运行视图才给反馈;没有活预览时不编一条成功提示。
if (activatedPreview) {
onRunNotice?.('已载入客户端运行视图');
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
@@ -9531,8 +9515,9 @@ export function App({
if (!directCodexProductRuntime) {
void refreshAgentRunTrace(nextProjectPath);
}
// 运行成功的提示不再写进聊天:它会一直堆在对话底部遮挡视野,而客户端运行视图
// 本身就是这条动作的可见反馈。失败仍按下面的分支回报,用户需要知道为什么没跑起来
// 运行成功的提示不再写进聊天会一直堆在对话底部遮挡运行画面):反馈走工作台壳的
// toast,预览地址由运行区域上方的小字常驻。失败仍按下面的分支回报
onRunNotice?.('运行通过,已载入客户端运行视图');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setLimitedCommandStatus(message);
@@ -0,0 +1,57 @@
import { PlatformRuntimeStatusToast } from '@genarrative/shared/components';
import { useEffect } from 'react';
import { createPortal } from 'react-dom';
const RUN_NOTICE_MILLIS = 2600;
export type RunNotice = {
/** 每次提示自增,保证重复触发同一个文案时也会重新弹一次。 */
id: number;
message: string;
};
/**
* 运行 / 预览类动作的浮层提示。
*
* 这类过程反馈以前以 assistant 消息写进对话区,会一直堆在对话底部挡住运行画面;
* 现在统一走 toast,对话区只保留对话内容,预览地址另由运行区域上方的小字常驻。
*/
export function RunNoticeToast({
notice,
onDismiss,
}: {
notice: RunNotice | null;
onDismiss: () => void;
}) {
useEffect(() => {
if (!notice) {
return;
}
const timer = window.setTimeout(onDismiss, RUN_NOTICE_MILLIS);
return () => {
window.clearTimeout(timer);
};
}, [notice, onDismiss]);
if (!notice) {
return null;
}
return createPortal(
<div
key={notice.id}
className="pointer-events-none fixed bottom-8 left-1/2 z-[1100] -translate-x-1/2"
data-project-run-notice-toast="true"
>
<PlatformRuntimeStatusToast
tone="success"
surface="solid"
size="sm"
shape="pill"
className="shadow-lg"
>
{notice.message}
</PlatformRuntimeStatusToast>
</div>,
document.body,
);
}
@@ -43,6 +43,7 @@ import {
} from './DeveloperAgentPanel';
import type { WorkspaceLauncherShellProps } from './model';
import { NonEmptyProjectDialog, ProjectsPage } from './ProjectCreation';
import { type RunNotice, RunNoticeToast } from './RunNoticeToast';
import { useAccountWallet } from './useAccountWallet';
import { useDeveloperAgentPanel } from './useDeveloperAgentPanel';
import {
@@ -79,6 +80,13 @@ export function WorkspaceLauncherShell({
title: string;
message: string;
} | null>(null);
/**
* 运行 / 预览类动作的浮层提示。
*
* 这类过程反馈不进对话区(见 `ProjectSupervisorComponentProps.onRunNotice`),
* `id` 每次自增,保证同一句文案连续触发时也会重新弹一次。
*/
const [runNotice, setRunNotice] = useState<RunNotice | null>(null);
// 「做成游戏」切换记录按项目上下文(路径 + createdAt)定位。运行模式必须随
// currentProjectContext 同步派生,不能靠 effect 后置修正:首帧挂错 lane 会先
// 以游戏运行时挂载并消耗首轮 claim,重挂后的策划实例再也发不出首轮。
@@ -521,6 +529,10 @@ export function WorkspaceLauncherShell({
);
}, []);
const handleRunNotice = useCallback((message: string) => {
setRunNotice((current) => ({ id: (current?.id ?? 0) + 1, message }));
}, []);
function showLauncherNotice(title: string) {
setLauncherNotice({
title,
@@ -750,6 +762,7 @@ export function WorkspaceLauncherShell({
onPlayRequestHandled={handlePlayRequestHandled}
onManifestChange={syncActiveProjectManifest}
onPreviewChange={setActiveProjectPreview}
onRunNotice={handleRunNotice}
onAgentRuntimeSummariesChange={
setActiveProjectAgentRuntimeSummaries
}
@@ -795,6 +808,7 @@ export function WorkspaceLauncherShell({
<AccountWalletBar controller={accountWallet} />
)}
<DeveloperAgentDialogs controller={developerAgent} />
<RunNoticeToast notice={runNotice} onDismiss={() => setRunNotice(null)} />
{launcherNotice ? (
<div
className="launcher-dialog-backdrop"
@@ -63,6 +63,13 @@ export type ProjectSupervisorComponentProps = {
metadata?: ProjectManifestSnapshotMetadata,
) => void;
onPreviewChange?: (preview: GameCreationAppPreviewState | null) => void;
/**
* 运行 / 预览类动作的一次性浮层提示(成功态)。
*
* 「跑起来了」「已切到运行视图」属于过程反馈,不进对话区——对话区只保留对话内容。
* 工作台壳收到后弹 toast;预览地址由运行区域上方的小字常驻,不再占对话位置。
*/
onRunNotice?: (message: string) => void;
onAgentRuntimeSummariesChange?: (
summaries: ProjectAgentRuntimeSummary[],
) => void;
+21
View File
@@ -8497,6 +8497,27 @@ iframe.preview-frame {
overflow: hidden;
}
/* 运行区域上方的小字:预览地址是过程信息,常驻在这里而不是写进对话区。 */
.game-run-preview-column {
display: flex;
min-height: 0;
flex-direction: column;
gap: 6px;
}
.game-run-status-hint {
margin: 0;
padding: 0 2px;
color: #96796d;
font-size: 11px;
line-height: 16px;
overflow-wrap: anywhere;
}
.game-run-preview-column > .game-run-preview {
flex: 1;
}
.game-run-preview iframe {
min-height: 0;
}
@@ -10850,22 +10850,33 @@ export default function ProjectDevelopmentView({
activeVersionId={activeVersionId}
onSelectVersion={selectActiveVersion}
/>
<div className="game-run-preview">
<div className="game-run-preview-column">
{/*
assistant
*/}
{embeddedPreviewUrl ? (
<LocalGamePreviewFrame
title={`${projectName} 游戏运行画面`}
preview={preview}
inspectMode={runtimeInspectMode}
onInspectSelection={handleRuntimeInspectSelection}
onInspectExit={() => setRuntimeInspectMode(false)}
/>
) : (
<div className="game-run-preview-empty">
<Sparkles size={28} aria-hidden="true" />
<strong></strong>
<span></span>
</div>
)}
<p className="game-run-status-hint">
{embeddedPreviewUrl}
</p>
) : null}
<div className="game-run-preview">
{embeddedPreviewUrl ? (
<LocalGamePreviewFrame
title={`${projectName} 游戏运行画面`}
preview={preview}
inspectMode={runtimeInspectMode}
onInspectSelection={handleRuntimeInspectSelection}
onInspectExit={() => setRuntimeInspectMode(false)}
/>
) : (
<div className="game-run-preview-empty">
<Sparkles size={28} aria-hidden="true" />
<strong></strong>
<span></span>
</div>
)}
</div>
</div>
<div className="game-run-panels">
<section aria-label="资源信息面板">
@@ -2292,12 +2292,14 @@ export function registerProjectCommandTests() {
await submitChat('/open-preview');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已切换到客户端运行视图:http://127.0.0.1:3210/'),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('activate_local_game_preview', {
projectPath: '/tmp/authorized-game',
});
// 成功反馈走工作台壳的 toast(见 RunNoticeToast / onRunNotice 用例),
// 对话区不再出现「已切换到客户端运行视图:URL」。
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('activate_local_game_preview', {
projectPath: '/tmp/authorized-game',
}),
);
expect(screen.queryByText(/已切换到客户端运行视图/)).toBeNull();
});
it('requires project policy confirmation before opening preview from chat', async () => {
@@ -2366,12 +2368,12 @@ export function registerProjectCommandTests() {
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已切换到客户端运行视图:http://127.0.0.1:3210/'),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('activate_local_game_preview', {
projectPath: '/tmp/authorized-game',
});
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('activate_local_game_preview', {
projectPath: '/tmp/authorized-game',
}),
);
expect(screen.queryByText(/已切换到客户端运行视图/)).toBeNull();
});
it('cancels pending preview open without opening preview', async () => {
@@ -2481,11 +2483,12 @@ export function registerProjectCommandTests() {
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(
/预览已在客户端运行视图启动:http:\/\/127\.0\.0\.1:3210\//,
),
).not.toBeNull();
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
projectPath: '/tmp/authorized-game',
}),
);
expect(screen.queryByText(/预览已在客户端运行视图启动/)).toBeNull();
expect(screen.queryByTitle('本地游戏预览')).toBeNull();
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
projectPath: '/tmp/authorized-game',
@@ -2581,11 +2584,12 @@ export function registerProjectCommandTests() {
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(
/预览已在客户端运行视图启动:http:\/\/127\.0\.0\.1:3210\//,
),
).not.toBeNull();
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
projectPath: '/tmp/authorized-game',
}),
);
expect(screen.queryByText(/预览已在客户端运行视图启动/)).toBeNull();
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
projectPath: '/tmp/authorized-game',
});
@@ -3025,13 +3029,17 @@ export function registerProjectCommandTests() {
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
// /run 成功只切换客户端运行视图,不再往聊天里追加“运行通过”提示。
// /run 成功只切换客户端运行视图:反馈走 toast,对话区不再追加“运行通过”提示。
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
projectPath: '/tmp/authorized-game',
}),
);
expect(screen.queryByText(/运行通过/)).toBeNull();
const chatMessages = Array.from(document.querySelectorAll('.message'));
expect(chatMessages.length).toBeGreaterThan(0);
expect(
chatMessages.some((node) => node.textContent?.includes('运行通过')),
).toBe(false);
expect(invoke).toHaveBeenCalledWith('run_limited_local_command', {
projectPath: '/tmp/authorized-game',
commandId: 'game.static_smoke',
@@ -2353,6 +2353,61 @@ export function registerProjectWorkbenchFoundationTests() {
expect(screen.getByLabelText('运行表现层')).not.toBeNull();
});
it('shows the live preview address as small text above the run area', async () => {
const manifest = createGameCreationAppManifest(
'workbench-run-status-hint',
'运行区域小字测试',
);
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_local_project_resource_graph') {
return resourceGraphForInputs(args);
}
if (command === 'read_local_project_resource_canvas_layout') {
return {
schemaVersion: 'game-creator-resource-layout.v1',
projectId: args?.expectedProjectId,
mode: args?.mode,
revision: 0,
positions: [],
updatedAt: 0,
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
render(
React.createElement(ProjectDevelopmentView, {
projectName: manifest.name,
projectPath: '/tmp/workbench-run-status-hint',
manifest,
attachments: [],
recentRunStatus: null,
recentRunStopReason: null,
preview: {
status: 'running',
url: 'http://127.0.0.1:4173/',
port: 4173,
},
supervisor: React.createElement('div', null, '项目总控'),
onHomeOpen: vi.fn(),
onProjectsOpen: vi.fn(),
}),
);
const hint = await screen.findByText(
'已载入客户端运行视图:http://127.0.0.1:4173/',
);
const surface = screen.getByLabelText('运行表现层');
expect(surface.contains(hint)).toBe(true);
// 小字紧贴在运行画面之上:它是预览框的前一个兄弟节点,而不是覆盖或寄生在别处。
const preview = surface.querySelector('.game-run-preview');
expect(preview).not.toBeNull();
expect(preview?.previousElementSibling).toBe(hint);
});
it('paints the marquee selection box with the scene token root and non-empty geometry', async () => {
const manifest = createGameCreationAppManifest(
'workbench-marquee-colour',
@@ -7185,6 +7240,7 @@ export function registerProjectSupervisorSurfaceTests() {
},
);
const handled = vi.fn();
const runNotice = vi.fn();
window.__TAURI__ = {
core: { invoke },
event: { listen: supervisorHarness.listen },
@@ -7196,11 +7252,12 @@ export function registerProjectSupervisorSurfaceTests() {
projectSupervisorOnly: true,
playRequest: { projectPath, requestId: 7 },
onPlayRequestHandled: handled,
onRunNotice: runNotice,
}),
);
// 顶部播放请求只负责把游戏起在运行视图里;聊天里不再新增“运行通过”提示,
// 否则每次播放都会在对话底部堆一条遮挡运行画面。
// 否则每次播放都会在对话底部堆一条遮挡运行画面——反馈改由工作台壳弹 toast
await waitFor(
() =>
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
@@ -7208,6 +7265,7 @@ export function registerProjectSupervisorSurfaceTests() {
}),
{ timeout: 3_000 },
);
expect(runNotice).toHaveBeenCalledWith('运行通过,已载入客户端运行视图');
expect(screen.queryByText(/运行通过/)).toBeNull();
expect(handled).toHaveBeenCalledWith(7);
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
@@ -822,8 +822,8 @@ export function registerProjectToolsAndPreviewTests() {
fireEvent.click(screen.getByRole('button', { name: '运行' }));
expect(await screen.findByText('game.run_local')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
// 运行成功不再写聊天提示(会一直堆在对话底部遮挡运行画面),改成断言预览真的起来了
// 且聊天里不再出现那条提示
// 运行成功不再写聊天提示(会一直堆在对话底部遮挡运行画面):反馈走 toast
// 预览地址改由运行区域上方的小字常驻
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
projectPath: '/tmp/authorized-game',
@@ -898,18 +898,23 @@ export function registerProjectToolsAndPreviewTests() {
fireEvent.click(screen.getByRole('button', { name: '启动预览' }));
expect(await screen.findByText('preview.start')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(
/预览已在客户端运行视图启动:http:\/\/127\.0\.0\.1:3210\//,
),
).not.toBeNull();
// 启动 / 切换运行视图的成功反馈统一走 toast,不再写进对话区。
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
projectPath: '/tmp/authorized-game',
}),
);
expect(screen.queryByText(/预览已在客户端运行视图启动/)).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '打开预览' }));
expect(await screen.findByText('preview.open')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已切换到客户端运行视图:http://127.0.0.1:3210/'),
).not.toBeNull();
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('activate_local_game_preview', {
projectPath: '/tmp/authorized-game',
}),
);
expect(screen.queryByText(/已切换到客户端运行视图/)).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '预览状态' }));
expect(
@@ -1852,12 +1852,14 @@ export function registerSupervisorRuntimeTests() {
await screen.findByText(/开始调用 LLM:Planner 正在整理规格。/),
).not.toBeNull();
expect(screen.getByText(/Generator 生成代码和资产清单/)).not.toBeNull();
expect(
await screen.findByText(
/已保存并在客户端运行视图启动预览:http:\/\/127\.0\.0\.1:3210\//,
),
).not.toBeNull();
expect(screen.getByText(/Runrun-chat-generate/)).not.toBeNull();
// 启动预览的成功反馈走 toast(见 `onRunNotice` 用例),对话区只保留 run trace 摘要。
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
projectPath: '/tmp/authorized-game',
}),
);
expect(screen.queryByText(/已保存并在客户端运行视图启动预览/)).toBeNull();
expect(await screen.findByText(/Runrun-chat-generate/)).not.toBeNull();
expect(
screen.getByText(/状态:passed · 2\/3 轮 · evaluator-passed/),
).not.toBeNull();
@@ -0,0 +1,73 @@
/** @vitest-environment jsdom */
import { act, cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { RunNoticeToast } from '../src/features/app-shell/RunNoticeToast';
const RUN_MESSAGE = '运行通过,已载入客户端运行视图';
function toastElement() {
return document.querySelector('[data-project-run-notice-toast="true"]');
}
describe('运行 / 预览浮层提示', () => {
afterEach(() => {
cleanup();
vi.useRealTimers();
});
it('没有提示时不渲染任何浮层', () => {
render(<RunNoticeToast notice={null} onDismiss={vi.fn()} />);
expect(toastElement()).toBeNull();
expect(screen.queryByText(RUN_MESSAGE)).toBeNull();
});
it('在浮层里显示提示,并在到点后自动收起', () => {
vi.useFakeTimers();
const onDismiss = vi.fn();
render(
<RunNoticeToast
notice={{ id: 1, message: RUN_MESSAGE }}
onDismiss={onDismiss}
/>,
);
expect(toastElement()).not.toBeNull();
expect(screen.getByText(RUN_MESSAGE)).not.toBeNull();
act(() => {
vi.advanceTimersByTime(2_600);
});
expect(onDismiss).toHaveBeenCalledTimes(1);
});
it('同一句文案连续触发时重新计时', () => {
vi.useFakeTimers();
const onDismiss = vi.fn();
const { rerender } = render(
<RunNoticeToast
notice={{ id: 1, message: RUN_MESSAGE }}
onDismiss={onDismiss}
/>,
);
rerender(
<RunNoticeToast
notice={{ id: 2, message: RUN_MESSAGE }}
onDismiss={onDismiss}
/>,
);
act(() => {
vi.advanceTimersByTime(2_000);
});
expect(onDismiss).not.toHaveBeenCalled();
act(() => {
vi.advanceTimersByTime(600);
});
expect(onDismiss).toHaveBeenCalledTimes(1);
});
});
@@ -9212,3 +9212,10 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 影响面:`apps/ai-game-creator-shell/src/view/project-development/index.tsx``.../features/resource-canvas/resourceCanvasFocusModel.ts``tests/{projectResourceLiveIntegration,resourceCanvasQuickEditDraft,resourceCanvasFloatingDismiss}.test.tsx`、PRD §3.10。
- 验证:定向 `projectResourceLiveIntegration`(32 条,三条断言面板留在失败态的用例按新口径改写为「重开面板再重试,身份不变」)、`resourceCanvasQuickEditDraft`10 条)、`resourceCanvasFloatingDismiss`18 条)全绿;`npm --prefix apps/ai-game-creator-shell run typecheck` 通过。
- 合并前复核(2026-09-21):合并 master 后按**仓库根**跑全量 `npx vitest run`,**373 个测试文件全过、4512 通过 / 34 跳过 / 0 失败**PR #419 显示 `No Conflicts`。真实客户端观感与远程 CI 未复验(后者按用户要求不追,runner/镜像问题见 Issue #431)。
## 2026-09-22 运行 / 预览的过程反馈退出对话区:改 toast + 运行区域小字
- 背景:客户端验收反馈——点播放,以及 `/preview``/open-preview`、生成后自动启动预览,都会往对话区写一条 assistant 提示(`运行通过,已载入客户端运行视图:http://127.0.0.1:63155/` 这类)。它常驻在对话底部遮挡运行画面,也让对话区混进非对话内容。
- 决策:对话区只保留对话内容(用户消息、Agent 回复、run trace 摘要、命令确认与错误)。运行 / 预览成功的反馈改由两条通道承载:① 工作台壳的 toast(`ProjectSupervisorComponentProps.onRunNotice``RunNoticeToast`,2.6 秒自动收起,同一句连续触发会重新计时);② 运行表现层预览框**上方**的小字(`已载入客户端运行视图:<url>`,只在有活预览时出现,不占位)。失败仍留在对话区——用户需要知道为什么没跑起来;`/preview-status` 这类命令问答也保留在对话区。
- 影响面:`apps/ai-game-creator-shell/src/App.tsx`(四处成功分支)、`src/features/app-shell/{model.ts,WorkspaceLauncher.tsx,RunNoticeToast.tsx}``src/view/project-development/index.tsx``src/styles.css`;随字符串守卫 `apps/ai-game-creator-shell/scripts/check-config.mjs``scripts/check-native-shells.mjs` 同步改判据。
- 验证:`appSurface` 全量 539 通过 / 17 跳过、0 失败;AGC 壳目录全量 169 文件 2146 通过 / 22 跳过;`tests/runNoticeToast.test.tsx` 3 条;`npm run agc:typecheck`(含 check-config)与 `npm run check:native-shells:contract` 通过;真机观感未复验。
+1 -1
View File
@@ -2723,7 +2723,7 @@ function assertAiGameCreatorShellUserDevBoundary() {
}
for (const snippet of [
"await invoke<LocalPreviewStatus>(\n 'activate_local_game_preview'",
'已切换到客户端运行视图',
'已载入客户端运行视图',
]) {
if (!sourceIncludesSnippet(aiGameCreatorShellAppSource, snippet)) {
throw new Error(