dsh-ui-fortifier 0.5.2 → 0.6.0

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.md CHANGED
@@ -22,6 +22,8 @@ Adds a "ui-fortifier" tab in Settings to toggle every feature module of this plu
22
22
 
23
23
  | Feature | Description |
24
24
  | --- | --- |
25
+ | session-id-copy | Copy-session-ID button in the Session header's utility group. |
26
+ | compact-button | Compact-context button at the right end of the composer tool row's left group; runs `/compact`. |
25
27
  | provider-label | Shows the currently selected provider to the left of the model selector. |
26
28
  | model-picker | Cascading model picker (provider → model) next to the model selector in the input row. |
27
29
  | open-dsh-folder | Adds an "Open .dsh folder" button next to "Open configuration file" at the top-right of the Settings page. |
package/README.zh.md CHANGED
@@ -22,6 +22,8 @@ dsh plugin --profile web add dsh-ui-fortifier
22
22
 
23
23
  | 功能 | 描述 |
24
24
  | --- | --- |
25
+ | session-id-copy | 会话标题栏工具区提供复制会话 ID 按钮。 |
26
+ | compact-button | 输入框工具行左组右端提供压缩上下文按钮,等价于提交 `/compact` 指令。 |
25
27
  | provider-label | 在模型选择按钮左侧显示当前选择的提供方。 |
26
28
  | model-picker | 输入行模型选择器旁提供级联模型选择器(提供商 → 模型)。 |
27
29
  | open-dsh-folder | 在设置页右上角「打开配置文件」旁增加「打开 .dsh 文件夹」。 |
package/lib/client.js CHANGED
@@ -9,13 +9,13 @@ window.__ModuleLoader__.load({
9
9
  let react = require("react");
10
10
  let react_dom = require("react-dom");
11
11
  //#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\settings\FortifierSettingsPage.module.css.mjs
12
- const css$4 = ".t1_2kq_page{flex-direction:column;gap:8px;max-width:520px;padding:16px;display:flex}.t1_2kq_heading{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;line-height:24px}.t1_2kq_list{flex-direction:column;margin:0;padding:0;list-style:none;display:flex}.t1_2kq_row{justify-content:space-between;align-items:center;gap:12px;padding:10px 12px;display:flex}.t1_2kq_row+.t1_2kq_row{border-top:.5px solid var(--dsw-alias-border-l3)}.t1_2kq_row:hover{background:var(--dsw-alias-interactive-bg-hover)}.t1_2kq_rowLabel{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}.t1_2kq_switch{box-sizing:border-box;background:var(--dsw-alias-border-l3);cursor:pointer;border:0;border-radius:10px;flex:none;width:36px;height:20px;padding:2px;position:relative}.t1_2kq_switchOn{background:var(--dsw-alias-brand-primary)}.t1_2kq_switch:disabled{cursor:default;opacity:.5}.t1_2kq_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.t1_2kq_thumb{corner-shape:round;background:var(--dsw-alias-label-primary-foreground);border-radius:50%;width:16px;height:16px;transition:transform .12s;display:block}.t1_2kq_switchOn .t1_2kq_thumb{transform:translate(16px)}";
13
- const tagId$4 = "dsh-ui-fortifier/FortifierSettingsPage.module.css";
14
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
12
+ const css$6 = ".t1_2kq_page{flex-direction:column;gap:8px;max-width:520px;padding:16px;display:flex}.t1_2kq_heading{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;line-height:24px}.t1_2kq_list{flex-direction:column;margin:0;padding:0;list-style:none;display:flex}.t1_2kq_row{justify-content:space-between;align-items:center;gap:12px;padding:10px 12px;display:flex}.t1_2kq_row+.t1_2kq_row{border-top:.5px solid var(--dsw-alias-border-l3)}.t1_2kq_row:hover{background:var(--dsw-alias-interactive-bg-hover)}.t1_2kq_rowLabel{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}.t1_2kq_switch{box-sizing:border-box;background:var(--dsw-alias-border-l3);cursor:pointer;border:0;border-radius:10px;flex:none;width:36px;height:20px;padding:2px;position:relative}.t1_2kq_switchOn{background:var(--dsw-alias-brand-primary)}.t1_2kq_switch:disabled{cursor:default;opacity:.5}.t1_2kq_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.t1_2kq_thumb{corner-shape:round;background:var(--dsw-alias-label-primary-foreground);border-radius:50%;width:16px;height:16px;transition:transform .12s;display:block}.t1_2kq_switchOn .t1_2kq_thumb{transform:translate(16px)}";
13
+ const tagId$6 = "dsh-ui-fortifier/FortifierSettingsPage.module.css";
14
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$6) + "]") === null) {
15
15
  const tag = document.createElement("style");
16
16
  tag.dataset.plugin = "dsh-ui-fortifier";
17
- tag.dataset.pluginCss = tagId$4;
18
- tag.textContent = css$4;
17
+ tag.dataset.pluginCss = tagId$6;
18
+ tag.textContent = css$6;
19
19
  document.head.appendChild(tag);
20
20
  }
21
21
  var FortifierSettingsPage_module_css_default = {
@@ -74,6 +74,8 @@ window.__ModuleLoader__.load({
74
74
  "config.open-dsh-folder": "Settings top-right - Open .dsh folder",
75
75
  "config.settings-frame": "Settings panel - Drag and resize",
76
76
  "config.model-picker": "Input - Cascading model picker",
77
+ "config.session-id-copy": "Session header - Copy session ID",
78
+ "config.compact-button": "Input left group - Compact context",
77
79
  openDshFolder: "Open .dsh folder",
78
80
  "openDshFolder.error": "Could not open the .dsh folder",
79
81
  "openDshFolder.pathLabel": "Path: ",
@@ -90,7 +92,14 @@ window.__ModuleLoader__.load({
90
92
  "modelPicker.loadError": "Failed to load models",
91
93
  "modelPicker.retry": "Retry",
92
94
  "modelPicker.provider": "Provider: {name}",
93
- "modelPicker.model": "Model: {name}"
95
+ "modelPicker.model": "Model: {name}",
96
+ "sessionIdCopy.action": "Copy Session ID",
97
+ "sessionIdCopy.hint": "Session ID: {id}",
98
+ "sessionIdCopy.copied": "Copied",
99
+ "compactButton.action": "Compact context",
100
+ "compactButton.confirmTitle": "Compact context",
101
+ "compactButton.confirmText": "Confirm compaction?",
102
+ "compactButton.confirm": "Confirm"
94
103
  };
95
104
  /** 中文文案。 */
96
105
  const zh = {
@@ -100,6 +109,8 @@ window.__ModuleLoader__.load({
100
109
  "config.open-dsh-folder": "设置右上角 - 打开 .dsh 文件夹",
101
110
  "config.settings-frame": "设置面板 - 拖动与缩放",
102
111
  "config.model-picker": "输入框 - 级联模型选择器",
112
+ "config.session-id-copy": "会话标题栏 - 复制 Session ID",
113
+ "config.compact-button": "输入框左组 - 压缩上下文",
103
114
  openDshFolder: "打开 .dsh 文件夹",
104
115
  "openDshFolder.error": "无法打开 .dsh 文件夹",
105
116
  "openDshFolder.pathLabel": "路径:",
@@ -116,7 +127,14 @@ window.__ModuleLoader__.load({
116
127
  "modelPicker.loadError": "模型加载失败",
117
128
  "modelPicker.retry": "重试",
118
129
  "modelPicker.provider": "提供商:{name}",
119
- "modelPicker.model": "模型:{name}"
130
+ "modelPicker.model": "模型:{name}",
131
+ "sessionIdCopy.action": "复制 Session ID",
132
+ "sessionIdCopy.hint": "Session ID: {id}",
133
+ "sessionIdCopy.copied": "已复制",
134
+ "compactButton.action": "压缩上下文",
135
+ "compactButton.confirmTitle": "压缩上下文",
136
+ "compactButton.confirmText": "确认压缩?",
137
+ "compactButton.confirm": "确认"
120
138
  };
121
139
  //#endregion
122
140
  //#region src/client/settings/index.ts
@@ -180,13 +198,13 @@ window.__ModuleLoader__.load({
180
198
  }
181
199
  //#endregion
182
200
  //#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\provider-label\ProviderLabel.module.css.mjs
183
- const css$3 = ".LjXNpG_label{text-overflow:ellipsis;white-space:nowrap;max-width:160px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;overflow:hidden}";
184
- const tagId$3 = "dsh-ui-fortifier/ProviderLabel.module.css";
185
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
201
+ const css$5 = ".LjXNpG_label{text-overflow:ellipsis;white-space:nowrap;max-width:160px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;overflow:hidden}";
202
+ const tagId$5 = "dsh-ui-fortifier/ProviderLabel.module.css";
203
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$5) + "]") === null) {
186
204
  const tag = document.createElement("style");
187
205
  tag.dataset.plugin = "dsh-ui-fortifier";
188
- tag.dataset.pluginCss = tagId$3;
189
- tag.textContent = css$3;
206
+ tag.dataset.pluginCss = tagId$5;
207
+ tag.textContent = css$5;
190
208
  document.head.appendChild(tag);
191
209
  }
192
210
  var ProviderLabel_module_css_default = { "label": "LjXNpG_label" };
@@ -194,7 +212,7 @@ window.__ModuleLoader__.load({
194
212
  //#region src/client/provider-label/ProviderLabel.tsx
195
213
  function ProviderLabel(props) {
196
214
  const provider = props.useDirectory((value) => value).current?.provider;
197
- if (provider === void 0 || provider.length === 0) return null;
215
+ if (!provider) return null;
198
216
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
199
217
  className: ProviderLabel_module_css_default.label,
200
218
  title: provider,
@@ -218,13 +236,13 @@ window.__ModuleLoader__.load({
218
236
  }
219
237
  //#endregion
220
238
  //#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\open-dsh-folder\OpenDshFolderAction.module.css.mjs
221
- const css$2 = "._8KsKXW_action{align-items:center;gap:8px;min-width:0;display:flex}._8KsKXW_error{max-width:180px;color:var(--dsw-alias-state-error-primary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}";
222
- const tagId$2 = "dsh-ui-fortifier/OpenDshFolderAction.module.css";
223
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
239
+ const css$4 = "._8KsKXW_action{align-items:center;gap:8px;min-width:0;display:flex}._8KsKXW_error{max-width:180px;color:var(--dsw-alias-state-error-primary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}";
240
+ const tagId$4 = "dsh-ui-fortifier/OpenDshFolderAction.module.css";
241
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
224
242
  const tag = document.createElement("style");
225
243
  tag.dataset.plugin = "dsh-ui-fortifier";
226
- tag.dataset.pluginCss = tagId$2;
227
- tag.textContent = css$2;
244
+ tag.dataset.pluginCss = tagId$4;
245
+ tag.textContent = css$4;
228
246
  document.head.appendChild(tag);
229
247
  }
230
248
  var OpenDshFolderAction_module_css_default = {
@@ -239,11 +257,11 @@ window.__ModuleLoader__.load({
239
257
  * @returns 操作按钮及其结果提示。
240
258
  */
241
259
  function OpenDshFolderAction(props) {
242
- const { openDshFolder, t } = props;
260
+ const t = props.t;
243
261
  const [state, setState] = (0, react.useState)({ kind: "idle" });
244
262
  const open = async () => {
245
263
  setState({ kind: "opening" });
246
- const result = await openDshFolder();
264
+ const result = await props.openDshFolder();
247
265
  if (!result.ok) {
248
266
  setState({ kind: "error" });
249
267
  return;
@@ -281,7 +299,7 @@ window.__ModuleLoader__.load({
281
299
  //#endregion
282
300
  //#region src/client/open-dsh-folder/index.ts
283
301
  /** 本模块文案命名空间(与 host 侧一致)。 */
284
- const NS$1 = "ui-fortifier";
302
+ const NS$2 = "ui-fortifier";
285
303
  /**
286
304
  * 注册「打开 .dsh 文件夹」到设置页右上角操作区,仅在回环连接下注册。
287
305
  * @param ctx - 需已提供 slots/connection 的客户端上下文。
@@ -295,19 +313,19 @@ window.__ModuleLoader__.load({
295
313
  name: "settings.action",
296
314
  id: "open-dsh-folder",
297
315
  order: 1,
298
- locale: NS$1,
316
+ locale: NS$2,
299
317
  inject: () => ({ openDshFolder })
300
318
  }, OpenDshFolderAction));
301
319
  }
302
320
  //#endregion
303
321
  //#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\settings-frame\SettingsFrameEnhancer.module.css.mjs
304
- const css$1 = ".hB-7Ba_hostAnchor{display:none}.hB-7Ba_dragHandle{box-sizing:border-box;width:22px;height:22px;color:var(--dsw-alias-label-secondary);cursor:grab;vertical-align:middle;touch-action:none;background:0 0;border:none;border-radius:6px;flex:none;justify-content:center;align-items:center;margin:0 8px 0 0;padding:0;display:inline-flex}.hB-7Ba_dragHandle:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.hB-7Ba_gripIcon{flex:none;width:10px;height:14px}.hB-7Ba_resizeHandle{cursor:nwse-resize;touch-action:none;background:0 0;border:none;width:16px;height:16px;margin:0;padding:0;position:absolute;bottom:8px;right:8px}.hB-7Ba_resizeHandle:before,.hB-7Ba_resizeHandle:after{content:\"\";pointer-events:none;position:absolute;bottom:0;right:0}.hB-7Ba_resizeHandle:after{border-right:2px solid var(--dsw-alias-label-secondary);border-bottom:2px solid var(--dsw-alias-label-secondary);width:10px;height:10px}.hB-7Ba_resizeHandle:before{border-right:2px solid var(--dsw-alias-label-tertiary);border-bottom:2px solid var(--dsw-alias-label-tertiary);width:6px;height:6px;bottom:4px;right:4px}.hB-7Ba_resizeHandle:hover:after{border-color:var(--dsw-alias-label-primary)}";
305
- const tagId$1 = "dsh-ui-fortifier/SettingsFrameEnhancer.module.css";
306
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
322
+ const css$3 = ".hB-7Ba_hostAnchor{display:none}.hB-7Ba_dragHandle{box-sizing:border-box;width:22px;height:22px;color:var(--dsw-alias-label-secondary);cursor:grab;vertical-align:middle;touch-action:none;background:0 0;border:none;border-radius:6px;flex:none;justify-content:center;align-items:center;margin:0 8px 0 0;padding:0;display:inline-flex}.hB-7Ba_dragHandle:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.hB-7Ba_gripIcon{flex:none;width:10px;height:14px}.hB-7Ba_resizeHandle{cursor:nwse-resize;touch-action:none;background:0 0;border:none;width:16px;height:16px;margin:0;padding:0;position:absolute;bottom:8px;right:8px}.hB-7Ba_resizeHandle:before,.hB-7Ba_resizeHandle:after{content:\"\";pointer-events:none;position:absolute;bottom:0;right:0}.hB-7Ba_resizeHandle:after{border-right:2px solid var(--dsw-alias-label-secondary);border-bottom:2px solid var(--dsw-alias-label-secondary);width:10px;height:10px}.hB-7Ba_resizeHandle:before{border-right:2px solid var(--dsw-alias-label-tertiary);border-bottom:2px solid var(--dsw-alias-label-tertiary);width:6px;height:6px;bottom:4px;right:4px}.hB-7Ba_resizeHandle:hover:after{border-color:var(--dsw-alias-label-primary)}";
323
+ const tagId$3 = "dsh-ui-fortifier/SettingsFrameEnhancer.module.css";
324
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
307
325
  const tag = document.createElement("style");
308
326
  tag.dataset.plugin = "dsh-ui-fortifier";
309
- tag.dataset.pluginCss = tagId$1;
310
- tag.textContent = css$1;
327
+ tag.dataset.pluginCss = tagId$3;
328
+ tag.textContent = css$3;
311
329
  document.head.appendChild(tag);
312
330
  }
313
331
  var SettingsFrameEnhancer_module_css_default = {
@@ -332,7 +350,7 @@ window.__ModuleLoader__.load({
332
350
  /** 读取持久化的面板几何;缺失或损坏返回 undefined。 */
333
351
  function readFrameGeometry() {
334
352
  const raw = localStorage.getItem(FRAME_STORAGE_KEY);
335
- if (raw === null) return void 0;
353
+ if (!raw) return void 0;
336
354
  try {
337
355
  const parsed = JSON.parse(raw);
338
356
  if (typeof parsed.left === "number" && Number.isFinite(parsed.left) && typeof parsed.top === "number" && Number.isFinite(parsed.top) && typeof parsed.width === "number" && Number.isFinite(parsed.width) && typeof parsed.height === "number" && Number.isFinite(parsed.height)) return parsed;
@@ -431,16 +449,16 @@ window.__ModuleLoader__.load({
431
449
  const [headerRow, setHeaderRow] = (0, react.useState)(null);
432
450
  (0, react.useEffect)(() => {
433
451
  const anchor = anchorRef.current;
434
- if (anchor === null) return;
452
+ if (!anchor) return;
435
453
  const dialog = anchor.closest("[role=\"dialog\"]");
436
454
  if (!(dialog instanceof HTMLElement)) return;
437
455
  const rect = dialog.getBoundingClientRect();
438
456
  const saved = readFrameGeometry();
439
457
  dialog.style.position = "fixed";
440
- dialog.style.left = `${saved === void 0 ? rect.left : saved.left}px`;
441
- dialog.style.top = `${saved === void 0 ? rect.top : saved.top}px`;
442
- dialog.style.width = `${saved === void 0 ? rect.width : saved.width}px`;
443
- dialog.style.height = `${saved === void 0 ? rect.height : saved.height}px`;
458
+ dialog.style.left = `${saved ? saved.left : rect.left}px`;
459
+ dialog.style.top = `${saved ? saved.top : rect.top}px`;
460
+ dialog.style.width = `${saved ? saved.width : rect.width}px`;
461
+ dialog.style.height = `${saved ? saved.height : rect.height}px`;
444
462
  dialog.style.margin = "0";
445
463
  dialog.style.maxWidth = "none";
446
464
  constrainToViewport(dialog);
@@ -450,7 +468,7 @@ window.__ModuleLoader__.load({
450
468
  setHeaderRow(row instanceof HTMLElement ? row : null);
451
469
  let windowResizeFrame = null;
452
470
  const onWindowResize = () => {
453
- if (windowResizeFrame !== null) return;
471
+ if (windowResizeFrame) return;
454
472
  windowResizeFrame = requestAnimationFrame(() => {
455
473
  windowResizeFrame = null;
456
474
  constrainToViewport(dialog);
@@ -459,7 +477,7 @@ window.__ModuleLoader__.load({
459
477
  };
460
478
  window.addEventListener("resize", onWindowResize);
461
479
  return () => {
462
- if (windowResizeFrame !== null) {
480
+ if (windowResizeFrame) {
463
481
  cancelAnimationFrame(windowResizeFrame);
464
482
  windowResizeFrame = null;
465
483
  }
@@ -477,7 +495,7 @@ window.__ModuleLoader__.load({
477
495
  }, []);
478
496
  (0, react.useEffect)(() => {
479
497
  const handle = dragHandleRef.current;
480
- if (handle === null || headerRow === null) return;
498
+ if (!handle || !headerRow) return;
481
499
  if (headerRow.firstChild === handle) return;
482
500
  headerRow.insertBefore(handle, headerRow.firstChild);
483
501
  }, [headerRow]);
@@ -488,7 +506,7 @@ window.__ModuleLoader__.load({
488
506
  });
489
507
  const frame = (0, react.useRef)(null);
490
508
  const onDragPointerDown = (event) => {
491
- if (panel === null) return;
509
+ if (!panel) return;
492
510
  event.preventDefault();
493
511
  event.currentTarget.setPointerCapture(event.pointerId);
494
512
  dragState.current = {
@@ -497,28 +515,28 @@ window.__ModuleLoader__.load({
497
515
  };
498
516
  };
499
517
  const onDragPointerMove = (event) => {
500
- if (dragState.current === null) return;
518
+ if (!dragState.current) return;
501
519
  latest.current = {
502
520
  x: event.clientX,
503
521
  y: event.clientY
504
522
  };
505
523
  frame.current ??= requestAnimationFrame(() => {
506
524
  frame.current = null;
507
- if (panel === null || dragState.current === null) return;
525
+ if (!panel || !dragState.current) return;
508
526
  panel.style.left = `${latest.current.x - dragState.current.offsetX}px`;
509
527
  panel.style.top = `${latest.current.y - dragState.current.offsetY}px`;
510
528
  constrainPanel(panel);
511
529
  });
512
530
  };
513
531
  const onDragPointerUp = (event) => {
514
- if (dragState.current === null) return;
515
- if (frame.current !== null) {
532
+ if (!dragState.current) return;
533
+ if (frame.current) {
516
534
  cancelAnimationFrame(frame.current);
517
535
  frame.current = null;
518
536
  }
519
537
  dragState.current = null;
520
538
  event.currentTarget.releasePointerCapture(event.pointerId);
521
- if (panel === null) return;
539
+ if (!panel) return;
522
540
  constrainPanel(panel);
523
541
  persistGeometry(panel);
524
542
  };
@@ -527,7 +545,7 @@ window.__ModuleLoader__.load({
527
545
  const resizeMax = (0, react.useRef)(null);
528
546
  const resizeFrame = (0, react.useRef)(null);
529
547
  const onResizePointerDown = (event) => {
530
- if (panel === null) return;
548
+ if (!panel) return;
531
549
  event.preventDefault();
532
550
  event.stopPropagation();
533
551
  event.currentTarget.setPointerCapture(event.pointerId);
@@ -545,14 +563,14 @@ window.__ModuleLoader__.load({
545
563
  };
546
564
  };
547
565
  const onResizePointerMove = (event) => {
548
- if (resizeOrigin.current === null || resizeBase.current === null || resizeMax.current === null) return;
566
+ if (!resizeOrigin.current || !resizeBase.current || !resizeMax.current) return;
549
567
  latest.current = {
550
568
  x: event.clientX,
551
569
  y: event.clientY
552
570
  };
553
571
  resizeFrame.current ??= requestAnimationFrame(() => {
554
572
  resizeFrame.current = null;
555
- if (panel === null || resizeOrigin.current === null || resizeBase.current === null || resizeMax.current === null) return;
573
+ if (!panel || !resizeOrigin.current || !resizeBase.current || !resizeMax.current) return;
556
574
  const width = clamp(resizeBase.current.width + (latest.current.x - resizeOrigin.current.x), MIN_WIDTH, Math.max(MIN_WIDTH, resizeMax.current.width));
557
575
  const height = clamp(resizeBase.current.height + (latest.current.y - resizeOrigin.current.y), MIN_HEIGHT, Math.max(MIN_HEIGHT, resizeMax.current.height));
558
576
  panel.style.width = `${width}px`;
@@ -561,15 +579,15 @@ window.__ModuleLoader__.load({
561
579
  });
562
580
  };
563
581
  const onResizePointerUp = (event) => {
564
- if (resizeOrigin.current === null) return;
565
- if (resizeFrame.current !== null) {
582
+ if (!resizeOrigin.current) return;
583
+ if (resizeFrame.current) {
566
584
  cancelAnimationFrame(resizeFrame.current);
567
585
  resizeFrame.current = null;
568
586
  }
569
587
  resizeOrigin.current = null;
570
588
  resizeBase.current = null;
571
589
  event.currentTarget.releasePointerCapture(event.pointerId);
572
- if (panel !== null) persistGeometry(panel);
590
+ if (panel) persistGeometry(panel);
573
591
  };
574
592
  const dragHandle = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
575
593
  ref: dragHandleRef,
@@ -597,14 +615,14 @@ window.__ModuleLoader__.load({
597
615
  ref: anchorRef,
598
616
  className: SettingsFrameEnhancer_module_css_default.hostAnchor
599
617
  }),
600
- headerRow !== null && (0, react_dom.createPortal)(dragHandle, headerRow),
601
- panel !== null && (0, react_dom.createPortal)(resizeHandle, panel)
618
+ headerRow && (0, react_dom.createPortal)(dragHandle, headerRow),
619
+ panel && (0, react_dom.createPortal)(resizeHandle, panel)
602
620
  ] });
603
621
  }
604
622
  //#endregion
605
623
  //#region src/client/settings-frame/index.ts
606
624
  /** 本模块文案命名空间(与 host 侧一致)。 */
607
- const NS = "ui-fortifier";
625
+ const NS$1 = "ui-fortifier";
608
626
  /**
609
627
  * 注册「拖动位置/大小」增强到设置页右上角操作区。
610
628
  * @param ctx - 需已提供 slots 的客户端上下文。
@@ -616,19 +634,19 @@ window.__ModuleLoader__.load({
616
634
  name: "settings.action",
617
635
  id: "settings-frame-enhancer",
618
636
  order: 2,
619
- locale: NS,
637
+ locale: NS$1,
620
638
  inject: injected
621
639
  }, SettingsFrameEnhancer));
622
640
  }
623
641
  //#endregion
624
642
  //#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\model-picker\ModelPicker.module.css.mjs
625
- const css = ".AGI7WG_root{flex:none;display:inline-flex}.AGI7WG_trigger{box-sizing:border-box;width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:14px;justify-content:center;align-items:center;padding:0;display:inline-flex}.AGI7WG_trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.AGI7WG_trigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.AGI7WG_triggerIcon{flex:none;width:14px;height:14px}.AGI7WG_menu{z-index:1100;box-sizing:border-box;background:var(--dsw-specific-menu);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);min-width:min(420px,100vw - 32px);max-width:min(520px,100vw - 32px);height:min(420px,100vh - 96px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;border-radius:16px;flex-direction:column;padding:6px;display:flex;position:fixed;overflow:hidden}.AGI7WG_search{box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);border-radius:10px;flex:none;align-items:center;gap:6px;height:28px;margin:0 2px 6px;padding:0 8px;display:flex}.AGI7WG_search:focus-within{border-color:var(--dsw-alias-brand-primary)}.AGI7WG_searchIcon{color:var(--dsw-alias-label-tertiary);flex:none;display:inline-flex}.AGI7WG_searchIcon svg{width:13px;height:13px}.AGI7WG_searchInput{min-width:0;color:var(--dsw-alias-label-primary);font:inherit;background:0 0;border:none;outline:none;flex:1;padding:0;font-size:13px;line-height:20px}.AGI7WG_searchInput::placeholder{color:var(--dsw-alias-label-tertiary)}.AGI7WG_columns{flex:1;min-height:0;display:flex;overflow:hidden}.AGI7WG_providers{flex:none;width:160px;padding-right:4px;overflow-y:auto}.AGI7WG_models{flex:1;min-width:0;padding-left:4px;overflow-y:auto}.AGI7WG_columnTitle{color:var(--dsw-alias-label-tertiary);padding:5px 8px 3px;font-size:12px;font-weight:500;line-height:18px}.AGI7WG_cell{box-sizing:border-box;width:100%;min-height:34px;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;align-items:center;gap:8px;padding:6px 8px;display:flex}.AGI7WG_cell:hover:not(:disabled),.AGI7WG_cell:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.AGI7WG_cellActive{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}.AGI7WG_cellSelected{color:var(--dsw-alias-label-primary);font-weight:500}.AGI7WG_cell:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.AGI7WG_modelName{overflow-wrap:anywhere;flex:1;min-width:0}.AGI7WG_check{color:var(--dsw-alias-brand-primary);flex:none}.AGI7WG_status{color:var(--dsw-alias-label-tertiary);padding:10px;font-size:13px;line-height:20px}.AGI7WG_error{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);border-radius:8px;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:4px;padding:7px 8px;font-size:12px;line-height:18px;display:flex}.AGI7WG_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600}";
626
- const tagId = "dsh-ui-fortifier/ModelPicker.module.css";
627
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
643
+ const css$2 = ".AGI7WG_root{flex:none;display:inline-flex}.AGI7WG_trigger{box-sizing:border-box;width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:14px;justify-content:center;align-items:center;padding:0;display:inline-flex}.AGI7WG_trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.AGI7WG_trigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.AGI7WG_triggerIcon{flex:none;width:14px;height:14px}.AGI7WG_menu{z-index:1100;box-sizing:border-box;background:var(--dsw-specific-menu);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);min-width:min(420px,100vw - 32px);max-width:min(520px,100vw - 32px);height:min(420px,100vh - 96px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;border-radius:16px;flex-direction:column;padding:6px;display:flex;position:fixed;overflow:hidden}.AGI7WG_search{box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);border-radius:10px;flex:none;align-items:center;gap:6px;height:28px;margin:0 2px 6px;padding:0 8px;display:flex}.AGI7WG_search:focus-within{border-color:var(--dsw-alias-brand-primary)}.AGI7WG_searchIcon{color:var(--dsw-alias-label-tertiary);flex:none;display:inline-flex}.AGI7WG_searchIcon svg{width:13px;height:13px}.AGI7WG_searchInput{min-width:0;color:var(--dsw-alias-label-primary);font:inherit;background:0 0;border:none;outline:none;flex:1;padding:0;font-size:13px;line-height:20px}.AGI7WG_searchInput::placeholder{color:var(--dsw-alias-label-tertiary)}.AGI7WG_columns{flex:1;min-height:0;display:flex;overflow:hidden}.AGI7WG_providers{flex:none;width:160px;padding-right:4px;overflow-y:auto}.AGI7WG_models{flex:1;min-width:0;padding-left:4px;overflow-y:auto}.AGI7WG_columnTitle{color:var(--dsw-alias-label-tertiary);padding:5px 8px 3px;font-size:12px;font-weight:500;line-height:18px}.AGI7WG_cell{box-sizing:border-box;width:100%;min-height:34px;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;align-items:center;gap:8px;padding:6px 8px;display:flex}.AGI7WG_cell:hover:not(:disabled),.AGI7WG_cell:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.AGI7WG_cellActive{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}.AGI7WG_cellSelected{color:var(--dsw-alias-label-primary);font-weight:500}.AGI7WG_cell:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.AGI7WG_modelName{overflow-wrap:anywhere;flex:1;min-width:0}.AGI7WG_check{color:var(--dsw-alias-brand-primary);flex:none}.AGI7WG_status{color:var(--dsw-alias-label-tertiary);padding:10px;font-size:13px;line-height:20px}.AGI7WG_error{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);border-radius:8px;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:4px;padding:7px 8px;font-size:12px;line-height:18px;display:flex}.AGI7WG_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600}";
644
+ const tagId$2 = "dsh-ui-fortifier/ModelPicker.module.css";
645
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
628
646
  const tag = document.createElement("style");
629
647
  tag.dataset.plugin = "dsh-ui-fortifier";
630
- tag.dataset.pluginCss = tagId;
631
- tag.textContent = css;
648
+ tag.dataset.pluginCss = tagId$2;
649
+ tag.textContent = css$2;
632
650
  document.head.appendChild(tag);
633
651
  }
634
652
  var ModelPicker_module_css_default = {
@@ -670,7 +688,7 @@ window.__ModuleLoader__.load({
670
688
  * @param target - 目标元素,缺失(如已被搜索过滤掉)时不处理。
671
689
  */
672
690
  function scrollIntoViewWithin(container, target) {
673
- if (container === null || target === null) return;
691
+ if (!container || !target) return;
674
692
  const containerRect = container.getBoundingClientRect();
675
693
  const targetRect = target.getBoundingClientRect();
676
694
  if (targetRect.top < containerRect.top) {
@@ -683,7 +701,7 @@ window.__ModuleLoader__.load({
683
701
  * 未定位态:隐藏但按原点参与布局,使首帧 offsetWidth/offsetHeight 为真实尺寸,
684
702
  * 定位不存在时也不会闪现在原点(与官方 ModelSelect 同法)。
685
703
  */
686
- const MEASURE_STYLE = {
704
+ const MEASURE_STYLE$1 = {
687
705
  visibility: "hidden",
688
706
  left: 0,
689
707
  top: 0
@@ -707,7 +725,7 @@ window.__ModuleLoader__.load({
707
725
  }
708
726
  const place = () => {
709
727
  const rect = anchorRef.current?.getBoundingClientRect();
710
- if (rect === void 0) return;
728
+ if (!rect) return;
711
729
  const panel = panelRef.current;
712
730
  const width = panel?.offsetWidth ?? 0;
713
731
  const height = panel?.offsetHeight ?? 0;
@@ -725,7 +743,7 @@ window.__ModuleLoader__.load({
725
743
  window.addEventListener("resize", place);
726
744
  const panel = panelRef.current;
727
745
  let observer = null;
728
- if (typeof ResizeObserver !== "undefined" && panel !== null) {
746
+ if (typeof ResizeObserver !== "undefined" && panel) {
729
747
  observer = new ResizeObserver(place);
730
748
  observer.observe(panel);
731
749
  }
@@ -785,7 +803,7 @@ window.__ModuleLoader__.load({
785
803
  const activeGroup = visibleGroups.find((group) => group.id === picker.provider) ?? visibleGroups[0];
786
804
  const activeProvider = activeGroup?.id ?? null;
787
805
  const activeModels = (0, react.useMemo)(() => {
788
- if (activeGroup === void 0) return [];
806
+ if (!activeGroup) return [];
789
807
  if (query === "") return activeGroup.models;
790
808
  if (hasModelHit) return activeGroup.models.filter((model) => matchesQuery(model.name, query));
791
809
  return activeGroup.models;
@@ -801,13 +819,13 @@ window.__ModuleLoader__.load({
801
819
  return;
802
820
  }
803
821
  if (pendingCurrentScrollRef.current) {
804
- if (currentModelRef.current === null) return;
822
+ if (!currentModelRef.current) return;
805
823
  scrollIntoViewWithin(modelsRef.current, currentModelRef.current);
806
824
  pendingCurrentScrollRef.current = false;
807
825
  return;
808
826
  }
809
827
  if (positionedRef.current) return;
810
- if (currentProviderRef.current === null && currentModelRef.current === null) return;
828
+ if (!currentProviderRef.current && !currentModelRef.current) return;
811
829
  scrollIntoViewWithin(providersRef.current, currentProviderRef.current);
812
830
  scrollIntoViewWithin(modelsRef.current, currentModelRef.current);
813
831
  positionedRef.current = true;
@@ -871,12 +889,12 @@ window.__ModuleLoader__.load({
871
889
  return;
872
890
  }
873
891
  pendingCurrentScrollRef.current = false;
874
- if (modelsRef.current !== null) modelsRef.current.scrollTop = 0;
892
+ if (modelsRef.current) modelsRef.current.scrollTop = 0;
875
893
  };
876
894
  const chooseModel = (modelId) => {
877
895
  if (activeProvider === null) return;
878
896
  const model = activeGroup?.models.find((m) => m.id === modelId);
879
- if (model === void 0) return;
897
+ if (!model) return;
880
898
  const selection = {
881
899
  provider: activeProvider,
882
900
  model: modelId,
@@ -960,7 +978,7 @@ window.__ModuleLoader__.load({
960
978
  className: ModelPicker_module_css_default.menu,
961
979
  role: "dialog",
962
980
  "aria-label": t("modelPicker.trigger"),
963
- style: position ?? MEASURE_STYLE,
981
+ style: position ?? MEASURE_STYLE$1,
964
982
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
965
983
  className: ModelPicker_module_css_default.search,
966
984
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1106,6 +1124,261 @@ window.__ModuleLoader__.load({
1106
1124
  }, ModelPicker));
1107
1125
  }
1108
1126
  //#endregion
1127
+ //#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\session-id-copy\SessionIdCopyAction.module.css.mjs
1128
+ const css$1 = ".i0sS3a_button{box-sizing:border-box;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:14px;flex:none;justify-content:center;align-items:center;gap:3px;padding:0 7px;display:inline-flex}.i0sS3a_button svg{width:13px;height:13px}.i0sS3a_button:hover{background:var(--dsw-alias-interactive-bg-hover)}.i0sS3a_button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}";
1129
+ const tagId$1 = "dsh-ui-fortifier/SessionIdCopyAction.module.css";
1130
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
1131
+ const tag = document.createElement("style");
1132
+ tag.dataset.plugin = "dsh-ui-fortifier";
1133
+ tag.dataset.pluginCss = tagId$1;
1134
+ tag.textContent = css$1;
1135
+ document.head.appendChild(tag);
1136
+ }
1137
+ var SessionIdCopyAction_module_css_default = { "button": "i0sS3a_button" };
1138
+ //#endregion
1139
+ //#region src/client/session-id-copy/SessionIdCopyAction.tsx
1140
+ /**
1141
+ * session-id-copy 功能:会话标题栏的复制 Session ID 按钮。
1142
+ */
1143
+ /**
1144
+ * 井号字形:官方图标库没有标识符类图标,故内联两竖两横的 # 描边。
1145
+ * 与相邻的复制图标并排,让按钮直接表达「复制标识符」而非只有复制。
1146
+ * @returns 装饰性内联图标(按钮已有 aria-label)。
1147
+ */
1148
+ function HashGlyph() {
1149
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1150
+ viewBox: "0 0 16 16",
1151
+ fill: "none",
1152
+ "aria-hidden": "true",
1153
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1154
+ d: "M6.2 2.2 4.9 13.8M11.1 2.2 9.8 13.8M2.6 5.6h11.2M2.2 10.4h11.2",
1155
+ stroke: "currentColor",
1156
+ strokeWidth: "1.4",
1157
+ strokeLinecap: "round"
1158
+ })
1159
+ });
1160
+ }
1161
+ /**
1162
+ * 渲染复制 Session ID 的图标按钮:悬停提示直接给出会话 ID,点击写入剪贴板,
1163
+ * 成功后按钮短暂换成对勾。
1164
+ * @param props - owner 参数(含当前会话 ID)与文案。
1165
+ * @returns 会话标题栏工具区的复制按钮。
1166
+ */
1167
+ function SessionIdCopyAction(props) {
1168
+ const t = props.t;
1169
+ const id = String(props.sessionId);
1170
+ const [copied, setCopied] = (0, react.useState)(false);
1171
+ const pending = (0, react.useRef)(false);
1172
+ const timer = (0, react.useRef)(null);
1173
+ const epoch = (0, react.useRef)(0);
1174
+ (0, react.useEffect)(() => () => {
1175
+ epoch.current += 1;
1176
+ pending.current = false;
1177
+ if (timer.current) clearTimeout(timer.current);
1178
+ }, []);
1179
+ const copy = () => {
1180
+ if (copied || pending.current) return;
1181
+ const started = epoch.current;
1182
+ pending.current = true;
1183
+ (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(id).then((accepted) => {
1184
+ if (started !== epoch.current) return;
1185
+ pending.current = false;
1186
+ if (!accepted) return;
1187
+ setCopied(true);
1188
+ timer.current = window.setTimeout(() => {
1189
+ timer.current = null;
1190
+ setCopied(false);
1191
+ }, 1e3);
1192
+ });
1193
+ };
1194
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
1195
+ label: copied ? t("sessionIdCopy.copied") : t("sessionIdCopy.hint", { id }),
1196
+ side: "bottom",
1197
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1198
+ type: "button",
1199
+ className: SessionIdCopyAction_module_css_default.button,
1200
+ "aria-label": t("sessionIdCopy.action"),
1201
+ onClick: copy,
1202
+ children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(HashGlyph, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, {})] })
1203
+ })
1204
+ });
1205
+ }
1206
+ //#endregion
1207
+ //#region src/client/session-id-copy/index.ts
1208
+ /** 本模块文案命名空间(与 host 侧一致)。 */
1209
+ const NS = "ui-fortifier";
1210
+ /**
1211
+ * 注册复制会话 ID 按钮到会话标题栏工具区(list 槽,追加式;order -20 排在最左,
1212
+ * 位于「在本地打开」按钮之前)。官方三点菜单的菜单项为硬编码、无插槽,
1213
+ * 故本功能以相邻按钮而非菜单项实现。
1214
+ * @param ctx - 需已提供 slots/locale 的客户端上下文。
1215
+ * @returns 停止注入并卸载注册的 disposer。
1216
+ */
1217
+ function installSessionIdCopy(ctx) {
1218
+ return ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
1219
+ name: "conversation.session.header.utilities",
1220
+ id: "session-id-copy",
1221
+ order: -20,
1222
+ locale: NS
1223
+ }, SessionIdCopyAction));
1224
+ }
1225
+ //#endregion
1226
+ //#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\compact-button\CompactContextButton.module.css.mjs
1227
+ const css = ".zxJg-q_root{flex:none;display:inline-flex}.zxJg-q_button{corner-shape:round;background:var(--dsw-specific-selector);width:28px;height:28px;color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:999px;flex:none;place-items:center;display:grid}.zxJg-q_button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.zxJg-q_button:disabled{opacity:.5;cursor:default}.zxJg-q_glyph{fill:currentColor;flex:none;width:16px;height:16px;overflow:hidden}.zxJg-q_popover{z-index:1100;box-sizing:border-box;background:var(--dsw-specific-menu);max-width:min(260px,100vw - 32px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);border-radius:12px;flex-direction:column;align-items:flex-start;gap:10px;padding:10px 12px;display:flex;position:fixed}.zxJg-q_popoverText{margin:0;font-size:13px;line-height:20px}";
1228
+ const tagId = "dsh-ui-fortifier/CompactContextButton.module.css";
1229
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1230
+ const tag = document.createElement("style");
1231
+ tag.dataset.plugin = "dsh-ui-fortifier";
1232
+ tag.dataset.pluginCss = tagId;
1233
+ tag.textContent = css;
1234
+ document.head.appendChild(tag);
1235
+ }
1236
+ var CompactContextButton_module_css_default = {
1237
+ "button": "zxJg-q_button",
1238
+ "glyph": "zxJg-q_glyph",
1239
+ "popover": "zxJg-q_popover",
1240
+ "popoverText": "zxJg-q_popoverText",
1241
+ "root": "zxJg-q_root"
1242
+ };
1243
+ //#endregion
1244
+ //#region src/client/compact-button/CompactContextButton.tsx
1245
+ /**
1246
+ * compact-button 功能:输入框工具行左组右端的压缩上下文按钮。
1247
+ */
1248
+ /** 气泡与按钮之间的间距。 */
1249
+ const POPOVER_GAP = 8;
1250
+ /** 气泡与视口边缘保留的距离。 */
1251
+ const POPOVER_MARGIN = 12;
1252
+ const MEASURE_STYLE = {
1253
+ visibility: "hidden",
1254
+ left: 0,
1255
+ top: 0
1256
+ };
1257
+ /**
1258
+ * 压缩字形:官方图标库没有压缩语义的字形(`IconApiOutline14` 是终端提示符、
1259
+ * `IconEnhanceOutline16` 是四条段落横线,在此都会误读),故内联字形,与
1260
+ * `session-id-copy` 因官方无井号而内联 `HashGlyph` 同一做法。字形由用户提供。
1261
+ * @returns 装饰性内联图标(按钮已有 aria-label)。
1262
+ */ function CompressGlyph() {
1263
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1264
+ className: CompactContextButton_module_css_default.glyph,
1265
+ viewBox: "0 0 1024 1024",
1266
+ "aria-hidden": "true",
1267
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M318.1248 840.533333l31.5264-99.8336a759.4624 759.4624 0 0 0 0-457.399466L318.1248 183.466667h332.7296l67.6416 67.6416a733.216 733.216 0 0 0-32.590933 526.184533L705.8752 840.533333H318.1248z m0-733.866666c-51.908267 0-88.866133 50.427733-73.2352 99.925333l31.5264 99.835733a682.658133 682.658133 0 0 1 0 411.144534l-31.5264 99.8336C229.258667 866.9056 266.216533 917.333333 318.1248 917.333333h387.7504c51.908267 0 88.866133-50.427733 73.2352-99.927466l-19.970133-63.240534a656.420267 656.420267 0 0 1 29.947733-472.744533 73.845333 73.845333 0 0 0-14.833067-83.165867l-69.0944-69.0944A76.8 76.8 0 0 0 650.8544 106.666667H318.1248zM152.123733 764.7168c-13.051733-5.437867-19.170133-20.413867-14.2656-33.674667a631.473067 631.473067 0 0 0 0-438.0864c-4.904533-13.2608 1.216-28.2368 14.2656-33.674666l15.752534-6.564267c13.051733-5.437867 28.087467 0.7232 33.045333 13.9648a699.746133 699.746133 0 0 1 0 490.634667c-4.957867 13.2416-19.9936 19.402667-33.045333 13.9648l-15.752534-6.564267z m733.538134-33.474133l-12.343467-29.6256a419.4688 419.4688 0 0 1 8.7744-342.293334c6.101333-12.7552 1.725867-28.311467-10.632533-35.178666l-14.920534-8.288c-12.3584-6.8672-28.010667-2.4384-34.2208 10.263466a487.741867 487.741867 0 0 0-12.017066 401.7536l12.343466 29.623467c5.437867 13.051733 20.426667 19.223467 33.4784 13.7856l15.752534-6.564267c13.051733-5.437867 19.223467-20.426667 13.7856-33.476266z" })
1268
+ });
1269
+ }
1270
+ /**
1271
+ * 渲染压缩上下文按钮:点击先弹出确认气泡,气泡内确认后才提交 `/compact` 指令;
1272
+ * 点气泡外或按 Escape 取消。回合运行中与提交等待期间按钮禁用。
1273
+ * @param props - 会话标准套件、本地化文案与注入的压缩回调。
1274
+ * @returns 工具行左组右端的图标按钮与确认气泡。
1275
+ */
1276
+ function CompactContextButton(props) {
1277
+ const running = props.useSession((snapshot) => snapshot.running);
1278
+ const [open, setOpen] = (0, react.useState)(false);
1279
+ const [pending, setPending] = (0, react.useState)(false);
1280
+ const rootRef = (0, react.useRef)(null);
1281
+ const panelRef = (0, react.useRef)(null);
1282
+ const alive = (0, react.useRef)(true);
1283
+ const position = (0, _deepseek_ai_dsh_client_ui_primitives.useAnchoredPosition)({
1284
+ open,
1285
+ anchorRef: rootRef,
1286
+ panelRef,
1287
+ side: "top",
1288
+ gap: POPOVER_GAP,
1289
+ margin: POPOVER_MARGIN
1290
+ });
1291
+ (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, setOpen, panelRef);
1292
+ (0, react.useEffect)(() => {
1293
+ alive.current = true;
1294
+ return () => {
1295
+ alive.current = false;
1296
+ };
1297
+ }, []);
1298
+ (0, react.useEffect)(() => {
1299
+ if (running) setOpen(false);
1300
+ }, [running]);
1301
+ (0, react.useEffect)(() => {
1302
+ if (!open) return;
1303
+ const onKeyDown = (event) => {
1304
+ if (event.key === "Escape") setOpen(false);
1305
+ };
1306
+ document.addEventListener("keydown", onKeyDown);
1307
+ return () => {
1308
+ document.removeEventListener("keydown", onKeyDown);
1309
+ };
1310
+ }, [open]);
1311
+ const compact = () => {
1312
+ if (pending) return;
1313
+ setOpen(false);
1314
+ setPending(true);
1315
+ const settle = () => {
1316
+ if (alive.current) setPending(false);
1317
+ };
1318
+ props.compact().then(settle, settle);
1319
+ };
1320
+ const disabled = running || pending;
1321
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1322
+ ref: rootRef,
1323
+ className: CompactContextButton_module_css_default.root,
1324
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
1325
+ label: props.t("compactButton.action"),
1326
+ side: "top",
1327
+ delayMs: 500,
1328
+ disabled: open,
1329
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1330
+ type: "button",
1331
+ className: CompactContextButton_module_css_default.button,
1332
+ "aria-label": props.t("compactButton.action"),
1333
+ "aria-haspopup": "dialog",
1334
+ "aria-expanded": open,
1335
+ disabled,
1336
+ onMouseDown: (event) => {
1337
+ event.preventDefault();
1338
+ },
1339
+ onClick: () => {
1340
+ setOpen((value) => !value);
1341
+ },
1342
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompressGlyph, {})
1343
+ })
1344
+ }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1345
+ ref: panelRef,
1346
+ className: CompactContextButton_module_css_default.popover,
1347
+ role: "dialog",
1348
+ "aria-label": props.t("compactButton.confirmTitle"),
1349
+ style: position ?? MEASURE_STYLE,
1350
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1351
+ className: CompactContextButton_module_css_default.popoverText,
1352
+ children: props.t("compactButton.confirmText")
1353
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1354
+ variant: "primary",
1355
+ size: "sm",
1356
+ disabled,
1357
+ onClick: compact,
1358
+ children: props.t("compactButton.confirm")
1359
+ })]
1360
+ }), document.body)]
1361
+ });
1362
+ }
1363
+ //#endregion
1364
+ //#region src/client/compact-button/index.ts
1365
+ /**
1366
+ * 注册压缩上下文按钮到 conversation.input.left。
1367
+ * @param ctx - 需已提供 slots 与 remote.commands 的客户端上下文。
1368
+ * @returns 停止注入并卸载注册的 disposer。
1369
+ */
1370
+ function installCompactButton(ctx) {
1371
+ return ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
1372
+ name: "conversation.input.left",
1373
+ id: "compact-button",
1374
+ order: 100,
1375
+ locale: "ui-fortifier",
1376
+ inject: (sessionId) => ({ compact: async () => {
1377
+ await ctx.remote.commands.execute(sessionId, "/compact", []);
1378
+ } })
1379
+ }, CompactContextButton));
1380
+ }
1381
+ //#endregion
1109
1382
  //#region src/client/index.ts
1110
1383
  /** 功能模块清单:字段名(settings 开关 key) → 模块安装面。 */
1111
1384
  const modules = {
@@ -1124,10 +1397,20 @@ window.__ModuleLoader__.load({
1124
1397
  "model-picker": {
1125
1398
  module: installModelPicker,
1126
1399
  dispose: void 0
1400
+ },
1401
+ "session-id-copy": {
1402
+ module: installSessionIdCopy,
1403
+ dispose: void 0
1404
+ },
1405
+ "compact-button": {
1406
+ module: installCompactButton,
1407
+ dispose: void 0
1127
1408
  }
1128
1409
  };
1129
- /** 设置页开关行的显示顺序(按位置排序);不在表中的字段排在其后。 */
1410
+ /** 设置页开关行的显示顺序:按各模块在界面上的位置,从会话标题栏到输入行再到设置页。 */
1130
1411
  const MODULE_ORDER = [
1412
+ "session-id-copy",
1413
+ "compact-button",
1131
1414
  "provider-label",
1132
1415
  "model-picker",
1133
1416
  "open-dsh-folder",
@@ -1141,7 +1424,8 @@ window.__ModuleLoader__.load({
1141
1424
  "connection",
1142
1425
  "sessions",
1143
1426
  "remote",
1144
- "remote.session"
1427
+ "remote.session",
1428
+ "remote.commands"
1145
1429
  ];
1146
1430
  /**
1147
1431
  * 客户端插件体:注册设置页并据开关状态挂载各功能模块。
package/lib/index.js CHANGED
@@ -161,7 +161,9 @@ const Config = z.object({
161
161
  "provider-label": z.boolean().default(true),
162
162
  "open-dsh-folder": z.boolean().default(true),
163
163
  "settings-frame": z.boolean().default(true),
164
- "model-picker": z.boolean().default(true)
164
+ "model-picker": z.boolean().default(true),
165
+ "session-id-copy": z.boolean().default(true),
166
+ "compact-button": z.boolean().default(true)
165
167
  });
166
168
  /**
167
169
  * 注册功能开关的 settings 命名空间并暴露打开 .dsh 文件夹的 Remote 服务。
@@ -0,0 +1,16 @@
1
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
+ /** 注入面:提交一次压缩指令。 */
3
+ export interface CompactContextButtonInjected {
4
+ /** 提交 `/compact` 指令;指令生命周期由 host 写入会话流。 */
5
+ compact: () => Promise<void>;
6
+ }
7
+ /** 槽位 owner 参数、本地化文案与注入回调。 */
8
+ export type CompactContextButtonProps = PropsRuntime<'conversation.input.left'> & PropsLocale<'ui-fortifier'> & InjectFace<CompactContextButtonInjected>;
9
+ /**
10
+ * 渲染压缩上下文按钮:点击先弹出确认气泡,气泡内确认后才提交 `/compact` 指令;
11
+ * 点气泡外或按 Escape 取消。回合运行中与提交等待期间按钮禁用。
12
+ * @param props - 会话标准套件、本地化文案与注入的压缩回调。
13
+ * @returns 工具行左组右端的图标按钮与确认气泡。
14
+ */
15
+ export declare function CompactContextButton(props: CompactContextButtonProps): import("react").JSX.Element;
16
+ //# sourceMappingURL=CompactContextButton.d.ts.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * compact-button 功能:在输入框工具行左组右端注册压缩上下文按钮。
3
+ */
4
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
5
+ /**
6
+ * 注册压缩上下文按钮到 conversation.input.left。
7
+ * @param ctx - 需已提供 slots 与 remote.commands 的客户端上下文。
8
+ * @returns 停止注入并卸载注册的 disposer。
9
+ */
10
+ export declare function installCompactButton(ctx: ClientContext): () => void;
11
+ //# sourceMappingURL=index.d.ts.map
@@ -10,7 +10,7 @@ interface ModuleEntry {
10
10
  }
11
11
  /** 功能模块清单:字段名(settings 开关 key) → 模块安装面。 */
12
12
  export declare const modules: Record<keyof Config, ModuleEntry>;
13
- /** 设置页开关行的显示顺序(按位置排序);不在表中的字段排在其后。 */
13
+ /** 设置页开关行的显示顺序:按各模块在界面上的位置,从会话标题栏到输入行再到设置页。 */
14
14
  export declare const MODULE_ORDER: readonly (keyof Config)[];
15
15
  export declare const inject: string[];
16
16
  /**
@@ -1,6 +1,6 @@
1
1
  /** dsh-ui-fortifier 的文案。 */
2
2
  /** locale key。 */
3
- export type UiFortifierLocaleKey = 'nav' | 'empty' | 'config.provider-label' | 'config.open-dsh-folder' | 'config.settings-frame' | 'config.model-picker' | 'openDshFolder' | 'openDshFolder.error' | 'openDshFolder.pathLabel' | 'settingsFrame.drag' | 'settingsFrame.resize' | 'modelPicker.trigger' | 'modelPicker.searchPlaceholder' | 'modelPicker.providers' | 'modelPicker.models' | 'modelPicker.emptyProviders' | 'modelPicker.emptySearch' | 'modelPicker.emptyModels' | 'modelPicker.loading' | 'modelPicker.loadError' | 'modelPicker.retry' | 'modelPicker.provider' | 'modelPicker.model';
3
+ export type UiFortifierLocaleKey = 'nav' | 'empty' | 'config.provider-label' | 'config.open-dsh-folder' | 'config.settings-frame' | 'config.model-picker' | 'config.session-id-copy' | 'config.compact-button' | 'openDshFolder' | 'openDshFolder.error' | 'openDshFolder.pathLabel' | 'settingsFrame.drag' | 'settingsFrame.resize' | 'modelPicker.trigger' | 'modelPicker.searchPlaceholder' | 'modelPicker.providers' | 'modelPicker.models' | 'modelPicker.emptyProviders' | 'modelPicker.emptySearch' | 'modelPicker.emptyModels' | 'modelPicker.loading' | 'modelPicker.loadError' | 'modelPicker.retry' | 'modelPicker.provider' | 'modelPicker.model' | 'sessionIdCopy.action' | 'sessionIdCopy.hint' | 'sessionIdCopy.copied' | 'compactButton.action' | 'compactButton.confirmTitle' | 'compactButton.confirmText' | 'compactButton.confirm';
4
4
  /** 英文文案。 */
5
5
  export declare const en: Record<UiFortifierLocaleKey, string>;
6
6
  /** 中文文案。 */
@@ -0,0 +1,11 @@
1
+ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
+ /** 槽位 owner 参数与本地化文案。 */
3
+ export type SessionIdCopyActionProps = PropsRuntime<'conversation.session.header.utilities'> & PropsLocale<'ui-fortifier'>;
4
+ /**
5
+ * 渲染复制 Session ID 的图标按钮:悬停提示直接给出会话 ID,点击写入剪贴板,
6
+ * 成功后按钮短暂换成对勾。
7
+ * @param props - owner 参数(含当前会话 ID)与文案。
8
+ * @returns 会话标题栏工具区的复制按钮。
9
+ */
10
+ export declare function SessionIdCopyAction(props: SessionIdCopyActionProps): import("react").JSX.Element;
11
+ //# sourceMappingURL=SessionIdCopyAction.d.ts.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * session-id-copy 功能:在会话标题栏工具区注册复制会话 ID 的按钮。
3
+ */
4
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
5
+ /**
6
+ * 注册复制会话 ID 按钮到会话标题栏工具区(list 槽,追加式;order -20 排在最左,
7
+ * 位于「在本地打开」按钮之前)。官方三点菜单的菜单项为硬编码、无插槽,
8
+ * 故本功能以相邻按钮而非菜单项实现。
9
+ * @param ctx - 需已提供 slots/locale 的客户端上下文。
10
+ * @returns 停止注入并卸载注册的 disposer。
11
+ */
12
+ export declare function installSessionIdCopy(ctx: ClientContext): () => void;
13
+ //# sourceMappingURL=index.d.ts.map
@@ -12,6 +12,10 @@ export interface Config {
12
12
  'settings-frame': boolean;
13
13
  /** 输入框右侧显示级联模型选择器(先选提供商、再选模型)。 */
14
14
  'model-picker': boolean;
15
+ /** 会话标题栏显示复制会话 ID 按钮。 */
16
+ 'session-id-copy': boolean;
17
+ /** 输入框工具行左组右端显示压缩上下文按钮。 */
18
+ 'compact-button': boolean;
15
19
  }
16
20
  /** 打开 .dsh 文件夹的返回值。 */
17
21
  export interface DshFolderOpenValue {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-ui-fortifier",
3
3
  "description": "UI 强化",
4
- "version": "0.5.2",
4
+ "version": "0.6.0",
5
5
  "license": "MIT",
6
6
  "contributors": [
7
7
  "pgnqukezrdxmhjso"
@@ -44,9 +44,9 @@
44
44
  "lib/types/**/*.d.ts"
45
45
  ],
46
46
  "peerDependencies": {
47
- "@deepseek-ai/dsh-native-command": "^0.1.5-rc.2",
48
- "@deepseek-ai/dsh-home-paths": "^0.1.5-rc.2",
49
47
  "@deepseek-ai/cordis": "^4.0.2",
48
+ "@deepseek-ai/dsh-home-paths": "^0.1.5-rc.2",
49
+ "@deepseek-ai/dsh-native-command": "^0.1.5-rc.2",
50
50
  "@deepseek-ai/dsh-typert-protocol": "^0.1.5-rc.2",
51
51
  "@deepseek-ai/schemastery": "^3.18.2"
52
52
  },
@@ -55,23 +55,24 @@
55
55
  "@types/react-dom": "~18.3.0",
56
56
  "react": "^18.2.0",
57
57
  "react-dom": "^18.2.0",
58
- "@deepseek-ai/cordis": "^4.0.2",
59
58
  "@deepseek-ai/dsh-api-remotes": "^0.1.5-rc.2",
60
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
59
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.2",
60
+ "@deepseek-ai/cordis": "^4.0.2",
61
61
  "@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2",
62
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
62
63
  "@deepseek-ai/dsh-client-ui-model-selection": "^0.1.5-rc.2",
63
64
  "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
64
- "@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.2",
65
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
66
65
  "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
67
66
  "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
68
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
69
67
  "@deepseek-ai/dsh-home-paths": "^0.1.5-rc.2",
70
- "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
68
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
69
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
70
+ "@deepseek-ai/dsh-client-ui-session": "^0.1.5-rc.2",
71
71
  "@deepseek-ai/dsh-native-command": "^0.1.5-rc.2",
72
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
73
72
  "@deepseek-ai/dsh-typert-protocol": "^0.1.5-rc.2",
74
- "@deepseek-ai/schemastery": "^3.18.2"
73
+ "@deepseek-ai/schemastery": "^3.18.2",
74
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
75
+ "@deepseek-ai/dsh-settings": "^0.1.5-rc.2"
75
76
  },
76
77
  "dsh": {
77
78
  "bundle": {
@@ -98,7 +99,7 @@
98
99
  "pub": "pnpm run build && pnpm login --registry=https://registry.npmjs.org && pnpm publish --registry=https://registry.npmjs.org --no-git-checks",
99
100
  "pack": "pnpm run build && pnpm pack --pack-destination lib",
100
101
  "pack-test-i": "pnpm run pack && (pnpm dsh plugin --profile pkgtest remove %npm_package_name% & pnpm dsh plugin --profile pkgtest add ./lib/%npm_package_name%-%npm_package_version%.tgz)",
101
- "npm-test-i": "pnpm dsh plugin --profile pkgtest remove %npm_package_name% & pnpm dsh plugin --profile pkgtest add %npm_package_name%@%npm_package_version%",
102
+ "npm-test-i": "pnpm dsh plugin --profile pkgtest remove %npm_package_name% & pnpm dsh plugin --profile pkgtest add --registry=https://registry.npmjs.org %npm_package_name%@%npm_package_version%",
102
103
  "test-create": "pnpm dsh --profile pkgtest --from-default-profile web",
103
104
  "test-start": "pnpm dsh --profile pkgtest --port 3081 --no-open"
104
105
  }