dsh-focus-overlay 0.12.2 → 0.12.4

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.en.md CHANGED
@@ -10,10 +10,11 @@
10
10
  </p>
11
11
 
12
12
  <p align="center">
13
- <img src="https://img.shields.io/npm/v/dsh-focus-overlay" alt="npm version">
14
- <img src="https://img.shields.io/npm/dm/dsh-focus-overlay" alt="npm downloads (monthly)">
15
- <img src="https://badgen.net/badge/license/MIT/green" alt="license">
16
- <img src="https://badgen.net/badge/dsh/%3E%3D0.1.2-rc.1/blue" alt="dsh version">
13
+ <a href="https://www.npmjs.com/package/dsh-focus-overlay"><img src="https://img.shields.io/npm/v/dsh-focus-overlay" alt="npm version"></a>
14
+ <a href="https://www.npmjs.com/package/dsh-focus-overlay"><img src="https://img.shields.io/npm/dm/dsh-focus-overlay" alt="npm downloads (monthly)"></a>
15
+ <a href="https://github.com/boogoo619/dsh-focus-overlay/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license: MIT"></a>
16
+ <a href="https://github.com/boogoo619/dsh-focus-overlay/blob/master/package.json"><img src="https://img.shields.io/badge/dsh-%3E%3D0.1.2--rc.1-blue" alt="dsh version"></a>
17
+ <a href="https://github.com/awesome-dsh-plugin/awesome-dsh-plugin"><img src="https://img.shields.io/badge/awesome-dsh--plugin-orange" alt="awesome dsh plugin"></a>
17
18
  </p>
18
19
 
19
20
  ## Features
@@ -55,7 +56,7 @@ Click the line to expand that turn's full work detail (per-category one-line sum
55
56
  | Feature | Description |
56
57
  | --- | --- |
57
58
  | Full-screen overlay | Registered into `shell.overlay` (additive, `replaceRisk: none`) — covers header / composer / sidebar; the thin top bar keeps only the session title and an exit button |
58
- | Official rendering primitives | Assistant text through official `MarkdownText` (GFM + code highlighting + TeX + code copy buttons + footnotes); user messages through `MessageText`; buttons, modals and icons are all official primitives |
59
+ | Official rendering primitives | Assistant text through official `MarkdownText` (GFM + code highlighting + TeX + code copy buttons + footnotes); user messages through the official `projectUserText` projection; buttons, modals and icons are all official primitives |
59
60
  | Image resolution | Assistant `image` blocks resolve through `uiConversation.imageUrl` (the dsh 0.1.2+ session-authorized image cache), falling back to the legacy resolver on older dsh |
60
61
  | Tool-call folding | Follows the official "transcript view" preference (normal / compact); compact mode folds each turn's work part into a "N tool calls · M messages · K subagents" disclosure line ("Thought for a while" when nothing is countable), clickable to expand the full detail; steering messages always stay top-level, never buried; the last turn stays unfolded while streaming |
61
62
  | Categorized summaries | The expanded detail counts tools by family: commands / edits / searches / reads / directory listings / subagents / todos / goals / workflows / skills / questions / plans / background jobs / context injections |
@@ -75,12 +76,13 @@ Click the line to expand that turn's full work detail (per-category one-line sum
75
76
  | Focus counter | A row at the bottom of the settings card totals your focus-mode entries (auto-entries included), persisted to `localStorage`; past 100 entries a Star nudge appears — after the first use it is remembered and replaced by a direct "open the project page" button |
76
77
  | Esc peels layer by layer | Answer card → input bar → focus mode, one layer per press; Esc mid-IME-composition cancels the composition first |
77
78
  | i18n | Chinese / English copy registered under the `focus` namespace, follows the UI language |
78
- | Plugin configuration card | A collapsible card under "Settings → Plugins → Plugin configuration", preferences persisted to `localStorage`; the Node half registers the same settings namespace (schemastery schema) so the tab dispatches it |
79
- | First-run onboarding | After installing, the welcome page shows a one-time "Focus Mode" intro (feature overview + in-place configuration) as a `settings.onboarding` step with its own versioned seen-flag |
80
- | Official preference sync | Reads the official "transcript view" preference (normal / compact); flipping it applies immediately |
79
+ | Plugin configuration | On dsh 0.2 and newer, open this plugin's detail page from the sidebar "Plugins" page — the configuration renders between the package description and its component rows (`plugins.bundle.config`). On dsh ≤ 0.1.x it stays the collapsible "Settings → Plugins → Plugin configuration" card (`settings.plugin.item`). Preferences persist to `localStorage`; the Node half registers the same settings namespace (schemastery schema) |
80
+ | First-run onboarding | After installing, the welcome page shows a one-time "Focus Mode" intro (feature overview + in-place configuration) as a `settings.onboarding` step with its own versioned seen-flag; its "open settings" action jumps to whichever configuration surface the running dsh declares |
81
+ | Official preference sync | Reads the official "transcript view" preference (normal / compact on dsh ≤ 0.1.x; compact / standard / detailed / verbose on dsh 0.2); flipping it applies immediately |
81
82
  | Render error boundary | The overlay content is wrapped in an error boundary: one render crash never bricks the Focus button — the next open retries |
82
83
  | DSH-better-sidebar compatibility | While in focus, a `<body>` marker makes the package stylesheet hide better-sidebar's top-right panel toggles and any open right/bottom panels (and release the squeezed layout); restored on exit |
83
84
  | Legacy compatibility mode | On dsh < 0.1.2 missing the new services, the overlay degrades gracefully (legacy snapshot adapters) with a one-time console notice — no crash |
85
+ | Cross-release primitive compatibility | Official primitives renamed between major releases fall back by availability (dsh 0.2 removed the fixed-size `…14` / `…16` icons in favour of `…Regular` + `size`), so an `undefined` component can never throw React #130 |
84
86
 
85
87
  ## Install
86
88
 
@@ -119,7 +121,7 @@ dsh web
119
121
 
120
122
  Restart `dsh web` after install.
121
123
 
