DSH / Atlas
2026-09-04implementedfeature

Right Sidebar docking infrastructure

右侧 Sidebar 停靠基础设施

The Web client's right column was a single-purpose Detail panel: `ui-chat` occupied the `details` slot with `DetailsPanel`, which showed one selected Tool call's raw payload through a `conversation.details.tool` child seat. Nothing else could live there. A plugin that wanted a persistent side surface — a file preview, a task list, a diff — had no seat to register into, no way to open its content from the conversation

English

Problem

The Web client's right column was a single-purpose Detail panel: ui-chat occupied the details slot with DetailsPanel, which showed one selected Tool call's raw payload through a conversation.details.tool child seat. Nothing else could live there. A plugin that wanted a persistent side surface — a file preview, a task list, a diff — had no seat to register into, no way to open its content from the conversation, and no shared layout to share the column with.

Files the agent produced were the sharpest case. A produced-file chip or a read row's path link handed the path to the operating system through session/openWorkspacePath, so a browser that was not on the Host machine could not look at the file at all, and even a local one left the product to do so. The details panel meanwhile duplicated the chat rows' cards at full height, a second presentation surface every card had to keep in step.

Decision

The right column is a per-session docking surface — split panes, tabs, floating panels, and an undoable operation sequence — owned by ui-sidebar-right over the ui-dockkit engine, replacing the Detail panel. This note owns the surface: the engine, the frame's right column, the panel's presentations and controls, and the per-session state. What lives in the surface is decided elsewhere: how plugins declare tab types, open content, and receive their props is tab types and navigation; live data behind an address is the client resource model; reading workspace files is the workspace file service; and the guide, the text preview, and the file tree are the shipped types.

Package topology

The stable Sidebar mounting decision owns one retained tab-content tree for docking, floating and Session switches. The engine, layout state and gesture ownership in this note remain applicable.

PackageKindOwns
packages/client/ui-dockkitstatic-linked library, zero DSH dependenciesthe layout engine and the React components that render and drive it; consumers compile its sources, and it keeps exactly one stylesheet because a consumer de-duplicates injected sheets by file name
packages/client/ui-sidebar-rightdynamic pluginthe rightbar panel seat and the conversation.session.header.corner expand button over one store, one surface per session, both presentations, the float host, ctx.sidebarRight, ctx.sidebarRightTabs, the tab domain (one occurrence per tab record), the three extension seats, the guide tab type, and the sidebarRight copy namespace

The kit is the product's first embedder and knows nothing about it: every string arrives through DockLabels, every tab body through a TabRenderer dispatching on an opaque kind, and every gesture leaves through DockIntents. The integration package supplies what the kit refuses to know.

Layout engine

The engine is a normalized recursive split tree: nodes keyed by id, rootId for the docked root, floats bottom-to-top. The ids are branded (PaneId, SplitId, TabId; NodeId is the pane/split union), minted only by a Mint, so no id kind stands in for another or for a bare string. A floating panel is a pane whose host is 'float' with capacity one, drawn without a tab strip. applyOp(state, op) returns the next state and the operations that undo it, captured when the operation runs because the pre-operation state is gone by undo time. Every operation carries the ids it creates, so replay(initial, ops) reproduces the same tree from the same start; the engine reads no clock and no random source. The Sequencer keeps a linear history with one entry per intent — the operations one gesture or command produced undo and redo together — a run of consecutive focus-only entries steps as one, and a new entry after stepping back drops the forward branch. Planners are the pure intent layer — (state, mint, args) → LayoutOp[] — and DockController is a thin observable shell over them. planSettle is the opt-in follow-up planner that merges away every docked pane an intent emptied and reseeds an emptied root pane through the embedder's factory.

