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 文件工具。