fix(web): keep the input card in place across view tabs
The composer seat is one node laid out against two different edges: in Chat it is a sticky child of the column's scroller and rides its content box, which a space-consuming scrollbar shortens, while a view declaring a composer overlay gets an absolutely positioned seat against the padding box, which no bar reduces. With the transcript scrolling the two tabs disagreed by the bar's width, so the centred input card moved 4px sideways on every switch — and the same 4px inside Chat when a growing transcript began to scroll. The column now reserves its scrollbar gutter unconditionally and states the overlay branch a scroll container on the same axes, so both states measure against the same width.
This commit is contained in:
@@ -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-conversation/README.md
|
||||
README.md: 7d3a4b5fe07cc8858c2f2059e6f65b6e27602b4d
|
||||
README.zh.md: d93af91381157bb4e8e4b6a14ad00edecd505246
|
||||
README.md: ce728fb8b35d9813050a00cf9fa6a4f268820fea
|
||||
README.zh.md: 7e73a2b4a4d19d81ea62701dd71ed68f32492c48
|
||||
|
||||
@@ -6,7 +6,7 @@ Conversation domain: skeleton (header/tabs/composer/empty state), chat view (gro
|
||||
|
||||
Compaction renders as one collapsed row at the checkpoint's flow position without replacing the transcript above it. The disclosure renders the checkpoint's `compact/summary` provenance; when that event is outside the loaded window, the row remains visible but non-expandable. The framed checkpoint payload is model-facing and never renders.
|
||||
|
||||
The resident conversation shell survives no-session and session transitions. Without a current session it renders a disabled input bar; its root-scoped `conversation.hero.workspace` slot hosts the Workspace picker. Selecting a Workspace connects or reuses its Host-owned blank session and opens that session without replacing the shell. Blank sessions render the same composer body as active sessions, while the InputHub carries drafts across Workspace switches and mirrors them into the session store. In the active phase the session header shows only the current session title and view tabs as ordinary column chrome; fork lineage remains session data and is not projected into the header. Beneath it a scrollport (`data-conversation-scroll`) holds the flowing views and the sticky composer stack (stats dock + input docks + bar). Wheel over the textarea chains: the capped draft scrolls locally until its edge, then forwards to that host.
|
||||
The resident conversation shell survives no-session and session transitions. Without a current session it renders a disabled input bar; its root-scoped `conversation.hero.workspace` slot hosts the Workspace picker. Selecting a Workspace connects or reuses its Host-owned blank session and opens that session without replacing the shell. Blank sessions render the same composer body as active sessions, while the InputHub carries drafts across Workspace switches and mirrors them into the session store. In the active phase the session header shows only the current session title and view tabs as ordinary column chrome; fork lineage remains session data and is not projected into the header. Beneath it a scrollport (`data-conversation-scroll`) holds the flowing views and the sticky composer stack (stats dock + input docks + bar). That scrollport reserves its scrollbar gutter unconditionally, and a view opting into a composer overlay leaves it a scroll container, so the input card keeps one horizontal position whether or not the transcript scrolls and whichever view tab is shown ([decision](../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). Wheel over the textarea chains: the capped draft scrolls locally until its edge, then forwards to that host.
|
||||
|
||||
The view ring IS a slot: the conversation registration declares the `'conversation.view'` list slot (session scope) in its `children` table, ConversationRoot renders the active entry through its renderSlot share (`only: <active id>`), and view tabs project from the ring ledger's registration options (`id`/`order`/`label`). The chat view is this package's own ring entry; other plugins (ui-trajectory) contribute tabs through plain `ctx.slots.register` — the former package-local view registry (`registerView`/`ViewEntry`/`ConversationViewMap` and the chrome attachment table) is retired, with per-view chrome dissolved into the view components themselves.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
压缩(compaction)在检查点自身的消息流位置渲染为一行折叠标记,不替换其上方的 transcript(文本记录)。展开内容来自检查点溯源的 `compact/summary`;该事件位于已加载窗口之外时,标记仍然可见但不可展开。面向模型的带框检查点载荷绝不渲染。
|
||||
|
||||
常驻会话壳会跨无会话与会话状态切换而保留。没有当前会话时,它会渲染禁用输入栏;其根作用域的 `conversation.hero.workspace` slot 承载 Workspace 选择器。选择 Workspace 会连接或复用由 Host 拥有的空白会话,并在不替换会话壳的情况下打开该会话。空白会话与活跃会话渲染相同的输入区主体;InputHub 则在 Workspace 切换间携带草稿,并将草稿镜像到会话 store。活跃阶段,会话标题栏作为普通列 chrome,仅显示当前会话标题和视图标签;fork 谱系仍保留为会话数据,不投影到标题栏。其下滚动容器(`data-conversation-scroll`)承载流动排版的各视图与 sticky 编辑器栈(统计 dock+输入区 dock+输入栏)。textarea 上的滚轮会链式处理:限高草稿先在本地滚动,到达边缘后再转交给该宿主。
|
||||
常驻会话壳会跨无会话与会话状态切换而保留。没有当前会话时,它会渲染禁用输入栏;其根作用域的 `conversation.hero.workspace` slot 承载 Workspace 选择器。选择 Workspace 会连接或复用由 Host 拥有的空白会话,并在不替换会话壳的情况下打开该会话。空白会话与活跃会话渲染相同的输入区主体;InputHub 则在 Workspace 切换间携带草稿,并将草稿镜像到会话 store。活跃阶段,会话标题栏作为普通列 chrome,仅显示当前会话标题和视图标签;fork 谱系仍保留为会话数据,不投影到标题栏。其下滚动容器(`data-conversation-scroll`)承载流动排版的各视图与 sticky 编辑器栈(统计 dock+输入区 dock+输入栏)。该滚动容器无条件预留自己的滚动条槽,选用编辑器 overlay 的视图也仍把它保留为滚动容器,因此无论对话记录是否滚动、无论展示哪个视图标签,输入卡片都保持同一个横向位置([决策](../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md))。textarea 上的滚轮会链式处理:限高草稿先在本地滚动,到达边缘后再转交给该宿主。
|
||||
|
||||
视图环本身就是 slot:会话注册声明 `'conversation.view'` 列表 slot(Session scope),并将其列在 `children` 表中;ConversationRoot 通过 renderSlot share 渲染活跃配置项(`only: <active id>`);视图标签页从环账本的注册选项(`id`/`order`/`label`)投影而来。聊天视图是该包(package)自身的环配置项;其他插件(ui-trajectory)通过普通的 `ctx.slots.register` 贡献标签页。先前包内的视图注册表(`registerView`/`ViewEntry`/`ConversationViewMap` 及 chrome 附加表)已退役,逐视图 chrome 则被拆入视图组件自身。
|
||||
|
||||
|
||||
@@ -182,6 +182,17 @@
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
/* One reserved gutter for every view, because the composer seat takes its
|
||||
horizontal geometry from two different edges: in Chat it is a sticky CHILD
|
||||
of this scroller and so rides the content box, which the scrollbar
|
||||
shortens, while under a view's composer overlay it is positioned against
|
||||
the padding box, which the scrollbar never reduces. `auto` would reserve
|
||||
only while this box overflows, which is precisely the difference between
|
||||
the two states, so the input card would move sideways by the bar's width
|
||||
on every tab switch — and inside Chat alone the moment a growing
|
||||
transcript starts to scroll. `stable` makes both edges the same edge at
|
||||
every content height. */
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.root[data-phase='active'] .viewArea {
|
||||
@@ -212,7 +223,16 @@
|
||||
ownership of the seat geometry and its active-phase precedence. */
|
||||
.scrollBody:has([data-conversation-composer-overlay]) {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* Still a box nothing scrolls out of — the view fills it and the seat is out
|
||||
of flow — but declared as a scroll container on the same axes as the Chat
|
||||
state instead of `overflow: hidden`, so the reservation above reaches this
|
||||
state too: WebKit honours `scrollbar-gutter` on an `overflow-y: auto` box
|
||||
and ignores it on a hidden one (measured for the composer's own layers,
|
||||
see InputBar.module.css). The horizontal axis is declared rather than left
|
||||
to compute, because a box that scrolls on one axis computes `visible` on
|
||||
the other to `auto` and would grow a horizontal bar of its own. */
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.scrollBody:has([data-conversation-composer-overlay]) > .viewArea {
|
||||
|
||||
Reference in New Issue
Block a user