Components render a snapshot and report settled intents, one per gesture: a drag previews in local state while the gesture's facts stay in its closure, and the release folds the net result into one operation. Gestures are pointer events with pointer capture rather than HTML5 drag-and-drop. A chip is a capsule carrying one control, its close; a secondary press opens the context menu (close plus the embedder's renderTabMenuItems). After the chips sits the add control, which asks the embedder through DockIntents.addTab to seat its seeded tab (planAddTab). Floating is the drag released clear of the surface, and copying has no kit control at all — DockIntents.duplicateTab stays for embedder APIs. The split control's glyph is a frame bisected vertically, as the split is. Four interaction rules fix defects found in a real browser and are kept on purpose: capture the pointer on gesture start, never make the tab strip a scroll container, land focus on click rather than press, and let a control nested in a draggable chip stop its own press. The tab's actions menu renders in a portal positioned against its control, because the strip's deliberate overflow clip would otherwise cut it off. The kit ships no undo/redo control, and no header: an embedder's surface-wide controls go through DockSurface's chrome prop, which the kit places at the far end of the top-right pane's strip (topRightPaneId: the last child of every row split, the first of every column split). The generic kit defaults to four panes; the Sidebar supplies its two-pane product limit.

The frame's right column

Responsive Sidebar and tab information supersedes this note's no-concession layout, overlay presentation and product pane limit. ui-layout still owns three-column geometry and pixel width preferences; the Sidebar occupant reports presentation through ctx.layout.openRightbar(track, fullscreen) and closeRightbar(), without the frame injecting the Sidebar package. Exact width rules belong to ui-layout.

The right Sidebar uses one mounted content tree in normal and fullscreen modes; hiding preserves retained tab state, and fullscreen covers the viewport while retaining underlying column reservation. DockLayout renders stable Grid cells with fixed-position floating frames in the same tree; foreground floats remain open when the Sidebar closes. The product limits docking to two horizontal panes and a 20–80% divider; the generic engine keeps its own defaults.

State

Sidebar guide rules define default-page selection and explicit closing, while moving tabs still settles emptied panes.

ui-sidebar-right keeps one SurfaceState per session id — the layout, its history, and the mint counter — in a store declared at the seat registration. Every action mints the ids its intent needs, asks a kit planner for the operations, runs the settle planner over the result, and records the whole intent as one history entry before assigning the session's surface back; no action edits a layout in place. The settle step is the product's rule: a docked pane whose last tab is closed, moved out, or floated is merged away, and an expanded empty root pane receives the current default page. A collapsed surface may remain empty until its next expansion; no separate pane-closing gesture exists. Layout persistence and provider recovery owns Session-scoped browser storage and reload. Layout is presentation state and never enters the session log.

Beyond the surface

The surface renders tabs whose bodies it does not know: each tab carries a kind, and the panel asks the type registry for the implementation in force and dispatches to its keyed body seat. Everything a body may rely on — its record, its pane, whether it is visible, how it was navigated to, an abort signal, and the actions it may take — is read through the framework-injected useTabInfo(). The registry, the navigation face ctx.sidebarRight, the seats, and the tab information are specified in tab types and navigation; a body that shows data reads it through the client resource model.

Entry points and removals

ui-chat's openFile(path, { line? }) — reached by tool-row path links, produced-file chips, and closing-message mentions — now opens the file into the Sidebar through the navigation face (see tab types and navigation). The Show in folder action and its canOpenWorkspacePath probe are removed from ui-deliverables: the Sidebar has no directory form, and the product keeps no secondary entry. DetailsPanel, ToolDetails, the tool-node reader, the chat store's selection, ToolDetailsProps, and CENTER_MIN are removed. session/openWorkspacePath remains on the Host with no web caller.

Alternatives considered

Adopt a docking library. Six engines were evaluated against the product's state-ownership requirement (the layout is a recorded, replayable sequence the product owns). dockview is uncontrolled and its only external entry is a destructive fromJSON, with undo in a paid tier; react-mosaic has no floating layer and rests on a drag base unmaintained for years; rc-dock, golden-layout, and Lumino failed on state ownership. FlexLayout 0.10.x was the one viable candidate — external Model, vetoable onAction, content-preserving fromJson — and was kept as a verified fallback whose switch points were the prototype's five-zone dock and multi-float tests. Both passed self-built with no switch signal, and its 0.x minors carry breaking changes, so it was not adopted.

Layered reuse: react-resizable-panels for sizes, Pragmatic drag-and-drop for gestures. The planned main line before the prototype. Rejected once the prototype's own size and gesture layers passed in a real browser: the layers the plan meant to save had already been written and verified, so the remaining value was only long-tail edge handling. It stays a replaceable layer if snap or priority sizing is ever required.

