Skip to content

restyle chat ui - #622

Open
Loosand wants to merge 52 commits into
Stack-Cairn:mainfrom
Loosand:codex/chat-ui-gallery
Open

restyle chat ui#622
Loosand wants to merge 52 commits into
Stack-Cairn:mainfrom
Loosand:codex/chat-ui-gallery

Conversation

@Loosand

@Loosand Loosand commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

wait

Loosand and others added 15 commits August 19, 2026 15:50
- Introduced macOS DMG settings in tauri.conf.json, including custom background image and window/app positioning.
- Added a new background image for the DMG installer.
…tion scripts

- Added macOS DMG settings to the Makefile, including background image and volume icon configurations.
- Introduced a new Python script for deterministic DMG layout, ensuring stable Finder metadata.
- Created a verification script to check the integrity of the generated DMG, ensuring required files and symlinks are present.
- Updated GitHub Actions workflow to install the necessary dmgbuild tool for building DMGs.
…ffTable, FilterTable, FineTuneCard, Flowchat, and LoadingState
…unctionality

- Updated CSS for chat gallery to enhance overflow handling and layout consistency.
- Modified `ComposerGallery` to support additional props and improve state management.
- Introduced new `LIVEAGENT_OVERVIEW_MESSAGES` for project overview in fixtures.
- Added scenario for `liveagent-overview` to enhance conversation context.
- Integrated `GalleryComposerHarness` into `TranscriptGallery` for better composer handling.
- Improved Markdown styling for better readability and consistency across components.
- Introduced a new TaskRows component to display task progress with animated status indicators.
- Implemented SpinnerRing and Badge components for visual feedback on task states.
- Enhanced task detail display with expandable rows and dynamic status updates.
- Updated tests to validate the new task progress semantics and UI behavior.
- Adjusted existing test cases to accommodate the new structure and ensure consistency.
…tus tests

- Changed the opacity class assertion in AskUserQuestionCard test from "opacity-55" to "opacity-50".
- Refactored AssistantStatus test to simplify the rendering check, ensuring it validates the text-only label without the Loader2 component.
- Adjusted imports and removed unused components in AssistantStatus for cleaner code.
@Loosand Loosand changed the title Codex/chat UI gallery chat ui Aug 25, 2026
@StackCairn
StackCairn marked this pull request as draft August 25, 2026 15:35
@github-actions

Copy link
Copy Markdown
Contributor

PR governance checks failed — this PR has been converted to draft.

  • No linked issue: the PR body must contain Closes #123 / Fixes #123 / Resolves #123. This project requires an issue before a PR — see the contribution guidelines.
  • UI change without screenshots: this PR modifies frontend code. Please add before/after screenshots or a recording under "Screenshots / preview" in the PR body.

Fix the items above, then click Ready for review to re-run the checks.

@Loosand Loosand changed the title chat ui restyle chat ui Aug 25, 2026
Loosand added 11 commits August 25, 2026 23:44
…ompact chip design

- Added a `compactChip` prop to `ToolCallItem` for a more streamlined display option.
- Updated the layout and styling of `ToolCallItem` to support compact chip rendering, improving visual consistency.
- Refactored `ToolTraceGroup` to utilize the new `compactChip` feature, enhancing the overall user interface.
- Adjusted component structure for better responsiveness and usability in chat interactions.
- Introduced a new `resultContent` variable to streamline the rendering of tool results based on various conditions.
- Enhanced error handling and readability for tool results, ensuring errors are displayed prominently.
- Adjusted layout and styling for better integration with the `compactChip` design, improving overall user experience.
- Simplified conditional rendering to improve code clarity and maintainability.
…atus handling

- Removed the LiveStatusFooter component to simplify the rendering logic for live statuses.
- Updated GatewayTranscriptListRegion to directly manage tool status and duration calculations.
- Refactored test cases to align with the new structure and ensure accurate assertions for live status rendering.
- Improved the AssistantBubble component to utilize the new AssistantTurnContent for better organization of round data.
- Enhanced the AssistantWorkTrace component to display elapsed time for running tasks, improving user feedback during live interactions.
…d ToolTraceGroup components

- Introduced user interaction tracking to prevent automatic expansion/collapse of components based on running state until user interaction occurs.
- Updated state management logic in both AssistantWorkTrace and ToolTraceGroup to improve user experience during live interactions.
- Ensured that component states are only modified after user actions, enhancing control over UI behavior.
… layout

- Introduced new tests for the AssistantWorkTrace and ToolTraceGroup components to ensure proper behavior during state transitions.
- Added tests for the mention popup layout to verify that the popup list remains compact and aligns file names with icons.
- Enhanced the ChatComposerBar component to conditionally display the expand toggle based on editor overflow state.
…ionality