122
- > **First run**: after restarting, the welcome page shows a one-time "Focus Mode" intro that explains the features and lets you configure them in place; all of these settings can be changed anytime in "Settings → Plugins → Plugin configuration".
124
+ > **First run**: after restarting, the welcome page shows a one-time "Focus Mode" intro that explains the features and lets you configure them in place; all of these settings can be changed anytime — on newer dsh open this plugin's detail page from the sidebar "Plugins" page, on older dsh use "Settings → Plugins → Plugin configuration".
123
125
 
124
126
  ## Usage
125
127
 
@@ -133,7 +135,11 @@ Restart `dsh web` after install.
133
135
 
134
136
  ## Settings
135
137
 
136
- In the sidebar "Settings → Plugins → Plugin configuration", expand the "Focus Mode" card:
138
+ **On dsh 0.2 and newer**: sidebar "Plugins" page → find `dsh-focus-overlay` → the configuration shows on its detail page, between the package description and the component rows.
139
+
140
+ **On dsh ≤ 0.1.x**: sidebar "Settings → Plugins → Plugin configuration" → expand the "Focus Mode" card.
141
+
142
+ Both locations present the same options:
137
143
 
138
144
  | Option | Description |
139
145
  | --- | --- |
package/README.md CHANGED
@@ -8,10 +8,11 @@
8
8
  </p>
9
9
 
10
10
  <p align="center">
11
- <img src="https://img.shields.io/npm/v/dsh-focus-overlay" alt="npm version">
12
- <img src="https://img.shields.io/npm/dm/dsh-focus-overlay" alt="npm downloads (monthly)">
13
- <img src="https://badgen.net/badge/license/MIT/green" alt="license">
14
- <img src="https://badgen.net/badge/dsh/%3E%3D0.1.2-rc.1/blue" alt="dsh version">
11
+ <a href="https://www.npmjs.com/package/dsh-focus-overlay"><img src="https://img.shields.io/npm/v/dsh-focus-overlay" alt="npm version"></a>
12
+ <a href="https://www.npmjs.com/package/dsh-focus-overlay"><img src="https://img.shields.io/npm/dm/dsh-focus-overlay" alt="npm downloads (monthly)"></a>
13
+ <a href="https://github.com/boogoo619/dsh-focus-overlay/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license: MIT"></a>
14
+ <a href="https://github.com/boogoo619/dsh-focus-overlay/blob/master/package.json"><img src="https://img.shields.io/badge/dsh-%3E%3D0.1.2--rc.1-blue" alt="dsh version"></a>
15
+ <a href="https://github.com/awesome-dsh-plugin/awesome-dsh-plugin"><img src="https://img.shields.io/badge/awesome-dsh--plugin-orange" alt="awesome dsh plugin"></a>
15
16
  </p>
16
17
 
17
18
  ## 功能
@@ -53,7 +54,7 @@
53
54
  | 功能 | 说明 |
54
55
  | --- | --- |
55
56
  | 全屏专注遮罩 | 经 `shell.overlay` 注册(纯增量、`replaceRisk: none`),覆盖标题区 / 输入区 / 侧栏;顶部只留会话标题与「退出专注」按钮 |
56
- | 官方渲染原语 | AI 文本走官方 `MarkdownText`(GFM + 代码高亮 + TeX + 代码复制按钮 + 脚注),用户消息走 `MessageText`;按钮、模态、图标均为官方原语 |
57
+ | 官方渲染原语 | AI 文本走官方 `MarkdownText`(GFM + 代码高亮 + TeX + 代码复制按钮 + 脚注),用户消息走官方 `projectUserText` 投影;按钮、模态、图标均为官方原语 |
57
58
  | 图片解析 | assistant 的 `image` 块经 `uiConversation.imageUrl`(dsh 0.1.2+ 的会话授权图片缓存)解析,旧版 dsh 自动回退到 legacy 解析器 |
58
59
  | 工具调用折叠 | 遵循官方「对话显示」偏好(normal / compact);compact 模式把每轮的工作部分折叠成「N 次工具调用 · M 条消息 · K 个 subagent」披露行(无内容时显示「已思考」),点击可展开完整明细;steering 消息始终独立成行不被折叠;流式中的最后一轮保持展开直到结束 |
59
60
  | 分类摘要明细 | 展开后按工具类别计数:命令 / 编辑 / 搜索 / 读取 / 列目录 / 子代理 / 待办 / 目标 / 工作流 / 技能 / 提问 / 计划 / 后台任务 / 上下文注入 |
@@ -73,12 +74,13 @@
73
74
  | 专注计数 | 设置卡片最底部一行累计进入专注模式的次数(含自动进入),持久化到 `localStorage`;超过 100 次出现 Star 引导按钮,点击前往仓库页面并记住状态,之后改为「前往 GitHub 项目页面」直达按钮 |
74
75
  | Esc 逐层退出 | 答题卡 → 输入条 → 专注模式,一层一层退;输入法组词中的 Esc 优先取消组词 |
75
76
  | i18n | 中 / 英文案,注册到 `focus` 命名空间,跟随界面语言 |
76
- | 插件配置卡片 | 「设置 → 插件 → 插件配置」中的可折叠卡片,偏好持久化到 `localStorage`;Node 半区以 schemastery schema 注册同名设置 namespace 供其分发 |
77
- | 首次引导 | 首次安装后欢迎页弹出一次「专注模式」引导(功能一览 + 就地配置),`settings.onboarding` 步骤自带已读标记与版本号 |
78
- | 官方偏好联动 | 读取官方「对话显示」(normal / compact)偏好,切换即时生效 |
77
+ | 插件配置卡片 | dsh 0.2 起在侧栏「Plugins」页面打开本插件详情页,配置直接渲染在包描述与组件列表之间(`plugins.bundle.config`);dsh ≤ 0.1.x 仍为「设置 → 插件 → 插件配置」中的可折叠卡片(`settings.plugin.item`)。偏好持久化到 `localStorage`;Node 半区以 schemastery schema 注册同名设置 namespace |
78
+ | 首次引导 | 首次安装后欢迎页弹出一次「专注模式」引导(功能一览 + 就地配置),`settings.onboarding` 步骤自带已读标记与版本号;「打开设置」按当前 dsh 版本跳到对应配置页 |
79
+ | 官方偏好联动 | 读取官方「对话显示」偏好(dsh ≤ 0.1.x:normal / compact;dsh 0.2:compact / standard / detailed / verbose),切换即时生效 |
79
80
  | 渲染错误兜底 | 遮罩内容包裹错误边界:一次渲染崩溃不会永久打死「专注」按钮,下次打开自动重试 |
