@dsh-plus/subagent-model 0.3.20 → 0.3.21

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
@@ -12,9 +12,72 @@ window.__ModuleLoader__.load({
12
12
  kind: "idle",
13
13
  text: ""
14
14
  };
15
+ /** 状态行 + 动作按钮(两种形态共用,类名随形态切换度量)。 */
16
+ function CardFoot(props) {
17
+ const p = props.prefix;
18
+ const section = props.variant === "section";
19
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
20
+ className: section ? `${p}-sectionFooter` : `${p}-footer`,
21
+ children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
22
+ className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
23
+ role: "status",
24
+ children: props.status.text
25
+ }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
26
+ type: "button",
27
+ className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
28
+ disabled: action.disabled === true,
29
+ onClick: action.onClick,
30
+ children: action.label
31
+ }, action.key))]
32
+ });
33
+ }
34
+ /** 只读提示 + 字段区 + 页脚(两种形态共用)。 */
35
+ function CardBody(props) {
36
+ const p = props.prefix;
37
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
38
+ props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
39
+ className: `${p}-readOnly`,
40
+ role: "status",
41
+ children: props.readOnlyNotice
42
+ }) : null,
43
+ props.children,
44
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFoot, { ...props })
45
+ ] });
46
+ }
15
47
  function CardChrome(props) {
16
48
  const p = props.prefix;
17
49
  const badge = props.statusBadge;
50
+ if (props.variant === "section") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
51
+ className: `${p}-section`,
52
+ "aria-label": props.title,
53
+ children: [
54
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55
+ className: `${p}-sectionHead`,
56
+ children: [
57
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
58
+ className: `${p}-sectionTitle`,
59
+ children: props.title
60
+ }),
61
+ badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
62
+ className: `${p}-sectionBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
63
+ children: badge.text
64
+ }) : null,
65
+ props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
66
+ className: `${p}-pending`,
67
+ children: props.dirtyLabel
68
+ }) : null
69
+ ]
70
+ }),
71
+ props.description === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
72
+ className: `${p}-sectionDesc`,
73
+ children: props.description
74
+ }),
75
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
76
+ className: `${p}-sectionBody`,
77
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, { ...props })
78
+ })
79
+ ]
80
+ });
18
81
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
19
82
  className: `${p}-card${props.open ? ` ${p}-cardOpen` : ""}`,
20
83
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -44,38 +107,44 @@ window.__ModuleLoader__.load({
44
107
  }) : null,
45
108
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}` })
46
109
  ]
47
- }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
110
+ }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48
111
  className: `${p}-body`,
49
- children: [
50
- props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
51
- className: `${p}-readOnly`,
52
- role: "status",
53
- children: props.readOnlyNotice
54
- }) : null,
55
- props.children,
56
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57
- className: `${p}-footer`,
58
- children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
59
- className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
60
- role: "status",
61
- children: props.status.text
62
- }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
63
- type: "button",
64
- className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
65
- disabled: action.disabled === true,
66
- onClick: action.onClick,
67
- children: action.label
68
- }, action.key))]
69
- })
70
- ]
112
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, { ...props })
71
113
  }) : null]
72
114
  });
73
115
  }
116
+ /** 取值未到达时的占位:与 CardChrome 同形态,避免宿主页里闪一次卡片外壳。 */
117
+ function CardLoading(props) {
118
+ if (props.variant === "section") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
119
+ className: `${props.prefix}-section`,
120
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
121
+ className: `${props.prefix}-readOnly`,
122
+ role: "status",
123
+ children: props.text
124
+ })
125
+ });
126
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
127
+ className: `${props.prefix}-card`,
128
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
129
+ className: `${props.prefix}-readOnly`,
130
+ role: "status",
131
+ children: props.text
132
+ })
133
+ });
134
+ }
74
135
  //#endregion
75
136
  //#region ../shared/src/client/config-slots.ts