- Updated TaskProgressIndicator to include a panel toggle for task details, improving user interaction.
- Refactored tests to validate the new panel toggle behavior, ensuring it correctly reflects the expanded/collapsed state.
- Adjusted rendering logic to manage task display states more effectively, enhancing overall user experience during task progress visualization.
…functionality

- Added a new ToolCategoryGallery component to showcase various tool categories and their functionalities.
- Updated ChatGalleryPage and ChatGalleryPreview to include the new tooling category, enhancing the overall chat gallery experience.
- Introduced new scenarios for tool categories in the scenarios.ts file, allowing for better organization and access to tool-related activities.
- Enhanced AssistantBubble and AssistantWorkTrace components to improve user interaction and display of tool activities.
- Added tests to ensure proper functionality of the new tool category features and their integration within the chat gallery.
…nality

- Refactored `AssistantBubbleUnit` and `AssistantActivityRow` to remove `isAgentMode` prop, streamlining the component structure.
- Enhanced `AssistantWorkTrace` to utilize new utilities for managing work entries and thinking states.
- Updated tests to reflect changes in component behavior, ensuring accurate rendering and functionality.
- Introduced new tests for `ThinkingDisclosure` and `thinking-trace` to validate reasoning and live status features.
- Removed obsolete `ThinkingActivity` tests and related components to clean up the codebase.
- Introduced `awaitingDecision` state to manage user interactions that require approval, freezing progress indicators during this state.
- Updated `LiveSparkle` and `AssistantWorkTrace` components to reflect the new state, ensuring accurate visual feedback when awaiting user decisions.
- Enhanced CSS styles for better visibility of action rows based on pointer capabilities, improving user experience across devices.
- Added tests to validate the new behavior of components under the awaiting decision state, ensuring robustness in rendering and functionality.
- Improved translations for user-facing messages related to decision waiting states in both English and Chinese.
- Adjusted test assertions in `thinking-trace.test.mjs` and `live-status-width.test.mjs` to reflect changes in the `LiveSparkle` component and `AssistantWorkTrace` class names.
- Modified spacing in `AssistantBubbleUnit` and `RoundBlockContent` to ensure consistent vertical rhythm in the UI.
- Enhanced `LazyCollapse` and `ToolTraceGroup` components to improve layout behavior and prevent unnecessary gaps.
- Updated test descriptions for clarity and accuracy regarding the expected layout behavior.
…ents

- Introduced new tests in `live-status-width.test.mjs` to validate the layout of the `LiveSparkle`, `AssistantWorkTrace`, `ToolTraceGroup`, and `ToolCallItem` components.
- Updated the `AssistantWorkTrace` and `LiveSparkle` components to ensure proper alignment and spacing of icons within their respective layouts.
- Enhanced the `ToolTraceGroup` and `ToolCallItem` components to maintain consistent visual alignment with the overall chat interface.
- Improved test descriptions for clarity regarding the expected layout behavior of the components.
BingZi-233

This comment was marked as low quality.

su-fen and others added 6 commits September 2, 2026 01:42
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	crates/agent-ui/src/components/IconSet.tsx
#	crates/agent-ui/src/pages/chat/ChatComposerBar.tsx
合并 main 时这两个文件被错误还原为合并前版本,丢失了
CLI identity 头相关导出,导致 GUI/Gateway WebUI tsc 失败。

Co-authored-by: Cursor <cursoragent@cursor.com>
任意数值圆角(rounded-[Npx])不通过 check:ui-boundaries:
4px→rounded-xs、10px→rounded-lg、24/25px→rounded-4xl,
4px 像素点上的 1px 圆角视觉无感知,直接移除。

Co-authored-by: Cursor <cursoragent@cursor.com>
AssistantBubble/ChatTranscript 自动合并产生的格式与 import
顺序偏移用 biome 修复;布局一致性测试中硬编码的
rounded-[25px]/[24px]/[4px] 断言同步改为标准 token。

Co-authored-by: Cursor <cursoragent@cursor.com>
@Loosand
Loosand marked this pull request as ready for review September 2, 2026 03:05
@StackCairn
StackCairn marked this pull request as draft September 2, 2026 03:05
@su-fen su-fen added the governance-exempt Skip PR governance checks label Sep 2, 2026
@su-fen
su-fen marked this pull request as ready for review September 2, 2026 05:02
时间戳原先是独立 chrome,可能一直显示;收进左侧三个按钮的同一容器,避免 hover 不同步。

Co-authored-by: Cursor <cursoragent@cursor.com>
@su-fen