80
81
  | 兼容 DSH-better-sidebar | 进入专注时在 `<body>` 打标记,样式层自动隐藏其右上角的收放面板按钮与已开启的右侧 / 底部面板,并释放被挤压的布局;退出专注后按原状态还原 |
81
82
  | 旧版兼容模式 | dsh < 0.1.2 缺少新服务时降级运行(legacy 快照适配),控制台一次性提示,不会崩溃 |
83
+ | 跨版本构件兼容 | 图标等官方构件在两个大版本间改名时按可用性回退(dsh 0.2 移除了 `…14` / `…16` 定尺图标,改用 `…Regular` + `size`),不会因为 `undefined` 组件触发 React #130 |
82
84
 
83
85
  ## 安装
84
86
 
@@ -117,7 +119,7 @@ dsh web
117
119
 
118
120
  安装后重启 `dsh web` 生效。
119
121
 
120
- > **首次安装**:重启后欢迎页会弹出一次「专注模式」引导,说明功能并让你就地完成配置;其中所有设置随时可在「设置 → 插件 → 插件配置」中更改。
122
+ > **首次安装**:重启后欢迎页会弹出一次「专注模式」引导,说明功能并让你就地完成配置;其中所有设置随时可以更改——新版 dsh 在侧栏「Plugins」页面中打开本插件的详情页,旧版在「设置 → 插件 → 插件配置」中。
121
123
 
122
124
  ## 使用
123
125
 
@@ -131,7 +133,11 @@ dsh web
131
133
 
132
134
  ## 设置
133
135
 
134
- 侧栏「设置 → 插件 → 插件配置」中,展开「专注模式」卡片:
136
+ **dsh 0.2 起**:侧栏「Plugins」页面 → 找到 `dsh-focus-overlay` → 详情页中,配置直接显示在包描述与组件列表之间。
137
+
138
+ **dsh ≤ 0.1.x**:侧栏「设置 → 插件 → 插件配置」→ 展开「专注模式」卡片。
139
+
140
+ 两种位置呈现同一组选项:
135
141
 
136
142
  | 选项 | 说明 |
137
143
  | --- | --- |
