修复策划对话布局与流式显示
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m22s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m49s
Project CI / Backend tests (pull_request) Successful in 4m58s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m0s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m3s
Project CI / Native shell tests (pull_request) Successful in 6m44s
Project CI / Frontend tests (pull_request) Successful in 3m18s
Project CI / Repository checks (pull_request) Successful in 3m31s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m40s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m22s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m49s
Project CI / Backend tests (pull_request) Successful in 4m58s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m0s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m3s
Project CI / Native shell tests (pull_request) Successful in 6m44s
Project CI / Frontend tests (pull_request) Successful in 3m18s
Project CI / Repository checks (pull_request) Successful in 3m31s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m40s
恢复固定消息区域和工作区状态条样式,修正窄屏单列布局 限制长阶段提示、澄清卡片和文件导入结果高度,保证输入区可达 补齐实时正文和思考的滚动跟随,保留用户上滚意图 移除读取策划历史时补造的发送时间 补充布局与显示回归测试,同步技术方案和共享排障约定
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -10178,6 +10199,7 @@ button.design-workspace-tree__entry:hover,
|
||||
@media (max-width: 760px) {
|
||||
.game-workbench-layout--design {
|
||||
height: auto;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.game-workbench-layout--design .game-workbench-stage,
|
||||
@@ -10185,6 +10207,10 @@ button.design-workspace-tree__entry:hover,
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.game-workbench-layout--design .game-workbench-chat {
|
||||
height: clamp(560px, calc(100dvh - 154px), 900px);
|
||||
}
|
||||
|
||||
.game-workbench-layout--design .project-chat-surface,
|
||||
.game-workbench-layout--design .project-chat-conversation {
|
||||
height: 100%;
|
||||
@@ -11019,6 +11045,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 +11285,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 +11300,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 +11313,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 +11322,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 +11334,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 {
|
||||
|
||||
+5
-3
@@ -199,9 +199,11 @@ export function PlanningChatView({
|
||||
className={`project-chat-topbar-dot${controlBusy ? ' is-busy' : ''}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{controlBusy
|
||||
? '策划 Agent 正在处理'
|
||||
: workspaceStatusForDisplay(workspaceStatus)}
|
||||
<span>
|
||||
{controlBusy
|
||||
? '策划 Agent 正在处理'
|
||||
: workspaceStatusForDisplay(workspaceStatus)}
|
||||
</span>
|
||||
</span>
|
||||
</header>
|
||||
<div
|
||||
|
||||
@@ -405,6 +405,148 @@ export function registerDesignAgentSurfaceTests() {
|
||||
);
|
||||
});
|
||||
|
||||
it('follows streamed Design Agent content until the user scrolls up', async () => {
|
||||
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<void>((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(),
|
||||
|
||||
@@ -416,3 +416,97 @@ describe('陶泥儿对话区:Codex 三段式(顶栏 / 唯一滚动区 / 文
|
||||
expect(pixelValue(listInPanelDirectCodex, 'min-height')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('策划对话:消息伸缩与长内容边界', () => {
|
||||
function planningDeclarations(className: string, width: number) {
|
||||
const root = document.createElement('div');
|
||||
root.innerHTML = `<div class="game-workbench-layout game-workbench-layout--design">
|
||||
<aside class="game-workbench-chat"><header>策划</header>
|
||||
<section class="project-chat-surface"><div class="project-chat-conversation">
|
||||
<section class="design-agent-controls"></section>
|
||||
<header class="project-chat-topbar"><span class="project-chat-topbar-status">
|
||||
<span class="project-chat-topbar-dot"></span><span>已打开:项目</span>
|
||||
</span></header>
|
||||
<div class="message-list project-chat-message-list"></div>
|
||||
<section class="design-agent-pending-actions"></section>
|
||||
<form class="project-chat-composer"><p class="project-chat-composer-notice"></p></form>
|
||||
</div></section>
|
||||
</aside></div>`;
|
||||
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('窄屏工作台为单列,对话仍有明确高度而非随历史撑长', () => {
|
||||
const layout = planningDeclarations('.game-workbench-layout', 390);
|
||||
const chat = planningDeclarations('.game-workbench-chat', 390);
|
||||
expect(declaration(layout, 'grid-template-columns')).toBe('minmax(0, 1fr)');
|
||||
expect(declaration(chat, 'height')).toBe(
|
||||
'clamp(560px, calc(100dvh - 154px), 900px)',
|
||||
);
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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` 这类标记,**整行**就被换成 `<sensitive diagnostic details redacted>`,只留下时间戳与 `RUST module:` 前缀;同时每行还会被截到 2048 字符。于是把“身份字段 + 诊断正文”拼成一行 `app_log!` 时,正文里一个凭据词就可能让整条记录连 `eventId`、`code` 一起消失(2026-09-21 加统一错误事件的日志投影时按两行落:身份行只放程序生成与调用方常量字段,summary / hint / detail 等自由文本一律只放详情行,且自由文本先自行压平换行——裸词标记脱敏消不掉,自由文本放错行会把 eventId、code 一起带走)。
|
||||
|
||||
## 策划聊天不能按可选子节点序号分配消息高度
|
||||
|
||||
策划与开发 Agent 共用聊天类名,但布局合同不同。策划新增状态条后,按第二个子节点分配 `1fr` 会把空白给状态条、让消息框随回复增长;只给 `.is-direct-codex` 的状态样式也不会覆盖策划。策划使用纵向 Flex,仅消息列表伸缩,阶段/待处理区域限高滚动;改共用样式时同时核对两种入口。jsdom 交互通过不代表布局正确,须匹配完整 CSS 层叠并用真实浏览器核对空/短/长消息与长待办。实时正文/思考不更新持久消息数组,滚动跟随必须覆盖这些独立状态并保留用户上滚门禁;历史消息缺失的时间不得用读取时刻填补。详见 AGC 实施计划的“策划 Agent 对话显示与滚动合同”。
|
||||
|
||||
## 2026-09-22 Rust 对象快照必须对齐 CI 编译目录和 Cargo 环境
|
||||
|
||||
- 现象:sccache 快照已包含数百 MiB 对象,但全新 target 的“热缓存”仍然全部 miss,甚至比直接 rustc 更慢。
|
||||
|
||||
@@ -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 状态,不写入正式会话。
|
||||
- 历史消息只有收到真实发送时间才能显示时间。现有策划持久消息不含逐消息时间,读取或刷新时保持未知,不以当前时刻补造。当前会话刚提交的乐观消息可以显示已知发送时间;正式快照覆盖后不补造或猜配旧消息时间。
|
||||
- 窄屏下资源区与对话区单列排列,对话区仍保持受限高度,长对话不撑长整页。正式主窗最小宽度不变,窄屏验收覆盖浏览器响应式布局。
|
||||
- 审批、澄清、导入期间禁用、错误重试、项目归属和发送权限沿用现有行为。此次调整不改变 Runtime、API、持久协议或数据库,无数据迁移;不重做开发 Agent 的对话布局。
|
||||
|
||||
验收使用策划组件与现有界面测试覆盖流式/历史跟随、未知时间、审批澄清和导入禁用;使用真实浏览器与当前样式核对桌面/窄屏的空、短、长消息,长项目名、长错误与长澄清,以及输入框和菜单位置。开发 Agent 的既有布局回归同批执行。真实 Provider 请求不作为纯展示修复的前置条件,验证记录须区分模拟事件与真实 Provider。
|
||||
|
||||
| 验收面 | 对应证据 |
|
||||
| --- | --- |
|
||||
| 策划与开发 Agent 布局隔离、伸缩与窄屏单列 | `chatDialogFrameLayout.test.ts` 的完整选择器层叠回归;Chrome 实际组件与完整样式的桌面/390px 几何和截图检查 |
|
||||
| 实时正文/思考跟随、上滚停留、发送恢复 | `appSurface/design-agent.suite.ts` 的实时事件与发送恢复用例;还原旧滚动依赖时新增用例失败 |
|
||||
| 历史未知时间与乐观发送时间 | 同套界面测试覆盖读取、乐观发送和正式快照覆盖;恢复 `Date.now()` 补造时新增用例失败 |
|
||||
| 审批/澄清、文件导入禁用及共用控件 | `appSurface.test.ts` 现有交互回归与 Chrome 模型菜单位置检查;不改变原生调用和业务状态 |
|
||||
|
||||
## 策划 Agent 批量局部修改
|
||||
|
||||
`patch_file` 的所有 edits 均匹配同一份原文件,参数顺序不影响结果。完成唯一匹配与不重叠校验后,按原文起点升序拼接未修改片段与替换文本,最后一次性写入;任一校验失败时不写文件。回归用例覆盖乱序 edits、中文内容与替换长度增减,并核对完整落盘内容。此行为仅属于策划 Agent 文件工具。
|
||||
|
||||
Reference in New Issue
Block a user