A drawer over shell.overlay, or a double-layer shell (root rail, session content). The prototype shipped as a drawer to avoid touching the frame. Rejected for the product: a drawer is not a column and never squeezes the conversation, and the double-layer shell hit the slot core's one-handle-one-scope rule, which would have made collapsing un-undoable. The frame owns a real column; content and state stay session-bound.

A frame-owned 32px rail as the collapsed state, the panel living inside the animated grid track, and the overlay as a separate portal. The first shipped form. Rejected after review: a solid rail track pushes the conversation's scrollbar inboard for a strip that exists only while collapsed; a panel inside the animating track is stretched and re-laid-out by every track transition, so the Sidebar itself visibly moved when it should not; and two code paths for one panel meant switching presentation remounted it. The panel is now one edge-anchored box that slides and the track only reserves room.

A 40px rail inside the conversation column, with its own sidebar.right.rail.item seat. Tried next, so the rail could leave with the panel. Rejected on review as visually too heavy for what it carried: a full-height strip for one button and a placeholder. The expand control is now a single header button and the collapsed-state seat is deferred until something needs it.

A header row on the panel. The first form carried a title and its controls in a 40px row above the strip. Removed: the strip already is the panel's top edge, so the controls sit at the strip's end in the top-right pane through the kit's chrome seat, and the title said nothing the tabs did not.

The expand button as a conversation.session.header.utilities entry. Tried after the rail. Rejected on review: as a list entry it sat inside the utilities row, so it was not at the header's true corner, and its appearance and disappearance shifted the Session log control beside it. A dedicated corner seat with a reserved footprint fixes both.

A per-chip "more" control with copy and float items. The first form gave every chip a ⋯ menu holding close, copy, and float. Rejected on review: the chip now carries only its close, the menu moved to the secondary press with only close (plus embedder items), and copy and float left the panel entirely — copy stays an API (open with duplicate: true), float stays the drag. The kit's duplicateTab / floatTab intents and planners are unchanged.

Undo and redo buttons on the panel header. Shipped first, then removed: the sequence is an architectural fact, and stepping it is not a product action yet. The API stays reachable as @internal methods for tests and the future navigation controller.

Empty panes as a persistent state. The first design allowed a pane to stay after its last tab left, with a placeholder. Rejected because nothing offered a way to close such a pane; every intent settles the surface so an emptied side pane is merged away. An empty root receives the current default page only while the column is expanded.

Inline the kit through packages/util and the INLINE_SAFE list. A build probe showed it works, but the util build chain has no CSS pipeline and the kit ships a stylesheet; the static-linked client package (the ui-primitives precedent) was chosen knowing that changing the kit means rebuilding the shell and reloading.

Consequences

  • The docking surface itself no longer overflows its panel: .surface and .pane clamp to the column (min-width: 0, overflow: hidden), so a long unwrapped line scrolls inside the body and the strip's controls stay in view in every split.
  • Layout is undoable, persisted and per Session. Undo is reachable only through @internal service methods; the product shows no history controls.
  • An expanded surface has no empty panes. Empty side panes merge away; an empty root receives the current default page only while expanded. New sessions and a collapsed surface whose last tab closed remain empty until expansion.
  • A pane holds at most one guide tab: a second one cannot be added, opened, duplicated, or moved in; the guide's uniqueness is per pane, so a split still seeds its new pane with a guide.
  • A pane may split only when each equal half can still hold what cannot shrink: the strip's fixed controls (its width minus the chip box and the fill, so the top-right pane's chrome counts on the half that hosts it) plus one chip at its minimum, measured in the component layer after every commit and on resize. Otherwise the split control stays, disabled with its own copy, the matching edge drop zones are withheld, and panes the user narrows keep their size; the product permits at most two horizontal panes, regardless of widening or divider movement.
  • The Sidebar panel never moves when the presentation switches, and its slide is the same in both presentations; the conversation is the only thing that animates on a switch. A hidden panel keeps its tabs mounted, so a preview survives a collapse.
  • Collapsed, the Sidebar is one header-corner button: the conversation keeps its full width and its scrollbar at its edge, the button leaves when the panel opens, and its footprint stays so nothing else in the header moves.
  • A tab is closed from its chip; copying and floating have no panel control (copying is API-only, floating is the drag). The context menu is reachable by right-click and carries close plus embedder items.
  • The Detail panel and its duplicate card presentation are gone (a net removal of roughly 1,400 lines); cards are read in place, and inspect opens the trajectory view.
  • The frame has no centre floor: a viewport narrower than the two edge columns squeezes the conversation toward zero instead of closing a column.
  • The kit is compiled by its consumers, so a kit change requires a shell rebuild and a page reload; there is no HMR for it.
  • Docked cells, floating cells and the portalled tab menu use fixed stacking levels; floating cells use CSS paint order within their level. The client still has no global z-index token layer.

