This commit is contained in:
07akioni
2026-07-29 20:56:56 +08:00
parent 382884e237
commit 2a12057345
9 changed files with 96 additions and 67 deletions

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 .agents/notes/implemented/bug-fix/2026-07-29-sticky-composer-conversation-scroll.md
2026-07-29-sticky-composer-conversation-scroll.md: f245e7ca0404df4f644504ac9e1b101e659b6b68
2026-07-29-sticky-composer-conversation-scroll.zh.md: 7e4d5fb523f33f450c343e12e486e1d33784ee6c
2026-07-29-sticky-composer-conversation-scroll.md: 803033613c715a1b5c8d299ab0df985e6a02fa6b
2026-07-29-sticky-composer-conversation-scroll.zh.md: 0c3e83ce13f70e0e425e89f20ca133a55d9a663d

View File

@@ -10,9 +10,9 @@ The active conversation column split scrolling: the chat (and trajectory) view o
## Decision
Active phase keeps the session header as `flex: none` column chrome above the scrollport. `ConversationRoot` supplies a `wrapActiveBody` owner callback that wraps the view ring in a `data-conversation-scroll` body and places the composer stack inside that body with `position: sticky; bottom: 0`. Hero and settling keep the composer as a Root child (centered hero card). ChatView and Trajectory/Waterfall keep a local scroller only when mounted outside that host (unit tests); under the host they set `overflow: visible` and resolve bottom-follow / prepend anchoring through `closest('[data-conversation-scroll]')`.
While a session exists, `ConversationRoot` always supplies a `wrapActiveBody` owner callback that wraps the view ring in a `data-conversation-scroll` body and places the composer stack inside that body. Active CSS sticks the composer with `position: sticky; bottom: 0`; hero CSS centers the same stack inside the scroll body. `ConversationSession` keeps a chrome-hidden header + body shell while blank so that tree seat does not change on the first send. The session header remains `flex: none` column chrome above the scrollport when visible. ChatView and Trajectory/Waterfall keep a local scroller only when mounted outside that host (unit tests); under the host they set `overflow: visible` and resolve bottom-follow / prepend anchoring through `closest('[data-conversation-scroll]')`.
Session stats live on `'conversation.composer.dock'` (above `'conversation.input.dock'`). The InputBar textarea, when inside the host, listens for `wheel` with `{ passive: false }`, calls `preventDefault`, and applies `deltaY` to the host — hero mounts have no host and keep native textarea wheel behavior. Moving the composer into the Session scrollport on the hero → active flip may remount the textarea; the InputHub draft is the durable carrier across that flip.
Session stats live on `'conversation.composer.dock'` (above `'conversation.input.dock'`). The InputBar textarea, when inside the host, listens for `wheel` with `{ passive: false }`, calls `preventDefault`, and applies `deltaY` to the host.
## Alternatives considered
@@ -26,4 +26,4 @@ Session stats live on `'conversation.composer.dock'` (above `'conversation.input
## Consequences
Wheel over the footer scrolls the transcript; the visible layout is a fixed header, scrolling transcript, and sticky bottom composer. Stats appear on every active view tab. Nested view scrollers under the host are suppressed so sticky Turn headers in Trajectory stick to the column host. Hero → active asserts draft survival through the InputHub, not textarea DOM identity.
Wheel over the footer scrolls the transcript; the visible layout is a fixed header, scrolling transcript, and sticky bottom composer. Stats appear on every active view tab. Nested view scrollers under the host are suppressed so sticky Turn headers in Trajectory stick to the column host. Hero → active keeps the same textarea DOM node (assembled slash-flow snapshot) and the InputHub draft.

View File

@@ -10,9 +10,9 @@ Status: implemented
## Decision
活跃阶段会话标题栏保持为滚动容器之上的 `flex: none` 列 chrome。`ConversationRoot` 提供 `wrapActiveBody` owner 回调,将视图环包进 `data-conversation-scroll` 主体,并把编辑器栈以 `position: sticky; bottom: 0` 放进该主体。Herosettling 仍把编辑器作为 Root 子节点(居中 hero 卡片)。ChatView 与 Trajectory/Waterfall 仅在宿主之外挂载时(单元测试)保留本地 scroller位于宿主下时设为 `overflow: visible`,并通过 `closest('[data-conversation-scroll]')` 解析贴底跟随与前置锚定。
只要存在会话,`ConversationRoot` 就会始终提供 `wrapActiveBody` owner 回调,将视图环包进 `data-conversation-scroll` 主体,并把编辑器栈放进该主体。活跃阶段 CSS `position: sticky; bottom: 0` 钉住编辑器hero CSS 在同一滚动主体内居中同一栈。`ConversationSession` 在 blank 时保留隐藏 chrome 的 header + body 壳,使首次发送时树座位不变。可见时会话标题栏仍是滚动容器之上的 `flex: none` 列 chrome。ChatView 与 Trajectory/Waterfall 仅在宿主之外挂载时(单元测试)保留本地 scroller位于宿主下时设为 `overflow: visible`,并通过 `closest('[data-conversation-scroll]')` 解析贴底跟随与前置锚定。
会话统计挂在 `'conversation.composer.dock'`(位于 `'conversation.input.dock'` 之上。InputBar 的 textarea 在宿主内以 `{ passive: false }` 监听 `wheel`,调用 `preventDefault`,并将 `deltaY` 施加到宿主——hero 挂载没有宿主,保留 textarea 原生滚轮行为。hero → active 翻转时编辑器进入 Session 滚动容器可能重挂载 textarea跨该翻转的耐久载体是 InputHub 草稿
会话统计挂在 `'conversation.composer.dock'`(位于 `'conversation.input.dock'` 之上。InputBar 的 textarea 在宿主内以 `{ passive: false }` 监听 `wheel`,调用 `preventDefault`,并将 `deltaY` 施加到宿主。
## Alternatives considered
@@ -26,4 +26,4 @@ Status: implemented
## Consequences
在页脚上滚轮会滚动 transcript可见布局是固定标题栏、可滚动 transcript 与 sticky 底部编辑器。统计出现在每一个活跃视图标签上。宿主下的嵌套视图 scroller 被抑制,因而 Trajectory 的 sticky Turn 标题贴在列宿主上。hero → active 断言经 InputHub 的草稿存续,而非 textarea DOM 身份
在页脚上滚轮会滚动 transcript可见布局是固定标题栏、可滚动 transcript 与 sticky 底部编辑器。统计出现在每一个活跃视图标签上。宿主下的嵌套视图 scroller 被抑制,因而 Trajectory 的 sticky Turn 标题贴在列宿主上。hero → active 保持同一 textarea DOM 节点assembled slash-flow 快照)以及 InputHub 草稿