fix(chat): restore turn rail width threshold and clarify scroll gestures

This commit is contained in:
imccyu committed 2026-09-22 22:25:32 +08:00
1 parent f6966f8fab
commit 43a8a22824
6 files changed
+35 -5

No files matched your search

@@ -566,6 +566,27 @@ describe('web e2e: long Chat scroll contract', () => {
await expectControls()
await composer.fill('')
await expectControls()
const rail = world.page.getByRole('navigation', { name: 'Turn navigation', includeHidden: true })
const frame = rail.locator('../..')
const originalStyle = await frame.getAttribute('style')
try {
for (const clearance of [16, 8]) {
for (const contentWidth of [901, 900, 899, 901]) {
await frame.evaluate((element, { clearance, contentWidth }) => {
element.style.width = `${contentWidth + 2 * (clearance + 16)}px`
element.style.setProperty('--dsh-composer-side-clearance', `${clearance}px`)
}, { clearance, contentWidth })
await expect.poll(() => rail.isVisible()).toBe(contentWidth > 900)
}
}
} finally {
await frame.evaluate((element, style) => {
if (style === null) element.removeAttribute('style')
else element.setAttribute('style', style)
}, originalStyle)
}
await expectControls()
await backToBottom.click()
await expectBottom(world.page)
assertClean(world)
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-chat/README.md
README.md: 406d8de41fb8ee3f8d1f4f0676ee36e68d5bc684
README.zh.md: 6ca8c67cda401e96995631105642d3d4ab14c541
README.md: 12f9b2470c107775ff57c2c7f04c649841dd6f7a
README.zh.md: 4d25db34d283ed2dc62fcfbb15d6ab4797511881
+3 -1
View File
@@ -102,7 +102,7 @@ The Chat-node slot injects a reset-bound `useDisclosure` Hook for reasoning and
Chat restores semantic anchors across history prepend and renderer remounts, with browser scroll anchoring disabled on its scrollport only while following the tail. Pinned scroll deliveries without reader movement, and reader input that reaches the exact floor, update follow ownership immediately, before subsequent layout changes can invalidate their floor. Other reader movement remains pending until the sampling interval or `scrollend`, even inside the follow threshold, so layout growth cannot erase small scroll gestures. Submitting transcript input or steering immediately restores tail following and clears an older pending reader sample. While the reader is pinned to the floor, `ResizeObserver` follows the new floor and selects the latest loaded Turn without reading row geometry. Once the reader moves away, flow-height changes preserve the top position and the reading-line geometry selects the active Turn. Turn-rail previews paint above sticky Markdown code-block banners, while the rail frame remains inside the transcript band above the composer.
The turn rail and back-to-bottom button sit outside the clipped transcript. They use the shared conversation scrollport for sticky positioning, or the Chat frame for absolute positioning when Chat owns its scrollport.
The turn rail and back-to-bottom button sit outside the clipped transcript. They use the shared conversation scrollport for sticky positioning, or the Chat frame for absolute positioning when Chat owns its scrollport. The rail hides when the transcript's available width, excluding its horizontal padding, is at most 900px; the browser viewport width is not the criterion.
The transcript root uses `overflow-x: visible; overflow-y: clip`: vertical overflow is clipped without creating a scroll container. Sticky Markdown code banners and expanded compaction headers therefore retain the actual conversation scrollport as their reference when no nearer scrolling ancestor exists. Capped process groups and terminal sections keep their own scrollports.
@@ -122,6 +122,8 @@ While the pointer is outside the rail, automatic follow keeps the rail still whe
<details>
<summary>Scroll implementation — click to expand</summary>
Within a process group, wheel, touchstart, and any pointerdown interrupt an active smooth animation, including presses on tool cards. ArrowUp/ArrowDown, PageUp/PageDown, Home/End, and Space keys also interrupt it unless a child has prevented the key's default action; editable controls are not excluded. These events stop the animation without requiring a scroll displacement. Subsequent position sampling determines whether following continues.
`useScrollFollow` supplies independent controllers for shared bottom thresholds, follow intent, and native scrolling. `useProcessScroll` owns group observation, initial placement, and edge fades. Outer following remains immediate; group growth uses native smooth scrolling unless reduced motion is requested. Growth retains an in-flight target until `scrollend`; arrival at that target or its shrink-clamped position continues toward the latest floor, while another endpoint releases following. Opening placement remains immediate. A bottom-follow request within tolerance uses immediate positioning when no animation is outstanding, so a fractional no-op cannot leave a pending smooth target.
`useChatViewport` owns turn-aware DOM reads, clamped writes, native events, and one retained paging anchor. For Load older, Node and Group seats mark eligible anchors from their existing disclosure state. The viewport selects the first nonempty, unhidden marker in transcript order without hit testing or geometry-based search, then measures that element and its scroll containers. It compensates the anchor's capped group first, then gives the remaining displacement to the transcript scrollport. An inner compensation write cancels that group's animation through its bound controller and pauses following; reader scrolling back to the bottom resumes it. Commits and later content resizes reuse that anchor; a remounted row is resolved by the same semantic key. Compensation stays within the actual scroll ranges without adding bottom space.
+3 -1
View File
@@ -102,7 +102,7 @@ Chat 节点 slot 为推理与工具注入绑定重置来源的 `useDisclosure`
Chat 会在历史前插与 renderer 重新挂载时恢复语义锚点,并且只在跟随底部时禁用自己滚动区域的浏览器自动锚定。没有读者移动的贴底滚动事件,以及确实到达最底部的读者输入,会立即更新跟随归属,避免后续布局变化使其底部位置失效。其他读者移动即使位于跟随阈值内,也保持待处理直到采样周期或 `scrollend`,防止布局增长抵消小幅滚动操作。提交正文输入或 steering 时立即恢复跟随底部,并清除先前待处理的读者滚动采样。读者跟随底部时,`ResizeObserver` 追随新的底部,并且无需读取行几何就选中最后一个已加载轮次;读者离开底部后,高度变化会保持顶部位置,再由阅读线几何选择活跃轮次。轮次导航预览位于 Markdown 代码块粘性头栏上方,而导航外框始终处于 composer 上方的 transcript 区域内。
轮次轨道与回到底部按钮位于正文裁剪层外。它们相对共享会话滚动容器做 sticky 定位;Chat 自己持有滚动容器时,则相对 Chat 外框做 absolute 定位。
轮次轨道与回到底部按钮位于正文裁剪层外。它们相对共享会话滚动容器做 sticky 定位;Chat 自己持有滚动容器时,则相对 Chat 外框做 absolute 定位。正文扣除水平内边距后的可用宽度不超过 900px 时,隐藏轮次轨道;判断依据不是浏览器视口宽度。
正文根节点使用 `overflow-x: visible; overflow-y: clip`:裁剪纵向溢出,但不创建滚动容器。因此,在没有更近的滚动祖先时,Markdown 代码块的 sticky 头栏和展开的压缩摘要头栏仍以实际会话滚动容器为参照。限高过程组和终端区域保留各自的滚动容器。
@@ -122,6 +122,8 @@ Chat 会在历史前插与 renderer 重新挂载时恢复语义锚点,并且
<details>
<summary>滚动实现——点击展开</summary>
过程组内的 wheel、touchstart 和任意 pointerdown 都会中断正在进行的平滑动画,包括按下工具卡片。上下方向键、PageUp/PageDown、Home/End 和空格键也会中断动画,除非子控件已阻止该按键的默认行为;输入控件不被排除。这些事件无需产生滚动位移就会停止动画,之后的位置采样再决定是否继续跟随。
`useScrollFollow` 提供各自独立的控制器,共用触底阈值判断、跟随意图与原生滚动实现。`useProcessScroll` 负责小组观察、初始定位及边缘渐隐。外层保持即时跟随;组内增长使用原生平滑滚动,减少动态效果偏好开启时改为即时滚动。`scrollend` 前的增长保留当前动画目标;到达该目标或因内容缩短而钳制的位置后,再追随最新底部;停在其他位置则释放跟随。展开时仍即时定位。没有进行中动画时,容差范围内的贴底请求使用即时定位,避免小数位置上的无位移操作留下未结束的平滑目标。
`useChatViewport` 负责识别轮次的 DOM 读取、限制范围后的写入、原生事件及一个持续保留的分页锚点。加载更早时,Node 与 Group 容器根据已有开合状态标记可选锚点。视口按正文顺序选择第一个非空、未隐藏的标记,不做命中测试或按几何位置搜索,再测量该元素及其滚动容器。先补偿锚点所在的限高组,再把剩余位移交给整段文本的滚动区域。内层补偿写入通过该组绑定的控制器取消动画并暂停跟随,读者滚回底部后恢复。提交和后续内容尺寸变化复用该锚点;消息行重挂载时按同一语义 key 重新定位。补偿限于实际可滚动范围,不增加底部空白。
@@ -8,6 +8,9 @@
the composer, so its preview never needs to cross the footer. */
z-index: 7;
height: 0;
/* Query the transcript's usable width while keeping the rail full-bleed. */
padding-inline: calc(var(--dsh-composer-side-clearance) + 16px);
container-type: inline-size;
pointer-events: none;
}
@@ -201,7 +204,7 @@
}
@container (max-width: 900px) {
.slot {
.frame {
display: none;
}
}
@@ -8,6 +8,8 @@ const SCROLL_KEYS = new Set(['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home
/**
* Observe one group's body and content without coupling its follow intent to the outer transcript.
* Wheel, touchstart, any pointerdown, and unprevented scroll keys interrupt active animations,
* including events from editable controls; subsequent position sampling determines follow intent.
* @param bodyRef - capped scrolling body.
* @param contentRef - uncapped content whose size reports growth.
* @param open - local disclosure state.