su-fen commented Sep 2, 2026

Copy link
Copy Markdown
Member

fix: 模型回复时间戳现在与左侧三个操作按钮共用同一套 hover 显隐。

时间戳原先是独立的 hover chrome,和复制 / 重试 / 分支按钮的 opacity 可能脱节,于是出现「按钮会藏、时间一直亮着」的情况。现已把时钟收进按钮同一容器,并用纯颜色 token 避免 /70 修饰符覆盖父级 opacity: 0

提交:1f2ea45efix(chat): 模型回复时间戳与操作按钮共用 hover 显隐

悬浮面板的 ul 只设了 overflow-y-auto,overflow-x 随之变成 auto;无空格的长路径 /
URL 撑开行宽后整张气泡出现横向滚动条。

现在标题 line-clamp-2 + break-words,列表显式 overflow-x-hidden;所有行共用一个
tooltip(base-ui 分离式触发器 + payload),只有真正被截断的行才在 hover 时弹出完整
标题,完整可见的短行不打扰。指针从截断行直接滑进相邻完整行时顺手收掉旧弹层,避免
hover 逻辑把这次移动当作「换触发器」而让弹层卡住。

Co-authored-by: Cursor <cursoragent@cursor.com>
@su-fen

su-fen commented Sep 2, 2026

Copy link
Copy Markdown
Member

fix: 任务列表悬浮气泡里超长的任务标题现在两行截断,被截断的行 hover 时用 tooltip 展示完整标题。

气泡的 ul 只设了 overflow-y-autooverflow-x 随之变成 auto;无空格的长路径 / URL 撑开行宽后整张气泡出现横向滚动条。现已改为标题 line-clamp-2 + break-words,列表显式 overflow-x-hidden,横向滚动条不会再出现。

Tooltip 用 base-ui 的分离式触发器(createHandle + payload),所有行共用一个实例;打开前以 scrollHeight > clientHeight 判断该行是否真被截断,完整可见的短行不弹。弹层本身 pointer-events-none,盖住上一行时不会打断外层 hover 面板;指针从截断行直接滑进相邻完整行时也会顺手收掉旧弹层,避免 hover 逻辑把这次移动当作「换触发器」而让弹层卡住。

已在 dev server 上实测明暗两套主题(中文长句 / 无空格长路径 / 长英文句),并给 gui 与 webui 两份 indicator 测试补上截断、共享 handle、打开否决与 stale-close 的用例;tsc(ui / gui / webui)、biome、test:gui 2913 通过、test:webui 683 通过。

提交:a87b3446fix(chat): 任务列表气泡超长标题两行截断并用 tooltip 展示全文

WebUI 经共享 AssistantTurnContent 传了 collapseAfterAnswer,回合落定且有总结
文案时「处理中」区块折叠成一行「已处理 Ns」;GUI 为虚拟化拆行绕过了该组件、
自行拼装 AssistantWorkTrace,却没接这个 prop,加上 rowModel 零 remount 让
running 期间初始化的 expanded=true 一直保留,落定后区块始终展开。

work-trace 单元新增 hasAnswer(layout.answer 非空),纳入 canReuseLiveUnit
比较以便落定时刷新缓存单元;GUI 气泡据此传 collapseAfterAnswer。仅有工具
没有文案的回合不折叠——此时 trace 就是整条回复。

Co-authored-by: Cursor <cursoragent@cursor.com>
@su-fen

su-fen commented Sep 2, 2026

Copy link
Copy Markdown
Member

WebUI / GUI 同步核对

对本 PR 的 UI 改动做了一遍两端同步核对。绝大部分改动落在共享 crates/agent-ui,两端自动同步;但 GUI 为虚拟化拆行绕过了共享 AssistantTurnContent,在 agent-gui/src/pages/chat/components/AssistantBubble.tsx 自行拼装 AssistantWorkTrace,所以 RoundContent.tsx 里的接线改动不会自动到 GUI。

发现一处行为不一致,已在 c552424 修复:

  • collapseAfterAnswer(cc1bafe9 引入)只接在共享 RoundContent,GUI 没传。WebUI 回合落定且有总结文案时「处理中」区块折叠成一行「已处理 Ns」;GUI 因 rowModel 零 remount 让 running 期间初始化的 expanded=true 一直保留,落定后始终展开。
  • 修复:work-trace 单元新增 hasAnswerlayout.answer.length > 0,与 WebUI 同源),纳入 canReuseLiveUnit 比较;GUI 气泡传 collapseAfterAnswer={unit.hasAnswer}。仅有工具没有文案的回合不折叠。
  • 测试:transcript-row-model.test.mjs 覆盖 live / 落定有文案 / 落定仅工具三种取值;thinking-trace.test.mjs 把共享 RoundContent、GUI AssistantBubblerowModel 三处接线一起钉住。test/chat 1250 用例、tsc、biome 全过。

