@klarkxy/dsh-session-manager 0.2.1 → 0.3.1

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/lib/client.js CHANGED
@@ -7,13 +7,175 @@ window.__ModuleLoader__.load({
7
7
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
9
  let react = require("react");
10
- let react_jsx_runtime = require("react/jsx-runtime");
11
10
  let react_dom = require("react-dom");
11
+ let react_jsx_runtime = require("react/jsx-runtime");
12
+ //#region src/ui-copy.ts
13
+ function copyLocale(active) {
14
+ return active?.toLowerCase().startsWith("zh") ? "zh" : "en";
15
+ }
16
+ const reasons = {
17
+ zh: {
18
+ modified: "后续操作可能涉及修改,暂时不能重试。",
19
+ unknown: "无法确认这个操作的结果。",
20
+ pending: "这条消息还在等待处理。",
21
+ stale: "这个聊天已经更新,请再试一次。",
22
+ running: "这个聊天仍在运行。请等待当前工作结束后再试。",
23
+ cleanup: "清理没有完成。",
24
+ unsupported: "当前版本不能完成这个操作。"
25
+ },
26
+ en: {
27
+ modified: "Later operations may have made changes. Retry is unavailable.",
28
+ unknown: "This action's result could not be confirmed.",
29
+ pending: "This message is still pending.",
30
+ stale: "This chat has changed. Try again.",
31
+ running: "This chat is still running. Wait for the current work to finish, then try again.",
32
+ cleanup: "Cleanup did not finish.",
33
+ unsupported: "This runtime cannot complete that action."
34
+ }
35
+ };
36
+ const text = {
37
+ zh: {
38
+ tempWorkspace: "临时工作区",
39
+ createFailed: "无法创建临时工作区",
40
+ retryList: "重试",
41
+ deleteChat: "删除聊天",
42
+ deleting: "正在删除",
43
+ deleteFree: "将删除这个聊天及其拥有的文件。",
44
+ deleteOrdinary: "将删除这个聊天。项目文件会保留。",
45
+ cancel: "取消",
46
+ close: "关闭",
47
+ retry: "重试",
48
+ edit: "编辑并重发",
49
+ send: "发送",
50
+ editTitle: "编辑并重发",
51
+ draft: "消息文本",
52
+ continueDefault: "继续",
53
+ welcomeBack: "欢迎回来",
54
+ recapSummary: "在聊什么",
55
+ recapProgress: "目前进展",
56
+ recapNextStep: "建议下一步",
57
+ cacheNotice: "离开时间较久,模型的提示缓存可能已失效,下一次发送会重新处理完整上下文。可以压缩上下文,或在新会话中继续;也可以忽略提示直接发送。",
58
+ compactNow: "压缩上下文",
59
+ compacting: "正在压缩",
60
+ compactCommandSent: "已调用宿主压缩命令,结果请查看对话中的压缩记录。",
61
+ continueNew: "在新会话中继续",
62
+ creatingContinuation: "正在创建新会话",
63
+ continueNewHint: "在来源会话的工作区新建会话,将摘要和原会话 ID 填入草稿,不自动发送。",
64
+ compactNothing: "没有值得压缩的内容。",
65
+ settingsMode: "继续灰影",
66
+ modeFixed: "固定文本",
67
+ modeAuto: "模型生成",
68
+ settingsModeHint: "输入框为空时显示的灰色继续文本:Tab 填入,点击发送。",
69
+ continueTextLabel: "固定文本内容",
70
+ continueTextPlaceholder: "留空使用默认「继续」",
71
+ ghostModelLabel: "灰影模型",
72
+ ghostModelHint: "只在上一轮结束后生成一次,并缓存在这个会话中;留空跟随会话或宿主默认。",
73
+ welcomeBackGroup: "欢迎回来",
74
+ recapEnabledLabel: "欢迎回来摘要",
75
+ recapEnabledHint: "回到空闲的会话时,在对话顶部生成回顾卡片。",
76
+ recapMinutesLabel: "摘要阈值(分钟)",
77
+ compactEnabledLabel: "压缩建议",
78
+ compactEnabledHint: "空闲较久时,回到会话会建议先压缩上下文。",
79
+ compactMinutesLabel: "建议阈值(分钟)",
80
+ modelLabel: "摘要模型",
81
+ modelFollowOption: "当前会话 / 宿主默认模型",
82
+ modelGroupHint: "只用于欢迎回来摘要;留空跟随会话或宿主默认。",
83
+ saveSettings: "保存",
84
+ settingsSaving: "正在保存",
85
+ settingsSaved: "已保存",
86
+ settingsLoading: "正在加载",
87
+ settingsUnavailable: "当前宿主不支持在线修改配置",
88
+ minutesInvalid: "请填写大于等于 1 的整数分钟。",
89
+ externalChange: "配置已在别处修改。草稿已保留,请刷新页面后再保存。"
90
+ },
91
+ en: {
92
+ tempWorkspace: "Temporary Workspace",
93
+ createFailed: "Could not create the temporary workspace",
94
+ retryList: "Retry",
95
+ deleteChat: "Delete chat",
96
+ deleting: "Deleting",
97
+ deleteFree: "This deletes this chat and its owned files.",
98
+ deleteOrdinary: "This deletes this chat. Project files remain.",
99
+ cancel: "Cancel",
100
+ close: "Close",
101
+ retry: "Retry",
102
+ edit: "Edit and resend",
103
+ send: "Send",
104
+ editTitle: "Edit and resend",
105
+ draft: "Message text",
106
+ continueDefault: "Continue",
107
+ welcomeBack: "Welcome back",
108
+ recapSummary: "What you were discussing",
109
+ recapProgress: "Where things stand",
110
+ recapNextStep: "Suggested next step",
111
+ cacheNotice: "The model prompt cache may have expired, so the next message reprocesses the full context. Compact the context, continue in a new session, or ignore this notice and send normally.",
112
+ compactNow: "Compact context",
113
+ compacting: "Compacting",
114
+ compactCommandSent: "Host compaction command invoked. See the compaction entry in the conversation for its result.",
115
+ continueNew: "Continue in a new session",
116
+ creatingContinuation: "Creating new session",
117
+ continueNewHint: "Create a session in the source session's workspace and fill its draft with the recap and source session ID, without sending.",
118
+ compactNothing: "There is nothing useful to compact.",
119
+ settingsMode: "Continue ghost",
120
+ modeFixed: "Fixed text",
121
+ modeAuto: "Model-generated",
122
+ settingsModeHint: "The grey continuation shown on an empty composer: Tab fills it, clicking sends it.",
123
+ continueTextLabel: "Fixed text",
124
+ continueTextPlaceholder: "Empty uses the default \"Continue\"",
125
+ ghostModelLabel: "Ghost model",
126
+ ghostModelHint: "Generated once after the previous turn ends and cached for this session; empty follows the conversation or host default.",
127
+ welcomeBackGroup: "Welcome back",
128
+ recapEnabledLabel: "Welcome-back recap",
129
+ recapEnabledHint: "Shows a recap card at the top of the conversation when you return to an idle session.",
130
+ recapMinutesLabel: "Recap threshold (minutes)",
131
+ compactEnabledLabel: "Compaction suggestion",
132
+ compactEnabledHint: "Suggests compacting the context when you return after a longer idle stretch.",
133
+ compactMinutesLabel: "Suggestion threshold (minutes)",
134
+ modelLabel: "Recap model",
135
+ modelFollowOption: "Current conversation / host default",
136
+ modelGroupHint: "Used only for the welcome-back recap; empty follows the conversation or host default.",
137
+ saveSettings: "Save",
138
+ settingsSaving: "Saving",
139
+ settingsSaved: "Saved",
140
+ settingsLoading: "Loading",
141
+ settingsUnavailable: "This runtime cannot edit the plugin configuration",
142
+ minutesInvalid: "Enter a whole number of minutes of at least 1.",
143
+ externalChange: "Settings changed elsewhere. The draft was kept; reload this page before saving."
144
+ }
145
+ };
146
+ function uiText(locale, key) {
147
+ return text[locale][key];
148
+ }
149
+ function failureReason(failure) {
150
+ if (failure.code === "session/lifecycle-unavailable" || failure.code === "session/fork-unavailable" || failure.code === "session-manager/unavailable") return "unsupported";
151
+ if (failure.code === "session-manager/busy") return "running";
152
+ if (failure.code === "session/retry-revision" || failure.code === "session/branch-moved" || failure.code === "session/retry-input" || failure.code === "session/retry-conflict") return "stale";
153
+ if (failure.code === "session/delete-pending") return "cleanup";
154
+ if (failure.code === "session/agent-busy" && failure.reason === "free-session-work-active") return "running";
155
+ if (failure.reason === "modified" || failure.reason === "pending" || failure.reason === "running" || failure.reason === "stale") return failure.reason;
156
+ if (failure.reason === "stop-failed" || failure.reason === "not-quiet") return "running";
157
+ if (failure.reason === "not-human-prompt" || failure.reason === "missing-evidence") return "unknown";
158
+ return "unknown";
159
+ }
160
+ function failureText(locale, failure) {
161
+ return reasons[locale][failureReason(failure)];
162
+ }
163
+ function deleteBody(locale, classification) {
164
+ return classification === "free" ? text[locale].deleteFree : text[locale].deleteOrdinary;
165
+ }
166
+ /** Idle duration sentence on the welcome-back card. */
167
+ function idleText(locale, minutes) {
168
+ return locale === "zh" ? `距离上次响应已约 ${minutes} 分钟。` : `About ${minutes} minutes have passed since the last response.`;
169
+ }
170
+ /** Compaction outcome line; a null token count means the host did not report one. */
171
+ function compactedText(locale, tokens) {
172
+ if (locale === "zh") return tokens === null ? "上下文已压缩。" : `上下文已压缩,约 ${tokens} tokens 的历史被折叠。`;
173
+ return tokens === null ? "Context compacted." : `Context compacted; about ${tokens} tokens of history were folded.`;
174
+ }
175
+ //#endregion
12
176
  //#region src/contracts.ts
13
177
  /** Shared names and bounds for cross-session discovery. */
14
178
  const PLUGIN_NAME = "@klarkxy/dsh-session-manager";
15
- const COPY_THREAD_MENU_ID = "@klarkxy/dsh-session-manager.copy-thread-id";
16
- const COPY_THREAD_SHORTCUT_ID = "session-manager.copy-thread-id";
17
179
  const TEMP_WORKSPACE_CHANNEL = "/dsh-session-manager";
18
180
  const TEMP_WORKSPACE_FLOW_SLOT = "session-manager.workspace.directoryFlow";
19
181
  const USER_ACTIONS_SLOT = "conversation.chat.user-actions";
@@ -31,155 +193,46 @@ const WELCOME_BACK_CARD_SLOT = "conversation.chat.turnTail";
31
193
  const CONTINUE_GHOST_ID = "@klarkxy/dsh-session-manager.continue-ghost";
32
194
  const CONTINUE_GHOST_SLOT = "conversation.input.overlay";
33
195
  //#endregion
34
- //#region src/copy-action.ts
35
- const COPY_THREAD_LABEL = {
36
- zh: "复制线程 ID",
37
- en: "Copy thread ID"
38
- };
39
- const COPY_THREAD_FAILED = {
40
- zh: "无法复制线程 ID",
41
- en: "Could not copy the thread ID"
42
- };
43
- const COPY_THREAD_COPIED = {
44
- zh: "已复制线程 ID",
45
- en: "Thread ID copied"
46
- };
47
- const COPY_THREAD_NO_SESSION = {
48
- zh: "请先打开一个会话",
49
- en: "Open a session first"
50
- };
51
- function copyLocale(active) {
52
- return active?.toLowerCase().startsWith("zh") ? "zh" : "en";
53
- }
54
- /** Copy one session id. Rejection stays with the caller so the menu can show it. */
55
- async function copyThreadId(sessionId, writeText) {
56
- await writeText(sessionId);
57
- }
58
- //#endregion
59
- //#region src/copy-shortcut.tsx
60
- async function writeClipboard(value) {
61
- const clipboard = globalThis.navigator?.clipboard;
62
- if (clipboard === void 0) throw new Error("clipboard unavailable");
63
- await clipboard.writeText(value);
64
- }
65
- /** Uses the same mainView reference as ui-workspace/src/client/shortcuts.ts.
66
- * The host registry owns key dispatch, remapping, conflicts, IME and repeat guards.
67
- */
68
- function CopyThreadShortcutNotice({ shortcuts, list, locale, currentLocale, writeText = writeClipboard }) {
69
- const [notice, setNotice] = (0, react.useState)(null);
70
- const sequence = (0, react.useRef)(0);
71
- (0, react.useEffect)(() => {
72
- if (shortcuts === void 0) return;
73
- let active = true;
74
- const notify = (copied) => {
75
- if (active) setNotice({
76
- copied,
77
- seq: ++sequence.current
78
- });
79
- };
80
- const binding = {
81
- code: "KeyC",
82
- modifiers: ["primary", "alt"]
83
- };
84
- const stop = shortcuts.register({
85
- id: COPY_THREAD_SHORTCUT_ID,
86
- label: () => COPY_THREAD_LABEL[copyLocale(currentLocale())],
87
- aliases: [
88
- "copy thread id",
89
- "copy session id",
90
- "复制线程 ID"
91
- ],
92
- defaults: {
93
- "desktop:windows": binding,
94
- "desktop:macos": binding,
95
- "web:windows": binding,
96
- "web:macos": binding
97
- },
98
- regions: ["page", "editable"],
99
- modals: [],
100
- resolve(context) {
101
- const target = Object.values(list?.getSnapshot().byId ?? {}).find((row) => (row?.retainedBy?.mainView ?? 0) > 0);
102
- if (target === void 0 || context.modal !== null || context.region === "terminal") return {
103
- status: "blocked",
104
- reason: COPY_THREAD_NO_SESSION[copyLocale(currentLocale())]
105
- };
106
- return {
107
- status: "handled",
108
- run() {
109
- copyThreadId(target.id, writeText).then(() => notify(true), () => notify(false));
110
- }
111
- };
112
- }
113
- });
114
- return () => {
115
- active = false;
116
- stop();
117
- };
118
- }, [
119
- shortcuts,
120
- list,
121
- currentLocale,
122
- writeText
123
- ]);
124
- return notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
125
- text: (notice.copied ? COPY_THREAD_COPIED : COPY_THREAD_FAILED)[locale],
126
- tone: notice.copied ? "success" : void 0,
127
- onDone: () => setNotice(null)
128
- }, notice.seq);
129
- }
130
- //#endregion
131
196
  //#region src/context-menu.ts