package/lib/client.js CHANGED
@@ -3,9 +3,62 @@ window.__ModuleLoader__.load({
3
3
  factory: (require) => {
4
4
  var module = { exports: {} };
5
5
  module.exports;
6
+ //#region \0rolldown/runtime.js
7
+ var __create = Object.create;
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __getProtoOf = Object.getPrototypeOf;
12
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
15
+ key = keys[i];
16
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
17
+ get: ((k) => from[k]).bind(null, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+ //#endregion
6
28
  let react = require("react");
7
29
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
30
+ _deepseek_ai_dsh_client_ui_primitives = __toESM(_deepseek_ai_dsh_client_ui_primitives, 1);
8
31
  let react_jsx_runtime = require("react/jsx-runtime");
32
+ //#region src/client/icons.ts
33
+ /** Cross-release product-icon resolution.
34
+ *
35
+ * The official primitives package renamed its icon set in dsh 0.2: the
36
+ * fixed-size artwork variants (`IconCheckOutline14`, `IconChevronDownOutline14`,
37
+ * `IconEditOutline16`, `IconSendOutline14`) were removed, leaving one
38
+ * `…OutlineRegular` / `…OutlineMedium` component per glyph that takes an
39
+ * explicit `size` prop. A stale named import therefore resolves to
40
+ * `undefined`, and React throws #130 ("element type is invalid") the moment
41
+ * the component renders — the same failure mode the 0.1.3 `MessageText`
42
+ * removal produced.
43
+ *
44
+ * Import the namespace once and pick the first name the running build exports,
45
+ * so one bundle serves both generations. Each fallback carries the size the
46
+ * removed variant was drawn at, keeping layout identical across releases. */
47
+ function pick(names, size) {
48
+ for (const name of names) {
49
+ const component = _deepseek_ai_dsh_client_ui_primitives[name];
50
+ if (typeof component === "function") return (props) => component({
51
+ size,
52
+ ...props
53
+ });
54
+ }
55
+ return () => null;
56
+ }
57
+ const IconCheck = pick(["IconCheckOutline14", "IconCheckOutlineRegular"], 14);
58
+ const IconChevronDown = pick(["IconChevronDownOutline14", "IconChevronDownOutlineRegular"], 14);
59
+ const IconEdit = pick(["IconEditOutline16", "IconEditOutlineRegular"], 16);
60
+ const IconSend = pick(["IconSendOutline14", "IconSendOutlineRegular"], 14);
61
+ //#endregion
9
62
  //#region src/client/model.ts
10
63
  function legacySliceOf(chatView, snap) {
11
64
  const legacy = chatView && chatView.legacy ? chatView.legacy : null;
@@ -25,6 +78,61 @@ window.__ModuleLoader__.load({
25
78
  } catch {}
26
79
  return null;
27
80
  }
81
+ /** dsh 0.2 moved the pending interaction off the uiSession
82
+ * `pendingInteractions` map and onto the unified `sessionStatus` snapshot
83
+ * (`ReadonlyMap<sessionId, { running, pendingInteraction, completionUnread }>`).
84
+ * Both carriers answer with the same interaction face, so the overlay's answer
85
+ * card resolves through either. Null when nothing is pending. */
86
+ function pendingInteractionFromStatus(status, sessionId) {
87
+ try {
88
+ const entry = status && typeof status.get === "function" ? status.get(sessionId) : null;
89
+ return entry && entry.pendingInteraction || null;
90
+ } catch {}
91
+ return null;
92
+ }
93
+ /** The current session id, resolved across dsh generations.
94
+ *
95
+ * dsh <= 0.1.x carried the viewer's selection on the session-list snapshot
96
+ * (`sessions.list.getSnapshot().current`). dsh 0.2 dropped that field: the
97
+ * selection is the session the main view retains, published on the list rows
98
+ * themselves (`byId[id].retainedBy.mainView > 0`) — the same public read the
99
+ * official cordis inspector uses. `mainBinding` is the uiSession main-binding
100
+ * snapshot, kept as a guarded last resort: it carries the same id, but that
101
+ * member is typed private, so the public list data stays primary. */
102
+ function currentSessionIdOf(listState, mainBinding) {
103
+ const fromList = listState ? listState.current : void 0;
104
+ if (fromList !== void 0 && fromList !== null) return fromList;
105
+ const byId = listState && listState.byId;
106
+ if (byId && typeof byId === "object") {
107
+ for (const row of Object.values(byId)) if (row && (row.retainedBy && row.retainedBy.mainView || 0) > 0) return row.id;
108
+ }
109
+ if (mainBinding) {
110
+ const id = mainBinding.key ?? (mainBinding.props ? mainBinding.props.sessionId : void 0);
111
+ if (id !== void 0 && id !== null) return id;
112
+ }
113
+ }
114
+ /** The overlay fold mode derived from the official "对话显示" preference.
115
+ *
116
+ * dsh <= 0.1.x served it through the `settingsScope` service bound to the
117
+ * `ui-chat` namespace (`{ transcriptView: 'normal' | 'compact' }`). dsh 0.2
118
+ * retired `settingsScope`; the same preference is a Host settings field read
119
+ * through `configForms.get('ui-chat')`, whose snapshot nests it under `value`
120
+ * (and widened the accepted set to compact | standard | detailed | verbose,
121
+ * with `normal` / `expanded` kept as legacy saved values). Only `compact`
122
+ * folds the tool flow; every other explicit mode reads as the expanded
123
+ * presentation. Absence keeps the overlay's own compact default. */
124
+ function transcriptViewOf(scopeOrForm) {
125
+ try {
126
+ if (!scopeOrForm || typeof scopeOrForm.getSnapshot !== "function") return "compact";
127
+ const snapshot = scopeOrForm.getSnapshot();
128
+ const section = snapshot && snapshot.value && typeof snapshot.value === "object" ? snapshot.value : snapshot;
129
+ const mode = section ? section.transcriptView : null;
130
+ if (mode === void 0 || mode === null) return "compact";
131
+ return mode === "compact" ? "compact" : "normal";
132
+ } catch {
133
+ return "compact";
134
+ }
135
+ }
28
136
  /** Legacy pending interaction — pre-0.1.2 dsh carried a flat `pending` array
29
137
  * on the session snapshot, with a different carrier shape (`payload`
30
138
  * envelope, answers via `wait.respond({ok, value})` returning a `receipt`).
@@ -70,6 +178,28 @@ window.__ModuleLoader__.load({
70
178
  });
71
179
  return shaped;
72
180
  }
181
+ /** Build the `dsh-resource://file/session/<id>/<path>` address that dsh 0.2's
182
+ * Sidebar resource controller opens a file through.
183
+ *
184
+ * The official `fileAddressFor` lives in `@deepseek-ai/dsh-util-workspace-path`,
185
+ * but the client bundle purity gate keeps feature bundles from importing other
186
+ * plugins' values across the boundary — the official Chat package inlines its
187
+ * own copy of the same grammar, and so does this one. Absolute paths inside the
188
+ * session workspace are reduced to workspace-relative; every id and path
189
+ * segment is component-encoded, with `:` kept literal for Windows drives. */
190
+ function sessionFileAddressOf(sessionId, cwd, path) {
191
+ const encode = (segment) => encodeURIComponent(segment).replace(/%3A/gi, ":");
192
+ const normalized = String(path).replace(/\\/g, "/").replace(/^(?:\.\/)+/, "");
193
+ const absolute = normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized);
194
+ let relative = normalized;
195
+ if (absolute) {
196
+ const root = cwd === void 0 || cwd === null ? "" : String(cwd).replace(/\\/g, "/").replace(/\/+$/, "");
197
+ if (root !== "" && normalized === root) relative = "";
198
+ else if (root !== "" && normalized.startsWith(root + "/")) relative = normalized.slice(root.length + 1);
199
+ }
200
+ const body = relative.split("/").map(encode).join("/");
201
+ return `dsh-resource://file/session/${encode(String(sessionId))}/${body}`;
202
+ }
73
203
  function flattenText(content) {
74
204
  if (!content || !content.length) return "";
75
205
  let out = "";
@@ -933,7 +1063,7 @@ window.__ModuleLoader__.load({
933
1063
  "onboarding.feature.autoFocus": "自动专注与提醒 —— 回复完成自动进入专注,AI 提问 / 等待时提醒",
934
1064
  "onboarding.feature.hotkey": "快捷键 F —— 任意界面按 F 一键进入专注,可在设置中关闭",
935
1065
  "onboarding.configure.title": "快速配置",
936
- "onboarding.laterNote": "这些设置随时可在「设置 → 插件 → 插件配置」中更改。",
1066
+ "onboarding.laterNote": "这些设置随时可以更改:新版 dsh 在侧栏「Plugins」页面中打开本插件的详情页,旧版在「设置 → 插件 → 插件配置」中。",
937
1067
  "onboarding.openSettings": "打开设置",
938
1068
  "onboarding.done": "完成"
939
1069
  };
@@ -1019,7 +1149,7 @@ window.__ModuleLoader__.load({
1019
1149
  "onboarding.feature.autoFocus": "Auto-focus & reminders — auto-enter after a reply, notify while the AI waits",
1020
1150
  "onboarding.feature.hotkey": "Hotkey F — press F anywhere to enter focus mode (toggleable in settings)",
1021
1151
  "onboarding.configure.title": "Quick setup",
1022
- "onboarding.laterNote": "You can change these anytime in Settings → Plugins → Plugin configuration.",
1152
+ "onboarding.laterNote": "Change these anytime: on newer dsh open this plugin’s detail page from the sidebar Plugins page; on older dsh use Settings → Plugins → Plugin configuration.",
1023
1153
  "onboarding.openSettings": "Open settings",
1024
1154
  "onboarding.done": "Done"
1025
1155
  };
@@ -1109,7 +1239,7 @@ window.__ModuleLoader__.load({
1109
1239
  "aria-pressed": on,
1110
1240
  children: [multi ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1111
1241
  className: "fm-opt-check" + (on ? " fm-opt-check-on" : ""),
1112
- children: on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline14, {}) : null
1242
+ children: on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCheck, {}) : null
1113
1243
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1114
1244
  className: "fm-opt-num",
1115
1245
  children: index + 1
@@ -1220,7 +1350,7 @@ window.__ModuleLoader__.load({
1220
1350
  title: t("answer.close"),
1221
1351
  "aria-label": t("answer.close"),
1222
1352
  onClick: onClose,
1223
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
1353
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDown, {})
1224
1354
  })]
1225
1355
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1226
1356
  className: "fm-card-head",
@@ -1236,7 +1366,7 @@ window.__ModuleLoader__.load({
1236
1366
  title: t("answer.close"),
1237
1367
  "aria-label": t("answer.close"),
1238
1368
  onClick: onClose,
1239
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
1369
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDown, {})
1240
1370
  })
1241
1371
  ]
1242
1372
  }),
@@ -1434,7 +1564,7 @@ window.__ModuleLoader__.load({
1434
1564
  disabled: empty,
1435
1565
  onMouseDown: (e) => e.preventDefault(),
1436
1566
  onClick: onSend,
1437
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSendOutline14, {})
1567
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconSend, {})
1438
1568
  })