核对为等价实现、无需处理:

  • awaitingDecision 冻结指示器:WebUI 靠 readToolApprovalPending 读 tool arguments,GUI 靠订阅 toolApproval store;机制不同效果一致。
  • attentionRequired:WebUI 需要它把审批中的工具从 trace 里露出来;GUI 的审批不在转录里(独立 ToolApprovalBar),layout.work 不会出现待交互项,无需接。
  • scrollbar-gutter:stable / motion-reduce:WebUI 隐藏原生滚动条用浮动自绘,且没有对应的 settle 过渡,无处可加。
  • data-chat-width-owner、删扫描动画、去 isAgentMode(any-hover:none) CSS:架构 / 平台差异。

su-fen and others added 2 commits September 2, 2026 21:42
折叠后的工具组头部把各类别标签拼成一句,zh-CN 用空串连接,"读取了文件"和
"运行了命令"黏成"读取了文件运行了命令",中文没有词边界无法断句。

现在 zh-CN 用全角竖线 "|" 分隔(全角字符自带留白,不另加空格),其他语言用
" | ";同一类别多次操作仍只出一次标签,不会产生多余分隔符。

Co-authored-by: Cursor <cursoragent@cursor.com>
思考块此前靠 flex-col-reverse 做纯 CSS 贴底。WKWebView 在 column-reverse 容器里
追加内容时不会稳定锚定在底部,每次流式追加都让视口位移(滚动条整体上移),手动滚
动前部分内容还会不可见;同时 column-reverse 容器的 scrollTop 恒 <= 0,外层会话
的 useScrollFollow 用 scrollTop > 0 判断内层滚动器是否在消费向上滚轮,永远判否,
叠加 overscroll-contain 后表现为鼠标滚不动。

现在滚动容器与内层内容元素经 callback ref 接入共享的 useScrollFollow(content 传
内层包裹层:max-h 夹住后外层盒子不再变化,只有 scrollHeight 在涨),reattachZonePx
为 0,仅在 active && open 时启用。流式时自动贴底,用户上滚即脱离、滚回底部重新贴
底;已结束的段落展开后从头往下正常阅读。

Co-authored-by: Cursor <cursoragent@cursor.com>
@su-fen

su-fen commented Sep 2, 2026

Copy link
Copy Markdown
Member

fix: 两处回复区交互问题,分别在 7daab81c1660f9 修复。

1. 折叠的工具批次标签黏连(7daab819)

工具组折叠后头部把各类别标签拼成一句,zh-CN 用空串连接,"读取了文件"和"运行了命令"黏成 读取了文件运行了命令,中文没有词边界无法断句。现在 zh-CN 用全角竖线分隔 → 读取了文件|运行了命令(全角字符自带留白,不另加空格),其他语言用 |Read files | Ran commands。同一类别多次操作仍只出一次标签,不会产生多余分隔符。

2. 思考块流式输出时滚动条上移 / 鼠标滚不动(c1660f99)

ThinkingDisclosure 此前靠 flex flex-col-reverse 做纯 CSS 贴底,在桌面端 WKWebView 里不可靠:

  • WebKit 在 column-reverse 容器里追加内容时不会稳定锚定在底部,每次流式追加都让视口位移(即"整个滚动条上移"),手动滚动前部分内容还会不可见。
  • column-reverse 容器的 scrollTop<= 0,而外层会话 useScrollFollowscrollTop > 0 判断内层滚动器是否在消费向上滚轮,永远判否;叠加 overscroll-contain 后表现为鼠标滚不动。

现在滚动容器与内层内容元素经 callback ref 接入共享的 useScrollFollowcontent 传内层包裹层:max-h 夹住后外层盒子不再变化,只有 scrollHeight 在涨),reattachZonePx: 0,仅在 active && open 时启用。流式时自动贴底,用户上滚即脱离、滚回底部重新贴底,且不会误把外层会话的跟随打断;已结束的段落展开后从头往下正常阅读。顺手把 scrollFollowCore.ts 头注释里过期的 <pre> 描述改掉。

验证

  • thinking-disclosure.test.mjs 新增两条用例:滚动容器不再使用 flex-col-reverse 且正确接入 useScrollFollow(viewport / content / reattachZonePx);折叠或结束时跟随关闭。activity-disclosure-state.test.mjs 改为断言 分隔,并补单类别无分隔符用例。
  • agent-guitest/chat 全量 1253 条通过;gateway web 相关源码断言测试通过;tsc(ui / gui)与 biome 无问题。
  • WebKit 相关现象在 Chromium 里复现不出来,未做端到端浏览器复现,建议桌面端实际跑一次流式思考确认滚动手感。