132
197
  /**
133
- * Right-click forwarding for the sidebar browser: a `contextmenu` gesture on a
134
- * session or workspace row clicks that row's native "..." trigger, so the
135
- * host's own menu — including every `sidebar.workspaces.session.menu.item`
136
- * slot contribution, this plugin's rows included — opens exactly as a
137
- * left-click on the trigger would. The host owns the menu; this module only
138
- * relays the gesture. No host code registers a contextmenu listener, so the
139
- * capture-phase relay does not fight an existing handler.
198
+ * Relay sidebar right-clicks to the host's own session actions menu.
140
199
  *
141
- * DOM contract (dsh-client-ui-workspace 0.2.0-rc.2, lib/client.js):
142
- * - Rows are `div[role="treeitem"][data-row-key="session:<id>"]` or
143
- * `data-row-key="workspace:<id>"` inside `div[role="tree"]`. Overflow and
144
- * empty rows carry other prefixes, search rows carry no `data-row-key`, and
145
- * AnimatedRows exit clones have the attribute removed — all are ignored by
146
- * the prefix match. (client.js:1287, 1590-1596, 1729, 1842-1846)
147
- * - The trigger is the first `button[type="button"]` inside the row's trailing
148
- * `span` whose CSS-module class ends in `_rowActions`. It stays mounted —
149
- * CSS `display:none` until hover or menu-open — so a programmatic click
150
- * toggles the menu. Blank new-session rows and the ungrouped bucket render
151
- * no trigger; right-click does nothing there. (client.js:1061, 1314-1340,
152
- * 1640-1660)
153
- * - Class names carry a per-build hash prefix and the trigger's aria-label is
154
- * localized; only `data-row-key` and the structural suffix are stable.
155
- * - The opened menu portals to `document.body`, outside the tree, so a
156
- * right-click on a menu row never re-enters this relay.
200
+ * dsh-client-ui-workspace SessionNodeItem renders a keyed treeitem, with a
201
+ * trailing rowActions span. Its first button is the Menu anchor; subsequent
202
+ * buttons are contributed row actions. The span stops click propagation, so
203
+ * clicking the anchor opens the menu without opening the session. Menu portals
204
+ * outside the tree and the row carries a *_menuOpen class while it is open.
205
+ * Blank rows, search results, and AnimatedRows exit clones have no keyed menu
206
+ * anchor and are left alone. CSS-module hash prefixes vary between host builds.
157
207
  */