76
137
  /** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
77
138
  const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
78
139
  /**
140
+ * 视图 → 外壳形态:插件页 page 视图渲染在官方页面容器内(宿主已画标题、图标、
141
+ * 面包屑与页面级内边距),再套卡片会形成卡片套卡片并压缩移动端可用宽度,
142
+ * 故用无边框分节;summary 与 legacy 槽位保持卡片形态。
143
+ */
144
+ function cardVariantFor(view) {
145
+ return view === "page" ? "section" : "card";
146
+ }
147
+ /**
79
148
  * 把一张配置卡片同时注册进 legacy 与新版三个槽位。
80
149
  * 任一槽位在当前 dsh 版本未声明时对应 inject 挂起等待,版本不命中即 no-op。
81
150
  */
@@ -101,6 +170,17 @@ window.__ModuleLoader__.load({
101
170
  }, reg.component));
102
171
  }
103
172
  //#endregion
173
+ //#region ../shared/src/client/draft.ts
174
+ /**
175
+ * 单命名空间 staged 草稿(简单配置卡片共用):读 scope 快照、播种草稿、
176
+ * 脏判定、保存(revision fencing + 重新装载)与状态行。
177
+ *
178
+ * 语义与官方 staged 表单一致:Host 更新不覆盖在途编辑,只有保存才写回;
179
+ * 保存成功后按 Host 解析值重置草稿。`from`/`patch` 参与 effect 与 useMemo
180
+ * 依赖,必须是模块级稳定函数(不要内联箭头函数)。
181
+ * @module @dsh-plus/shared/client/draft
182
+ */
183
+ //#endregion
104
184
  //#region ../shared/src/client/fields.tsx
105
185
  /**
106
186
  * 勾选行:自绘圆角扁平开关(switch),对齐官方 WebUI 无原生 checkbox 的
@@ -302,6 +382,9 @@ window.__ModuleLoader__.load({
302
382
  * - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
303
383
  * - footer 按钮等宽换行、状态文字独占一行;
304
384
  * - 展开态 body 底部 sticky 保存栏(长表单滚动中始终可达)。
385
+ * - 分节形态(-section,插件页 page 视图内嵌宿主页面):无边框/背景、横向零
386
+ * 内边距,页脚不 sticky——官方插件页已提供页面级内边距,再叠一层卡片会
387
+ * 缩窄移动端可用宽度(卡片套卡片)。
305
388
  * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
306
389
  * @module @dsh-plus/shared/client/styles
307
390
  */
