Просмотр исходного кода

docs(client): document global main-panel extension points

imccyu 4 дней назад
Родитель
Сommit
fbbcbf33fe
27 измененных файлов с 212 добавлено и 102 удалено
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml
  2. 3 1
      .agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md
  3. 3 1
      .agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md
  4. 6 0
      .agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml
  5. 33 0
      .agents/notes/implemented/architecture/2026-09-08-global-main-panels.md
  6. 33 0
      .agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md
  7. 2 2
      docs/subsystems/slots.i18n.yaml
  8. 41 36
      docs/subsystems/slots.md
  9. 41 36
      docs/subsystems/slots.zh.md
  10. 2 2
      packages/client/ui-conversation/README.i18n.yaml
  11. 1 1
      packages/client/ui-conversation/README.md
  12. 1 1
      packages/client/ui-conversation/README.zh.md
  13. 2 2
      packages/client/ui-layout/README.i18n.yaml
  14. 5 3
      packages/client/ui-layout/README.md
  15. 5 3
      packages/client/ui-layout/README.zh.md
  16. 2 2
      packages/client/ui-sidebar-right/README.i18n.yaml
  17. 2 0
      packages/client/ui-sidebar-right/README.md
  18. 2 0
      packages/client/ui-sidebar-right/README.zh.md
  19. 2 2
      packages/client/ui-sidebar/README.i18n.yaml
  20. 5 1
      packages/client/ui-sidebar/README.md
  21. 5 1
      packages/client/ui-sidebar/README.zh.md
  22. 2 2
      packages/client/ui-workspace/README.i18n.yaml
  23. 2 0
      packages/client/ui-workspace/README.md
  24. 2 0
      packages/client/ui-workspace/README.zh.md
  25. 2 2
      packages/test-support/client-runtime/README.i18n.yaml
  26. 3 1
      packages/test-support/client-runtime/README.md
  27. 3 1
      packages/test-support/client-runtime/README.zh.md

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml

@@ -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/architecture/2026-07-22-slot-type-chain-implementation.md
-2026-07-22-slot-type-chain-implementation.md: 0e493a19e232acf6f289714e53e0cf64b3ffc4b0
-2026-07-22-slot-type-chain-implementation.zh.md: 5e5c2ec808cec6949396a333c6a86e267bb4268b
+2026-07-22-slot-type-chain-implementation.md: 98585a42f71844594da4ada19d7438a7d8321702
+2026-07-22-slot-type-chain-implementation.zh.md: d1093537ccbc5ee5ff2dc18c234233ffca39879d

+ 3 - 1
.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md

@@ -12,6 +12,8 @@ The page is composed at runtime from independently loaded plugins, so the UI nee
 
 ## Decision
 
+Global main-panel selection and its root lifetime are defined by the [global main-panels decision](2026-09-08-global-main-panels.md).
+
 One sentence: **the ui-renderer renders only `'root'`; a plugin composes UI through a single `register` call that simultaneously occupies a slot, declares+authorizes its child slots, declares its store, and injects its business face; components are pure functions whose props arrive in four shares, each auto-derived from its single source of truth.**
 
 ### 'root' is the only a-priori slot