158
- const ROW_SELECTOR = "[data-row-key^=\"session:\"], [data-row-key^=\"workspace:\"]";
159
- const TRIGGER_SELECTOR = "span[class*=\"rowActions\"] button[type=\"button\"]";
160
- /** Resolve the native "..." trigger of the row containing `target`, if any. */
161
- function findRowMenuTrigger(target) {
208
+ const ROW_SELECTOR = "[role=\"treeitem\"][data-row-key^=\"session:\"]";
209
+ const ACTIONS_SELECTOR = "span[class*=\"_rowActions\"]";
210
+ /** Resolve a live session row in the sidebar tree. */
211
+ function findSessionRow(target) {
162
212
  if (typeof target.closest !== "function") return null;
163
213
  const row = target.closest(ROW_SELECTOR);
164
- if (row === null || row.closest("[role=\"tree\"]") === null) return null;
165
- const trigger = row.querySelector(TRIGGER_SELECTOR);
166
- if (trigger === null || typeof trigger.click !== "function") return null;
167
- return trigger;
214
+ if (row === null || row.closest("[role=\"tree\"]") === null || row.closest("[inert]") !== null) return null;
215
+ return row;
168
216
  }
169
- /**
170
- * Forward one `contextmenu` event to the row's native menu trigger.
171
- * Returns true when the gesture was relayed (and the default suppressed).
172
- */
217
+ /** Forward a context-menu gesture only when the host provides a menu anchor. */
173
218
  function forwardContextMenu(event) {
219
+ if (event.defaultPrevented) return false;
174
220
  const target = event.target;
175
221
  if (target === null || typeof target.closest !== "function") return false;
176
- const trigger = findRowMenuTrigger(target);
177
- if (trigger === null) return false;
222
+ const element = target;
223
+ const row = findSessionRow(element);
224
+ if (row === null) return false;
225
+ const trigger = row.querySelector(ACTIONS_SELECTOR)?.querySelector("button");
226
+ if (trigger == null || trigger.disabled) return false;
227
+ if (element.closest(ACTIONS_SELECTOR) !== null && !trigger.contains(element)) return false;
178
228
  event.preventDefault();
179
- trigger.click();
229
+ if (!Array.from(row.classList).some((name) => name.endsWith("_menuOpen"))) {
230
+ trigger.focus({ preventScroll: true });
231
+ trigger.click();
232
+ }
180
233
  return true;
181
234
  }
182
- /** Capture-phase relay on the document; the returned disposer uninstalls it. */
235
+ /** Capture-phase relay; disposing the client removes the listener. */
183
236
  function installContextMenuForwarding(doc) {
184
237
  const listener = (event) => {
185
238
  forwardContextMenu(event);
@@ -365,6 +418,17 @@ ${scope} {
365
418
  /* --- Layout scaffolding. --- */
366
419
  & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
367
420
  & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
421
+ /* Plugin detail pages share one reading column. These are layout recipes;
422
+ * controls keep their own primitive styles. Form spacing follows the host's
423
+ * settings-form/fields.module.css and SettingsForm.module.css. */
424
+ & .dsh-ui-page { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 760px; min-width: 0; }
425
+ & .dsh-ui-form { display: flex; flex-direction: column; gap: 0; min-width: 0; }
426
+ &.dsh-ui-form > .dsh-ui-field, & .dsh-ui-form > .dsh-ui-field { padding: 12px 0; }
427
+ & .dsh-ui-form-footer { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-top: 16px; min-width: 0; }
428
+ & .dsh-ui-fields-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; min-width: 0; margin: 0; padding: 0; border: 0; }
429
+ @media (max-width: 560px) {
430
+ & .dsh-ui-fields-grid { grid-template-columns: minmax(0, 1fr); }
431
+ }
368
432
  & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
369
433
  & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
370
434
  & .dsh-ui-spacer { flex: 1; min-width: 0; }
@@ -392,6 +456,9 @@ ${scope} {
392
456
  * official primitive, so this only owns the vertical rhythm. --- */
393
457
  & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
394
458
  & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
459
+ /* Grid fields share a row, so the stacked-field divider must not win later in
460
+ * the cascade. Reset it here, after the field recipe. */
461
+ & .dsh-ui-fields-grid > .dsh-ui-field { padding: 0; border: 0; }
395
462
  & .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }
396
463
  & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
397
464
  & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
@@ -582,7 +649,7 @@ ${scope} {
582
649
  & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
583
650
  }
584
651
  }
585
- `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
652
+ `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?=\s*\{)/gm, (_match, className) => `&${className}, & ${className}`);
586
653
  }