@@ -348,6 +431,15 @@ window.__ModuleLoader__.load({
348
431
  .${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
349
432
  .${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
350
433
  .${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
434
+ /* 宿主内嵌分节形态(插件页 page 视图):官方设置页同构——无边框/背景,
435
+ 横向零内边距(宽度交给宿主页面容器),小节标题 13px/600,页脚非 sticky。 */
436
+ .${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
437
+ .${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
438
+ .${prefix}-sectionTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;margin:0;font-size:13px;font-weight:600;line-height:1.5}
439
+ .${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
440
+ .${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
441
+ .${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
442
+ .${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
351
443
  .${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
352
444
  .${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
353
445
  .${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
@@ -369,6 +461,8 @@ window.__ModuleLoader__.load({
369
461
  .${prefix}-status{flex-basis:100%;flex:none}
370
462
  .${prefix}-btn{min-height:44px;flex:1;min-width:96px}
371
463
  .${prefix}-statusBadge{display:none}
464
+ .${prefix}-sectionBadge{display:inline-block}
465
+ .${prefix}-sectionFooter{position:static}
372
466
  }
373
467
  @media (pointer:coarse){
374
468
  .${prefix}-btn{min-height:44px}
@@ -585,16 +679,84 @@ ${extra}
585
679
  ]
586
680
  });
587
681
  }
682
+ /** 目录状态行:拉取失败给重试入口,拉取中与空行集各给一行提示。 */
683
+ function CatalogStatus(props) {
684
+ if (props.catalogFailed) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
685
+ className: "dsm-banner",
686
+ role: "status",
687
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: props.t("catalogError") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
688
+ type: "button",
689
+ className: "dsm-bannerRetry",
690
+ onClick: props.onRetry,
691
+ children: props.t("catalogRetry")
692
+ })]
693
+ });
694
+ if (props.catalog === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
695
+ className: "dsm-empty",
696
+ role: "status",
697
+ children: props.t("catalogLoading")
698
+ });
699
+ if (props.rowCount === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
700
+ className: "dsm-empty",
701
+ role: "status",
702
+ children: props.t("noRows")
703
+ });
704
+ return null;
705
+ }
706
+ /**
707
+ * 子代理 provider 目录(展开后拉取一次;失败保留重试入口,重试即回到未加载态)。
708
+ * 与草稿播种共用同一个 catalog:目录到达后由调用方补缺失的空行。
709
+ */
710
+ function useCatalog(open) {
711
+ const [catalog, setCatalog] = (0, react.useState)(null);
712
+ const [catalogFailed, setCatalogFailed] = (0, react.useState)(false);
713
+ (0, react.useEffect)(() => {
714
+ if (!open || catalog !== null) return;
715
+ let alive = true;
716
+ fetchCatalog().then((loaded) => {
717
+ if (!alive) return;
718
+ setCatalog(loaded);
719
+ setCatalogFailed(false);
720
+ }).catch(() => {
721
+ if (alive) setCatalogFailed(true);
722
+ });
723
+ return () => {
724
+ alive = false;
725
+ };
726
+ }, [open, catalog]);
727
+ return {
728
+ catalog,
729
+ catalogFailed,
730
+ retry: () => {
731
+ setCatalog(null);
732
+ setCatalogFailed(false);
733
+ }
734
+ };
735
+ }
736
+ /** 卡片动作:放弃(回到 Host 值)与保存(无效行或只读时禁用)。 */ function cardActions(options) {
737
+ const { t } = options;
738
+ return [{
739
+ key: "discard",
740
+ label: t("discard"),
741
+ disabled: !options.dirty || options.saving,
742
+ onClick: options.onDiscard
743
+ }, {
744
+ key: "save",
745
+ label: t(options.saving ? "saving" : "save"),
746
+ variant: "primary",
747
+ disabled: !options.dirty || options.invalid || options.saving || options.disabled,
748
+ onClick: options.onSave
749
+ }];
750
+ }
588
751
  function SubagentModelCard(props) {
589
752
  const { t, scope, api } = props;
590
753
  const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
591
754
  const value = snapshot.value;
592
755
  const [open, setOpen] = (0, react.useState)(props.view === "page");
593
756
  const [draft, setDraft] = (0, react.useState)(null);
594
- const [catalog, setCatalog] = (0, react.useState)(null);
595
- const [catalogFailed, setCatalogFailed] = (0, react.useState)(false);
596
757
  const [saving, setSaving] = (0, react.useState)(false);
597
758
  const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
759
+ const { catalog, catalogFailed, retry: retryCatalog } = useCatalog(open);
598
760
  (0, react.useEffect)(() => {
599
761
  if (value === void 0) return;
600
762
  setDraft((current) => {
@@ -613,20 +775,6 @@ ${extra}
613
775
  } : current;
614
776
  });
615
777
  }, [value, catalog]);
616
- (0, react.useEffect)(() => {
617
- if (!open || catalog !== null) return;
618
- let alive = true;
619
- fetchCatalog().then((loaded) => {
620
- if (!alive) return;
621
- setCatalog(loaded);
622
- setCatalogFailed(false);
623
- }).catch(() => {
624
- if (alive) setCatalogFailed(true);
625
- });
626
- return () => {
627
- alive = false;
628
- };
629
- }, [open, catalog]);
630
778
  const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFrom(value, catalog))), [
631
779
  value,
632
780
  draft,
@@ -634,12 +782,11 @@ ${extra}
634
782
  ]);
635
783
  const invalid = (0, react.useMemo)(() => draft !== null && Object.values(draft.rows).some((row) => row.model.length > 0 && row.provider.length === 0), [draft]);
636
784
  if (props.view === "summary") return t("summaryLine");
637
- if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
638
- className: "dsm-card",
639
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
640
- className: "dsm-empty",
641
- children: t("loading")
642
- })
785
+ const variant = cardVariantFor(props.view);
786
+ if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardLoading, {
787
+ prefix: "dsm",
788
+ variant,
789
+ text: t("loading")
643
790
  });