1439
1569
  ]
1440
1570
  })
@@ -1471,7 +1601,7 @@ window.__ModuleLoader__.load({
1471
1601
  title: t("composer.expand"),
1472
1602
  "aria-label": t("composer.expand"),
1473
1603
  onClick: onExpand,
1474
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {}), hasDraft ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "fm-pill-dot" }) : null]
1604
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconEdit, {}), hasDraft ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "fm-pill-dot" }) : null]
1475
1605
  });
1476
1606
  }
1477
1607
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -1480,7 +1610,7 @@ window.__ModuleLoader__.load({
1480
1610
  title: t("backToLatest"),
1481
1611
  "aria-label": t("backToLatest"),
1482
1612
  onClick: onJumpBottom,
1483
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 16 })
1613
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDown, { size: 16 })
1484
1614
  });
1485
1615
  }
1486
1616
  //#endregion
@@ -1813,6 +1943,9 @@ window.__ModuleLoader__.load({
1813
1943
  .fm-plugin-card-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}
1814
1944
  .fm-plugin-card-chevron-open{transform:rotate(180deg)}
1815
1945
  .fm-plugin-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:16px}
1946
+ /* dsh 0.2 Plugins-page bundle configuration: the page draws its own title,
1947
+ icon and crumb, so this surface is body-only — no card border or collapse. */
1948
+ .fm-plugin-page{flex-direction:column;display:flex}
1816
1949
  .fm-plugin-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
1817
1950
  .fm-plugin-field+.fm-plugin-field{border-top:1px solid var(--dsw-alias-border-l2)}
1818
1951
  /* The stats row ends the card body, so it carries NO bottom padding of its
@@ -2264,6 +2397,19 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
2264
2397
  (0, react.useEffect)(() => focusStore.subscribe(() => setV(focusStore.getDonePing())), []);
2265
2398
  return v;
2266
2399
  }
2400
+ /** Body of a sent user message / steering row.
2401
+ *
2402
+ * dsh 0.1.3-alpha.2 removed the `MessageText` component from the primitives
2403
+ * module in favour of the `projectUserText` display projection (which has
2404
+ * existed since 0.1.2 and is still exported by 0.1.5). Importing the removed
2405
+ * name made it `undefined`, so the overlay threw React #130 the moment it
2406
+ * rendered a session containing a user message — the "toggle does nothing"
2407
+ * symptom. `projectUserText` returns inline plain runs plus `@session` /
2408
+ * `/skill` chips; `.fm-user` still supplies the bubble chrome and
2409
+ * `white-space: pre-wrap`, so the text keeps its shape. */
2410
+ function UserText({ text }) {
2411
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: (0, _deepseek_ai_dsh_client_ui_primitives.projectUserText)(text, []) });
2412
+ }
2267
2413
  let bodyEl = null;
2268
2414
  const anchors = {};
2269
2415
  function useSessionSnapshot(sessions, sessionId) {
@@ -2288,13 +2434,16 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
2288
2434
  return snap;
2289
2435
  }
2290
2436
  /** The session's pending user interaction (question / plan review / approval),
2291
- * read from the uiSession pending-interactions map — dsh 0.1.2 moved the old
2292
- * snapshot `pending` array into this root-level observable. When the service
2293
- * is absent (older dsh), falls back to the session snapshot's flat `pending`
2294
- * array through the `legacyPendingOf` adapter. Returns null when this session
2295
- * has nothing pending. */
2437
+ * read from the uiSession services. dsh 0.1.2 exposed the root-level
2438
+ * `pendingInteractions` map; dsh 0.2 unified it into `sessionStatus`
2439
+ * (`ReadonlyMap<sessionId, { running, pendingInteraction, completionUnread }>`).
2440
+ * When neither service is present (older dsh), falls back to the session
2441
+ * snapshot's flat `pending` array through the `legacyPendingOf` adapter.
2442
+ * Returns null when this session has nothing pending. */
2296
2443
  function usePendingInteraction(uiSession, sessionId, snap) {
2297
- const source = uiSession && uiSession.pendingInteractions;
2444
+ const legacyMap = uiSession && uiSession.pendingInteractions;
2445
+ const statusMap = uiSession && uiSession.sessionStatus;
2446
+ const source = legacyMap || statusMap;
2298
2447
  const read = () => {
2299
2448
  try {
2300
2449
  return source ? source.getSnapshot() : null;
@@ -2315,7 +2464,47 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
2315
2464
  return;
2316
2465
  }
2317
2466
  }, [source]);
