运行视图的自动切换只认会话内确认过的预览
- index.tsx:自动切运行的那条 effect 从「合并后的预览 URL」收窄为「会话预览 URL」(resolveEmbeddedPreviewUrl(previewOverride))。manifest.preview 是"上次留下的一条落盘记录":预览服务器是进程内线程、非 Linux 端口还是随机临时端口,客户端退出后记录仍会写着 running,照着它自动切视图就是把用户丢进一个打不开的运行界面。运行入口(运行 tab、播放按钮)与 runAvailable 一行未动,PRD §4.1「运行入口不可用时仍允许点击…但不切换状态」继续成立。 - useHomeProjectCreation.ts:enterProjectDevelopment 不再把 context.manifest.preview 播种成本次会话的预览(改为 null)。会话预览此后只由活体确认(下一提交)与本次会话里 preview.start / preview.status 的返回值经 onPreviewChange 回流两处写入。 - 有意接受的代价:「回到仍在运行的预览」从自动变成用户点一下「运行」;下一提交会把"确实还在跑"这条路径按活体确认接回自动进入。 - project-development.suite.ts:新增两条用例——① 仅 manifest.preview 为 running(不传 preview)时初始仍是「资源管理」、运行 tab 仍可用、点一下仍能进运行视图;② 会话预览为 running 时自动切到运行表现层。 - 变异验证:把这条 effect 还原成读合并后的 URL(即撤掉本次收窄),用例①立即红灯(已实测)。
This commit is contained in:
@@ -177,7 +177,13 @@ export function useHomeProjectCreation({
|
||||
|
||||
function enterProjectDevelopment(context: LauncherProjectContext) {
|
||||
setCurrentProjectContext(context);
|
||||
setActiveProjectPreview(context.manifest.preview ?? null);
|
||||
/**
|
||||
* 进项目时**不**把 manifest 里的 `preview` 当成本次会话的预览:那是"上次留下的一条
|
||||
* 落盘记录",预览服务器是进程内线程、非 Linux 端口还是随机临时端口,客户端退出后
|
||||
* 记录仍会写着 running。会话预览只由活体确认(下一步的 registry 核对)或本次会话里
|
||||
* 的 preview.start / preview.status 写入,见 `activeProjectPreview` 的其它赋值点。
|
||||
*/
|
||||
setActiveProjectPreview(null);
|
||||
setAgentRuntimeSummaries([]);
|
||||
setAgentResults([]);
|
||||
setProjectPath(context.projectPath);
|
||||
|
||||
@@ -3421,8 +3421,19 @@ export default function ProjectDevelopmentView({
|
||||
});
|
||||
}, [mode, resourceSectionScrollKey, sortMode]);
|
||||
|
||||
/**
|
||||
* 会话内确认过的预览才自动切到运行视图。
|
||||
*
|
||||
* 只认 `previewOverride`:它是 launcher 按内存 registry 的**活体**确认后交给工作台的
|
||||
* 会话预览,也是本次会话里 preview.start / preview.status 的返回值。`manifest.preview`
|
||||
* 只是"上次留下的一条落盘记录"——预览服务器是进程内线程、非 Linux 端口还是随机临时
|
||||
* 端口,客户端退出后那条记录仍然写着 running,照着它自动切视图就是把用户丢进一个
|
||||
* 打不开的运行界面。切视图的入口照旧齐全(运行 tab 与播放按钮,PRD §4.1),只是
|
||||
* "回到仍在运行的预览"从自动变成用户点一下。
|
||||
*/
|
||||
const sessionPreviewUrl = resolveEmbeddedPreviewUrl(previewOverride);
|
||||
useEffect(() => {
|
||||
if (embeddedPreviewUrl) {
|
||||
if (sessionPreviewUrl) {
|
||||
canvasOpenEpochRef.current += 1;
|
||||
advanceFocusGeneration();
|
||||
activeFocusFlowIdRef.current = null;
|
||||
@@ -3433,7 +3444,7 @@ export default function ProjectDevelopmentView({
|
||||
setQuickEditSourceLayer(null);
|
||||
setMode('run');
|
||||
}
|
||||
}, [advanceFocusGeneration, embeddedPreviewUrl]);
|
||||
}, [advanceFocusGeneration, sessionPreviewUrl]);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
|
||||
@@ -1741,6 +1741,129 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
expect(screen.getByRole('region', { name: '资源总览' })).not.toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the resource view when only the persisted preview record says running', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-persisted-preview',
|
||||
'落盘预览记录测试',
|
||||
);
|
||||
// manifest.preview 是"上次留下的一条落盘记录":进程退出后它仍会写着 running,
|
||||
// 但预览服务器是进程内线程、端口还是随机临时端口,真实情况是已经打不开了。
|
||||
manifest.preview = {
|
||||
status: 'running',
|
||||
url: 'http://127.0.0.1:4173/',
|
||||
port: 4173,
|
||||
};
|
||||
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-persisted-preview',
|
||||
manifest,
|
||||
attachments: [],
|
||||
recentRunStatus: null,
|
||||
recentRunStopReason: null,
|
||||
supervisor: React.createElement('div', null, '项目总控'),
|
||||
onHomeOpen: vi.fn(),
|
||||
onProjectsOpen: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
// 初始视图是资源管理,不是运行:落盘记录不触发自动切换。
|
||||
expect(
|
||||
screen
|
||||
.getByRole('tab', { name: '资源管理' })
|
||||
.getAttribute('aria-selected'),
|
||||
).toBe('true');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '运行' }).getAttribute('aria-selected'),
|
||||
).toBe('false');
|
||||
expect(screen.queryByLabelText('运行表现层')).toBeNull();
|
||||
expect(document.querySelector('[data-resource-view-state]')).not.toBeNull();
|
||||
// 但运行入口照旧可用(PRD §4.1:入口不可用时仍允许点击,只是不切换状态)。
|
||||
const runTab = screen.getByRole('tab', {
|
||||
name: '运行',
|
||||
}) as HTMLButtonElement;
|
||||
expect(runTab.disabled).toBe(false);
|
||||
expect(runTab.getAttribute('data-unavailable')).toBeNull();
|
||||
// 用户点一下仍然能进运行视图("回到仍在运行的预览"从自动变手动)。
|
||||
fireEvent.click(runTab);
|
||||
expect(runTab.getAttribute('aria-selected')).toBe('true');
|
||||
expect(screen.getByLabelText('运行表现层')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('switches to the run view when the session confirms a live preview', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-session-preview',
|
||||
'会话活体预览测试',
|
||||
);
|
||||
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-session-preview',
|
||||
manifest,
|
||||
attachments: [],
|
||||
recentRunStatus: null,
|
||||
recentRunStopReason: null,
|
||||
// 会话预览:launcher 按内存 registry 的活体确认后交进来(或本次会话里
|
||||
// preview.start / preview.status 的返回值)。
|
||||
preview: {
|
||||
status: 'running',
|
||||
url: 'http://127.0.0.1:4173/',
|
||||
port: 4173,
|
||||
},
|
||||
supervisor: React.createElement('div', null, '项目总控'),
|
||||
onHomeOpen: vi.fn(),
|
||||
onProjectsOpen: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '运行' }).getAttribute('aria-selected'),
|
||||
).toBe('true'),
|
||||
);
|
||||
expect(screen.getByLabelText('运行表现层')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('paints the marquee selection box with the scene token root and non-empty geometry', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-marquee-colour',
|
||||
|
||||
Reference in New Issue
Block a user