From 3775e0597004480a57271644cd2e37e0b891abe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=94=E4=BB=A4=E5=BC=98?= Date: Tue, 22 Sep 2026 16:25:51 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=AD=96=E5=88=92=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E5=B8=83=E5=B1=80=E4=B8=8E=E6=B5=81=E5=BC=8F=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=20(#460)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 恢复固定消息区域和工作区状态条样式,修正窄屏单列布局 限制长阶段提示、澄清卡片和文件导入结果高度,保证输入区可达 补齐实时正文和思考的滚动跟随,保留用户上滚意图 移除读取策划历史时补造的发送时间 补充布局与显示回归测试,同步技术方案和共享排障约定 Reviewed-on: http://genarrative-station/git/GenarrativeAI/Genarrative/pulls/460 --- apps/ai-game-creator-shell/src/App.tsx | 13 +- apps/ai-game-creator-shell/src/styles.css | 57 ++++++- .../planning/PlanningChatView.tsx | 8 +- .../tests/appSurface/design-agent.suite.ts | 142 ++++++++++++++++++ .../tests/chatDialogFrameLayout.test.ts | 135 +++++++++++++++++ docs/project-memory/shared-memory/pitfalls.md | 4 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 19 +++ 7 files changed, 365 insertions(+), 13 deletions(-) diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 2403d3913..9a4989b99 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -513,6 +513,7 @@ export function App({ texts.push(entry.text); reasoningByMessageId.set(entry.messageId, texts); } + // 持久策划消息没有发送时间,不能把读取时刻显示成历史发送时间。 const messages: ChatMessage[] = view.messages .filter((message) => message.text.trim()) .map((message) => ({ @@ -521,7 +522,6 @@ export function App({ runtimeOwned: true, messageId: message.id, reasoningText: reasoningByMessageId.get(message.id)?.join('\n\n'), - updatedAt: Date.now(), })); const initialPrompt = initialPlanningPromptLatchRef.current.prompt; if ( @@ -534,7 +534,6 @@ export function App({ role: 'user', text: initialPrompt, runtimeOwned: true, - updatedAt: Date.now(), }); } return messages; @@ -872,7 +871,15 @@ export function App({ if (messageList) { messageList.scrollTop = messageList.scrollHeight; } - }, [messages, projectChatError]); + }, [ + messages, + projectChatError, + designAgentTransientReply, + designAgentReasoning, + designAgentView, + pendingUiConfirmation, + chatFileImportNotice, + ]); useEffect(() => { latestMessagesRef.current = messages; diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 4a3f31494..3b62e9118 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -10121,8 +10121,8 @@ button.design-workspace-tree__entry:hover, justify-self: start; } -/* 策划聊天区包含阶段控制卡、消息、Runtime 状态和输入框。GameAgent 资源工作台的 - 消息列表默认占满整个聊天区,策划模式需要单独恢复五行布局,避免输入框被推到视口外。 */ +/* 阶段、状态条和可选待办按内容排布,只有消息列表吸收剩余高度。 + 不按子节点序号分配 grid 行,避免状态条或待办占用消息的伸缩空间。 */ /* 策划工作台保留标题行,避免共用跨行规则将标题挤到底部。 */ .game-workbench-layout--design .game-workbench-chat { grid-template-rows: auto minmax(0, 1fr); @@ -10142,13 +10142,24 @@ button.design-workspace-tree__entry:hover, } .game-workbench-layout--design .project-chat-conversation { - display: grid; - grid-template-rows: auto minmax(0, 1fr) auto auto auto; + display: flex; + flex-direction: column; height: 100%; min-height: 0; overflow: visible; } +.game-workbench-layout--design .project-chat-conversation > * { + flex: 0 0 auto; + min-width: 0; +} + +.game-workbench-layout--design + .project-chat-conversation + > .project-chat-message-list { + flex: 1 1 0; +} + .game-workbench-layout--design .project-chat-message-list { height: auto; min-height: 0; @@ -10156,6 +10167,16 @@ button.design-workspace-tree__entry:hover, padding-bottom: 12px; } +.game-workbench-layout--design .design-agent-controls, +.game-workbench-layout--design .design-agent-pending-actions, +.game-workbench-layout--design .pending-command { + flex-shrink: 1; + min-height: 0; + max-height: 30%; + overflow: auto; + overflow-wrap: anywhere; +} + .game-workbench-layout--design .project-chat-composer { min-height: 0; grid-template-columns: minmax(0, 1fr); @@ -10176,13 +10197,24 @@ button.design-workspace-tree__entry:hover, } @media (max-width: 760px) { - .game-workbench-layout--design { - height: auto; + .game-project-workbench--design { + overflow-x: hidden; + overflow-y: auto; + } + + /* 两块面板在固定外壳内滚动;覆盖外壳的 height: 100%,避免第二行被裁切。 */ + .game-project-workbench--design .game-workbench-layout--design { + height: auto; + grid-template-columns: minmax(0, 1fr); + } + + .game-workbench-layout--design .game-workbench-stage { + height: 560px; } - .game-workbench-layout--design .game-workbench-stage, .game-workbench-layout--design .game-workbench-chat { min-height: 560px; + height: clamp(560px, calc(100dvh - 154px), 900px); } .game-workbench-layout--design .project-chat-surface, @@ -11019,6 +11051,12 @@ button.design-workspace-tree__entry:hover, font-variant-numeric: tabular-nums; } +.game-workbench-layout--design .project-chat-composer-notice { + max-height: 96px; + overflow: auto; + overflow-wrap: anywhere; +} + .game-workbench-layout--design .project-chat-composer-notice, .game-workbench-chat .project-chat-surface.is-direct-codex @@ -11253,6 +11291,7 @@ button.design-workspace-tree__entry:hover, /* 顶部极简条:44px 高、无独立背景块,只有一条极细分隔线。 左右内缩与消息内容 / 输入盒统一为 16px(面板自身 padding 已归 0)。 */ +.game-workbench-layout--design .project-chat-topbar, .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-topbar { @@ -11267,6 +11306,7 @@ button.design-workspace-tree__entry:hover, background: transparent; } +.game-workbench-layout--design .project-chat-topbar-status, .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-topbar-status { @@ -11279,6 +11319,7 @@ button.design-workspace-tree__entry:hover, line-height: 1.5; } +.game-workbench-layout--design .project-chat-topbar-status > span:last-child, .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-topbar-status @@ -11287,6 +11328,7 @@ button.design-workspace-tree__entry:hover, overflow-wrap: anywhere; } +.game-workbench-layout--design .project-chat-topbar-dot, .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-topbar-dot { @@ -11298,6 +11340,7 @@ button.design-workspace-tree__entry:hover, background: var(--platform-neutral-bg); } +.game-workbench-layout--design .project-chat-topbar-dot.is-busy, .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-topbar-dot.is-busy { diff --git a/apps/ai-game-creator-shell/src/view/project-development/planning/PlanningChatView.tsx b/apps/ai-game-creator-shell/src/view/project-development/planning/PlanningChatView.tsx index 81f8fe86d..4c08ac303 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/planning/PlanningChatView.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/planning/PlanningChatView.tsx @@ -199,9 +199,11 @@ export function PlanningChatView({ className={`project-chat-topbar-dot${controlBusy ? ' is-busy' : ''}`} aria-hidden="true" /> - {controlBusy - ? '策划 Agent 正在处理' - : workspaceStatusForDisplay(workspaceStatus)} + + {controlBusy + ? '策划 Agent 正在处理' + : workspaceStatusForDisplay(workspaceStatus)} +
{ + const harness = createProjectChatRuntimeHarness({ + designAgentView: designConversationView(), + designAgentContinueView: designConversationView(), + }); + renderDesignAgent(harness); + + const input = await screen.findByLabelText('项目需求'); + await setComposerText(input, '请继续完善方案'); + fireEvent.submit(input.closest('form') as HTMLFormElement); + await waitFor(() => + expect(harness.invoke).toHaveBeenCalledWith( + 'continue_design_agent_session', + expect.objectContaining({ + input: { type: 'message', text: '请继续完善方案' }, + }), + ), + ); + const continueCall = [...harness.invoke.mock.calls] + .reverse() + .find(([command]) => command === 'continue_design_agent_session'); + const clientTurnId = String( + (continueCall?.[1] as { clientTurnId?: string }).clientTurnId, + ); + const messageList = screen.getByLabelText('立项策划消息'); + Object.defineProperty(messageList, 'scrollHeight', { + configurable: true, + value: 1000, + }); + Object.defineProperty(messageList, 'clientHeight', { + configurable: true, + value: 200, + }); + + messageList.scrollTop = 800; + fireEvent.scroll(messageList); + act(() => + harness.emitDesignAgentEvent({ + projectPath: harness.projectPath, + clientTurnId, + kind: 'reasoning', + reasoningText: '先核对玩法循环', + }), + ); + await screen.findAllByText('先核对玩法循环'); + await waitFor(() => expect(messageList.scrollTop).toBe(1000)); + + messageList.scrollTop = 800; + fireEvent.scroll(messageList); + act(() => + harness.emitDesignAgentEvent({ + projectPath: harness.projectPath, + clientTurnId, + kind: 'text', + text: '正在补充关卡节奏', + }), + ); + await screen.findByLabelText('策划 Agent 实时回复'); + await waitFor(() => expect(messageList.scrollTop).toBe(1000)); + + messageList.scrollTop = 120; + fireEvent.scroll(messageList); + act(() => { + harness.emitDesignAgentEvent({ + projectPath: harness.projectPath, + clientTurnId, + kind: 'reasoning', + reasoningText: '继续检查多人规则', + }); + harness.emitDesignAgentEvent({ + projectPath: harness.projectPath, + clientTurnId, + kind: 'text', + text: '正在补充关卡节奏与多人规则', + }); + }); + await screen.findAllByText('继续检查多人规则'); + await waitFor(() => + expect( + screen.getByLabelText('策划 Agent 实时回复').textContent, + ).toContain('多人规则'), + ); + expect(messageList.scrollTop).toBe(120); + }); + + it('shows only known optimistic send times and does not invent persisted times', async () => { + const hydratedView = { + ...designConversationView(), + messages: [{ id: 'user-history', role: 'user', text: '历史策划需求' }], + }; + const continuedView = { + ...designConversationView(), + messages: [ + ...hydratedView.messages, + { id: 'user-current', role: 'user', text: '当前补充需求' }, + ], + }; + const harness = createProjectChatRuntimeHarness({ + designAgentView: hydratedView, + designAgentContinueView: continuedView, + }); + const originalInvoke = harness.invoke.getMockImplementation()!; + let finishContinue!: () => void; + const continueGate = new Promise((resolve) => { + finishContinue = resolve; + }); + harness.invoke.mockImplementation(async (command, args) => { + if (command === 'continue_design_agent_session') { + await continueGate; + } + return originalInvoke(command, args); + }); + renderDesignAgent(harness); + + const historicalText = await screen.findByText('历史策划需求'); + expect( + historicalText.closest('.message')?.querySelector('time'), + ).toBeNull(); + + const input = screen.getByLabelText('项目需求'); + await setComposerText(input, '当前补充需求'); + fireEvent.submit(input.closest('form') as HTMLFormElement); + const optimisticText = await screen.findByText('当前补充需求'); + const optimisticTime = optimisticText + .closest('.message') + ?.querySelector('time'); + expect(optimisticTime).not.toBeNull(); + expect(optimisticTime?.getAttribute('dateTime')).toBeTruthy(); + + await act(async () => finishContinue()); + await waitFor(() => { + const currentText = screen.getByText('当前补充需求'); + expect(currentText.closest('.message')?.querySelector('time')).toBeNull(); + }); + expect( + screen + .getByText('历史策划需求') + .closest('.message') + ?.querySelector('time'), + ).toBeNull(); + }); + it('hydrates an existing design session and decides approval through design commands', async () => { const harness = createProjectChatRuntimeHarness({ designAgentView: designApprovalView(), diff --git a/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts b/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts index c185710a3..20a1ff525 100644 --- a/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts +++ b/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts @@ -416,3 +416,138 @@ describe('陶泥儿对话区:Codex 三段式(顶栏 / 唯一滚动区 / 文 expect(pixelValue(listInPanelDirectCodex, 'min-height')).toBe(0); }); }); + +describe('策划对话:消息伸缩与长内容边界', () => { + function planningDeclarations(className: string, width: number) { + const root = document.createElement('div'); + root.innerHTML = `
+
+
+
+
+
+
+
`; + const element = root.querySelector(className)!; + // 从 DOM 匹配完整选择器,防止遗漏基础规则或误把 Direct 专用样式算到策划入口。 + const selectors = rules.flatMap((rule) => + rule.selectors.filter((selector) => { + try { + return element.matches(selector); + } catch { + // jsdom 不支持的浏览器伪类与本面板无关。 + return false; + } + }), + ); + return resolveDeclarations(rules, selectors, width); + } + + it.each([1440, 390])( + '%ipx:消息吸收剩余高度,状态和输入区不参与拉伸', + (width) => { + const conversation = planningDeclarations( + '.project-chat-conversation', + width, + ); + const list = planningDeclarations('.project-chat-message-list', width); + const composer = planningDeclarations('.project-chat-composer', width); + const topbar = planningDeclarations('.project-chat-topbar', width); + expect(declaration(conversation, 'display')).toBe('flex'); + expect(declaration(conversation, 'flex-direction')).toBe('column'); + expect(declaration(conversation, 'height')).toBe('100%'); + expect(declaration(conversation, 'overflow')).toBe('visible'); + expect(declaration(list, 'flex')).toBe('1 1 0'); + expect(declaration(list, 'min-height')).toBe('0'); + expect(declaration(list, 'overflow-y')).toBe('auto'); + expect(declaration(composer, 'flex')).toBe('0 0 auto'); + expect(declaration(topbar, 'flex')).toBe('0 0 auto'); + expect(declaration(topbar, 'min-height')).toBe('44px'); + expect( + declaration( + planningDeclarations('.project-chat-topbar-status', width), + 'font-size', + ), + ).toBe('12px'); + expect( + declaration( + planningDeclarations( + '.project-chat-topbar-status > span:last-child', + width, + ), + 'overflow-wrap', + ), + ).toBe('anywhere'); + for (const selector of [ + '.design-agent-controls', + '.design-agent-pending-actions', + ]) { + const panel = planningDeclarations(selector, width); + expect(declaration(panel, 'max-height')).toBe('30%'); + expect(declaration(panel, 'min-height')).toBe('0'); + expect(declaration(panel, 'overflow')).toBe('auto'); + } + }, + ); + + it.each([390, 760])( + '%ipx:固定外壳内工作台可滚动到对话,两块面板保持明确高度', + (width) => { + const workbench = planningDeclarations('.game-project-workbench', width); + const layout = planningDeclarations('.game-workbench-layout', width); + const stage = planningDeclarations('.game-workbench-stage', width); + const chat = planningDeclarations('.game-workbench-chat', width); + expect(declaration(workbench, 'min-height')).toBe('0'); + expect(declaration(workbench, 'overflow-y')).toBe('auto'); + expect(declaration(workbench, 'overflow-x')).toBe('hidden'); + expect(declaration(layout, 'height')).toBe('auto'); + expect(declaration(layout, 'grid-template-columns')).toBe( + 'minmax(0, 1fr)', + ); + expect(declaration(stage, 'height')).toBe('560px'); + expect(declaration(chat, 'height')).toBe( + 'clamp(560px, calc(100dvh - 154px), 900px)', + ); + expect( + declaration( + planningDeclarations('.design-workspace-panel', width), + 'height', + ), + ).toBe('100%'); + expect( + declaration( + planningDeclarations('.design-workspace-tree', width), + 'overflow', + ), + ).toBe('auto'); + }, + ); + + it.each([761, 1280])('%ipx:双栏继续填满外壳剩余高度', (width) => { + const workbench = planningDeclarations('.game-project-workbench', width); + const layout = planningDeclarations('.game-workbench-layout', width); + expect(declaration(workbench, 'overflow')).toBe('hidden'); + expect(workbench.has('overflow-y')).toBe(false); + expect(declaration(layout, 'height')).toBe('100%'); + expect(declaration(layout, 'grid-template-columns')).toBe( + 'minmax(0, 1fr) minmax(380px, 0.42fr)', + ); + }); + + it('批量导入失败提示可滚动且可换行,不撑高输入区或撑宽面板', () => { + const notice = planningDeclarations('.project-chat-composer-notice', 390); + expect(declaration(notice, 'max-height')).toBe('96px'); + expect(declaration(notice, 'overflow')).toBe('auto'); + expect(declaration(notice, 'overflow-wrap')).toBe('anywhere'); + }); +}); diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index b229b2883..9a7496644 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -5943,6 +5943,10 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` `append_application_log_line` 在落盘前对整行做 `sanitize_diagnostic_message`:行内只要出现 `token=`、`bearer `、`authorization`、`credential`、`api key` / `apikey` / `api_key` 这类标记,**整行**就被换成 ``,只留下时间戳与 `RUST module:` 前缀;同时每行还会被截到 2048 字符。于是把“身份字段 + 诊断正文”拼成一行 `app_log!` 时,正文里一个凭据词就可能让整条记录连 `eventId`、`code` 一起消失(2026-09-21 加统一错误事件的日志投影时按两行落:身份行只放程序生成与调用方常量字段,summary / hint / detail 等自由文本一律只放详情行,且自由文本先自行压平换行——裸词标记脱敏消不掉,自由文本放错行会把 eventId、code 一起带走)。 +## 策划聊天不能按可选子节点序号分配消息高度 + +策划与开发 Agent 共用聊天类名,但布局合同不同。策划新增状态条后,按第二个子节点分配 `1fr` 会把空白给状态条、让消息框随回复增长;只给 `.is-direct-codex` 的状态样式也不会覆盖策划。策划使用纵向 Flex,仅消息列表伸缩,阶段/待处理区域限高滚动;改共用样式时同时核对两种入口。jsdom 交互通过不代表布局正确,须匹配完整 CSS 层叠并用真实浏览器核对空/短/长消息与长待办。窄屏上下堆叠必须在固定外壳内提供工作台滚动容器,两块面板明确限高;低优先级的 `height: auto` 不能覆盖外壳后代规则的 `height: 100%`。布局夹具必须包含窗口外壳与启动器层级,并实际滚动验证输入框可见可操作,不能只检查输入框在聊天面板内部。实时正文/思考不更新持久消息数组,滚动跟随必须覆盖这些独立状态并保留用户上滚门禁;历史消息缺失的时间不得用读取时刻填补。详见 AGC 实施计划的“策划 Agent 对话显示与滚动合同”。 + ## 2026-09-22 Rust 对象快照必须对齐 CI 编译目录和 Cargo 环境 - 现象:sccache 快照已包含数百 MiB 对象,但全新 target 的“热缓存”仍然全部 miss,甚至比直接 rustc 更慢。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 8613a787b..724656800 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -241,6 +241,25 @@ Rust 侧在 `server-rs/crates/shared-contracts` 维护唯一权威 `GameCreation Rust 分片日志在失败时输出有界 stdout 尾部中的失败段,保留 panic 位置、断言详情和 Rust 汇总;selected 只表示该分片选中的用例数量。成功分片保留简短摘要。定向验证使用相关 Rust 用例与 `node --test apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.test.mjs`。 +## 策划 Agent 对话显示与滚动合同 + +策划工作台的对话区保留阶段状态、工作区状态、消息、可选确认/澄清和输入区。面板高度由工作台和视口决定;空消息、短回复和流式长回复不能改变消息区域的上下边界。阶段、状态条、输入区按内容排列,消息区独占剩余空间并内部滚动;待处理卡片出现时只让出其实际所需且有上限的空间,不依赖可选节点的序号分配伸缩高度。 + +- 工作区状态条复用现有聊天状态条的字号、间距和状态点。长项目名允许换行,不能撑宽面板;长错误、澄清和批量文件导入结果在受限区域内完整可读,不能挤走输入框。输入编辑器保留原有高度上限,模型/推理菜单继续允许弹出面板。 +- 已在消息底部时,实时正文与思考增高后继续跟随;用户主动上滚阅读历史后不自动抢回底部,发送新消息后恢复跟随。阶段/待处理卡片改变消息可用高度时,仍遵守同一跟随意图。滚动仅为临时 UI 状态,不写入正式会话。 +- 历史消息只有收到真实发送时间才能显示时间。现有策划持久消息不含逐消息时间,读取或刷新时保持未知,不以当前时刻补造。当前会话刚提交的乐观消息可以显示已知发送时间;正式快照覆盖后不补造或猜配旧消息时间。 +- ≤760px 时资源区与对话区单列排列,策划工作台在固定外壳内纵向滚动,用户向下滚动可到达输入区。布局使用内容高度,并以同等或更高选择器优先级覆盖外壳的 `height: 100%`;资源区明确为 560px,对话区高度为 `clamp(560px, calc(100dvh - 154px), 900px)`。文件树与消息列表各自内部滚动,长内容不增加两块面板高度。>760px 继续共用外壳剩余高度,不启用工作台整体滚动。正式主窗最小宽度不变,窄屏验收覆盖浏览器响应式布局。 +- 审批、澄清、导入期间禁用、错误重试、项目归属和发送权限沿用现有行为。此次调整不改变 Runtime、API、持久协议或数据库,无数据迁移;不重做开发 Agent 的对话布局。 + +验收使用策划组件与现有界面测试覆盖流式/历史跟随、未知时间、审批澄清和导入禁用;使用真实浏览器与当前样式核对桌面/窄屏的空、短、长消息,长项目名、长错误与长澄清,以及输入框和菜单位置。布局验收必须包含 `window-chrome → window-chrome__content → launcher-shell → launcher-main → game-project-workbench` 的完整外壳层级,覆盖 390px、760px、761px 和桌面尺寸;窄屏实际滚动工作台后,输入框须进入可视范围且可操作,长文件树、长消息和待确认内容不能使面板失去高度边界。不能只断言输入框位于聊天面板内部。开发 Agent 的既有布局回归同批执行。真实 Provider 请求不作为纯展示修复的前置条件,验证记录须区分模拟事件与真实 Provider。 + +| 验收面 | 对应证据 | +| --- | --- | +| 策划与开发 Agent 布局隔离、伸缩与窄屏滚动可达 | `chatDialogFrameLayout.test.ts` 包含外壳的选择器层叠回归;Chrome 实际组件、完整外壳与完整样式的 390px/760px/761px/桌面几何、滚动和输入操作检查 | +| 实时正文/思考跟随、上滚停留、发送恢复 | `appSurface/design-agent.suite.ts` 的实时事件与发送恢复用例;还原旧滚动依赖时新增用例失败 | +| 历史未知时间与乐观发送时间 | 同套界面测试覆盖读取、乐观发送和正式快照覆盖;恢复 `Date.now()` 补造时新增用例失败 | +| 审批/澄清、文件导入禁用及共用控件 | `appSurface.test.ts` 现有交互回归与 Chrome 模型菜单位置检查;不改变原生调用和业务状态 | + ## 策划 Agent 批量局部修改 `patch_file` 的所有 edits 均匹配同一份原文件,参数顺序不影响结果。完成唯一匹配与不重叠校验后,按原文起点升序拼接未修改片段与替换文本,最后一次性写入;任一校验失败时不写文件。回归用例覆盖乱序 edits、中文内容与替换长度增减,并核对完整落盘内容。此行为仅属于策划 Agent 文件工具。