DSH / Atlas
2026-09-22implementedbug-fixarchived 2026-09-30

Separate Chat clipping and independent follow intent

分离 Chat 裁剪与独立滚动跟随意图

Transcript overflow must remain clipped without trapping floating controls or changing the scroll ancestor of sticky content. An `overflow-y: hidden` ancestor becomes the nearest scroll container even when readers scroll a different outer element. It prevents Markdown code banners and expanded compaction headers from sticking to the visible conversation scrollport; floating controls inside that ancestor also use tran

English

Problem

Transcript overflow must remain clipped without trapping floating controls or changing the scroll ancestor of sticky content. An overflow-y: hidden ancestor becomes the nearest scroll container even when readers scroll a different outer element. It prevents Markdown code banners and expanded compaction headers from sticking to the visible conversation scrollport; floating controls inside that ancestor also use transcript geometry instead of viewport geometry.

A height-capped process group also has a separate reading position. Outer bottom-follow cannot expose newly appended content inside a group whose own scroll offset stays unchanged. Treating every non-bottom scroll event as reader input does not support native smooth following: the browser emits those same events while an animation approaches its target.

Decision

The transcript, floating controls, and scroll intent retain separate ownership. The scroll-state table and group display rules define the current product behavior.

AreaImplementationReason
Transcript clippingChatView uses overflow-x: visible; overflow-y: clip on .root inside an unclipped frame.Constrain vertical overflow without introducing a scroll container between sticky content and its actual scrollport.
Floating controlsTurnNavigator and the back-to-bottom button are siblings of the clipped transcript.Shared Conversations use the outer scrollport for sticky positioning; standalone Chat uses its frame for absolute positioning.
Follow intentuseScrollFollow retains one independent controller per scrollport.Reuse thresholds, position sampling, and native scrolling without sharing one follow flag.
Group lifecycleuseProcessScroll owns its existing body/content observer, directional fades, and one-shot opening position.Group growth remains observable after the body reaches its cap; opening performs no immediate geometry read.

The clipped root owns no scroll offset. Shared Conversation scrolling, standalone Chat scrolling, capped process groups, and terminal sections retain their existing scrollports. The horizontal axis stays visible: pairing vertical clip with horizontal auto or hidden computes the vertical overflow back to hidden and restores the unwanted scroll container.

An unclosed group opens at its bottom; a closed group opens at its top. The group’s closed value, not the whole Turn’s status or an all-node search, decides that initial position. A later close does not reset an already-open reader. Browser find keeps its own reveal position, and display-mode changes preserve mounted members.

Native smooth growth keeps an outstanding target separate from the current position. Animation progress retains follow intent. Input interrupts native motion before the browser applies reader movement; the resulting actual displacement determines whether following stops or resumes. Further growth retains the issued target until scrollend. Arrival at that target, including shrink clamping, allows one request to the latest floor; an off-target stop releases following so focus and find keep their landing. Reduced motion and explicit opening positions use immediate scrolling. Outer following keeps its existing immediate behavior and sampling policy.

The back-to-bottom button depends only on outer follow intent and restores only that intent. Inner scrolling affects the outer controller only when scroll chaining actually moves the outer scrollport. Group height growth can resize the outer transcript, but the controllers do not invoke each other’s follow actions. Paging and semantic restoration remain with the existing outer reading/navigation owners.

Alternatives considered

Remove transcript clipping. That would abandon the overflow constraint. Vertical clip retains clipping without creating a scroll container; the unclipped sibling frame keeps floating controls outside the clipped area without a Portal or document-level coordinate synchronization.

Keep overflow-y: hidden and move only floating controls. That protects the controls but leaves sticky content inside the unwanted scroll container. Changing the root to clip also preserves the scroll ancestor of code and compaction headers without moving those headers out of their content.

Share one follow flag, or duplicate the complete controllers. One flag would pull a group away from a reader merely because the outer transcript follows. Two complete implementations would duplicate animation attribution and threshold rules. Sharing the small controller retains independent state without moving paging, Turn navigation, or semantic memory into it.

Only replace scrollTop assignment with smooth scrollTo. Intermediate animation offsets would appear to be reader movement and disable following. Native motion needs an explicit target and input interruption; a custom animation-frame interpolator is unnecessary.

Verification

Controller tests cover independent thresholds, animation progress, target reuse, input interruption, reduced motion, and immediate positioning. Chat component tests cover group opening, pause/resume, closing while open, and browser-find preservation. Viewport tests retain the outer anchor and attribution behavior.

Browser scroll cases exercise floating-control geometry, nested following, paging, streaming, and real wheel input through the shipped Web composition.

Seeded-history cases check an expanded compaction header and its nested code banner against the actual conversation scrollport, including their stacking and copy-button access.

Consequences

The shared controller stores intent without scheduling React renders; callers still publish visible state changes, and group edge fades update only when their booleans change. Follow operations read the affected scrollport, not its Node collection. Native scrolling still performs browser layout and paint work. These changes establish behavior and ownership, not a measured latency or memory improvement.

The navigation decision still owns sampled reading, known landings, and batched historical jumps. The grouping decision still owns Definition-based membership and stable rendering identities. Extracting scroll-follow mechanics does not supersede either decision.