587
654
  //#endregion
588
655
  //#region ../dsh-model-route/src/route.ts
@@ -814,7 +881,14 @@ function useModelCatalog(load, options = {}) {
814
881
  refresh
815
882
  };
816
883
  }
817
- /** Also accepts a mapped state from a caller that owns broader async orchestration. */
884
+ /**
885
+ * Also accepts a mapped state from a caller that owns broader async orchestration.
886
+ *
887
+ * The dsh-ui-* classes carry the styling and resolve through the consuming
888
+ * plugin's own officialUiCss scope (every consumer mounts its root class, so
889
+ * e.g. `.dsh-model-hub .dsh-ui-loading` paints this); dsh-model-catalog-status
890
+ * stays as a structural hook for tests and layout, never a colour source.
891
+ */
818
892
  function ModelCatalogStatus({ state, locale }) {
819
893
  const zh = locale?.toLowerCase().startsWith("zh") === true;
820
894
  const copy = zh ? {
@@ -830,15 +904,17 @@ function ModelCatalogStatus({ state, locale }) {
830
904
  };
831
905
  if (!state.loading && !state.error && state.failures.length === 0) return null;
832
906
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
833
- className: "dsh-model-catalog-status",
907
+ className: "dsh-model-catalog-status dsh-ui-stack",
834
908
  lang: zh ? "zh" : "en",
835
909
  children: [
836
910
  state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
837
911
  role: "status",
912
+ className: "dsh-ui-loading",
838
913
  children: copy.loading
839
914
  }) : null,
840
915
  state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
841
916
  role: "alert",
917
+ className: "dsh-ui-error dsh-ui-wrap",
842
918
  children: [
843
919
  copy.error,
844
920
  ": ",
@@ -847,20 +923,27 @@ function ModelCatalogStatus({ state, locale }) {
847
923
  }) : null,
848
924
  state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
849
925
  role: "status",
850
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
851
- failure.name || failure.id,
852
- ": ",
853
- failure.message
854
- ] }, `${failure.id}:${index}`)) })]
926
+ className: "dsh-ui-meta",
927
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
928
+ className: "dsh-ui-list",
929
+ children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
930
+ failure.name || failure.id,
931
+ ": ",
932
+ failure.message
933
+ ] }, `${failure.id}:${index}`))
934
+ })]
855
935
  }) : null,
856
- state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
857
- type: "button",
858
- size: "sm",
859
- disabled: state.loading,
860
- onClick: () => {
861
- state.refresh();
862
- },
863
- children: copy.retry
936
+ state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
937
+ className: "dsh-ui-actions",
938
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
939
+ type: "button",
940
+ size: "sm",
941
+ disabled: state.loading,
942
+ onClick: () => {
943
+ state.refresh();
944
+ },
945
+ children: copy.retry
946
+ })
864
947
  }) : null
865
948
  ]
866
949
  });
