@deepseek-ai/dsh-client-ui-layout 0.1.3-alpha.2 → 0.1.5-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-layout/README.md
5
- README.md: aa00242f1d63924b990cfec23bd0438332662b06
6
- README.zh.md: 801346967f9fbb031e5e6c2612dff84af79f2d45
5
+ README.md: 4fe19fb9ec0a549dc76ca311d0564cbe86c9f5f9
6
+ README.zh.md: 7a61e7808ffb3d81214388a2792a901ced8d9564
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Shell layout for the Web GUI: the three-column AppFrame with drag handles, concession behavior, the panel-geometry service, and theme presentation; for users and maintainers of the window chrome."
2
+ description: "Shell layout for the Web GUI: the three-column AppFrame whose right column is a track for an edge-anchored panel, the panel-geometry service, and theme presentation; for users and maintainers of the window chrome."
3
3
  kind: "package-reference"
4
4
  ---
5
5
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- This package provides the shell layout of the Web GUI: a three-column AppFrame with resizable sidebar and details panels, a concession chain that shrinks the details column and then auto-closes it when space runs out, and the `ctx.layout` panel-geometry service other plugins call to open or close the details column. It also seats the theme presenter, which projects the resolved color scheme, alias tokens, content font size, and `theme-color` metadata onto the document. Choose it for the standard window chrome; panel geometry is transient and resets on reload.
12
+ This package provides the Web GUI's three-column AppFrame, edge-column widths, and `ctx.layout` presentation control. The right column concedes space before the center; its occupant renders fullscreen while the frame retains the wide-screen track underneath. The theme presenter owns color scheme, alias tokens, content font size, and document metadata. Layout state resets on reload.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -25,7 +25,9 @@ This package provides the shell layout of the Web GUI: a three-column AppFrame w
25
25
  <a id="use-this-package"></a>
26
26
  ## Use this package
27
27
 
28
- Mount this plugin at the root slot; it then renders the app frame around whatever occupies the sidebar, conversation, and details columns. Users resize the sidebar by dragging its invisible hit strip and the details panel by dragging its floating pill; when the window narrows, only details shrinks, then auto-closes. A closed sidebar retains a 56px control rail; details closes to zero width.
28
+ 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.
29
+
30
+ 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.
29
31
 
30
32
  ### Theme presentation
31
33
 
@@ -39,7 +41,9 @@ The presenter consumes resolved theme snapshots and projects them onto the docum
39
41
  <details>
40
42
  <summary>Implementation internals — click to expand</summary>
41
43
 
42
- One `register()` call contributes `AppFrame` into the runtime's built-in `'root'` slot and, in the same breath, declares the four child slots (`sidebar`, `conversation`, `details`, `shell.overlay`), seats the layout store (panel geometry), and wires the `ctx.layout` panel-action service. The transient layout store starts the sidebar at its default width and details closed, and never reads or writes `localStorage`. AppFrame always mounts the conversation and details columns; a connected Session renders through `SessionProvider`. 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.
44
+ `selectPanel(id)` checks the live `main` registry before changing selection; an absent key throws and leaves the current panel intact. `beginNavigation()` returns an abort signal for an asynchronous UI navigation. A later call, a valid panel selection (including repeated selection), or layout disposal aborts that signal without cancelling underlying Session creation. Consumers check the signal before committing navigation or moving drafts.
45
+
46
+ 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.
43
47
 
44
48
  </details>
45
49
 
@@ -51,7 +55,8 @@ One `register()` call contributes `AppFrame` into the runtime's built-in `'root'
51
55
  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.
52
56
 
53
57
  - [ui-sidebar](../ui-sidebar/README.md) — occupies the `sidebar` column and its seats.
54
- - [ui-conversation](../ui-conversation/README.md) — occupies the `conversation` and `details` columns.
58
+ - [ui-conversation](../ui-conversation/README.md) — occupies the `main` key `conversation`.
59
+ - [ui-sidebar-right](../ui-sidebar-right/README.md) — occupies the `rightbar` column with one docking surface per session.
55
60
  - [ui-theme](../ui-theme/README.md) — the theme seam whose resolved snapshots the presenter consumes.
56
61
  - [Web client architecture](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) — how browser plugin rows load and register slots.
57
62
 
@@ -73,8 +78,9 @@ None; this package neither assembles nor sends a provider request.
73
78
 
74
79
  These limits define the current layout behavior. They are current package constraints, not a general window-manager comparison or a task backlog.
75
80
 
76
- - **Panel geometry is transient** — reload restores the sidebar default and details closed; switching between distinct Session ids also closes details and forgets its dragged width, while unselected surfaces render details at zero width without modifying geometry.
77
- - **Concession-chain auto-close derives a zero width without touching the preferred width** the panel restores itself when the window widens; consumers must not read the stored details width as the rendered truth.
81
+ - **Panel geometry is transient** — reload restores the sidebar default and the right panel hidden; each dragged width is one frame-wide preference, not a per-Session fact.
82
+ - **Extremely narrow windows** after the right panel closes, the center may still fall below 400px; the left 56px rail remains.
83
+ - **Track and panel travel on one shared curve** — the frame's track transition and the occupant's slide read the same duration and easing variables; an occupant that used its own would detach the panel's edge from the conversation's while squeezing.
78
84
  - **No scroll anchoring during squeeze reflow** — layout changes may move the reader's viewport.
79
85
 
80
86
  <a id="dev-note"></a>
@@ -87,4 +93,4 @@ None.
87
93
 
88
94
  </details>
89
95
 
90
- **Runtime invariant:** No companion is published. The shell viewing-state store behind ctx.layout emits no cordis events; clamp/prune/concession-chain sequencing is asserted directly by this package's columns and service specs.
96
+ **Runtime invariant:** No companion is published. The shell viewing-state store behind ctx.layout emits no cordis events; clamp and track sequencing is asserted directly by this package's columns and service specs.
package/README.zh.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Web GUI 的外壳布局:三栏 AppFrame、拖动手柄与让步行为、面板几何服务与主题呈现;供窗口外观的用户与维护者阅读。"
2
+ description: "Web GUI 的外壳布局:三栏 AppFrame——其右栏是贴边面板的轨道——面板几何服务与主题呈现;供窗口外观的用户与维护者阅读。"
3
3
  kind: "package-reference"
4
4
  ---
5
5
 
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- 本包提供 Web GUI 的外壳布局:一个三栏 AppFrame,带可缩放的侧栏与详情面板;一条让步链,在空间不足时先收缩详情栏、随后自动关闭它;以及 `ctx.layout` 面板几何服务,供其他插件调用以打开或关闭详情栏。它还承载主题呈现器,把解析后的配色方案、别名 token、正文字号与 `theme-color` 元数据投影到 document。需要标准窗口外观时选择它;面板几何是瞬时的,重新加载即重置。
12
+ 本包提供 Web GUI 的三栏 AppFrame、左右栏宽度与 `ctx.layout` 呈现控制。右栏先让步以保护中栏空间,全屏由占用方呈现,框架保留宽屏底层轨道。主题呈现器负责配色、别名 token、正文字号与 document 元数据;布局状态在刷新后重置。
13
13
 
14
14
  ## 目录
15
15
 
@@ -25,7 +25,9 @@ kind: "package-reference"
25
25
  <a id="use-this-package"></a>
26
26
  ## 使用本包
27
27
 
28
- root 槽位挂载本插件;它随即围绕占据侧栏、会话与详情栏的内容渲染应用框架。用户拖动不可见命中条带缩放侧栏、拖动浮动胶囊缩放详情面板;窗口变窄时只有详情栏收缩,随后自动关闭。关闭的侧栏保留 56px 控制栏;详情栏关闭到零宽度。
28
+ 本插件在 root slot 组合侧栏、中央内容与右栏。左栏为264~420px,默认280px,收起后保留56px;窗口低于1024px时自动收起,打开右栏也会收起手动展开的左栏。右栏首次打开使用窗口宽度的45%,之后保留用户像素偏好,上限为70%;中栏不足400px时先把右栏压到300px,仍不足则通知占用方收起,最后才继续压缩中栏。拖拽跟手且无过渡延迟,关闭或全屏时不显示右栏拖拽区。
29
+
30
+ 全局面板占据 root 作用域的 `main` keyed slot;`conversation` 是为会话界面保留的 key。`ctx.layout.selectPanel(id)` 选中已注册面板,`null` 则选中会话界面,但不改变当前会话。默认组合不注册任何全局面板。
29
31
 
30
32
  ### 主题呈现
31
33
 
@@ -39,7 +41,9 @@ kind: "package-reference"
39
41
  <details>
40
42
  <summary>实现细节——点击展开</summary>
41
43
 