Testing

ui-dockkit specs pin the engine's invariants — every operation's inverse round-trips, replay over any prefix of the history equals the recorded state, one compound intent steps as one entry, focus runs coalesce symmetrically, the pane cap and the width rule refuse with no record — and drive the components with props alone, with no scaffold. ui-sidebar-right specs cover the per-session store, the seat's presentations and controls, per-pane guide uniqueness, and the width-aware split. The Web e2e suite drives the shipped Sidebar in Chromium through the real plugin graph: expand and collapse, split to the limit and the greyed control, floats, docking back, and the guide. Both suites are keyless.

Deferred

  • A z-index token layer, then the panel's, the float host's, and the menu's hard-coded values.
  • The assembled session-switch case, blocked on the fixture composition opening its settings surface by default.
  • Chinese counterparts for the new packages' READMEs and for the English documentation this change edited.
  • Snap or priority pane sizing, touch tuning, and keyboard routes for split, move, and float.
  • Persistence of the layout, popout windows, and a content navigation stack (entries keyed by pane and content, adjacent duplicates replaced, a navigating guard, closed tabs left in the stack).
  • A non-closable tab (a closable flag on TabRecord, drawn as a fixed leading marker rather than a capsule) once a tab type needs one.

中文

Problem

Web 客户端的右列曾是单一用途的 Detail 面板:ui-chat 以 DetailsPanel 占据 details 坑位,通过 conversation.details.tool 子坑位展示一个被选中 Tool 调用的原始载荷。其他任何内容都无法住在那里。想要一个常驻侧面的插件——文件预览、任务清单、diff——没有可注册的坑位,没有从会话流打开自己内容的通道,也没有可共享的布局来与他人分享这一列。

Agent 产出的文件是最尖锐的案例。产出文件 chip 或 read 行的路径链接会经 session/openWorkspacePath 把路径交给操作系统,因此不在 Host 机器上的浏览器完全看不到该文件,即便本机浏览器也要离开产品才能查看。与此同时 Detail 面板以全高重复渲染会话行的卡片,成为每张卡片都必须保持同步的第二个展示面。

Decision

右列是每会话一份的停靠面(分栏 pane、tab、浮动面板与可撤销的操作序列),由 ui-sidebar-right 基于 ui-dockkit 引擎持有,取代原来的 Detail 面板。本篇只管这个面:引擎、框架的右列、面板的两种呈现与控件、每会话的状态。面里放什么由别处决定:插件如何声明 tab 类型、打开内容、拿到 props 见tab 类型与导航;地址背后的活数据见客户端资源模型;读工作区文件见工作区文件服务;引导页、文本预览与文件树见随包类型。

包拓扑

Sidebar 稳定挂载决策负责停靠、浮动与会话切换共用的一棵保活 Tab 内容树。本文的引擎、布局状态与手势所有权仍然适用。

包形态所有物
packages/client/ui-dockkit静态链接库,零 DSH 依赖布局引擎与渲染/驱动它的 React 组件;消费方编译其源码,且它只保留一张样式表,因为消费方按文件名去重注入的样式表
packages/client/ui-sidebar-right动态插件共用一个 store 的 rightbar 面板坑位与 conversation.session.header.corner 展开按钮、每会话一份 surface、两种呈现模式、浮层宿主、ctx.sidebarRight、ctx.sidebarRightTabs、tab 域(每条 tab 记录一个 occurrence)、三个扩展坑位、引导 tab 类型与 sidebarRight 文案命名空间

该库的第一个嵌入方就是本产品,而库对此一无所知:所有字符串经 DockLabels 传入,所有 tab 正文经按不透明 kind 分派的 TabRenderer 传入,所有手势经 DockIntents 传出。集成包提供库拒绝知晓的一切。

布局引擎