任务进度药丸与回到底部按钮都锚定在输入卡片顶部上方且垂直区间几乎重合,
药丸所在的输入区层 z-20 盖住 z-10 的按钮;按钮又以面板中心定位,而 desktop
卡片列为对齐正文右移 18px,两者水平也对不齐。

输入区新增 measureComposerOverlay 一并测出预留线之上浮动元素(药丸、队列
面板)的额外高度与卡片列中心偏移,通过 onFloatingOverhangChange /
onCenterOffsetChange 上报;转录页按钮 bottom 再让出这段高度、left 按偏移平移。
正文底部预留与 FloorNavRail 不受影响。

Co-authored-by: Cursor <cursoragent@cursor.com>
@su-fen

su-fen commented Sep 2, 2026

Copy link
Copy Markdown
Member

fix: 回到底部按钮不再与任务进度药丸("全部完成")重叠,并与输入卡片、药丸居中对齐。提交:8740e817fix(chat): 回到底部按钮避开任务进度药丸并与输入卡片居中对齐

现象

有任务列表时,回到底部按钮几乎完全被"全部完成"药丸盖住;把按钮抬高后又能看到两者水平中心差约 18px。

原因

  • 两者都锚定在输入卡片顶部上方:药丸在 ChatComposerBarabsolute bottom-full mb-3(卡片顶 +12~44px),按钮在 ChatTranscriptbottom: bottomReservePx + 16(卡片顶 +16~48px)。bottomReservePx 只量输入区层高度,药丸是绝对定位、不在布局流里,按钮并不知道它存在;输入区层 z-20 又盖过按钮的 z-10
  • 按钮以整个面板为中心(left-1/2),而 desktop 卡片列为对齐模型回复正文(非头像轨)整体 translate-x-[18px],药丸在卡片列内居中,所以中心自然差 18px。

修复

  • 新增 agent-ui/lib/chat/composerOverlayMetrics.tsmeasureComposerOverlay 在原有 heightPx(底部预留,语义不变)之外再算出 floatingOverhangPx(预留线之上被药丸、队列面板额外占据的高度)和 centerOffsetPx(卡片列中心相对输入区层中心的水平偏移)。
  • ChatComposerBar:药丸容器改 callback ref 存 state、卡片列加 ref,两者一并纳入 ResizeObserver(药丸出现 / 消失不会改变 composerLayer 尺寸,必须单独观察);新增可选 onFloatingOverhangChange / onCenterOffsetChange,随原有测量一起上报。gateway 走 CSS 变量的路径未改。
  • ChatTranscript:按钮 bottom = reserve + overhang + 16left = calc(50% + offset).chat-jump-to-bottomtranslateX(-50%) 不变。有任务列表时按钮位于药丸上方 16px;没有时垂直偏移与之前一致,水平上则会与输入卡片居中对齐而不再偏向面板中心。
  • 正文底部预留与 FloorNavRail 不受影响,药丸仍按原设计浮在正文之上;不写死 18px,偏移全部来自测量。顺带把队列面板也计入 overhang,队列展开时按钮同样不会再被盖住。

验证

  • 新增 composer-overlay-metrics.test.mjs:无浮层 / 仅药丸 / 队列 + 药丸 / 隐藏 / 分屏(层 left ≠ 0)/ 正文宽度收窄 / 小数几何取整等几何用例,加上 ChatComposerBarChatTranscriptConversationPaneHost 三处接线的源码断言。
  • agent-gui test/chat 全量 1261 条通过;tsc(ui / gui)与 biome 无问题;UI boundary 检查通过。
  • 桌面端是 Tauri 应用、没有浏览器运行模式,未做端到端截图,几何计算以单元测试为准,建议桌面端实际带任务列表滚上去看一眼。

模型回复中途触发上下文压缩时,历史持久化为 assistant → summary → assistant
(两半之间没有 user 消息),旧渲染逐项出行,变成"两条回复夹一张检查点卡",
且前半段没有 answer 文案,折叠后只剩已编辑文件卡。

方案:把压缩视为回复内部的里程碑而不是回复之间的分隔。

- 共享真源 lib/chat/replyContinuity.ts:stitchCompactedReplies 把
  assistant/checkpoint 连续链缝成一条 reply;检查点变成零 block 的
  CompactionSeamRound,块级消费者(已编辑文件/用量/回复文本/用量环倒扫)
  天然穿透,已编辑文件卡跨压缩点聚合全部文件。尾随检查点(空闲手动压缩、
  压缩后即停)仍为独立分隔卡。