@@ -920,7 +1003,8 @@ function groupModelMenuChoices(choices, query) {
920
1003
  * Sticky group headings, subsequence ranking, highlight walking and focus
921
1004
  * hand-back all match the host. SYNC POINT: when the host's ModelSelect or
922
1005
  * ModelSelect.module.css changes, this copy must be re-transcribed; styles
923
- * below may only reference --dsw-* theme tokens, never literal colors.
1006
+ * below may only reference allowlisted theme tokens (the --dsw-* set plus the
1007
+ * host's --ds-* motion tokens), never literal colors.
924
1008
  *
925
1009
  * It statically imports the primitives, so only browser clients should load
926
1010
  * this entry; host code and node tests use the core entry (`./index.ts`).
@@ -1428,7 +1512,8 @@ function ModelMenu(props) {
1428
1512
  * sticky group headings (MenuGroup's own CSS), and the l1-stroke prominent
1429
1513
  * elevation at portal z-index 1100. The trigger mirrors the composer's 28px
1430
1514
  * model pill, chevron rotating open. MenuSurface paints fill and backdrop.
1431
- * Only --dsw-* theme tokens may appear here — a literal color is a bug.
1515
+ * Only allowlisted --dsw- and --ds- theme tokens may appear here — a literal
1516
+ * color is a bug.
1432
1517
  */
1433
1518
  const modelMenuCss = `
1434
1519
  .dsh-model-menu-root { min-width: 0; position: relative; display: inline-flex; }
@@ -1451,12 +1536,14 @@ const modelMenuCss = `
1451
1536
  display: flex;
1452
1537
  }
1453
1538
  .dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
1454
- .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
1539
+ .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 var(--dsw-focus-ring-width) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
1455
1540
  .dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
1456
1541
  .dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
1457
1542
  .dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
1458
- .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform 0.12s; }
1543
+ .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform var(--ds-transition-duration) var(--ds-ease-in-out); }
1459
1544
  .dsh-model-menu-chevronOpen { transform: rotate(180deg); }
1545
+ /* Portal overlay: the host publishes no z-index token, so the card keeps the
1546
+ * native menu layer's 1100 verbatim. */
1460
1547
  .dsh-model-menu-menu {
1461
1548
  z-index: 1100;
1462
1549
  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
@@ -1482,6 +1569,8 @@ const modelMenuCss = `
1482
1569
  .dsh-model-menu-searchWithQuery { padding-right: 34px; }
1483
1570
  .dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
1484
1571
  .dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
1572
+ /* The clear button is a circle by geometry (border-radius: 50%), a shape the
1573
+ * radius token scale does not describe. */
1485
1574
  .dsh-model-menu-searchClear {
1486
1575
  width: 24px;
1487
1576
  height: 24px;
@@ -1649,182 +1738,7 @@ ${modelMenuCss}
1649
1738
  .${FREE_CHAT_ROOT} .dsh-welcome-done { margin: 6px 0 0; }
1650
1739
  `;
1651
1740
  //#endregion
1652
- //#region src/ui-copy.ts
1653
- const reasons = {
1654
- zh: {
1655
- modified: "后续操作可能涉及修改,暂时不能重试。",
1656
- unknown: "无法确认这个操作的结果。",
1657
- pending: "这条消息还在等待处理。",
1658
- stale: "这个聊天已经更新,请再试一次。",
1659
- running: "这个聊天仍在运行。请等待当前工作结束后再试。",
1660
- cleanup: "清理没有完成。",
1661
- unsupported: "当前版本不能完成这个操作。"
1662
- },
1663
- en: {
1664
- modified: "Later operations may have made changes. Retry is unavailable.",
1665
- unknown: "This action's result could not be confirmed.",
1666
- pending: "This message is still pending.",
1667
- stale: "This chat has changed. Try again.",
1668
- running: "This chat is still running. Wait for the current work to finish, then try again.",
1669
- cleanup: "Cleanup did not finish.",
1670
- unsupported: "This runtime cannot complete that action."
1671
- }
1672
- };
1673
- const text = {
1674
- zh: {
1675
- tempWorkspace: "临时工作区",
1676
- createFailed: "无法创建临时工作区",
1677
- retryList: "重试",
1678
- deleteChat: "删除聊天",
1679
- deleting: "正在删除",
1680
- deleteFree: "将删除这个聊天及其拥有的文件。",
1681
- deleteOrdinary: "将删除这个聊天。项目文件会保留。",
1682
- cancel: "取消",
1683
- close: "关闭",
1684
- retry: "重试",
1685
- edit: "编辑并重发",
1686
- send: "发送",
1687
- editTitle: "编辑并重发",
1688
- draft: "消息文本",
1689
- continueDefault: "继续",
1690
- welcomeBack: "欢迎回来",
1691
- recapSummary: "在聊什么",
1692
- recapProgress: "目前进展",
1693
- recapNextStep: "建议下一步",
1694
- cacheNotice: "离开时间较久,模型的提示缓存可能已失效,下一次发送会重新处理完整上下文。可以压缩上下文,或在新会话中继续;也可以忽略提示直接发送。",
1695
- compactNow: "压缩上下文",
1696
- compacting: "正在压缩",
1697
- compactCommandSent: "已调用宿主压缩命令,结果请查看对话中的压缩记录。",
1698
- continueNew: "在新会话中继续",
1699
- creatingContinuation: "正在创建新会话",
1700
- continueNewHint: "在来源会话的工作区新建会话,将摘要和原会话 ID 填入草稿,不自动发送。",
1701
- compactNothing: "没有值得压缩的内容。",
1702
- settingsMode: "继续灰影",
1703
- modeFixed: "固定文本",
1704
- modeAuto: "模型生成",
1705
- settingsModeHint: "输入框为空时显示的灰色继续文本:Tab 填入,点击发送。",
1706
- continueTextLabel: "固定文本内容",
1707
- continueTextPlaceholder: "留空使用默认「继续」",
1708
- ghostModelLabel: "灰影模型",
1709
- ghostModelHint: "只在上一轮结束后生成一次,并缓存在这个会话中;留空跟随会话或宿主默认。",
1710
- welcomeBackGroup: "欢迎回来",
1711
- recapEnabledLabel: "欢迎回来摘要",
1712
- recapEnabledHint: "回到空闲的会话时,在对话顶部生成回顾卡片。",
1713
- recapMinutesLabel: "摘要阈值(分钟)",
1714
- compactEnabledLabel: "压缩建议",
1715
- compactEnabledHint: "空闲较久时,回到会话会建议先压缩上下文。",
1716
- compactMinutesLabel: "建议阈值(分钟)",
1717
- modelLabel: "摘要模型",
1718
- modelFollowOption: "当前会话 / 宿主默认模型",
1719
- modelGroupHint: "只用于欢迎回来摘要;留空跟随会话或宿主默认。",
1720
- saveSettings: "保存",
1721
- settingsSaving: "正在保存",
1722
- settingsSaved: "已保存",
1723
- settingsLoading: "正在加载",
1724
- settingsUnavailable: "当前宿主不支持在线修改配置",
1725
- minutesInvalid: "请填写大于等于 1 的整数分钟。",
1726
- externalChange: "配置已在别处修改。草稿已保留,请刷新页面后再保存。"
1727
- },
1728
- en: {
1729
- tempWorkspace: "Temporary Workspace",
1730
- createFailed: "Could not create the temporary workspace",
1731
- retryList: "Retry",
1732
- deleteChat: "Delete chat",
1733
- deleting: "Deleting",
1734
- deleteFree: "This deletes this chat and its owned files.",
1735
- deleteOrdinary: "This deletes this chat. Project files remain.",
1736
- cancel: "Cancel",
1737
- close: "Close",
1738
- retry: "Retry",
1739
- edit: "Edit and resend",
1740
- send: "Send",
1741
- editTitle: "Edit and resend",
1742
- draft: "Message text",
1743
- continueDefault: "Continue",
1744
- welcomeBack: "Welcome back",
1745
- recapSummary: "What you were discussing",
1746
- recapProgress: "Where things stand",
1747
- recapNextStep: "Suggested next step",
1748
- cacheNotice: "The model prompt cache may have expired, so the next message reprocesses the full context. Compact the context, continue in a new session, or ignore this notice and send normally.",
1749
- compactNow: "Compact context",
1750
- compacting: "Compacting",
1751
- compactCommandSent: "Host compaction command invoked. See the compaction entry in the conversation for its result.",
1752
- continueNew: "Continue in a new session",
1753
- creatingContinuation: "Creating new session",
1754
- continueNewHint: "Create a session in the source session's workspace and fill its draft with the recap and source session ID, without sending.",
1755
- compactNothing: "There is nothing useful to compact.",
1756
- settingsMode: "Continue ghost",
1757
- modeFixed: "Fixed text",
1758
- modeAuto: "Model-generated",
1759
- settingsModeHint: "The grey continuation shown on an empty composer: Tab fills it, clicking sends it.",
1760
- continueTextLabel: "Fixed text",
1761
- continueTextPlaceholder: "Empty uses the default \"Continue\"",
1762
- ghostModelLabel: "Ghost model",
1763
- ghostModelHint: "Generated once after the previous turn ends and cached for this session; empty follows the conversation or host default.",
1764
- welcomeBackGroup: "Welcome back",
1765
- recapEnabledLabel: "Welcome-back recap",
1766
- recapEnabledHint: "Shows a recap card at the top of the conversation when you return to an idle session.",
1767
- recapMinutesLabel: "Recap threshold (minutes)",
1768
- compactEnabledLabel: "Compaction suggestion",
1769
- compactEnabledHint: "Suggests compacting the context when you return after a longer idle stretch.",
1770
- compactMinutesLabel: "Suggestion threshold (minutes)",
1771
- modelLabel: "Recap model",
1772
- modelFollowOption: "Current conversation / host default",
1773
- modelGroupHint: "Used only for the welcome-back recap; empty follows the conversation or host default.",
1774
- saveSettings: "Save",
1775
- settingsSaving: "Saving",
1776
- settingsSaved: "Saved",
1777
- settingsLoading: "Loading",
1778
- settingsUnavailable: "This runtime cannot edit the plugin configuration",
1779
- minutesInvalid: "Enter a whole number of minutes of at least 1.",
1780
- externalChange: "Settings changed elsewhere. The draft was kept; reload this page before saving."
1781
- }
1782
- };
1783
- function uiText(locale, key) {
1784
- return text[locale][key];
1785
- }
1786
- function failureReason(failure) {
1787
- if (failure.code === "session/lifecycle-unavailable" || failure.code === "session/fork-unavailable" || failure.code === "session-manager/unavailable") return "unsupported";
1788
- if (failure.code === "session-manager/busy") return "running";
1789
- if (failure.code === "session/retry-revision" || failure.code === "session/branch-moved" || failure.code === "session/retry-input" || failure.code === "session/retry-conflict") return "stale";
1790
- if (failure.code === "session/delete-pending") return "cleanup";
1791
- if (failure.code === "session/agent-busy" && failure.reason === "free-session-work-active") return "running";
1792
- if (failure.reason === "modified" || failure.reason === "pending" || failure.reason === "running" || failure.reason === "stale") return failure.reason;
1793
- if (failure.reason === "stop-failed" || failure.reason === "not-quiet") return "running";
1794
- if (failure.reason === "not-human-prompt" || failure.reason === "missing-evidence") return "unknown";
1795
- return "unknown";
1796
- }
1797
- function failureText(locale, failure) {
1798
- return reasons[locale][failureReason(failure)];
1799
- }
1800
- function deleteBody(locale, classification) {
1801
- return classification === "free" ? text[locale].deleteFree : text[locale].deleteOrdinary;
1802
- }
1803
- /** Idle duration sentence on the welcome-back card. */
1804
- function idleText(locale, minutes) {
1805
- return locale === "zh" ? `距离上次响应已约 ${minutes} 分钟。` : `About ${minutes} minutes have passed since the last response.`;
1806
- }
1807
- /** Compaction outcome line; a null token count means the host did not report one. */
1808
- function compactedText(locale, tokens) {
1809
- if (locale === "zh") return tokens === null ? "上下文已压缩。" : `上下文已压缩,约 ${tokens} tokens 的历史被折叠。`;
1810
- return tokens === null ? "Context compacted." : `Context compacted; about ${tokens} tokens of history were folded.`;
1811
- }
1812
- //#endregion
1813
1741
  //#region src/free-panel.tsx
1814
- function FreeChatIcon({ size }) {
1815
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1816
- width: size,
1817
- height: size,
1818
- viewBox: "0 0 16 16",
1819
- "aria-hidden": "true",
1820
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1821
- d: "M2.5 3.5h11v7.5h-4.2L6 13.5V11H2.5z",
1822
- fill: "none",
1823
- stroke: "currentColor",
1824
- strokeWidth: "1.2"
1825
- })
1826
- });
1827
- }
1828
1742
  /** Create failures used to surface inside the standalone panel. The panel is gone,
1829
1743
  * so they get a small overlay instead; closing it dismisses the stored notice. */