644
791
  const edit = (key, editValue) => {
645
792
  setDraft({
@@ -673,10 +820,6 @@ ${extra}
673
820
  });
674
821
  }).finally(() => setSaving(false));
675
822
  };
676
- const onRetryCatalog = () => {
677
- setCatalog(null);
678
- setCatalogFailed(false);
679
- };
680
823
  const disabled = !snapshot.writable;
681
824
  const rowNames = Object.keys(draft.rows).sort();
682
825
  const providerCount = (catalog?.providers ?? []).length;
@@ -686,6 +829,7 @@ ${extra}
686
829
  description: t("description"),
687
830
  open,
688
831
  onToggle: setOpen,
832
+ variant,
689
833
  statusBadge: {
690
834
  text: t(draft.enabled ? "enabledOn" : "enabledOff"),
691
835
  on: draft.enabled
@@ -694,21 +838,18 @@ ${extra}
694
838
  dirtyLabel: t("unsaved"),
695
839
  readOnlyNotice: disabled ? t("readOnly") : void 0,
696
840
  status,
697
- actions: [{
698
- key: "discard",
699
- label: t("discard"),
700
- disabled: !dirty || saving,
701
- onClick: () => {
841
+ actions: cardActions({
842
+ t,
843
+ dirty,
844
+ invalid,
845
+ saving,
846
+ disabled,
847
+ onDiscard: () => {
702
848
  setDraft(draftFrom(value, catalog));
703
849
  setStatus(IDLE_STATUS);
704
- }
705
- }, {
706
- key: "save",
707
- label: t(saving ? "saving" : "save"),
708
- variant: "primary",
709
- disabled: !dirty || invalid || saving || disabled,
710
- onClick: onSave
711
- }],
850
+ },
851
+ onSave
852
+ }),
712
853
  children: [
713
854
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
714
855
  prefix: "dsm",
@@ -718,26 +859,13 @@ ${extra}
718
859
  disabled,
719
860
  onEdit: (v) => edit("enabled", v)
720
861
  }),
721
- catalogFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
722
- className: "dsm-banner",
723
- role: "status",
724
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("catalogError") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
725
- type: "button",
726
- className: "dsm-bannerRetry",
727
- onClick: onRetryCatalog,
728
- children: t("catalogRetry")
729
- })]
730
- }) : null,
731
- catalog === null && !catalogFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
732
- className: "dsm-empty",
733
- role: "status",
734
- children: t("catalogLoading")
735
- }) : null,
736
- rowNames.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
737
- className: "dsm-empty",
738
- role: "status",
739
- children: t("noRows")
740
- }) : null,
862
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CatalogStatus, {
863
+ t,
864
+ catalog,
865
+ catalogFailed,
866
+ rowCount: rowNames.length,
867
+ onRetry: retryCatalog
868
+ }),
741
869
  rowNames.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowBlock, {
742
870
  name,
743
871
  row: draft.rows[name] ?? { ...EMPTY_ROW },
@@ -830,9 +958,14 @@ ${extra}
830
958
  .dsm-banner{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);justify-content:space-between;align-items:center;gap:8px;border-radius:8px;margin-top:12px;padding:7px 8px;font-size:12px;line-height:18px;display:flex}
831
959
  .dsm-bannerRetry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600}