引擎是归一化的递归分裂树:nodes 按 id 索引,rootId 指向停靠根,floats 自底向上排列。id 带 brand(PaneId、SplitId、TabId;NodeId 是 pane 与 split 的并集),只由 Mint 铸出,任何一种 id 都不能充当另一种或裸字符串。悬浮面板是 host 为 'float'、容量为一的 pane,不绘制 tab 条。applyOp(state, op) 返回下一状态以及撤销它的操作,逆操作在执行时刻捕获,因为到 undo 时操作前状态已不存在。每个操作携带自己创建的 id,所以 replay(initial, ops) 从同一初态重现同一棵树;引擎不读时钟也不读随机源。Sequencer 维护线性历史,一个意图一条账——一次手势或命令产生的全部操作一起撤销与重做——连续的纯焦点条目合并为一步,后退后的新条目丢弃前向分支。planner 是纯意图层——(state, mint, args) → LayoutOp[]——DockController 是其上的薄可观察壳。planSettle 是可选的收尾 planner:合并掉意图留下的每个空停靠 pane,并经嵌入方的工厂重新种上被清空的根 pane。

组件渲染快照并上报已落定的意图,每个手势一条:拖拽过程只在本地 state 预览、手势的事实住在它的闭包里,松手把净结果折成一条操作。手势基于 pointer 事件与 pointer capture,而非 HTML5 拖放。chip 是胶囊形,只带一个控件——关闭;右键打开上下文菜单(关闭加嵌入方的 renderTabMenuItems)。chip 之后是添加控件,经 DockIntents.addTab 请嵌入方种入它的种子 tab(planAddTab)。悬浮是把拖拽松手在面之外,复制则完全没有库控件——DockIntents.duplicateTab 留给嵌入方 API。分栏控件的图标是被竖线一分为二的方框,与分栏本身一致。四条交互规则修复了真实浏览器中发现的缺陷并被有意保留:手势开始即捕获指针;tab 条绝不做滚动容器;焦点落在 click 而非 press;可拖 chip 内嵌的控件自行拦截按下。tab 的操作菜单以 portal 渲染并对着它的按钮定位,因为 tab 条刻意裁切溢出,画在条内的菜单会被一起切掉。库不自带 undo/redo 控件,也不自带头部:嵌入方的面级控件经 DockSurface 的 chrome prop 传入,库把它们放在右上 pane 的 tab 条末端(topRightPaneId:每个行分裂取最后一个子节点、每个列分裂取第一个)。通用工具包默认允许四个窗格;Sidebar 传入产品的两格上限。

框架的右列

响应式 Sidebar 与标签信息取代本记录中的无让步布局、覆盖模式与产品窗格上限。ui-layout 仍拥有三列几何与像素宽度偏好,Sidebar 占位项通过 ctx.layout.openRightbar(track, fullscreen) 和 closeRightbar() 报告呈现方式,框架不注入 Sidebar 包。具体宽度规则见 ui-layout。

右栏在普通与全屏模式下使用同一棵已挂载内容树;隐藏保留已保活的标签状态,全屏覆盖视口并保留底层列占位。DockLayout 在同一棵树中渲染稳定 Grid cell 与 fixed 定位的浮动 frame,前台浮窗不随右栏关闭。产品限制为两个水平窗格与 20–80% 分割比例,通用引擎保留自己的默认值。

状态

Sidebar 引导页规则定义默认页选择与显式关闭,移动 tab 仍会处理被清空的格。

ui-sidebar-right 为每个会话 id 保存一份 SurfaceState——布局、历史与铸造计数——住在坑位注册时声明的 store 里。每个 action 先铸造意图所需的 id,向库的 planner 索取操作,对结果跑一遍 settle planner,把整个意图记为一条历史账,再把该会话的 surface 整体赋回;没有 action 就地改布局。settle 是产品规则:最后一个 tab 被关闭、拖走或悬浮出去的停靠 pane 会被合并掉;展开且为空的根 pane 会填入当前默认页。折叠的布局可以保持为空,直到下次展开;没有单独的关闭 pane 手势。布局持久化与 provider 恢复负责 Session 作用域的浏览器存储和刷新。布局是呈现状态,永不进入会话日志。

面之外