1830
1744
  function CreateNoticeDialog(props) {
@@ -2022,7 +1936,7 @@ function TemporaryWorkspacePicker(props) {
2022
1936
  }] : [], {
2023
1937
  id: "::temporary-workspace",
2024
1938
  label: uiText(locale, "tempWorkspace"),
2025
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FreeChatIcon, { size: 16 }),
1939
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutlineRegular, { size: 16 }),
2026
1940
  disabled: busy
2027
1941
  }],
2028
1942
  onSelect: (id) => {
@@ -2035,6 +1949,7 @@ function TemporaryWorkspacePicker(props) {
2035
1949
  }),
2036
1950
  props.open && workspaces.phase === "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2037
1951
  role: "status",
1952
+ className: `dsh-free-chat dsh-ui-loading`,
2038
1953
  children: props.t("picker.loading")
2039
1954
  }),
2040
1955
  props.renderSlot(TEMP_WORKSPACE_FLOW_SLOT, {
@@ -2070,6 +1985,7 @@ function TemporaryWorkspacePicker(props) {
2070
1985
  }),
2071
1986
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2072
1987
  open: failure !== void 0,
1988
+ className: FREE_CHAT_ROOT,
2073
1989
  title: props.t("folderError.title"),
2074
1990
  closeLabel: props.t("close"),
2075
1991
  onClose: () => setFailure(void 0),
@@ -2094,6 +2010,7 @@ function TemporaryWorkspaceNotice({ model, locale }) {
2094
2010
  const state = (0, react.useSyncExternalStore)(model.subscribe, model.getSnapshot, model.getSnapshot);
2095
2011
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2096
2012
  open: state.failure !== void 0,
2013
+ className: FREE_CHAT_ROOT,
2097
2014
  title: uiText(locale, "createFailed"),
2098
2015
  closeLabel: uiText(locale, "close"),
2099
2016
  onClose: model.dismissFailure,
@@ -3207,25 +3124,18 @@ function createWelcomeBackModel(host, ids = () => crypto.randomUUID(), minInterv
3207
3124
  }
3208
3125
  //#endregion
3209
3126
  //#region ../dsh-plugin-kit/src/contracts.ts
3210
- /**
3211
- * Remote contribution for model-settings change notifications.
3212
- * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
3213
- * subscribers degrade silently when that package is not installed.
3214
- * The stream carries invalidations only — `ready` is never a settings snapshot.
3215
- */
3216
- const MODEL_SETTINGS_CHANGES_REMOTE = {
3217
- package: "@klarkxy/dsh-model-hub",
3218
- descriptors: [{
3219
- id: "modelSettingsChanges/watch",
3220
- service: "modelSettingsChangesController",
3221
- namespace: "modelSettingsChanges",
3222
- method: "watch",
3223
- mode: "stream",
3224
- invocation: { kind: "direct" },
3225
- parameters: [],
3226
- cancellation: { parameter: "signal" }
3227
- }]
3228
- };
3127
+ /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
3128
+ function modelSettingsChangesIdentity(packageName) {
3129
+ const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
3130
+ const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
3131
+ if (!parts.length) throw new Error("Model settings package name has no RPC segment");
3132
+ const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
3133
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
3134
+ return {
3135
+ service: segment,
3136
+ namespace: segment
3137
+ };
3138
+ }
3229
3139
  //#endregion
3230
3140
  //#region ../dsh-plugin-kit/src/model-settings-client.ts
3231
3141
  /** This stream carries invalidations only; ready is never a settings snapshot. */
@@ -3240,17 +3150,44 @@ function parseModelSettingsChangeFrame(value) {
3240
3150
  };
3241
3151
  throw new Error("Invalid model settings change frame");
3242
3152
  }
3243
- MODEL_SETTINGS_CHANGES_REMOTE.package, MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
3244
- ...descriptor,
3245
- result: {
3246
- mode: "strict",
3247
- typeSymbol: "ModelSettings.ChangeFrame",
3248
- create: () => ({ parse: parseModelSettingsChangeFrame })
3249
- }
3250
- }));
3153
+ /** One owner's contribution. Each package mounts only its own endpoint. */
3154
+ function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
3155
+ return {
3156
+ package: packageName,
3157
+ descriptors: [{
3158
+ id: `${identity.namespace}/watch`,
3159
+ service: identity.service,
3160
+ namespace: identity.namespace,
3161
+ method: "watch",
3162
+ mode: "stream",
3163
+ invocation: { kind: "direct" },
3164
+ parameters: [],
3165
+ cancellation: { parameter: "signal" },
3166
+ result: {
3167
+ mode: "strict",
3168
+ typeSymbol: "ModelSettings.ChangeFrame",
3169
+ create: () => ({ parse: parseModelSettingsChangeFrame })
3170
+ }
3171
+ }]
3172
+ };
3173
+ }
3174
+ /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
3175
+ * then withdraws it, so the contribution cannot outlive the plugin. */
3176
+ function mountModelSettingsChanges(ctx, packageName) {
3177
+ const contribution = modelSettingsChangesRemote(packageName);
3178
+ ctx.inject(["remote"], (injected) => {
3179
+ const scope = injected;
3180
+ const mounting = scope.remote.$mount(contribution);
3181
+ mounting.catch(() => {});
3182
+ scope.effect(() => async () => {
3183
+ await (await mounting.catch(() => void 0))?.();
3184
+ });
3185
+ });
3186
+ }
3251
3187
  /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