- rekeyContinuationRounds 按位置重键续跑段(p<n>:r<i>),live 续跑与其
  落库孪生按同一规则重键,落定零 remount。
- 布局层新增 GroupedRoundBlock{kind:"checkpoint"} 工作区条目,作为阶段
  边界,两侧工具批次不合并。
- GUI rowModel:历史按缝合组建行(缓存按 leader 记录成员);运行中把刚
  落库的前半段 + 检查点吸收进 live activity 行;adoptSettledTwin 的 origin
  别名挂在 reply leader 上。
- WebUI rows.ts:raw pass 之后加一次 stitchCompactedRows,流式与历史两条
  路径都覆盖。
- 视觉:压缩家族共用紫色色带 CompactionBand(轻依赖、不引 Markdown)。
  运行中"正在压缩上下文"= active 色带(底缘扫光 + 图标呼吸,reduced-motion
  退化为静态线);落定"已压缩上下文,继续处理"= 可点色带 + 消息数/压缩后
  token 两枚 chip,展开摘要正文。与灰色的思考/工具行明显区分,便于浏览历史
  时定位压缩点。

测试:web reply-continuity 10 例、GUI 行模型 4 例(含运行中吸收 + 落定零
remount)、jsdom 真渲染 2 例、WebUI store 流式场景 1 例;预览页
web/test/browser/compaction-seam-preview.html 供肉眼回归。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@su-fen

su-fen commented Sep 2, 2026

Copy link
Copy Markdown
Member

运行中压缩后的回复缝合 + 紫色压缩色带(eafcc8a7)

问题

模型回复中途触发上下文压缩时,历史持久化为 assistant(前半) → summary → assistant(后半),两半之间没有 user 消息。旧渲染逐项出行,界面上变成"两条回复夹一张检查点卡";前半段没有 answer 文案,折叠后只剩已编辑文件卡,看起来像个空壳。

方案:一条回复,检查点变"缝合行"

把压缩视为回复内部的里程碑,而不是回复之间的分隔。

  • 共享真源 crates/agent-ui/src/lib/chat/replyContinuity.ts
    • stitchCompactedReplies:assistant/checkpoint 连续链且 checkpoint 后仍有 assistant ⇒ 缝成一条回复。尾随检查点(空闲手动压缩、压缩后即停)保持独立分隔卡。
    • assembleContinuousReply:检查点变成零 block 的 CompactionSeamRound,块级消费者(已编辑文件 / 用量 / 回复文本 / 用量环倒扫)天然穿透,已编辑文件卡跨压缩点聚合全部文件。
    • rekeyContinuationRounds:GUI 历史轮次 key 是每组序数 r1,两半会撞;后续段按位置重键 p<n>:r<i>。live 续跑段与其落库孪生按同一规则重键 ⇒ 落定零 remount(测试覆盖)。
  • 布局层 assistantBubbleUtils.resolveAssistantTurnLayout:新增 GroupedRoundBlock{kind:"checkpoint"} 工作区条目,作为阶段边界(两侧工具批次不合并),resolveActiveWorkEntry 对它恒返回 null。
  • GUI rowModel.ts:历史按缝合组建行,rowCache 按 leader 记录成员;运行中把刚落库的前半段 + 检查点吸收进 live activity 行;adoptSettledTwin 的 origin 别名挂在 reply leader 上。
  • WebUI rows.ts:原 raw pass 之后加一次 stitchCompactedRows,流式与历史两条路径都覆盖。WebUI 轮次 key 自带组前缀,不需重键。

视觉

压缩家族共用紫色色带 components/chat/CompactionBand.tsx(violet 系,刻意避开产品里 sky=链接 / amber=警告 的既有语义,与灰色的思考/工具行一眼区分):

  • 正在压缩上下文(运行中):全宽色带,图标片呼吸、标签 shimmer、底缘紫色扫光;prefers-reduced-motion 下退化为静态底线。
  • 已压缩上下文,继续处理(落定):同款静止色带 + 94 条消息 / 压缩后 18.4K 两枚 chip,点击展开摘要正文(左侧紫色竖线延续色带语义)。回合结束后随工作区块一起折叠。

⚠️ CompactionBand 必须保持轻依赖、不引 Markdown:AssistantStatus 层引它,否则 assistant-status / shell-session-ui 测试会因 @streamdown/cjk 的 exports 在 node 加载器里炸掉(第一版踩过)。