2318
- return pendingInteractionOf(map, sessionId) || legacyPendingOf(snap);
2467
+ return (legacyMap ? pendingInteractionOf(map, sessionId) : pendingInteractionFromStatus(map, sessionId)) || legacyPendingOf(snap);
2468
+ }
2469
+ /** The viewed session id, resolved across dsh generations. dsh <= 0.1.x carried
2470
+ * the selection on the session-list snapshot (`listState.current`); dsh 0.2
2471
+ * dropped it and publishes the main view's retained session as the uiSession
2472
+ * main binding. Subscribes to whichever source exists (both, when both do), so
2473
+ * switching sessions re-resolves either way. */
2474
+ function useCurrentSessionId(sessions, uiSession, listState) {
2475
+ const read = () => {
2476
+ if (!sessions || typeof sessions.list?.getSnapshot !== "function") return void 0;
2477
+ let mainBinding = null;
2478
+ try {
2479
+ mainBinding = uiSession && uiSession.current ? uiSession.current.getSnapshot() : null;
2480
+ } catch {
2481
+ mainBinding = null;
2482
+ }
2483
+ return currentSessionIdOf(sessions.list.getSnapshot(), mainBinding);
2484
+ };
2485
+ const [id, setId] = (0, react.useState)(read);
2486
+ (0, react.useEffect)(() => {
2487
+ if (!sessions || typeof sessions.list?.subscribe !== "function") {
2488
+ setId(void 0);
2489
+ return;
2490
+ }
2491
+ const sync = () => setId(read());
2492
+ sync();
2493
+ const unsubs = [];
2494
+ try {
2495
+ unsubs.push(sessions.list.subscribe(sync));
2496
+ } catch {}
2497
+ const main = uiSession && uiSession.current;
2498
+ if (main && typeof main.subscribe === "function") try {
2499
+ unsubs.push(main.subscribe(sync));
2500
+ } catch {}
2501
+ return () => {
2502
+ for (const u of unsubs) try {
2503
+ u();
2504
+ } catch {}
2505
+ };
2506
+ }, [sessions, uiSession]);
2507
+ return id;
2319
2508
  }
2320
2509
  /** The session's Chat view snapshot (dsh 0.1.2 `ChatSnapshot`) from the
2321
2510
  * uiConversation assembly — the first subscriber activates the chat target,
@@ -2342,20 +2531,25 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
2342
2531
  }, [uiConversation, sessionId]);
2343
2532
  return chat;
2344
2533
  }
2345
- /** The official "对话显示" preference (`ui-chat.transcriptView`, values
2346
- * normal | compact), read reactively from the settings scope the official
2347
- * TranscriptViewPolicy binds. Defaults to compact — the official default —
2348
- * when the scope or the section has not arrived yet. */
2349
- function useTranscriptViewMode(settingsScope) {
2350
- const source = settingsScope;
2351
- const read = () => {
2534
+ /** The official "对话显示" preference (`ui-chat.transcriptView`), read
2535
+ * reactively. dsh <= 0.1.x serves it through the settings scope the official
2536
+ * TranscriptViewPolicy binds (values normal | compact); dsh 0.2 retired that
2537
+ * service and serves the same field through the `configForms` controller for
2538
+ * the `ui-chat` namespace (compact | standard | detailed | verbose, with the
2539
+ * legacy normal | expanded values still accepted). Both are read through the
2540
+ * shared `transcriptViewOf` projection; only `compact` folds the tool flow.
2541
+ * Defaults to compact when no preference has arrived yet. */
2542
+ function useTranscriptViewMode(settingsScope, configForms) {
2543
+ const form = (0, react.useMemo)(() => {
2544
+ if (settingsScope) return settingsScope;
2352
2545
  try {
2353
- const section = source ? source.getSnapshot().value : null;
2354
- return (section ? section.transcriptView : null) === "normal" ? "normal" : "compact";
2546
+ return configForms && typeof configForms.get === "function" ? configForms.get("ui-chat") : null;
2355
2547
  } catch {
2356
- return "compact";
2548
+ return null;
2357
2549
  }
2358
- };
2550
+ }, [settingsScope, configForms]);
2551
+ const source = form && typeof form.getSnapshot === "function" ? form : null;
2552
+ const read = () => transcriptViewOf(source);
2359
2553
  const [mode, setMode] = (0, react.useState)(read);
2360
2554
  (0, react.useEffect)(() => {
2361
2555
  if (!source) {
@@ -2420,13 +2614,13 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
2420
2614
  return seat ? seat.querySelector("[contenteditable=\"true\"]") : null;
2421
2615
  }
2422
2616
  function FocusContent(props) {
2423
- const { useSessions, sessions, workspaces, conversation, chatFileMentions, uiSession, uiConversation, settingsScope, t } = props;
2617
+ const { useSessions, sessions, workspaces, conversation, chatFileMentions, uiSession, uiConversation, settingsScope, configForms, sidebarRight, t } = props;
2424
2618
  const prefs = usePrefs();
2425
2619
  const listState = useSessions((s) => s);
2426
- const currentId = listState ? listState.current : void 0;
2620
+ const currentId = useCurrentSessionId(sessions, uiSession, listState);
2427
2621
  const snap = useSessionSnapshot(sessions, currentId);
2428
2622
  const chatView = useChatView(uiConversation, currentId);
2429
- const transcriptView = useTranscriptViewMode(settingsScope);
2623
+ const transcriptView = useTranscriptViewMode(settingsScope, configForms);
2430
2624
  const legacy = (0, react.useMemo)(() => legacySliceOf(chatView, snap), [chatView, snap]);
2431
2625
  const stable = legacy.partial == null;
2432
2626
  const mdLabels = (0, react.useMemo)(() => markdownLabels(t), [t]);
@@ -2752,6 +2946,24 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
2752
2946
  if (conversation && typeof conversation.resolveImage === "function") return conversation.resolveImage(currentId, attachment);
2753
2947
  return Promise.reject(/* @__PURE__ */ new Error("dsh-focus-overlay: image resolver unavailable"));
2754
2948
  }
2949
+ const openMentionedFile = (0, react.useCallback)((path, options) => {
2950
+ try {
2951
+ if (workspaces && typeof workspaces.openPath === "function") {
2952
+ workspaces.openPath(path).catch(() => {});
2953
+ return;
2954
+ }
2955
+ const cwd = currentId != null ? sessions?.list?.getSnapshot?.()?.byId?.[currentId]?.cwd : void 0;
2956
+ if (sidebarRight && currentId != null && typeof sidebarRight.openResource === "function") {
2957
+ const address = sessionFileAddressOf(String(currentId), cwd, path);
2958
+ sidebarRight.openResource(address, options && options.line !== void 0 ? { params: { line: options.line } } : void 0);
2959
+ }
2960
+ } catch {}
2961
+ }, [
2962
+ sidebarRight,
2963
+ workspaces,
2964
+ sessions,
2965
+ currentId
2966
+ ]);
2755
2967
  const fileMentionsFor = (0, react.useCallback)((node) => {
2756
2968
  if (!chatFileMentions) return void 0;
2757
2969
  try {
@@ -2761,14 +2973,16 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
2761
2973
  step: node.step
2762
2974
  },
2763
2975
  seq: node.seq,
2764
- openFile: (path) => {
2765
- if (workspaces) workspaces.openPath(path).catch(() => {});
2766
- }
2767
- });
2976
+ openFile: (path, options) => openMentionedFile(path, options)
2977
+ }, currentId);
2768
2978
  } catch {
2769
2979
  return;
2770
2980
  }
2771
- }, [chatFileMentions, workspaces]);
2981
+ }, [
2982
+ chatFileMentions,
2983
+ currentId,
2984
+ openMentionedFile
2985
+ ]);
2772
2986
  const binding = (0, react.useMemo)(() => sessions && currentId != null ? sessions.binding(currentId) : void 0, [sessions, currentId]);