42
- 一次 `register()` 调用把 `AppFrame` 贡献进运行时的内建 `'root'` 槽位,并在同一刻声明四个子槽位(`sidebar`、`conversation`、`details`、`shell.overlay`)、安放布局 store(面板几何)并接好 `ctx.layout` 面板动作服务。瞬时布局 store 以默认宽度启动侧栏、保持详情栏关闭,从不读写 `localStorage`。AppFrame 始终挂载会话与详情两栏;已连接 Session 经 `SessionProvider` 渲染。它把所选 Session 标题投影到构建配置的产品标题或本地化 `common.brand.localBuild` 回退值之上,因此 locale revision 会随根 entry 一起更新文档元数据。主题呈现器是第二个 effect:从解析后的快照做纯 DOM 写入——初始状态经 getter 读取一次,此后仅事件驱动,不经过 React。它先应用调色板、字号与 token 变量,再把渲染出的背景测量为唯一的颜色依据。
44
+ `selectPanel(id)` 在改变选中态前检查实时 `main` 注册表;缺失的 key 会抛错并保留当前面板。`beginNavigation()` 为异步 UI 导航返回 abort signal。后续调用、有效面板选择(包括重复选择)或布局释放会中止该 signal,但不取消底层会话创建。消费者在提交导航或搬移草稿前检查 signal。
45
+
46
+ 一次注册声明四个子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 变量,再把渲染出的背景测量为唯一的颜色依据。 全屏呈现禁用网格和手柄过渡;占用方完全覆盖框架后才报告新的列宽。 退出全屏时,框架先保持无过渡并安装目标布局:关闭移除右轨道,恢复保留右轨道。后续普通几何操作恢复正常过渡。
43
47
 
44
48
  </details>
45
49
 
@@ -51,7 +55,8 @@ kind: "package-reference"
51
55
  当布局面不够用时阅读以下页面。它们从框架进入它所渲染的栏与它所呈现的主题。
52
56
 
53
57
  - [ui-sidebar](../ui-sidebar/README.zh.md)——占据 `sidebar` 栏及其座位。
54
- - [ui-conversation](../ui-conversation/README.zh.md)——占据 `conversation` `details` 栏。
58
+ - [ui-conversation](../ui-conversation/README.zh.md)——占据 `main` 中的 `conversation` key。
59
+ - [ui-sidebar-right](../ui-sidebar-right/README.zh.md)——以每会话一个停靠面占据 `rightbar` 栏。
55
60
  - [ui-theme](../ui-theme/README.zh.md)——呈现器消费其解析快照的主题 seam。
56
61
  - [Web 客户端架构](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md)——浏览器插件行如何加载并注册槽位。
57
62
 
@@ -73,8 +78,9 @@ kind: "package-reference"
73
78
 
74
79
  这些限制界定了当前布局行为。它们是当前包约束,不是通用窗口管理器对比或任务积压。
75
80
 
76
- - **面板几何是瞬时状态**——重新加载会恢复侧栏默认值并保持详情栏关闭;在不同会话 id 之间切换同样会关闭详情栏并忘记拖动后的宽度,而未选中表面以零宽度渲染详情栏却不修改几何。
77
- - **让步链自动关闭通过推导零宽度实现,不触碰偏好宽度**——窗口变宽时面板自行恢复;消费方不得把 store 中的详情宽度当作渲染真值。
81
+ - **面板几何是瞬时状态**——重新加载会恢复侧栏默认值并隐藏右侧面板;每个拖出的宽度都是一份框架级偏好,不是按 Session 的事实。
82
+ - **极窄窗口**——右栏关闭后,中栏仍可能小于400px;左侧56px控制栏保留。
83
+ - **轨道与面板沿同一条曲线运动**——框架的轨道过渡和占位方的滑入读取同一组时长与缓动变量;占位方若自用一套,挤压时面板边缘就会与对话边缘脱开。
78
84
  - **挤压重排期间无滚动锚定**——布局变化可能移动读者的视口。
79
85
 
80
86
  <a id="dev-note"></a>
@@ -87,4 +93,4 @@ kind: "package-reference"
87
93
 
88
94
  </details>
89
95
 
