运行/预览提示退出对话区,改 toast 与运行区域小字
- 删除播放、/preview、/open-preview 与生成后自动启动预览写进对话区的成功提示,对话区只保留对话内容 - 新增 onRunNotice 通道,工作台壳用 RunNoticeToast 弹 2.6 秒浮层,同一句连续触发会重新计时 - 运行表现层在预览框上方常驻小字「已载入客户端运行视图:URL」,没有活预览时不占位 - 更新 appSurface 四处用例,新增 onRunNotice、小字与 RunNoticeToast 用例 - 同步 check-config 与 check-native-shells 的字符串判据,并记录决策到 shared-memory
This commit is contained in:
@@ -1876,7 +1876,7 @@ for (const snippet of [
|
||||
'runtime_config.save',
|
||||
"'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'",
|
||||
"'activate_local_game_preview'",
|
||||
'已切换到客户端运行视图',
|
||||
'已载入客户端运行视图',
|
||||
'async function executeRunLocal',
|
||||
'function needsInitializedChatProject',
|
||||
'function resolvePendingCommandProjectPath',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', {
|
||||
|
||||
+15
-10
@@ -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(/Run:run-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(/Run:run-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` 通过;真机观感未复验。
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user