修 review 发现:运行反馈全面退出对话区,并补齐顶栏与守卫细节
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m5s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m13s
Project CI / Frontend tests (pull_request) Successful in 3m15s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m56s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m55s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m21s
Project CI / Native shell tests (pull_request) Successful in 8m53s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m5s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m13s
Project CI / Frontend tests (pull_request) Successful in 3m15s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m56s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m55s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m21s
Project CI / Native shell tests (pull_request) Successful in 8m53s
- executeRunLocal 的三条失败路径(缺 Tauri / 缺项目 / 启动预览报错)也改走 onRunNotice 失败色提示,对话区不再出现过程行;随之删除已无调用方的 announceProjectChatMessage 与不再有意义的 announceToChat 参数 - RunNoticeToast 的失败提示延长到 6 秒(成功仍 2.6 秒),避免错误一闪而过 - 版本入口在 UI 编辑器壳里不渲染;版本名改由一层 span 承载省略号,长版本名不再被硬裁 - 视图拿不到 onNotice 时把「在浏览器打开失败」写进 console,不再静默吞掉 - check-native-shells 增加「运行页视图里 openUrl( 只有一个调用点」的判据,堵住自动开浏览器的口子 - 新增外壳级接线用例 runNoticeShellWiring;previewActivation 补「启动失败走失败色提示且不写对话区」;runNoticeToast 补失败时长;样式守卫补省略号 - decision-log 收敛到最终口径(成功与失败都出对话区、失败留 6 秒、编辑器不挂版本入口)
This commit is contained in:
@@ -806,9 +806,7 @@ export function App({
|
||||
const agentRuntimeResumeProjectPathRef = useRef<string | null>(null);
|
||||
const initialProjectOpenedRef = useRef(false);
|
||||
const pendingUiConfirmationActionRef = useRef<(() => void) | null>(null);
|
||||
const executeRunLocalRef = useRef<(announceToChat: boolean) => void>(
|
||||
() => undefined,
|
||||
);
|
||||
const executeRunLocalRef = useRef<() => void>(() => undefined);
|
||||
const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false);
|
||||
|
||||
function requestRuntimeConfigOpen() {
|
||||
@@ -1309,7 +1307,7 @@ export function App({
|
||||
}
|
||||
handledPlayRequestRef.current = requestKey;
|
||||
onPlayRequestHandled?.(playRequest.requestId);
|
||||
void executeRunLocalRef.current(true);
|
||||
void executeRunLocalRef.current();
|
||||
}, [localProject?.projectPath, onPlayRequestHandled, playRequest]);
|
||||
|
||||
/**
|
||||
@@ -1641,20 +1639,6 @@ export function App({
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作台壳要把一句结果说给用户在项目对话里听。
|
||||
*
|
||||
* DirectProject 的会话由聊天容器持有,壳只把这句话交给聊天的本地消息流;
|
||||
* 立项策划路径仍写壳自己的 `messages`。
|
||||
*/
|
||||
function announceProjectChatMessage(text: string) {
|
||||
if (directProjectMode) {
|
||||
directProjectChatRef.current?.announce(text);
|
||||
return;
|
||||
}
|
||||
setMessages((current) => [...current, { role: 'assistant', text }]);
|
||||
}
|
||||
|
||||
async function executeChatAgentReply({
|
||||
prompt,
|
||||
clientTurnId: directConversationTurnId,
|
||||
@@ -1760,19 +1744,18 @@ export function App({
|
||||
}
|
||||
}
|
||||
|
||||
async function executeRunLocal(announceToChat: boolean) {
|
||||
async function executeRunLocal() {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
if (announceToChat) {
|
||||
announceProjectChatMessage('需要在 Tauri App 内运行。');
|
||||
}
|
||||
onRunNotice?.({ tone: 'error', message: '需要在 Tauri App 内运行。' });
|
||||
return;
|
||||
}
|
||||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||||
if (!nextProjectPath) {
|
||||
if (announceToChat) {
|
||||
announceProjectChatMessage('请先用 /project 设置本地项目。');
|
||||
}
|
||||
onRunNotice?.({
|
||||
tone: 'error',
|
||||
message: '请先用 /project 设置本地项目。',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1797,12 +1780,19 @@ export function App({
|
||||
if (!directProjectMode) {
|
||||
void refreshAgentRunTrace(nextProjectPath);
|
||||
}
|
||||
/*
|
||||
* 成功与失败都走工作台壳的 toast,对话区不再承载这条过程反馈,所以这两处不受
|
||||
* `announceToChat` 约束(它是旧的「聊天播报」开关)。当前唯一调用点由播放请求驱动、
|
||||
* 恒为 true(见 `executeRunLocalRef.current(true)`)。
|
||||
*/
|
||||
onRunNotice?.({ message: '运行通过,已载入客户端运行视图' });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (announceToChat) {
|
||||
announceProjectChatMessage(message);
|
||||
}
|
||||
onRunNotice?.({
|
||||
tone: 'error',
|
||||
message: `运行游戏失败:${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,16 @@ import { createPortal } from 'react-dom';
|
||||
|
||||
import type { ProjectRunNotice } from './model';
|
||||
|
||||
const RUN_NOTICE_MILLIS = 2600;
|
||||
/**
|
||||
* 成功提示一闪而过就够;失败提示要留得够久,用户得看清是什么没跑起来。
|
||||
*
|
||||
* 「运行 / 预览失败」这类错误已经不再写对话区(那里只保留对话内容),所以这枚 toast 是它
|
||||
* 唯一的出口——2.6 秒对错误太短。
|
||||
*/
|
||||
const RUN_NOTICE_MILLIS: Record<'success' | 'error', number> = {
|
||||
success: 2600,
|
||||
error: 6000,
|
||||
};
|
||||
|
||||
export type RunNotice = ProjectRunNotice & {
|
||||
/** 每次提示自增,保证重复触发同一个文案时也会重新弹一次。 */
|
||||
@@ -28,7 +37,10 @@ export function RunNoticeToast({
|
||||
if (!notice) {
|
||||
return;
|
||||
}
|
||||
const timer = window.setTimeout(onDismiss, RUN_NOTICE_MILLIS);
|
||||
const timer = window.setTimeout(
|
||||
onDismiss,
|
||||
RUN_NOTICE_MILLIS[notice.tone ?? 'success'],
|
||||
);
|
||||
return () => {
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
|
||||
@@ -75,7 +75,14 @@ export function GameRunVersionPicker({
|
||||
aria-label={`当前版本:${formatIterationVersionLabel(currentVersion)}`}
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
>
|
||||
{formatIterationVersionLabel(currentVersion)}
|
||||
{/*
|
||||
版本名可能很长(`初始版本 · 2026/9/19 02:10:03`)。按钮是 flex 容器,直接放文本节点
|
||||
时 `text-overflow: ellipsis` 不生效(匿名 flex item 不参与父级省略),所以套一层
|
||||
span 由它省略(见样式里的 `.game-run-version-trigger-label`)。
|
||||
*/}
|
||||
<span className="game-run-version-trigger-label">
|
||||
{formatIterationVersionLabel(currentVersion)}
|
||||
</span>
|
||||
</button>
|
||||
{open
|
||||
? createPortal(
|
||||
|
||||
@@ -8128,6 +8128,15 @@ iframe.preview-frame {
|
||||
|
||||
.game-run-version-trigger {
|
||||
max-width: min(18rem, 60vw);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 版本名单独一层才省得掉:按钮是 flex 容器,文本直接挂在按钮上时 `text-overflow`
|
||||
* 落在匿名 flex item 上、不生效(见 `GameRunVersionPicker` 里的注释)。
|
||||
*/
|
||||
.game-run-version-trigger-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -2734,7 +2734,12 @@ export default function ProjectDevelopmentView({
|
||||
try {
|
||||
await openUrl(embeddedPreviewUrl);
|
||||
} catch (error) {
|
||||
onNotice?.({
|
||||
// 没有提示通道时(测试挂载、未来宿主)至少留下排查痕迹,不静默吞掉。
|
||||
if (!onNotice) {
|
||||
console.error('[agc] 在浏览器打开失败', error);
|
||||
return;
|
||||
}
|
||||
onNotice({
|
||||
tone: 'error',
|
||||
message: `在浏览器打开失败:${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
@@ -9788,13 +9793,18 @@ export default function ProjectDevelopmentView({
|
||||
{/*
|
||||
C7 版本入口:与「打开项目目录 / 在浏览器打开」同处顶栏动作区,外观也走同一套
|
||||
(基础规则在 `.game-workbench-view-actions button`)。它不再浮在运行画面上:
|
||||
以前是绝对定位压在画面右上角,挡画面且与左侧的小字不对齐。
|
||||
以前是绝对定位压在画面右上角,挡画面且与预览地址不对齐。
|
||||
|
||||
**UI 编辑器壳里不渲染**:那一页是聚焦编辑某个资源的界面,顶栏只留通用动作;
|
||||
版本入口服务于资源画布与运行页(`@` 面板按当前版本取素材),编辑器不需要它。
|
||||
*/}
|
||||
<GameRunVersionPicker
|
||||
versions={projectVersions}
|
||||
activeVersionId={activeVersionId}
|
||||
onSelectVersion={selectActiveVersion}
|
||||
/>
|
||||
{uiEditorRoute ? null : (
|
||||
<GameRunVersionPicker
|
||||
versions={projectVersions}
|
||||
activeVersionId={activeVersionId}
|
||||
onSelectVersion={selectActiveVersion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/*
|
||||
布局状态提示**不进动作行**:它是一段随保存过程变长的文案(空 →「保存中」→
|
||||
@@ -10989,7 +10999,7 @@ export default function ProjectDevelopmentView({
|
||||
它**不**参与 `isResourceCanvasFloatingPanelOpen` 的模态遮挡判据——生成在后台跑,
|
||||
侧栏展开时画布必须照样能看能用;折叠只影响这个视图,任务本身活在账本与本地队列里。
|
||||
|
||||
**运行表现层不挂它**:运行页要留给游戏画面,右上是预览地址小字与版本入口,
|
||||
**运行表现层不挂它**:运行页要留给游戏画面,右上角是版本入口与「在浏览器打开」,
|
||||
再叠一枚任务开关(或展开的面板)就会压在画面上。任务不会因此丢,切回资源页即可见。
|
||||
*/}
|
||||
{mode === 'run' ? null : (
|
||||
|
||||
@@ -56,7 +56,12 @@ describe('运行页顶栏动作区样式', () => {
|
||||
expect(trigger.has('border')).toBe(false);
|
||||
expect(trigger.has('background')).toBe(false);
|
||||
expect(trigger.has('color')).toBe(false);
|
||||
expect(declaration(trigger, 'text-overflow')).toBe('ellipsis');
|
||||
expect(declaration(trigger, 'min-width')).toBe('0');
|
||||
// 省略号要真的生效:按钮是 flex 容器,文本必须挂在自带 overflow 的 span 上。
|
||||
const label = resolved(['.game-run-version-trigger-label']);
|
||||
expect(declaration(label, 'overflow')).toBe('hidden');
|
||||
expect(declaration(label, 'text-overflow')).toBe('ellipsis');
|
||||
expect(declaration(label, 'white-space')).toBe('nowrap');
|
||||
expect(hasRule('.game-run-version-trigger:hover')).toBe(false);
|
||||
expect(hasRule('.game-run-version-trigger:focus-visible')).toBe(false);
|
||||
|
||||
|
||||
@@ -45,7 +45,10 @@ function createFixtureManifest(): GameCreationAppManifest {
|
||||
* `activate_local_game_preview` 的替身由用例给定:它决定「这条预览还活着吗」,
|
||||
* 其余命令沿用聊天 harness,运行入口之外的链路保持真实形状。
|
||||
*/
|
||||
function installTauri(activateLocalGamePreview: () => unknown) {
|
||||
function installTauri(
|
||||
activateLocalGamePreview: () => unknown,
|
||||
options: { startFails?: string } = {},
|
||||
) {
|
||||
const manifest = createFixtureManifest();
|
||||
const chatHarness = createProjectChatRuntimeHarness({
|
||||
projectPath: PROJECT_PATH,
|
||||
@@ -64,6 +67,9 @@ function installTauri(activateLocalGamePreview: () => unknown) {
|
||||
return activateLocalGamePreview();
|
||||
}
|
||||
if (command === 'start_local_game_preview') {
|
||||
if (options.startFails) {
|
||||
throw new Error(options.startFails);
|
||||
}
|
||||
return { url: PREVIEW_URL, port: 43210, root: PROJECT_PATH };
|
||||
}
|
||||
return chatHarness.invoke(command, args);
|
||||
@@ -164,4 +170,22 @@ describe('运行入口切到已经在跑的客户端预览', () => {
|
||||
'运行通过,已载入客户端运行视图',
|
||||
);
|
||||
});
|
||||
|
||||
it('启动预览失败时走失败色提示,不再写进对话区', async () => {
|
||||
installTauri(
|
||||
() => ({ status: 'stopped', url: null, port: null, root: null }),
|
||||
{ startFails: '预览端口被占用' },
|
||||
);
|
||||
|
||||
const { onRunNotice } = renderRunningProjectChat();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(onRunNotice).toHaveBeenCalledWith({
|
||||
tone: 'error',
|
||||
message: '运行游戏失败:预览端口被占用',
|
||||
}),
|
||||
);
|
||||
const surface = await screen.findByLabelText('陶泥儿项目对话');
|
||||
expect(surface.textContent ?? '').not.toContain('预览端口被占用');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
/**
|
||||
* 运行提示的外壳级接线:`ProjectChat` 发一条提示,工作台壳真的把它渲染成浮层。
|
||||
*
|
||||
* 单测(`runNoticeToast`)只证明组件本身,`previewActivation` 只证明 App 会调这条通道;
|
||||
* 从「通道被调用」到「用户看到 toast」之间还差一层壳的接线(prop 名、handler、portal 挂点、
|
||||
* tone 传递)。这里用替身聊天把这一层单独钉住:以后有人把 `<RunNoticeToast/>` 挪进条件
|
||||
* 分支、或在传 prop 时写错名字,这条会红。
|
||||
*/
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import type { ProjectChatComponentProps } from '../src/features/app-shell/model';
|
||||
import { WorkspaceLauncherShell } from '../src/features/app-shell/WorkspaceLauncher';
|
||||
import {
|
||||
createProjectChatRuntimeHarness,
|
||||
pickProjectFromLauncher,
|
||||
testAuthUser,
|
||||
} from './appSurface/harness';
|
||||
|
||||
const PROJECT_PATH = '/tmp/run-notice-shell-project';
|
||||
|
||||
function StubRunNoticeChat({ onRunNotice }: ProjectChatComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onRunNotice?.({ message: '运行通过,已载入客户端运行视图' })
|
||||
}
|
||||
>
|
||||
触发成功提示
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onRunNotice?.({
|
||||
tone: 'error',
|
||||
message: '运行游戏失败:预览端口被占用',
|
||||
})
|
||||
}
|
||||
>
|
||||
触发失败提示
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开工作台所需的最小命令集照抄 `home.suite` 的活动清单用例:项目目录探测 + 清单 +
|
||||
* 资源图 / 布局读回,其余命令沿用聊天 harness。
|
||||
*/
|
||||
function renderWorkbench() {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'run-notice-shell-project',
|
||||
'运行提示接线项目',
|
||||
);
|
||||
const runtimeHarness = createProjectChatRuntimeHarness({
|
||||
projectPath: PROJECT_PATH,
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath: PROJECT_PATH,
|
||||
exists: true,
|
||||
isDirectory: true,
|
||||
isGameCreatorProject: true,
|
||||
projectName: manifest.name,
|
||||
recentRunStatus: null,
|
||||
recentRunStopReason: null,
|
||||
};
|
||||
}
|
||||
if (command === 'get_local_game_manifest') {
|
||||
return manifest;
|
||||
}
|
||||
if (command === 'read_local_project_resource_graph') {
|
||||
return {
|
||||
resourceIds: [],
|
||||
referenceEdges: [],
|
||||
taskFlows: [],
|
||||
connectionIndex: [],
|
||||
producerAssignments: [],
|
||||
dependencyDepths: [],
|
||||
unresolvedReferenceResourceIds: [],
|
||||
cyclicResourceIds: [],
|
||||
cyclicTaskIds: [],
|
||||
producerMappingTruncated: false,
|
||||
};
|
||||
}
|
||||
if (command === 'read_local_project_resource_canvas_layout') {
|
||||
return {
|
||||
schemaVersion: 'game-creator-resource-layout.v1',
|
||||
projectId: manifest.projectId,
|
||||
mode: args?.mode,
|
||||
revision: 0,
|
||||
positions: [],
|
||||
updatedAt: 0,
|
||||
};
|
||||
}
|
||||
return runtimeHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: invoke as never },
|
||||
event: { listen: runtimeHarness.listen as never },
|
||||
};
|
||||
render(
|
||||
<WorkspaceLauncherShell
|
||||
currentUser={testAuthUser}
|
||||
initialView="projects"
|
||||
onLogout={vi.fn()}
|
||||
ProjectChat={StubRunNoticeChat}
|
||||
/>,
|
||||
);
|
||||
pickProjectFromLauncher(PROJECT_PATH);
|
||||
}
|
||||
|
||||
function toastElement() {
|
||||
return document.querySelector('[data-project-run-notice-toast="true"]');
|
||||
}
|
||||
|
||||
describe('运行提示的外壳接线', () => {
|
||||
it('成功提示渲染成浮层,且不落在对话容器里', async () => {
|
||||
renderWorkbench();
|
||||
await screen.findByLabelText('项目开发工作台');
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', { name: '触发成功提示' }),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(toastElement()).not.toBeNull());
|
||||
expect(toastElement()?.textContent ?? '').toContain(
|
||||
'运行通过,已载入客户端运行视图',
|
||||
);
|
||||
// 「对话区里没有这条提示」由 `previewActivation` 用真实聊天容器断言;这里用的替身
|
||||
// 聊天没有消息列表,重复断言只会自证。
|
||||
});
|
||||
|
||||
it('失败提示按 alert 渲染', async () => {
|
||||
renderWorkbench();
|
||||
await screen.findByLabelText('项目开发工作台');
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', { name: '触发失败提示' }),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(toastElement()?.querySelector('[role="alert"]')).not.toBeNull(),
|
||||
);
|
||||
expect(toastElement()?.textContent ?? '').toContain(
|
||||
'运行游戏失败:预览端口被占用',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -92,4 +92,29 @@ describe('运行 / 预览浮层提示', () => {
|
||||
screen.getByText('在浏览器打开失败:permission denied'),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('失败提示比成功提示留得久:2.6 秒不该把错误收走', () => {
|
||||
vi.useFakeTimers();
|
||||
const onDismiss = vi.fn();
|
||||
render(
|
||||
<RunNoticeToast
|
||||
notice={{
|
||||
id: 1,
|
||||
tone: 'error',
|
||||
message: '运行游戏失败:预览端口被占用',
|
||||
}}
|
||||
onDismiss={onDismiss}
|
||||
/>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(2_600);
|
||||
});
|
||||
expect(onDismiss).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(3_400);
|
||||
});
|
||||
expect(onDismiss).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9316,9 +9316,10 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
## 2026-09-22 运行页收口:过程提示退出对话区,顶栏统一承载运行入口
|
||||
|
||||
- 背景:点播放(以及历史上 `/preview`、`/open-preview`、生成后自动启动预览)都会往对话区写一条 assistant 提示(`运行通过,已载入客户端运行视图:http://127.0.0.1:63155/` 这类)。它常驻对话底部遮挡运行画面,也让对话区混进非对话内容;运行页本身还有三处遮挡与两套皮:预览地址是一行常驻小字(不可点)、版本入口是绝对定位压在画面右上角的浮层、右上角还叠着「生成任务」开关。
|
||||
- 决策(对话区只留对话内容):运行 / 预览成功的反馈不再进对话区,改走工作台壳的 toast——`ProjectChatComponentProps.onRunNotice` → `RunNoticeToast`(2.6 秒自动收起,同一句连续触发重新计时,`tone` 决定成功 / 失败观感)。失败(启动预览失败、在浏览器打开失败)也走这条通道,不再往对话区写过程行。
|
||||
- 决策(运行页顶栏是唯一入口):运行区域上方的状态行与预览地址小字整体退役(运行画面回到两行栅格);预览地址改成顶栏动作区里的一枚「在浏览器打开」按钮(opener 插件的 `openUrl`,只在有活预览时渲染);版本入口搬进同一个顶栏动作区,外观复用 `.game-workbench-view-actions button` 的基础规则,不再自带边框 / 底色 / hover,也不再是绝对定位浮层。
|
||||
- 决策(对话区只留对话内容):运行 / 预览的反馈不再进对话区,**成功与失败都**走工作台壳的 toast——`ProjectChatComponentProps.onRunNotice` → `RunNoticeToast`(同一句连续触发重新计时;成功 2.6 秒收起,失败 6 秒收起,`tone` 决定观感)。失败三处:启动预览报错(`运行游戏失败:…`)、缺 Tauri / 缺项目这两条前置条件、以及「在浏览器打开失败」。原先承载这些文案的 `announceProjectChatMessage` 已无调用方,连同删除。
|
||||
- 与上一条的关系(2026-09-21「预览激活回接运行入口」):那条接线决策(先问 Rust 要活体预览、命中就只切视图不重启)原样保留;被本次改掉的只是它当时为这条链路选的**播报渠道**——`经 DirectProjectChatHandle.announce 说一句「已切换到客户端运行视图:<url>」` 按验收反馈(提示常驻对话底部遮挡运行画面)改成 toast,「复用活体预览不重启」的判据与 `tests/previewActivation.test.tsx` 的三条路径不变。
|
||||
- 决策(运行页顶栏是唯一入口):运行区域上方的状态行与预览地址小字整体退役(运行画面回到两行栅格);预览地址改成顶栏动作区里的一枚「在浏览器打开」按钮(opener 插件的 `openUrl`,只在有活预览时渲染;`scripts/check-native-shells.mjs` 另加「视图里 `openUrl(` 只有一个调用点」的判据);版本入口搬进同一个顶栏动作区,外观复用 `.game-workbench-view-actions button` 的基础规则,不再自带边框 / 底色 / hover,也不再是绝对定位浮层。版本入口在**资源画布与运行页**显示,UI 编辑器壳里不渲染(那一页是聚焦编辑某个资源的界面)。版本名改用一层 span 承载省略号——按钮是 flex 容器,文本直接挂在按钮上时 `text-overflow` 不生效。
|
||||
- 决策(运行页不挂生成任务):`ResourceCanvasAssetGenerationTasksPanelView` 只在资源画布 / UI 编辑器出现,运行页的入口、面板与锚点都不渲染;`[data-generation-tasks-placement='run']` 那一档坐标与组件 `placement` 联合类型里的 `run` 一并删除。任务不丢,切回资源页即可见。
|
||||
- 边界:预览的**启动与切换**仍然只走内置运行画面、不自动开系统浏览器——`scripts/check-native-shells.mjs` 那条负向守卫保持原样,本轮只补「浏览器入口只有顶栏这一枚按钮」的正向断言。
|
||||
- 影响面:`apps/ai-game-creator-shell/src/{App.tsx,styles.css,view/project-development/index.tsx,features/app-shell/*,features/resource-canvas/*}`;用例新增 `tests/runPreviewBrowserOpen.test.tsx`、`tests/runNoticeToast.test.tsx`、`tests/gameRunToolbarActionsStyle.test.ts`,改写 `tests/previewActivation.test.tsx`(原断言「聊天里出现已载入运行视图」的地方改为断言 toast 通道 + 对话容器里没有这类提示)与 `tests/appSurface/project-development.suite.ts` 的生成任务入口用例。
|
||||
- 影响面:`apps/ai-game-creator-shell/src/{App.tsx,styles.css,view/project-development/index.tsx,features/app-shell/*,features/resource-canvas/*}`;用例新增 `tests/runPreviewBrowserOpen.test.tsx`、`tests/runNoticeToast.test.tsx`、`tests/runNoticeShellWiring.test.tsx`(外壳级:替身聊天发提示 → 壳真的渲染浮层)、`tests/gameRunToolbarActionsStyle.test.ts`,改写 `tests/previewActivation.test.tsx`(原断言「聊天里出现已载入运行视图」的地方改为断言 toast 通道 + 对话容器里没有这类提示,并补一条「启动失败走失败色提示且不写对话区」)与 `tests/appSurface/project-development.suite.ts` 的生成任务入口用例。
|
||||
- 验证:`appSurface` 全量、AGC 壳目录全量、`npm run typecheck`、`check:native-shells:contract`、`check:encoding`、`git diff --check` 全绿;真机观感未复验。
|
||||
|
||||
@@ -2695,6 +2695,18 @@ function assertAiGameCreatorShellUserDevBoundary() {
|
||||
);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 上面那条负向守卫只盯着 App.tsx 里的两条老路,而真正的浏览器出口在运行页视图里。
|
||||
* 这里再补一条「调用点只有一个」的判据:视图里冒出第二个 `openUrl(`(自动跳浏览器、
|
||||
* 或者拿它去开任意地址)时必须先显式改这条守卫,而不是顺手加一行。
|
||||
*/
|
||||
const openUrlCallCount =
|
||||
aiGameCreatorProjectDevelopmentSource.split('openUrl(').length - 1;
|
||||
if (openUrlCallCount !== 1) {
|
||||
throw new Error(
|
||||
`AI game creator must keep exactly one user-clicked browser-open call in the run view (found ${openUrlCallCount})`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
aiGameCreatorShellTauriSource.includes('fn open_developer_window(') ||
|
||||
aiGameCreatorShellTauriSource.includes(
|
||||
|
||||
Reference in New Issue
Block a user