90
- **运行时不变式:** 不发布伴生入口。`ctx.layout` 后的 viewing-state store 不发出 Cordis 事件;clamp、pruneconcession-chain 顺序由本包测试覆盖。
96
+ **运行时不变式:** 不发布伴生入口。`ctx.layout` 后的 viewing-state store 不发出 Cordis 事件;clamp 与轨道的时序由本包的 columns service 规格直接断言。
package/lib/client.js CHANGED
@@ -11,6 +11,10 @@ window.__ModuleLoader__.load({
11
11
  * LG breakpoint); a manual toggle below it re-expands over the squeezed center
12
12
  * (stores.ts narrowExpanded). */
13
13
  const SIDEBAR_AUTO_COLLAPSE = 1024;
14
+ /** Maximum normal right panel width as a fraction of the frame. */
15
+ const RIGHTBAR_MAX_RATIO = .7;
16
+ /** First-open right panel preference as a fraction of the frame. */
17
+ const RIGHTBAR_DEFAULT_RATIO = .45;
14
18
  /**
15
19
  * Clamp a panel width into its contract range.
16
20
  * @param px - requested width.
@@ -22,44 +26,38 @@ window.__ModuleLoader__.load({
22
26
  return Math.min(max, Math.max(min, Math.round(px)));
23
27
  }
24
28
  /**
25
- * Solve the three column widths for one viewport frame. Pure: no hysteresis —
26
- * the output is a function of (viewport, preferences) only, so recovery on
27
- * re-widening is automatic. Preferences re-clamp here because they cross the
28
- * store boundary and callers may still supply stale ranges.
29
+ * Solve the three column widths for one viewport frame.
29
30
  * @param viewport - available frame width in px.
30
31
  * @param sidebar - sidebar width preference in px (0 = closed).
31
- * @param details - details width preference in px (0 = closed).
32
- * @returns resolved widths; details 0 means visually closed (never unmounted), while a closed sidebar keeps its compact rail.
32
+ * @param rightbar - requested right panel width in px (0 = no track).
33
+ * @returns actual widths after shrinking or removing the right track; only
34
+ * without that track may the center fall below its minimum, down to zero.
33
35
  */
34
- function computeColumns(viewport, sidebar, details) {
36
+ function computeColumns(viewport, sidebar, rightbar) {
35
37
  const s = sidebar === 0 ? 56 : clampWidth(sidebar, 264, 420);
36
- const d0 = details === 0 ? 0 : clampWidth(details, 300, 520);
37
- if (s + d0 + 640 <= viewport) return {
38
- sidebar: s,
39
- center: viewport - s - d0,
40
- details: d0
41
- };
42
- const d1 = d0 === 0 ? 0 : Math.max(300, viewport - s - 640);
43
- if (s + d1 + 640 <= viewport) return {
44
- sidebar: s,
45
- center: 640,
46
- details: d1
47
- };
38
+ const available = viewport - s - 400;
39
+ const r = rightbar === 0 || available < 300 ? 0 : Math.min(available, clampWidth(rightbar, 300, viewport * RIGHTBAR_MAX_RATIO));
48
40
  return {
49
41
  sidebar: s,
50
- center: Math.max(0, viewport - s),
51
- details: 0
42
+ center: Math.max(0, viewport - s - r),
43
+ rightbar: r
52
44
  };
53
45
  }
54
46
  //#endregion
55
47
  //#region lib/types/client/DocumentTitle.js
48
+ /** Browser title selection follows the active main panel without subscribing the frame. */
56
49
  /**
57
50
  * Project the selected durable session title into the browser title and
58
51
  * restore the build-selected product title when unmounted.
59
52
  * @param props - Selected session title projection.
60
53
  * @returns No rendered content.
61
54
  */
62
- function DocumentTitle({ title, productTitle }) {
55
+ function DocumentTitle({ useSessions, usePanelInfo, productTitle }) {
56
+ const showSessionTitle = usePanelInfo((info) => info.activePanelId === null);
57
+ const title = useSessions((state) => {
58
+ const current = state.current;
59
+ return !showSessionTitle || current === void 0 ? void 0 : state.byId[current]?.title;
60
+ });
63
61
  (0, react.useEffect)(() => {
64
62
  document.title = title === void 0 ? productTitle : `${title} — ${productTitle}`;
65
63
  return () => {
@@ -70,7 +68,7 @@ window.__ModuleLoader__.load({
70
68
  }
71
69
  //#endregion
72
70
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-layout/src/client/AppFrame.module.css.mjs
73
- const css = ".pI_x6G_frame{background:var(--dsw-alias-bg-base);height:100%;transition:grid-template-columns var(--ds-transition-duration-slow) var(--ds-ease-in-out);grid-template-rows:100%;display:grid;position:relative;overflow:hidden}.pI_x6G_frame[data-dragging]{transition:none}@media (prefers-reduced-motion:reduce){.pI_x6G_frame{transition:none}}.pI_x6G_sidebarCol{background:var(--dsw-specific-sidebar-fill);border-right:.5px solid var(--dsw-alias-border-l3);min-width:0;overflow:hidden}.pI_x6G_centerCol{flex-direction:column;min-width:0;display:flex;overflow:hidden}.pI_x6G_detailsCol{border-left:.5px solid var(--dsw-alias-border-l3);min-width:0;overflow:hidden}.pI_x6G_frame[data-details-collapsed] .pI_x6G_detailsCol{border-left:none}.pI_x6G_handle{cursor:col-resize;z-index:2;touch-action:none;width:8px;transition:left var(--ds-transition-duration-slow) var(--ds-ease-in-out);margin-left:-4px;position:absolute;top:0;bottom:0}.pI_x6G_frame[data-dragging] .pI_x6G_handle{transition:none}@media (prefers-reduced-motion:reduce){.pI_x6G_handle{transition:none}}.pI_x6G_handle[data-side=details]:after{content:\"\";box-sizing:border-box;background:var(--dsw-alias-button-floating-fill);border:.5px solid var(--dsw-alias-border-l2-darkmode-thin);opacity:0;width:12px;height:32px;transition:opacity var(--ds-transition-duration-slow) var(--ds-ease-in-out), background var(--ds-transition-duration-slow) var(--ds-ease-in-out);border-radius:10px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.pI_x6G_detailsCol:hover~.pI_x6G_handle[data-side=details]:after,.pI_x6G_handle[data-side=details]:hover:after,.pI_x6G_handle[data-side=details][data-dragging=true]:after{opacity:1}.pI_x6G_handle[data-side=details]:hover:after,.pI_x6G_handle[data-side=details][data-dragging=true]:after{background:var(--dsw-alias-button-floating-hover);border-color:var(--dsw-alias-border-l3)}.pI_x6G_overlayLayer{z-index:20;pointer-events:none;position:absolute;inset:0}.pI_x6G_overlayLayer>*{pointer-events:auto}";
71
+ const css = ".pI_x6G_frame{background:var(--dsw-alias-bg-base);height:100%;transition:grid-template-columns var(--ds-transition-duration-slow) var(--ds-ease-in-out);grid-template-rows:100%;display:grid;position:relative;overflow:hidden}.pI_x6G_frame[data-dragging]{transition:none}@media (prefers-reduced-motion:reduce){.pI_x6G_frame{transition:none}}.pI_x6G_sidebarCol{background:var(--dsw-specific-sidebar-fill);border-right:.5px solid var(--dsw-alias-border-l3);min-width:0;overflow:hidden}.pI_x6G_centerCol{flex-direction:column;min-width:0;display:flex;overflow:hidden}.pI_x6G_handle{cursor:col-resize;z-index:11;touch-action:none;width:8px;transition:left var(--ds-transition-duration-slow) var(--ds-ease-in-out);margin-left:-4px;position:absolute;top:0;bottom:0}.pI_x6G_frame[data-dragging] .pI_x6G_handle,.pI_x6G_frame[data-rightbar-fullscreen],.pI_x6G_frame[data-rightbar-fullscreen] .pI_x6G_handle,.pI_x6G_frame[data-rightbar-instant],.pI_x6G_frame[data-rightbar-instant] .pI_x6G_handle{transition:none}@media (prefers-reduced-motion:reduce){.pI_x6G_handle{transition:none}}.pI_x6G_rightbarCol{min-width:0;position:relative;overflow:visible}.pI_x6G_overlayLayer{z-index:20;pointer-events:none;position:absolute;inset:0}.pI_x6G_overlayLayer>*{pointer-events:auto}";
74
72
  const tagId = "@deepseek-ai/dsh-client-ui-layout/AppFrame.module.css";
75
73
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
76
74
  const tag = document.createElement("style");
@@ -81,10 +79,10 @@ window.__ModuleLoader__.load({
81
79
  }
82
80
  var AppFrame_module_css_default = {
83
81
  "centerCol": "pI_x6G_centerCol",
84
- "detailsCol": "pI_x6G_detailsCol",
85
82
  "frame": "pI_x6G_frame",
86
83
  "handle": "pI_x6G_handle",
87
84
  "overlayLayer": "pI_x6G_overlayLayer",
85
+ "rightbarCol": "pI_x6G_rightbarCol",
88
86
  "sidebarCol": "pI_x6G_sidebarCol"
89
87
  };
90
88
  //#endregion
@@ -92,14 +90,18 @@ window.__ModuleLoader__.load({
92
90
  /**
93
91
  * Three-column shell frame, registered into the built-in 'root' slot (the web
94
92
  * shell renders only 'root'). Owns the grid tracks (sidebar | center |
95
- * details), the drag handles (pointer capture + rAF throttle), the concession
96
- * chain (columns.ts), and the child-slot render decisions: the sidebar slot
97
- * renders HERE with live parameters from the concession solve, and the
98
- * session-aware occupants render in fixed column positions; strict entries
99
- * gate themselves on current-session availability while session-maybe
100
- * entries retain identity. Pure component: everything arrives
101
- * through the three framework shares zero cordis or framework imports,
102
- * zero self-made hooks.
93
+ * rightbar), the drag handles (pointer capture + rAF throttle), the column
94
+ * solve (columns.ts), and the child-slot render decisions: the sidebar slot
95
+ * receives live parameters from that solve. The root-scoped main slot selects
96
+ * the Conversation or a global panel. Each column occupant owns its Session
97
+ * binding and reports the geometry it needs.
98
+ *
99
+ * The right column is a track, not a box: its occupant draws its panel anchored
100
+ * to the frame's right edge at the resolved normal width, and the
101
+ * track only decides whether the centre makes room for it. The occupant reports
102
+ * shown/track/fullscreen through `ctx.layout`; fullscreen keeps the reported
103
+ * track but hides the outer resize handle. Everything arrives through the framework
104
+ * shares — zero cordis or framework imports, zero self-made hooks.
103
105
  */
104
106
  /** Center column grid item (session-body building block). */
105
107
  function CenterColumn(props) {
@@ -108,10 +110,19 @@ window.__ModuleLoader__.load({
108
110
  children: props.children
109
111
  });
110
112
  }
111
- /** Details column grid item; width 0 keeps the subtree mounted (never unmount on close). */
112
- function DetailsColumn(props) {
113
+ /** Subscribe to the main key without subscribing the column frame to each panel id. */
114
+ function MainPanel({ usePanelInfo, renderSlot }) {
115
+ return renderSlot("main", {}, { entryKey: usePanelInfo((info) => info.activePanelId) ?? "conversation" });
116
+ }
117
+ /**
118
+ * Right column grid item. Zero-width unless the occupant asked for a track; the
119
+ * occupant's panel is positioned against the column's right edge, which never
120
+ * moves, so it can hang over the centre when there is no track.
121
+ */
122
+ function RightbarColumn(props) {
113
123
  return (0, react_jsx_runtime.jsx)("div", {
114
- className: AppFrame_module_css_default.detailsCol,
124
+ className: AppFrame_module_css_default.rightbarCol,
125
+ "data-rightbar-col": true,
115
126
  children: props.children
116
127
  });
117
128
  }
@@ -124,6 +135,7 @@ window.__ModuleLoader__.load({
124
135
  const origin = (0, react.useRef)(0);
125
136
  const latest = (0, react.useRef)(0);
126
137
  const frame = (0, react.useRef)(null);
138
+ const capture = (0, react.useRef)(null);
127
139
  const callbacks = (0, react.useRef)({
128
140
  onStart: props.onStart,
129
141
  onDrag: props.onDrag,
@@ -134,16 +146,34 @@ window.__ModuleLoader__.load({
134
146
  onDrag: props.onDrag,
135
147
  onEnd: props.onEnd
136
148
  };
149
+ const endDrag = (0, react.useCallback)(() => {
150
+ const active = capture.current;
151
+ if (active === null) return;
152
+ capture.current = null;
153
+ if (frame.current !== null) {
154
+ cancelAnimationFrame(frame.current);
155
+ frame.current = null;
156
+ }
157
+ if (active.element.hasPointerCapture(active.id)) active.element.releasePointerCapture(active.id);
158
+ setDragging(false);
159
+ callbacks.current.onEnd();
160
+ }, []);
161
+ (0, react.useEffect)(() => endDrag, [endDrag]);
137
162
  const onPointerDown = (0, react.useCallback)((e) => {
163
+ if (e.button !== 0 || capture.current !== null) return;
138
164
  e.preventDefault();
139
165
  e.currentTarget.setPointerCapture(e.pointerId);
166
+ capture.current = {
167
+ element: e.currentTarget,
168
+ id: e.pointerId
169
+ };
140
170
  origin.current = e.clientX;
141
171
  latest.current = e.clientX;
142
172
  callbacks.current.onStart();
143
173
  setDragging(true);
144
174
  }, []);
145
175
  const onPointerMove = (0, react.useCallback)((e) => {
146
- if (!e.currentTarget.hasPointerCapture(e.pointerId)) return;
176
+ if (capture.current?.id !== e.pointerId) return;
147
177
  latest.current = e.clientX;
148
178
  frame.current ??= requestAnimationFrame(() => {
149
179
  frame.current = null;
@@ -151,16 +181,13 @@ window.__ModuleLoader__.load({
151
181
  });
152
182
  }, []);
153
183
  const onPointerUp = (0, react.useCallback)((e) => {
154
- if (!e.currentTarget.hasPointerCapture(e.pointerId)) return;
155
- e.currentTarget.releasePointerCapture(e.pointerId);
156
- if (frame.current !== null) {
157
- cancelAnimationFrame(frame.current);
158
- frame.current = null;
159
- }
160
- callbacks.current.onDrag(latest.current - origin.current);
161
- setDragging(false);
162
- callbacks.current.onEnd();
163
- }, []);
184
+ if (capture.current?.id !== e.pointerId) return;
185
+ callbacks.current.onDrag(e.clientX - origin.current);
186
+ endDrag();
187
+ }, [endDrag]);
188
+ const onPointerCancel = (0, react.useCallback)((e) => {
189
+ if (capture.current?.id === e.pointerId) endDrag();
190
+ }, [endDrag]);
164
191
  return (0, react_jsx_runtime.jsx)("div", {
165
192
  className: AppFrame_module_css_default.handle,
166
193
  style: { left: props.left },
@@ -168,56 +195,53 @@ window.__ModuleLoader__.load({
168
195
  "data-dragging": dragging || void 0,
169
196
  onPointerDown,
170
197
  onPointerMove,
171
- onPointerUp
198
+ onPointerUp,
199
+ onPointerCancel,
200
+ onLostPointerCapture: onPointerCancel
172
201
  });
173
202
  }
174
203
  /** The three-column frame (see module doc). */
175
- function AppFrame({ useStore, useSessions, actions, renderSlot, SessionProvider, t }) {
176
- const panels = useStore((s) => s);
177
- const detailsSession = useSessions((s) => {
178
- const current = s.current;
179
- return current !== void 0 && s.byId[current]?.blank === false ? current : void 0;
180
- });
181
- const documentTitle = useSessions((s) => {
182
- const current = s.current;
183
- return current === void 0 ? void 0 : s.byId[current]?.title;
184
- });
204
+ function AppFrame({ useStore, useSessions, usePanelInfo, actions, renderSlot, t }) {
205
+ const layoutInfo = useStore((state) => state.layoutInfo);
185
206
  const frameRef = (0, react.useRef)(null);
186
- const [viewport, setViewport] = (0, react.useState)(() => window.innerWidth);
187
- const lastSession = (0, react.useRef)(detailsSession);
207
+ const viewport = layoutInfo.viewportWidth;
188
208
  (0, react.useLayoutEffect)(() => {
189
- if (detailsSession === void 0) return;
190
- if (lastSession.current !== void 0 && lastSession.current !== detailsSession) actions.closeDetails();
191
- lastSession.current = detailsSession;
192
- }, [actions, detailsSession]);
193
- (0, react.useEffect)(() => {
194
209
  const el = frameRef.current;
195
210
  /* v8 ignore next -- the ref is always attached by effect time: the frame div renders unconditionally. */
196
211
  if (el === null) return;
197
212
  let raf = null;
213
+ let disposed = false;
214
+ const measure = () => {
215
+ const width = el.getBoundingClientRect().width;
216
+ if (width > 0) actions.setViewportWidth(width);
217
+ };
218
+ measure();
198
219
  const observer = new ResizeObserver(() => {
220
+ if (disposed) return;
199
221
  raf ??= requestAnimationFrame(() => {
200
222
  raf = null;
201
- const width = el.getBoundingClientRect().width;
202
- if (width > 0) setViewport(width);
223
+ measure();
203
224
  });
204
225
  });
205
226
  observer.observe(el);
206
227
  return () => {
228
+ disposed = true;
207
229
  observer.disconnect();
208
230
  if (raf !== null) cancelAnimationFrame(raf);
209
231
  };
210
- }, []);
232
+ }, [actions]);
211
233
  const narrow = viewport < SIDEBAR_AUTO_COLLAPSE;
212
- (0, react.useEffect)(() => {
213
- actions.setNarrow(narrow);
214
- }, [actions, narrow]);
215
- const sidebarCollapsed = narrow ? !panels.narrowExpanded : panels.sidebar === 0;
216
- const cols = computeColumns(viewport, sidebarCollapsed ? 0 : panels.sidebar === 0 ? 280 : panels.sidebar, detailsSession === void 0 ? 0 : panels.details);
234
+ const sidebarCollapsed = narrow ? !layoutInfo.narrowExpanded : layoutInfo.sidebar === 0;
235
+ const sidebarPreference = sidebarCollapsed ? 0 : layoutInfo.sidebar === 0 ? 280 : layoutInfo.sidebar;
236
+ const rightbarPreference = layoutInfo.rightbar ?? viewport * .45;
237
+ const normal = computeColumns(viewport, !layoutInfo.rightbarShown && narrow ? 0 : sidebarPreference, rightbarPreference);
238
+ const cols = computeColumns(viewport, sidebarPreference, layoutInfo.rightbarTrack ? rightbarPreference : 0);
217
239
  const colsRef = (0, react.useRef)(cols);
218
240
  colsRef.current = cols;
241
+ const rightbarWidth = (0, react.useRef)(normal.rightbar);
242
+ rightbarWidth.current = normal.rightbar;
219
243
  const sidebarBase = (0, react.useRef)(0);
220
- const detailsBase = (0, react.useRef)(0);
244
+ const rightbarBase = (0, react.useRef)(0);
221
245
  const [dragging, setDragging] = (0, react.useState)(false);
222
246
  const onDragEnd = (0, react.useCallback)(() => {
223
247
  setDragging(false);
@@ -226,40 +250,58 @@ window.__ModuleLoader__.load({
226
250
  sidebarBase.current = colsRef.current.sidebar;
227
251
  setDragging(true);
228
252
  }, []);
229
- const onDetailsStart = (0, react.useCallback)(() => {
230
- detailsBase.current = colsRef.current.details;
231
- setDragging(true);
232
- }, []);
233
253
  const onSidebarDrag = (0, react.useCallback)((dx) => {
234
254
  actions.setSidebar(sidebarBase.current + dx);
235
255
  }, [actions]);
236
- const onDetailsDrag = (0, react.useCallback)((dx) => {
237
- actions.setDetails(detailsBase.current - dx);
256
+ const onRightbarStart = (0, react.useCallback)(() => {
257
+ rightbarBase.current = rightbarWidth.current;
258
+ setDragging(true);
259
+ }, []);
260
+ const onRightbarDrag = (0, react.useCallback)((dx) => {
261
+ actions.setRightbar(rightbarBase.current - dx);
238
262
  }, [actions]);
263
+ const productTitle = "DeepSeek Harness";
264
+ const sidebar = (0, react.useMemo)(() => renderSlot("sidebar", {
265
+ collapsed: sidebarCollapsed,
266
+ width: cols.sidebar
267
+ }), [
268
+ renderSlot,
269
+ sidebarCollapsed,
270
+ cols.sidebar
271
+ ]);
272
+ const main = (0, react.useMemo)(() => (0, react_jsx_runtime.jsx)(MainPanel, {
273
+ usePanelInfo,
274
+ renderSlot
275
+ }), [usePanelInfo, renderSlot]);
276
+ const overlays = (0, react.useMemo)(() => renderSlot("shell.overlay", {}), [renderSlot]);
239
277
  return (0, react_jsx_runtime.jsxs)("div", {
240
278
  ref: frameRef,
241
279
  className: AppFrame_module_css_default.frame,
242
- style: { gridTemplateColumns: `${cols.sidebar}px minmax(0, 1fr) ${cols.details}px` },
280
+ style: { gridTemplateColumns: `${cols.sidebar}px minmax(0, 1fr) ${cols.rightbar}px` },
243
281
  "data-sidebar-collapsed": sidebarCollapsed || void 0,
244
- "data-details-collapsed": cols.details === 0 || void 0,
282
+ "data-rightbar-collapsed": cols.rightbar === 0 || void 0,
283
+ "data-rightbar-fullscreen": layoutInfo.rightbarFullscreen || void 0,
284
+ "data-rightbar-instant": layoutInfo.rightbarInstant || void 0,
245
285
  "data-dragging": dragging || void 0,
246
286
  children: [
247
287
  (0, react_jsx_runtime.jsx)(DocumentTitle, {
248
- productTitle: "DeepSeek Harness",
249
- ...documentTitle === void 0 ? {} : { title: documentTitle }
288
+ productTitle,
289
+ useSessions,
290
+ usePanelInfo
250
291
  }),
251
292
  (0, react_jsx_runtime.jsx)("div", {
252
293
  className: AppFrame_module_css_default.sidebarCol,
253
- children: renderSlot("sidebar", {
254
- collapsed: sidebarCollapsed,
255
- width: cols.sidebar
256
- })
294
+ children: sidebar
257
295
  }),
258
- (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(CenterColumn, { children: renderSlot("conversation", {}) }), (0, react_jsx_runtime.jsx)(DetailsColumn, { children: (0, react_jsx_runtime.jsx)(SessionProvider, { children: renderSlot("details", {}) }) })] }),
296
+ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(CenterColumn, { children: main }), (0, react_jsx_runtime.jsx)(RightbarColumn, { children: renderSlot("rightbar", {
297
+ width: normal.rightbar,
298
+ viewportWidth: viewport,
299
+ canShow: normal.rightbar > 0
300
+ }) })] }),
259
301
  (0, react_jsx_runtime.jsx)("div", {
260
302
  className: AppFrame_module_css_default.overlayLayer,
261
303
  "data-shell-overlay": true,
262
- children: renderSlot("shell.overlay", {})
304
+ children: overlays
263
305
  }),
264
306
  !sidebarCollapsed && (0, react_jsx_runtime.jsx)(DragHandle, {
265
307
  side: "sidebar",
@@ -268,11 +310,11 @@ window.__ModuleLoader__.load({
268
310
  onDrag: onSidebarDrag,
269
311
  onEnd: onDragEnd
270
312
  }),
271
- cols.details > 0 && (0, react_jsx_runtime.jsx)(DragHandle, {
272
- side: "details",
273
- left: viewport - cols.details,
274
- onStart: onDetailsStart,
275
- onDrag: onDetailsDrag,
313
+ layoutInfo.rightbarShown && !layoutInfo.rightbarFullscreen && normal.rightbar > 0 && (0, react_jsx_runtime.jsx)(DragHandle, {
314
+ side: "rightbar",
315
+ left: viewport - normal.rightbar,
316
+ onStart: onRightbarStart,
317
+ onDrag: onRightbarDrag,
276
318
  onEnd: onDragEnd
277
319
  })
278
320
  ]
@@ -281,53 +323,72 @@ window.__ModuleLoader__.load({
281
323
  //#endregion
282
324
  //#region lib/types/client/stores.js
283
325
  /**
284
- * The root entry's transient layout store: panel geometry as plain widths in
285
- * px (0 = closed). Module level exports the factory only a module-level
286
- * handle would pin the store's identity in the module
287
- * cache (a de-facto singleton surviving plugin reloads). register() receives
288
- * the factory (exclusive use: the framework instantiates per entry), AppFrame
289
- * derives its PropsStore share from the return type, and the service face
290
- * receives the bound actions through the registration's inject hook.
326
+ * Root-owned frame measurement, panel preferences, and presentation reports.
327
+ * The registration supplies a fresh store and binds its actions to ctx.layout.
291
328
  */
292
329
  /**
293
- * Create the layout panel store handle. The preference IS the width, so
294
- * closing a panel forgets its drag width — reopening restores the contract
295
- * default. Actions are the complete write set: drag writes clamp
296
- * into the panel's contract range and never cross the open/closed line;
297
- * open/close transitions write 0 / the default explicitly. Below the
298
- * auto-collapse breakpoint (AppFrame feeds setNarrow) the sidebar toggle
299
- * flips the narrowExpanded override instead of the preference.
330
+ * Create the layout panel store handle. For the sidebar the preference IS the
331
+ * width, so closing it forgets its drag width — reopening restores the contract
332
+ * default. The right panel initializes at 45% of the frame on first opening
333
+ * and keeps that px preference across resizes and close. Drag writes clamp to
334
+ * the current frame's range. Narrow sidebar toggles change only the expansion
335
+ * override; opening the right panel clears that override.
300
336
  * @returns the store handle (spec + type + identity + factory in one).
301
337
  */
302
338
  function createLayoutStore() {
303
339
  return (0, _deepseek_ai_dsh_client_store.defineStore)({
304
340
  init: () => ({
305
- sidebar: 280,
306
- details: 0,
307
- narrow: false,
308
- narrowExpanded: false
341
+ panelInfo: { activePanelId: null },
342
+ layoutInfo: {
343
+ sidebar: 280,
344
+ viewportWidth: window.innerWidth,
345
+ narrowExpanded: false,
346
+ rightbar: null,
347
+ rightbarShown: false,
348
+ rightbarTrack: false,
349
+ rightbarFullscreen: false,
350
+ rightbarInstant: false
351
+ }
309
352
  }),
310
353
  actions: {
311
- setSidebar: (d, px) => {
312
- d.sidebar = clampWidth(px, 264, 420);
354
+ selectPanel: (d, panelId) => {
355
+ d.panelInfo.activePanelId = panelId;
356
+ },
357
+ retainMainPanels: (d, panelIds) => {
358
+ if (d.panelInfo.activePanelId !== null && !panelIds.includes(d.panelInfo.activePanelId)) d.panelInfo.activePanelId = null;
313
359
  },
314
- setDetails: (d, px) => {
315
- d.details = clampWidth(px, 300, 520);
360
+ setSidebar: (d, px) => {
361
+ d.layoutInfo.rightbarInstant = false;
362
+ d.layoutInfo.sidebar = clampWidth(px, 264, 420);
316
363
  },
317
364
  toggleSidebar: (d) => {
318
- if (d.narrow) d.narrowExpanded = !d.narrowExpanded;
319
- else d.sidebar = d.sidebar === 0 ? 280 : 0;
365
+ d.layoutInfo.rightbarInstant = false;
366
+ if (d.layoutInfo.viewportWidth < 1024) d.layoutInfo.narrowExpanded = !d.layoutInfo.narrowExpanded;
367
+ else d.layoutInfo.sidebar = d.layoutInfo.sidebar === 0 ? 280 : 0;
320
368
  },
321
- setNarrow: (d, narrow) => {
322
- if (d.narrow === narrow) return;
323
- d.narrow = narrow;
324
- d.narrowExpanded = false;
369
+ setViewportWidth: (d, width) => {
370
+ if (d.layoutInfo.viewportWidth === width) return;
371
+ d.layoutInfo.rightbarInstant = false;
372
+ if (d.layoutInfo.viewportWidth < 1024 !== width < 1024) d.layoutInfo.narrowExpanded = false;
373
+ d.layoutInfo.viewportWidth = width;
325
374
  },
326
- openDetails: (d) => {
327
- if (d.details === 0) d.details = 360;
375
+ setRightbar: (d, px) => {
376
+ d.layoutInfo.rightbarInstant = false;
377
+ d.layoutInfo.rightbar = clampWidth(px, 300, Math.max(300, d.layoutInfo.viewportWidth * RIGHTBAR_MAX_RATIO));
328
378
  },
329
- closeDetails: (d) => {
330
- d.details = 0;
379
+ openRightbar: (d, track, fullscreen) => {
380
+ if (!d.layoutInfo.rightbarShown || d.layoutInfo.rightbarTrack !== track || d.layoutInfo.rightbarFullscreen !== fullscreen) d.layoutInfo.rightbarInstant = d.layoutInfo.rightbarFullscreen && !fullscreen;
381
+ if (!d.layoutInfo.rightbarShown && d.layoutInfo.viewportWidth < 1024) d.layoutInfo.narrowExpanded = false;
382
+ d.layoutInfo.rightbar ??= Math.max(300, Math.round(d.layoutInfo.viewportWidth * RIGHTBAR_DEFAULT_RATIO));
383
+ d.layoutInfo.rightbarShown = true;
384
+ d.layoutInfo.rightbarTrack = track;
385
+ d.layoutInfo.rightbarFullscreen = fullscreen;
386
+ },
387
+ closeRightbar: (d) => {
388
+ if (d.layoutInfo.rightbarShown) d.layoutInfo.rightbarInstant = d.layoutInfo.rightbarFullscreen;
389
+ d.layoutInfo.rightbarShown = false;
390
+ d.layoutInfo.rightbarTrack = false;
391
+ d.layoutInfo.rightbarFullscreen = false;
331
392
  }
332
393
  }
333
394
  });
@@ -336,32 +397,44 @@ window.__ModuleLoader__.load({
336
397
  //#region lib/types/client/service.js
337
398
  /** Cross-plugin panel-action face (ctx.layout). */
338
399
  var LayoutController = class {
339
- #panels;
400
+ panels;
401
+ hasMainPanel;
402
+ navigation = new AbortController();
340
403
  /**
341
- * Adopt the root entry's bound store actions. Called from the root
342
- * registration's inject hook (a sanctioned assembly side effect), so the
343
- * face is live from the entry's first render; on entry re-register the
344
- * fresh actions overwrite the stale set.
345
- * @param actions - bound actions of the entry's layout store instance.
404
+ * @param panels - actions of the instance shared with the root entry.
405
+ * @param hasMainPanel - checks the live main-slot registry for a panel id.
346
406
  */
347
- attachPanels(actions) {
348
- this.#panels = actions;
407
+ constructor(panels, hasMainPanel) {
408
+ this.panels = panels;
409
+ this.hasMainPanel = hasMainPanel;
410
+ }
411
+ /** Select a global panel or return to the Conversation. */
412
+ selectPanel(panelId) {
413
+ if (panelId !== null && !this.hasMainPanel(panelId)) throw new Error(`layout.selectPanel: main panel "${panelId}" is not registered`);
414
+ this.navigation.abort();
415
+ this.panels.selectPanel(panelId);
416
+ }
417
+ /** @returns the new pending navigation's cancellation signal. */
418
+ beginNavigation() {
419
+ this.navigation.abort();
420
+ this.navigation = new AbortController();
421
+ return this.navigation.signal;
422
+ }
423
+ /** Invalidate pending navigations when the layout owner is unloaded. */
424
+ dispose() {
425
+ this.navigation.abort();
349
426
  }
350
427
  /** Toggle the sidebar panel (closed ⟷ contract default width). */
351
428
  toggleSidebar() {
352
- this.#require().toggleSidebar();
353
- }
354
- /** Open the details panel (no-op when already open). */
355
- openDetails() {
356
- this.#require().openDetails();
429
+ this.panels.toggleSidebar();
357
430
  }
358
- /** Close the details panel. */
359
- closeDetails() {
360
- this.#require().closeDetails();
431
+ /** Report the right panel's track and fullscreen presentation. */
432
+ openRightbar(track, fullscreen) {
433
+ this.panels.openRightbar(track, fullscreen);
361
434
  }
362
- #require() {
363
- if (this.#panels === void 0) throw new Error("layout: panel actions not wired (root entry not mounted)");
364
- return this.#panels;
435
+ /** Report the right panel as hidden: no track, no handle. */
436
+ closeRightbar() {
437
+ this.panels.closeRightbar();
365
438
  }
366
439
  };
367
440
  //#endregion
@@ -428,12 +501,25 @@ window.__ModuleLoader__.load({
428
501
  /**
429
502
  * Client plugin body: provide ctx.layout, then one register() call — AppFrame
430
503
  * into 'root' with the four child-slot declarations, the layout store seat,
431
- * and the inject hook that hands the store's bound actions to the service.
504
+ * and the shared root instance supplying commands and the panel-info source.
432
505
  * @param ctx - client root context.
433
506
  */
434
507
  function apply(ctx) {
435
- const layout = new LayoutController();
436
508
  ctx.effect(() => {
509
+ const handle = createLayoutStore();
510
+ const instance = handle.create();
511
+ const store = {
512
+ ...handle,
513
+ create: () => instance
514
+ };
515
+ const layout = new LayoutController(instance.actions, (id) => ctx.slots.entries("main").some((entry) => entry.options.key === id));
516
+ const retainMainPanels = () => {
517
+ instance.actions.retainMainPanels(ctx.slots.entries("main").flatMap((entry) => entry.options.key === void 0 ? [] : [entry.options.key]));
518
+ };
519
+ const disposePanelInfo = ctx.slots.provideRoot({ hooks: { panelInfo: {
520
+ getSnapshot: () => instance.getSnapshot().panelInfo,
521
+ subscribe: (listener) => instance.subscribe(listener)
522
+ } } });
437
523
  const disposeService = ctx.reflect.provide("layout", layout);
438
524
  const disposeRegistration = ctx.slots.register({
439
525
  name: "root",
@@ -443,27 +529,28 @@ window.__ModuleLoader__.load({
443
529
  kind: "single",
444
530
  scope: "root"
445
531
  },
446
- "conversation": {
447
- kind: "single",
448
- scope: "session-maybe"
532
+ "main": {
533
+ kind: "keyed",
534
+ scope: "root"
449
535
  },
450
- "details": {
536
+ "rightbar": {
451
537
  kind: "single",
452
- scope: "session"
538
+ scope: "root"
453
539
  },
454
540
  "shell.overlay": {
455
541
  kind: "list",
456
542
  scope: "root"
457
543
  }
458
544
  },
459
- store: createLayoutStore,
460
- inject: (actions) => {
461
- layout.attachPanels(actions);
462
- return {};
463
- }
545
+ store
464
546
  }, AppFrame);
547
+ const disposePanels = ctx.slots.subscribe("main", retainMainPanels);
548
+ retainMainPanels();
465
549
  return () => {
550
+ layout.dispose();
551
+ disposePanels();
466
552
  disposeRegistration();
553
+ disposePanelInfo();
467
554
  disposeService();
468
555
  };
469
556
  }, "ui-layout: service + root registration");
@@ -1,7 +1,7 @@
1
1
  import type { PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
2
2
  import type { createLayoutStore } from './stores.ts';
3
3
  /** Full composed props: runtime share + child-slot render share + store share. */
4
- export type AppFrameProps = PropsRuntime<'root'> & PropsRenderSlots<'sidebar' | 'conversation' | 'details' | 'shell.overlay'> & PropsStore<ReturnType<typeof createLayoutStore>> & PropsLocale<'common'>;
4
+ export type AppFrameProps = PropsRuntime<'root'> & PropsRenderSlots<'sidebar' | 'main' | 'rightbar' | 'shell.overlay'> & PropsStore<ReturnType<typeof createLayoutStore>> & PropsLocale<'common'>;
5
5
  /** The three-column frame (see module doc). */
6
- export declare function AppFrame({ useStore, useSessions, actions, renderSlot, SessionProvider, t, }: AppFrameProps): import("react").JSX.Element;
6
+ export declare function AppFrame({ useStore, useSessions, usePanelInfo, actions, renderSlot, t, }: AppFrameProps): import("react").JSX.Element;
7
7
  //# sourceMappingURL=AppFrame.d.ts.map
@@ -1,15 +1,14 @@
1
+ import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
1
2
  /** Props for the browser title projection. */
2
- export interface DocumentTitleProps {
3
- /** Durable title of the selected session, or undefined for the product title. */
4
- title?: string;
3
+ export type DocumentTitleProps = Pick<PropsRuntime<'root'>, 'useSessions' | 'usePanelInfo'> & {
5
4
  /** Build-configured or localized product title. */
6
5
  productTitle: string;
7
- }
6
+ };
8
7
  /**
9
8
  * Project the selected durable session title into the browser title and
10
9
  * restore the build-selected product title when unmounted.
11
10
  * @param props - Selected session title projection.
12
11
  * @returns No rendered content.
13
12
  */
14
- export declare function DocumentTitle({ title, productTitle }: DocumentTitleProps): null;
13
+ export declare function DocumentTitle({ useSessions, usePanelInfo, productTitle }: DocumentTitleProps): null;
15
14
  //# sourceMappingURL=DocumentTitle.d.ts.map
@@ -1,25 +1,16 @@
1
1
  /**
2
- * Pure concession-chain column solver for the three-column AppFrame.
3
- * Chain order is fixed by contract: keep center >= CENTER_MIN by shrinking
4
- * details, then auto-closing it (derived zero width — preferred width
5
- * preferences are never rewritten, so widening the window restores them).
6
- * The sidebar never concedes: its rendered width is always the drag
7
- * preference (or the collapsed rail), and center absorbs any remaining
8
- * deficit as the last resort. Inputs are the layout store's plain width
9
- * preferences (0 = closed); a closed sidebar resolves to the fixed
10
- * SIDEBAR_COLLAPSED control rail while closed details resolve to zero width.
11
- * The SIDEBAR_AUTO_COLLAPSE breakpoint is consumed by AppFrame, which decides
12
- * the effective sidebar preference before solving; the solver itself stays
13
- * breakpoint-free.
2
+ * Normal column geometry: the right column shrinks, then loses its track,
3
+ * before the center drops below its minimum. The sidebar never concedes here;
4
+ * AppFrame supplies its effective preference after responsive collapse.
14
5
  */
15
- /** Resolved widths for one frame; center may drop below CENTER_MIN only at the final fallback. */
6
+ /** Resolved widths for one frame. */
16
7
  export interface Columns {
17
8
  sidebar: number;
18
9
  center: number;
19
- details: number;
10
+ rightbar: number;
20
11
  }
21
- /** Center column floor; only the final fallback may go below it. */
22
- export declare const CENTER_MIN = 640;
12
+ /** Center width protected while the normal right column is open. */
13
+ export declare const CENTER_MIN = 400;
23
14
  /** Sidebar drag clamp floor. */
24
15
  export declare const SIDEBAR_MIN = 264;
25
16
  /** Sidebar drag clamp ceiling. */
@@ -32,12 +23,12 @@ export declare const SIDEBAR_COLLAPSED = 56;
32
23
  * LG breakpoint); a manual toggle below it re-expands over the squeezed center
33
24
  * (stores.ts narrowExpanded). */
34
25
  export declare const SIDEBAR_AUTO_COLLAPSE = 1024;
35
- /** Details drag clamp floor. */
36
- export declare const DETAILS_MIN = 300;
37
- /** Details drag clamp ceiling. */
38
- export declare const DETAILS_MAX = 520;
39
- /** Details width before any user drag. */
40
- export declare const DETAILS_DEFAULT = 360;
26
+ /** Right column drag clamp floor. */
27
+ export declare const RIGHTBAR_MIN = 300;
28
+ /** Maximum normal right panel width as a fraction of the frame. */
29
+ export declare const RIGHTBAR_MAX_RATIO = 0.7;
30
+ /** First-open right panel preference as a fraction of the frame. */
31
+ export declare const RIGHTBAR_DEFAULT_RATIO = 0.45;
41
32
  /**
42
33
  * Clamp a panel width into its contract range.
43
34
  * @param px - requested width.
@@ -47,14 +38,12 @@ export declare const DETAILS_DEFAULT = 360;
47
38
  */
48
39
  export declare function clampWidth(px: number, min: number, max: number): number;
49
40
  /**
50
- * Solve the three column widths for one viewport frame. Pure: no hysteresis —
51
- * the output is a function of (viewport, preferences) only, so recovery on
52
- * re-widening is automatic. Preferences re-clamp here because they cross the
53
- * store boundary and callers may still supply stale ranges.
41
+ * Solve the three column widths for one viewport frame.
54
42
  * @param viewport - available frame width in px.
55
43
  * @param sidebar - sidebar width preference in px (0 = closed).
56
- * @param details - details width preference in px (0 = closed).
57
- * @returns resolved widths; details 0 means visually closed (never unmounted), while a closed sidebar keeps its compact rail.
44
+ * @param rightbar - requested right panel width in px (0 = no track).
45
+ * @returns actual widths after shrinking or removing the right track; only
46
+ * without that track may the center fall below its minimum, down to zero.
58
47
  */
59
- export declare function computeColumns(viewport: number, sidebar: number, details: number): Columns;
48
+ export declare function computeColumns(viewport: number, sidebar: number, rightbar: number): Columns;
60
49
  //# sourceMappingURL=columns.d.ts.map
@@ -3,13 +3,17 @@
3
3
  * the runtime's built-in 'root' slot and, in the same breath, declares the
4
4
  * four child slots (declaration = exclusive render authority), seats the
5
5
  * layout store (panel geometry), and wires the panel-action service face.
6
- * ctx.layout is the cross-plugin panel-action contract; navigation state lives
7
- * with the runtime sessions service. A second effect seats the theme
6
+ * ctx.layout selects the main panel and controls column geometry; Session
7
+ * selection belongs to the Session Controller. A second effect seats the theme
8
8
  * presenter, which projects ctx.theme snapshots onto document.body.
9
9
  */
10
10
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
11
+ import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
12
+ import type { PanelInfo } from './service.ts';
11
13
  export { LayoutController } from './service.ts';
12
- export type { ILayout } from './service.ts';
14
+ export type { ILayout, MainPanelId, PanelInfo } from './service.ts';
15
+ /** Selector hook over root-scoped panel selection. */
16
+ export type UsePanelInfo = SnapshotSelectorHook<PanelInfo>;
13
17
  declare module '@deepseek-ai/cordis' {
14
18
  interface Context {
15
19
  /** The outward face only; the concrete service stays inside this plugin. */
@@ -17,6 +21,10 @@ declare module '@deepseek-ai/cordis' {
17
21
  }
18
22
  }
19
23
  declare module '@deepseek-ai/dsh-client-ui-slots' {
24
+ interface GlobalStandardProps {
25
+ /** Subscribe to the selected main panel independently of parent renders. */
26
+ usePanelInfo: UsePanelInfo;
27
+ }
20
28
  interface SlotMap {
21
29
  /**
22
30
  * The whole left column. OCCUPIED by ui-sidebar's SidebarRoot, which
@@ -34,35 +42,30 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
34
42
  owner: SidebarOwnerProps;
35
43
  };
36
44
  /**
37
- * The whole center column, across both the no-session hero and a live
38
- * conversation. OCCUPIED by ui-conversation's ConversationRoot, which
39
- * declares the session body, composer, and input seats inside it —
40
- * registering here replaces the entire conversation surface (and removes
41
- * every seat it declares) rather than adding to it.
42
- *
43
- * Current-session-optional: the occupant owns both states without
44
- * changing its React identity, so it keeps its own state across a session
45
- * switch. It receives no owner props; session facts arrive through the
46
- * framework hooks of the `session-maybe` scope.
45
+ * Central panel selected by sidebar entry id. The reserved `conversation`
46
+ * key hosts the Conversation; other keys receive no Session binding.
47
47
  */
48
- 'conversation': {
49
- kind: 'single';
50
- scope: 'session-maybe';
51
- owner: ConvOwnerProps;
48
+ 'main': {
49
+ kind: 'keyed';
50
+ scope: 'root';
52
51
  };
53
52
  /**
54
- * The right details column, shown when the layout opens it. OCCUPIED by
55
- * ui-conversation's DetailsPanel, which declares the tool-details seat
56
- * inside it registering here replaces the column and takes that seat
57
- * with it. Absent an occupant the column renders nothing.
53
+ * The right column: a track the centre makes room for, or nothing. OCCUPIED
54
+ * by the right Sidebar, which uses the resolved column width in normal
55
+ * mode and covers the viewport in fullscreen, retaining the wide-screen
56
+ * column reservation underneath.
58
57
  *
59
- * No owner props: the framework injects the session id and hooks for the
60
- * `session` scope, and `ctx.layout` owns whether the column is open.
58
+ * Whether the panel is shown, and whether it takes a track, is the
59
+ * occupant's own recorded business it reports the composition of its
60
+ * expanded and presentation state through `ctx.layout`, and the frame sizes
61
+ * the track and places the resize handle from that. The expand control is
62
+ * not this column's: it is a button in the conversation header. The root
63
+ * occupant decides when to render its Session-bound content.
61
64
  */
62
- 'details': {
65
+ 'rightbar': {
63
66
  kind: 'single';
64
- scope: 'session';
65
- owner: DetailsOwnerProps;
67
+ scope: 'root';
68
+ owner: RightbarOwnerProps;
66
69
  };
67
70
  /**
68
71
  * Frame-wide floating layer, above every column and outside their scroll
@@ -87,18 +90,24 @@ export interface SidebarOwnerProps {
87
90
  /** Rendered column width in px (SIDEBAR_COLLAPSED when collapsed). */
88
91
  width: number;
89
92
  }
90
- /** Conversation owner share: business state and actions belong to the registrant. */
91
- export interface ConvOwnerProps {
92
- }
93
- /** Details owner share: empty — sessionId arrives as a framework-standard prop. */
94
- export interface DetailsOwnerProps {
93
+ /** Right column owner share: resolved normal geometry and opening eligibility. */
94
+ export interface RightbarOwnerProps {
95
+ /** Resolved normal panel width in px, not the saved preference; zero if it cannot fit. */
96
+ width: number;
97
+ /** Current frame width in px. */
98
+ viewportWidth: number;
99
+ /**
100
+ * Whether a normal right panel can retain 300px beside a 400px center.
101
+ * Before a narrow opening, includes the space from collapsing the left sidebar.
102
+ */
103
+ canShow: boolean;
95
104
  }
96
105
  /** Required services (cordis fiber inject — the loader passes all module exports as an object plugin). */
97
106
  export declare const inject: string[];
98
107
  /**
99
108
  * Client plugin body: provide ctx.layout, then one register() call — AppFrame
100
109
  * into 'root' with the four child-slot declarations, the layout store seat,
101
- * and the inject hook that hands the store's bound actions to the service.
110
+ * and the shared root instance supplying commands and the panel-info source.
102
111
  * @param ctx - client root context.
103
112
  */
104
113
  export declare function apply(ctx: ClientContext): void;
@@ -1,47 +1,72 @@
1
1
  /**
2
2
  * LayoutController: the cross-plugin panel-action face behind ctx.layout.
3
- * Panel geometry itself lives in the root entry's layout store (stores.ts);
3
+ * Panel geometry and main-panel selection live in the root layout store;
4
4
  * the current-session selection lives with the runtime sessions service, and
5
5
  * the per-session active view dissolved into ui-conversation's session store
6
6
  * (its only consumer). What remains here is the contract other plugins'
7
- * apply worlds reach for panel transitions (sidebar toggle from ui-sidebar,
8
- * details open/close from ui-conversation) — writes stay inside the store's
9
- * declared action set, delivered as the registration's bound actions.
7
+ * apply worlds reach for panel transitions (main-panel selection and sidebar toggle,
8
+ * right-panel show/hide from ui-sidebar-right) — writes stay inside the
9
+ * store's declared action set, shared with the root registration.
10
10
  */
11
11
  import type { BoundActions } from '@deepseek-ai/dsh-client-ui-slots';
12
+ import type { Branded } from '@deepseek-ai/dsh-brand';
12
13
  import type { createLayoutStore } from './stores.ts';
14
+ /** Identity shared by a sidebar panel entry and its main-slot occupant. */
15
+ export type MainPanelId = Branded<'MainPanelId'>;
16
+ /** Root-scoped navigation state exposed to panel-aware components. */
17
+ export interface PanelInfo {
18
+ /** Selected global panel; null displays the current Conversation. */
19
+ readonly activePanelId: MainPanelId | null;
20
+ }
13
21
  /** The layout store's bound action set (framework-baked, draft params peeled). */
14
22
  export type PanelActions = BoundActions<ReturnType<typeof createLayoutStore>>;
15
- /**
16
- * The outward layout face (`ctx.layout`): the panel transitions other
17
- * plugins may trigger — and exactly what a test fake must supply. The
18
- * attachPanels wiring hook stays on the concrete class (root-entry assembly
19
- * only).
20
- */
23
+ /** Panel navigation and geometry actions exposed through ctx.layout. */
21
24
  export interface ILayout {
25
+ /**
26
+ * Select a global central panel without changing the current Session.
27
+ * @param panelId - registered main key, or null to show the Conversation.
28
+ * @throws if the selected main key is not registered; preserves the current selection.
29
+ */
30
+ selectPanel(panelId: MainPanelId | null): void;
31
+ /**
32
+ * Start an asynchronous navigation, superseding any earlier pending navigation.
33
+ * @returns a signal aborted by the next navigation or layout disposal; check it before committing UI state.
34
+ */
35
+ beginNavigation(): AbortSignal;
22
36
  /** Toggle the sidebar panel (closed ⟷ contract default width). */
23
37
  toggleSidebar(): void;
24
- /** Open the details panel (no-op when already open). */
25
- openDetails(): void;
26
- /** Close the details panel. */
27
- closeDetails(): void;
38
+ /**
39
+ * Report the right panel's presentation without changing its expanded state.
40
+ * @param track - whether the normal panel width reserves a grid track,
41
+ * including beneath a fullscreen overlay.
42
+ * @param fullscreen - whether the panel covers the frame and hides its outer
43
+ * resize handle; independent of the underlying grid track.
44
+ */
45
+ openRightbar(track: boolean, fullscreen: boolean): void;
46
+ /** Report the right panel as hidden: no track, no handle. */
47
+ closeRightbar(): void;
28
48
  }
29
49
  /** Cross-plugin panel-action face (ctx.layout). */
30
50
  export declare class LayoutController implements ILayout {
31
- #private;
51
+ private readonly panels;
52
+ private readonly hasMainPanel;
53
+ private navigation;
32
54
  /**
33
- * Adopt the root entry's bound store actions. Called from the root
34
- * registration's inject hook (a sanctioned assembly side effect), so the
35
- * face is live from the entry's first render; on entry re-register the
36
- * fresh actions overwrite the stale set.
37
- * @param actions - bound actions of the entry's layout store instance.
55
+ * @param panels - actions of the instance shared with the root entry.
56
+ * @param hasMainPanel - checks the live main-slot registry for a panel id.
38
57
  */
39
- attachPanels(actions: PanelActions): void;
58
+ constructor(panels: PanelActions, hasMainPanel: (id: MainPanelId) => boolean);
59
+ /** Select a global panel or return to the Conversation. */
60
+ selectPanel(panelId: MainPanelId | null): void;
61
+ /** @returns the new pending navigation's cancellation signal. */
62
+ beginNavigation(): AbortSignal;
63
+ /** Invalidate pending navigations when the layout owner is unloaded. */
64
+ dispose(): void;
40
65
  /** Toggle the sidebar panel (closed ⟷ contract default width). */
41
66
  toggleSidebar(): void;
42
- /** Open the details panel (no-op when already open). */
43
- openDetails(): void;
44
- /** Close the details panel. */
45
- closeDetails(): void;
67
+ /** Report the right panel's track and fullscreen presentation. */
68
+ openRightbar(track: boolean, fullscreen: boolean): void;
69
+ /** Report the right panel as hidden: no track, no handle. */
70
+ closeRightbar(): void;
46
71
  }
47
72
  //# sourceMappingURL=service.d.ts.map
@@ -1,46 +1,70 @@
1
1
  /**
2
- * The root entry's transient layout store: panel geometry as plain widths in
3
- * px (0 = closed). Module level exports the factory only a module-level
4
- * handle would pin the store's identity in the module
5
- * cache (a de-facto singleton surviving plugin reloads). register() receives
6
- * the factory (exclusive use: the framework instantiates per entry), AppFrame
7
- * derives its PropsStore share from the return type, and the service face
8
- * receives the bound actions through the registration's inject hook.
2
+ * Root-owned frame measurement, panel preferences, and presentation reports.
3
+ * The registration supplies a fresh store and binds its actions to ctx.layout.
9
4
  */
10
5
  import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
6
+ import type { MainPanelId } from './service.ts';
11
7
  /**
12
- * Layout store state: panel width preferences in px (0 = closed), plus the
13
- * narrow-viewport pair `narrow` mirrors AppFrame's breakpoint reading
14
- * (viewport < SIDEBAR_AUTO_COLLAPSE) so toggleSidebar can pick semantics, and
15
- * `narrowExpanded` is the manual override that re-expands the auto-collapsed
16
- * sidebar over the squeezed center without rewriting the width preference.
8
+ * Transient layout preferences. Responsive concessions never rewrite widths;
9
+ * the right panel's expanded state belongs to its occupant.
17
10
  */
18
11
  type LayoutState = {
12
+ panelInfo: {
13
+ /** Null selects the Conversation; global panels keep the current Session intact. */
14
+ activePanelId: MainPanelId | null;
15
+ };
16
+ layoutInfo: LayoutInfo;
17
+ };
18
+ type LayoutInfo = {
19
19
  sidebar: number;
20
- details: number;
21
- narrow: boolean;
20
+ /** Last positive frame measurement; window width bootstraps the first render. */
21
+ viewportWidth: number;
22
22
  narrowExpanded: boolean;
23
+ /**
24
+ * Saved right panel width in px, or null before its first opening. Resizing
25
+ * the frame and closing the panel preserve this preference.
26
+ */
27
+ rightbar: number | null;
28
+ /**
29
+ * Whether the right panel is drawn at all, in either presentation.
30
+ *
31
+ * Derived chrome, not a source of truth: whether the right surface is
32
+ * expanded is a recorded fact owned by that surface, reported here so the
33
+ * frame can place the panel's resize handle. The occupant reports it; nothing
34
+ * else writes it.
35
+ */
36
+ rightbarShown: boolean;
37
+ /**
38
+ * Whether the normal panel width reserves a grid track, including beneath
39
+ * fullscreen. Reported by the occupant; always false while hidden.
40
+ */
41
+ rightbarTrack: boolean;
42
+ /** Reported fullscreen presentation; hides the outer resize handle. */
43
+ rightbarFullscreen: boolean;
44
+ /** Suppress transitions for a fullscreen exit until another geometry action. */
45
+ rightbarInstant: boolean;
23
46
  };
24
47
  /**
25
48
  * Annotation twin of the actions literal below (the export needs a declared
26
49
  * return type); drift fails assignability at the defineStore call.
27
50
  */
28
51
  type LayoutActions = {
52
+ selectPanel: (draft: LayoutState, panelId: MainPanelId | null) => void;
53
+ retainMainPanels: (draft: LayoutState, panelIds: readonly string[]) => void;
29
54
  setSidebar: (draft: LayoutState, px: number) => void;
30
- setDetails: (draft: LayoutState, px: number) => void;
31
55
  toggleSidebar: (draft: LayoutState) => void;
32
- setNarrow: (draft: LayoutState, narrow: boolean) => void;
33
- openDetails: (draft: LayoutState) => void;
34
- closeDetails: (draft: LayoutState) => void;
56
+ setViewportWidth: (draft: LayoutState, width: number) => void;
57
+ setRightbar: (draft: LayoutState, px: number) => void;
58
+ openRightbar: (draft: LayoutState, track: boolean, fullscreen: boolean) => void;
59
+ closeRightbar: (draft: LayoutState) => void;
35
60
  };
36
61
  /**
37
- * Create the layout panel store handle. The preference IS the width, so
38
- * closing a panel forgets its drag width — reopening restores the contract
39
- * default. Actions are the complete write set: drag writes clamp
40
- * into the panel's contract range and never cross the open/closed line;
41
- * open/close transitions write 0 / the default explicitly. Below the
42
- * auto-collapse breakpoint (AppFrame feeds setNarrow) the sidebar toggle
43
- * flips the narrowExpanded override instead of the preference.
62
+ * Create the layout panel store handle. For the sidebar the preference IS the
63
+ * width, so closing it forgets its drag width — reopening restores the contract
64
+ * default. The right panel initializes at 45% of the frame on first opening
65
+ * and keeps that px preference across resizes and close. Drag writes clamp to
66
+ * the current frame's range. Narrow sidebar toggles change only the expansion
67
+ * override; opening the right panel clears that override.
44
68
  * @returns the store handle (spec + type + identity + factory in one).
45
69
  */
46
70
  export declare function createLayoutStore(): EngineStoreHandle<LayoutState, LayoutActions>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-ui-layout",
3
3
  "description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)",
4
- "version": "0.1.3-alpha.2",
4
+ "version": "0.1.5-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -43,12 +43,13 @@
43
43
  "devDependencies": {
44
44
  "@types/react": "~18.3.1",
45
45
  "react": "^18.2.0",
46
- "@deepseek-ai/dsh-client-locale": "^0.1.3-alpha.2",
47
- "@deepseek-ai/dsh-client-store": "^0.1.3-alpha.2",
48
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.3-alpha.2",
49
- "@deepseek-ai/dsh-client-ui-session": "^0.1.3-alpha.2",
50
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.3-alpha.2",
51
- "@deepseek-ai/dsh-client-ui-theme": "^0.1.3-alpha.2",
46
+ "@deepseek-ai/dsh-brand": "^0.1.5-alpha.2",
47
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-alpha.2",
48
+ "@deepseek-ai/dsh-client-store": "^0.1.5-alpha.2",
49
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-alpha.2",
50
+ "@deepseek-ai/dsh-client-ui-session": "^0.1.5-alpha.2",
51
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-alpha.2",
52
+ "@deepseek-ai/dsh-client-ui-theme": "^0.1.5-alpha.2",
52
53
  "@deepseek-ai/cordis": "^4.0.2"
53
54
  },
54
55
  "files": [