@@ -25,7 +27,7 @@ ctx.slots.register({
   name: 'root',
   children: {
     'sidebar':      { kind: 'single', scope: 'root' },
-    'conversation': { kind: 'single', scope: 'session' },
+    'main':         { kind: 'keyed', scope: 'root' },
   },
   store: createLayoutStore,      // StoreHandle or factory (below)
   inject: injectFrame,           // business face (below)

+ 3 - 1
.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md

@@ -12,6 +12,8 @@ Status: implemented
 
 ## 决策
 
+全局主面板选择及其 root 生命周期由[全局主面板决策](2026-09-08-global-main-panels.zh.md)定义。
+
 一句话:**ui-renderer 只渲染 `'root'`;插件用单独一次 `register` 调用组合 UI——这一次调用同时占用 slot、声明并授权子 slot、声明 store、注入业务面;组件是纯函数,props 分四份额到达,每一份额都从各自唯一的真源自动推导。**
 
 ### 'root' 是唯一的先验 slot
@@ -25,7 +27,7 @@ ctx.slots.register({
   name: 'root',
   children: {
     'sidebar':      { kind: 'single', scope: 'root' },
-    'conversation': { kind: 'single', scope: 'session' },
+    'main':         { kind: 'keyed', scope: 'root' },
   },
   store: createLayoutStore,      // StoreHandle or factory (below)
   inject: injectFrame,           // business face (below)

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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/architecture/2026-09-08-global-main-panels.md
+2026-09-08-global-main-panels.md: 510b869b9bd32f4edd054a982a77baa57dace9c3
+2026-09-08-global-main-panels.zh.md: 226f46918132237f2049844e8647e01e6d2e63c4

+ 33 - 0
.agents/notes/implemented/architecture/2026-09-08-global-main-panels.md

@@ -0,0 +1,33 @@
+# Agent Note: Global main panels without default UI additions
+
+Status: implemented
+
+English | [中文](2026-09-08-global-main-panels.zh.md)
+
+## Problem
+
+Plugins need application-wide views that do not belong to a Session. A Session-scoped Conversation view cannot provide that lifetime, and replacing the Conversation's single slot removes the ordinary conversation surface. Adding this extension must not add navigation controls or reserved space to the default application.
+
+## Decision
+
+The layout declares a root-scoped keyed `main` slot. The reserved `conversation` key belongs to the Conversation plugin, whose `main.conversation` child retains optional-Session binding. Other main entries receive no implicit Session binding.
+
+The sidebar owns the root-scoped `sidebar.panellist` list and `sidebar.panellist.title` keyed slot. Each list entry supplies its icon and an id matching its main entry; its label provides ordinary text and the accessible name. A title registration can replace the visible label with React content. The shipped composition registers no panel entry, so the empty list has no DOM or spacing.
+
+One eagerly created root store is shared by the renderer and layout controller. Its `panelInfo` and `layoutInfo` objects preserve independent references. The framework supplies `usePanelInfo`; individual rows and main content subscribe to their required selection values, while AppFrame reads only layout information. The right Sidebar's root controller decides whether to mount its Session subtree and reports the resulting track requirements to the frame.
+
+`uiWorkspace.openSession(id)` selects the Session before returning the main area to the Conversation, including when the same Session is selected again. New Session and workspace navigation use that operation. Panel navigation neither cancels the retained Session nor writes a Session event.
+
+## Alternatives considered
+
+**Session-scoped main views.** Their lifetime and standard props bind application-wide state to whichever Session happens to be current.
+
+**A second navigation stack.** Back buttons and saved return destinations are unnecessary when New Session and workspace Session rows already provide explicit destinations.
+
+**Flat selection and layout state with shallow comparison.** Separating the two stored objects preserves reference equality directly and avoids allocating and comparing a fresh layout projection on every panel selection.
+
+## Consequences
+
+The default sidebar snapshots remain unchanged. Extension panels have no right Sidebar, and selecting a different global panel does not change layout preferences. Switching between a Conversation with a visible right Sidebar and a global panel still changes the required column widths; this is not a promise of zero browser layout work.
+
+Panel selection is transient and resets on reload. Plugin disposal removes its contributions; removing the selected main entry returns the main area to the Conversation. Tests cover independent stored references, explicit Session navigation, declaration lifetimes, and the empty default sidebar. The [Slots reference](../../../../docs/subsystems/slots.md) owns the composition API.

+ 33 - 0
.agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md

@@ -0,0 +1,33 @@
+# Agent Note: 不增加默认界面的全局主面板
+
+Status: implemented
+
+[English](2026-09-08-global-main-panels.md) | 中文
+
+## 问题
+
+插件需要不属于任何会话的应用级视图。会话作用域的 Conversation 视图无法提供这种生命周期,而替换 Conversation 的 single slot 又会移除普通会话界面。增加此扩展不能在默认应用中增加导航控件或预留空间。
+
+## 决策
+
+布局声明 root 作用域的 keyed `main` slot。保留的 `conversation` key 属于 Conversation 插件,其 `main.conversation` 子 slot 保留可选的会话绑定。其他主面板条目不获得隐式会话绑定。
+
+侧栏拥有 root 作用域的 `sidebar.panellist` list 和 `sidebar.panellist.title` keyed slot。每个 list 条目提供图标,以及与主面板条目匹配的 id;标签提供普通文字和无障碍名称。标题注册可用 React 内容替换可见标签。默认组合不注册面板条目,因此空列表没有 DOM 或间距。
+
+渲染器与布局控制器共享一个直接创建的 root 存储。其 `panelInfo` 和 `layoutInfo` 对象保持独立的引用。框架提供 `usePanelInfo`;各行和中央内容订阅所需的选中态值,AppFrame 仅读取布局信息。右侧 Sidebar 的 root 控制器决定是否挂载其会话子树,并把最终所需的列宽报告给框架。
+
+`uiWorkspace.openSession(id)` 先选中会话,再将中央区域切回 Conversation,包括再次选中同一个会话的情况。新会话和工作区导航使用该操作。面板导航既不取消保留的会话,也不写入会话事件。
+
+## 考虑过的替代方案
+
+**会话作用域的主视图。** 其生命周期和标准 props 会把应用级状态绑定到恰好处于当前态的会话。
+
+**另一套导航栈。** 新会话和工作区会话行已经提供明确目标,不需要返回按钮或保存返回目的地。
+
+**平铺选中态和布局状态,再做浅比较。** 将两者存为独立对象可以直接保持引用相等,避免每次选择面板都分配并比较新的布局投影。
+
+## 后果
+
+默认侧栏快照保持不变。扩展面板没有右侧 Sidebar,选择另一个全局面板不会改变布局偏好。在显示右侧 Sidebar 的 Conversation 与全局面板之间切换时,所需列宽仍会变化;这并不保证浏览器完全不计算布局。
+
+面板选中态是瞬时状态,刷新后重置。插件 dispose(资源释放)会移除其贡献;移除当前选中的主面板条目会使中央区域回到 Conversation。测试覆盖存储引用的独立性、显式会话导航、声明生命周期和默认空侧栏。[Slots 参考](../../../../docs/subsystems/slots.zh.md)拥有组合 API 的说明。

+ 2 - 2
docs/subsystems/slots.i18n.yaml

@@ -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 docs/subsystems/slots.md
-slots.md: 136607b035c4a40c8ab96d6bf933e6b5c8a925bf
-slots.zh.md: b3a54048f5df20ad3ef271aea868ce15d5e18bce
+slots.md: 92206d2deb3d1681e6c1d230d1f37146af6b720b
+slots.zh.md: 47028aec897adcfdca499a19513162ad825928b4

+ 41 - 36
docs/subsystems/slots.md

@@ -82,6 +82,7 @@ The shipped adapters add these standard props. They are available according to t
 |---|---|---|
 | every scope | `useSessions`, `useSessionPendingInteraction` | `ui-session` |
 | every scope | `useWorkspaces` | `ui-workspace` |
+| every scope | `usePanelInfo` | `ui-layout` |
 | `session` | `sessionId`, `useSession`, `useProjection` | `ui-session` |
 | `session-maybe` | optional `sessionId`, `useSession`, `useProjection` results | `ui-session` |
 | `session` | `useConversation`, `useInput`, `inputActions` | `ui-conversation` |
@@ -112,6 +113,8 @@ root
 ├─ sidebar
 │  ├─ sidebar.brand.mark
 │  ├─ sidebar.brand.name
+│  ├─ sidebar.panellist
+│  ├─ sidebar.panellist.title
 │  ├─ sidebar.footer.action
 │  ├─ sidebar.workspaces
 │  │  └─ sidebar.workspaces.directoryFlow
@@ -127,43 +130,45 @@ root
 │        ├─ settings.models.footer
 │        └─ settings.plugins.tab
 │           └─ settings.plugin.item
-├─ conversation
-│  ├─ conversation.session
-│  │  └─ conversation.view
-│  │     ├─ conversation.chat.node
-│  │     │  ├─ conversation.chat.assistant-actions
-│  │     │  ├─ conversation.chat.commandview
-│  │     │  ├─ conversation.chat.turnTail
-│  │     │  └─ tool.call.toolview
-│  │     │     ├─ tool.call.images
-│  │     │     └─ tool.view.cordis
-│  │     ├─ conversation.message.images
-│  │     └─ conversation.trajectory.images
-│  ├─ conversation.session.header
-│  │  ├─ conversation.session.header.lineage
-│  │  ├─ conversation.session.header.actions
-│  │  ├─ conversation.session.header.utilities
-│  │  └─ conversation.session.header.corner
-│  ├─ conversation.composer
-│  │  └─ conversation.approval.detail
-│  ├─ conversation.composer.bar
-│  │  ├─ conversation.input.attachments
-│  │  ├─ conversation.input.plan
-│  │  └─ conversation.input.model
-│  ├─ conversation.input.overlay
-│  ├─ conversation.input.dock
-│  ├─ conversation.composer.dock
-│  ├─ conversation.input.left
-│  ├─ conversation.input.right
-│  ├─ conversation.hero.brand.mark
-│  ├─ conversation.hero.workspace
-│  │  └─ conversation.hero.workspace.directoryFlow
-│  └─ conversation.hero.agentPreset
+├─ main
+│  └─ main.conversation
+│     ├─ conversation.session
+│     │  └─ conversation.view
+│     │     ├─ conversation.chat.node
+│     │     │  ├─ conversation.chat.assistant-actions
+│     │     │  ├─ conversation.chat.commandview
+│     │     │  ├─ conversation.chat.turnTail
+│     │     │  └─ tool.call.toolview
+│     │     │     ├─ tool.call.images
+│     │     │     └─ tool.view.cordis
+│     │     ├─ conversation.message.images
+│     │     └─ conversation.trajectory.images
+│     ├─ conversation.session.header
+│     │  ├─ conversation.session.header.lineage
+│     │  ├─ conversation.session.header.actions
+│     │  ├─ conversation.session.header.utilities
+│     │  └─ conversation.session.header.corner
+│     ├─ conversation.composer
+│     │  └─ conversation.approval.detail
+│     ├─ conversation.composer.bar
+│     │  ├─ conversation.input.attachments
+│     │  ├─ conversation.input.plan
+│     │  └─ conversation.input.model
+│     ├─ conversation.input.overlay
+│     ├─ conversation.input.dock
+│     ├─ conversation.composer.dock
+│     ├─ conversation.input.left
+│     ├─ conversation.input.right
+│     ├─ conversation.hero.brand.mark
+│     ├─ conversation.hero.workspace
+│     │  └─ conversation.hero.workspace.directoryFlow
+│     └─ conversation.hero.agentPreset
 ├─ rightbar
-│  ├─ sidebar.right.pane.tab
-│  │  └─ sidebar.right.tab.guide
-│  ├─ sidebar.right.pane.tab.title
-│  └─ sidebar.right.tab.menu.item
+│  └─ rightbar.session
+│     ├─ sidebar.right.pane.tab
+│     │  └─ sidebar.right.tab.guide
+│     ├─ sidebar.right.pane.tab.title
+│     └─ sidebar.right.tab.menu.item
 └─ shell.overlay
 ```
 

+ 41 - 36
docs/subsystems/slots.zh.md

@@ -82,6 +82,7 @@ Slot 声明固定两个相互独立的维度。
 |---|---|---|
 | 所有 scope | `useSessions`、`useSessionPendingInteraction` | `ui-session` |
 | 所有 scope | `useWorkspaces` | `ui-workspace` |
+| 所有作用域 | `usePanelInfo` | `ui-layout` |
 | `session` | `sessionId`、`useSession`、`useProjection` | `ui-session` |
 | `session-maybe` | 结果可选的 `sessionId`、`useSession`、`useProjection` | `ui-session` |
 | `session` | `useConversation`、`useInput`、`inputActions` | `ui-conversation` |
@@ -112,6 +113,8 @@ root
 ├─ sidebar
 │  ├─ sidebar.brand.mark
 │  ├─ sidebar.brand.name
+│  ├─ sidebar.panellist
+│  ├─ sidebar.panellist.title
 │  ├─ sidebar.footer.action
 │  ├─ sidebar.workspaces
 │  │  └─ sidebar.workspaces.directoryFlow
@@ -127,43 +130,45 @@ root
 │        ├─ settings.models.footer
 │        └─ settings.plugins.tab
 │           └─ settings.plugin.item
-├─ conversation
-│  ├─ conversation.session
-│  │  └─ conversation.view
-│  │     ├─ conversation.chat.node
-│  │     │  ├─ conversation.chat.assistant-actions
-│  │     │  ├─ conversation.chat.commandview
-│  │     │  ├─ conversation.chat.turnTail
-│  │     │  └─ tool.call.toolview
-│  │     │     ├─ tool.call.images
-│  │     │     └─ tool.view.cordis
-│  │     ├─ conversation.message.images
-│  │     └─ conversation.trajectory.images
-│  ├─ conversation.session.header
-│  │  ├─ conversation.session.header.lineage
-│  │  ├─ conversation.session.header.actions
-│  │  ├─ conversation.session.header.utilities
-│  │  └─ conversation.session.header.corner
-│  ├─ conversation.composer
-│  │  └─ conversation.approval.detail
-│  ├─ conversation.composer.bar
-│  │  ├─ conversation.input.attachments
-│  │  ├─ conversation.input.plan
-│  │  └─ conversation.input.model
-│  ├─ conversation.input.overlay
-│  ├─ conversation.input.dock
-│  ├─ conversation.composer.dock
-│  ├─ conversation.input.left
-│  ├─ conversation.input.right
-│  ├─ conversation.hero.brand.mark
-│  ├─ conversation.hero.workspace
-│  │  └─ conversation.hero.workspace.directoryFlow
-│  └─ conversation.hero.agentPreset
+├─ main
+│  └─ main.conversation
+│     ├─ conversation.session
+│     │  └─ conversation.view
+│     │     ├─ conversation.chat.node
+│     │     │  ├─ conversation.chat.assistant-actions
+│     │     │  ├─ conversation.chat.commandview
+│     │     │  ├─ conversation.chat.turnTail
+│     │     │  └─ tool.call.toolview
+│     │     │     ├─ tool.call.images
+│     │     │     └─ tool.view.cordis
+│     │     ├─ conversation.message.images
+│     │     └─ conversation.trajectory.images
+│     ├─ conversation.session.header
+│     │  ├─ conversation.session.header.lineage
+│     │  ├─ conversation.session.header.actions
+│     │  ├─ conversation.session.header.utilities
+│     │  └─ conversation.session.header.corner
+│     ├─ conversation.composer
+│     │  └─ conversation.approval.detail
+│     ├─ conversation.composer.bar
+│     │  ├─ conversation.input.attachments
+│     │  ├─ conversation.input.plan
+│     │  └─ conversation.input.model
+│     ├─ conversation.input.overlay
+│     ├─ conversation.input.dock
+│     ├─ conversation.composer.dock
+│     ├─ conversation.input.left
+│     ├─ conversation.input.right
+│     ├─ conversation.hero.brand.mark
+│     ├─ conversation.hero.workspace
+│     │  └─ conversation.hero.workspace.directoryFlow
+│     └─ conversation.hero.agentPreset
 ├─ rightbar
-│  ├─ sidebar.right.pane.tab
-│  │  └─ sidebar.right.tab.guide
-│  ├─ sidebar.right.pane.tab.title
-│  └─ sidebar.right.tab.menu.item
+│  └─ rightbar.session
+│     ├─ sidebar.right.pane.tab
+│     │  └─ sidebar.right.tab.guide
+│     ├─ sidebar.right.pane.tab.title
+│     └─ sidebar.right.tab.menu.item
 └─ shell.overlay
 ```
 

+ 2 - 2
packages/client/ui-conversation/README.i18n.yaml

@@ -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: edfcb83193d4513fba846f673d3572c35e8c0d1e
-README.zh.md: f92e4c77ba3d3a2ceb96758e5817e447ba2ec1ac
+README.md: 633a35b3a66056628a0eeaff0261c97ab5e93c33
+README.zh.md: f86dd863ecc338cca63956ae6e75bf9c7c5fea6a

+ 1 - 1
packages/client/ui-conversation/README.md

@@ -36,7 +36,7 @@ Target packages declaration-merge their snapshot and Location data maps, then re
 <a id="shell-and-standard-props"></a>
 ## Shell and standard props
 
-The package registers the optional-Session `conversation` shell, strict Session header/body entries, View list, composer chain and bar, input regions, Hero regions, queue dock, draft persistence, and phase calculation. `ctx.uiSession.provide()` materializes the Conversation and input sources from the same Session binding and supplies `inputActions` as a stable standard prop.
+The package occupies the root-scoped `main` key `conversation`, whose wrapper declares the optional-Session `main.conversation` shell. It registers strict Session header/body entries, View list, composer chain and bar, input regions, Hero regions, queue dock, draft persistence, and phase calculation. `ctx.uiSession.provide()` materializes the Conversation and input sources from the same Session binding and supplies `inputActions` as a stable standard prop.
 
 View selection is deterministic: a registered persisted selection wins, otherwise registered `chat` wins, otherwise no View renders. It never chooses the first registered View. Shell phase combines Session lifecycle with the active-target set; no target-specific snapshot is read by the shell.
 

+ 1 - 1
packages/client/ui-conversation/README.zh.md

@@ -36,7 +36,7 @@ target package 通过 declaration merge 扩展 snapshot 与 Location data map,
 <a id="shell-and-standard-props"></a>
 ## Shell 与标准 props
 
-本包注册 optional-Session `conversation` shell、strict Session header/body、View list、composer chain 与 bar、输入区域、Hero 区域、queue dock、草稿持久化和 phase 计算。`ctx.uiSession.provide()` 从同一个 Session binding 物化 Conversation 与 input source,并将 `inputActions` 作为稳定标准 prop 提供。
+本包占据 root 作用域 `main` 中的 `conversation` key,其包装层声明 optional-Session `main.conversation` shell。本包注册strict Session header/body、View list、composer chain 与 bar、输入区域、Hero 区域、queue dock、草稿持久化和 phase 计算。`ctx.uiSession.provide()` 从同一个 Session binding 物化 Conversation 与 input source,并将 `inputActions` 作为稳定标准 prop 提供。
 
 View 选择规则固定:有效且已注册的持久化选择优先,其次是已注册的 `chat`,否则不渲染 View;绝不选择第一个已注册 View。Shell phase 只组合 Session lifecycle 与 active-target set,不读取任何 target-specific snapshot。
 

+ 2 - 2
packages/client/ui-layout/README.i18n.yaml

@@ -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-layout/README.md
-README.md: 0e353e21f39ba8a84e661f1c540db2c2424bcbfa
-README.zh.md: 16471fdabb46bbfb35158ef85aa2544db4ceae93
+README.md: 50fef3853cdab83c7c04edd46d73a31967a495b7
+README.zh.md: d649643155dd5e23ee185ff282aa14011ed73793

+ 5 - 3
packages/client/ui-layout/README.md

@@ -25,7 +25,9 @@ This package provides the Web GUI's three-column AppFrame, edge-column widths, a
 <a id="use-this-package"></a>
 ## Use this package
 
-The root slot composes the sidebar, conversation, and right column. The sidebar spans 264–420px, defaults to 280px, and retains a 56px rail when collapsed; below 1024px it collapses automatically, and opening the right panel collapses a manually expanded sidebar. The right panel first opens at 45% of the viewport, then retains the user's pixel preference, capped at 70%. To protect 400px for the center, the frame first reduces the right panel to 300px, then reports insufficient room so its occupant closes it, and only then compresses the center further. Dragging has no transition delay; the right handle is absent while closed or fullscreen.
+The root slot composes the sidebar, main content, and right column. The sidebar spans 264–420px, defaults to 280px, and retains a 56px rail when collapsed; below 1024px it collapses automatically, and opening the right panel collapses a manually expanded sidebar. The right panel first opens at 45% of the viewport, then retains the user's pixel preference, capped at 70%. To protect 400px for the center, the frame first reduces the right panel to 300px, then reports insufficient room so its occupant closes it, and only then compresses the center further. Dragging has no transition delay; the right handle is absent while closed or fullscreen.
+
+Global panels occupy the root-scoped `main` keyed slot; `conversation` is the reserved key for the Conversation. `ctx.layout.selectPanel(id)` selects a registered panel, and `null` selects the Conversation without changing the current Session. No global panel is registered by the shipped composition.
 
 ### Theme presentation
 
@@ -39,7 +41,7 @@ The presenter consumes resolved theme snapshots and projects them onto the docum
 <details>
 <summary>Implementation internals — click to expand</summary>
 
-One registration declares four child slots and binds `ctx.layout` methods `toggleSidebar`, `openRightbar(track, fullscreen)`, and `closeRightbar`. The store owns the single frame-width measurement, width preferences, and the occupant's presentation report. The `rightbar` owner supplies actual `width`, `viewportWidth`, and normal-presentation eligibility `canShow`; insufficient room causes a deterministic close, never automatic reopening on widening. Fullscreen hides the width handle without releasing a track the occupant retains. AppFrame always mounts the conversation and right columns; a connected Session renders through `SessionProvider`, and without one the right column is an empty zero-width track. It projects the selected Session title over the build-configured product title or the localized `common.brand.localBuild` fallback, so locale revisions update document metadata with the root entry. The theme presenter is a second effect: pure DOM writes from resolved snapshots — initial state through the getter once, then event-driven only, with no React path. It applies palette, font-size, and token variables before measuring the rendered background as the single color authority. Fullscreen presentation suppresses grid and handle transitions; its occupant reports the new columns only after covering the frame. Fullscreen exit keeps transitions suppressed while the frame installs its destination geometry: close removes the right track, and restore retains it. Subsequent normal geometry actions restore ordinary transitions.
+One registration declares four child slots and binds `ctx.layout` methods `selectPanel`, `toggleSidebar`, `openRightbar(track, fullscreen)`, and `closeRightbar`. One root store separates `panelInfo` selection from `layoutInfo` measurements, width preferences, and presentation reports. `usePanelInfo` subscribes to the stable selection object; AppFrame subscribes to the stable layout object. The `rightbar` owner supplies actual `width`, `viewportWidth`, and normal-presentation eligibility `canShow`; insufficient room causes a deterministic close, never automatic reopening on widening. Fullscreen hides the width handle without releasing a track the occupant retains. AppFrame keeps the column containers mounted. The right column's root controller renders `rightbar.session` through `SessionProvider` only while the Conversation is selected; its unmount report releases the track. The independent title component uses the selected Session title only while the Conversation is visible, with the build-configured product title or localized `common.brand.localBuild` as its fallback; locale revisions update that fallback. The theme presenter is a second effect: pure DOM writes from resolved snapshots — initial state through the getter once, then event-driven only, with no React path. It applies palette, font-size, and token variables before measuring the rendered background as the single color authority. Fullscreen presentation suppresses grid and handle transitions; its occupant reports the new columns only after covering the frame. Fullscreen exit keeps transitions suppressed while the frame installs its destination geometry: close removes the right track, and restore retains it. Subsequent normal geometry actions restore ordinary transitions.
 
 </details>
 
@@ -51,7 +53,7 @@ One registration declares four child slots and binds `ctx.layout` methods `toggl
 Read these pages when the layout surface is not enough. They move from the frame to the columns it renders and the theme it presents.
 
 - [ui-sidebar](../ui-sidebar/README.md) — occupies the `sidebar` column and its seats.
-- [ui-conversation](../ui-conversation/README.md) — occupies the `conversation` column.
+- [ui-conversation](../ui-conversation/README.md) — occupies the `main` key `conversation`.
 - [ui-sidebar-right](../ui-sidebar-right/README.md) — occupies the `rightbar` column with one docking surface per session.
 - [ui-theme](../ui-theme/README.md) — the theme seam whose resolved snapshots the presenter consumes.
 - [Web client architecture](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) — how browser plugin rows load and register slots.

+ 5 - 3
packages/client/ui-layout/README.zh.md

@@ -25,7 +25,9 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-本插件在 root slot 组合侧栏、会话与右栏。左栏为264~420px,默认280px,收起后保留56px;窗口低于1024px时自动收起,打开右栏也会收起手动展开的左栏。右栏首次打开使用窗口宽度的45%,之后保留用户像素偏好,上限为70%;中栏不足400px时先把右栏压到300px,仍不足则通知占用方收起,最后才继续压缩中栏。拖拽跟手且无过渡延迟,关闭或全屏时不显示右栏拖拽区。
+本插件在 root slot 组合侧栏、中央内容与右栏。左栏为264~420px,默认280px,收起后保留56px;窗口低于1024px时自动收起,打开右栏也会收起手动展开的左栏。右栏首次打开使用窗口宽度的45%,之后保留用户像素偏好,上限为70%;中栏不足400px时先把右栏压到300px,仍不足则通知占用方收起,最后才继续压缩中栏。拖拽跟手且无过渡延迟,关闭或全屏时不显示右栏拖拽区。
+
+全局面板占据 root 作用域的 `main` keyed slot;`conversation` 是为会话界面保留的 key。`ctx.layout.selectPanel(id)` 选中已注册面板,`null` 则选中会话界面,但不改变当前会话。默认组合不注册任何全局面板。
 
 ### 主题呈现
 
@@ -39,7 +41,7 @@ kind: "package-reference"
 <details>
 <summary>实现细节——点击展开</summary>
 
-一次注册声明四个子slot并绑定 `ctx.layout` 的 `toggleSidebar`、`openRightbar(track, fullscreen)` 与 `closeRightbar`。store持有唯一的frame宽度测量、左右栏偏好及占用方报告的呈现状态。`rightbar` 的owner参数为实际 `width`、`viewportWidth` 与普通呈现的 `canShow`;占用方在空间不足时执行确定性的收起,变宽不自行重新展开。全屏隐藏宽度手柄,但不自行释放占用方要求保留的轨道。AppFrame 始终挂载会话与右栏;已连接 Session 经 `SessionProvider` 渲染,没有 Session 时右栏是一条空的零宽轨道。它把所选 Session 标题投影到构建配置的产品标题或本地化 `common.brand.localBuild` 回退值之上,因此 locale revision 会随根 entry 一起更新文档元数据。主题呈现器是第二个 effect:从解析后的快照做纯 DOM 写入——初始状态经 getter 读取一次,此后仅事件驱动,不经过 React。它先应用调色板、字号与 token 变量,再把渲染出的背景测量为唯一的颜色依据。 全屏呈现禁用网格和手柄过渡;占用方完全覆盖框架后才报告新的列宽。 退出全屏时,框架先保持无过渡并安装目标布局:关闭移除右轨道,恢复保留右轨道。后续普通几何操作恢复正常过渡。
+一次注册声明四个子slot并绑定 `ctx.layout` 的 `selectPanel`、`toggleSidebar`、`openRightbar(track, fullscreen)` 与 `closeRightbar`。同一个 root 存储把 `panelInfo` 选中态与 `layoutInfo` 测量、宽度偏好、呈现报告分开。`usePanelInfo` 订阅引用稳定的选中态对象,AppFrame 订阅引用稳定的布局对象。`rightbar` 的owner参数为实际 `width`、`viewportWidth` 与普通呈现的 `canShow`;占用方在空间不足时执行确定性的收起,变宽不自行重新展开。全屏隐藏宽度手柄,但不自行释放占用方要求保留的轨道。AppFrame 保持各列容器挂载。右栏的 root 控制器仅在选中会话界面时,经 `SessionProvider` 渲染 `rightbar.session`;内容卸载时的报告释放列宽。独立的标题组件仅在会话界面可见时使用所选会话标题,以构建配置的产品标题或本地化 `common.brand.localBuild` 为回退值;语言变化会更新该回退值。主题呈现器是第二个 effect:从解析后的快照做纯 DOM 写入——初始状态经 getter 读取一次,此后仅事件驱动,不经过 React。它先应用调色板、字号与 token 变量,再把渲染出的背景测量为唯一的颜色依据。 全屏呈现禁用网格和手柄过渡;占用方完全覆盖框架后才报告新的列宽。 退出全屏时,框架先保持无过渡并安装目标布局:关闭移除右轨道,恢复保留右轨道。后续普通几何操作恢复正常过渡。
 
 </details>
 
@@ -51,7 +53,7 @@ kind: "package-reference"
 当布局面不够用时阅读以下页面。它们从框架进入它所渲染的栏与它所呈现的主题。
 
 - [ui-sidebar](../ui-sidebar/README.zh.md)——占据 `sidebar` 栏及其座位。
-- [ui-conversation](../ui-conversation/README.zh.md)——占据 `conversation` 栏
+- [ui-conversation](../ui-conversation/README.zh.md)——占据 `main` 中的 `conversation` key
 - [ui-sidebar-right](../ui-sidebar-right/README.zh.md)——以每会话一个停靠面占据 `rightbar` 栏。
 - [ui-theme](../ui-theme/README.zh.md)——呈现器消费其解析快照的主题 seam。
 - [Web 客户端架构](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md)——浏览器插件行如何加载并注册槽位。

+ 2 - 2
packages/client/ui-sidebar-right/README.i18n.yaml

@@ -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-sidebar-right/README.md
-README.md: feb75c6184098f54961ca161b4dc5ca5d0c7fc78
-README.zh.md: 03c841dfdaac970e938cf934e7f09d151268be8b
+README.md: 7520a927f8e77b4fca1bbb7e61fe415dda814b4b
+README.zh.md: 648a2c20d8e55a48a684a92a351b7900f3a8d517

+ 2 - 0
packages/client/ui-sidebar-right/README.md

@@ -54,6 +54,8 @@ While the panel is hidden, one button in the conversation header's corner seat (
 
 The panel takes the conversation's ground colour and content font sizes rather than a raised layer of its own: it is a column of the page, not a card over it.
 
+The `rightbar` entry is a root-scoped controller. It reads `usePanelInfo` and mounts the Session-scoped `rightbar.session` subtree only while the Conversation is selected. Switching to a global panel hides the right Sidebar and releases its frame track without deleting the Session's tab state.
+
 <a id="state"></a>
 ## State
 

+ 2 - 0
packages/client/ui-sidebar-right/README.zh.md

@@ -54,6 +54,8 @@ kind: "package-reference"
 
 面板取会话区的底色与正文字号,而不是自成一层浮起的表面:它是页面的一列,不是压在页面上的卡片。
 
+`rightbar` 入口是 root 作用域的控制器。它读取 `usePanelInfo`,仅在选中会话界面时挂载 session 作用域的 `rightbar.session` 子树。切换到全局面板会隐藏右侧 Sidebar 并释放框架列宽,但不删除会话的 tab 状态。
+
 <a id="state"></a>
 ## 状态
 

+ 2 - 2
packages/client/ui-sidebar/README.i18n.yaml

@@ -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-sidebar/README.md
-README.md: 87b04e965fbe26591e38d49a5c272113edfd28aa
-README.zh.md: 5a7f050d7099437e5c8be965e89be405492d5353
+README.md: 00c0ffce05f19c86bff86d0eff01154b98d24aeb
+README.zh.md: 4e4c9777a30680a5015e69c11c8cb57b187f1b0a

+ 5 - 1
packages/client/ui-sidebar/README.md

@@ -31,6 +31,10 @@ The sidebar is the navigation shell: users see the brand, start new sessions, co
 
 The expanded brand row renders `sidebar.brand.mark` and `sidebar.brand.name` as independent single slots; the collapsed rail renders the same mark slot. Without occupants, the shell uses the fish mark and a localized local-build label. A complete build stacks a code badge below the label as `version[-commit][-dirty]`, using `DSH_CLIENT_VERSION`, the optional 7-character `DSH_CLIENT_COMMIT_HASH`, and `DSH_CLIENT_GIT_DIRTY=true`; missing version metadata omits the badge. New Session targets the explicit Workspace used by a scoped action, otherwise the current Session's Workspace, otherwise the most recently active Workspace; when none exists it clears into the blank New Session page.
 
+### Global panel entries
+
+Plugins add an icon component to the root-scoped `sidebar.panellist` list with an `id`, optional `order`, and a string or locale-aware `label`. The same id addresses the component registered in the layout's root-scoped `main` keyed slot. An optional `sidebar.panellist.title` keyed registration replaces the label's visible content; the label remains the accessible name. Each row reads its own selected state through `usePanelInfo`. With no registrations, neither the list nor spacing for it is rendered. The shipped composition registers no example panel.
+
 ### Collapse behavior
 
 During a live collapse, the expanded content fades out at its current width, the upper controls share one fade and leftward translation into the 56px rail, and the layout's column slide ends the motion. A page that starts collapsed renders the rail statically, and reduced-motion mode disables both transitions. The bottom-pinned `sidebar.settings` control shares the fade timing but has no horizontal translation.
@@ -47,7 +51,7 @@ Scrollbars in the column are a pointer affordance: the shell rebinds the scrollb
 <details>
 <summary>Implementation internals — click to expand</summary>
 
-The shell is pure composition: `SidebarRootComponentProps` composes the layout owner share, the global `useSessions` and `useWorkspaces` hooks, the declared brand, the `sidebar.workspaces` and `sidebar.settings` child slots, and injected `startSession` plus sidebar-toggle callbacks. There is no plugin store.
+The shell is pure composition: `SidebarRootComponentProps` composes the layout owner share, the global `useSessions` and `useWorkspaces` hooks, the declared brand, the `sidebar.workspaces` and `sidebar.settings` child slots, and injected navigation callbacks. Panel entries and their optional titles use the same composition path. Panel metadata is derived from list registrations and locale changes; selection belongs to the layout store.
 
 ### Slot discipline
 

+ 5 - 1
packages/client/ui-sidebar/README.zh.md

@@ -31,6 +31,10 @@ dsh Web 客户端的侧边栏让用户识别当前构建、启动新会话、将
 
 展开的品牌行把 `sidebar.brand.mark` 与 `sidebar.brand.name` 渲染为两个独立的 single slot;收起轨道则渲染同一个 mark slot。没有占位者时,外壳使用鱼形标记和本地化的本地构建标签。完整构建会在标签下方显示代码徽标;该徽标使用 `DSH_CLIENT_VERSION`、可选的 7 位 `DSH_CLIENT_COMMIT_HASH` 与 `DSH_CLIENT_GIT_DIRTY=true` 组装成 `version[-commit][-dirty]`;缺少版本元数据时不显示徽标。New Session 优先使用作用域操作明确指定的 Workspace,否则使用当前 Session 所属 Workspace,再否则使用最近活跃 Workspace;一个 Workspace 都没有时则清空选择,进入空白 New Session 页面。
 
+### 全局面板入口
+
+插件在 root 作用域的 `sidebar.panellist` list 中注册图标组件,提供 `id`、可选 `order`,以及字符串或随语言变化的 `label`。同一个 id 寻址布局中 root 作用域 `main` keyed slot 的组件。可选的 `sidebar.panellist.title` keyed 注册替换标签的可见内容;`label` 仍用作无障碍名称。每一行通过 `usePanelInfo` 读取自己的选中态。没有注册项时,列表及其间距均不渲染。默认组合不注册示例面板。
+
 ### 折叠行为
 
 实时收起时,展开内容在当前宽度淡出,上方控件共用一次淡入并左移进入 56px 轨道,由布局的栏滑动结束整段动画。页面初始即为收起状态时会静态渲染轨道;减少动态效果模式会禁用两段过渡。固定在底部的 `sidebar.settings` 控件只共用淡入时序,不发生横向位移。
@@ -47,7 +51,7 @@ dsh Web 客户端的侧边栏让用户识别当前构建、启动新会话、将
 <details>
 <summary>实现细节——点击展开</summary>
 
-外壳是纯组合:`SidebarRootComponentProps` 组合布局 owner share、全局 `useSessions` 与 `useWorkspaces` 钩子、已声明的品牌、`sidebar.workspaces` 与 `sidebar.settings` 子 slot,以及注入的 `startSession` 与侧边栏切换回调。这里没有插件 store
+外壳是纯组合:`SidebarRootComponentProps` 组合布局 owner share、全局 `useSessions` 与 `useWorkspaces` 钩子、已声明的品牌、`sidebar.workspaces` 与 `sidebar.settings` 子 slot,以及注入的导航回调。面板入口及其可选标题使用相同的组合方式。面板元数据由列表注册和语言变化派生;选中态属于布局存储
 
 ### Slot 纪律
 

+ 2 - 2
packages/client/ui-workspace/README.i18n.yaml

@@ -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-workspace/README.md
-README.md: a819b07fecb74faeb3bebb6665aaabfdc8ecfba6
-README.zh.md: f9565b12bccd596fa61d1f1436f82b090139f506
+README.md: 231cff649addac22c4383f372201c2ce79a22100
+README.zh.md: e8bac20531bee009cb61db42749597a415de4279

+ 2 - 0
packages/client/ui-workspace/README.md

@@ -51,6 +51,8 @@ The value is intentionally best effort for cold Sessions. An identity-matching u
 
 -----
 
+`ctx.uiWorkspace.openSession(id)` selects the Session and returns the main area to the Conversation as one UI navigation action, including when that Session was already current. New Session uses the same path after resolving its target; selection failure leaves a global panel visible. Session rows read `usePanelInfo` to suppress their selected appearance while a global panel is active.
+
 <a id="understand-the-implementation"></a>
 ## Understand the implementation
 

+ 2 - 0
packages/client/ui-workspace/README.zh.md

@@ -51,6 +51,8 @@ Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示**
 
 -----
 
+`ctx.uiWorkspace.openSession(id)` 将选中会话与中央区域返回会话界面作为一次 UI 导航操作,包括目标会话已经是当前会话的情况。新会话操作解析目标后使用同一路径;选中失败时保留当前全局面板。会话行读取 `usePanelInfo`,在全局面板活跃时不显示会话选中样式。
+
 <a id="understand-the-implementation"></a>
 ## 理解实现
 

+ 2 - 2
packages/test-support/client-runtime/README.i18n.yaml

@@ -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/test-support/client-runtime/README.md
-README.md: 616eab2cd5ef03ae4e514ca335b0c60b43d62cf0
-README.zh.md: 08effcef7eaacad67b5496507925337e772d4ce9
+README.md: 38b0c1b305d83920a819c6eddf9b54ecc895ef7f
+README.zh.md: 301e3633d25a7fa7599eb9283d141c641163d588

+ 3 - 1
packages/test-support/client-runtime/README.md

@@ -29,7 +29,7 @@ This package gives a browser feature spec a real runtime to mount against: creat
 
 ### Setting up a feature spec
 
-`SlotTestRuntime.create()` assembles the runtime, `declare(children)` registers an auto frame whose per-key `<div data-slot>` wrappers become snapshot roots, `mount(plugin)` runs the feature on a real fiber, and `renderSlot(key, owner)` returns the slot-local view with scoped queries and in-place updates:
+`SlotTestRuntime.create()` assembles the runtime, `declare(children)` registers an auto frame whose per-key `<div data-slot>` wrappers become snapshot roots, `mount(plugin)` runs the feature on a real fiber, and `renderSlot(key, owner, opts?)` returns the slot-local view with scoped queries and in-place updates:
 
 ```text
 const runtime = await SlotTestRuntime.create()
@@ -42,6 +42,8 @@ await runtime.dispose()
 
 `mount` prechecks required services and fails loud when one is missing — `provide(name, value)` supplies an extra service first. The runtime provides an unavailable `fileUpload` stub so assemblies can mount; replace `runtime.fileUpload.upload` before mounting when a test exercises upload behavior. `storeOf(key, scopeKey)` returns the live store instance the renderer hands a slot's component for identity and action-driven-write assertions.
 
+The optional render options select a keyed entry with `entryKey` or a list item with `only`; `view.update(owner)` retains that selection. `runtime.panelInfo` supplies the default `usePanelInfo` source with no global panel selected. Release it with `releasePanelInfoSource()` before mounting the production Layout owner.
+
 ### Local DOM snapshots
 
 A registered snapshot serializer folds CSS-module class hashes (`_frame_a1b2c3` → `frame`) so `.snap` files stay structural, and collapses `<svg>` internals to a `data-content` fingerprint. Suites needing a custom page frame use `root.declare(children, Frame)` instead of the auto frame; `dispose()` tears down views, feature fibers, minted scopes, and persisted store state on one axis and is idempotent.

+ 3 - 1
packages/test-support/client-runtime/README.zh.md

@@ -29,7 +29,7 @@ kind: "package-library"
 
 ### 搭建功能测试
 
-`SlotTestRuntime.create()` 组装运行时,`declare(children)` 注册一个自动 frame,其逐 key 的 `<div data-slot>` 包裹层成为快照根,`mount(plugin)` 在真实 fiber 上运行功能,`renderSlot(key, owner)` 返回带限定查询与原位更新的 slot 局部视图:
+`SlotTestRuntime.create()` 组装运行时,`declare(children)` 注册一个自动 frame,其逐 key 的 `<div data-slot>` 包裹层成为快照根,`mount(plugin)` 在真实 fiber 上运行功能,`renderSlot(key, owner, opts?)` 返回带限定查询与原位更新的 slot 局部视图:
 
 ```text
 const runtime = await SlotTestRuntime.create()
@@ -42,6 +42,8 @@ await runtime.dispose()
 
 `mount` 会预检必需服务,缺失时自明报错——先用 `provide(name, value)` 提供额外服务。运行时会提供不可用的 `fileUpload` 替身,使装配可以挂载;测试上传行为时,需要在挂载前替换 `runtime.fileUpload.upload`。`storeOf(key, scopeKey)` 返回渲染器交给 slot 组件的实时 store 实例,用于身份与动作驱动写入断言。
 
+可选渲染参数通过 `entryKey` 选择 keyed 条目,或通过 `only` 选择 list 条目;`view.update(owner)` 保留该选择。`runtime.panelInfo` 提供默认的 `usePanelInfo` 数据源,初始不选中全局面板。挂载生产 Layout 所有者之前,先调用 `releasePanelInfoSource()` 释放该数据源。
+
 ### 局部 DOM 快照
 
 注册的快照序列化器把 CSS-module 哈希类名折回语义名(`_frame_a1b2c3` → `frame`),使 `.snap` 文件只含结构,并把 `<svg>` 内部折叠为 `data-content` 指纹。需要自定义页面 frame 的套件改用 `root.declare(children, Frame)` 而非自动 frame;`dispose()` 沿单一轴拆除视图、feature fiber、已铸 scope 与持久化 store 状态,且幂等。