2773
2987
  const inputFace = useInputFace(conversation, binding && binding.ctx);
2774
2988
  const inputState = useInputState(inputFace);
@@ -2975,7 +3189,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
2975
3189
  },
2976
3190
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2977
3191
  className: "fm-user",
2978
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MessageText, { text: it.text })
3192
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserText, { text: it.text })
2979
3193
  })
2980
3194
  }, key);
2981
3195
  if (it.kind === "steering") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -2986,7 +3200,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
2986
3200
  },
2987
3201
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2988
3202
  className: "fm-user fm-steering",
2989
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MessageText, { text: it.text })
3203
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserText, { text: it.text })
2990
3204
  })
2991
3205
  }, key);
2992
3206
  if (it.kind === "assistant") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AssistantItem, {
@@ -3024,7 +3238,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
3024
3238
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3025
3239
  className: "fm-process-label",
3026
3240
  children: processLabel(it.counts, t)
3027
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: "fm-process-chevron" })]
3241
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDown, { className: "fm-process-chevron" })]
3028
3242
  }) : null, open ? it.workItems.map((wi, wi2) => renderItem(wi, key + "-w" + wi2, false)) : null] }, key);
3029
3243
  }
3030
3244
  return null;
@@ -3358,6 +3572,15 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
3358
3572
  })
3359
3573
  });
3360
3574
  }
3575
+ /** The settings body both plugin-settings surfaces share: the preference
3576
+ * fields plus the focus-entry counter. Rendered by the legacy Settings card
3577
+ * and by the dsh 0.2 Plugins-page bundle page. */
3578
+ function FocusSettingsBody({ t }) {
3579
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusPrefsFields, { t }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusEntryStat, { t })] });
3580
+ }
3581
+ /** Legacy surface (dsh <= 0.1.x): a collapsible `<li>` card in the Settings
3582
+ * → Plugins tab, registered into the keyed `settings.plugin.item` slot. dsh
3583
+ * 0.2 removed that slot, so this only renders where the slot is declared. */
3361
3584
  function FocusSettingsCard({ t }) {
3362
3585
  const [open, setOpen] = (0, react.useState)(false);
3363
3586
  const title = t("settings.label");
@@ -3378,13 +3601,24 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
3378
3601
  className: "fm-plugin-card-desc",
3379
3602
  children: t("settings.description")
3380
3603
  })]
3381
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: "fm-plugin-card-chevron" + (open ? " fm-plugin-card-chevron-open" : "") })]
3382
- }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3604
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDown, { className: "fm-plugin-card-chevron" + (open ? " fm-plugin-card-chevron-open" : "") })]
3605
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3383
3606
  className: "fm-plugin-card-body",
3384
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusPrefsFields, { t }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusEntryStat, { t })]
3607
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusSettingsBody, { t })
3385
3608
  }) : null]
3386
3609
  });
3387
3610
  }
3611
+ /** dsh 0.2 surface: the bundle's own configuration on the sidebar Plugins
3612
+ * page (`plugins.bundle.config`, keyed by the bundle's package name). The page
3613
+ * draws the title, icon, and crumb itself and always asks for `view: 'page'`,
3614
+ * so this renders the body alone — no card chrome, no collapse. */
3615
+ function FocusBundleConfig({ t, view }) {
3616
+ if (view !== void 0 && view !== "page") return null;
3617
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3618
+ className: "fm-plugin-page",
3619
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusSettingsBody, { t })
3620
+ });
3621
+ }
3388
3622
  function FocusOnboarding(props) {
3389
3623
  const { complete, openSection, t } = props;
3390
3624
  const [done, setDone] = (0, react.useState)(() => onboardingStore.isDone());
@@ -3416,7 +3650,13 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
3416
3650
  onboardingStore.markDone();
3417
3651
  setDone(true);
3418
3652
  finish();
3419
- openSection("plugins");
3653
+ let opened = false;
3654
+ if (typeof props.openSettings === "function") try {
3655
+ opened = props.openSettings() !== false;
3656
+ } catch {
3657
+ opened = false;
3658
+ }
3659
+ if (!opened && typeof openSection === "function") openSection("plugins");
3420
3660
  };
3421
3661
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
3422
3662
  open: true,
