dsh-archify-projects 0.1.0 → 0.2.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
@@ -27,6 +27,31 @@
27
27
 
28
28
  ---
29
29
 
30
+ ## 效果
31
+
32
+ > 截图来自真实运行——被测仓库就是这个插件自己,数据都是跑出来的,不是设计稿。
33
+
34
+ **总览**:每个仓库一张卡片,直接看出分支、工作区改动、概念数、哪些图已经就绪、有没有人在跑。
35
+
36
+ ![总览:仓库卡片、概念数、图状态](https://raw.githubusercontent.com/guicaiyue/dsh-archify-projects/main/docs/verify/shot-atlas.png)
37
+
38
+ **工作台**:图名 tab 在图片上方(只有文字,不占高度),左边是图,右边是检查器——
39
+ prompt、概念范围、分支、进度、token 用量、运行记录都在这一栏。
40
+
41
+ ![工作台:图名 tab、成品图、右侧检查器](https://raw.githubusercontent.com/guicaiyue/dsh-archify-projects/main/docs/verify/shot-workbench.png)
42
+
43
+ **提问**:对着某张图问细节,agent 在**只读会话**里读代码回答,回答可以直接「补进图里」
44
+ 或变成一张子图。
45
+
46
+ ![提问:只读会话基于图和代码回答](https://raw.githubusercontent.com/guicaiyue/dsh-archify-projects/main/docs/verify/shot-qa.png)
47
+
48
+ **新建图**:图型 tag 直接对应 archify 的 5 种渲染器,点一下告诉你「适合看什么 / 会展示
49
+ 代码库的哪些信息 / 能让你掌握什么」;不选也行,由 agent 判断。
50
+
51
+ ![新建图:图型 tag 与说明](https://raw.githubusercontent.com/guicaiyue/dsh-archify-projects/main/docs/verify/shot-dialog.png)
52
+
53
+ ---
54
+
30
55
  ## 安装
31
56
 
32
57
  装的是**已发布的包**,一条命令,装在哪个 profile 就写哪个名字(默认 `web`):
@@ -37,10 +62,12 @@ dsh plugin --profile web add dsh-archify-projects
37
62
 
38
63
  然后重启 dsh(宿主侧代码,必须重启)。重启后左栏出现「架构建模」。
39
64
 
40
- **还没有 npm 包时**,用 GitHub Release 上的预构建 tarball 装(和 npm 包内容一致):
65
+ 也在 npm 上(`dsh-archify-projects@0.1.1`)。
66
+
67
+ **备用路径**:GitHub Release 上的预构建 tarball(和 npm 包内容一致):
41
68
 
42
69
  ```bash
43
- dsh plugin --profile web add https://github.com/guicaiyue/dsh-archify-projects/releases/download/v0.1.0/dsh-archify-projects-0.1.0.tgz
70
+ dsh plugin --profile web add https://github.com/guicaiyue/dsh-archify-projects/releases/download/v0.1.1/dsh-archify-projects-0.1.1.tgz
44
71
  ```
45
72
 
46
73
  > ⚠️ **不要用 `add github:guicaiyue/dsh-archify-projects` 从源码装**:
@@ -71,6 +98,8 @@ pnpm add link:/absolute/path/to/dsh-archify-projects # 或在 package.json 里
71
98
  3. 首次添加会自动跑一次 `create`:结构图 + 领域图各一张,通常几分钟。
72
99
  4. 之后随时「更新」:切回记录的分支 → 快进到最新 → 重画两张图。
73
100
  5. 图是可以直接看的 HTML(archify 产物:可搜索、可缩放、可导出),不是截图。
101
+ 6. 右上角**齿轮**是设置:只有一个「分析模型」下拉,选项就是你在 DSH 里已经配好、
102
+ 当前真能路由的模型;不选=跟随宿主默认。改完对下一次运行生效。
74
103
 
75
104
  ---
76
105
 
@@ -99,6 +128,16 @@ pnpm add link:/absolute/path/to/dsh-archify-projects # 或在 package.json 里
99
128
  `restrict` 只能装在 scoped ctx(`agent.ctx`)上,写在 preset 行里会直接抛 ——
100
129
  所以白名单与守卫在 `src/host/policy.ts` 里,运行开始时对当次会话安装。
101
130
 
131
+ ### 分析跑在子代理会话里,不占你的会话列表
132
+
133
+ 每次出图 / 提问都用一个 **`origin: 'subagent'` 的会话**(`ctx.agents.create` +
134
+ `agentPresets.mount`),所以:
135
+
136
+ - DSH 左侧会话列表按 origin 过滤子代理会话 —— 分析不会往你的对话列表里塞新条目;
137
+ - 会话的模型走 `agentOptions`,**只影响这一次运行**,不会改掉 DSH 里你的默认模型;
138
+ - 宿主版本没有这条路(缺 `agents.create` / `agentPresets`)时安静退回普通会话,
139
+ 工具白名单、preset、产物回收都不变。
140
+
102
141
  ### 深浅色
103
142
 
104
143
  页面只用 DSH 的主题令牌(`--dsw-alias-*` / `--dsw-specific-*` / `--dsw-radius-*` /
@@ -190,6 +229,7 @@ scripts/ artifact 门禁
190
229
  | 位置 | 内容 |
191
230
  | ------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
192
231
  | `~/.dsh/archify-projects/projects.json`(可用 `dataDir` 改) | 台账:项目、Git 快照、两张图的状态、运行历史(保留最近 20 条) |
232
+ | 同目录 `settings.json` | 插件设置:分析用哪个模型(`{ provider, model }`,null = 跟随宿主默认) |
193
233
  | `~/.dsh/archify-projects/workspace/<projectId>/` | 每个项目的工作目录;`.archify/<kind>-<slug>-<stamp>/` 里是 `candidate.json` 与成品 HTML |
194
234
  | `<插件目录>/skills/archify/` | 内嵌的 archify skill(不进 git:见 `.gitignore`;npm 包用 `files` 带上) |
195
235
 
package/dist/client.js CHANGED
@@ -28,6 +28,8 @@ const ROUTES = {
28
28
  mergeQa: (id, qaId) => `${ROUTE_PREFIX}/projects/${encodeURIComponent(id)}/qa/${encodeURIComponent(qaId)}/merge`,
29
29
  childQa: (id, qaId) => `${ROUTE_PREFIX}/projects/${encodeURIComponent(id)}/qa/${encodeURIComponent(qaId)}/child`,
30
30
  runs: (id) => `${ROUTE_PREFIX}/projects/${encodeURIComponent(id)}/runs`,
31
+ /** 插件级设置(目前只有「分析用哪个模型」)。 */
32
+ settings: `${ROUTE_PREFIX}/settings`,
31
33
  artifact: (relativePath) => `${ROUTE_PREFIX}/artifact/${relativePath.split(/[\\/]+/).map((part) => encodeURIComponent(part)).join("/")}`
32
34
  };
33
35
 
@@ -163,7 +165,7 @@ async function api(path, init) {
163
165
  const message = result.error?.message ?? "未知错误";
164
166
  throw new ApiError(code, message);
165
167
  }
166
- /** POST/PATCH 的便捷封装。 */
168
+ /** POST/PUT/PATCH/DELETE 的便捷封装。 */
167
169
  function send(path, method, body) {
168
170
  return api(path, {
169
171
  method,
@@ -171,6 +173,12 @@ function send(path, method, body) {
171
173
  });
172
174
  }
173
175
 
176
+ // src/shared/model-key.ts
177
+ /** 生成路由键。 */
178
+ function modelKey(ref) {
179
+ return `${ref.provider}::${ref.model}`;
180
+ }
181
+
174
182
  // src/client/components.tsx
175
183
  /**
176
184
  * 页面里可复用的小组件:标签、状态点、两个对话框。
@@ -531,6 +539,136 @@ function NewDiagramDialog({ t, project, selectedId, busy, onClose, onSubmit }) {
531
539
  })
532
540
  });
533
541
  }
542
+ /** 齿轮图标(设置入口)。尺寸跟随字号,颜色跟随按钮。 */
543
+ function GearIcon({ size = 13 }) {
544
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
545
+ width: size,
546
+ height: size,
547
+ viewBox: "0 0 24 24",
548
+ fill: "none",
549
+ stroke: "currentColor",
550
+ strokeWidth: 1.7,
551
+ strokeLinecap: "round",
552
+ strokeLinejoin: "round",
553
+ "aria-hidden": "true",
554
+ focusable: "false",
555
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
556
+ cx: "12",
557
+ cy: "12",
558
+ r: "3.1"
559
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M19.4 15a1.6 1.6 0 0 0 .32 1.77l.06.06a1.9 1.9 0 1 1-2.7 2.7l-.06-.06a1.6 1.6 0 0 0-1.77-.32 1.6 1.6 0 0 0-.97 1.47V21a1.9 1.9 0 1 1-3.8 0v-.1a1.6 1.6 0 0 0-1.05-1.47 1.6 1.6 0 0 0-1.77.32l-.06.06a1.9 1.9 0 1 1-2.7-2.7l.06-.06A1.6 1.6 0 0 0 4.6 15a1.6 1.6 0 0 0-1.47-.97H3a1.9 1.9 0 1 1 0-3.8h.1a1.6 1.6 0 0 0 1.47-1.05 1.6 1.6 0 0 0-.32-1.77l-.06-.06a1.9 1.9 0 1 1 2.7-2.7l.06.06A1.6 1.6 0 0 0 9 4.6a1.6 1.6 0 0 0 .97-1.47V3a1.9 1.9 0 1 1 3.8 0v.1a1.6 1.6 0 0 0 .97 1.47 1.6 1.6 0 0 0 1.77-.32l.06-.06a1.9 1.9 0 1 1 2.7 2.7l-.06.06a1.6 1.6 0 0 0-.32 1.77V9a1.6 1.6 0 0 0 1.47.97H21a1.9 1.9 0 1 1 0 3.8h-.1a1.6 1.6 0 0 0-1.47.97z" })]
560
+ });
561
+ }
562
+ /**
563
+ * 设置对话框:**只有一个字段**——分析用哪个模型。
564
+ *
565
+ * 模型选项不是插件编的,是宿主 `modelCatalog()` 列出来的(用户在 DSH 里
566
+ * 配好、当前真的能路由到的那些)。目录拿不到时只留「跟随宿主默认」一项,
567
+ * 并且把原因写在提示里,不假装能选。
568
+ */
569
+ function SettingsDialog({ t, settings, busy, onClose, onSubmit }) {
570
+ const catalog = settings?.catalog ?? null;
571
+ const current = settings?.model ?? null;
572
+ const [chosen, setChosen] = (0, react.useState)(current === null ? "" : modelKey(current));
573
+ const options = catalog?.options ?? [];
574
+ const selectedOption = options.find((option) => option.key === chosen) ?? null;
575
+ const submit = () => {
576
+ onSubmit(selectedOption === null ? null : {
577
+ provider: selectedOption.provider,
578
+ model: selectedOption.model
579
+ });
580
+ };
581
+ const fallbackText = catalog !== null && catalog.fallback !== null ? `${catalog.fallback.provider} / ${catalog.fallback.model}` : null;
582
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
583
+ className: "dap-mask",
584
+ role: "presentation",
585
+ onClick: (e) => e.target === e.currentTarget && onClose(),
586
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
587
+ className: "dap-dialog",
588
+ role: "dialog",
589
+ "aria-modal": "true",
590
+ "aria-label": t("dialog.settings"),
591
+ children: [
592
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
593
+ className: "dap-dialog__title",
594
+ children: t("dialog.settings")
595
+ }),
596
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
597
+ className: "dap-dialog__sub",
598
+ children: t("dialog.settingsSub")
599
+ }),
600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
601
+ className: "dap-dialog__row",
602
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
603
+ className: "dap-field__label",
604
+ children: t("field.model")
605
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
606
+ className: "dap-select",
607
+ value: chosen,
608
+ onChange: (e) => setChosen(e.target.value),
609
+ "aria-label": t("field.model"),
610
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
611
+ value: "",
612
+ children: fallbackText === null ? t("model.followDefault") : t("model.followDefaultNamed", { model: fallbackText })
613
+ }), options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
614
+ value: option.key,
615
+ children: [
616
+ option.providerName,
617
+ " · ",
618
+ option.name
619
+ ]
620
+ }, option.key))]
621
+ })]
622
+ }),
623
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
624
+ className: "dap-hint",
625
+ children: catalog === null || catalog.source === "unavailable" ? t("settings.unavailable") : options.length === 0 ? t("settings.noModels") : t("field.modelHint")
626
+ }),
627
+ selectedOption?.description != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
628
+ className: "dap-hint",
629
+ children: selectedOption.description
630
+ }) : null,
631
+ catalog !== null && catalog.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
632
+ className: "dap-hint dap-hint--warn",
633
+ children: t("settings.providerFailed", { list: catalog.failures.map((f) => f.providerName).join("、") })
634
+ }) : null,
635
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
636
+ className: "dap-dialog__row",
637
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
638
+ className: "dap-field__label",
639
+ children: t("field.sessionMode")
640
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
641
+ className: "dap-hint",
642
+ children: settings?.subagentSessions === true ? t("settings.sessionSubagent") : t("settings.sessionPlain")
643
+ })]
644
+ }),
645
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
646
+ className: "dap-dialog__foot",
647
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
648
+ className: "dap-hint",
649
+ children: t("settings.scopeHint")
650
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
651
+ style: {
652
+ display: "flex",
653
+ gap: 6
654
+ },
655
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
656
+ className: "dap-btn dap-btn--sm",
657
+ onClick: onClose,
658
+ disabled: busy,
659
+ children: t("action.cancel")
660
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
661
+ className: "dap-btn dap-btn--sm dap-btn--primary",
662
+ onClick: submit,
663
+ disabled: busy,
664
+ children: t("action.save")
665
+ })]
666
+ })]
667
+ })
668
+ ]
669
+ })
670
+ });
671
+ }
534
672
 