3252
3188
  * A factory shares one logical stream while at least one subscriber remains. */
3253
- function createModelSettingsSubscriber(ctx, namespace) {
3189
+ function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
3190
+ const endpoint = identity;
3254
3191
  const listeners = /* @__PURE__ */ new Set();
3255
3192
  let injection;
3256
3193
  let current;
@@ -3276,12 +3213,18 @@ function createModelSettingsSubscriber(ctx, namespace) {
3276
3213
  };
3277
3214
  const start = () => {
3278
3215
  const life = ++lifecycle;
3279
- injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
3216
+ injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
3280
3217
  if (life !== lifecycle || !listeners.size) return;
3281
3218
  const scope = injected;
3219
+ const service = scope.remote[endpoint.namespace];
3220
+ if (!service || !("watch" in service) || !service.watch) {
3221
+ report("Model settings sync service unavailable");
3222
+ return;
3223
+ }
3224
+ const watch = service.watch;
3282
3225
  const stream = scope.remote.$stream({
3283
3226
  name: "Model settings sync",
3284
- open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
3227
+ open: (signal) => watch(signal),
3285
3228
  ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
3286
3229
  });
3287
3230
  const session = {
@@ -3316,7 +3259,7 @@ function createModelSettingsSubscriber(ctx, namespace) {
3316
3259
  invalidate();
3317
3260
  } else {
3318
3261
  if (!ready) throw new Error("Model settings sync change arrived before ready");
3319
- if (namespace === "*" || frame.ns === namespace) invalidate();
3262
+ if (accept(frame.ns)) invalidate();
3320
3263
  }
3321
3264
  }
3322
3265
  } catch (error) {
@@ -3700,7 +3643,7 @@ function SessionManagerSettings(props) {
3700
3643
  });
3701
3644
  };