这个面渲染的 tab 正文它自己并不认识:每个 tab 带一个 kind,面板向类型注册表询问该 kind 生效的实现,再派发到其 keyed 正文坑位。正文能依赖的一切——它的记录、所在格、是否可见、如何被导航到、中止信号、可做的动作——均通过框架注入的 useTabInfo() 从标签域读取。注册表、导航面 ctx.sidebarRight、坑位与 标签信息 在tab 类型与导航里定;展示数据的正文经客户端资源模型读取。

入口与删除

ui-chat 的 openFile(path, { line? })——工具行路径链接、产出文件 chip 与收尾消息提及都经由它——现在经导航面把文件开进 Sidebar(见tab 类型与导航)。Show in folder 动作及其 canOpenWorkspacePath 探针从 ui-deliverables 移除:Sidebar 没有目录形态,产品也不保留次级入口。DetailsPanel、ToolDetails、tool-node reader、chat store 的 selection、ToolDetailsProps 与 CENTER_MIN 一并删除。session/openWorkspacePath 留在 Host 上,已无 web 调用方。

Alternatives considered

采用停靠库。 六个引擎按产品的状态所有权要求(布局是产品拥有的、可记录可回放的序列)评估。dockview 非受控,唯一外部入口是破坏性的 fromJSON,undo 划入付费层;react-mosaic 没有浮层,拖拽底座多年未维护;rc-dock、golden-layout、Lumino 在状态所有权上不可用。FlexLayout 0.10.x 是唯一可行候选——外置 Model、可否决的 onAction、保内容的 fromJson——被保留为已验证的降级预案,切换判定点是原型的五区 dock 与多浮层测试。两项自研均通过、无切换信号,且其 0.x minor 携带 breaking change,故未采用。

分层复用:react-resizable-panels 管尺寸、Pragmatic drag-and-drop 管手势。 原型之前的规划主线。原型自己的尺寸层与手势层在真实浏览器中通过后即否决:计划要省下的两层已经写完并验证,剩余价值只在长尾边界处理。若将来需要 snap 或 priority 尺寸语义,它仍是可替换的一层。

挂在 shell.overlay 上的抽屉,或双层外壳(root rail、session 内容)。 原型以抽屉形态交付以避免触碰框架。产品层面否决:抽屉不是一列,永不挤压会话区;双层外壳撞上 slot core 的 one-handle-one-scope 规则,会让折叠不可撤销。框架拥有一条真实的列;内容与状态仍绑定会话。

框架自有的 32px rail 作为折叠态、面板住在带动画的 grid 轨道里、覆盖态另走 portal。 第一版交付形态。评审后否决:实体 rail 轨道为一条只在折叠态存在的条带把会话区滚动条向内挤;住在动画轨道里的面板被每次轨道过渡拉伸重排,Sidebar 自己在动而它本不该动;一个面板两条代码路径意味着切换呈现模式要重挂载它。现在面板是一个锚在边缘的盒子做平移,轨道只负责占位。

会话列内一条 40px 的 rail,带自己的 sidebar.right.rail.item 坑位。 随后一试,好让 rail 能随面板离场。评审否决:为一个按钮加一个占位画一整条竖带,视觉太重。展开入口现在是头部的单个按钮,折叠态坑位推迟到有真实需求时再声明。

面板自带头部行。 第一版在 tab 条上方有一条 40px 的标题加控件行。删除:tab 条本来就是面板的顶边,控件经库的 chrome 坑位坐到右上 pane 的条末端,标题说的也不比 tab 多。

展开按钮作为 conversation.session.header.utilities 的一个 list 条目。 rail 之后的一试。评审否决:作为 list 条目它坐在工具区行内,不在头部真正的角落,而且它的出现与消失会挪动旁边的 Session log 控件。专设一个保留占位宽度的角落坑位同时解决两点。

每个 chip 一个带复制与悬浮项的"更多"控件。 第一版给每个 chip 一个 ⋯ 菜单,装关闭、复制、悬浮。评审否决:chip 现在只带关闭,菜单挪到右键且只剩关闭(加嵌入方条目),复制与悬浮整体离开面板——复制仍是 API(open 带 duplicate: true),悬浮仍是拖拽。库的 duplicateTab / floatTab 意图与 planner 不变。

面板头部的 undo 与 redo 按钮。 先上后撤:序列是架构事实,步进它现在还不是产品动作。API 以 @internal 方法保留给测试与将来的导航控制器。