535
673
  // src/client/page.tsx
536
674
  /**
@@ -557,6 +695,7 @@ function StudioPage({ t }) {
557
695
  const [diagramId, setDiagramId] = (0, react.useState)(null);
558
696
  const [tab, setTab] = (0, react.useState)("info");
559
697
  const [dialog, setDialog] = (0, react.useState)(null);
698
+ const [settings, setSettings] = (0, react.useState)(null);
560
699
  const [busy, setBusy] = (0, react.useState)(null);
561
700
  const [notice, setNotice] = (0, react.useState)(null);
562
701
  const [question, setQuestion] = (0, react.useState)("");
@@ -590,6 +729,18 @@ function StudioPage({ t }) {
590
729
  (0, react.useEffect)(() => {
591
730
  load();
592
731
  }, [load]);
732
+ (0, react.useEffect)(() => {
733
+ let alive = true;
734
+ (async () => {
735
+ try {
736
+ const value = await api(ROUTES.settings);
737
+ if (alive) setSettings(value);
738
+ } catch {}
739
+ })();
740
+ return () => {
741
+ alive = false;
742
+ };
743
+ }, []);
593
744
  const running = state.projects.some((project) => isRunning(project));
594
745
  (0, react.useEffect)(() => {
595
746
  if (!running) return void 0;
@@ -634,6 +785,19 @@ function StudioPage({ t }) {
634
785
  setBusy(null);
635
786
  }
636
787
  };
788
+ const saveSettings = async (model) => {
789
+ setBusy("settings");
790
+ setNotice(null);
791
+ try {
792
+ const value = await send(ROUTES.settings, "PUT", { model });
793
+ if (mounted.current) setSettings(value);
794
+ setDialog(null);
795
+ } catch (err) {
796
+ fail(err);
797
+ } finally {
798
+ setBusy(null);
799
+ }
800
+ };
637
801
  const updateAll = async () => {
638
802
  for (const item of state.projects) {
639
803
  if (isRunning(item)) continue;
@@ -783,14 +947,20 @@ function StudioPage({ t }) {
783
947
  className: "dap-head__sub",
784
948
  children: project.repoPath
785
949
  })
786
- ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
950
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
787
951
  className: "dap-head__actions",
788
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
952
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
953
+ className: "dap-iconbtn",
954
+ onClick: () => setDialog("settings"),
955
+ title: t("action.settings"),
956
+ "aria-label": t("action.settings"),
957
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GearIcon, {})
958
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
789
959
  className: "dap-btn dap-btn--sm dap-btn--primary",
790
960
  onClick: () => void runUpdate(project),
791
961
  disabled: busy !== null || isRunning(project),
792
962
  children: t("action.update")
793
- })
963
+ })]
794
964
  })]
795
965
  }),
796
966
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -818,6 +988,13 @@ function StudioPage({ t }) {
818
988
  onNewDiagram: () => setDialog("diagram")
819
989
  })]
820
990
  }),
991
+ dialog === "settings" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsDialog, {
992
+ t,
993
+ settings,
994
+ busy: busy !== null,
995
+ onClose: () => setDialog(null),
996
+ onSubmit: (model) => void saveSettings(model)
997
+ }) : null,
821
998
  dialog === "diagram" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NewDiagramDialog, {
822
999
  t,
823
1000
  project,
@@ -841,16 +1018,26 @@ function StudioPage({ t }) {
841
1018
  children: t("page.subtitle")
842
1019
  })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
843
1020
  className: "dap-head__actions",
844
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
845
- className: "dap-btn dap-btn--sm",
846
- onClick: () => void updateAll(),
847
- disabled: busy !== null || state.projects.length === 0,
848
- children: t("action.updateAll")
849
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
850
- className: "dap-btn dap-btn--sm dap-btn--primary",
851
- onClick: () => setDialog("project"),
852
- children: ["+ ", t("action.add")]
853
- })]
1021
+ children: [
1022
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1023
+ className: "dap-iconbtn",
1024
+ onClick: () => setDialog("settings"),
1025
+ title: t("action.settings"),
1026
+ "aria-label": t("action.settings"),
1027
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GearIcon, {})
1028
+ }),
1029
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1030
+ className: "dap-btn dap-btn--sm",
1031
+ onClick: () => void updateAll(),
1032
+ disabled: busy !== null || state.projects.length === 0,
1033
+ children: t("action.updateAll")
1034
+ }),
1035
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1036
+ className: "dap-btn dap-btn--sm dap-btn--primary",
1037
+ onClick: () => setDialog("project"),
1038
+ children: ["+ ", t("action.add")]
1039
+ })
1040
+ ]
854
1041
  })]
855
1042
  }),
856
1043
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -938,6 +1125,13 @@ function StudioPage({ t }) {
938
1125
  })] })
939
1126
  ]
940
1127
  }),
1128
+ dialog === "settings" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsDialog, {
1129
+ t,
1130
+ settings,
1131
+ busy: busy !== null,
1132
+ onClose: () => setDialog(null),
1133
+ onSubmit: (model) => void saveSettings(model)
1134
+ }) : null,
941
1135
  dialog === "project" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NewProjectDialog, {
942
1136
  t,
943
1137
  busy: busy !== null,
@@ -1126,7 +1320,8 @@ function Workbench({ t, project, diagram, tab, busy, question, pricing, ago, onT
1126
1320
  className: "dap-iframe",
1127
1321
  title: diagram.title,
1128
1322
  src: ROUTES.artifact(diagram.htmlPath),
1129
- sandbox: "allow-scripts allow-same-origin"
1323
+ sandbox: "allow-scripts allow-same-origin",
1324
+ onLoad: (event) => hideArtifactHeader(event.currentTarget)
1130
1325
  })]
1131
1326
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1132
1327
  className: "dap-frame",
@@ -1416,6 +1611,25 @@ function ThumbStrip({ t, project, selectedId, onSelect, onNewDiagram }) {
1416
1611
  });
1417
1612
  }
1418
1613
  /**
1614
+ * 内嵌查看器里去掉成品自带的页头(大标题 + 状态圆点)。
1615
+ *
1616
+ * 成品 HTML 顶部有一行「<项目名> · 图名」的大标题和一个 pulse-dot,而插件自己的
1617
+ * 查看器标题栏已经写了图名、帧头也写了分支与时间——重复一遍只是白吃画布高度。
1618
+ * 只在 iframe 里注入,点「新窗口」单独打开时仍是成品原样(那里没有外层标题)。
1619
+ * 同源才改得动;被沙箱或跨域挡住就静默跳过,不影响看图。
1620
+ */
1621
+ function hideArtifactHeader(frame) {
1622
+ try {
1623
+ const doc = frame.contentDocument;
1624
+ if (doc === null) return;
1625
+ if (doc.head.querySelector("style[data-embedded=\"compact\"]") !== null) return;
1626
+ const style = doc.createElement("style");
1627
+ style.dataset.embedded = "compact";
1628
+ style.textContent = ".header{display:none!important}";
1629
+ doc.head.appendChild(style);
1630
+ } catch {}
1631
+ }
1632
+ /**
1419
1633
  * 一个图名 tab:只有文字,没有缩略图。
1420
1634
  *
1421
1635
  * 之前用 0.25 倍 iframe 做真预览,一条占 91px,把图片本身压下去;改成纯文字后
@@ -1618,6 +1832,20 @@ const zh = {
1618
1832
  "action.delete": "删除",
1619
1833
  "action.openSession": "查看会话",
1620
1834
  "action.cancel": "取消",
1835
+ "action.settings": "设置",
1836
+ "dialog.settings": "设置",
1837
+ "dialog.settingsSub": "只影响这个插件的分析会话,不动 DSH 里你自己的默认模型。",
1838
+ "field.model": "分析模型",
1839
+ "field.modelHint": "选项来自你在 DSH 里已经配好、当前能路由的模型。",
1840
+ "field.sessionMode": "会话形态",
1841
+ "model.followDefault": "跟随宿主默认",
1842
+ "model.followDefaultNamed": "跟随宿主默认({model})",
1843
+ "settings.noModels": "宿主的模型目录是空的:去 DSH 设置里配一个模型,这里才有得选。",
1844
+ "settings.unavailable": "这个宿主版本没有提供模型目录,只能跟随宿主默认。",
1845
+ "settings.providerFailed": "{list} 的模型列表取不到(网络或凭据),暂时不能选。",
1846
+ "settings.sessionSubagent": "子代理会话(不占用左侧会话列表)",
1847
+ "settings.sessionPlain": "普通会话(宿主不支持子代理会话)",
1848
+ "settings.scopeHint": "保存后对下一次运行生效",
1621
1849
  "action.submit": "创建并分析",
1622
1850
  "action.close": "关闭",
1623
1851
  "action.back": "返回图册",
@@ -1779,6 +2007,20 @@ const en = {
1779
2007
  "action.delete": "Delete",
1780
2008
  "action.openSession": "Open session",
1781
2009
  "action.cancel": "Cancel",
2010
+ "action.settings": "Settings",
2011
+ "dialog.settings": "Settings",
2012
+ "dialog.settingsSub": "Only affects this plugin's analysis sessions; your own default model in DSH stays untouched.",
2013
+ "field.model": "Analysis model",
2014
+ "field.modelHint": "Options are the models already configured in DSH and currently routable.",
2015
+ "field.sessionMode": "Session",
2016
+ "model.followDefault": "Follow the host default",
2017
+ "model.followDefaultNamed": "Follow the host default ({model})",
2018
+ "settings.noModels": "The host catalog is empty. Configure a model in DSH settings to pick one here.",
2019
+ "settings.unavailable": "This host version exposes no model catalog; the host default is used.",
2020
+ "settings.providerFailed": "Could not list models for {list} (network or credentials).",
2021
+ "settings.sessionSubagent": "Subagent session (kept out of the sidebar list)",
2022
+ "settings.sessionPlain": "Plain session (this host lacks subagent sessions)",
2023
+ "settings.scopeHint": "Applies to the next run",
1782
2024
  "action.submit": "Create and analyse",
1783
2025
  "action.close": "Close",
1784
2026
  "action.back": "Back to atlas",
@@ -2170,6 +2412,25 @@ const PAGE_CSS = `
2170
2412
 
2171
2413
 
2172
2414
  .dap-dialog--wide { width: 680px; }
2415
+
2416
+ /* 头部图标按钮(设置齿轮):与 dap-btn--sm 同高,窄一点 */
2417
+ .dap-iconbtn {
2418
+ display: inline-flex; align-items: center; justify-content: center;
2419
+ width: 26px; height: 26px; border-radius: var(--dsw-radius-md);
2420
+ border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2);
2421
+ color: var(--dsw-alias-label-secondary); cursor: pointer;
2422
+ }
2423
+ .dap-iconbtn:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l2); }
2424
+
2425
+ /* 设置弹窗的模型下拉:原生 select 最省事,长列表也能滚动 */
2426
+ .dap-select {
2427
+ min-width: 240px; max-width: 380px; height: 28px; padding: 0 8px;
2428
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-md);
2429
+ background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary);
2430
+ font: inherit; font-size: 12px; outline: none; cursor: pointer;
2431
+ }
2432
+ .dap-select:focus { border-color: var(--dsw-alias-border-l3); }
2433
+ .dap-hint--warn { color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-label-tertiary)); }
2173
2434
  .dap-typebar { display: flex; flex-wrap: wrap; gap: 5px; margin: 6px 0 8px; }
2174
2435
  .dap-type-tag {
2175
2436
  display: inline-flex; align-items: center; gap: 5px; height: 27px; padding: 0 9px;