验证

  • tsc:agent-ui / agent-gui / agent-gateway-web 三包全过;check-ui-boundaries 通过;biome 仅剩 HEAD 已有的 transcript.css 警告。
  • 测试:GUI 2932 / WebUI 695 全绿。新增:
    • web/test/reply-continuity.test.mjs 10 例(分组 / 尾随检查点 / 重键 / 布局 / 已编辑文件聚合)
    • gui/test/chat/transcript-row-model.test.mjs 末尾 4 例(历史缝合 / 独立卡 / 运行中吸收 + 落定零 remount / 空闲手动压缩)
    • gui/test/chat/compaction-seam-row.test.mjs 2 例(jsdom 真渲染)
    • web/test/transcript-store.test.mjs 1 例(流式 checkpoint token 落在同一 run 内)
  • 视觉:真浏览器(headless Chrome)深/浅两套主题截图已核对。肉眼回归可起 vite --port 1432 打开 crates/agent-gateway/web/test/browser/compaction-seam-preview.html,页面内含"落定 / 正在压缩 / 压缩后续跑"三个状态。

🤖 Generated with Claude Code

往上滚到顶部加载更早的一页历史时,桌面端视口会跳约一屏,且在 WebKit 上
甩动一次可能连续拉进多页。

根因有三层:

- GUI ChatTranscript 在 prepend 后手写 scrollTop 做补偿(scrollTop +
  scrollHeight 差),但虚拟列表已经用 'origin' 模式把新页吸进原点、行位置
  没动;两套补偿叠加,视口被拉回到发请求那一刻记下的位置,且绕开了
  virtualizer 的写入追踪。
- virtual-core 的 rebase 先写 scrollTop 再 notify 布局:浏览器按写入瞬间的
  sizer 高度钳制,sizer 只有消费者渲染后才长高,正债(前置一页)写入落短,
  而布局已按整笔债移动 → 视口与布局撕开。空闲状态下前置一页没有滚动事件
  来结算债务,一直挂到用户下次滚动才写,届时必钳制。
- 翻页触发读的是 DOM scrollTop:origin 模式下未结算期间 scrollTop 停在原
  处,每个滚轮 tick 都重新满足"接近顶部",一页接一页叠上去。

修复:

- virtual-core maybeRebaseOrigin:先 notify(true) 同步发布 rebased 布局(
  React 适配器 flushSync 让 sizer 先长高),再写 scrollTop;写前记录可滚动
  上限,echo 恰好落在上限且意图在其之外 = 被钳制而非用户移动,同一趟把未
  确认的余量退回 origin(isClampedRebaseEcho / resolveRebaseTransaction)
  并强制一次 notify 重绘;携带用户移动的 echo 仍按落地处理。视口已进入顶部
  空带时只以可达部分(max(0, offset+delta))武装事务,避免退回再钳制的死循
  环。新增 scheduleIdleRebase:_willUpdate / 空闲 re-measure 产生的债务在下
  一帧结算,不再等滚动事件。
- 新增 getSettledScrollOffset()= scrollOffset - originOffset,即结算后离内容
  真实顶部的距离,供翻页触发使用;types/index.d.ts 同步。
- GUI:翻页逻辑迁到 TranscriptList 紧挨 virtualizer,删除手写 scrollTop 补
  偿;阈值读 settled offset(480px)。硬顶(scrollTop ≤ 1)保留为独立触发
  (WebKit 甩动期间推迟 rebase,视口钉在 0 而债务仍在上方,用户顶着顶部滚
  不进去),但按"到访一次触发一次"上锁,橡皮筋事件不会重复触发;in-flight
  锁在 finally 后延一帧释放,让新页先进入 settled offset。
- WebUI GatewayTranscript:同一套 settled offset + 硬顶上锁判定。
- 两端 vite.config 把 @tanstack/react-virtual 排除出 optimizeDeps:预打包会
  把 vendored fork 源码内联进 .vite 缓存,改 fork 不 --force 就一直跑旧代码。

测试:virtual-core harness 新增 DOM sizer 模拟(只在 notify 时跟随布局,
写入按 sizer 钳制)与 notifies 记录;origin-anchoring 新增 7 例(空闲前置
一页次帧结算零跳动 / 钳制 echo 退回余量 / 无 echo 由 verify 帧回滚 / 用户
移动不误判钳制 / 顶部空带内 rebase 不死循环 / 空闲 re-measure 次帧结算 /
rebase 同步发布)。GUI chat-transcript-scroller 断言两端不再手写 scrollTop、
触发读 settled offset、硬顶上锁存在。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@su-fen

su-fen commented Sep 3, 2026

Copy link
Copy Markdown
Member