空 pane 作为一种持久状态。 第一版允许 pane 在最后一个 tab 离开后带占位留下。否决,因为没有任何方式关掉这样的 pane;每个意图都会整理 surface,被清空的侧 pane 合并掉。空根 pane 只在该列展开时填入当前默认页。

经 packages/util 与 INLINE_SAFE 清单内联库。 构建探针证明可行,但 util 构建链没有 CSS 管线而库带样式表;在知晓改库须重建壳并刷新页面的前提下,选择静态链接的 client 包(ui-primitives 先例)。

Consequences

  • 停靠面自身不再溢出面板:.surface 与 .pane 收在列内(min-width: 0、overflow: hidden),长的不换行行在正文内滚动,tab 条控件在任何分栏下都可见。
  • 布局可撤销、可持久化且按 Session 隔离。undo 只能经 @internal 服务方法触达;产品不显示历史控件。
  • 展开的布局不保留空 pane。空侧 pane 被合并,空根 pane 只在展开时填入当前默认页。新会话以及关闭最后一个 tab 后收起的布局保持为空,直到下次展开。
  • 一个 pane 最多持有一个引导 tab:第二个不能被添加、打开、复制或搬入;唯一性按 pane 算,所以分栏仍给新 pane 种引导。
  • pane 只有在等分后的两半都仍能容下不可收缩部分时才可分栏:tab 条的固定控件(条宽减去 chip 盒与填充,因此右上 pane 的面板控件只计在承载它的那一半)加一个最小宽度的 chip,由组件层在每次提交与尺寸变化后测量。否则分栏控件保留但禁用并带自己的文案,对应的边缘落区不再提供,用户拖窄的 pane 保持原尺寸;产品最多两个水平窗格,不因拉宽或拖分隔条而提高上限。
  • 切换呈现模式时 Sidebar 面板一动不动,两种模式的平移一模一样;切换时只有会话区在动。隐藏的面板保持 tab 挂载,预览在折叠后仍在。
  • 折叠时 Sidebar 只是头部角落的一个按钮:会话区保持全宽、滚动条停在自己的边缘,面板展开时按钮离场但占位保留,头部其余内容不动。
  • tab 从 chip 上关闭;复制与悬浮在面板上没有控件(复制仅 API、悬浮靠拖拽)。上下文菜单经右键打开,含关闭与嵌入方条目。
  • Detail 面板及其重复的卡片展示消失(净删约 1,400 行);卡片就地阅读,inspect 打开 trajectory 视图。
  • 框架没有中列下限:视口窄于两侧列之和时会话区被挤向零,而不是关掉某一列。
  • 库由消费方编译,改库须重建壳并刷新页面;它没有 HMR。
  • 停靠 cell、浮动 cell 和 portal 菜单使用固定层级,浮动 cell 在所属层内通过 CSS 绘制顺序排序;客户端仍没有全局 z-index token 层。

Testing

ui-dockkit 的规格钉住引擎不变量——每个操作的逆操作往返恒等、历史任意前缀的 replay 等于记录状态、一个复合意图作为一条账步进、焦点段对称合并、格数上限与宽度规则拒绝且零记账——并仅以 props 驱动组件,不用 scaffold。ui-sidebar-right 的规格覆盖 per-session store、座位的两种呈现与控件、按格唯一的引导、宽度感知分栏。Web e2e 套件在 Chromium 里经真实插件图驱动随包交付的 Sidebar:展开与收起、分到上限与置灰控件、浮窗、回坞、引导页。两套均无需密钥。

Deferred

  • z-index token 层,随后替换面板、浮层宿主与菜单的硬编码值。
  • 组合层的切换会话用例,受阻于 fixture 组合默认打开设置面。
  • 新包 README 与本次改动的英文文档的中文对。
  • snap 或 priority 面板尺寸语义、触屏调优,以及分栏/移动/悬浮的键盘路径。
  • 布局持久化、popout 窗口,以及内容导航栈(条目以 pane 与内容为键、相邻重复替换、navigating 守卫、已关 tab 留在栈中)。
  • 不可关闭的 tab(TabRecord 上的 closable 标志,画成固定的前置标记而非胶囊),等到有 tab 类型需要时再做。