@@ -3481,13 +3721,20 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
3481
3721
  /**
3482
3722
  * Focus Mode, browser half: a full-screen reading overlay in `shell.overlay`
3483
3723
  * (additive, never replaces shipped UI), a "专注" button in the session header
3484
- * action row, and a collapsible plugin card in the Plugins settings tab
3485
- * (`settings.plugin.item`). Rendering reuses the official client UI primitives
3486
- * module (`@deepseek-ai/dsh-client-ui-primitives` runtime id) — Markdown /
3487
- * MessageText / Tooltip / Button — plus the conversation service's composer
3488
- * registry and the uiSession pending-interaction service (dsh 0.1.2).
3724
+ * action row, and the plugin's settings card. The settings surface lives where
3725
+ * the running dsh keeps it: the Settings → Plugins tab's keyed
3726
+ * `settings.plugin.item` slot on dsh <= 0.1.x, and the sidebar Plugins page's
3727
+ * `plugins.bundle.config` slot (keyed by this bundle's package name) on dsh
3728
+ * 0.2, which retired both that slot and the `settingsScope` service. Rendering
3729
+ * reuses the official client UI primitives module
3730
+ * (`@deepseek-ai/dsh-client-ui-primitives` runtime id) — Markdown /
3731
+ * projectUserText / Button — plus the conversation service's composer registry
3732
+ * and the uiSession pending-interaction / main-binding service.
3489
3733
  */
3490
3734
  const NS = "focus";
3735
+ /** This bundle's package name — the key `plugins.bundle.config` and the
3736
+ * `pluginNavigation.openBundle` deep link address this plugin by (dsh 0.2). */
3737
+ const PACKAGE_NAME = "dsh-focus-overlay";
3491
3738
  let warnedLegacy = false;
3492
3739
  function warnLegacyOnce() {
3493
3740
  if (warnedLegacy) return;
@@ -3503,8 +3750,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
3503
3750
  "locale",
3504
3751
  "workspaces",
3505
3752
  "uiSession",
3506
- "uiConversation",
3507
- "settingsScope"
3753
+ "uiConversation"
3508
3754
  ],
3509
3755
  apply(ctx) {
3510
3756
  const style = document.createElement("style");
@@ -3537,6 +3783,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
3537
3783
  const workspaces = ctx.workspaces;
3538
3784
  const conversation = ctx.get("conversation");
3539
3785
  const chatFileMentions = ctx.get("chatFileMentions");
3786
+ const sidebarRight = ctx.get("sidebarRight");
3540
3787
  const uiSession = ctx.get("uiSession");
3541
3788
  const uiConversation = ctx.get("uiConversation");
3542
3789
  if (!uiSession || !uiConversation) warnLegacyOnce();
@@ -3547,6 +3794,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
3547
3794
  } catch {
3548
3795
  settingsScope = null;
3549
3796
  }
3797
+ const configForms = ctx.get("configForms");
3550
3798
  ctx.effect(() => {
3551
3799
  let currentId = void 0;
3552
3800
  let unsubSession = null;
@@ -3609,18 +3857,27 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
3609
3857
  } catch {}
3610
3858
  if (!unsubChat) chatSlice = legacySliceOf(null, face.getSnapshot());
3611
3859
  };
3860
+ const mainBinding = () => {
3861
+ try {
3862
+ return uiSession && uiSession.current ? uiSession.current.getSnapshot() : null;
3863
+ } catch {
3864
+ return null;
3865
+ }
3866
+ };
3612
3867
  const onList = () => {
3613
- const ls = sessions.list.getSnapshot();
3614
- const next = ls ? ls.current : void 0;
3868
+ const next = currentSessionIdOf(sessions.list.getSnapshot(), mainBinding());
3615
3869
  if (next !== currentId) {
3616
3870
  currentId = next;
3617
3871
  watch(next);
3618
- }
3872
+ } else if (next != null && !unsubSession) watch(next);
3619
3873
  };
3620
3874
  onList();
3621
3875
  const unsubList = sessions.list.subscribe(onList);
3876
+ const mainSource = uiSession && uiSession.current;
3877
+ const unsubMain = mainSource && typeof mainSource.subscribe === "function" ? mainSource.subscribe(onList) : null;
3622
3878
  return () => {
3623
3879
  if (unsubList) unsubList();
3880
+ if (unsubMain) unsubMain();
3624
3881
  if (unsubSession) unsubSession();
3625
3882
  if (unsubChat) unsubChat();
3626
3883
  };
@@ -3655,6 +3912,8 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
3655
3912
  uiSession,
3656
3913
  uiConversation,
3657
3914
  settingsScope,
3915
+ configForms,
3916
+ sidebarRight,
3658
3917
  t
3659
3918
  })));
3660
3919
  ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
@@ -3669,13 +3928,30 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
3669
3928
  id: "dsh-focus-overlay",
3670
3929
  order: 1e3
3671
3930
  }, () => (0, react.createElement)(FocusSettingsCard, { t })));
3931
+ ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
3932
+ name: "plugins.bundle.config",
3933
+ key: PACKAGE_NAME
3934
+ }, (props) => (0, react.createElement)(FocusBundleConfig, {
3935
+ ...props,
3936
+ t
3937
+ })));
3938
+ const pluginNavigation = ctx.get("pluginNavigation");
3939
+ const openSettings = pluginNavigation && typeof pluginNavigation.openBundle === "function" ? () => {
3940
+ try {
3941
+ pluginNavigation.openBundle(PACKAGE_NAME);
3942
+ return true;
3943
+ } catch {
3944
+ return false;
3945
+ }
3946
+ } : void 0;
3672
3947
  ctx.slots.inject("settings.onboarding", () => ctx.slots.register({
3673
3948
  name: "settings.onboarding",
3674
3949
  id: "dsh-focus-overlay",
3675
3950
  order: 10
3676
3951
  }, (props) => (0, react.createElement)(FocusOnboarding, {
3677
3952
  ...props,
3678
- t
3953
+ t,
3954
+ openSettings
3679
3955
  })));
3680
3956
  }
3681
3957
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-focus-overlay",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "description": "Focus Mode for the dsh web GUI: a full-screen reading overlay that hides chrome and folds the AI tool-call flow into summaries, reusing the official Markdown / image primitives.",
5
5
  "type": "module",
6
6
  "main": "lib/index.mjs",
@@ -30,7 +30,9 @@
30
30
  "dshReleases": {
31
31
  "0.1.2-alpha.5": "compatible",
32
32
  "0.1.2-rc.1": "compatible",
33
- "0.1.3-alpha.1": "compatible"
33
+ "0.1.3-alpha.1": "compatible",
34
+ "0.1.5-rc.1": "compatible",
35
+ "0.2.0-rc.2": "compatible"
34
36
  }
35
37
  }
36
38
  },