3702
3645
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3703
- className: `${FREE_CHAT_ROOT} dsh-ui-panel`,
3646
+ className: `${FREE_CHAT_ROOT} dsh-ui-page`,
3704
3647
  children: [
3705
3648
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3706
3649
  className: "dsh-ui-card",
@@ -3967,7 +3910,7 @@ function SessionManagerSettings(props) {
3967
3910
  ]
3968
3911
  }),
3969
3912
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3970
- className: "dsh-ui-actions",
3913
+ className: "dsh-ui-form-footer",
3971
3914
  children: [
3972
3915
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3973
3916
  variant: "primary",
@@ -4098,58 +4041,31 @@ const inject = [
4098
4041
  "locale",
4099
4042
  "sessions",
4100
4043
  "uiWorkspace",
4101
- "shortcuts",
4102
4044
  "connection",
4103
4045
  "workspaces",
4104
4046
  "layout",
4105
4047
  "conversation",
4106
4048
  ...MODEL_CATALOG_INJECT
4107
4049
  ];
4108
- function CopyThreadMenuItem(props) {
4109
- const [failed, setFailed] = (0, react.useState)(false);
4110
- const [, setMenuOpen] = props.useMenuOpenState();
4111
- const label = failed ? COPY_THREAD_FAILED[props.locale] : COPY_THREAD_LABEL[props.locale];
4112
- const onSelect = () => copyThreadId(props.sessionId, props.writeText ?? writeClipboard).then(() => {
4113
- setFailed(false);
4114
- setMenuOpen(false);
4115
- }, () => {
4116
- setFailed(true);
4117
- });
4118
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
4119
- separatorBefore: true,
4120
- shortcut: props.shortcut,
4121
- onSelect,
4122
- children: label
4123
- });
4124
- }
4125
4050
  function apply(ctx) {
4126
4051
  const client = ctx;
4052
+ mountModelSettingsChanges(client, PLUGIN_NAME);
4127
4053
  const host = bindHost(client);
4128
4054
  const chats = createFreeChatModel(host);
4129
4055
  const retries = createRetryModel(host);
4130
4056
  const temporaryHost = bindTemporaryWorkspaceHost(client);
4131
4057
  const temporary = createTemporaryWorkspaceModel(temporaryHost, void 0, () => uiText(copyLocale(client.locale?.getSnapshot().active), "tempWorkspace"));
4132
- const currentLocale = () => client.locale?.getSnapshot().active;
4133
4058
  const presenceHost = bindPresenceHost(client);
4134
4059
  const continues = createContinueModel(presenceHost);
4135
4060
  const welcomeBack = createWelcomeBackModel(presenceHost);
4136
4061
  const presenceConfig = createPresenceConfigModel(presenceHost);
4137
- const settingsSubscriber = createModelSettingsSubscriber(client, PLUGIN_NAME);
4062
+ const settingsSubscriber = createModelSettingsSubscriber(client, PLUGIN_NAME, modelSettingsChangesIdentity(PLUGIN_NAME));
4138
4063
  const subscribeSettings = (onChange, onError) => settingsSubscriber.subscribe(onChange, onError);
4139
4064
  const modelCatalog = () => readModelCatalog(client);
4140
4065
  const subscribeCatalog = (onChange) => client.connection?.generation?.subscribe(onChange);
4141
4066
  client.effect(() => {
4142
4067
  const stops = [];
4143
4068
  if (typeof document !== "undefined") stops.push(installContextMenuForwarding(document));
4144
- stops.push(client.slots.inject("sidebar.workspaces.session.menu.item", () => client.slots.register({
4145
- name: "sidebar.workspaces.session.menu.item",
4146
- id: COPY_THREAD_MENU_ID,
4147
- order: 500
4148
- }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyThreadSlot, {
4149
- locale: client.locale,
4150
- shortcuts: client.shortcuts,
4151
- ...props
4152
- }))));
4153
4069
  stops.push(client.slots.inject("sidebar.workspaces.session.menu.item", () => client.slots.register({
4154
4070
  name: "sidebar.workspaces.session.menu.item",
4155
4071
  id: DELETE_DIALOG_ID,
@@ -4214,12 +4130,6 @@ function apply(ctx) {
4214
4130
  const locale = useLiveLocale(client.locale);
4215
4131
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4216
4132
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: freeChatCss }),
4217
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyThreadShortcutNotice, {
4218
- shortcuts: client.shortcuts,
4219
- list: client.sessions?.list,
4220
- locale,
4221
- currentLocale
4222
- }),
4223
4133
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TemporaryWorkspaceNotice, {
4224
4134
  model: temporary,
4225
4135
  locale
@@ -4314,17 +4224,6 @@ function apply(ctx) {
4314
4224
  };
4315
4225
  });
4316
4226
  }
4317
- const EMPTY_SHORTCUTS = [];
4318
- function CopyThreadSlot(props) {
4319
- const locale = useLiveLocale(props.locale);
4320
- const shortcuts = (0, react.useSyncExternalStore)((listener) => props.shortcuts?.catalog.subscribe(listener) ?? (() => {}), () => props.shortcuts?.catalog.getSnapshot() ?? EMPTY_SHORTCUTS, () => EMPTY_SHORTCUTS);
4321
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyThreadMenuItem, {
4322
- sessionId: props.sessionId,
4323
- useMenuOpenState: props.useMenuOpenState,
4324
- locale,
4325
- shortcut: shortcuts.find((row) => row.id === COPY_THREAD_SHORTCUT_ID)
4326
- });
4327
- }
4328
4227
  function DeleteMenuItem(props) {
4329
4228
  const locale = useLiveLocale(props.locale);
4330
4229
  const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
@@ -4343,7 +4242,6 @@ function useLiveLocale(locale) {
4343
4242
  return copyLocale((0, react.useSyncExternalStore)((listener) => locale?.subscribe(listener) ?? (() => {}), () => locale?.getSnapshot().active ?? "en", () => locale?.getSnapshot().active ?? "en"));
4344
4243
  }
4345
4244
  //#endregion
4346
- exports.CopyThreadMenuItem = CopyThreadMenuItem;
4347
4245
  exports.apply = apply;
4348
4246
  exports.inject = inject;
4349
4247
  exports.name = name;