加载更早历史:交给虚拟列表 origin 锚定,修复翻页跳动与连续叠页(b81b4663)

现象

往上滚到顶部触发"加载更早"时,桌面端视口会跳约一屏;在 WebKit 上甩动一次还可能连续拉进多页历史。

根因(三层叠在一起)

  1. 两套补偿打架。 GUI ChatTranscript 在 prepend 后手写 scrollTop(旧 scrollTop + scrollHeight 差)做位置补偿,但虚拟列表已经用 'origin' 模式把新页吸进原点、视口下的行本来就没动。手写补偿把视口拉回"发请求那一刻"记下的位置,而且绕开了 virtualizer 的写入追踪,verify 帧无法识别。
  2. virtual-core rebase 顺序反了。 maybeRebaseOrigin 先写 scrollTopnotify 布局。浏览器按写入瞬间的 sizer 高度钳制,而 sizer 只在消费者渲染新布局后才长高:正债(前置一页)写入落短,布局却已按整笔债移动,视口与布局撕开。另外空闲状态下前置一页没有滚动事件来结算债务,会一直挂到用户下次滚动才写,届时必然钳制。
  3. 翻页触发读的是 DOM scrollTop。 origin 模式下债务未结算期间 scrollTop 停在原处,每个滚轮 tick 都重新满足"接近顶部",一页接一页叠上去。

修复

virtual-core(crates/virtual-core/src/index.ts

  • maybeRebaseOrigin 改为notify(true) 同步发布 rebased 布局(React 适配器 flushSync,sizer 先长高),再写 scrollTop。写前记录可滚动上限 _pendingRebaseMaxOffset:echo 恰好落在上限、意图却在其之外 ⇒ 是被钳制而非用户移动(isClampedRebaseEcho),同一趟把未确认的余量退回 origin 并强制一次 notify 重绘,不会有一帧画错位;携带用户移动的 echo 仍按落地处理(保留原先"不误判为吞写"的语义)。
  • 视口已进入顶部空带时,事务只以可达部分 max(0, offset + delta) 武装:布局照样关闭整条空带,视口跟不上的部分是一次性位移,不再作为余量退回(否则退回 → 再钳制死循环)。
  • 新增 scheduleIdleRebase_willUpdate(setOptions 吸收 prepend)与空闲 re-measure 产生的债务在下一帧结算,不再等滚动事件;卸载时随其他 rAF 一起取消。
  • 新增 getSettledScrollOffset() = scrollOffset - originOffset:结算后离内容真实顶部的距离,供翻页触发使用。types/index.d.ts 同步。

GUI

  • 翻页逻辑从 ChatTranscript 迁到 TranscriptList 紧挨 virtualizer,删除手写 scrollTop 补偿,阈值改读 settled offset(480px)。
  • 硬顶(scrollTop ≤ 1)保留为独立触发:WebKit 甩动期间推迟 rebase,视口钉在 0 而债务仍在上方,用户顶着顶部滚不进去。但按"到访一次触发一次"上锁(hardTopLatchedRef),橡皮筋事件不会重复触发;in-flight 锁在 finally 后延一帧释放,让新页先进入 settled offset。

WebUI GatewayTranscript:同一套 settled offset + 硬顶上锁判定,替换原 scrollTop > clientHeight 检查。

dev 体验:两端 vite.config.ts@tanstack/react-virtual 排除出 optimizeDeps。预打包会把 vendored fork 源码内联进 node_modules/.vite,只按 lockfile/config 失效,改 fork 不 --force 就一直跑旧代码。

验证

  • virtual-core harness 新增 DOM sizer 模拟(只在 notify 时跟随布局,写入按 sizer 钳制,与浏览器行为一致)与 notifies 记录,origin-anchoring.test.mjs 新增 7 例:空闲前置一页次帧结算零跳动 / 钳制 echo 退回余量并重贴 / 无 echo 由 verify 帧回滚 / 用户移动不误判钳制 / 顶部空带内 rebase 不死循环 / 空闲 re-measure 次帧结算 / rebase 同步发布。
  • chat-transcript-scroller.test.mjs 断言:ChatTranscript / TranscriptList 不再手写 scrollTop、触发读 getSettledScrollOffset、硬顶上锁存在。
  • virtual-core 23 / GUI test/chat 1268 / WebUI 695 全绿;tsc(virtual-core / gui / gateway-web)与 biome 无问题;UI boundary 检查通过。
  • 几何以 harness 单测为准,未做真机端到端;建议桌面端开一个长会话往上甩到顶几次,确认无跳动且每次只进一页。

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

governance-exempt Skip PR governance checks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants