dsh-ui-fortifier 0.1.2 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -5,25 +5,32 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let react = require("react");
10
+ let react_dom = require("react-dom");
8
11
  //#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\settings\FortifierSettingsPage.module.css.mjs
9
- const css$1 = ".t1_2kq_page{flex-direction:column;gap:8px;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;gap:4px;margin:0;padding:0;list-style:none;display:flex}.t1_2kq_row{background:var(--dsw-alias-bg-layer-2);border-radius:6px;align-items:center;gap:8px;padding:8px;display:flex}.t1_2kq_row label{cursor:pointer;color:var(--dsw-alias-label-primary);align-items:center;gap:8px;font-size:13px;line-height:20px;display:flex}";
10
- const tagId$1 = "dsh-ui-fortifier/FortifierSettingsPage.module.css";
11
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
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
15
  const tag = document.createElement("style");
13
16
  tag.dataset.plugin = "dsh-ui-fortifier";
14
- tag.dataset.pluginCss = tagId$1;
15
- tag.textContent = css$1;
17
+ tag.dataset.pluginCss = tagId$4;
18
+ tag.textContent = css$4;
16
19
  document.head.appendChild(tag);
17
20
  }
18
21
  var FortifierSettingsPage_module_css_default = {
19
22
  "heading": "t1_2kq_heading",
20
23
  "list": "t1_2kq_list",
21
24
  "page": "t1_2kq_page",
22
- "row": "t1_2kq_row"
25
+ "row": "t1_2kq_row",
26
+ "rowLabel": "t1_2kq_rowLabel",
27
+ "switch": "t1_2kq_switch",
28
+ "switchOn": "t1_2kq_switchOn",
29
+ "thumb": "t1_2kq_thumb"
23
30
  };
24
31
  //#endregion
25
32
  //#region src/client/settings/FortifierSettingsPage.tsx
26
- /** 设置页:读取配置项自动生成开关行。 */
33
+ /** 设置页:读取配置项自动生成切换按钮行。 */
27
34
  function FortifierSettingsPage(props) {
28
35
  const t = props.t;
29
36
  const rows = props.useToggles((value) => value);
@@ -34,16 +41,26 @@ window.__ModuleLoader__.load({
34
41
  children: t("nav")
35
42
  }), rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("empty") }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
36
43
  className: FortifierSettingsPage_module_css_default.list,
37
- children: rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
38
- className: FortifierSettingsPage_module_css_default.row,
39
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
40
- type: "checkbox",
41
- checked: row.value,
42
- onChange: (event) => {
43
- props.set(row.field, event.target.checked);
44
- }
45
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`config.${row.field}`) })] })
46
- }, row.field))
44
+ children: rows.map((row) => {
45
+ const label = t(`config.${row.field}`);
46
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
47
+ className: FortifierSettingsPage_module_css_default.row,
48
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49
+ className: FortifierSettingsPage_module_css_default.rowLabel,
50
+ children: label
51
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
52
+ type: "button",
53
+ role: "switch",
54
+ "aria-checked": row.value,
55
+ "aria-label": label,
56
+ className: `${FortifierSettingsPage_module_css_default.switch} ${row.value ? FortifierSettingsPage_module_css_default.switchOn : ""}`,
57
+ onClick: () => {
58
+ props.set(row.field, !row.value);
59
+ },
60
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: FortifierSettingsPage_module_css_default.thumb })
61
+ })]
62
+ }, row.field);
63
+ })
47
64
  })]
48
65
  });
49
66
  }
@@ -53,13 +70,53 @@ window.__ModuleLoader__.load({
53
70
  const en = {
54
71
  nav: "ui-fortifier",
55
72
  empty: "No feature modules registered.",
56
- "config.provider-label": "Show provider in input"
73
+ "config.provider-label": "Input - Provider",
74
+ "config.open-dsh-folder": "Settings top-right - Open .dsh folder",
75
+ "config.settings-frame": "Settings panel - Drag and resize",
76
+ "config.model-picker": "Input - Cascading model picker",
77
+ openDshFolder: "Open .dsh folder",
78
+ "openDshFolder.error": "Could not open the .dsh folder",
79
+ "openDshFolder.pathLabel": "Path: ",
80
+ "settingsFrame.drag": "Drag the settings panel",
81
+ "settingsFrame.resize": "Resize the settings panel",
82
+ "modelPicker.trigger": "Open cascading model picker",
83
+ "modelPicker.searchPlaceholder": "Search providers or models",
84
+ "modelPicker.providers": "Providers",
85
+ "modelPicker.models": "Models",
86
+ "modelPicker.emptyProviders": "No providers loaded.",
87
+ "modelPicker.emptySearch": "No matching providers or models.",
88
+ "modelPicker.emptyModels": "No models for this provider.",
89
+ "modelPicker.loading": "Loading…",
90
+ "modelPicker.loadError": "Failed to load models",
91
+ "modelPicker.retry": "Retry",
92
+ "modelPicker.provider": "Provider: {name}",
93
+ "modelPicker.model": "Model: {name}"
57
94
  };
58
95
  /** 中文文案。 */
59
96
  const zh = {
60
97
  nav: "ui-fortifier",
61
98
  empty: "没有已注册的功能模块。",
62
- "config.provider-label": "输入框显示提供商"
99
+ "config.provider-label": "输入框 - 提供商",
100
+ "config.open-dsh-folder": "设置右上角 - 打开 .dsh 文件夹",
101
+ "config.settings-frame": "设置面板 - 拖动与缩放",
102
+ "config.model-picker": "输入框 - 级联模型选择器",
103
+ openDshFolder: "打开 .dsh 文件夹",
104
+ "openDshFolder.error": "无法打开 .dsh 文件夹",
105
+ "openDshFolder.pathLabel": "路径:",
106
+ "settingsFrame.drag": "拖动设置面板",
107
+ "settingsFrame.resize": "缩放设置面板",
108
+ "modelPicker.trigger": "打开级联模型选择器",
109
+ "modelPicker.searchPlaceholder": "搜索提供商或模型",
110
+ "modelPicker.providers": "提供商",
111
+ "modelPicker.models": "模型",
112
+ "modelPicker.emptyProviders": "没有已加载的提供商。",
113
+ "modelPicker.emptySearch": "没有匹配的提供商或模型。",
114
+ "modelPicker.emptyModels": "该提供商没有模型。",
115
+ "modelPicker.loading": "加载中…",
116
+ "modelPicker.loadError": "模型加载失败",
117
+ "modelPicker.retry": "重试",
118
+ "modelPicker.provider": "提供商:{name}",
119
+ "modelPicker.model": "模型:{name}"
63
120
  };
64
121
  //#endregion
65
122
  //#region src/client/settings/index.ts
@@ -68,10 +125,10 @@ window.__ModuleLoader__.load({
68
125
  /**
69
126
  * 注册设置页并绑定功能开关的 settings 作用域。
70
127
  * @param ctx - 需已提供 slots/locale/settingsScope 的客户端上下文。
71
- * @param moduleFields - 功能模块字段列表(settings 开关 key),由调用方传入。
128
+ * @param moduleOrder - 功能模块字段的显示顺序(settings 开关 key)。
72
129
  * @returns 功能开关的 settings 绑定,供调用方控制子模块注册。
73
130
  */
74
- function installSettings(ctx, moduleFields) {
131
+ function installSettings(ctx, moduleOrder) {
75
132
  ctx.effect(() => ctx.locale.register(UI_FORTIFIER, {
76
133
  zh,
77
134
  en
@@ -86,7 +143,7 @@ window.__ModuleLoader__.load({
86
143
  hooks: { toggles: {
87
144
  getSnapshot: () => {
88
145
  const value = view()?.value ?? {};
89
- const rows = moduleFields.map((field) => ({
146
+ const rows = moduleOrder.map((field) => ({
90
147
  field,
91
148
  value: value[field] ?? true
92
149
  }));
@@ -123,13 +180,13 @@ window.__ModuleLoader__.load({
123
180
  }
124
181
  //#endregion
125
182
  //#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\provider-label\ProviderLabel.module.css.mjs
126
- const css = ".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}";
127
- const tagId = "dsh-ui-fortifier/ProviderLabel.module.css";
128
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
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) {
129
186
  const tag = document.createElement("style");
130
187
  tag.dataset.plugin = "dsh-ui-fortifier";
131
- tag.dataset.pluginCss = tagId;
132
- tag.textContent = css;
188
+ tag.dataset.pluginCss = tagId$3;
189
+ tag.textContent = css$3;
133
190
  document.head.appendChild(tag);
134
191
  }
135
192
  var ProviderLabel_module_css_default = { "label": "LjXNpG_label" };
@@ -160,24 +217,938 @@ window.__ModuleLoader__.load({
160
217
  }, ProviderLabel));
161
218
  }
162
219
  //#endregion
220
+ //#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) {
224
+ const tag = document.createElement("style");
225
+ tag.dataset.plugin = "dsh-ui-fortifier";
226
+ tag.dataset.pluginCss = tagId$2;
227
+ tag.textContent = css$2;
228
+ document.head.appendChild(tag);
229
+ }
230
+ var OpenDshFolderAction_module_css_default = {
231
+ "action": "_8KsKXW_action",
232
+ "error": "_8KsKXW_error"
233
+ };
234
+ //#endregion
235
+ //#region src/client/open-dsh-folder/OpenDshFolderAction.tsx
236
+ /**
237
+ * 渲染「打开 .dsh 文件夹」操作,点击后请求 Host 打开 .dsh 目录。
238
+ * @param props - header owner 参数、本地化文案与注入的打开回调。
239
+ * @returns 操作按钮及其结果提示。
240
+ */
241
+ function OpenDshFolderAction(props) {
242
+ const { openDshFolder, t } = props;
243
+ const [state, setState] = (0, react.useState)({ kind: "idle" });
244
+ const open = async () => {
245
+ setState({ kind: "opening" });
246
+ const result = await openDshFolder();
247
+ if (!result.ok) {
248
+ setState({ kind: "error" });
249
+ return;
250
+ }
251
+ setState(result.value.opened ? { kind: "idle" } : {
252
+ kind: "path",
253
+ path: result.value.path
254
+ });
255
+ };
256
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
257
+ className: OpenDshFolderAction_module_css_default.action,
258
+ children: [
259
+ state.kind === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
260
+ className: OpenDshFolderAction_module_css_default.error,
261
+ role: "alert",
262
+ children: t("openDshFolder.error")
263
+ }) : null,
264
+ state.kind === "path" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
265
+ className: OpenDshFolderAction_module_css_default.error,
266
+ role: "alert",
267
+ children: [t("openDshFolder.pathLabel"), state.path]
268
+ }) : null,
269
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
270
+ variant: "outline",
271
+ size: "sm",
272
+ disabled: state.kind === "opening",
273
+ onClick: () => {
274
+ open();
275
+ },
276
+ children: t("openDshFolder")
277
+ })
278
+ ]
279
+ });
280
+ }
281
+ //#endregion
282
+ //#region src/client/open-dsh-folder/index.ts
283
+ /** 本模块文案命名空间(与 host 侧一致)。 */
284
+ const NS$1 = "ui-fortifier";
285
+ /**
286
+ * 注册「打开 .dsh 文件夹」到设置页右上角操作区,仅在回环连接下注册。
287
+ * @param ctx - 需已提供 slots/connection 的客户端上下文。
288
+ * @returns 停止注入并卸载注册的 disposer。
289
+ */
290
+ function installOpenDshFolder(ctx) {
291
+ const connection = ctx.get("connection");
292
+ if (!connection.isLoopback) return () => {};
293
+ const openDshFolder = () => connection.rpc.call("/api", "uiFortifier/openDshFolder", { args: {} });
294
+ return ctx.slots.inject("settings.action", () => ctx.slots.register({
295
+ name: "settings.action",
296
+ id: "open-dsh-folder",
297
+ order: 1,
298
+ locale: NS$1,
299
+ inject: () => ({ openDshFolder })
300
+ }, OpenDshFolderAction));
301
+ }
302
+ //#endregion
303
+ //#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) {
307
+ const tag = document.createElement("style");
308
+ tag.dataset.plugin = "dsh-ui-fortifier";
309
+ tag.dataset.pluginCss = tagId$1;
310
+ tag.textContent = css$1;
311
+ document.head.appendChild(tag);
312
+ }
313
+ var SettingsFrameEnhancer_module_css_default = {
314
+ "dragHandle": "hB-7Ba_dragHandle",
315
+ "gripIcon": "hB-7Ba_gripIcon",
316
+ "hostAnchor": "hB-7Ba_hostAnchor",
317
+ "resizeHandle": "hB-7Ba_resizeHandle"
318
+ };
319
+ //#endregion
320
+ //#region src/client/settings-frame/SettingsFrameEnhancer.tsx
321
+ /**
322
+ * settings-frame 功能:让设置面板可拖动位置与缩放大小。
323
+ * 组件注册在 settings.action 槽(作为宿主获得面板生命周期),两个手柄均经 portal
324
+ * 挂载:拖动手柄进左上角标题行,缩放柄进面板右下角;面板改 fixed 定位后仅改内联
325
+ * 几何,关闭时恢复由 CSS 负责的默认居中尺寸。
326
+ */
327
+ /** 面板最小宽高,防止缩放过小无法操作。 */
328
+ const MIN_WIDTH = 480;
329
+ const MIN_HEIGHT = 320;
330
+ /** localStorage 中面板几何的存储键。 */
331
+ const FRAME_STORAGE_KEY = "dsh.ui-fortifier.settingsFrame";
332
+ /** 读取持久化的面板几何;缺失或损坏返回 undefined。 */
333
+ function readFrameGeometry() {
334
+ const raw = localStorage.getItem(FRAME_STORAGE_KEY);
335
+ if (raw === null) return void 0;
336
+ try {
337
+ const parsed = JSON.parse(raw);
338
+ 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;
339
+ return;
340
+ } catch {
341
+ return;
342
+ }
343
+ }
344
+ /** 写回持久化的面板几何。 */
345
+ function writeFrameGeometry(geometry) {
346
+ localStorage.setItem(FRAME_STORAGE_KEY, JSON.stringify(geometry));
347
+ }
348
+ /** 读取面板当前几何并持久化(拖动/缩放结束后调用)。 */
349
+ function persistGeometry(panel) {
350
+ writeFrameGeometry({
351
+ left: panel.offsetLeft,
352
+ top: panel.offsetTop,
353
+ width: panel.offsetWidth,
354
+ height: panel.offsetHeight
355
+ });
356
+ }
357
+ /** 将面板几何钳制在视口内(左上角不小于 0,右下角不越出视口)。 */
358
+ function constrainPanel(panel) {
359
+ const vw = window.innerWidth;
360
+ const vh = window.innerHeight;
361
+ panel.style.left = `${clamp(panel.offsetLeft, 0, Math.max(0, vw - panel.offsetWidth))}px`;
362
+ panel.style.top = `${clamp(panel.offsetTop, 0, Math.max(0, vh - panel.offsetHeight))}px`;
363
+ }
364
+ /** 窗口尺寸变化时把面板收进视口:先收窄超出视口的宽高(不低于最小尺寸,窗口小于最小尺寸时随窗口),再钳制位置。 */
365
+ function constrainToViewport(panel) {
366
+ const vw = window.innerWidth;
367
+ const vh = window.innerHeight;
368
+ const width = clamp(panel.offsetWidth, Math.min(MIN_WIDTH, vw), vw);
369
+ const height = clamp(panel.offsetHeight, Math.min(MIN_HEIGHT, vh), vh);
370
+ panel.style.width = `${width}px`;
371
+ panel.style.height = `${height}px`;
372
+ panel.style.left = `${clamp(panel.offsetLeft, 0, Math.max(0, vw - width))}px`;
373
+ panel.style.top = `${clamp(panel.offsetTop, 0, Math.max(0, vh - height))}px`;
374
+ }
375
+ /** 将一个数值限制在闭合区间。 */
376
+ function clamp(value, min, max) {
377
+ return Math.min(Math.max(value, min), max);
378
+ }
379
+ /** 六点拖动把手图形(2 列 x 3 行规整点阵)。 */
380
+ function GripDots() {
381
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
382
+ className: SettingsFrameEnhancer_module_css_default.gripIcon,
383
+ viewBox: "0 0 10 14",
384
+ fill: "currentColor",
385
+ "aria-hidden": "true",
386
+ children: [
387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
388
+ cx: "2.5",
389
+ cy: "2.5",
390
+ r: "1.3"
391
+ }),
392
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
393
+ cx: "7.5",
394
+ cy: "2.5",
395
+ r: "1.3"
396
+ }),
397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
398
+ cx: "2.5",
399
+ cy: "7",
400
+ r: "1.3"
401
+ }),
402
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
403
+ cx: "7.5",
404
+ cy: "7",
405
+ r: "1.3"
406
+ }),
407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
408
+ cx: "2.5",
409
+ cy: "11.5",
410
+ r: "1.3"
411
+ }),
412
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
413
+ cx: "7.5",
414
+ cy: "11.5",
415
+ r: "1.3"
416
+ })
417
+ ]
418
+ });
419
+ }
420
+ /**
421
+ * 渲染「拖动位置/大小」手柄。
422
+ * @param props - header owner 参数与本地化文案。
423
+ * @returns 挂入标题行的拖动手柄与挂入面板右下角的缩放柄。
424
+ */
425
+ function SettingsFrameEnhancer(props) {
426
+ const { t } = props;
427
+ const anchorRef = (0, react.useRef)(null);
428
+ const dragHandleRef = (0, react.useRef)(null);
429
+ const resizeHandleRef = (0, react.useRef)(null);
430
+ const [panel, setPanel] = (0, react.useState)(null);
431
+ const [headerRow, setHeaderRow] = (0, react.useState)(null);
432
+ (0, react.useEffect)(() => {
433
+ const anchor = anchorRef.current;
434
+ if (anchor === null) return;
435
+ const dialog = anchor.closest("[role=\"dialog\"]");
436
+ if (!(dialog instanceof HTMLElement)) return;
437
+ const rect = dialog.getBoundingClientRect();
438
+ const saved = readFrameGeometry();
439
+ 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`;
444
+ dialog.style.margin = "0";
445
+ dialog.style.maxWidth = "none";
446
+ constrainToViewport(dialog);
447
+ if (window.innerWidth >= MIN_WIDTH && window.innerHeight >= MIN_HEIGHT) persistGeometry(dialog);
448
+ setPanel(dialog);
449
+ const row = dialog.querySelector("nav")?.firstElementChild;
450
+ setHeaderRow(row instanceof HTMLElement ? row : null);
451
+ let windowResizeFrame = null;
452
+ const onWindowResize = () => {
453
+ if (windowResizeFrame !== null) return;
454
+ windowResizeFrame = requestAnimationFrame(() => {
455
+ windowResizeFrame = null;
456
+ constrainToViewport(dialog);
457
+ if (window.innerWidth >= MIN_WIDTH && window.innerHeight >= MIN_HEIGHT) persistGeometry(dialog);
458
+ });
459
+ };
460
+ window.addEventListener("resize", onWindowResize);
461
+ return () => {
462
+ if (windowResizeFrame !== null) {
463
+ cancelAnimationFrame(windowResizeFrame);
464
+ windowResizeFrame = null;
465
+ }
466
+ window.removeEventListener("resize", onWindowResize);
467
+ dialog.style.position = "";
468
+ dialog.style.left = "";
469
+ dialog.style.top = "";
470
+ dialog.style.width = "";
471
+ dialog.style.height = "";
472
+ dialog.style.margin = "";
473
+ dialog.style.maxWidth = "";
474
+ setPanel(null);
475
+ setHeaderRow(null);
476
+ };
477
+ }, []);
478
+ (0, react.useEffect)(() => {
479
+ const handle = dragHandleRef.current;
480
+ if (handle === null || headerRow === null) return;
481
+ if (headerRow.firstChild === handle) return;
482
+ headerRow.insertBefore(handle, headerRow.firstChild);
483
+ }, [headerRow]);
484
+ const dragState = (0, react.useRef)(null);
485
+ const latest = (0, react.useRef)({
486
+ x: 0,
487
+ y: 0
488
+ });
489
+ const frame = (0, react.useRef)(null);
490
+ const onDragPointerDown = (event) => {
491
+ if (panel === null) return;
492
+ event.preventDefault();
493
+ event.currentTarget.setPointerCapture(event.pointerId);
494
+ dragState.current = {
495
+ offsetX: event.clientX - panel.offsetLeft,
496
+ offsetY: event.clientY - panel.offsetTop
497
+ };
498
+ };
499
+ const onDragPointerMove = (event) => {
500
+ if (dragState.current === null) return;
501
+ latest.current = {
502
+ x: event.clientX,
503
+ y: event.clientY
504
+ };
505
+ frame.current ??= requestAnimationFrame(() => {
506
+ frame.current = null;
507
+ if (panel === null || dragState.current === null) return;
508
+ panel.style.left = `${latest.current.x - dragState.current.offsetX}px`;
509
+ panel.style.top = `${latest.current.y - dragState.current.offsetY}px`;
510
+ constrainPanel(panel);
511
+ });
512
+ };
513
+ const onDragPointerUp = (event) => {
514
+ if (dragState.current === null) return;
515
+ if (frame.current !== null) {
516
+ cancelAnimationFrame(frame.current);
517
+ frame.current = null;
518
+ }
519
+ dragState.current = null;
520
+ event.currentTarget.releasePointerCapture(event.pointerId);
521
+ if (panel === null) return;
522
+ constrainPanel(panel);
523
+ persistGeometry(panel);
524
+ };
525
+ const resizeOrigin = (0, react.useRef)(null);
526
+ const resizeBase = (0, react.useRef)(null);
527
+ const resizeMax = (0, react.useRef)(null);
528
+ const resizeFrame = (0, react.useRef)(null);
529
+ const onResizePointerDown = (event) => {
530
+ if (panel === null) return;
531
+ event.preventDefault();
532
+ event.stopPropagation();
533
+ event.currentTarget.setPointerCapture(event.pointerId);
534
+ resizeOrigin.current = {
535
+ x: event.clientX,
536
+ y: event.clientY
537
+ };
538
+ resizeBase.current = {
539
+ width: panel.offsetWidth,
540
+ height: panel.offsetHeight
541
+ };
542
+ resizeMax.current = {
543
+ width: window.innerWidth - panel.offsetLeft,
544
+ height: window.innerHeight - panel.offsetTop
545
+ };
546
+ };
547
+ const onResizePointerMove = (event) => {
548
+ if (resizeOrigin.current === null || resizeBase.current === null || resizeMax.current === null) return;
549
+ latest.current = {
550
+ x: event.clientX,
551
+ y: event.clientY
552
+ };
553
+ resizeFrame.current ??= requestAnimationFrame(() => {
554
+ resizeFrame.current = null;
555
+ if (panel === null || resizeOrigin.current === null || resizeBase.current === null || resizeMax.current === null) return;
556
+ const width = clamp(resizeBase.current.width + (latest.current.x - resizeOrigin.current.x), MIN_WIDTH, Math.max(MIN_WIDTH, resizeMax.current.width));
557
+ const height = clamp(resizeBase.current.height + (latest.current.y - resizeOrigin.current.y), MIN_HEIGHT, Math.max(MIN_HEIGHT, resizeMax.current.height));
558
+ panel.style.width = `${width}px`;
559
+ panel.style.height = `${height}px`;
560
+ constrainPanel(panel);
561
+ });
562
+ };
563
+ const onResizePointerUp = (event) => {
564
+ if (resizeOrigin.current === null) return;
565
+ if (resizeFrame.current !== null) {
566
+ cancelAnimationFrame(resizeFrame.current);
567
+ resizeFrame.current = null;
568
+ }
569
+ resizeOrigin.current = null;
570
+ resizeBase.current = null;
571
+ event.currentTarget.releasePointerCapture(event.pointerId);
572
+ if (panel !== null) persistGeometry(panel);
573
+ };
574
+ const dragHandle = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
575
+ ref: dragHandleRef,
576
+ type: "button",
577
+ className: SettingsFrameEnhancer_module_css_default.dragHandle,
578
+ title: t("settingsFrame.drag"),
579
+ "aria-label": t("settingsFrame.drag"),
580
+ onPointerDown: onDragPointerDown,
581
+ onPointerMove: onDragPointerMove,
582
+ onPointerUp: onDragPointerUp,
583
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripDots, {})
584
+ });
585
+ const resizeHandle = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
586
+ ref: resizeHandleRef,
587
+ type: "button",
588
+ className: SettingsFrameEnhancer_module_css_default.resizeHandle,
589
+ title: t("settingsFrame.resize"),
590
+ "aria-label": t("settingsFrame.resize"),
591
+ onPointerDown: onResizePointerDown,
592
+ onPointerMove: onResizePointerMove,
593
+ onPointerUp: onResizePointerUp
594
+ });
595
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
596
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
597
+ ref: anchorRef,
598
+ className: SettingsFrameEnhancer_module_css_default.hostAnchor
599
+ }),
600
+ headerRow !== null && (0, react_dom.createPortal)(dragHandle, headerRow),
601
+ panel !== null && (0, react_dom.createPortal)(resizeHandle, panel)
602
+ ] });
603
+ }
604
+ //#endregion
605
+ //#region src/client/settings-frame/index.ts
606
+ /** 本模块文案命名空间(与 host 侧一致)。 */
607
+ const NS = "ui-fortifier";
608
+ /**
609
+ * 注册「拖动位置/大小」增强到设置页右上角操作区。
610
+ * @param ctx - 需已提供 slots 的客户端上下文。
611
+ * @returns 停止注入并卸载注册的 disposer。
612
+ */
613
+ function installSettingsFrame(ctx) {
614
+ const injected = () => ({});
615
+ return ctx.slots.inject("settings.action", () => ctx.slots.register({
616
+ name: "settings.action",
617
+ id: "settings-frame-enhancer",
618
+ order: 2,
619
+ locale: NS,
620
+ inject: injected
621
+ }, SettingsFrameEnhancer));
622
+ }
623
+ //#endregion
624
+ //#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) {
628
+ const tag = document.createElement("style");
629
+ tag.dataset.plugin = "dsh-ui-fortifier";
630
+ tag.dataset.pluginCss = tagId;
631
+ tag.textContent = css;
632
+ document.head.appendChild(tag);
633
+ }
634
+ var ModelPicker_module_css_default = {
635
+ "cell": "AGI7WG_cell",
636
+ "cellActive": "AGI7WG_cellActive",
637
+ "cellSelected": "AGI7WG_cellSelected",
638
+ "check": "AGI7WG_check",
639
+ "columnTitle": "AGI7WG_columnTitle",
640
+ "columns": "AGI7WG_columns",
641
+ "error": "AGI7WG_error",
642
+ "menu": "AGI7WG_menu",
643
+ "modelName": "AGI7WG_modelName",
644
+ "models": "AGI7WG_models",
645
+ "providers": "AGI7WG_providers",
646
+ "retry": "AGI7WG_retry",
647
+ "root": "AGI7WG_root",
648
+ "search": "AGI7WG_search",
649
+ "searchIcon": "AGI7WG_searchIcon",
650
+ "searchInput": "AGI7WG_searchInput",
651
+ "status": "AGI7WG_status",
652
+ "trigger": "AGI7WG_trigger",
653
+ "triggerIcon": "AGI7WG_triggerIcon"
654
+ };
655
+ //#endregion
656
+ //#region src/client/model-picker/ModelPicker.tsx
657
+ /**
658
+ * model-picker 功能:输入框右下角(following provider-label)的级联模型选择器。
659
+ * 左列提供商、右列该提供商的模型;顶部搜索框按名称过滤提供商与模型。
660
+ * 选择写回官方共享的 modelDirectories,因此与官方模型选择按钮/斜杠命令互通。
661
+ */
662
+ /** 名称是否命中搜索关键字。 */
663
+ function matchesQuery(name, query) {
664
+ return query === "" || name.toLowerCase().includes(query);
665
+ }
666
+ /**
667
+ * 在滚动容器内把目标滚进视野。只调整容器自身的 scrollTop,不联动页面滚动,
668
+ * 因此面板浮在 body 上时不会带动背景页面。
669
+ * @param container - 滚动容器,缺失时不处理。
670
+ * @param target - 目标元素,缺失(如已被搜索过滤掉)时不处理。
671
+ */
672
+ function scrollIntoViewWithin(container, target) {
673
+ if (container === null || target === null) return;
674
+ const containerRect = container.getBoundingClientRect();
675
+ const targetRect = target.getBoundingClientRect();
676
+ if (targetRect.top < containerRect.top) {
677
+ container.scrollTop -= containerRect.top - targetRect.top;
678
+ return;
679
+ }
680
+ if (targetRect.bottom > containerRect.bottom) container.scrollTop += targetRect.bottom - containerRect.bottom;
681
+ }
682
+ /**
683
+ * 未定位态:隐藏但按原点参与布局,使首帧 offsetWidth/offsetHeight 为真实尺寸,
684
+ * 定位不存在时也不会闪现在原点(与官方 ModelSelect 同法)。
685
+ */
686
+ const MEASURE_STYLE = {
687
+ visibility: "hidden",
688
+ left: 0,
689
+ top: 0
690
+ };
691
+ /**
692
+ * 面板坐标:右边缘对齐触发按钮右边缘,仅当这样越出视口时才平移回边界内。
693
+ * 官方 useAnchoredPosition 固定以触发器左边缘为起点且无对齐选项,故本地实现。
694
+ * @param open - 面板是否展开;关闭时清空坐标。
695
+ * @param anchorRef - 触发按钮。
696
+ * @param panelRef - 面板,用于量取自身尺寸做视口钳位。
697
+ * @param gap - 面板下边缘与触发器上边缘的间距。
698
+ * @param margin - 面板与视口各边的最小距离。
699
+ * @returns 面板的 fixed 坐标;首次量取前为 null。
700
+ */
701
+ function useRightAlignedMenuPosition(open, anchorRef, panelRef, gap, margin) {
702
+ const [position, setPosition] = (0, react.useState)(null);
703
+ (0, react.useLayoutEffect)(() => {
704
+ if (!open) {
705
+ setPosition(null);
706
+ return;
707
+ }
708
+ const place = () => {
709
+ const rect = anchorRef.current?.getBoundingClientRect();
710
+ if (rect === void 0) return;
711
+ const panel = panelRef.current;
712
+ const width = panel?.offsetWidth ?? 0;
713
+ const height = panel?.offsetHeight ?? 0;
714
+ let left = rect.right - width;
715
+ let top = rect.top - gap - height;
716
+ if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin);
717
+ if (height > 0) top = Math.min(Math.max(top, margin), window.innerHeight - height - margin);
718
+ setPosition({
719
+ left,
720
+ top
721
+ });
722
+ };
723
+ place();
724
+ window.addEventListener("scroll", place, true);
725
+ window.addEventListener("resize", place);
726
+ const panel = panelRef.current;
727
+ let observer = null;
728
+ if (typeof ResizeObserver !== "undefined" && panel !== null) {
729
+ observer = new ResizeObserver(place);
730
+ observer.observe(panel);
731
+ }
732
+ return () => {
733
+ observer?.disconnect();
734
+ window.removeEventListener("scroll", place, true);
735
+ window.removeEventListener("resize", place);
736
+ };
737
+ }, [
738
+ open,
739
+ anchorRef,
740
+ panelRef,
741
+ gap,
742
+ margin
743
+ ]);
744
+ return position;
745
+ }
746
+ /**
747
+ * 渲染级联模型选择器(左列提供商、右列模型)。
748
+ * @param props - owner 参数、文案与注入面。
749
+ * @returns 图标触发器按钮与展开时的两列面板。
750
+ */
751
+ function ModelPicker(props) {
752
+ const t = props.t;
753
+ const state = props.useDirectory((value) => value);
754
+ const [picker, setPicker] = (0, react.useState)({
755
+ open: false,
756
+ provider: null,
757
+ query: "",
758
+ error: null
759
+ });
760
+ const [loading, setLoading] = (0, react.useState)(false);
761
+ const rootRef = (0, react.useRef)(null);
762
+ const triggerRef = (0, react.useRef)(null);
763
+ const menuRef = (0, react.useRef)(null);
764
+ const providersRef = (0, react.useRef)(null);
765
+ const modelsRef = (0, react.useRef)(null);
766
+ const currentProviderRef = (0, react.useRef)(null);
767
+ const currentModelRef = (0, react.useRef)(null);
768
+ const searchRef = (0, react.useRef)(null);
769
+ const positionedRef = (0, react.useRef)(false);
770
+ const pendingCurrentScrollRef = (0, react.useRef)(false);
771
+ const position = useRightAlignedMenuPosition(picker.open, triggerRef, menuRef, 8, 12);
772
+ const groups = state.groups;
773
+ const current = state.current;
774
+ const query = picker.query.trim().toLowerCase();
775
+ const hasModelHit = groups.some((group) => group.models.some((model) => matchesQuery(model.name, query)));
776
+ const visibleGroups = (0, react.useMemo)(() => {
777
+ if (query === "") return groups;
778
+ if (hasModelHit) return groups.filter((group) => group.models.some((model) => matchesQuery(model.name, query)));
779
+ return groups.filter((group) => matchesQuery(group.name, query));
780
+ }, [
781
+ groups,
782
+ query,
783
+ hasModelHit
784
+ ]);
785
+ const activeGroup = visibleGroups.find((group) => group.id === picker.provider) ?? visibleGroups[0];
786
+ const activeProvider = activeGroup?.id ?? null;
787
+ const activeModels = (0, react.useMemo)(() => {
788
+ if (activeGroup === void 0) return [];
789
+ if (query === "") return activeGroup.models;
790
+ if (hasModelHit) return activeGroup.models.filter((model) => matchesQuery(model.name, query));
791
+ return activeGroup.models;
792
+ }, [
793
+ activeGroup,
794
+ query,
795
+ hasModelHit
796
+ ]);
797
+ (0, react.useLayoutEffect)(() => {
798
+ if (!picker.open) {
799
+ positionedRef.current = false;
800
+ pendingCurrentScrollRef.current = false;
801
+ return;
802
+ }
803
+ if (pendingCurrentScrollRef.current) {
804
+ if (currentModelRef.current === null) return;
805
+ scrollIntoViewWithin(modelsRef.current, currentModelRef.current);
806
+ pendingCurrentScrollRef.current = false;
807
+ return;
808
+ }
809
+ if (positionedRef.current) return;
810
+ if (currentProviderRef.current === null && currentModelRef.current === null) return;
811
+ scrollIntoViewWithin(providersRef.current, currentProviderRef.current);
812
+ scrollIntoViewWithin(modelsRef.current, currentModelRef.current);
813
+ positionedRef.current = true;
814
+ }, [
815
+ picker.open,
816
+ activeProvider,
817
+ current?.provider,
818
+ current?.model,
819
+ visibleGroups,
820
+ activeModels
821
+ ]);
822
+ (0, react.useEffect)(() => {
823
+ if (!picker.open) return;
824
+ const closeOutside = (event) => {
825
+ const target = event.target;
826
+ if (rootRef.current?.contains(target) === true) return;
827
+ if (menuRef.current?.contains(target) === true) return;
828
+ setPicker((p) => ({
829
+ ...p,
830
+ open: false
831
+ }));
832
+ };
833
+ document.addEventListener("mousedown", closeOutside);
834
+ return () => {
835
+ document.removeEventListener("mousedown", closeOutside);
836
+ };
837
+ }, [picker.open]);
838
+ const open = () => {
839
+ setPicker((p) => ({
840
+ ...p,
841
+ open: true,
842
+ provider: p.provider ?? current?.provider ?? null
843
+ }));
844
+ props.load();
845
+ setLoading(state.status === "loading" || state.status === "idle");
846
+ queueMicrotask(() => {
847
+ searchRef.current?.focus();
848
+ });
849
+ };
850
+ const close = (restoreFocus = false) => {
851
+ setPicker((p) => ({
852
+ ...p,
853
+ open: false
854
+ }));
855
+ if (restoreFocus) queueMicrotask(() => {
856
+ triggerRef.current?.focus();
857
+ });
858
+ };
859
+ const chooseProvider = (provider) => {
860
+ setPicker((p) => ({
861
+ ...p,
862
+ provider
863
+ }));
864
+ if (provider === current?.provider) {
865
+ if (provider === activeProvider) {
866
+ scrollIntoViewWithin(modelsRef.current, currentModelRef.current);
867
+ pendingCurrentScrollRef.current = false;
868
+ return;
869
+ }
870
+ pendingCurrentScrollRef.current = true;
871
+ return;
872
+ }
873
+ pendingCurrentScrollRef.current = false;
874
+ if (modelsRef.current !== null) modelsRef.current.scrollTop = 0;
875
+ };
876
+ const chooseModel = (modelId) => {
877
+ if (activeProvider === null) return;
878
+ const model = activeGroup?.models.find((m) => m.id === modelId);
879
+ if (model === void 0) return;
880
+ const selection = {
881
+ provider: activeProvider,
882
+ model: modelId,
883
+ ...model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: model.reasoning.defaultEffort }
884
+ };
885
+ if (current?.provider === activeProvider && current.model === modelId) {
886
+ close(true);
887
+ return;
888
+ }
889
+ props.select(selection).then((accepted) => {
890
+ if (accepted) close(true);
891
+ else setPicker((p) => ({
892
+ ...p,
893
+ error: t("modelPicker.loadError")
894
+ }));
895
+ });
896
+ };
897
+ const hasError = state.error !== null || picker.error !== null;
898
+ const shownError = state.error ?? picker.error;
899
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
900
+ ref: rootRef,
901
+ className: ModelPicker_module_css_default.root,
902
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
903
+ ref: triggerRef,
904
+ type: "button",
905
+ className: ModelPicker_module_css_default.trigger,
906
+ "aria-label": t("modelPicker.trigger"),
907
+ "aria-haspopup": "dialog",
908
+ "aria-expanded": picker.open,
909
+ title: t("modelPicker.trigger"),
910
+ onClick: () => {
911
+ if (picker.open) close();
912
+ else open();
913
+ },
914
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
915
+ viewBox: "0 0 16 16",
916
+ className: ModelPicker_module_css_default.triggerIcon,
917
+ "aria-hidden": "true",
918
+ fill: "none",
919
+ children: [
920
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
921
+ x: "2",
922
+ y: "2",
923
+ width: "5",
924
+ height: "5",
925
+ rx: "1.5",
926
+ stroke: "currentColor",
927
+ strokeWidth: "1.5"
928
+ }),
929
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
930
+ x: "9",
931
+ y: "2",
932
+ width: "5",
933
+ height: "5",
934
+ rx: "1.5",
935
+ stroke: "currentColor",
936
+ strokeWidth: "1.5"
937
+ }),
938
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
939
+ x: "2",
940
+ y: "9",
941
+ width: "5",
942
+ height: "5",
943
+ rx: "1.5",
944
+ stroke: "currentColor",
945
+ strokeWidth: "1.5"
946
+ }),
947
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
948
+ x: "9",
949
+ y: "9",
950
+ width: "5",
951
+ height: "5",
952
+ rx: "1.5",
953
+ stroke: "currentColor",
954
+ strokeWidth: "1.5"
955
+ })
956
+ ]
957
+ })
958
+ }), picker.open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
959
+ ref: menuRef,
960
+ className: ModelPicker_module_css_default.menu,
961
+ role: "dialog",
962
+ "aria-label": t("modelPicker.trigger"),
963
+ style: position ?? MEASURE_STYLE,
964
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
965
+ className: ModelPicker_module_css_default.search,
966
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
967
+ className: ModelPicker_module_css_default.searchIcon,
968
+ "aria-hidden": "true",
969
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
970
+ viewBox: "0 0 16 16",
971
+ fill: "none",
972
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
973
+ cx: "7",
974
+ cy: "7",
975
+ r: "4.5",
976
+ stroke: "currentColor",
977
+ strokeWidth: "1.5"
978
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
979
+ d: "M10.5 10.5 14 14",
980
+ stroke: "currentColor",
981
+ strokeWidth: "1.5",
982
+ strokeLinecap: "round"
983
+ })]
984
+ })
985
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
986
+ ref: searchRef,
987
+ className: ModelPicker_module_css_default.searchInput,
988
+ type: "text",
989
+ value: picker.query,
990
+ placeholder: t("modelPicker.searchPlaceholder"),
991
+ onChange: (event) => {
992
+ setPicker((p) => ({
993
+ ...p,
994
+ query: event.target.value
995
+ }));
996
+ },
997
+ "aria-label": t("modelPicker.searchPlaceholder")
998
+ })]
999
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1000
+ className: ModelPicker_module_css_default.columns,
1001
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
1002
+ ref: providersRef,
1003
+ className: ModelPicker_module_css_default.providers,
1004
+ "aria-label": t("modelPicker.providers"),
1005
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1006
+ className: ModelPicker_module_css_default.columnTitle,
1007
+ children: t("modelPicker.providers")
1008
+ }), state.status === "loading" || loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1009
+ className: ModelPicker_module_css_default.status,
1010
+ children: t("modelPicker.loading")
1011
+ }) : groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1012
+ className: ModelPicker_module_css_default.status,
1013
+ children: t("modelPicker.emptyProviders")
1014
+ }) : visibleGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1015
+ className: ModelPicker_module_css_default.status,
1016
+ children: t("modelPicker.emptySearch")
1017
+ }) : visibleGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1018
+ ref: group.id === activeProvider ? currentProviderRef : null,
1019
+ type: "button",
1020
+ className: `${ModelPicker_module_css_default.cell} ${group.id === activeProvider ? ModelPicker_module_css_default.cellActive : ""}`,
1021
+ onClick: () => {
1022
+ chooseProvider(group.id);
1023
+ },
1024
+ children: group.name
1025
+ }, group.id))]
1026
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1027
+ ref: modelsRef,
1028
+ className: ModelPicker_module_css_default.models,
1029
+ "aria-label": t("modelPicker.models"),
1030
+ children: [
1031
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1032
+ className: ModelPicker_module_css_default.columnTitle,
1033
+ children: t("modelPicker.models")
1034
+ }),
1035
+ hasError && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1036
+ className: ModelPicker_module_css_default.error,
1037
+ role: "alert",
1038
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: shownError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1039
+ type: "button",
1040
+ className: ModelPicker_module_css_default.retry,
1041
+ onClick: () => {
1042
+ props.load();
1043
+ setPicker((p) => ({
1044
+ ...p,
1045
+ error: null
1046
+ }));
1047
+ },
1048
+ children: t("modelPicker.retry")
1049
+ })]
1050
+ }),
1051
+ activeProvider === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1052
+ className: ModelPicker_module_css_default.status,
1053
+ children: t("modelPicker.emptyModels")
1054
+ }) : activeModels.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1055
+ className: ModelPicker_module_css_default.status,
1056
+ children: t("modelPicker.emptyModels")
1057
+ }) : activeModels.map((model) => {
1058
+ const selected = current?.provider === activeProvider && current.model === model.id;
1059
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1060
+ ref: selected ? currentModelRef : null,
1061
+ type: "button",
1062
+ className: `${ModelPicker_module_css_default.cell} ${selected ? ModelPicker_module_css_default.cellSelected : ""}`,
1063
+ disabled: state.status === "selecting",
1064
+ onClick: () => {
1065
+ chooseModel(model.id);
1066
+ },
1067
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1068
+ className: ModelPicker_module_css_default.modelName,
1069
+ children: model.name
1070
+ }), selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1071
+ className: ModelPicker_module_css_default.check,
1072
+ "aria-hidden": "true",
1073
+ children: "✓"
1074
+ }) : null]
1075
+ }, model.id);
1076
+ })
1077
+ ]
1078
+ })]
1079
+ })]
1080
+ }), document.body)]
1081
+ });
1082
+ }
1083
+ //#endregion
1084
+ //#region src/client/model-picker/index.ts
1085
+ /**
1086
+ * 注册模型选择器到 conversation.input.right(provider-label 右侧)。
1087
+ * @param ctx - 需已提供 modelDirectories 服务的客户端上下文。
1088
+ * @returns 停止注入并卸载注册的 disposer。
1089
+ */
1090
+ function installModelPicker(ctx) {
1091
+ return ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
1092
+ name: "conversation.input.right",
1093
+ id: "model-picker",
1094
+ order: 1e3,
1095
+ locale: "ui-fortifier",
1096
+ inject: (sessionId) => {
1097
+ const directory = ctx.modelDirectories.directoryFor(sessionId);
1098
+ return {
1099
+ hooks: { directory: directory.store },
1100
+ load: () => {
1101
+ directory.load().catch(() => {});
1102
+ },
1103
+ select: (selection) => directory.select(selection).then(() => true, () => false)
1104
+ };
1105
+ }
1106
+ }, ModelPicker));
1107
+ }
1108
+ //#endregion
163
1109
  //#region src/client/index.ts
164
1110
  /** 功能模块清单:字段名(settings 开关 key) → 模块安装面。 */
165
- const modules = { "provider-label": {
166
- module: installProviderLabel,
167
- dispose: void 0
168
- } };
1111
+ const modules = {
1112
+ "provider-label": {
1113
+ module: installProviderLabel,
1114
+ dispose: void 0
1115
+ },
1116
+ "open-dsh-folder": {
1117
+ module: installOpenDshFolder,
1118
+ dispose: void 0
1119
+ },
1120
+ "settings-frame": {
1121
+ module: installSettingsFrame,
1122
+ dispose: void 0
1123
+ },
1124
+ "model-picker": {
1125
+ module: installModelPicker,
1126
+ dispose: void 0
1127
+ }
1128
+ };
1129
+ /** 设置页开关行的显示顺序(按位置排序);不在表中的字段排在其后。 */
1130
+ const MODULE_ORDER = [
1131
+ "provider-label",
1132
+ "model-picker",
1133
+ "open-dsh-folder",
1134
+ "settings-frame"
1135
+ ];
169
1136
  const inject = [
170
1137
  "slots",
171
1138
  "locale",
172
1139
  "settingsScope",
173
- "modelDirectories"
1140
+ "modelDirectories",
1141
+ "connection",
1142
+ "sessions",
1143
+ "remote",
1144
+ "remote.session"
174
1145
  ];
175
1146
  /**
176
1147
  * 客户端插件体:注册设置页并据开关状态挂载各功能模块。
177
- * @param ctx - 需已提供 slots/locale/settingsScope/modelDirectories 的客户端上下文。
1148
+ * @param ctx - 需已提供 slots/locale/settingsScope/modelDirectories/connection/sessions/remote 的客户端上下文。
178
1149
  */
179
1150
  function apply(ctx) {
180
- const scope = installSettings(ctx, Object.keys(modules));
1151
+ const scope = installSettings(ctx, MODULE_ORDER);
181
1152
  for (const field of Object.keys(modules)) {
182
1153
  const item = modules[field];
183
1154
  const sync = () => {
@@ -191,6 +1162,7 @@ window.__ModuleLoader__.load({
191
1162
  }
192
1163
  }
193
1164
  //#endregion
1165
+ exports.MODULE_ORDER = MODULE_ORDER;
194
1166
  exports.apply = apply;
195
1167
  exports.inject = inject;
196
1168
  exports.modules = modules;