中文

问题

正文溢出必须保持裁剪,同时不能限制悬浮控件或改变 sticky 内容的滚动祖先。即使读者实际滚动的是外层元素,overflow-y: hidden 祖先也会成为最近的滚动容器。这会阻止 Markdown 代码块头栏和展开的压缩摘要头栏吸附在可见会话滚动区域;位于该祖先内的悬浮控件也会参照正文几何,而非视口几何。

限高过程组也有独立阅读位置。若组内偏移不变,外层跟随底部无法展示组内新增内容。把每个未到底的滚动事件都当成读者输入,也无法支持原生平滑跟随:浏览器在动画接近目标期间同样会发出这些事件。

决策

正文、悬浮控件及滚动意图分别由独立对象负责。滚动状态表与分组展示规则定义当前产品行为。

范围实现原因
正文裁剪ChatView 在无裁剪的外框内,对 .root 使用 overflow-x: visible; overflow-y: clip。限制纵向溢出,不在 sticky 内容与其实际滚动区域之间引入滚动容器。
悬浮控件TurnNavigator 与回到底部按钮位于正文裁剪层的同级。共享 Conversation 相对外层滚动容器做 sticky 定位;独立 Chat 相对自身外框做 absolute 定位。
跟随意图useScrollFollow 为每个滚动区保留独立控制器。共用阈值、位置采样及原生滚动,不共用一个跟随开关。
小组生命周期useProcessScroll 负责既有的正文与内容观察器、方向渐隐和一次性展开位置。正文达到限高后,内容增长仍可被观察;展开时不立即读取几何。

被裁剪的根节点不持有滚动偏移。共享 Conversation、独立 Chat、限高过程组和终端区域保留各自原有的滚动容器。横轴保持 visible:纵向 clip 如果与横向 auto 或 hidden 组合,纵向溢出的计算值会变回 hidden,重新引入不需要的滚动容器。

未关闭的小组展开到自己的底部,已关闭的小组展开到顶部。初始位置由组自身的 closed 决定,不取整个 Turn 的状态,也不遍历全部节点。组随后关闭不会重置已展开的阅读位置。浏览器查找保留自身的展开定位,模式切换保留已挂载成员。

原生平滑跟随将未完成的目标与当前位置分开记录。动画进度保留跟随意图。输入在浏览器处理读者移动前中断原生动画,再由实际位移决定是否暂停或恢复跟随。后续增长保留已发出的目标,直到 scrollend。到达该目标(包括内容缩短后的钳制位置)才允许再次请求最新底部;停在目标之外则释放跟随,让焦点和查找保留落点。减少动态效果与明确的展开定位使用即时滚动。外层跟随保留既有的即时行为和采样策略。

回到底部按钮只取决于外层跟随意图,也只恢复该意图。只有滚动传递确实移动外层滚动区域时,内层滚动才影响外层控制器。小组长高可以改变外层正文尺寸,但两个控制器不会互相调用跟随操作。分页和语义恢复仍由既有的外层阅读与导航对象负责。

考虑过的替代方案

删除正文裁剪。 这会放弃溢出约束。纵向 clip 保留裁剪而不创建滚动容器;无裁剪的同级外框让悬浮控件位于裁剪区域之外,不需要 Portal 或文档级坐标同步。

保留 overflow-y: hidden,仅移动悬浮控件。 这可以保护控件,但 sticky 内容仍位于不需要的滚动容器内。将根节点改为 clip 也能保留代码和压缩摘要头栏的滚动祖先,无需把这些头栏移出其正文。

共用一个跟随开关,或复制完整控制器。 共用开关会仅因外层跟随就把小组拉离读者位置。复制两套完整实现则会重复动画归因和阈值规则。共用小控制器可以保留独立状态,同时不把分页、轮次导航及语义记忆搬入其中。

只把 scrollTop 赋值换成平滑 scrollTo。 动画中间偏移会被误判为读者移动,导致跟随关闭。原生动画需要明确的目标与输入中断处理,无需自写动画帧插值器。

验证

控制器测试覆盖独立阈值、动画进度、目标复用、输入中断、减少动态效果和即时定位。Chat 组件测试覆盖小组展开、暂停与恢复、展开期间关闭及浏览器查找位置保持。视口测试保留外层锚点与归因行为。

浏览器滚动用例通过正式 Web 装配验证悬浮控件几何、嵌套跟随、分页、流式内容与真实滚轮输入。

历史回放用例相对实际会话滚动区域检查展开的压缩摘要头栏及其内部代码头栏,包括层叠关系和复制按钮的可访问性。

影响

共享控制器只保存意图,不自行调度 React 渲染;调用方仍发布可见状态变化,小组边缘渐隐仅在布尔值变化时更新。跟随操作读取受影响的滚动容器,而非节点集合。原生滚动仍有浏览器布局和绘制成本。这些改动确立行为与职责,不代表已测得延迟或内存收益。

导航决策继续负责采样阅读、已知落点与聚合历史跳转。分组决策继续负责 Definition 成员关系与稳定渲染身份。提取滚动跟随机制不取代这两项决策。