832
960
  .dsm-empty{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
961
+ /* 分节形态(插件页 page 视图):provider 行块改为分隔线分组,去掉第二层内边距,
962
+ 移动端可用宽度不再被卡片与行块两级内边距同时扣减。 */
963
+ .dsm-section .dsm-row{background:0 0;border:0;border-top:1px solid var(--dsw-alias-border-l2);border-radius:0;margin:0;padding:10px 0 2px}
964
+ .dsm-section .dsm-row:first-of-type{border-top:0}
833
965
  @media (max-width:767px){
834
966
  .dsm-row{padding:2px 8px 8px}
835
967
  .dsm-rowHead{flex-wrap:wrap}
968
+ .dsm-section .dsm-row{padding:10px 0 2px}
836
969
  }
837
970
  `);
838
971
  /** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/subagent-model",
3
- "version": "0.3.20",
3
+ "version": "0.3.21",
4
4
  "description": "dsh-plus service+ui plugin: 为 subagent/subagent_fork 等子代理按提供商配置模型与思考程度(或继承主代理)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -19,7 +19,7 @@
19
19
  "src"
20
20
  ],
21
21
  "dependencies": {
22
- "@dsh-plus/shared": "0.1.30"
22
+ "@dsh-plus/shared": "0.1.31"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "@deepseek-ai/cordis": "^4.0.4",
@@ -11,9 +11,12 @@
11
11
  */
12
12
 
13
13
  import {
14
+ type CardAction,
14
15
  CardChrome,
16
+ CardLoading,
15
17
  type CardStatusState,
16
18
  CheckRow,
19
+ cardVariantFor,
17
20
  IDLE_STATUS,
18
21
  type NamespaceSettingsApi,
19
22
  type PluginConfigViewProps,
@@ -147,6 +150,105 @@ function RowBlock(props: RowBlockProps): ReactElement {
147
150
  )
148
151
  }
149
152
 
153
+ /** 目录状态行:拉取失败给重试入口,拉取中与空行集各给一行提示。 */
154
+ function CatalogStatus(props: {
155
+ t: Translate
156
+ catalog: ModelCatalog | null
157
+ catalogFailed: boolean
158
+ rowCount: number
159
+ onRetry(): void
160
+ }): ReactElement | null {
161
+ if (props.catalogFailed) {
162
+ return (
163
+ <div className="dsm-banner" role="status">
164
+ <span>{props.t('catalogError')}</span>
165
+ <button type="button" className="dsm-bannerRetry" onClick={props.onRetry}>
166
+ {props.t('catalogRetry')}
167
+ </button>
168
+ </div>
169
+ )
170
+ }
171
+ if (props.catalog === null) {
172
+ return (
173
+ <p className="dsm-empty" role="status">
174
+ {props.t('catalogLoading')}
175
+ </p>
176
+ )
177
+ }
178
+ if (props.rowCount === 0) {
179
+ return (
180
+ <p className="dsm-empty" role="status">
181
+ {props.t('noRows')}
182
+ </p>
183
+ )
184
+ }
185
+ return null
186
+ }
187
+
188
+ /**
189
+ * 子代理 provider 目录(展开后拉取一次;失败保留重试入口,重试即回到未加载态)。
190
+ * 与草稿播种共用同一个 catalog:目录到达后由调用方补缺失的空行。
191
+ */
192
+ function useCatalog(open: boolean): {
193
+ catalog: ModelCatalog | null
194
+ catalogFailed: boolean
195
+ retry(): void
196
+ } {
197
+ const [catalog, setCatalog] = useState<ModelCatalog | null>(null)
198
+ const [catalogFailed, setCatalogFailed] = useState(false)
199
+ useEffect(() => {
200
+ if (!open || catalog !== null) return
201
+ let alive = true
202
+ fetchCatalog()
203
+ .then((loaded) => {
204
+ if (!alive) return
205
+ setCatalog(loaded)
206
+ setCatalogFailed(false)
207
+ })
208
+ .catch(() => {
209
+ if (alive) setCatalogFailed(true)
210
+ })
211
+ return () => {
212
+ alive = false
213
+ }
214
+ }, [open, catalog])
215
+ return {
216
+ catalog,
217
+ catalogFailed,
218
+ retry: () => {
219
+ setCatalog(null)
220
+ setCatalogFailed(false)
221
+ },
222
+ }
223
+ }
224
+
225
+ /** 卡片动作:放弃(回到 Host 值)与保存(无效行或只读时禁用)。 */ function cardActions(options: {
226
+ t: Translate
227
+ dirty: boolean
228
+ invalid: boolean
229
+ saving: boolean
230
+ disabled: boolean
231
+ onDiscard(): void
232
+ onSave(): void
233
+ }): CardAction[] {
234
+ const { t } = options
235
+ return [
236
+ {
237
+ key: 'discard',
238
+ label: t('discard'),
239
+ disabled: !options.dirty || options.saving,
240
+ onClick: options.onDiscard,
241
+ },
242
+ {
243
+ key: 'save',
244
+ label: t(options.saving ? 'saving' : 'save'),
245
+ variant: 'primary',
246
+ disabled: !options.dirty || options.invalid || options.saving || options.disabled,
247
+ onClick: options.onSave,
248
+ },
249
+ ]
250
+ }
251
+
150
252
  export function SubagentModelCard(props: CardProps): ReactElement | string | null {
151
253
  const { t, scope, api } = props
152
254
  const snapshot = useSyncExternalStore(
@@ -157,10 +259,9 @@ export function SubagentModelCard(props: CardProps): ReactElement | string | nul
157
259
  // 0.1.6-alpha.2 插件页 page 视图为表单落地页,默认展开;旧槽位无 view,保持折叠。
158
260
  const [open, setOpen] = useState(props.view === 'page')
159
261
  const [draft, setDraft] = useState<Draft | null>(null)
160
- const [catalog, setCatalog] = useState<ModelCatalog | null>(null)
161
- const [catalogFailed, setCatalogFailed] = useState(false)
162
262
  const [saving, setSaving] = useState(false)
163
263
  const [status, setStatus] = useState<CardStatusState>(IDLE_STATUS)
264
+ const { catalog, catalogFailed, retry: retryCatalog } = useCatalog(open)
164
265
 
165
266
  // 播种草稿(行集 = 已配置条目);catalog 到达后只补缺失的 provider 空行,
166
267
  // 不覆盖在途编辑;后续 Host 更新同样不覆盖。
@@ -180,23 +281,6 @@ export function SubagentModelCard(props: CardProps): ReactElement | string | nul
180
281
  })
181
282
  }, [value, catalog])
182
283
 
183
- useEffect(() => {
184
- if (!open || catalog !== null) return
185
- let alive = true
186
- fetchCatalog()
187
- .then((loaded) => {
188
- if (!alive) return
189
- setCatalog(loaded)
190
- setCatalogFailed(false)
191
- })
192
- .catch(() => {
193
- if (alive) setCatalogFailed(true)
194
- })
195
- return () => {
196
- alive = false
197
- }
198
- }, [open, catalog])
199
-
200
284
  const dirty = useMemo(
201
285
  () =>
202
286
  value !== undefined &&
@@ -214,12 +298,11 @@ export function SubagentModelCard(props: CardProps): ReactElement | string | nul
214
298
  // 插件页 summary 视图只出一行简介(hooks 已全部落定,可安全提前返回)。
215
299
  if (props.view === 'summary') return t('summaryLine')
216
300
 
301
+ // 插件页 page 视图内嵌宿主页面容器(已带页面级内边距与标题),用无边框分节。
302
+ const variant = cardVariantFor(props.view)
303
+
217
304
  if (value === undefined || draft === null) {
218
- return (
219
- <li className="dsm-card">
220
- <p className="dsm-empty">{t('loading')}</p>
221
- </li>
222
- )
305
+ return <CardLoading prefix="dsm" variant={variant} text={t('loading')} />
223
306
  }
224
307
  const edit = <K extends keyof Draft>(key: K, editValue: Draft[K]): void => {
225
308
  setDraft({ ...draft, [key]: editValue })
@@ -248,10 +331,6 @@ export function SubagentModelCard(props: CardProps): ReactElement | string | nul
248
331
  })
249
332
  .finally(() => setSaving(false))
250
333
  }
251
- const onRetryCatalog = (): void => {
252
- setCatalog(null)
253
- setCatalogFailed(false)
254
- }
255
334
 
256
335
  const disabled = !snapshot.writable
257
336
  const rowNames = Object.keys(draft.rows).sort()
@@ -263,29 +342,24 @@ export function SubagentModelCard(props: CardProps): ReactElement | string | nul
263
342
  description={t('description')}
264
343
  open={open}
265
344
  onToggle={setOpen}
345
+ variant={variant}
266
346
  statusBadge={{ text: t(draft.enabled ? 'enabledOn' : 'enabledOff'), on: draft.enabled }}
267
347
  dirty={dirty}
268
348
  dirtyLabel={t('unsaved')}
269
349
  readOnlyNotice={disabled ? t('readOnly') : undefined}
270
350
  status={status}
271
- actions={[
272
- {
273
- key: 'discard',
274
- label: t('discard'),
275
- disabled: !dirty || saving,
276
- onClick: () => {
277
- setDraft(draftFrom(value, catalog))
278
- setStatus(IDLE_STATUS)
279
- },
351
+ actions={cardActions({
352
+ t,
353
+ dirty,
354
+ invalid,
355
+ saving,
356
+ disabled,
357
+ onDiscard: () => {
358
+ setDraft(draftFrom(value, catalog))
359
+ setStatus(IDLE_STATUS)
280
360
  },
281
- {
282
- key: 'save',
283
- label: t(saving ? 'saving' : 'save'),
284
- variant: 'primary',
285
- disabled: !dirty || invalid || saving || disabled,
286
- onClick: onSave,
287
- },
288
- ]}
361
+ onSave,
362
+ })}
289
363
  >
290
364
  <CheckRow
291
365
  prefix="dsm"
@@ -295,24 +369,13 @@ export function SubagentModelCard(props: CardProps): ReactElement | string | nul
295
369
  disabled={disabled}
296
370
  onEdit={(v) => edit('enabled', v)}
297
371
  />
298
- {catalogFailed ? (
299
- <div className="dsm-banner" role="status">
300
- <span>{t('catalogError')}</span>
301
- <button type="button" className="dsm-bannerRetry" onClick={onRetryCatalog}>
302
- {t('catalogRetry')}
303
- </button>
304
- </div>
305
- ) : null}
306
- {catalog === null && !catalogFailed ? (
307
- <p className="dsm-empty" role="status">
308
- {t('catalogLoading')}
309
- </p>
310
- ) : null}
311
- {rowNames.length === 0 ? (
312
- <p className="dsm-empty" role="status">
313
- {t('noRows')}
314
- </p>
315
- ) : null}
372
+ <CatalogStatus
373
+ t={t}
374
+ catalog={catalog}
375
+ catalogFailed={catalogFailed}
376
+ rowCount={rowNames.length}
377
+ onRetry={retryCatalog}
378
+ />
316
379
  {rowNames.map((name) => (
317
380
  <RowBlock
318
381
  key={name}
@@ -16,9 +16,14 @@ const extraCss = `
16
16
  .dsm-banner{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);justify-content:space-between;align-items:center;gap:8px;border-radius:8px;margin-top:12px;padding:7px 8px;font-size:12px;line-height:18px;display:flex}
17
17
  .dsm-bannerRetry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600}
18
18
  .dsm-empty{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
19
+ /* 分节形态(插件页 page 视图):provider 行块改为分隔线分组,去掉第二层内边距,
20
+ 移动端可用宽度不再被卡片与行块两级内边距同时扣减。 */
21
+ .dsm-section .dsm-row{background:0 0;border:0;border-top:1px solid var(--dsw-alias-border-l2);border-radius:0;margin:0;padding:10px 0 2px}
22
+ .dsm-section .dsm-row:first-of-type{border-top:0}
19
23
  @media (max-width:767px){
20
24
  .dsm-row{padding:2px 8px 8px}
21
25
  .dsm-rowHead{flex-wrap:wrap}
26
+ .dsm-section .dsm-row{padding:10px 0 2px}
22
27
  }
23
28
  `
24
29