@klarkxy/dsh-model-hub 0.1.8 → 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.
@@ -1,176 +1,582 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let react = require("react");
3
- let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
4
3
  let react_dom = require("react-dom");
4
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
5
5
  let react_jsx_runtime = require("react/jsx-runtime");
6
- //#region ../dsh-model-route/src/route.ts
7
- /** Empty route: no explicit model, the feature follows the host default. */
8
- function defaultModelRoute() {
9
- return {
10
- provider: "",
11
- model: ""
12
- };
13
- }
14
- const MAX_ID_CHARS = 250;
15
- const MAX_EFFORT_CHARS = 80;
16
- function asText$1(value) {
17
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
18
- }
6
+ Object.values({
7
+ /** Surfaces, from the page up to the topmost nested card. */
8
+ surface: [
9
+ "--dsw-alias-bg-base",
10
+ "--dsw-alias-bg-layer-1",
11
+ "--dsw-alias-bg-layer-2",
12
+ "--dsw-alias-bg-layer-3",
13
+ "--dsw-alias-bg-layer-4",
14
+ "--dsw-alias-bg-module-platform",
15
+ "--dsw-alias-bg-overlay"
16
+ ],
17
+ /** Hairlines. l1 is near-invisible on a menu surface, so menus use l2+. */
18
+ border: [
19
+ "--dsw-alias-border-l1",
20
+ "--dsw-alias-border-l2",
21
+ "--dsw-alias-border-l3",
22
+ "--dsw-alias-border-l4"
23
+ ],
24
+ /** Text. `caption` is menu shortcut text, `dimmed` is placeholder text. */
25
+ label: [
26
+ "--dsw-alias-label-primary",
27
+ "--dsw-alias-label-secondary",
28
+ "--dsw-alias-label-tertiary",
29
+ "--dsw-alias-label-caption",
30
+ "--dsw-alias-label-dimmed",
31
+ "--dsw-alias-label-error",
32
+ "--dsw-alias-label-primary-foreground"
33
+ ],
34
+ /** Interactive fills. The `-danger` hover belongs to destructive rows. */
35
+ interactive: [
36
+ "--dsw-alias-interactive-bg-hover",
37
+ "--dsw-alias-interactive-bg-active",
38
+ "--dsw-alias-interactive-bg-hover-danger"
39
+ ],
40
+ /** Accent and status. `business` is the focus ring's default colour. */
41
+ accent: [
42
+ "--dsw-alias-brand-primary",
43
+ "--dsw-alias-button-primary-fill",
44
+ "--dsw-alias-button-primary-hover",
45
+ "--dsw-alias-button-tool-bar-fill",
46
+ "--dsw-alias-button-tool-bar-hover",
47
+ "--dsw-alias-button-ghost-active-fill",
48
+ "--dsw-alias-button-ghost-active-border"
49
+ ],
50
+ state: [
51
+ "--dsw-alias-state-business-primary",
52
+ "--dsw-alias-state-success-primary",
53
+ "--dsw-alias-state-warn-primary",
54
+ "--dsw-alias-state-error-primary",
55
+ "--dsw-alias-state-idle-primary"
56
+ ],
57
+ /** Corner scale: sm 8, md 12, lg the large card, panel the modal. */
58
+ radius: [
59
+ "--dsw-radius-sm",
60
+ "--dsw-radius-md",
61
+ "--dsw-radius-lg",
62
+ "--dsw-radius-panel"
63
+ ],
64
+ /** An elevated surface draws its edge in the shadow, so it keeps border: 0. */
65
+ elevation: [
66
+ "--dsw-elevation-soft",
67
+ "--dsw-elevation-stroke",
68
+ "--dsw-elevation-stroke-color",
69
+ "--dsw-elevation-panel",
70
+ "--dsw-elevation-prominent"
71
+ ],
72
+ /** Floats, masks and scrollbars. */
73
+ overlay: [
74
+ "--dsw-menu-surface-fill",
75
+ "--dsw-menu-backdrop-filter",
76
+ "--dsw-mask-blur",
77
+ "--dsw-alias-bg-mask-1",
78
+ "--dsw-alias-menu-icon",
79
+ "--dsw-alias-scrollbar-bg-l2",
80
+ "--dsw-alias-scrollbar-hover-l2",
81
+ "--dsw-shadow-lv3"
82
+ ],
83
+ /** Motion, focus and type axis, published by the host rather than by a component. */
84
+ system: [
85
+ "--dsw-focus-ring-width",
86
+ "--dsw-focus-ring-color",
87
+ "--dsw-font-markdown-code-font-family",
88
+ "--ds-transition-duration",
89
+ "--ds-ease-in-out",
90
+ "--ds-font-family-code",
91
+ "--dsh-scrollbar-thumb",
92
+ "--dsh-scrollbar-thumb-hover",
93
+ "--dsh-content-font-size-secondary",
94
+ "--dsh-content-font-delta",
95
+ "--dsh-frame-top-clearance"
96
+ ]
97
+ }).flat();
19
98
  /**
20
- * Coerce a stored value into a ModelRoute. Anything malformed collapses to the
21
- * empty route, matching the per-plugin normalizers this contract replaces.
99
+ * The host's focus ring shorthand, verbatim. Writing this per control is how
100
+ * rings drift apart; a stylesheet that draws its own focus style should use
101
+ * {@link officialFocus} instead. The colour fallback is the host's own default.
22
102
  */
23
- function normalizeModelRoute(value) {
24
- if (!value || typeof value !== "object" || Array.isArray(value)) return defaultModelRoute();
25
- const row = value;
26
- const provider = asText$1(row.provider) ?? "";
27
- const model = asText$1(row.model) ?? "";
28
- const effort = asText$1(row.reasoningEffort);
29
- if (!provider || !model || provider.length > MAX_ID_CHARS || model.length > MAX_ID_CHARS) return defaultModelRoute();
30
- return effort && effort.length <= MAX_EFFORT_CHARS ? {
31
- provider,
32
- model,
33
- reasoningEffort: effort
34
- } : {
35
- provider,
36
- model
37
- };
38
- }
39
- //#endregion
40
- //#region ../dsh-model-route/src/catalog.ts
41
- /** Both services are required: Cordis checks the nested namespace separately. */
42
- const MODEL_CATALOG_INJECT = ["remote", "remote.session"];
43
- function asRecord(value) {
44
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
45
- }
46
- function asText(value) {
47
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
103
+ const OFFICIAL_FOCUS_RING = "var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))";
104
+ /**
105
+ * A complete `outline` declaration at the official geometry. `offset` defaults
106
+ * to the 2px the native controls use; the host's inline field rules sit at 1px
107
+ * and a segmented tab pulls the ring inside itself at -2px.
108
+ */
109
+ function officialFocus(offset = "2px") {
110
+ return `outline: ${OFFICIAL_FOCUS_RING}; outline-offset: ${offset}`;
48
111
  }
49
- /** A useful diagnostic even when a transport rejects with a plain object. */
50
- function modelCatalogErrorMessage(value) {
51
- if (value instanceof Error && value.message) return value.message;
52
- return asText(asRecord(value)?.message) ?? asText(value) ?? "Unable to load the model catalog.";
112
+ /**
113
+ * An elevated surface. The host draws a raised surface's edge inside its
114
+ * shadow rather than as a border, so a card sets `border: 0` and takes the edge
115
+ * from the elevation — which is also why its border stays off the alias-border
116
+ * set that a flat, non-elevated rule would use.
117
+ */
118
+ function officialElevation(level) {
119
+ return `border: 0; box-shadow: var(--dsw-elevation-${level})`;
53
120
  }
54
121
  /**
55
- * Validate and unwrap a successful catalog without discarding provider failures
56
- * or newer host metadata. Names and metadata remain optional for older hosts.
57
- * A broken response is never presented as a successfully loaded empty menu.
122
+ * The shared stylesheet for a plugin UI, scoped under that plugin's root class.
123
+ *
124
+ * Scoping is what keeps this safe to always-bundle into every client: the rules
125
+ * can only match inside the plugin's own subtree, so two plugins can both mount
126
+ * `dsh-ui-card` without either styling the other's markup.
127
+ *
128
+ * Each recipe matches the root element *and* its descendants, because the
129
+ * commonest arrangement is one element that is both — a surface that carries
130
+ * the root class and `dsh-ui-card`, or a portaled dialog that is the root and
131
+ * the `dsh-ui-surface`. A descendant-only scope would silently leave those
132
+ * unstyled, with no error anywhere.
133
+ *
134
+ * A caveat worth knowing: `Modal` and `Menu` portal to `document.body`, so their
135
+ * content leaves the plugin's subtree entirely. A portaled surface therefore
136
+ * needs its own root class on an element inside the portal.
137
+ *
138
+ * Only patterns that primitives do not provide live here. If a control appears
139
+ * below that `Button`/`Input`/`Switch`/`Tag`/`Menu` already covers, that is a
140
+ * bug in this file: use the primitive and delete the recipe.
58
141
  */
59
- function checkedModelCatalog(value) {
60
- const envelope = asRecord(value);
61
- if (envelope?.ok === false) throw new Error(modelCatalogErrorMessage(envelope.error ?? envelope));
62
- const root = envelope?.ok === true ? asRecord(envelope.value) : envelope;
63
- const malformed = (field) => {
64
- throw new Error(`Invalid model catalog: ${field}.`);
65
- };
66
- if (!root || !Array.isArray(root.groups)) return malformed("groups must be an array");
67
- for (const value of root.groups) {
68
- const group = asRecord(value);
69
- if (!group || !asText(group.id) || !Array.isArray(group.models)) return malformed("each group needs an id and models array");
70
- for (const value of group.models) {
71
- const model = asRecord(value);
72
- if (!model || !asText(model.id)) return malformed("each model needs an id");
73
- if (model.reasoning !== void 0) {
74
- const reasoning = asRecord(model.reasoning);
75
- if (!reasoning || !Array.isArray(reasoning.efforts) || reasoning.efforts.some((value) => !parseEffort(value))) return malformed("reasoning efforts must be an array of identified efforts");
76
- }
77
- }
78
- }
79
- if (root.failures !== void 0 && (!Array.isArray(root.failures) || root.failures.some((value) => {
80
- const failure = asRecord(value);
81
- return !failure || !asText(failure.id) || typeof failure.name !== "string" || typeof failure.message !== "string";
82
- }))) return malformed("failures must be an array of provider failures");
83
- return root;
142
+ function officialUiCss(roots) {
143
+ const scope = (typeof roots === "string" ? [roots] : roots).map((root) => `.${root}`).join(", ");
144
+ return `
145
+ /* Official DSH Web UI contract. Generated by @klarkxy/dsh-plugin-kit/official-ui.
146
+ * Do not hand-edit the copy inside a plugin: change this module so every plugin
147
+ * moves together. Scope: ${scope} */
148
+ /* The scope rule below stays open for the whole sheet: every rule after it
149
+ * nests with the parent selector, so one stylesheet can cover several unrelated
150
+ * roots without leaking into a neighbouring plugin. Its closing brace is last. */
151
+ ${scope} {
152
+ color: var(--dsw-alias-label-primary);
153
+ font-family: inherit;
154
+ font-size: 14px;
155
+ line-height: 22px;
156
+ min-width: 0;
157
+
158
+ & *, & *::before, & *::after { box-sizing: border-box; }
159
+ & h1, & h2, & h3, & h4, & p, & figure { margin: 0; }
160
+ /* Keyboard focus only: pointer input keeps the native resting appearance. The
161
+ * ring needs no radius of its own — the outline already follows the control's
162
+ * own border-radius, and overriding it here would flatten every control. */
163
+ & :focus-visible { ${officialFocus()}; }
164
+ & [hidden] { display: none !important; }
165
+
166
+ /* --- Type tiers. The same 14/22 and 13/20 steps the primitives use. --- */
167
+ & .dsh-ui-title { font-size: 16px; line-height: 24px; font-weight: 500; color: var(--dsw-alias-label-primary); }
168
+ & .dsh-ui-heading { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
169
+ & .dsh-ui-compact { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary); }
170
+ & .dsh-ui-meta { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
171
+ & .dsh-ui-hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
172
+ & .dsh-ui-error { font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); }
173
+ & .dsh-ui-warn { color: var(--dsw-alias-state-warn-primary); }
174
+ & .dsh-ui-success { color: var(--dsw-alias-state-success-primary); }
175
+ & .dsh-ui-muted { color: var(--dsw-alias-label-tertiary); }
176
+ & .dsh-ui-truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
177
+ & .dsh-ui-wrap { min-width: 0; overflow-wrap: anywhere; }
178
+
179
+ /* --- Layout scaffolding. --- */
180
+ & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
181
+ & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
182
+ /* Plugin detail pages share one reading column. These are layout recipes;
183
+ * controls keep their own primitive styles. Form spacing follows the host's
184
+ * settings-form/fields.module.css and SettingsForm.module.css. */
185
+ & .dsh-ui-page { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 760px; min-width: 0; }
186
+ & .dsh-ui-form { display: flex; flex-direction: column; gap: 0; min-width: 0; }
187
+ &.dsh-ui-form > .dsh-ui-field, & .dsh-ui-form > .dsh-ui-field { padding: 12px 0; }
188
+ & .dsh-ui-form-footer { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-top: 16px; min-width: 0; }
189
+ & .dsh-ui-fields-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; min-width: 0; margin: 0; padding: 0; border: 0; }
190
+ @media (max-width: 560px) {
191
+ & .dsh-ui-fields-grid { grid-template-columns: minmax(0, 1fr); }
84
192
  }
85
- /** Read through the injected session namespace; absence and rejection are errors. */
86
- async function readModelCatalog(client) {
87
- const session = client.remote?.session;
88
- if (typeof session?.modelCatalog !== "function") throw new Error("Model catalog unavailable: inject both remote and remote.session.");
89
- return checkedModelCatalog(await session.modelCatalog());
193
+ & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
194
+ & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
195
+ & .dsh-ui-spacer { flex: 1; min-width: 0; }
196
+ /* A 0.5px l2 rule is the host's section divider; reserve the same rhythm. */
197
+ & .dsh-ui-divider { height: 0.5px; background: var(--dsw-alias-border-l2); }
198
+ & .dsh-ui-section { display: flex; flex-direction: column; gap: 12px; padding-top: 16px; border-top: 0.5px solid var(--dsw-alias-border-l2); }
199
+
200
+ /* --- Card: a raised surface whose edge lives in the shadow. --- */
201
+ & .dsh-ui-card {
202
+ ${officialElevation("stroke")}
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: 12px;
206
+ padding: 16px;
207
+ border-radius: var(--dsw-radius-lg);
208
+ background: var(--dsw-alias-bg-layer-1);
209
+ color: var(--dsw-alias-label-primary);
210
+ min-width: 0;
90
211
  }
91
- /** Provider failures are partial results, not a transport failure. */
92
- function modelCatalogFailures(value) {
93
- try {
94
- return checkedModelCatalog(value).failures ?? [];
95
- } catch {
96
- return [];
97
- }
212
+ & .dsh-ui-card--flat { border: 0.5px solid var(--dsw-alias-border-l3); box-shadow: none; }
213
+ & .dsh-ui-card--nested { background: var(--dsw-alias-bg-layer-2); }
214
+ & .dsh-ui-card--plain { background: none; }
215
+
216
+ /* --- Field: label, control, help, error. The control itself is an
217
+ * official primitive, so this only owns the vertical rhythm. --- */
218
+ & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
219
+ & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
220
+ /* Grid fields share a row, so the stacked-field divider must not win later in
221
+ * the cascade. Reset it here, after the field recipe. */
222
+ & .dsh-ui-fields-grid > .dsh-ui-field { padding: 0; border: 0; }
223
+ & .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }
224
+ & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
225
+ & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
226
+ & .dsh-ui-help p { margin: 0; }
227
+ & .dsh-ui-help p + p { margin-top: 8px; }
228
+ & .dsh-ui-required { color: var(--dsw-alias-state-error-primary); }
229
+ & .dsh-ui-control { width: 100%; min-width: 0; }
230
+ /* An invalid control is marked on the control, not only in the message. */
231
+ & .dsh-ui-control[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
232
+ & .dsh-ui-readonly { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
233
+ & .dsh-ui-readonly > dt { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
234
+ & .dsh-ui-readonly > dd { margin: 0; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
235
+
236
+ /* --- Buttons row. A no-op container: it exists so actions line up on the
237
+ * trailing edge the way every native panel does. --- */
238
+ & .dsh-ui-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 4px; }
239
+ & .dsh-ui-actions-end { justify-content: flex-end; }
240
+
241
+ /* --- Banners. A banner is a left-rail notice, the shape used for warnings,
242
+ * deprecations and blocking instructions. --- */
243
+ & .dsh-ui-banner {
244
+ display: flex;
245
+ flex-direction: column;
246
+ gap: 8px;
247
+ padding: 10px 14px;
248
+ border: 0.5px solid var(--dsw-alias-border-l3);
249
+ border-left: 3px solid var(--dsw-alias-state-warn-primary);
250
+ border-radius: var(--dsw-radius-md);
251
+ background: var(--dsw-alias-bg-layer-1);
252
+ font-size: 13px;
253
+ line-height: 20px;
254
+ color: var(--dsw-alias-label-secondary);
98
255
  }
99
- /** Text adapter for existing model-hub status messages. */
100
- function catalogFailures(value) {
101
- return modelCatalogFailures(value).map((failure) => `${failure.name || failure.id}: ${failure.message}`);
256
+ & .dsh-ui-banner-title { font-weight: 500; color: var(--dsw-alias-state-warn-primary); }
257
+ & .dsh-ui-banner--info { border-left-color: var(--dsw-alias-state-business-primary); }
258
+ & .dsh-ui-banner--danger { border-left-color: var(--dsw-alias-state-error-primary); }
259
+ & .dsh-ui-banner--success { border-left-color: var(--dsw-alias-state-success-primary); }
260
+ & .dsh-ui-banner p { margin: 0; }
261
+
262
+ /* --- Inline notice: a transient confirmation with no chrome of its own. --- */
263
+ & .dsh-ui-notice { display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-success-primary); }
264
+ & .dsh-ui-notice--error { color: var(--dsw-alias-state-error-primary); }
265
+
266
+ /* --- Empty and loading states. Both sit in a dashed well so an empty panel
267
+ * reads as "nothing here yet" rather than as a broken layout. --- */
268
+ & .dsh-ui-empty {
269
+ display: flex;
270
+ flex-direction: column;
271
+ align-items: flex-start;
272
+ gap: 6px;
273
+ padding: 20px 16px;
274
+ border: 0.5px dashed var(--dsw-alias-border-l3);
275
+ border-radius: var(--dsw-radius-md);
276
+ font-size: 13px;
277
+ line-height: 20px;
278
+ color: var(--dsw-alias-label-secondary);
102
279
  }
103
- function parseEffort(value) {
104
- const row = asRecord(value);
105
- const id = row ? asText(row.id) : void 0;
106
- if (!id) return void 0;
107
- return {
108
- id,
109
- name: row ? asText(row.name) ?? id : id
110
- };
280
+ & .dsh-ui-loading { display: flex; align-items: center; gap: 8px; padding: 20px 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }
281
+
282
+ /* --- Selectable list. A row is a button-sized target with a hover fill and
283
+ * a selected fill; the name carries the weight, the description does not. --- */
284
+ & .dsh-ui-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
285
+ & .dsh-ui-list-scroll { max-height: min(66vh, 760px); overflow-y: auto; overflow-x: hidden; }
286
+ & .dsh-ui-list-row { border-radius: var(--dsw-radius-md); }
287
+ & .dsh-ui-list-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
288
+ & .dsh-ui-list-row[aria-selected='true'], & .dsh-ui-list-row[data-selected] { background: var(--dsw-alias-interactive-bg-active); }
289
+ & .dsh-ui-list-item {
290
+ display: flex;
291
+ flex-direction: column;
292
+ align-items: flex-start;
293
+ gap: 4px;
294
+ width: 100%;
295
+ min-width: 0;
296
+ padding: 10px 12px;
297
+ border: 0;
298
+ border-radius: var(--dsw-radius-md);
299
+ background: none;
300
+ font: inherit;
301
+ color: inherit;
302
+ text-align: start;
303
+ cursor: pointer;
111
304
  }
112
- /** Parse the host session catalog into flat choices, keeping a saved route selectable. */
113
- function parseModelMenuChoices(value, bound) {
114
- const envelope = asRecord(value);
115
- const root = (envelope && envelope.ok === true ? asRecord(envelope.value) : void 0) ?? envelope ?? {};
116
- const groups = Array.isArray(root.groups) ? root.groups : [];
117
- const choices = [];
118
- for (const group of groups) {
119
- const groupRow = asRecord(group);
120
- const provider = groupRow ? asText(groupRow.id) : void 0;
121
- if (!provider || !groupRow || !Array.isArray(groupRow.models)) continue;
122
- const providerName = asText(groupRow.name) ?? provider;
123
- for (const entry of groupRow.models) {
124
- const modelRow = asRecord(entry);
125
- const model = modelRow ? asText(modelRow.id) : void 0;
126
- if (!model || !modelRow) continue;
127
- const name = asText(modelRow.name) ?? model;
128
- const reasoning = asRecord(modelRow.reasoning);
129
- const efforts = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts.flatMap((effort) => {
130
- const parsed = parseEffort(effort);
131
- return parsed ? [parsed] : [];
132
- }) : [];
133
- choices.push({
134
- provider,
135
- model,
136
- label: `${providerName} / ${name}`,
137
- efforts
138
- });
139
- }
140
- }
141
- if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
142
- provider: bound.provider,
143
- model: bound.model,
144
- label: `${bound.provider} / ${bound.model}`,
145
- efforts: []
146
- });
147
- return choices;
305
+ & .dsh-ui-list-item:disabled { cursor: default; }
306
+ & .dsh-ui-list-item:disabled:hover { background: none; }
307
+ & .dsh-ui-list-name { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }
308
+ & .dsh-ui-list-desc { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
309
+
310
+ /* --- Code and structured output. Wraps rather than scrolls, because a
311
+ * feature panel is read rather than scanned. --- */
312
+ & .dsh-ui-code {
313
+ padding: 10px 12px;
314
+ border: 0.5px solid var(--dsw-alias-border-l3);
315
+ border-radius: var(--dsw-radius-md);
316
+ background: var(--dsw-alias-bg-layer-1);
317
+ font-family: var(--ds-font-family-code);
318
+ font-size: 12px;
319
+ line-height: 20px;
320
+ color: var(--dsw-alias-label-secondary);
321
+ overflow-wrap: anywhere;
322
+ white-space: pre-wrap;
148
323
  }
149
- /** Reasoning-effort options for one choice; the saved value stays selectable. */
150
- function modelMenuEffortOptions(choice, current) {
151
- const listed = (choice?.efforts ?? []).map((item) => ({ ...item }));
152
- if (current && !listed.some((item) => item.id === current)) listed.push({
153
- id: current,
154
- name: current
155
- });
156
- return listed;
324
+ & .dsh-ui-scroll { max-height: 60vh; overflow: auto; overscroll-behavior: contain; }
325
+
326
+ /* --- A toggle row: label on the left, switch on the trailing edge. This is
327
+ * layout only; the switch itself is the official primitive. --- */
328
+ & .dsh-ui-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-width: 0; padding: 12px 0; }
329
+ & .dsh-ui-toggle-row + .dsh-ui-toggle-row { border-top: 0.5px solid var(--dsw-alias-border-l2); }
330
+ & .dsh-ui-toggle-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
331
+ & .dsh-ui-toggle-label { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
332
+
333
+ /* --- Floating surface: a panel anchored to a trigger. This is the material
334
+ * a menu card is made of — the translucent fill plus its backdrop blur, with
335
+ * the edge riding inside the prominent elevation. A plugin that hand-rolls this
336
+ * is how tokens like --dsw-specific-menu get invented; use this instead. --- */
337
+ & .dsh-ui-surface {
338
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
339
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
340
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
341
+ position: relative;
342
+ display: flex;
343
+ flex-direction: column;
344
+ border: 0;
345
+ border-radius: var(--dsw-radius-lg);
346
+ background: var(--dsw-menu-surface-fill);
347
+ backdrop-filter: var(--dsw-menu-backdrop-filter);
348
+ box-shadow: var(--dsw-elevation-prominent);
349
+ color: var(--dsw-alias-label-primary);
350
+ font-size: 13px;
351
+ line-height: 20px;
352
+ min-width: 0;
157
353
  }
158
- //#endregion
159
- //#region ../dsh-model-route/src/menu.ts
160
- /** Localize standard effort ids only when the provider has not supplied a display name. */
161
- function modelMenuEffortLabel(effort, locale) {
162
- if (effort.name !== effort.id) return effort.name;
163
- return {
164
- off: ["关闭", "Off"],
165
- none: ["无", "None"],
166
- minimal: ["最低", "Minimal"],
167
- low: ["低", "Low"],
168
- medium: ["中", "Medium"],
169
- high: ["高", "High"],
170
- xhigh: ["极高", "Extra high"],
171
- max: ["最高", "Maximum"],
172
- ultra: ["超高", "Ultra"]
173
- }[effort.id]?.[locale?.toLowerCase().startsWith("zh") ? 0 : 1] ?? effort.name;
354
+ & .dsh-ui-surface-body { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px 14px; overflow-y: auto; }
355
+ & .dsh-ui-surface-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
356
+ & .dsh-ui-surface-title { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
357
+
358
+ /* --- Native select. The primitives ship no select, so a plugin that offers a
359
+ * closed list of choices keeps the platform control and matches the official
360
+ * settings field geometry exactly (34px, 0.5px l4, radius-md, layer-3). A
361
+ * SegmentedControl is the primitive to reach for when the choices are few
362
+ * enough to show at once. --- */
363
+ & .dsh-ui-select {
364
+ box-sizing: border-box;
365
+ width: 100%;
366
+ min-width: 0;
367
+ height: 34px;
368
+ padding: 0 12px;
369
+ border: 0.5px solid var(--dsw-alias-border-l4);
370
+ border-radius: var(--dsw-radius-md);
371
+ background: var(--dsw-alias-bg-layer-3);
372
+ font: inherit;
373
+ font-size: 13px;
374
+ line-height: 1.5;
375
+ color: var(--dsw-alias-label-primary);
376
+ cursor: pointer;
377
+ }
378
+ & .dsh-ui-select:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
379
+ & .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
380
+ & .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
381
+
382
+ /* --- Native textarea. The primitives ship no textarea either, so a settings
383
+ * prompt keeps the platform element and the same field shell as the select:
384
+ * 0.5px l4, radius-md, layer-3, 13/1.5 type. It grows vertically instead of
385
+ * locking to the select's 34px row. --- */
386
+ & .dsh-ui-textarea {
387
+ box-sizing: border-box;
388
+ width: 100%;
389
+ min-width: 0;
390
+ resize: vertical;
391
+ padding: 8px 12px;
392
+ border: 0.5px solid var(--dsw-alias-border-l4);
393
+ border-radius: var(--dsw-radius-md);
394
+ background: var(--dsw-alias-bg-layer-3);
395
+ font: inherit;
396
+ font-size: 13px;
397
+ line-height: 1.5;
398
+ color: var(--dsw-alias-label-primary);
399
+ }
400
+ & .dsh-ui-textarea:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
401
+ & .dsh-ui-textarea:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
402
+ & .dsh-ui-textarea::placeholder { color: var(--dsw-alias-label-dimmed); }
403
+ & .dsh-ui-textarea[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
404
+
405
+ /* Long panels scroll inside themselves instead of growing the host page. */
406
+ & .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
407
+ & .dsh-ui-panel-wide { max-width: 100%; }
408
+
409
+ @media (prefers-reduced-motion: reduce) {
410
+ & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
411
+ }
412
+ }
413
+ `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?=\s*\{)/gm, (_match, className) => `&${className}, & ${className}`);
414
+ }
415
+ //#endregion
416
+ //#region ../dsh-model-route/src/route.ts
417
+ /** Empty route: no explicit model, the feature follows the host default. */
418
+ function defaultModelRoute() {
419
+ return {
420
+ provider: "",
421
+ model: ""
422
+ };
423
+ }
424
+ const MAX_ID_CHARS = 250;
425
+ const MAX_EFFORT_CHARS = 80;
426
+ function asText$1(value) {
427
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
428
+ }
429
+ /**
430
+ * Coerce a stored value into a ModelRoute. Anything malformed collapses to the
431
+ * empty route, matching the per-plugin normalizers this contract replaces.
432
+ */
433
+ function normalizeModelRoute(value) {
434
+ if (!value || typeof value !== "object" || Array.isArray(value)) return defaultModelRoute();
435
+ const row = value;
436
+ const provider = asText$1(row.provider) ?? "";
437
+ const model = asText$1(row.model) ?? "";
438
+ const effort = asText$1(row.reasoningEffort);
439
+ if (!provider || !model || provider.length > MAX_ID_CHARS || model.length > MAX_ID_CHARS) return defaultModelRoute();
440
+ return effort && effort.length <= MAX_EFFORT_CHARS ? {
441
+ provider,
442
+ model,
443
+ reasoningEffort: effort
444
+ } : {
445
+ provider,
446
+ model
447
+ };
448
+ }
449
+ //#endregion
450
+ //#region ../dsh-model-route/src/catalog.ts
451
+ /** Both services are required: Cordis checks the nested namespace separately. */
452
+ const MODEL_CATALOG_INJECT = ["remote", "remote.session"];
453
+ function asRecord(value) {
454
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
455
+ }
456
+ function asText(value) {
457
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
458
+ }
459
+ /** A useful diagnostic even when a transport rejects with a plain object. */
460
+ function modelCatalogErrorMessage(value) {
461
+ if (value instanceof Error && value.message) return value.message;
462
+ return asText(asRecord(value)?.message) ?? asText(value) ?? "Unable to load the model catalog.";
463
+ }
464
+ /**
465
+ * Validate and unwrap a successful catalog without discarding provider failures
466
+ * or newer host metadata. Names and metadata remain optional for older hosts.
467
+ * A broken response is never presented as a successfully loaded empty menu.
468
+ */
469
+ function checkedModelCatalog(value) {
470
+ const envelope = asRecord(value);
471
+ if (envelope?.ok === false) throw new Error(modelCatalogErrorMessage(envelope.error ?? envelope));
472
+ const root = envelope?.ok === true ? asRecord(envelope.value) : envelope;
473
+ const malformed = (field) => {
474
+ throw new Error(`Invalid model catalog: ${field}.`);
475
+ };
476
+ if (!root || !Array.isArray(root.groups)) return malformed("groups must be an array");
477
+ for (const value of root.groups) {
478
+ const group = asRecord(value);
479
+ if (!group || !asText(group.id) || !Array.isArray(group.models)) return malformed("each group needs an id and models array");
480
+ for (const value of group.models) {
481
+ const model = asRecord(value);
482
+ if (!model || !asText(model.id)) return malformed("each model needs an id");
483
+ if (model.reasoning !== void 0) {
484
+ const reasoning = asRecord(model.reasoning);
485
+ if (!reasoning || !Array.isArray(reasoning.efforts) || reasoning.efforts.some((value) => !parseEffort(value))) return malformed("reasoning efforts must be an array of identified efforts");
486
+ }
487
+ }
488
+ }
489
+ if (root.failures !== void 0 && (!Array.isArray(root.failures) || root.failures.some((value) => {
490
+ const failure = asRecord(value);
491
+ return !failure || !asText(failure.id) || typeof failure.name !== "string" || typeof failure.message !== "string";
492
+ }))) return malformed("failures must be an array of provider failures");
493
+ return root;
494
+ }
495
+ /** Read through the injected session namespace; absence and rejection are errors. */
496
+ async function readModelCatalog(client) {
497
+ const session = client.remote?.session;
498
+ if (typeof session?.modelCatalog !== "function") throw new Error("Model catalog unavailable: inject both remote and remote.session.");
499
+ return checkedModelCatalog(await session.modelCatalog());
500
+ }
501
+ /** Provider failures are partial results, not a transport failure. */
502
+ function modelCatalogFailures(value) {
503
+ try {
504
+ return checkedModelCatalog(value).failures ?? [];
505
+ } catch {
506
+ return [];
507
+ }
508
+ }
509
+ function parseEffort(value) {
510
+ const row = asRecord(value);
511
+ const id = row ? asText(row.id) : void 0;
512
+ if (!id) return void 0;
513
+ return {
514
+ id,
515
+ name: row ? asText(row.name) ?? id : id
516
+ };
517
+ }
518
+ /** Parse the host session catalog into flat choices, keeping a saved route selectable. */
519
+ function parseModelMenuChoices(value, bound) {
520
+ const envelope = asRecord(value);
521
+ const root = (envelope && envelope.ok === true ? asRecord(envelope.value) : void 0) ?? envelope ?? {};
522
+ const groups = Array.isArray(root.groups) ? root.groups : [];
523
+ const choices = [];
524
+ for (const group of groups) {
525
+ const groupRow = asRecord(group);
526
+ const provider = groupRow ? asText(groupRow.id) : void 0;
527
+ if (!provider || !groupRow || !Array.isArray(groupRow.models)) continue;
528
+ const providerName = asText(groupRow.name) ?? provider;
529
+ for (const entry of groupRow.models) {
530
+ const modelRow = asRecord(entry);
531
+ const model = modelRow ? asText(modelRow.id) : void 0;
532
+ if (!model || !modelRow) continue;
533
+ const name = asText(modelRow.name) ?? model;
534
+ const reasoning = asRecord(modelRow.reasoning);
535
+ const efforts = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts.flatMap((effort) => {
536
+ const parsed = parseEffort(effort);
537
+ return parsed ? [parsed] : [];
538
+ }) : [];
539
+ choices.push({
540
+ provider,
541
+ model,
542
+ label: `${providerName} / ${name}`,
543
+ efforts
544
+ });
545
+ }
546
+ }
547
+ if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
548
+ provider: bound.provider,
549
+ model: bound.model,
550
+ label: `${bound.provider} / ${bound.model}`,
551
+ efforts: []
552
+ });
553
+ return choices;
554
+ }
555
+ /** Reasoning-effort options for one choice; the saved value stays selectable. */
556
+ function modelMenuEffortOptions(choice, current) {
557
+ const listed = (choice?.efforts ?? []).map((item) => ({ ...item }));
558
+ if (current && !listed.some((item) => item.id === current)) listed.push({
559
+ id: current,
560
+ name: current
561
+ });
562
+ return listed;
563
+ }
564
+ //#endregion
565
+ //#region ../dsh-model-route/src/menu.ts
566
+ /** Localize standard effort ids only when the provider has not supplied a display name. */
567
+ function modelMenuEffortLabel(effort, locale) {
568
+ if (effort.name !== effort.id) return effort.name;
569
+ return {
570
+ off: ["关闭", "Off"],
571
+ none: ["无", "None"],
572
+ minimal: ["最低", "Minimal"],
573
+ low: ["低", "Low"],
574
+ medium: ["中", "Medium"],
575
+ high: ["高", "High"],
576
+ xhigh: ["极高", "Extra high"],
577
+ max: ["最高", "Maximum"],
578
+ ultra: ["超高", "Ultra"]
579
+ }[effort.id]?.[locale?.toLowerCase().startsWith("zh") ? 0 : 1] ?? effort.name;
174
580
  }
175
581
  /** The short model label shown inside a provider group. */
176
582
  function modelMenuShortName(choice) {
@@ -196,48 +602,310 @@ function groupModelMenuChoices(choices, query) {
196
602
  return groups;
197
603
  }
198
604
  //#endregion
199
- //#region ../dsh-model-route/src/catalog-ui.tsx
605
+ //#region ../dsh-plugin-kit/src/contracts.ts
606
+ /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
607
+ function modelSettingsChangesIdentity(packageName) {
608
+ const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
609
+ const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
610
+ if (!parts.length) throw new Error("Model settings package name has no RPC segment");
611
+ const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
612
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
613
+ return {
614
+ service: segment,
615
+ namespace: segment
616
+ };
617
+ }
200
618
  /**
201
- * Also accepts a mapped state from a caller that owns broader async orchestration.
202
- *
203
- * The dsh-ui-* classes carry the styling and resolve through the consuming
204
- * plugin's own officialUiCss scope (every consumer mounts its root class, so
205
- * e.g. `.dsh-model-hub .dsh-ui-loading` paints this); dsh-model-catalog-status
206
- * stays as a structural hook for tests and layout, never a colour source.
619
+ * Host-owned nav icon seat: a root-scoped list where each entry's `id` must
620
+ * equal the `settings.section` id it decorates. Owner props are
621
+ * `{size, active}`; icons stay `aria-hidden` so the section label remains the
622
+ * accessible name. Contributors into a shared section may register the same
623
+ * icon `id` with different priorities — the ledger keeps every entry while the
624
+ * host renders one icon per section.
207
625
  */
208
- function ModelCatalogStatus({ state, locale }) {
209
- const zh = locale?.toLowerCase().startsWith("zh") === true;
210
- const copy = zh ? {
211
- loading: "正在加载模型…",
212
- error: "模型列表加载失败",
213
- partial: "部分提供商的模型未能加载",
214
- retry: "重试"
215
- } : {
216
- loading: "Loading models…",
217
- error: "Unable to load models",
218
- partial: "Some providers could not load their models",
219
- retry: "Retry"
626
+ const SETTINGS_SECTION_ICON_SLOT = "settings.section.icon";
627
+ modelSettingsChangesIdentity("@klarkxy/dsh-plugin-kit");
628
+ //#endregion
629
+ //#region ../dsh-plugin-kit/src/client-utils.ts
630
+ /** One `<svg>` with the shared icon chrome; children are plain elements. */
631
+ function lineIcon(children) {
632
+ return function SettingsSectionIcon(props) {
633
+ return (0, react.createElement)("svg", {
634
+ "aria-hidden": "true",
635
+ width: props.size ?? 16,
636
+ height: props.size ?? 16,
637
+ viewBox: "0 0 16 16",
638
+ fill: "none",
639
+ stroke: "currentColor",
640
+ strokeWidth: 1,
641
+ strokeLinecap: "round",
642
+ strokeLinejoin: "round"
643
+ }, ...children.map(([tag, attrs], index) => (0, react.createElement)(tag, {
644
+ key: index,
645
+ ...attrs
646
+ })));
220
647
  };
221
- if (!state.loading && !state.error && state.failures.length === 0) return null;
222
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
223
- className: "dsh-model-catalog-status dsh-ui-stack",
224
- lang: zh ? "zh" : "en",
225
- children: [
226
- state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
227
- role: "status",
228
- className: "dsh-ui-loading",
229
- children: copy.loading
230
- }) : null,
231
- state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
232
- role: "alert",
233
- className: "dsh-ui-error dsh-ui-wrap",
234
- children: [
235
- copy.error,
236
- ": ",
237
- state.error
238
- ]
239
- }) : null,
240
- state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
648
+ }
649
+ /** Linked model nodes for the Model Management section. */
650
+ const IconModelNetworkSettings = lineIcon([
651
+ ["line", {
652
+ x1: 10.6,
653
+ y1: 5.3,
654
+ x2: 8.8,
655
+ y2: 7.1
656
+ }],
657
+ ["line", {
658
+ x1: 7.2,
659
+ y1: 8.9,
660
+ x2: 5.4,
661
+ y2: 10.7
662
+ }],
663
+ ["circle", {
664
+ cx: 12,
665
+ cy: 4,
666
+ r: 1.9
667
+ }],
668
+ ["circle", {
669
+ cx: 4,
670
+ cy: 12,
671
+ r: 1.9
672
+ }],
673
+ ["circle", {
674
+ cx: 8,
675
+ cy: 8,
676
+ r: 1.5
677
+ }]
678
+ ]);
679
+ /**
680
+ * Contribute the nav icon of a `settings.section` entry. The `settings
681
+ * .section.icon` seat is declared by the host independently of the section
682
+ * list, so the wait runs in parallel: the icon appears as soon as both seats
683
+ * exist and unloading removes it again, restoring the host's default glyph.
684
+ * The `id` must equal the section id; `order`/`priority` mirror `order` so the
685
+ * icon cell sorts like its section and, for shared sections, several
686
+ * contributors can register the same icon id under different priorities
687
+ * without duplicating nav entries.
688
+ */
689
+ function registerSettingsSectionIcon(ctx, sectionId, order, component) {
690
+ const slots = ctx.slots;
691
+ return slots.inject(SETTINGS_SECTION_ICON_SLOT, () => slots.register({
692
+ name: SETTINGS_SECTION_ICON_SLOT,
693
+ id: sectionId,
694
+ order,
695
+ priority: order
696
+ }, component));
697
+ }
698
+ //#endregion
699
+ //#region ../dsh-plugin-kit/src/model-settings-client.ts
700
+ /** This stream carries invalidations only; ready is never a settings snapshot. */
701
+ function parseModelSettingsChangeFrame(value) {
702
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid model settings change frame");
703
+ const frame = value;
704
+ if (frame.kind === "ready" && Object.keys(frame).length === 1) return { kind: "ready" };
705
+ if (frame.kind === "changed" && Object.keys(frame).length === 3 && typeof frame.ns === "string" && frame.ns.length > 0 && typeof frame.revision === "number" && Number.isSafeInteger(frame.revision) && frame.revision >= 0) return {
706
+ kind: "changed",
707
+ ns: frame.ns,
708
+ revision: frame.revision
709
+ };
710
+ throw new Error("Invalid model settings change frame");
711
+ }
712
+ /** One owner's contribution. Each package mounts only its own endpoint. */
713
+ function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
714
+ return {
715
+ package: packageName,
716
+ descriptors: [{
717
+ id: `${identity.namespace}/watch`,
718
+ service: identity.service,
719
+ namespace: identity.namespace,
720
+ method: "watch",
721
+ mode: "stream",
722
+ invocation: { kind: "direct" },
723
+ parameters: [],
724
+ cancellation: { parameter: "signal" },
725
+ result: {
726
+ mode: "strict",
727
+ typeSymbol: "ModelSettings.ChangeFrame",
728
+ create: () => ({ parse: parseModelSettingsChangeFrame })
729
+ }
730
+ }]
731
+ };
732
+ }
733
+ /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
734
+ * then withdraws it, so the contribution cannot outlive the plugin. */
735
+ function mountModelSettingsChanges(ctx, packageName) {
736
+ const contribution = modelSettingsChangesRemote(packageName);
737
+ ctx.inject(["remote"], (injected) => {
738
+ const scope = injected;
739
+ const mounting = scope.remote.$mount(contribution);
740
+ mounting.catch(() => {});
741
+ scope.effect(() => async () => {
742
+ await (await mounting.catch(() => void 0))?.();
743
+ });
744
+ });
745
+ }
746
+ /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
747
+ * A factory shares one logical stream while at least one subscriber remains. */
748
+ function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
749
+ const endpoint = identity;
750
+ const listeners = /* @__PURE__ */ new Set();
751
+ let injection;
752
+ let current;
753
+ let lifecycle = 0;
754
+ let failureMessage;
755
+ const report = (message) => {
756
+ failureMessage = message;
757
+ for (const listener of [...listeners]) try {
758
+ listener.error?.(message);
759
+ } catch {}
760
+ };
761
+ const invalidate = () => {
762
+ for (const listener of [...listeners]) {
763
+ if (!listeners.has(listener)) continue;
764
+ try {
765
+ listener.change();
766
+ } catch (error) {
767
+ try {
768
+ listener.error?.(error instanceof Error ? error.message : String(error));
769
+ } catch {}
770
+ }
771
+ }
772
+ };
773
+ const start = () => {
774
+ const life = ++lifecycle;
775
+ injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
776
+ if (life !== lifecycle || !listeners.size) return;
777
+ const scope = injected;
778
+ const service = scope.remote[endpoint.namespace];
779
+ if (!service || !("watch" in service) || !service.watch) {
780
+ report("Model settings sync service unavailable");
781
+ return;
782
+ }
783
+ const watch = service.watch;
784
+ const stream = scope.remote.$stream({
785
+ name: "Model settings sync",
786
+ open: (signal) => watch(signal),
787
+ ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
788
+ });
789
+ const session = {
790
+ stream,
791
+ active: true
792
+ };
793
+ current = session;
794
+ scope.effect(() => () => {
795
+ session.active = false;
796
+ if (current === session) {
797
+ current = void 0;
798
+ if (life === lifecycle && listeners.size) report("Model settings sync service unavailable");
799
+ }
800
+ return stream.dispose();
801
+ });
802
+ (async () => {
803
+ let generation;
804
+ let ready = false;
805
+ try {
806
+ for await (const item of stream) {
807
+ if (!session.active || life !== lifecycle || !listeners.size) break;
808
+ if (item.signal.aborted) continue;
809
+ if (generation !== item.generation) {
810
+ generation = item.generation;
811
+ ready = false;
812
+ }
813
+ const frame = parseModelSettingsChangeFrame(item.value);
814
+ if (frame.kind === "ready") {
815
+ ready = true;
816
+ failureMessage = void 0;
817
+ item.accept();
818
+ invalidate();
819
+ } else {
820
+ if (!ready) throw new Error("Model settings sync change arrived before ready");
821
+ if (accept(frame.ns)) invalidate();
822
+ }
823
+ }
824
+ } catch (error) {
825
+ if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error));
826
+ } finally {
827
+ if (current === session) current = void 0;
828
+ session.active = false;
829
+ await stream.dispose().catch(() => {});
830
+ }
831
+ })();
832
+ });
833
+ queueMicrotask(() => {
834
+ if (life === lifecycle && listeners.size && !current) report("Model settings sync service unavailable");
835
+ });
836
+ };
837
+ return Object.freeze({ subscribe(onChange, onError) {
838
+ const listener = {
839
+ change: onChange,
840
+ error: onError
841
+ };
842
+ listeners.add(listener);
843
+ if (!injection) start();
844
+ else if (failureMessage) queueMicrotask(() => {
845
+ if (listeners.has(listener) && failureMessage) try {
846
+ listener.error?.(failureMessage);
847
+ } catch {}
848
+ });
849
+ return () => {
850
+ if (!listeners.delete(listener) || listeners.size) return;
851
+ lifecycle++;
852
+ if (current) {
853
+ current.active = false;
854
+ current.stream.dispose().catch(() => {});
855
+ }
856
+ current = void 0;
857
+ failureMessage = void 0;
858
+ const previous = injection;
859
+ injection = void 0;
860
+ try {
861
+ Promise.resolve(previous?.dispose()).catch(() => {});
862
+ } catch {}
863
+ };
864
+ } });
865
+ }
866
+ //#endregion
867
+ //#region ../dsh-model-route/src/catalog-ui.tsx
868
+ /**
869
+ * Also accepts a mapped state from a caller that owns broader async orchestration.
870
+ *
871
+ * The dsh-ui-* classes carry the styling and resolve through the consuming
872
+ * plugin's own officialUiCss scope (every consumer mounts its root class, so
873
+ * e.g. `.dsh-model-hub .dsh-ui-loading` paints this); dsh-model-catalog-status
874
+ * stays as a structural hook for tests and layout, never a colour source.
875
+ */
876
+ function ModelCatalogStatus({ state, locale }) {
877
+ const zh = locale?.toLowerCase().startsWith("zh") === true;
878
+ const copy = zh ? {
879
+ loading: "正在加载模型…",
880
+ error: "模型列表加载失败",
881
+ partial: "部分提供商的模型未能加载",
882
+ retry: "重试"
883
+ } : {
884
+ loading: "Loading models…",
885
+ error: "Unable to load models",
886
+ partial: "Some providers could not load their models",
887
+ retry: "Retry"
888
+ };
889
+ if (!state.loading && !state.error && state.failures.length === 0) return null;
890
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
891
+ className: "dsh-model-catalog-status dsh-ui-stack",
892
+ lang: zh ? "zh" : "en",
893
+ children: [
894
+ state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
895
+ role: "status",
896
+ className: "dsh-ui-loading",
897
+ children: copy.loading
898
+ }) : null,
899
+ state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
900
+ role: "alert",
901
+ className: "dsh-ui-error dsh-ui-wrap",
902
+ children: [
903
+ copy.error,
904
+ ": ",
905
+ state.error
906
+ ]
907
+ }) : null,
908
+ state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
241
909
  role: "status",
242
910
  className: "dsh-ui-meta",
243
911
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
@@ -801,806 +1469,125 @@ const modelMenuCss = `
801
1469
  color: var(--dsw-alias-label-secondary);
802
1470
  cursor: pointer;
803
1471
  background: none;
804
- border: none;
805
- outline: none;
806
- align-items: center;
807
- gap: 4px;
808
- padding: 0 4px 0 8px;
809
- font-size: 13px;
810
- font-weight: 400;
811
- line-height: 20px;
812
- display: flex;
813
- }
814
- .dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
815
- .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 var(--dsw-focus-ring-width) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
816
- .dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
817
- .dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
818
- .dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
819
- .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform var(--ds-transition-duration) var(--ds-ease-in-out); }
820
- .dsh-model-menu-chevronOpen { transform: rotate(180deg); }
821
- /* Portal overlay: the host publishes no z-index token, so the card keeps the
822
- * native menu layer's 1100 verbatim. */
823
- .dsh-model-menu-menu {
824
- z-index: 1100;
825
- --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
826
- width: max-content;
827
- min-width: min(240px, 100vw - 32px);
828
- max-width: min(420px, 100vw - 32px);
829
- max-height: min(360px, 100vh - 96px);
830
- box-shadow: var(--dsw-elevation-prominent);
831
- color: var(--dsw-alias-label-primary);
832
- --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
833
- --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
834
- border: 0;
835
- flex-direction: column;
836
- padding: 4px;
837
- display: flex;
838
- position: fixed;
839
- overflow: hidden;
840
- }
841
- .dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
842
- .dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
843
- .dsh-model-menu-searchRow .dsh-model-menu-search { border-radius: var(--dsw-radius-md); background: none; border: 0 solid transparent; height: auto; padding: 5px 7px; display: flex; }
844
- .dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
845
- .dsh-model-menu-searchWithQuery { padding-right: 34px; }
846
- .dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
847
- .dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
848
- /* The clear button is a circle by geometry (border-radius: 50%), a shape the
849
- * radius token scale does not describe. */
850
- .dsh-model-menu-searchClear {
851
- width: 24px;
852
- height: 24px;
853
- color: var(--dsw-alias-label-secondary);
854
- cursor: pointer;
855
- background: none;
856
- border: none;
857
- border-radius: 50%;
858
- justify-content: center;
859
- align-items: center;
860
- padding: 0;
861
- display: inline-flex;
862
- position: absolute;
863
- top: 50%;
864
- right: 4px;
865
- transform: translateY(-50%);
866
- }
867
- .dsh-model-menu-searchClear:hover, .dsh-model-menu-searchClear:focus-visible { background: var(--dsw-alias-interactive-bg-hover); outline: none; }
868
- .dsh-model-menu-groups { min-height: 0; overflow-y: auto; }
869
- .dsh-model-menu-leading { border-bottom: 0.5px solid var(--dsw-alias-border-l2); margin-bottom: 3px; padding-bottom: 3px; }
870
- .dsh-model-menu-option {
871
- box-sizing: border-box;
872
- border-radius: var(--dsw-radius-md);
873
- width: auto;
874
- min-width: 100%;
875
- min-height: 34px;
876
- color: inherit;
877
- text-align: left;
878
- cursor: pointer;
879
- background: none;
880
- border: none;
881
- outline: none;
882
- align-items: center;
883
- gap: 6px;
884
- padding: 5px 7px;
885
- display: flex;
886
- }
887
- .dsh-model-menu-option:not(.dsh-model-menu-modelOption):hover:not(:disabled),
888
- .dsh-model-menu-option:focus-visible,
889
- .dsh-model-menu-optionActive:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
890
- .dsh-model-menu-modelOption:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
891
- .dsh-model-menu-selected { background: none; }
892
- .dsh-model-menu-option:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
893
- .dsh-model-menu-optionCopy { flex-direction: column; flex: 1; min-width: 0; display: flex; }
894
- .dsh-model-menu-modelName { color: inherit; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 400; line-height: 18px; overflow: hidden; }
895
- .dsh-model-menu-check { color: var(--dsw-alias-label-primary); flex: 0 0 14px; place-items: center; display: grid; }
896
- .dsh-model-menu-check svg { width: 14px; height: 14px; }
897
- .dsh-model-menu-cell {
898
- box-sizing: border-box;
899
- border-radius: var(--dsw-radius-md);
900
- width: auto;
901
- min-width: 100%;
902
- height: 34px;
903
- color: var(--dsw-alias-label-primary);
904
- cursor: pointer;
905
- text-align: left;
906
- background: none;
907
- border: none;
908
- outline: none;
909
- align-items: center;
910
- gap: 6px;
911
- padding: 0 8px;
912
- font-size: 13px;
913
- line-height: 20px;
914
- display: flex;
915
- }
916
- .dsh-model-menu-cell:hover, .dsh-model-menu-cell:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
917
- .dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
918
- .dsh-model-menu-cellValue { text-overflow: ellipsis; white-space: nowrap; text-align: right; min-width: 0; color: var(--dsw-alias-label-tertiary); flex: auto; overflow: hidden; }
919
- .dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
920
- `;
921
- Object.values({
922
- /** Surfaces, from the page up to the topmost nested card. */
923
- surface: [
924
- "--dsw-alias-bg-base",
925
- "--dsw-alias-bg-layer-1",
926
- "--dsw-alias-bg-layer-2",
927
- "--dsw-alias-bg-layer-3",
928
- "--dsw-alias-bg-layer-4",
929
- "--dsw-alias-bg-module-platform",
930
- "--dsw-alias-bg-overlay"
931
- ],
932
- /** Hairlines. l1 is near-invisible on a menu surface, so menus use l2+. */
933
- border: [
934
- "--dsw-alias-border-l1",
935
- "--dsw-alias-border-l2",
936
- "--dsw-alias-border-l3",
937
- "--dsw-alias-border-l4"
938
- ],
939
- /** Text. `caption` is menu shortcut text, `dimmed` is placeholder text. */
940
- label: [
941
- "--dsw-alias-label-primary",
942
- "--dsw-alias-label-secondary",
943
- "--dsw-alias-label-tertiary",
944
- "--dsw-alias-label-caption",
945
- "--dsw-alias-label-dimmed",
946
- "--dsw-alias-label-error",
947
- "--dsw-alias-label-primary-foreground"
948
- ],
949
- /** Interactive fills. The `-danger` hover belongs to destructive rows. */
950
- interactive: [
951
- "--dsw-alias-interactive-bg-hover",
952
- "--dsw-alias-interactive-bg-active",
953
- "--dsw-alias-interactive-bg-hover-danger"
954
- ],
955
- /** Accent and status. `business` is the focus ring's default colour. */
956
- accent: [
957
- "--dsw-alias-brand-primary",
958
- "--dsw-alias-button-primary-fill",
959
- "--dsw-alias-button-primary-hover",
960
- "--dsw-alias-button-tool-bar-fill",
961
- "--dsw-alias-button-tool-bar-hover",
962
- "--dsw-alias-button-ghost-active-fill",
963
- "--dsw-alias-button-ghost-active-border"
964
- ],
965
- state: [
966
- "--dsw-alias-state-business-primary",
967
- "--dsw-alias-state-success-primary",
968
- "--dsw-alias-state-warn-primary",
969
- "--dsw-alias-state-error-primary",
970
- "--dsw-alias-state-idle-primary"
971
- ],
972
- /** Corner scale: sm 8, md 12, lg the large card, panel the modal. */
973
- radius: [
974
- "--dsw-radius-sm",
975
- "--dsw-radius-md",
976
- "--dsw-radius-lg",
977
- "--dsw-radius-panel"
978
- ],
979
- /** An elevated surface draws its edge in the shadow, so it keeps border: 0. */
980
- elevation: [
981
- "--dsw-elevation-soft",
982
- "--dsw-elevation-stroke",
983
- "--dsw-elevation-stroke-color",
984
- "--dsw-elevation-panel",
985
- "--dsw-elevation-prominent"
986
- ],
987
- /** Floats, masks and scrollbars. */
988
- overlay: [
989
- "--dsw-menu-surface-fill",
990
- "--dsw-menu-backdrop-filter",
991
- "--dsw-mask-blur",
992
- "--dsw-alias-bg-mask-1",
993
- "--dsw-alias-menu-icon",
994
- "--dsw-alias-scrollbar-bg-l2",
995
- "--dsw-alias-scrollbar-hover-l2",
996
- "--dsw-shadow-lv3"
997
- ],
998
- /** Motion, focus and type axis, published by the host rather than by a component. */
999
- system: [
1000
- "--dsw-focus-ring-width",
1001
- "--dsw-focus-ring-color",
1002
- "--dsw-font-markdown-code-font-family",
1003
- "--ds-transition-duration",
1004
- "--ds-ease-in-out",
1005
- "--ds-font-family-code",
1006
- "--dsh-scrollbar-thumb",
1007
- "--dsh-scrollbar-thumb-hover",
1008
- "--dsh-content-font-size-secondary",
1009
- "--dsh-content-font-delta",
1010
- "--dsh-frame-top-clearance"
1011
- ]
1012
- }).flat();
1013
- /**
1014
- * The host's focus ring shorthand, verbatim. Writing this per control is how
1015
- * rings drift apart; a stylesheet that draws its own focus style should use
1016
- * {@link officialFocus} instead. The colour fallback is the host's own default.
1017
- */
1018
- const OFFICIAL_FOCUS_RING = "var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))";
1019
- /**
1020
- * A complete `outline` declaration at the official geometry. `offset` defaults
1021
- * to the 2px the native controls use; the host's inline field rules sit at 1px
1022
- * and a segmented tab pulls the ring inside itself at -2px.
1023
- */
1024
- function officialFocus(offset = "2px") {
1025
- return `outline: ${OFFICIAL_FOCUS_RING}; outline-offset: ${offset}`;
1026
- }
1027
- /**
1028
- * An elevated surface. The host draws a raised surface's edge inside its
1029
- * shadow rather than as a border, so a card sets `border: 0` and takes the edge
1030
- * from the elevation — which is also why its border stays off the alias-border
1031
- * set that a flat, non-elevated rule would use.
1032
- */
1033
- function officialElevation(level) {
1034
- return `border: 0; box-shadow: var(--dsw-elevation-${level})`;
1035
- }
1036
- /**
1037
- * The shared stylesheet for a plugin UI, scoped under that plugin's root class.
1038
- *
1039
- * Scoping is what keeps this safe to always-bundle into every client: the rules
1040
- * can only match inside the plugin's own subtree, so two plugins can both mount
1041
- * `dsh-ui-card` without either styling the other's markup.
1042
- *
1043
- * Each recipe matches the root element *and* its descendants, because the
1044
- * commonest arrangement is one element that is both — a surface that carries
1045
- * the root class and `dsh-ui-card`, or a portaled dialog that is the root and
1046
- * the `dsh-ui-surface`. A descendant-only scope would silently leave those
1047
- * unstyled, with no error anywhere.
1048
- *
1049
- * A caveat worth knowing: `Modal` and `Menu` portal to `document.body`, so their
1050
- * content leaves the plugin's subtree entirely. A portaled surface therefore
1051
- * needs its own root class on an element inside the portal.
1052
- *
1053
- * Only patterns that primitives do not provide live here. If a control appears
1054
- * below that `Button`/`Input`/`Switch`/`Tag`/`Menu` already covers, that is a
1055
- * bug in this file: use the primitive and delete the recipe.
1056
- */
1057
- function officialUiCss(roots) {
1058
- const scope = (typeof roots === "string" ? [roots] : roots).map((root) => `.${root}`).join(", ");
1059
- return `
1060
- /* Official DSH Web UI contract. Generated by @klarkxy/dsh-plugin-kit/official-ui.
1061
- * Do not hand-edit the copy inside a plugin: change this module so every plugin
1062
- * moves together. Scope: ${scope} */
1063
- /* The scope rule below stays open for the whole sheet: every rule after it
1064
- * nests with the parent selector, so one stylesheet can cover several unrelated
1065
- * roots without leaking into a neighbouring plugin. Its closing brace is last. */
1066
- ${scope} {
1067
- color: var(--dsw-alias-label-primary);
1068
- font-family: inherit;
1069
- font-size: 14px;
1070
- line-height: 22px;
1071
- min-width: 0;
1072
-
1073
- & *, & *::before, & *::after { box-sizing: border-box; }
1074
- & h1, & h2, & h3, & h4, & p, & figure { margin: 0; }
1075
- /* Keyboard focus only: pointer input keeps the native resting appearance. The
1076
- * ring needs no radius of its own — the outline already follows the control's
1077
- * own border-radius, and overriding it here would flatten every control. */
1078
- & :focus-visible { ${officialFocus()}; }
1079
- & [hidden] { display: none !important; }
1080
-
1081
- /* --- Type tiers. The same 14/22 and 13/20 steps the primitives use. --- */
1082
- & .dsh-ui-title { font-size: 16px; line-height: 24px; font-weight: 500; color: var(--dsw-alias-label-primary); }
1083
- & .dsh-ui-heading { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
1084
- & .dsh-ui-compact { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary); }
1085
- & .dsh-ui-meta { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
1086
- & .dsh-ui-hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
1087
- & .dsh-ui-error { font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); }
1088
- & .dsh-ui-warn { color: var(--dsw-alias-state-warn-primary); }
1089
- & .dsh-ui-success { color: var(--dsw-alias-state-success-primary); }
1090
- & .dsh-ui-muted { color: var(--dsw-alias-label-tertiary); }
1091
- & .dsh-ui-truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1092
- & .dsh-ui-wrap { min-width: 0; overflow-wrap: anywhere; }
1093
-
1094
- /* --- Layout scaffolding. --- */
1095
- & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
1096
- & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
1097
- /* Plugin detail pages share one reading column. These are layout recipes;
1098
- * controls keep their own primitive styles. Form spacing follows the host's
1099
- * settings-form/fields.module.css and SettingsForm.module.css. */
1100
- & .dsh-ui-page { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 760px; min-width: 0; }
1101
- & .dsh-ui-form { display: flex; flex-direction: column; gap: 0; min-width: 0; }
1102
- &.dsh-ui-form > .dsh-ui-field, & .dsh-ui-form > .dsh-ui-field { padding: 12px 0; }
1103
- & .dsh-ui-form-footer { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-top: 16px; min-width: 0; }
1104
- & .dsh-ui-fields-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; min-width: 0; margin: 0; padding: 0; border: 0; }
1105
- @media (max-width: 560px) {
1106
- & .dsh-ui-fields-grid { grid-template-columns: minmax(0, 1fr); }
1107
- }
1108
- & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
1109
- & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
1110
- & .dsh-ui-spacer { flex: 1; min-width: 0; }
1111
- /* A 0.5px l2 rule is the host's section divider; reserve the same rhythm. */
1112
- & .dsh-ui-divider { height: 0.5px; background: var(--dsw-alias-border-l2); }
1113
- & .dsh-ui-section { display: flex; flex-direction: column; gap: 12px; padding-top: 16px; border-top: 0.5px solid var(--dsw-alias-border-l2); }
1114
-
1115
- /* --- Card: a raised surface whose edge lives in the shadow. --- */
1116
- & .dsh-ui-card {
1117
- ${officialElevation("stroke")}
1118
- display: flex;
1119
- flex-direction: column;
1120
- gap: 12px;
1121
- padding: 16px;
1122
- border-radius: var(--dsw-radius-lg);
1123
- background: var(--dsw-alias-bg-layer-1);
1124
- color: var(--dsw-alias-label-primary);
1125
- min-width: 0;
1126
- }
1127
- & .dsh-ui-card--flat { border: 0.5px solid var(--dsw-alias-border-l3); box-shadow: none; }
1128
- & .dsh-ui-card--nested { background: var(--dsw-alias-bg-layer-2); }
1129
- & .dsh-ui-card--plain { background: none; }
1130
-
1131
- /* --- Field: label, control, help, error. The control itself is an
1132
- * official primitive, so this only owns the vertical rhythm. --- */
1133
- & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
1134
- & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
1135
- /* Grid fields share a row, so the stacked-field divider must not win later in
1136
- * the cascade. Reset it here, after the field recipe. */
1137
- & .dsh-ui-fields-grid > .dsh-ui-field { padding: 0; border: 0; }
1138
- & .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }
1139
- & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
1140
- & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
1141
- & .dsh-ui-help p { margin: 0; }
1142
- & .dsh-ui-help p + p { margin-top: 8px; }
1143
- & .dsh-ui-required { color: var(--dsw-alias-state-error-primary); }
1144
- & .dsh-ui-control { width: 100%; min-width: 0; }
1145
- /* An invalid control is marked on the control, not only in the message. */
1146
- & .dsh-ui-control[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
1147
- & .dsh-ui-readonly { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
1148
- & .dsh-ui-readonly > dt { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
1149
- & .dsh-ui-readonly > dd { margin: 0; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
1150
-
1151
- /* --- Buttons row. A no-op container: it exists so actions line up on the
1152
- * trailing edge the way every native panel does. --- */
1153
- & .dsh-ui-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 4px; }
1154
- & .dsh-ui-actions-end { justify-content: flex-end; }
1155
-
1156
- /* --- Banners. A banner is a left-rail notice, the shape used for warnings,
1157
- * deprecations and blocking instructions. --- */
1158
- & .dsh-ui-banner {
1159
- display: flex;
1160
- flex-direction: column;
1161
- gap: 8px;
1162
- padding: 10px 14px;
1163
- border: 0.5px solid var(--dsw-alias-border-l3);
1164
- border-left: 3px solid var(--dsw-alias-state-warn-primary);
1165
- border-radius: var(--dsw-radius-md);
1166
- background: var(--dsw-alias-bg-layer-1);
1167
- font-size: 13px;
1168
- line-height: 20px;
1169
- color: var(--dsw-alias-label-secondary);
1170
- }
1171
- & .dsh-ui-banner-title { font-weight: 500; color: var(--dsw-alias-state-warn-primary); }
1172
- & .dsh-ui-banner--info { border-left-color: var(--dsw-alias-state-business-primary); }
1173
- & .dsh-ui-banner--danger { border-left-color: var(--dsw-alias-state-error-primary); }
1174
- & .dsh-ui-banner--success { border-left-color: var(--dsw-alias-state-success-primary); }
1175
- & .dsh-ui-banner p { margin: 0; }
1176
-
1177
- /* --- Inline notice: a transient confirmation with no chrome of its own. --- */
1178
- & .dsh-ui-notice { display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-success-primary); }
1179
- & .dsh-ui-notice--error { color: var(--dsw-alias-state-error-primary); }
1180
-
1181
- /* --- Empty and loading states. Both sit in a dashed well so an empty panel
1182
- * reads as "nothing here yet" rather than as a broken layout. --- */
1183
- & .dsh-ui-empty {
1184
- display: flex;
1185
- flex-direction: column;
1186
- align-items: flex-start;
1187
- gap: 6px;
1188
- padding: 20px 16px;
1189
- border: 0.5px dashed var(--dsw-alias-border-l3);
1190
- border-radius: var(--dsw-radius-md);
1191
- font-size: 13px;
1192
- line-height: 20px;
1193
- color: var(--dsw-alias-label-secondary);
1194
- }
1195
- & .dsh-ui-loading { display: flex; align-items: center; gap: 8px; padding: 20px 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }
1196
-
1197
- /* --- Selectable list. A row is a button-sized target with a hover fill and
1198
- * a selected fill; the name carries the weight, the description does not. --- */
1199
- & .dsh-ui-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
1200
- & .dsh-ui-list-scroll { max-height: min(66vh, 760px); overflow-y: auto; overflow-x: hidden; }
1201
- & .dsh-ui-list-row { border-radius: var(--dsw-radius-md); }
1202
- & .dsh-ui-list-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
1203
- & .dsh-ui-list-row[aria-selected='true'], & .dsh-ui-list-row[data-selected] { background: var(--dsw-alias-interactive-bg-active); }
1204
- & .dsh-ui-list-item {
1205
- display: flex;
1206
- flex-direction: column;
1207
- align-items: flex-start;
1208
- gap: 4px;
1209
- width: 100%;
1210
- min-width: 0;
1211
- padding: 10px 12px;
1212
- border: 0;
1213
- border-radius: var(--dsw-radius-md);
1214
- background: none;
1215
- font: inherit;
1216
- color: inherit;
1217
- text-align: start;
1218
- cursor: pointer;
1219
- }
1220
- & .dsh-ui-list-item:disabled { cursor: default; }
1221
- & .dsh-ui-list-item:disabled:hover { background: none; }
1222
- & .dsh-ui-list-name { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }
1223
- & .dsh-ui-list-desc { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
1224
-
1225
- /* --- Code and structured output. Wraps rather than scrolls, because a
1226
- * feature panel is read rather than scanned. --- */
1227
- & .dsh-ui-code {
1228
- padding: 10px 12px;
1229
- border: 0.5px solid var(--dsw-alias-border-l3);
1230
- border-radius: var(--dsw-radius-md);
1231
- background: var(--dsw-alias-bg-layer-1);
1232
- font-family: var(--ds-font-family-code);
1233
- font-size: 12px;
1234
- line-height: 20px;
1235
- color: var(--dsw-alias-label-secondary);
1236
- overflow-wrap: anywhere;
1237
- white-space: pre-wrap;
1238
- }
1239
- & .dsh-ui-scroll { max-height: 60vh; overflow: auto; overscroll-behavior: contain; }
1240
-
1241
- /* --- A toggle row: label on the left, switch on the trailing edge. This is
1242
- * layout only; the switch itself is the official primitive. --- */
1243
- & .dsh-ui-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-width: 0; padding: 12px 0; }
1244
- & .dsh-ui-toggle-row + .dsh-ui-toggle-row { border-top: 0.5px solid var(--dsw-alias-border-l2); }
1245
- & .dsh-ui-toggle-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
1246
- & .dsh-ui-toggle-label { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
1247
-
1248
- /* --- Floating surface: a panel anchored to a trigger. This is the material
1249
- * a menu card is made of — the translucent fill plus its backdrop blur, with
1250
- * the edge riding inside the prominent elevation. A plugin that hand-rolls this
1251
- * is how tokens like --dsw-specific-menu get invented; use this instead. --- */
1252
- & .dsh-ui-surface {
1253
- --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
1254
- --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
1255
- --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
1256
- position: relative;
1257
- display: flex;
1258
- flex-direction: column;
1259
- border: 0;
1260
- border-radius: var(--dsw-radius-lg);
1261
- background: var(--dsw-menu-surface-fill);
1262
- backdrop-filter: var(--dsw-menu-backdrop-filter);
1263
- box-shadow: var(--dsw-elevation-prominent);
1264
- color: var(--dsw-alias-label-primary);
1265
- font-size: 13px;
1266
- line-height: 20px;
1267
- min-width: 0;
1268
- }
1269
- & .dsh-ui-surface-body { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px 14px; overflow-y: auto; }
1270
- & .dsh-ui-surface-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
1271
- & .dsh-ui-surface-title { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
1272
-
1273
- /* --- Native select. The primitives ship no select, so a plugin that offers a
1274
- * closed list of choices keeps the platform control and matches the official
1275
- * settings field geometry exactly (34px, 0.5px l4, radius-md, layer-3). A
1276
- * SegmentedControl is the primitive to reach for when the choices are few
1277
- * enough to show at once. --- */
1278
- & .dsh-ui-select {
1279
- box-sizing: border-box;
1280
- width: 100%;
1281
- min-width: 0;
1282
- height: 34px;
1283
- padding: 0 12px;
1284
- border: 0.5px solid var(--dsw-alias-border-l4);
1285
- border-radius: var(--dsw-radius-md);
1286
- background: var(--dsw-alias-bg-layer-3);
1287
- font: inherit;
1288
- font-size: 13px;
1289
- line-height: 1.5;
1290
- color: var(--dsw-alias-label-primary);
1291
- cursor: pointer;
1292
- }
1293
- & .dsh-ui-select:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
1294
- & .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
1295
- & .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
1296
-
1297
- /* --- Native textarea. The primitives ship no textarea either, so a settings
1298
- * prompt keeps the platform element and the same field shell as the select:
1299
- * 0.5px l4, radius-md, layer-3, 13/1.5 type. It grows vertically instead of
1300
- * locking to the select's 34px row. --- */
1301
- & .dsh-ui-textarea {
1302
- box-sizing: border-box;
1303
- width: 100%;
1304
- min-width: 0;
1305
- resize: vertical;
1306
- padding: 8px 12px;
1307
- border: 0.5px solid var(--dsw-alias-border-l4);
1308
- border-radius: var(--dsw-radius-md);
1309
- background: var(--dsw-alias-bg-layer-3);
1310
- font: inherit;
1311
- font-size: 13px;
1312
- line-height: 1.5;
1313
- color: var(--dsw-alias-label-primary);
1314
- }
1315
- & .dsh-ui-textarea:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
1316
- & .dsh-ui-textarea:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
1317
- & .dsh-ui-textarea::placeholder { color: var(--dsw-alias-label-dimmed); }
1318
- & .dsh-ui-textarea[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
1319
-
1320
- /* Long panels scroll inside themselves instead of growing the host page. */
1321
- & .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
1322
- & .dsh-ui-panel-wide { max-width: 100%; }
1323
-
1324
- @media (prefers-reduced-motion: reduce) {
1325
- & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
1326
- }
1327
- }
1328
- `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?=\s*\{)/gm, (_match, className) => `&${className}, & ${className}`);
1329
- }
1330
- //#endregion
1331
- //#region ../dsh-plugin-kit/src/contracts.ts
1332
- /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
1333
- function modelSettingsChangesIdentity(packageName) {
1334
- const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
1335
- const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
1336
- if (!parts.length) throw new Error("Model settings package name has no RPC segment");
1337
- const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
1338
- if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
1339
- return {
1340
- service: segment,
1341
- namespace: segment
1342
- };
1343
- }
1344
- //#endregion
1345
- //#region ../dsh-plugin-kit/src/model-settings-client.ts
1346
- /** This stream carries invalidations only; ready is never a settings snapshot. */
1347
- function parseModelSettingsChangeFrame(value) {
1348
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid model settings change frame");
1349
- const frame = value;
1350
- if (frame.kind === "ready" && Object.keys(frame).length === 1) return { kind: "ready" };
1351
- if (frame.kind === "changed" && Object.keys(frame).length === 3 && typeof frame.ns === "string" && frame.ns.length > 0 && typeof frame.revision === "number" && Number.isSafeInteger(frame.revision) && frame.revision >= 0) return {
1352
- kind: "changed",
1353
- ns: frame.ns,
1354
- revision: frame.revision
1355
- };
1356
- throw new Error("Invalid model settings change frame");
1357
- }
1358
- /** One owner's contribution. Each package mounts only its own endpoint. */
1359
- function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
1360
- return {
1361
- package: packageName,
1362
- descriptors: [{
1363
- id: `${identity.namespace}/watch`,
1364
- service: identity.service,
1365
- namespace: identity.namespace,
1366
- method: "watch",
1367
- mode: "stream",
1368
- invocation: { kind: "direct" },
1369
- parameters: [],
1370
- cancellation: { parameter: "signal" },
1371
- result: {
1372
- mode: "strict",
1373
- typeSymbol: "ModelSettings.ChangeFrame",
1374
- create: () => ({ parse: parseModelSettingsChangeFrame })
1375
- }
1376
- }]
1377
- };
1378
- }
1379
- /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
1380
- * then withdraws it, so the contribution cannot outlive the plugin. */
1381
- function mountModelSettingsChanges(ctx, packageName) {
1382
- const contribution = modelSettingsChangesRemote(packageName);
1383
- ctx.inject(["remote"], (injected) => {
1384
- const scope = injected;
1385
- const mounting = scope.remote.$mount(contribution);
1386
- mounting.catch(() => {});
1387
- scope.effect(() => async () => {
1388
- await (await mounting.catch(() => void 0))?.();
1389
- });
1390
- });
1391
- }
1392
- /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
1393
- * A factory shares one logical stream while at least one subscriber remains. */
1394
- function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
1395
- const endpoint = identity;
1396
- const listeners = /* @__PURE__ */ new Set();
1397
- let injection;
1398
- let current;
1399
- let lifecycle = 0;
1400
- let failureMessage;
1401
- const report = (message) => {
1402
- failureMessage = message;
1403
- for (const listener of [...listeners]) try {
1404
- listener.error?.(message);
1405
- } catch {}
1406
- };
1407
- const invalidate = () => {
1408
- for (const listener of [...listeners]) {
1409
- if (!listeners.has(listener)) continue;
1410
- try {
1411
- listener.change();
1412
- } catch (error) {
1413
- try {
1414
- listener.error?.(error instanceof Error ? error.message : String(error));
1415
- } catch {}
1416
- }
1417
- }
1418
- };
1419
- const start = () => {
1420
- const life = ++lifecycle;
1421
- injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
1422
- if (life !== lifecycle || !listeners.size) return;
1423
- const scope = injected;
1424
- const service = scope.remote[endpoint.namespace];
1425
- if (!service || !("watch" in service) || !service.watch) {
1426
- report("Model settings sync service unavailable");
1427
- return;
1428
- }
1429
- const watch = service.watch;
1430
- const stream = scope.remote.$stream({
1431
- name: "Model settings sync",
1432
- open: (signal) => watch(signal),
1433
- ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
1434
- });
1435
- const session = {
1436
- stream,
1437
- active: true
1438
- };
1439
- current = session;
1440
- scope.effect(() => () => {
1441
- session.active = false;
1442
- if (current === session) {
1443
- current = void 0;
1444
- if (life === lifecycle && listeners.size) report("Model settings sync service unavailable");
1445
- }
1446
- return stream.dispose();
1447
- });
1448
- (async () => {
1449
- let generation;
1450
- let ready = false;
1451
- try {
1452
- for await (const item of stream) {
1453
- if (!session.active || life !== lifecycle || !listeners.size) break;
1454
- if (item.signal.aborted) continue;
1455
- if (generation !== item.generation) {
1456
- generation = item.generation;
1457
- ready = false;
1458
- }
1459
- const frame = parseModelSettingsChangeFrame(item.value);
1460
- if (frame.kind === "ready") {
1461
- ready = true;
1462
- failureMessage = void 0;
1463
- item.accept();
1464
- invalidate();
1465
- } else {
1466
- if (!ready) throw new Error("Model settings sync change arrived before ready");
1467
- if (accept(frame.ns)) invalidate();
1468
- }
1469
- }
1470
- } catch (error) {
1471
- if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error));
1472
- } finally {
1473
- if (current === session) current = void 0;
1474
- session.active = false;
1475
- await stream.dispose().catch(() => {});
1476
- }
1477
- })();
1478
- });
1479
- queueMicrotask(() => {
1480
- if (life === lifecycle && listeners.size && !current) report("Model settings sync service unavailable");
1481
- });
1482
- };
1483
- return Object.freeze({ subscribe(onChange, onError) {
1484
- const listener = {
1485
- change: onChange,
1486
- error: onError
1487
- };
1488
- listeners.add(listener);
1489
- if (!injection) start();
1490
- else if (failureMessage) queueMicrotask(() => {
1491
- if (listeners.has(listener) && failureMessage) try {
1492
- listener.error?.(failureMessage);
1493
- } catch {}
1494
- });
1495
- return () => {
1496
- if (!listeners.delete(listener) || listeners.size) return;
1497
- lifecycle++;
1498
- if (current) {
1499
- current.active = false;
1500
- current.stream.dispose().catch(() => {});
1501
- }
1502
- current = void 0;
1503
- failureMessage = void 0;
1504
- const previous = injection;
1505
- injection = void 0;
1506
- try {
1507
- Promise.resolve(previous?.dispose()).catch(() => {});
1508
- } catch {}
1509
- };
1510
- } });
1511
- }
1512
- //#endregion
1513
- //#region src/client-model-menu.tsx
1514
- /** One detected field's standard editor: the same ModelMenu every marked field gets. */
1515
- function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, followLabel, reasoningEffort = true, onSet }) {
1516
- const [open, setOpen] = (0, react.useState)(false);
1517
- (0, react.useEffect)(() => {
1518
- if (disabled) setOpen(false);
1519
- }, [disabled]);
1520
- const route = normalizeModelRoute(current);
1521
- const follow = !route.provider || !route.model;
1522
- const choices = (0, react.useMemo)(() => parseModelMenuChoices(catalog, follow ? void 0 : route), [
1523
- catalog,
1524
- follow,
1525
- route.provider,
1526
- route.model
1527
- ]);
1528
- const selected = follow ? void 0 : choices.find((choice) => choice.provider === route.provider && choice.model === route.model);
1529
- const efforts = follow || !reasoningEffort ? [] : modelMenuEffortOptions(selected, route.reasoningEffort || "");
1530
- const currentLabel = follow ? followLabel || t("follow") : selected?.label ?? `${route.provider} / ${route.model}`;
1531
- const selectedEffort = efforts.find((item) => item.id === (route.reasoningEffort || ""));
1532
- const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, t.language) : void 0;
1533
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
1534
- locale: t.language,
1535
- open: open && !disabled,
1536
- onOpenChange: (next) => setOpen(next && !disabled),
1537
- listClassName: "dsh-model-hub",
1538
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1539
- type: "button",
1540
- className: "dsh-model-menu-trigger",
1541
- disabled,
1542
- "aria-label": `${label}: ${currentLabel}`,
1543
- "aria-haspopup": "menu",
1544
- "aria-expanded": open && !disabled,
1545
- children: [
1546
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1547
- className: "dsh-model-menu-triggerLabel",
1548
- children: currentLabel
1549
- }),
1550
- !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1551
- className: "dsh-model-menu-triggerEffort",
1552
- children: effortName ?? t("effortDefault")
1553
- }) : null,
1554
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })
1555
- ]
1556
- }),
1557
- choices,
1558
- selected: follow ? void 0 : {
1559
- provider: route.provider,
1560
- model: route.model
1561
- },
1562
- onPick: (picked) => onSet({
1563
- provider: picked.provider,
1564
- model: picked.model
1565
- }),
1566
- leading: allowFollow ? [{
1567
- id: "follow",
1568
- label: followLabel || t("follow"),
1569
- selected: follow
1570
- }] : [],
1571
- onPickLeading: () => onSet({
1572
- provider: "",
1573
- model: ""
1574
- }),
1575
- efforts,
1576
- selectedEffort: route.reasoningEffort || "",
1577
- onPickEffort: (effort) => onSet(follow ? {
1578
- provider: "",
1579
- model: ""
1580
- } : {
1581
- provider: route.provider,
1582
- model: route.model,
1583
- ...effort ? { reasoningEffort: effort } : {}
1584
- }),
1585
- modelLabel: t("model"),
1586
- modelValue: currentLabel,
1587
- effortLabel: t("effort"),
1588
- defaultEffortLabel: t("effortDefault"),
1589
- searchPlaceholder: t("searchModels"),
1590
- emptyLabel: t("noModels"),
1591
- clearSearchLabel: t("clearSearch")
1592
- });
1472
+ border: none;
1473
+ outline: none;
1474
+ align-items: center;
1475
+ gap: 4px;
1476
+ padding: 0 4px 0 8px;
1477
+ font-size: 13px;
1478
+ font-weight: 400;
1479
+ line-height: 20px;
1480
+ display: flex;
1593
1481
  }
1594
- function messageOf(error) {
1595
- if (error instanceof Error) return error.message;
1596
- if (error && typeof error === "object" && "message" in error) return String(error.message);
1597
- return String(error);
1482
+ .dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
1483
+ .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 var(--dsw-focus-ring-width) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
1484
+ .dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
1485
+ .dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
1486
+ .dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
1487
+ .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform var(--ds-transition-duration) var(--ds-ease-in-out); }
1488
+ .dsh-model-menu-chevronOpen { transform: rotate(180deg); }
1489
+ /* Portal overlay: the host publishes no z-index token, so the card keeps the
1490
+ * native menu layer's 1100 verbatim. */
1491
+ .dsh-model-menu-menu {
1492
+ z-index: 1100;
1493
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
1494
+ width: max-content;
1495
+ min-width: min(240px, 100vw - 32px);
1496
+ max-width: min(420px, 100vw - 32px);
1497
+ max-height: min(360px, 100vh - 96px);
1498
+ box-shadow: var(--dsw-elevation-prominent);
1499
+ color: var(--dsw-alias-label-primary);
1500
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
1501
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
1502
+ border: 0;
1503
+ flex-direction: column;
1504
+ padding: 4px;
1505
+ display: flex;
1506
+ position: fixed;
1507
+ overflow: hidden;
1508
+ }
1509
+ .dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
1510
+ .dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
1511
+ .dsh-model-menu-searchRow .dsh-model-menu-search { border-radius: var(--dsw-radius-md); background: none; border: 0 solid transparent; height: auto; padding: 5px 7px; display: flex; }
1512
+ .dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
1513
+ .dsh-model-menu-searchWithQuery { padding-right: 34px; }
1514
+ .dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
1515
+ .dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
1516
+ /* The clear button is a circle by geometry (border-radius: 50%), a shape the
1517
+ * radius token scale does not describe. */
1518
+ .dsh-model-menu-searchClear {
1519
+ width: 24px;
1520
+ height: 24px;
1521
+ color: var(--dsw-alias-label-secondary);
1522
+ cursor: pointer;
1523
+ background: none;
1524
+ border: none;
1525
+ border-radius: 50%;
1526
+ justify-content: center;
1527
+ align-items: center;
1528
+ padding: 0;
1529
+ display: inline-flex;
1530
+ position: absolute;
1531
+ top: 50%;
1532
+ right: 4px;
1533
+ transform: translateY(-50%);
1534
+ }
1535
+ .dsh-model-menu-searchClear:hover, .dsh-model-menu-searchClear:focus-visible { background: var(--dsw-alias-interactive-bg-hover); outline: none; }
1536
+ .dsh-model-menu-groups { min-height: 0; overflow-y: auto; }
1537
+ .dsh-model-menu-leading { border-bottom: 0.5px solid var(--dsw-alias-border-l2); margin-bottom: 3px; padding-bottom: 3px; }
1538
+ .dsh-model-menu-option {
1539
+ box-sizing: border-box;
1540
+ border-radius: var(--dsw-radius-md);
1541
+ width: auto;
1542
+ min-width: 100%;
1543
+ min-height: 34px;
1544
+ color: inherit;
1545
+ text-align: left;
1546
+ cursor: pointer;
1547
+ background: none;
1548
+ border: none;
1549
+ outline: none;
1550
+ align-items: center;
1551
+ gap: 6px;
1552
+ padding: 5px 7px;
1553
+ display: flex;
1554
+ }
1555
+ .dsh-model-menu-option:not(.dsh-model-menu-modelOption):hover:not(:disabled),
1556
+ .dsh-model-menu-option:focus-visible,
1557
+ .dsh-model-menu-optionActive:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
1558
+ .dsh-model-menu-modelOption:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
1559
+ .dsh-model-menu-selected { background: none; }
1560
+ .dsh-model-menu-option:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
1561
+ .dsh-model-menu-optionCopy { flex-direction: column; flex: 1; min-width: 0; display: flex; }
1562
+ .dsh-model-menu-modelName { color: inherit; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 400; line-height: 18px; overflow: hidden; }
1563
+ .dsh-model-menu-check { color: var(--dsw-alias-label-primary); flex: 0 0 14px; place-items: center; display: grid; }
1564
+ .dsh-model-menu-check svg { width: 14px; height: 14px; }
1565
+ .dsh-model-menu-cell {
1566
+ box-sizing: border-box;
1567
+ border-radius: var(--dsw-radius-md);
1568
+ width: auto;
1569
+ min-width: 100%;
1570
+ height: 34px;
1571
+ color: var(--dsw-alias-label-primary);
1572
+ cursor: pointer;
1573
+ text-align: left;
1574
+ background: none;
1575
+ border: none;
1576
+ outline: none;
1577
+ align-items: center;
1578
+ gap: 6px;
1579
+ padding: 0 8px;
1580
+ font-size: 13px;
1581
+ line-height: 20px;
1582
+ display: flex;
1598
1583
  }
1584
+ .dsh-model-menu-cell:hover, .dsh-model-menu-cell:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
1585
+ .dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
1586
+ .dsh-model-menu-cellValue { text-overflow: ellipsis; white-space: nowrap; text-align: right; min-width: 0; color: var(--dsw-alias-label-tertiary); flex: auto; overflow: hidden; }
1587
+ .dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
1588
+ `;
1599
1589
  //#endregion
1600
1590
  //#region src/client-copy.ts
1601
- function hubText(value, language) {
1602
- return typeof value === "string" ? value : value?.[language] ?? "";
1603
- }
1604
1591
  /** Local validation defaults, without rewriting provider diagnostics. */
1605
1592
  const hubMessages = [
1606
1593
  ["请求失败", "Request failed"],
@@ -1683,27 +1670,145 @@ function hubMessage(message, language) {
1683
1670
  return hubMessages.find((pair) => pair.includes(message))?.[language === "zh" ? 0 : 1] ?? message;
1684
1671
  }
1685
1672
  //#endregion
1673
+ //#region src/client-model-menu.tsx
1674
+ /**
1675
+ * One detected field's standard editor: the same ModelMenu every marked field gets.
1676
+ *
1677
+ * `display: 'short'` shows only the model name on the trigger; the accessible
1678
+ * name and the title keep the full provider/model route so two providers that
1679
+ * ship the same model name stay distinguishable. Picking another model keeps
1680
+ * the current reasoning effort when the new model advertises it, so switching a
1681
+ * route does not silently reset the user's choice.
1682
+ */
1683
+ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, followLabel, reasoningEffort = true, display = "full", onSet }) {
1684
+ const [open, setOpen] = (0, react.useState)(false);
1685
+ (0, react.useEffect)(() => {
1686
+ if (disabled) setOpen(false);
1687
+ }, [disabled]);
1688
+ const route = normalizeModelRoute(current);
1689
+ const follow = !route.provider || !route.model;
1690
+ const choices = (0, react.useMemo)(() => parseModelMenuChoices(catalog, follow ? void 0 : route), [
1691
+ catalog,
1692
+ follow,
1693
+ route.provider,
1694
+ route.model
1695
+ ]);
1696
+ const selected = follow ? void 0 : choices.find((choice) => choice.provider === route.provider && choice.model === route.model);
1697
+ const efforts = follow || !reasoningEffort ? [] : modelMenuEffortOptions(selected, route.reasoningEffort || "");
1698
+ const menuLabel = follow ? followLabel || t("follow") : selected?.label ?? `${route.provider} / ${route.model}`;
1699
+ const routeLabel = follow ? menuLabel : `${route.provider} / ${route.model}`;
1700
+ const accessible = label === routeLabel ? routeLabel : `${label}: ${routeLabel}`;
1701
+ const currentLabel = display === "short" && !follow ? selected ? modelMenuShortName(selected) : route.model : menuLabel;
1702
+ const selectedEffort = efforts.find((item) => item.id === (route.reasoningEffort || ""));
1703
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, t.language) : void 0;
1704
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
1705
+ locale: t.language,
1706
+ open: open && !disabled,
1707
+ onOpenChange: (next) => setOpen(next && !disabled),
1708
+ listClassName: "dsh-model-hub",
1709
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1710
+ type: "button",
1711
+ className: "dsh-model-menu-trigger",
1712
+ disabled,
1713
+ "aria-label": accessible,
1714
+ title: routeLabel,
1715
+ "aria-haspopup": "menu",
1716
+ "aria-expanded": open && !disabled,
1717
+ children: [
1718
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1719
+ className: "dsh-model-menu-triggerLabel",
1720
+ children: currentLabel
1721
+ }),
1722
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1723
+ className: "dsh-model-menu-triggerEffort",
1724
+ children: ` · ${effortName ?? t("effortDefault")}`
1725
+ }) : null,
1726
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })
1727
+ ]
1728
+ }),
1729
+ choices,
1730
+ selected: follow ? void 0 : {
1731
+ provider: route.provider,
1732
+ model: route.model
1733
+ },
1734
+ onPick: (picked) => {
1735
+ const choice = choices.find((item) => item.provider === picked.provider && item.model === picked.model);
1736
+ const effort = route.reasoningEffort;
1737
+ const supported = Boolean(effort) && Boolean(choice?.efforts.some((item) => item.id === effort));
1738
+ onSet({
1739
+ provider: picked.provider,
1740
+ model: picked.model,
1741
+ ...supported ? { reasoningEffort: effort } : {}
1742
+ });
1743
+ },
1744
+ leading: allowFollow ? [{
1745
+ id: "follow",
1746
+ label: followLabel || t("follow"),
1747
+ selected: follow
1748
+ }] : [],
1749
+ onPickLeading: () => onSet({
1750
+ provider: "",
1751
+ model: ""
1752
+ }),
1753
+ efforts,
1754
+ selectedEffort: route.reasoningEffort || "",
1755
+ onPickEffort: (effort) => onSet(follow ? {
1756
+ provider: "",
1757
+ model: ""
1758
+ } : {
1759
+ provider: route.provider,
1760
+ model: route.model,
1761
+ ...effort ? { reasoningEffort: effort } : {}
1762
+ }),
1763
+ modelLabel: t("model"),
1764
+ modelValue: currentLabel,
1765
+ effortLabel: t("effort"),
1766
+ defaultEffortLabel: t("effortDefault"),
1767
+ searchPlaceholder: t("searchModels"),
1768
+ emptyLabel: t("noModels"),
1769
+ clearSearchLabel: t("clearSearch")
1770
+ });
1771
+ }
1772
+ function messageOf(error) {
1773
+ if (error instanceof Error) return error.message;
1774
+ if (error && typeof error === "object" && "message" in error) return String(error.message);
1775
+ return String(error);
1776
+ }
1777
+ //#endregion
1686
1778
  //#region src/client-cast.tsx
1687
1779
  /**
1688
- * dsh-model-hub — subagent cast tab. Lists the allowlist of models subagents
1689
- * may pick from, each with an optional free-text task tendency the main agent
1690
- * consults when delegating. Every write goes through the hub host's cast RPC
1691
- * with the native settings revision as CAS; `cast.setTendency` uses its own
1692
- * durable file revision. The page owns its own fetch,
1693
- * subscription and write locking, following the HubPage RequestScope pattern.
1780
+ * dsh-model-hub — subagent configuration page.
1781
+ *
1782
+ * Lists the allowlist of models subagents may pick from. Each row carries the
1783
+ * model, its reasoning effort and an optional free-text task tendency the main
1784
+ * agent consults when delegating.
1785
+ *
1786
+ * Writes are split by owner: the native settings CAS owns provider and model
1787
+ * (the native schema strips a reasoning effort), while the plugin's own file
1788
+ * CAS owns the per-route effort and tendency. A preference write answers with
1789
+ * the committed state, so the page never waits for a later refresh that could
1790
+ * still carry the previous value.
1791
+ */
1792
+ /**
1793
+ * Settings namespaces this page watches. The native route list and the
1794
+ * plugin-side preference store are its own scope; unrelated model-settings
1795
+ * traffic (for example a running subagent's route refresh) must not re-read it.
1796
+ * The literals mirror `CAST_NAMESPACE` and `CAST_TENDENCIES_NAMESPACE` in
1797
+ * cast.ts, which the browser half cannot import (it is host-only code).
1694
1798
  */
1799
+ const CAST_WATCH_NAMESPACES = ["subagent-model-selection-settings", "@klarkxy/dsh-model-hub/cast-tendencies"];
1695
1800
  const TEXT$1 = {
1696
1801
  zh: {
1697
- title: "子代理阵容",
1698
- enabledLabel: "启用子代理阵容",
1699
- enabledHint: "此名单限制子代理的显式选模;不指定模型时仍可继承父会话。名单修改对新建会话生效,已有会话及其子代理沿用原名单。任务倾向供委派时参考。",
1802
+ title: "子代理配置",
1803
+ enabledLabel: "启用子代理选模",
1804
+ enabledHint: "配置新建子代理的模型与思考强度。名单修改对新建会话生效,已有会话沿用原名单。",
1700
1805
  loading: "正在读取…",
1701
1806
  failed: (message) => `读取失败:${message}`,
1702
1807
  syncFailed: (message) => `自动同步不可用:${message}`,
1703
1808
  writeFailed: (message) => `未保存:${message}。请刷新后重试。`,
1704
- saved: "已保存。名单修改对新建会话生效。",
1705
- savedRefreshFailed: (message) => `已保存;阵容刷新失败:${message}。请刷新以读取最新设置。`,
1706
- conflict: "阵容设置已在别处修改,当前输入保留。请刷新核对后重试。",
1809
+ saved: "已保存。",
1810
+ savedRefreshFailed: (message) => `已保存;配置刷新失败:${message}。请刷新以读取最新设置。`,
1811
+ conflict: "配置已在别处修改,当前输入保留。请刷新核对后重试。",
1707
1812
  tendencyFailed: (message) => `任务倾向不可用:${message}。原文件已保留;修复后刷新。`,
1708
1813
  draftChanged: (saved) => `此任务倾向已在别处更新。当前已保存:${saved || "(空)"}。输入已保留;刷新核对后再保存。`,
1709
1814
  refresh: "刷新",
@@ -1722,25 +1827,19 @@ const TEXT$1 = {
1722
1827
  effortDefault: "默认",
1723
1828
  searchModels: "搜索模型…",
1724
1829
  noModels: "没有匹配的模型",
1725
- clearSearch: "清除搜索",
1726
- runningTitle: "运行中的子代理",
1727
- runningEmpty: "当前没有运行中的子代理。",
1728
- runningPending: "尚未发起模型请求",
1729
- runningRemote: "远程子代理,无法读取路由",
1730
- runningFailed: (message) => `运行状态读取失败:${message}`,
1731
- effortAdapterDefault: (effort) => `${effort}(适配器默认)`
1830
+ clearSearch: "清除搜索"
1732
1831
  },
1733
1832
  en: {
1734
- title: "Subagent Cast",
1735
- enabledLabel: "Enable subagent cast",
1736
- enabledHint: "This list limits explicit subagent model choices; omitting a model can still inherit the parent session. List changes apply to new conversations. Existing conversations and their subagents keep their saved lists. Task tendencies guide delegation.",
1833
+ title: "Subagent configuration",
1834
+ enabledLabel: "Enable subagent model selection",
1835
+ enabledHint: "Configure the model and reasoning effort for new subagents. List changes apply to new conversations; existing conversations keep their saved list.",
1737
1836
  loading: "Loading…",
1738
1837
  failed: (message) => `Load failed: ${message}`,
1739
1838
  syncFailed: (message) => `Live sync unavailable: ${message}`,
1740
1839
  writeFailed: (message) => `Not saved: ${message}. Refresh, then try again.`,
1741
- saved: "Saved. List changes apply to new conversations.",
1742
- savedRefreshFailed: (message) => `Saved; cast refresh failed: ${message}. Refresh to read the latest settings.`,
1743
- conflict: "Cast settings changed elsewhere; your draft is preserved. Refresh, review, then try again.",
1840
+ saved: "Saved.",
1841
+ savedRefreshFailed: (message) => `Saved; configuration refresh failed: ${message}. Refresh to read the latest settings.`,
1842
+ conflict: "Settings changed elsewhere; your draft is preserved. Refresh, review, then try again.",
1744
1843
  tendencyFailed: (message) => `Task tendencies unavailable: ${message}. The original file is preserved; repair it, then refresh.`,
1745
1844
  draftChanged: (saved) => `This tendency changed elsewhere. Currently saved: ${saved || "(empty)"}. Your draft is preserved; refresh and review before saving.`,
1746
1845
  refresh: "Refresh",
@@ -1759,13 +1858,7 @@ const TEXT$1 = {
1759
1858
  effortDefault: "Default",
1760
1859
  searchModels: "Search models…",
1761
1860
  noModels: "No matching models",
1762
- clearSearch: "Clear search",
1763
- runningTitle: "Running subagents",
1764
- runningEmpty: "No subagents are running.",
1765
- runningPending: "No model request yet",
1766
- runningRemote: "Remote subagent; route unavailable",
1767
- runningFailed: (message) => `Running state failed: ${message}`,
1768
- effortAdapterDefault: (effort) => `${effort} (adapter default)`
1861
+ clearSearch: "Clear search"
1769
1862
  }
1770
1863
  };
1771
1864
  function useText$1(locale) {
@@ -1788,11 +1881,6 @@ const castCss = `
1788
1881
  .dsh-model-hub .cast-banner{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}
1789
1882
  .dsh-model-hub .cast-add{display:flex}
1790
1883
  .dsh-model-hub .cast-add .dsh-model-menu-root{max-width:100%}
1791
- .dsh-model-hub .cast-running{margin-top:16px}
1792
- .dsh-model-hub .cast-running-title{font-size:14px;line-height:22px;font-weight:600;margin:0 0 4px;color:var(--dsw-alias-label-primary)}
1793
- .dsh-model-hub .cast-running-row{display:flex;gap:12px;align-items:baseline;flex-wrap:wrap;padding:6px 0;border-bottom:.5px solid var(--dsw-alias-border-l1)}
1794
- .dsh-model-hub .cast-running-label{font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;min-width:0}
1795
- .dsh-model-hub .cast-running-route{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;min-width:0}
1796
1884
  @media(max-width:520px){.dsh-model-hub .cast-row{grid-template-columns:minmax(0,1fr);gap:8px}}
1797
1885
  `;
1798
1886
  /** CAS rejections all carry a refresh hint in either language. */
@@ -1805,25 +1893,14 @@ function localizeCastError(message, t) {
1805
1893
  if (duplicate) return t("routeExists", duplicate[1]);
1806
1894
  return hubMessage(message, t.language);
1807
1895
  }
1896
+ /** The native write carries provider and model only; the effort is separate. */
1808
1897
  function routeInput(route) {
1809
1898
  const normalized = normalizeModelRoute(route);
1810
- return normalized.reasoningEffort ? {
1811
- provider: normalized.provider,
1812
- model: normalized.model,
1813
- reasoningEffort: normalized.reasoningEffort
1814
- } : {
1899
+ return {
1815
1900
  provider: normalized.provider,
1816
1901
  model: normalized.model
1817
1902
  };
1818
1903
  }
1819
- /** One live subagent row: label (or short id) plus its effective route. */
1820
- function runningRouteText(entry, t) {
1821
- if (!entry.local) return t("runningRemote");
1822
- const route = entry.route;
1823
- if (!route) return t("runningPending");
1824
- const effort = route.reasoningEffort ? entry.effortIsAdapterDefault ? t("effortAdapterDefault", route.reasoningEffort) : route.reasoningEffort : "";
1825
- return `${route.provider} / ${route.model}${effort ? ` · ${effort}` : ""}`;
1826
- }
1827
1904
  /** One cast entry: model picker, tendency input saved on blur, remove button. */
1828
1905
  function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, onSetRoute, onSetTendency, onRemove }) {
1829
1906
  const label = `${route.provider} / ${route.model}`;
@@ -1865,7 +1942,8 @@ function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, t
1865
1942
  current: route,
1866
1943
  label,
1867
1944
  allowFollow: false,
1868
- reasoningEffort: false,
1945
+ reasoningEffort: true,
1946
+ display: "short",
1869
1947
  disabled: menuDisabled,
1870
1948
  onSet: onSetRoute
1871
1949
  })
@@ -1907,8 +1985,6 @@ function CastPage({ api, locale }) {
1907
1985
  const t = useText$1(locale);
1908
1986
  const [cast, setCast] = (0, react.useState)(null);
1909
1987
  const [catalog, setCatalog] = (0, react.useState)(null);
1910
- const [running, setRunning] = (0, react.useState)(null);
1911
- const [runningError, setRunningError] = (0, react.useState)("");
1912
1988
  const [loadError, setLoadError] = (0, react.useState)("");
1913
1989
  const [catalogError, setCatalogError] = (0, react.useState)("");
1914
1990
  const [syncError, setSyncError] = (0, react.useState)("");
@@ -1918,6 +1994,7 @@ function CastPage({ api, locale }) {
1918
1994
  const [saving, setSaving] = (0, react.useState)("");
1919
1995
  const [refreshing, setRefreshing] = (0, react.useState)(false);
1920
1996
  const [draftReview, setDraftReview] = (0, react.useState)(0);
1997
+ const [pending, setPending] = (0, react.useState)(void 0);
1921
1998
  const scopeRef = (0, react.useRef)(null);
1922
1999
  const refresh = (0, react.useCallback)(async (scope, reviewDrafts = false) => {
1923
2000
  if (!scope.active) return;
@@ -1928,11 +2005,7 @@ function CastPage({ api, locale }) {
1928
2005
  scope.queued = false;
1929
2006
  scope.busy = true;
1930
2007
  setRefreshing(true);
1931
- const [castResult, models, live] = await Promise.allSettled([
1932
- Promise.resolve().then(() => scope.api.getCast()),
1933
- Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedModelCatalog),
1934
- Promise.resolve().then(() => scope.api.listRunning())
1935
- ]);
2008
+ const [castResult, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.getCast()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedModelCatalog)]);
1936
2009
  if (!scope.active) return;
1937
2010
  if (castResult.status === "fulfilled") {
1938
2011
  setCast(castResult.value);
@@ -1946,10 +2019,6 @@ function CastPage({ api, locale }) {
1946
2019
  setCatalog(models.value);
1947
2020
  setCatalogError("");
1948
2021
  } else setCatalogError(messageOf(models.reason));
1949
- if (live.status === "fulfilled") {
1950
- setRunning(live.value);
1951
- setRunningError("");
1952
- } else setRunningError(messageOf(live.reason));
1953
2022
  scope.busy = false;
1954
2023
  setRefreshing(false);
1955
2024
  if (scope.queued) refresh(scope);
@@ -1971,8 +2040,7 @@ function CastPage({ api, locale }) {
1971
2040
  setSyncError("");
1972
2041
  setCast(null);
1973
2042
  setCatalog(null);
1974
- setRunning(null);
1975
- setRunningError("");
2043
+ setPending(void 0);
1976
2044
  refresh(scope);
1977
2045
  const unsubscribe = api.subscribeChanges?.(() => {
1978
2046
  if (!scope.active) return;
@@ -1998,7 +2066,10 @@ function CastPage({ api, locale }) {
1998
2066
  if ("committed" in value) {
1999
2067
  setSaved(true);
2000
2068
  scope.queued = true;
2001
- } else setCast(value);
2069
+ } else {
2070
+ setCast(value);
2071
+ setSaved(true);
2072
+ }
2002
2073
  setWriteError("");
2003
2074
  setConflict(false);
2004
2075
  }, (error) => {
@@ -2019,6 +2090,7 @@ function CastPage({ api, locale }) {
2019
2090
  const scope = scopeRef.current;
2020
2091
  if (scope?.api === api && !scope.busy) await refresh(scope, true);
2021
2092
  };
2093
+ const activePending = pending && cast?.routes.some((route) => route.provider === pending.provider && route.model === pending.model) ? pending : void 0;
2022
2094
  const disabled = saving !== "" || refreshing || writeError !== "" || saved && loadError !== "";
2023
2095
  const catalogStatus = {
2024
2096
  loading: refreshing,
@@ -2026,6 +2098,39 @@ function CastPage({ api, locale }) {
2026
2098
  failures: modelCatalogFailures(catalog),
2027
2099
  refresh: refreshNow
2028
2100
  };
2101
+ const preferencesRevision = cast?.tendencyRevision ?? 0;
2102
+ /** Save one route's plugin-side preferences; the answer is the new state. */
2103
+ const savePreferences = (provider, model, preferences, key) => {
2104
+ mutate(key, (scope) => scope.api.setCastRoutePreferences({
2105
+ provider,
2106
+ model,
2107
+ preferences,
2108
+ expectedRevision: preferencesRevision
2109
+ }));
2110
+ };
2111
+ /** Apply one pick of the row's menu: a new model, or only a new effort. */
2112
+ const applyRoute = (handle, route, next) => {
2113
+ const effort = next.reasoningEffort ?? "";
2114
+ if (next.provider === route.provider && next.model === route.model) {
2115
+ if (effort === (route.reasoningEffort ?? "")) return;
2116
+ savePreferences(route.provider, route.model, { reasoningEffort: effort }, `effort:${handle}`);
2117
+ return;
2118
+ }
2119
+ mutate(`route:${handle}`, async (scope) => {
2120
+ const commit = await scope.api.setCastRoute({
2121
+ index: handle,
2122
+ route: routeInput(next),
2123
+ expectedRevision: cast?.revision ?? 0
2124
+ });
2125
+ if (!next.reasoningEffort) return commit;
2126
+ return scope.api.setCastRoutePreferences({
2127
+ provider: next.provider,
2128
+ model: next.model,
2129
+ preferences: { reasoningEffort: next.reasoningEffort },
2130
+ expectedRevision: preferencesRevision
2131
+ });
2132
+ });
2133
+ };
2029
2134
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2030
2135
  className: "cast-root dsh-model-hub dsh-ui-stack",
2031
2136
  "aria-label": t("title"),
@@ -2117,11 +2222,7 @@ function CastPage({ api, locale }) {
2117
2222
  tendencyDisabled: disabled || !!cast.tendencyError || cast.tendencyRevision === void 0,
2118
2223
  disabled,
2119
2224
  menuDisabled: disabled || catalog === null,
2120
- onSetRoute: (next) => mutate(`route:${handle}`, (scope) => scope.api.setCastRoute({
2121
- index: handle,
2122
- route: routeInput(next),
2123
- expectedRevision: cast.revision
2124
- })),
2225
+ onSetRoute: (next) => applyRoute(handle, route, next),
2125
2226
  onSetTendency: (tendency, expectedRevision) => mutate(`tendency:${handle}`, (scope) => scope.api.setCastTendency({
2126
2227
  provider: route.provider,
2127
2228
  model: route.model,
@@ -2134,507 +2235,944 @@ function CastPage({ api, locale }) {
2134
2235
  }))
2135
2236
  }, JSON.stringify([route.provider, route.model]));
2136
2237
  }),
2137
- cast !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2138
2239
  className: "cast-add",
2139
2240
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldMenu, {
2140
2241
  t,
2141
2242
  catalog,
2142
- current: void 0,
2243
+ current: activePending,
2143
2244
  label: t("addModel"),
2144
2245
  allowFollow: false,
2145
2246
  followLabel: t("addModel"),
2146
- reasoningEffort: false,
2247
+ reasoningEffort: true,
2248
+ display: "short",
2147
2249
  disabled: disabled || catalog === null,
2148
- onSet: (picked) => mutate("add", (scope) => scope.api.addCastRoute({
2149
- route: routeInput(picked),
2150
- expectedRevision: cast.revision
2151
- }))
2250
+ onSet: (picked) => {
2251
+ if (!picked.provider || !picked.model) {
2252
+ setPending(void 0);
2253
+ return;
2254
+ }
2255
+ if (activePending && activePending.provider === picked.provider && activePending.model === picked.model) {
2256
+ const effort = picked.reasoningEffort ?? "";
2257
+ if (effort === (activePending.reasoningEffort ?? "")) return;
2258
+ mutate("add-effort", async (scope) => {
2259
+ const state = await scope.api.setCastRoutePreferences({
2260
+ provider: picked.provider,
2261
+ model: picked.model,
2262
+ preferences: { reasoningEffort: effort },
2263
+ expectedRevision: preferencesRevision
2264
+ });
2265
+ setPending(void 0);
2266
+ return state;
2267
+ });
2268
+ return;
2269
+ }
2270
+ mutate("add", async (scope) => {
2271
+ const commit = await scope.api.addCastRoute({
2272
+ route: routeInput(picked),
2273
+ expectedRevision: cast?.revision ?? 0
2274
+ });
2275
+ setPending(picked);
2276
+ if (!picked.reasoningEffort) return commit;
2277
+ return scope.api.setCastRoutePreferences({
2278
+ provider: picked.provider,
2279
+ model: picked.model,
2280
+ preferences: { reasoningEffort: picked.reasoningEffort },
2281
+ expectedRevision: preferencesRevision
2282
+ });
2283
+ });
2284
+ }
2152
2285
  })
2153
- }) : null,
2154
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2155
- className: "cast-running",
2156
- "aria-label": t("runningTitle"),
2157
- children: [
2158
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2159
- className: "cast-running-title",
2160
- children: t("runningTitle")
2161
- }),
2162
- runningError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2163
- role: "alert",
2164
- className: "dsh-ui-error dsh-ui-wrap",
2165
- children: t("runningFailed", hubMessage(runningError, t.language))
2166
- }) : null,
2167
- running === null && !runningError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2168
- className: "dsh-ui-loading",
2169
- children: t("loading")
2170
- }) : null,
2171
- running !== null && running.entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2172
- className: "dsh-ui-help",
2173
- children: t("runningEmpty")
2174
- }) : null,
2175
- running?.entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2176
- className: "cast-running-row",
2177
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2178
- className: "cast-running-label",
2179
- children: entry.label || entry.id
2180
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2181
- className: "cast-running-route",
2182
- children: runningRouteText(entry, t)
2183
- })]
2184
- }, entry.id))
2185
- ]
2186
2286
  })
2187
2287
  ]
2188
2288
  });
2189
2289
  }
2190
2290
  //#endregion
2191
- //#region src/client.tsx
2291
+ //#region src/client-subagent-catalog.tsx
2192
2292
  /**
2193
- * dsh-model-hub — browser half. One settings-page section listing every
2194
- * detected model-route field across plugins. Each row is edited with the
2195
- * contract's standard ModelMenu (dsh-model-route/ui) and written back through
2196
- * the hub's host RPC — `settings.mutate` on the host, CAS by descriptor
2197
- * revision. An empty route means "follow the host default" and unsets the
2198
- * field.
2199
- */
2200
- const name = "dsh-model-hub-client";
2201
- const inject = [
2202
- "slots",
2203
- "connection",
2204
- ...MODEL_CATALOG_INJECT,
2205
- "locale"
2206
- ];
2207
- const CHANNEL = "/dsh-model-hub";
2208
- const BUNDLE = "@klarkxy/dsh-model-hub";
2209
- const TEXT = {
2210
- zh: {
2211
- title: "模型枢纽",
2212
- intro: "一页查看并修改各插件的模型路由;改动立即写入对应插件自己的设置。",
2213
- tabsLabel: "模型枢纽分区",
2214
- tabFields: "字段总览",
2215
- tabCast: "子代理阵容",
2216
- loading: "正在读取…",
2217
- empty: "没有检测到模型字段。",
2218
- failed: (message) => `读取失败:${message}`,
2219
- syncFailed: (message) => `自动同步不可用:${message}`,
2220
- catalogFailed: (message) => `模型列表读取失败:${message}`,
2221
- writeFailed: (message) => `未保存:${message}。请刷新后重新选择。`,
2222
- saved: "已保存,正在刷新…",
2223
- savedRefreshFailed: (message) => `已保存;页面刷新失败:${message}。请刷新以读取最新设置。`,
2224
- refresh: "刷新",
2225
- refreshing: "正在刷新…",
2226
- saving: "正在保存…",
2227
- search: "搜索插件、字段或当前模型…",
2228
- noFields: "没有匹配的模型字段",
2229
- follow: "跟随宿主默认",
2230
- restorePluginDefault: "恢复插件默认",
2231
- model: "模型",
2232
- effort: "思考强度",
2233
- effortDefault: "默认",
2234
- searchModels: "搜索模型…",
2235
- noModels: "没有匹配的模型",
2236
- clearSearch: "清除搜索",
2237
- collectionTemplate: "集合中没有可编辑的条目;请先在所属插件中添加条目。",
2238
- items: "条目"
2293
+ * dsh-model-hub — read-only subagent model readout inside the host's own
2294
+ * subagent catalog popup.
2295
+ *
2296
+ * The host owns two session-header cells for the subagent catalog:
2297
+ * `conversation.session.header.actions` with id `subagent-catalog` (the
2298
+ * descendant count) and `conversation.session.header.lineage` (the breadcrumb
2299
+ * switcher plus the descendant count of the current child). This module
2300
+ * registers a copy of both cells under the host's own ids with a lower
2301
+ * priority, so the plugin's version is the one the header renders while the
2302
+ * host's entry stays in the slot ledger and returns when the plugin unloads.
2303
+ *
2304
+ * The copy keeps the host's tree, navigation, sidebar, hover and keyboard
2305
+ * behaviour and adds one read-only line per catalog row: the model and the
2306
+ * reasoning effort that child session actually used. Native copy comes from
2307
+ * the host's `subagent` locale namespace; only the new route labels are local.
2308
+ *
2309
+ * The route comes from the session's `modelSelection` projection view, read as
2310
+ * `projectionValues.modelSelection.lastUsed`. The host derives that value from
2311
+ * the last `request/header` event, so it is the selection a request really
2312
+ * used. `next` is the selection intended for the *next* request (a pending
2313
+ * choice) and is never shown as an already-used route. A child that never
2314
+ * made a model request reports `lastUsed: null`, which is reported as "no
2315
+ * model request yet" rather than as a route.
2316
+ *
2317
+ * The catalog rows live in the host's private DOM contract: this module keeps
2318
+ * the host's CSS-module class names (pinned per host build) and its tree
2319
+ * roles, so the popup keeps the host's own look. When the host renames those
2320
+ * classes the popup falls back to this module's own stylesheet.
2321
+ */
2322
+ /**
2323
+ * Native catalog text is supplied by the host's `subagent` translator.
2324
+ * Only the new route readout needs local translations.
2325
+ */
2326
+ const TEXT = {
2327
+ zh: {
2328
+ "route.effort": "思考强度",
2329
+ "route.effortDefault": "默认",
2330
+ "route.loading": "读取中…",
2331
+ "route.failed": "模型信息读取失败,点击重试",
2332
+ "route.notApplicable": "不适用",
2333
+ "route.noRequest": "尚未发起模型请求"
2239
2334
  },
2240
2335
  en: {
2241
- title: "Model Hub",
2242
- intro: "View and edit every plugin's model route on one page; changes write into the owning plugin's own settings at once.",
2243
- tabsLabel: "Model Hub sections",
2244
- tabFields: "Fields",
2245
- tabCast: "Subagent Cast",
2246
- loading: "Loading…",
2247
- empty: "No model fields detected.",
2248
- failed: (message) => `Load failed: ${message}`,
2249
- syncFailed: (message) => `Live sync unavailable: ${message}`,
2250
- catalogFailed: (message) => `Model list failed: ${message}`,
2251
- writeFailed: (message) => `Not saved: ${message}. Refresh, then choose again.`,
2252
- saved: "Saved; refreshing…",
2253
- savedRefreshFailed: (message) => `Saved; page refresh failed: ${message}. Refresh to read the latest settings.`,
2254
- refresh: "Refresh",
2255
- refreshing: "Refreshing…",
2256
- saving: "Saving…",
2257
- search: "Search plugins, fields or current models…",
2258
- noFields: "No matching model fields",
2259
- follow: "Follow host default",
2260
- restorePluginDefault: "Restore plugin default",
2261
- model: "Model",
2262
- effort: "Reasoning",
2263
- effortDefault: "Default",
2264
- searchModels: "Search models…",
2265
- noModels: "No matching models",
2266
- clearSearch: "Clear search",
2267
- collectionTemplate: "No collection item to edit. Add an item in the owning plugin first.",
2268
- items: "Items"
2336
+ "route.effort": "Reasoning",
2337
+ "route.effortDefault": "Default",
2338
+ "route.loading": "Loading…",
2339
+ "route.failed": "Could not load model information. Click to retry.",
2340
+ "route.notApplicable": "Not applicable",
2341
+ "route.noRequest": "No model request yet"
2269
2342
  }
2270
2343
  };
2344
+ /** Replace `{name}` placeholders; unknown names keep the placeholder. */
2345
+ function interpolate(template, params) {
2346
+ if (!params) return template;
2347
+ return template.replace(/\{(\w+)\}/g, (match, name) => {
2348
+ const value = params[name];
2349
+ return value === void 0 ? match : String(value);
2350
+ });
2351
+ }
2352
+ /** The host locale, one language at a time; missing services read as English. */
2271
2353
  function useText(locale) {
2272
2354
  const subscribe = (0, react.useCallback)((notify) => typeof locale === "object" ? locale.subscribe?.(notify) ?? (() => {}) : () => {}, [locale]);
2273
- const snapshot = (0, react.useCallback)(() => typeof locale === "string" ? locale : locale?.getSnapshot?.()?.active ?? "en", [locale]);
2274
- const active = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot);
2275
- const table = String(active || "en").toLowerCase().startsWith("zh") ? TEXT.zh : TEXT.en;
2276
- return (0, react.useMemo)(() => Object.assign((key, ...args) => {
2277
- const value = table[key];
2278
- return typeof value === "function" ? value(...args) : value;
2279
- }, { language: table === TEXT.zh ? "zh" : "en" }), [table]);
2280
- }
2281
- const css = `${officialUiCss("dsh-model-hub")}${modelMenuCss}
2282
- .dsh-model-hub .hub-toolbar{display:flex;gap:12px;align-items:center}
2283
- .dsh-model-hub .hub-search{flex:1;min-width:0}
2284
- .dsh-model-hub .hub-group{min-width:0}
2285
- .dsh-model-hub .hub-group-head{display:flex;gap:8px;align-items:baseline;flex-wrap:wrap;margin:12px 0 4px}
2286
- .dsh-model-hub .hub-group-title{font-size:14px;line-height:22px;font-weight:600;margin:0;color:var(--dsw-alias-label-primary)}
2287
- .dsh-model-hub .hub-namespace{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}
2288
- .dsh-model-hub .hub-row{display:grid;grid-template-columns:minmax(140px,1fr) minmax(180px,1fr);gap:16px;align-items:center;padding:8px 0;border-bottom:.5px solid var(--dsw-alias-border-l1)}
2289
- .dsh-model-hub .hub-row-copy{min-width:0}
2290
- .dsh-model-hub .hub-row-label{font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}
2291
- .dsh-model-hub .hub-row-description{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;margin:2px 0 0}
2292
- .dsh-model-hub .hub-editor{min-width:0;display:flex;justify-content:flex-end}
2293
- .dsh-model-hub .hub-editor .dsh-model-menu-root{max-width:100%}
2294
- .dsh-model-hub .hub-editor .dsh-model-menu-trigger{max-width:100%}
2295
- @media(max-width:520px){.dsh-model-hub .hub-toolbar{gap:8px}.dsh-model-hub .hub-row{grid-template-columns:minmax(0,1fr);gap:4px}.dsh-model-hub .hub-editor{justify-content:flex-start}.dsh-model-hub .hub-namespace{width:100%}}
2355
+ const snapshot = (0, react.useCallback)(() => String(typeof locale === "string" ? locale : locale?.getSnapshot?.()?.active ?? "en"), [locale]);
2356
+ const table = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot).toLowerCase().startsWith("zh") ? TEXT.zh : TEXT.en;
2357
+ return (0, react.useCallback)((key, params) => interpolate(table[key], params), [table]);
2358
+ }
2359
+ /**
2360
+ * The host's CSS-module class names for the subagent catalog, as of host
2361
+ * 0.2.2-rc.1. The host's stylesheet is injected into the document, so these
2362
+ * names keep the popup on the host's own look; the `mh-` classes below carry
2363
+ * a fallback stylesheet for hosts that renamed them.
2364
+ */
2365
+ const NATIVE_CLASS = {
2366
+ activitySlot: "rezYhW_activitySlot",
2367
+ ancestorSwitcherTrigger: "rezYhW_ancestorSwitcherTrigger",
2368
+ children: "rezYhW_children",
2369
+ clickarea: "rezYhW_clickarea",
2370
+ content: "rezYhW_content",
2371
+ currentLabel: "rezYhW_currentLabel",
2372
+ disclosure: "rezYhW_disclosure",
2373
+ disclosureOpen: "rezYhW_disclosureOpen",
2374
+ disclosureSpace: "rezYhW_disclosureSpace",
2375
+ error: "rezYhW_error",
2376
+ label: "rezYhW_label",
2377
+ menu: "rezYhW_menu",
2378
+ menuBody: "rezYhW_menuBody",
2379
+ metricDuration: "rezYhW_metricDuration",
2380
+ metricToken: "rezYhW_metricToken",
2381
+ metrics: "rezYhW_metrics",
2382
+ node: "rezYhW_node",
2383
+ notice: "rezYhW_notice",
2384
+ refresh: "rezYhW_refresh",
2385
+ root: "rezYhW_root",
2386
+ row: "rezYhW_row",
2387
+ rowActivitySlot: "rezYhW_rowActivitySlot",
2388
+ sidebarButton: "rezYhW_sidebarButton",
2389
+ sidebarSpace: "rezYhW_sidebarSpace",
2390
+ summary: "rezYhW_summary",
2391
+ switcherRoot: "rezYhW_switcherRoot",
2392
+ switcherTitle: "rezYhW_switcherTitle",
2393
+ switcherTrigger: "rezYhW_switcherTrigger",
2394
+ trigger: "rezYhW_trigger",
2395
+ triggerOpen: "rezYhW_triggerOpen"
2396
+ };
2397
+ /** Join class names, dropping empty ones. */
2398
+ function cx(...parts) {
2399
+ return parts.filter(Boolean).join(" ");
2400
+ }
2401
+ /**
2402
+ * Fallback stylesheet. Every selector is wrapped in `:where()` so it carries
2403
+ * no specificity: while the host's own stylesheet is present, the host wins;
2404
+ * when the host renamed its classes, these rules keep the popup readable.
2405
+ */
2406
+ const css$1 = `
2407
+ .dsh-model-hub-subagent :where(.mh-root){position:relative;display:inline-flex;align-items:center;gap:10px;min-width:0}
2408
+ .dsh-model-hub-subagent :where(.mh-switcher-root){min-width:0;margin-left:6px}
2409
+ .dsh-model-hub-subagent :where(.mh-trigger),.dsh-model-hub-subagent :where(.mh-switcher-trigger){display:inline-flex;align-items:center;gap:4px;min-height:28px;padding:3px 2px;border:0;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;cursor:pointer}
2410
+ .dsh-model-hub-subagent :where(.mh-switcher-trigger){min-width:0;max-width:244px;color:var(--dsw-alias-label-primary);font-weight:500}
2411
+ .dsh-model-hub-subagent :where(.mh-ancestor-switcher-trigger){color:var(--dsw-alias-label-tertiary);font-weight:400}
2412
+ .dsh-model-hub-subagent :where(.mh-switcher-title){flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2413
+ .dsh-model-hub-subagent :where(.mh-activity-slot){display:inline-flex;flex:none;align-items:center;justify-content:center;width:14px;height:14px}
2414
+ .dsh-model-hub-subagent :where(.mh-trigger-open){transform:rotate(180deg)}
2415
+ .dsh-model-hub-subagent :where(.mh-menu){position:fixed;z-index:100;box-sizing:border-box;display:flex;flex-direction:column;width:336px;max-width:min(400px,calc(100vw - 32px));max-height:min(560px,calc(100vh - 140px));padding:3px;overflow:hidden;border-radius:var(--dsw-radius-lg);box-shadow:var(--dsw-elevation-prominent);background:var(--dsw-alias-bg-layer-3)}
2416
+ .dsh-model-hub-subagent :where(.mh-menu-body){display:flex;flex-direction:column;flex:1 1 auto;min-height:0;overflow:auto}
2417
+ .dsh-model-hub-subagent :where(.mh-node){position:relative;min-width:0}
2418
+ .dsh-model-hub-subagent :where(.mh-row){position:relative;display:flex;align-items:flex-start;gap:6px;box-sizing:border-box;width:100%;min-height:44px;padding:6px 7px 6px 9px;border:0;border-radius:var(--dsw-radius-lg);background:transparent;color:var(--dsw-alias-label-primary);font-size:12px;line-height:17px;text-align:left;cursor:pointer;outline:none}
2419
+ .dsh-model-hub-subagent :where(.mh-clickarea){box-sizing:border-box;display:flex;flex:1;align-self:stretch;align-items:flex-start;gap:6px;min-width:0;margin:-6px -7px;padding:6px 7px;border-radius:var(--dsw-radius-lg)}
2420
+ .dsh-model-hub-subagent :where(.mh-row-activity-slot){display:inline-flex;flex:none;align-items:center;justify-content:center;width:14px;height:17px}
2421
+ .dsh-model-hub-subagent :where(.mh-disclosure),.dsh-model-hub-subagent :where(.mh-disclosure-space){flex:none;width:14px;height:17px}
2422
+ .dsh-model-hub-subagent :where(.mh-disclosure){display:inline-flex;align-items:center;justify-content:center;padding:0;border:0;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer}
2423
+ .dsh-model-hub-subagent :where(.mh-disclosure-open){transform:rotate(90deg)}
2424
+ .dsh-model-hub-subagent :where(.mh-content){display:flex;flex:1;flex-direction:column;min-width:0}
2425
+ .dsh-model-hub-subagent :where(.mh-label),.dsh-model-hub-subagent :where(.mh-summary){overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2426
+ .dsh-model-hub-subagent :where(.mh-summary),.dsh-model-hub-subagent :where(.mh-metrics){color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:15px}
2427
+ .dsh-model-hub-subagent :where(.mh-metrics){display:grid;grid-template-rows:17px 15px;flex:none;font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap}
2428
+ .dsh-model-hub-subagent :where(.mh-sidebar-button){display:inline-flex;flex:none;align-items:center;justify-content:center;width:28px;height:28px;margin:4px 0;padding:6px;border:0;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer}
2429
+ .dsh-model-hub-subagent :where(.mh-children){position:relative;margin-left:16px;padding-left:3px}
2430
+ .dsh-model-hub-subagent :where(.mh-notice),.dsh-model-hub-subagent :where(.mh-error){padding:8px 10px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
2431
+ .dsh-model-hub-subagent :where(.mh-error){display:flex;align-items:center;justify-content:space-between;gap:10px;color:var(--dsw-alias-state-error-primary)}
2432
+ .dsh-model-hub-subagent :where(.mh-refresh){display:inline-flex;flex:none;align-items:center;gap:3px;padding:3px 5px;border:0;border-radius:var(--dsw-radius-sm);background:transparent;color:inherit;cursor:pointer}
2433
+ .dsh-model-hub-subagent :where(.mh-route){display:block;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}
2434
+ .dsh-model-hub-subagent :where(.mh-route-retry){padding:0;border:0;background:transparent;color:inherit;font:inherit;cursor:pointer;text-decoration:underline}
2435
+ .dsh-model-hub-subagent :where(.mh-sidebar-space){flex:none;width:28px}
2296
2436
  `;
2297
- function Style() {
2298
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css });
2437
+ /**
2438
+ * Read one child's route.
2439
+ *
2440
+ * Only `lastUsed` counts: it is the selection a request really used. A child
2441
+ * whose projections are not loaded yet is `loading` (the host is asked for
2442
+ * them); a loaded child that never requested a model is `none`; the pending
2443
+ * `next` selection is never presented as an already-used route.
2444
+ */
2445
+ function readRoute(entry, summary, projection) {
2446
+ if (entry.mode === "external" || entry.mode === "unknown") return { kind: "notApplicable" };
2447
+ const selection = summary?.projectionValues?.modelSelection;
2448
+ if (selection === void 0) {
2449
+ if (projection?.state === "error" || projection?.state === "idle" && projection.error) return { kind: "failed" };
2450
+ return { kind: "loading" };
2451
+ }
2452
+ const lastUsed = selection.lastUsed;
2453
+ if (lastUsed === void 0) return { kind: "loading" };
2454
+ if (lastUsed === null) return { kind: "none" };
2455
+ return {
2456
+ kind: "used",
2457
+ selection: lastUsed
2458
+ };
2299
2459
  }
2300
- function humanize(value) {
2301
- const text = value.replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/[-_.]+/g, " ").trim();
2302
- return text.charAt(0).toUpperCase() + text.slice(1);
2460
+ /** One line of readout text for a child row. */
2461
+ function routeText(state, t) {
2462
+ switch (state.kind) {
2463
+ case "notApplicable": return t("route.notApplicable");
2464
+ case "loading": return t("route.loading");
2465
+ case "failed": return t("route.failed");
2466
+ case "none": return t("route.noRequest");
2467
+ case "used": {
2468
+ const effort = state.selection.reasoningEffort ?? t("route.effortDefault");
2469
+ return `${state.selection.model.split("/").filter(Boolean).at(-1) || state.selection.model} · ${effort}`;
2470
+ }
2471
+ }
2303
2472
  }
2304
- function fieldLabel(field, t) {
2305
- return hubText(field.marker?.label || field.label, t.language) || field.path.map((part) => part === "[]" ? t("items") : part === "model" ? t("model") : /^\d+$/.test(part) ? `#${Number(part) + 1}` : humanize(part)).join(" / ");
2473
+ /** Focusable rows of one rendered tree. */
2474
+ function treeItems(root) {
2475
+ if (root === null) return [];
2476
+ return Array.from(root.querySelectorAll("[role=\"treeitem\"]:not([aria-disabled=\"true\"])"));
2306
2477
  }
2307
- function followLabel(field, t) {
2308
- return typeof field.followLabel === "string" ? field.followLabel : field.followLabel?.[t.language] || t(field.via === "contribution" || field.arrayItem ? "follow" : "restorePluginDefault");
2478
+ /** Compact token count shared in shape with the conversation stats strip. */
2479
+ function formatTokens(value, t) {
2480
+ const scaled = (next) => next >= 100 ? String(Math.round(next)) : String(Math.round(next * 10) / 10);
2481
+ if (value < 1e3) return String(value);
2482
+ if (value < 1e6) return t("tokens.thousand", { value: scaled(value / 1e3) });
2483
+ return t("tokens.million", { value: scaled(value / 1e6) });
2309
2484
  }
2310
- function pluginTitle(entry, t) {
2311
- return hubText(entry.title, t.language) || humanize((entry.ns.split("/").at(-1) || entry.ns).replace(/^dsh-/, ""));
2485
+ /** Sum the four disjoint durable provider-usage buckets. */
2486
+ function tokenTotal(usage) {
2487
+ if (usage === void 0) return void 0;
2488
+ return usage.uncachedInputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
2312
2489
  }
2313
- function HubPage({ view, api, locale, initialEntries = null, initialCatalog = null }) {
2314
- const t = useText(locale);
2315
- const [entries, setEntries] = (0, react.useState)(initialEntries);
2316
- const [catalog, setCatalog] = (0, react.useState)(initialCatalog);
2317
- const [listError, setListError] = (0, react.useState)("");
2318
- const [catalogError, setCatalogError] = (0, react.useState)("");
2319
- const [writeError, setWriteError] = (0, react.useState)("");
2320
- const [saved, setSaved] = (0, react.useState)(false);
2321
- const [syncError, setSyncError] = (0, react.useState)("");
2322
- const [writing, setWriting] = (0, react.useState)("");
2323
- const [refreshing, setRefreshing] = (0, react.useState)(false);
2324
- const [query, setQuery] = (0, react.useState)("");
2325
- const [tab, setTab] = (0, react.useState)("fields");
2326
- const scopeRef = (0, react.useRef)(null);
2327
- const refresh = (0, react.useCallback)(async (scope) => {
2328
- if (!scope.active) return;
2329
- if (scope.busy) {
2330
- scope.queued = true;
2490
+ function splitDuration(ms) {
2491
+ const totalSeconds = Math.floor(Math.max(0, ms) / 1e3);
2492
+ const totalMinutes = Math.floor(totalSeconds / 60);
2493
+ const totalHours = Math.floor(totalMinutes / 60);
2494
+ const days = Math.floor(totalHours / 24);
2495
+ return {
2496
+ seconds: totalSeconds % 60,
2497
+ minutes: totalMinutes % 60,
2498
+ hours: totalHours % 24,
2499
+ days,
2500
+ totalMinutes,
2501
+ totalHours
2502
+ };
2503
+ }
2504
+ /** Format a duration with decreasing visual precision at larger scales. */
2505
+ function formatDuration(ms, t) {
2506
+ const { seconds, minutes, hours, days, totalMinutes, totalHours } = splitDuration(ms);
2507
+ if (days >= 365) {
2508
+ const years = Math.floor(days / 365);
2509
+ const months = Math.floor(days % 365 / 30);
2510
+ return months === 0 ? t("duration.years", { years }) : t("duration.yearsMonths", {
2511
+ years,
2512
+ months
2513
+ });
2514
+ }
2515
+ if (days >= 30) {
2516
+ const months = Math.floor(days / 30);
2517
+ const remainingDays = days % 30;
2518
+ return remainingDays === 0 ? t("duration.months", { months }) : t("duration.monthsDays", {
2519
+ months,
2520
+ days: remainingDays
2521
+ });
2522
+ }
2523
+ if (days > 0) return hours === 0 ? t("duration.days", { days }) : t("duration.daysHours", {
2524
+ days,
2525
+ hours
2526
+ });
2527
+ if (totalHours > 0) return t("duration.hours", {
2528
+ hours,
2529
+ minutes: String(minutes).padStart(2, "0"),
2530
+ seconds: String(seconds).padStart(2, "0")
2531
+ });
2532
+ if (totalMinutes > 0) return t("duration.minutes", {
2533
+ minutes: String(minutes).padStart(2, "0"),
2534
+ seconds: String(seconds).padStart(2, "0")
2535
+ });
2536
+ return t("duration.seconds", { seconds: String(seconds).padStart(2, "0") });
2537
+ }
2538
+ /** Preserve exact whole seconds for hover and accessible naming. */
2539
+ function formatExactDuration(ms, t) {
2540
+ const { seconds, minutes, hours, days } = splitDuration(ms);
2541
+ if (days === 0) return formatDuration(ms, t);
2542
+ return t("duration.exactDays", {
2543
+ days,
2544
+ hours: String(hours).padStart(2, "0"),
2545
+ minutes: String(minutes).padStart(2, "0"),
2546
+ seconds: String(seconds).padStart(2, "0")
2547
+ });
2548
+ }
2549
+ /** Exact whole-second active-turn duration for one catalog row. */
2550
+ function activityDuration(summary, activity, now) {
2551
+ if (summary === void 0) return void 0;
2552
+ const timing = summary.projectionValues?.subagentTiming;
2553
+ if (timing === void 0) return void 0;
2554
+ if (timing.active === void 0) return timing.settledMs;
2555
+ const end = activity === "running" ? now : timing.active.through;
2556
+ return timing.settledMs + Math.max(0, end - timing.active.since);
2557
+ }
2558
+ /** Render one catalog level and recurse only through explicitly expanded rows. */
2559
+ function CatalogRows({ parentSessionId, currentSessionId, catalog, catalogs, projections, summaries, expanded, level, openChild, openChildAside, refreshProjection, requestProjection, toggleBranch, closeCatalog, t, routeT }) {
2560
+ const [now, setNow] = (0, react.useState)(() => Date.now());
2561
+ const running = catalog.entries.some((entry) => entry.activity === "running");
2562
+ (0, react.useEffect)(() => {
2563
+ if (!running) return;
2564
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
2565
+ return () => clearInterval(timer);
2566
+ }, [running]);
2567
+ const emptyLoading = catalog.state === "loading" && catalog.entries.length === 0;
2568
+ const reserveDisclosure = catalog.entries.some((entry) => entry.mode !== "external" && !isKnownLeaf(catalogs[entry.id]) && summaries[entry.id]?.formatStatus !== "migration-required" && catalogs[entry.id]?.state !== "migration-required");
2569
+ (0, react.useEffect)(() => {
2570
+ for (const entry of catalog.entries) {
2571
+ if (readRoute(entry, summaries[entry.id], projections[entry.id]).kind !== "loading") continue;
2572
+ requestProjection(entry.id);
2573
+ }
2574
+ }, [
2575
+ catalog,
2576
+ projections,
2577
+ summaries,
2578
+ requestProjection
2579
+ ]);
2580
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2581
+ emptyLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2582
+ className: cx(NATIVE_CLASS.notice, "mh-notice"),
2583
+ children: t("loading.label")
2584
+ }) : null,
2585
+ catalog.state === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2586
+ className: cx(NATIVE_CLASS.error, "mh-error"),
2587
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: catalog.error?.message ?? t("load.error") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2588
+ type: "button",
2589
+ className: cx(NATIVE_CLASS.refresh, "mh-refresh"),
2590
+ onClick: () => refreshProjection(parentSessionId),
2591
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, { size: 14 }), t("retry")]
2592
+ })]
2593
+ }) : null,
2594
+ catalog.entries.map((entry) => {
2595
+ const external = entry.mode === "external";
2596
+ const childCatalog = catalogs[entry.id];
2597
+ const isCurrent = entry.id === currentSessionId;
2598
+ const knownLeaf = external || isKnownLeaf(childCatalog);
2599
+ const requiresOpening = summaries[entry.id]?.formatStatus === "migration-required" || childCatalog?.state === "migration-required";
2600
+ const canExpand = !knownLeaf && !requiresOpening;
2601
+ const isExpanded = canExpand && expanded.has(entry.id);
2602
+ const childLoading = childCatalog === void 0 || childCatalog.state === "loading" && childCatalog.entries.length === 0;
2603
+ const summary = summaries[entry.id];
2604
+ const label = entry.label ?? entry.id;
2605
+ const mode = external ? t("mode.external") : entry.mode === "unknown" ? t("mode.unknown") : entry.mode === "one-shot" ? t("mode.oneShot") : t("mode.continuable");
2606
+ const completed = !external && entry.activity === "inactive" && summary?.projectionValues?.subagentTiming?.lastTurnCompleted === true;
2607
+ const activity = external ? void 0 : entry.activity === "running" ? t("activity.running") : completed ? t("activity.completed") : t("activity.inactive");
2608
+ const secondary = [
2609
+ summary?.title,
2610
+ requiresOpening ? t("catalog.migrationRequired") : mode,
2611
+ activity
2612
+ ].filter((value) => value !== void 0).join(" · ");
2613
+ const totalTokens = tokenTotal(summary?.projectionValues?.tokenUsage);
2614
+ const durationMs = activityDuration(summary, entry.activity, now);
2615
+ const tokenMetric = totalTokens === void 0 ? void 0 : t("tokens.total", { value: formatTokens(totalTokens, t) });
2616
+ const durationMetric = durationMs === void 0 ? void 0 : {
2617
+ compact: formatDuration(durationMs, t),
2618
+ exact: formatExactDuration(durationMs, t)
2619
+ };
2620
+ const metrics = [tokenMetric, durationMetric?.exact].filter((value) => value !== void 0).join(" · ");
2621
+ const route = readRoute(entry, summary, projections[entry.id]);
2622
+ const routeLine = routeText(route, routeT);
2623
+ const open = () => {
2624
+ if (external) return;
2625
+ openChild({
2626
+ parentSessionId,
2627
+ childSessionId: entry.id,
2628
+ mode: addressMode(entry)
2629
+ });
2630
+ closeCatalog();
2631
+ };
2632
+ const handleKey = (event) => {
2633
+ if (event.key === "Enter" || event.key === " ") {
2634
+ event.preventDefault();
2635
+ event.stopPropagation();
2636
+ open();
2637
+ } else if (event.key === "ArrowRight" && canExpand && !isExpanded || event.key === "ArrowLeft" && isExpanded) {
2638
+ event.preventDefault();
2639
+ event.stopPropagation();
2640
+ toggleBranch(entry.id);
2641
+ }
2642
+ };
2643
+ const toggle = (event) => {
2644
+ event.preventDefault();
2645
+ event.stopPropagation();
2646
+ toggleBranch(entry.id);
2647
+ };
2648
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2649
+ className: cx(NATIVE_CLASS.node, "mh-node"),
2650
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2651
+ role: "treeitem",
2652
+ tabIndex: external ? -1 : 0,
2653
+ "aria-disabled": external || void 0,
2654
+ "aria-level": level,
2655
+ "aria-current": isCurrent || void 0,
2656
+ "aria-label": [
2657
+ label,
2658
+ secondary,
2659
+ metrics,
2660
+ routeLine
2661
+ ].filter((value) => value !== "").join(" "),
2662
+ ...canExpand ? { "aria-expanded": isExpanded } : {},
2663
+ className: cx(NATIVE_CLASS.row, "mh-row"),
2664
+ onClick: open,
2665
+ onKeyDown: handleKey,
2666
+ children: [!canExpand ? reserveDisclosure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cx(NATIVE_CLASS.disclosureSpace, "mh-disclosure-space") }) : null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2667
+ type: "button",
2668
+ tabIndex: -1,
2669
+ className: cx(NATIVE_CLASS.disclosure, "mh-disclosure", isExpanded ? NATIVE_CLASS.disclosureOpen : "", isExpanded ? "mh-disclosure-open" : ""),
2670
+ "aria-label": t(isExpanded ? "branch.collapse" : "branch.expand", { label }),
2671
+ onClick: toggle,
2672
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, {})
2673
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2674
+ className: cx(NATIVE_CLASS.clickarea, "mh-clickarea"),
2675
+ children: [
2676
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2677
+ className: cx(NATIVE_CLASS.rowActivitySlot, "mh-row-activity-slot"),
2678
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: entry.activity === "running" ? "ongoing" : completed ? "done" : "idle" })
2679
+ }),
2680
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2681
+ className: cx(NATIVE_CLASS.content, "mh-content"),
2682
+ children: [
2683
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2684
+ className: cx(NATIVE_CLASS.label, "mh-label", isCurrent ? NATIVE_CLASS.currentLabel : ""),
2685
+ children: label
2686
+ }),
2687
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2688
+ className: cx(NATIVE_CLASS.summary, "mh-summary"),
2689
+ children: secondary
2690
+ }),
2691
+ route.kind === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2692
+ type: "button",
2693
+ className: "mh-route mh-route-retry",
2694
+ onClick: (event) => {
2695
+ event.stopPropagation();
2696
+ requestProjection(entry.id, true);
2697
+ },
2698
+ children: routeLine
2699
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2700
+ className: "mh-route",
2701
+ title: route.kind === "used" ? `${route.selection.provider} / ${route.selection.model} · ${route.selection.reasoningEffort ?? routeT("route.effortDefault")}` : void 0,
2702
+ children: routeLine
2703
+ })
2704
+ ]
2705
+ }),
2706
+ metrics !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2707
+ className: cx(NATIVE_CLASS.metrics, "mh-metrics"),
2708
+ children: [tokenMetric !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2709
+ className: cx(NATIVE_CLASS.metricToken, "mh-metric-token"),
2710
+ children: tokenMetric
2711
+ }) : null, durationMetric !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2712
+ className: cx(NATIVE_CLASS.metricDuration, "mh-metric-duration"),
2713
+ title: t("duration.exactTitle", { duration: durationMetric.exact }),
2714
+ children: durationMetric.compact
2715
+ }) : null]
2716
+ }) : null,
2717
+ isCurrent || external ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cx(NATIVE_CLASS.sidebarSpace, "mh-sidebar-space") }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
2718
+ label: t("open.sidebar"),
2719
+ side: "bottom",
2720
+ align: "end",
2721
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2722
+ type: "button",
2723
+ className: cx(NATIVE_CLASS.sidebarButton, "mh-sidebar-button"),
2724
+ "aria-label": t("open.sidebar.aria", { label }),
2725
+ onClick: (event) => {
2726
+ event.preventDefault();
2727
+ event.stopPropagation();
2728
+ openChildAside({
2729
+ parentSessionId,
2730
+ childSessionId: entry.id,
2731
+ mode: addressMode(entry)
2732
+ });
2733
+ closeCatalog();
2734
+ },
2735
+ onKeyDown: (event) => {
2736
+ event.stopPropagation();
2737
+ },
2738
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, {})
2739
+ })
2740
+ })
2741
+ ]
2742
+ })]
2743
+ }), isExpanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2744
+ role: "group",
2745
+ className: cx(NATIVE_CLASS.children, "mh-children"),
2746
+ "aria-busy": childLoading || void 0,
2747
+ children: childCatalog === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2748
+ className: cx(NATIVE_CLASS.notice, "mh-notice"),
2749
+ children: t("loading.label")
2750
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CatalogRows, {
2751
+ parentSessionId: entry.id,
2752
+ currentSessionId,
2753
+ catalog: childCatalog,
2754
+ catalogs,
2755
+ projections,
2756
+ summaries,
2757
+ expanded,
2758
+ level: level + 1,
2759
+ openChild,
2760
+ openChildAside,
2761
+ refreshProjection,
2762
+ requestProjection,
2763
+ toggleBranch,
2764
+ closeCatalog,
2765
+ t,
2766
+ routeT
2767
+ })
2768
+ }) : null]
2769
+ }, entry.id);
2770
+ })
2771
+ ] });
2772
+ }
2773
+ /** A child becomes a known leaf only after its own authoritative catalog loads empty. */
2774
+ function isKnownLeaf(catalog) {
2775
+ return catalog?.state === "ready" && catalog.entries.length === 0;
2776
+ }
2777
+ /** The addressable mode of one catalog entry; unreadable modes stay unknown. */
2778
+ function addressMode(entry) {
2779
+ return entry.mode === "one-shot" || entry.mode === "continuable" ? entry.mode : "unknown";
2780
+ }
2781
+ const MENU_VIEWPORT_MARGIN = 16;
2782
+ /** Place a portaled catalog below its trigger without crossing the viewport edge. */
2783
+ function catalogMenuPosition(trigger) {
2784
+ const rect = trigger.getBoundingClientRect();
2785
+ const width = Math.min(336, window.innerWidth - 32);
2786
+ return {
2787
+ top: rect.bottom + 5,
2788
+ left: Math.min(Math.max(MENU_VIEWPORT_MARGIN, rect.left), window.innerWidth - width - MENU_VIEWPORT_MARGIN)
2789
+ };
2790
+ }
2791
+ /** One trigger-plus-tree dropdown over the catalog rooted at `rootSessionId`. */
2792
+ function CatalogDropdown({ rootSessionId, currentSessionId, displayTitle, openTitle, variant, useSessions, useSessionStatus, openChild, openChildAside, refreshProjection, t, routeT }) {
2793
+ const ancestorSwitcher = variant === "switcher" && openTitle !== void 0;
2794
+ const projections = useSessions((state) => state.projectionsBySession);
2795
+ const summaries = useSessions((state) => state.byId);
2796
+ const statuses = useSessionStatus((value) => value);
2797
+ const catalogs = (0, react.useMemo)(() => Object.fromEntries(Object.entries(projections).map(([id, snapshot]) => {
2798
+ const projected = snapshot.values.subagentCatalog;
2799
+ return [id, {
2800
+ state: snapshot.state === "idle" ? projected === void 0 ? "loading" : "ready" : snapshot.state === "migration-required" && projected !== void 0 ? "ready" : snapshot.state,
2801
+ error: snapshot.error,
2802
+ entries: (projected ?? []).map((entry) => ({
2803
+ ...entry,
2804
+ activity: (statuses.get(entry.id)?.running ?? summaries[entry.id]?.running) === true ? "running" : "inactive"
2805
+ }))
2806
+ }];
2807
+ })), [
2808
+ projections,
2809
+ summaries,
2810
+ statuses
2811
+ ]);
2812
+ const catalog = catalogs[rootSessionId];
2813
+ const [open, setOpen] = (0, react.useState)(false);
2814
+ const [menuPosition, setMenuPosition] = (0, react.useState)();
2815
+ const [expanded, setExpanded] = (0, react.useState)(() => /* @__PURE__ */ new Set());
2816
+ const rootRef = (0, react.useRef)(null);
2817
+ const triggerRef = (0, react.useRef)(null);
2818
+ const menuRef = (0, react.useRef)(null);
2819
+ const hoverOpenTimer = (0, react.useRef)(void 0);
2820
+ const hoverCloseTimer = (0, react.useRef)(void 0);
2821
+ const pinnedRef = (0, react.useRef)(false);
2822
+ const requestedRef = (0, react.useRef)(/* @__PURE__ */ new Set());
2823
+ const currentEntry = currentSessionId === void 0 ? void 0 : catalog?.entries.find((entry) => entry.id === currentSessionId);
2824
+ const switcherDisplayTitle = currentEntry !== void 0 ? currentEntry.label ?? currentEntry.id : displayTitle ?? rootSessionId;
2825
+ const directChildren = catalog?.entries ?? [];
2826
+ const directCount = directChildren.length;
2827
+ const runningCount = directChildren.filter((entry) => entry.activity === "running").length;
2828
+ const totalCountKey = directCount === 1 ? "count.total.one" : "count.total.other";
2829
+ const runningCountKey = runningCount === 1 ? "count.running.one" : "count.running.other";
2830
+ const presentedCatalog = catalog ?? (variant === "switcher" ? {
2831
+ entries: [],
2832
+ state: "loading",
2833
+ error: null
2834
+ } : void 0);
2835
+ const cancelHoverClose = () => {
2836
+ if (hoverCloseTimer.current === void 0) return;
2837
+ clearTimeout(hoverCloseTimer.current);
2838
+ hoverCloseTimer.current = void 0;
2839
+ };
2840
+ const cancelHoverOpen = () => {
2841
+ if (hoverOpenTimer.current === void 0) return;
2842
+ clearTimeout(hoverOpenTimer.current);
2843
+ hoverOpenTimer.current = void 0;
2844
+ };
2845
+ const changeOpen = (next, restoreFocus = false) => {
2846
+ cancelHoverOpen();
2847
+ cancelHoverClose();
2848
+ if (next) {
2849
+ const trigger = triggerRef.current;
2850
+ if (trigger === null) return;
2851
+ setOpen(true);
2852
+ setMenuPosition(catalogMenuPosition(trigger));
2853
+ } else {
2854
+ pinnedRef.current = false;
2855
+ setOpen(false);
2856
+ setMenuPosition(void 0);
2857
+ setExpanded(/* @__PURE__ */ new Set());
2858
+ }
2859
+ if (restoreFocus) queueMicrotask(() => {
2860
+ triggerRef.current?.focus();
2861
+ });
2862
+ };
2863
+ const scheduleHoverOpen = () => {
2864
+ cancelHoverOpen();
2865
+ cancelHoverClose();
2866
+ if (open) return;
2867
+ hoverOpenTimer.current = setTimeout(() => {
2868
+ hoverOpenTimer.current = void 0;
2869
+ changeOpen(true);
2870
+ }, 150);
2871
+ };
2872
+ const scheduleHoverClose = () => {
2873
+ cancelHoverOpen();
2874
+ cancelHoverClose();
2875
+ if (pinnedRef.current) return;
2876
+ hoverCloseTimer.current = setTimeout(() => {
2877
+ hoverCloseTimer.current = void 0;
2878
+ changeOpen(false);
2879
+ }, 120);
2880
+ };
2881
+ const closeBranch = (root) => {
2882
+ const closing = /* @__PURE__ */ new Set();
2883
+ const visit = (parentSessionId) => {
2884
+ if (closing.has(parentSessionId) || !expanded.has(parentSessionId)) return;
2885
+ closing.add(parentSessionId);
2886
+ const branch = catalogs[parentSessionId];
2887
+ for (const entry of branch?.entries ?? []) visit(entry.id);
2888
+ };
2889
+ visit(root);
2890
+ setExpanded((current) => new Set([...current].filter((id) => !closing.has(id))));
2891
+ };
2892
+ const toggleBranch = (childSessionId) => {
2893
+ if (expanded.has(childSessionId)) {
2894
+ closeBranch(childSessionId);
2331
2895
  return;
2332
2896
  }
2333
- scope.queued = false;
2334
- scope.busy = true;
2335
- setRefreshing(true);
2336
- const [fields, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.list()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedModelCatalog)]);
2337
- if (!scope.active) return;
2338
- if (fields.status === "fulfilled") {
2339
- setEntries(fields.value);
2340
- setListError("");
2341
- setWriteError("");
2342
- setSaved(false);
2343
- } else setListError(messageOf(fields.reason));
2344
- if (models.status === "fulfilled") {
2345
- setCatalog(models.value);
2346
- setCatalogError("");
2347
- } else setCatalogError(messageOf(models.reason));
2348
- scope.busy = false;
2349
- setRefreshing(false);
2350
- if (scope.queued) refresh(scope);
2351
- }, []);
2897
+ setExpanded((current) => new Set(current).add(childSessionId));
2898
+ requestProjection(childSessionId);
2899
+ };
2900
+ /** Ask the host for one child's projections, at most once per dropdown. */
2901
+ const requestProjection = (0, react.useCallback)((childSessionId, retry = false) => {
2902
+ if (requestedRef.current.has(childSessionId) && !retry) return;
2903
+ requestedRef.current.add(childSessionId);
2904
+ try {
2905
+ refreshProjection(childSessionId);
2906
+ } catch {}
2907
+ }, [refreshProjection]);
2352
2908
  (0, react.useEffect)(() => {
2353
- if (view === "summary") return;
2354
- const scope = {
2355
- api,
2356
- active: true,
2357
- busy: false,
2358
- queued: false
2909
+ if (!open) return;
2910
+ const closeOutside = (event) => {
2911
+ const target = event.target;
2912
+ if (target instanceof Node && !rootRef.current?.contains(target) && !menuRef.current?.contains(target)) changeOpen(false);
2359
2913
  };
2360
- scopeRef.current = scope;
2361
- setWriting("");
2362
- setWriteError("");
2363
- setSaved(false);
2364
- setListError("");
2365
- setCatalogError("");
2366
- setSyncError("");
2367
- setEntries(initialEntries);
2368
- setCatalog(initialCatalog);
2369
- refresh(scope);
2370
- const unsubscribe = api.subscribeChanges?.(() => {
2371
- if (!scope.active) return;
2372
- setSyncError("");
2373
- refresh(scope);
2374
- }, (message) => {
2375
- if (scope.active) setSyncError(message);
2376
- });
2914
+ document.addEventListener("pointerdown", closeOutside);
2915
+ return () => document.removeEventListener("pointerdown", closeOutside);
2916
+ }, [open]);
2917
+ (0, react.useEffect)(() => {
2918
+ if (!open) return;
2919
+ const placeMenu = () => {
2920
+ const trigger = triggerRef.current;
2921
+ if (trigger === null) return;
2922
+ setMenuPosition(catalogMenuPosition(trigger));
2923
+ };
2924
+ window.addEventListener("resize", placeMenu);
2925
+ document.addEventListener("scroll", placeMenu, true);
2377
2926
  return () => {
2378
- scope.active = false;
2379
- unsubscribe?.();
2380
- scopeRef.current = null;
2927
+ window.removeEventListener("resize", placeMenu);
2928
+ document.removeEventListener("scroll", placeMenu, true);
2381
2929
  };
2382
- }, [
2383
- api,
2384
- view,
2385
- refresh
2386
- ]);
2387
- const setRoute = (entry, field, route) => {
2388
- const scope = scopeRef.current;
2389
- if (!scope?.active || scope.api !== api || scope.busy || writeError) return;
2390
- const key = `${entry.ns}:${field.path.join(".")}`;
2391
- scope.busy = true;
2392
- setSaved(false);
2393
- setWriting(key);
2394
- Promise.resolve().then(() => scope.api.setField({
2395
- ns: entry.ns,
2396
- path: field.path,
2397
- route,
2398
- expectedRevision: entry.revision,
2399
- ...entry.ownerId ? { ownerId: entry.ownerId } : {}
2400
- })).then(() => {
2401
- if (scope.active) {
2402
- setSaved(true);
2403
- setWriteError("");
2404
- scope.queued = true;
2405
- }
2406
- }, (error) => {
2407
- if (scope.active) setWriteError(messageOf(error));
2408
- }).finally(() => {
2409
- if (scope.active) {
2410
- scope.busy = false;
2411
- setWriting("");
2412
- if (scope.queued) refresh(scope);
2413
- }
2414
- });
2930
+ }, [open]);
2931
+ (0, react.useEffect)(() => () => {
2932
+ cancelHoverOpen();
2933
+ cancelHoverClose();
2934
+ }, []);
2935
+ const visible = presentedCatalog !== void 0 && (variant === "switcher" || presentedCatalog.state === "error" || presentedCatalog.entries.length > 0);
2936
+ (0, react.useEffect)(() => {
2937
+ if (visible) return;
2938
+ cancelHoverOpen();
2939
+ cancelHoverClose();
2940
+ if (!open) return;
2941
+ pinnedRef.current = false;
2942
+ setOpen(false);
2943
+ setExpanded(/* @__PURE__ */ new Set());
2944
+ }, [visible, open]);
2945
+ if (!visible) return null;
2946
+ const focusAt = (index) => {
2947
+ const items = treeItems(menuRef.current);
2948
+ if (items.length === 0) return;
2949
+ items[(index + items.length) % items.length]?.focus();
2950
+ };
2951
+ const navigate = (event) => {
2952
+ const items = treeItems(menuRef.current);
2953
+ const index = items.indexOf(document.activeElement);
2954
+ if (event.key === "Escape") {
2955
+ event.preventDefault();
2956
+ changeOpen(false, true);
2957
+ } else if (event.key === "Home") {
2958
+ event.preventDefault();
2959
+ focusAt(0);
2960
+ } else if (event.key === "End") {
2961
+ event.preventDefault();
2962
+ focusAt(items.length - 1);
2963
+ } else if (event.key === "ArrowDown") {
2964
+ event.preventDefault();
2965
+ focusAt(index + 1);
2966
+ } else if (event.key === "ArrowUp") {
2967
+ event.preventDefault();
2968
+ focusAt(index < 0 ? items.length - 1 : index - 1);
2969
+ }
2415
2970
  };
2416
- if (view === "summary") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2417
- className: "dsh-model-hub",
2418
- lang: t.language,
2419
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Style, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2420
- className: "dsh-ui-compact dsh-ui-wrap",
2421
- children: t("intro")
2971
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2972
+ className: cx("dsh-model-hub-subagent", NATIVE_CLASS.root, "mh-root", variant === "switcher" ? NATIVE_CLASS.switcherRoot : "", variant === "switcher" ? "mh-switcher-root" : ""),
2973
+ ref: rootRef,
2974
+ onKeyDown: navigate,
2975
+ onPointerLeave: scheduleHoverClose,
2976
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2977
+ ref: triggerRef,
2978
+ onPointerEnter: (event) => {
2979
+ if (event.pointerType !== "touch") scheduleHoverOpen();
2980
+ },
2981
+ type: "button",
2982
+ className: variant === "switcher" ? cx(NATIVE_CLASS.switcherTrigger, "mh-switcher-trigger", ancestorSwitcher ? NATIVE_CLASS.ancestorSwitcherTrigger : "", ancestorSwitcher ? "mh-ancestor-switcher-trigger" : "") : cx(NATIVE_CLASS.trigger, "mh-trigger"),
2983
+ "aria-haspopup": "tree",
2984
+ "aria-expanded": open,
2985
+ "aria-label": variant === "switcher" ? t("switcher.aria", { title: switcherDisplayTitle }) : t(runningCount > 0 ? runningCountKey : totalCountKey, { count: runningCount > 0 ? runningCount : directCount }),
2986
+ onClick: openTitle === void 0 ? () => {
2987
+ cancelHoverOpen();
2988
+ cancelHoverClose();
2989
+ pinnedRef.current = true;
2990
+ if (!open) changeOpen(true);
2991
+ } : () => {
2992
+ cancelHoverOpen();
2993
+ if (open) changeOpen(false);
2994
+ openTitle();
2995
+ },
2996
+ onKeyDown: (event) => {
2997
+ if (event.key !== "ArrowDown") return;
2998
+ event.preventDefault();
2999
+ if (!open) changeOpen(true);
3000
+ queueMicrotask(() => {
3001
+ focusAt(0);
3002
+ });
3003
+ },
3004
+ children: [variant === "switcher" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3005
+ className: cx(NATIVE_CLASS.switcherTitle, "mh-switcher-title"),
3006
+ children: switcherDisplayTitle
3007
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [runningCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3008
+ className: cx(NATIVE_CLASS.activitySlot, "mh-activity-slot"),
3009
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" })
3010
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3011
+ className: "mh-count",
3012
+ children: t(totalCountKey, { count: directCount })
3013
+ })] }), variant === "switcher" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubagentSwitcherIcon, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: open ? cx(NATIVE_CLASS.triggerOpen, "mh-trigger-open") : void 0 })]
3014
+ }), open ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3015
+ ref: menuRef,
3016
+ className: cx("dsh-model-hub-subagent", NATIVE_CLASS.menu, "mh-menu"),
3017
+ style: menuPosition,
3018
+ onPointerEnter: (event) => {
3019
+ if (event.pointerType !== "touch") cancelHoverClose();
3020
+ },
3021
+ onPointerLeave: scheduleHoverClose,
3022
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3023
+ className: cx(NATIVE_CLASS.menuBody, "mh-menu-body"),
3024
+ role: "tree",
3025
+ "aria-label": t("tree.aria"),
3026
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CatalogRows, {
3027
+ parentSessionId: rootSessionId,
3028
+ currentSessionId,
3029
+ catalog: presentedCatalog,
3030
+ catalogs,
3031
+ projections,
3032
+ summaries,
3033
+ expanded,
3034
+ level: 1,
3035
+ openChild,
3036
+ openChildAside,
3037
+ refreshProjection,
3038
+ requestProjection,
3039
+ toggleBranch,
3040
+ closeCatalog: () => {
3041
+ changeOpen(false);
3042
+ },
3043
+ t,
3044
+ routeT
3045
+ })
3046
+ })
3047
+ }), document.body) : null]
3048
+ });
3049
+ }
3050
+ /** The host's chevron pair, used by the lineage switcher. */
3051
+ function SubagentSwitcherIcon() {
3052
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
3053
+ width: "16",
3054
+ height: "16",
3055
+ viewBox: "0 0 20 20",
3056
+ fill: "none",
3057
+ "aria-hidden": "true",
3058
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
3059
+ d: "M5.99951 12.7L8.95546 14.9478C9.40011 15.2859 9.62244 15.455 9.87526 15.488C9.95774 15.4988 10.0413 15.4988 10.1238 15.488C10.3766 15.455 10.5989 15.2859 11.0436 14.9478L13.9995 12.7",
3060
+ stroke: "currentColor",
3061
+ strokeWidth: "1.5"
3062
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
3063
+ d: "M13.9995 7.7417L11.0436 5.49387C10.5989 5.15574 10.3766 4.98668 10.1238 4.95362C10.0413 4.94283 9.95775 4.94283 9.87527 4.95362C9.62245 4.98668 9.40012 5.15574 8.95547 5.49387L5.99952 7.7417",
3064
+ stroke: "currentColor",
3065
+ strokeWidth: "1.5"
2422
3066
  })]
2423
3067
  });
2424
- const needle = query.trim().toLocaleLowerCase();
2425
- const groups = (entries ?? []).map((entry) => ({
2426
- ...entry,
2427
- fields: entry.fields.filter((field) => !(entry.ns === "@klarkxy/dsh-model-hub" && field.path.length === 1 && ["selfTestMeta", "selfTestRole"].includes(field.path[0] || "")))
2428
- })).filter((entry) => entry.ns !== "subagent-model-selection-settings" && (entry.fields.length > 0 || entry.error || entry.catalogError));
2429
- const visible = groups.map((entry) => ({
2430
- ...entry,
2431
- fields: entry.fields.filter((field) => {
2432
- if (!needle) return true;
2433
- const route = normalizeModelRoute(field.current);
2434
- const model = parseModelMenuChoices(entry.catalog !== void 0 ? entry.catalog : catalog, route).find((choice) => choice.provider === route.provider && choice.model === route.model)?.label || "";
2435
- return [
2436
- entry.ns,
2437
- pluginTitle(entry, t),
2438
- fieldLabel(field, t),
2439
- hubText(field.description, t.language),
2440
- field.path.join("."),
2441
- route.provider,
2442
- route.model,
2443
- model,
2444
- !route.model ? followLabel(field, t) : ""
2445
- ].some((text) => text?.toLocaleLowerCase().includes(needle));
2446
- })
2447
- })).filter((entry) => entry.fields.length > 0 || (entry.error || entry.catalogError) && (!needle || [entry.ns, pluginTitle(entry, t)].some((text) => text.toLocaleLowerCase().includes(needle))));
2448
- const disabled = writing !== "" || refreshing || writeError !== "" || saved && listError !== "";
2449
- const refreshNow = async () => {
2450
- const scope = scopeRef.current;
2451
- if (scope?.api === api && !scope.busy) await refresh(scope);
2452
- };
2453
- const catalogStatus = {
2454
- loading: refreshing,
2455
- error: catalogError ? hubMessage(catalogError, t.language) : "",
2456
- failures: modelCatalogFailures(catalog),
2457
- refresh: refreshNow
3068
+ }
3069
+ /**
3070
+ * Session-header catalog action for root sessions: the descendant count and
3071
+ * its dropdown after the preset label in the header actions band. Child
3072
+ * sessions render nothing here — their breadcrumb switcher in the lineage
3073
+ * slot owns the same navigation.
3074
+ */
3075
+ function SubagentCatalogAction(props) {
3076
+ const t = useText(props.locale);
3077
+ const nativeT = (key, params) => props.hostT(key, params);
3078
+ if (props.useSessions((state) => state.byId[props.sessionId]?.origin === "subagent")) return null;
3079
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css$1 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CatalogDropdown, {
3080
+ rootSessionId: props.sessionId,
3081
+ variant: "count",
3082
+ useSessions: props.useSessions,
3083
+ useSessionStatus: props.useSessionStatus,
3084
+ openChild: props.openChild,
3085
+ openChildAside: props.openChildAside,
3086
+ refreshProjection: props.refreshProjection,
3087
+ t: nativeT,
3088
+ routeT: t
3089
+ })] });
3090
+ }
3091
+ /**
3092
+ * Render one breadcrumb title together with its subagent navigation: the
3093
+ * ancestor switcher and, on a child session, its own descendant count.
3094
+ */
3095
+ function SubagentHeaderLineage(props) {
3096
+ const routeT = useText(props.locale);
3097
+ const address = props.useSession((session) => session.subagent?.address);
3098
+ const parentId = props.useSessions((state) => {
3099
+ if (address !== void 0 && address.childSessionId === props.lineageSessionId) return address.parentSessionId;
3100
+ for (const [candidate, snapshot] of Object.entries(state.projectionsBySession)) if (snapshot.values.subagentCatalog?.some((entry) => entry.id === props.lineageSessionId)) return candidate;
3101
+ const summary = state.byId[props.lineageSessionId];
3102
+ return summary?.origin === "subagent" ? summary.parentId : void 0;
3103
+ });
3104
+ if (parentId === void 0) return null;
3105
+ const shared = {
3106
+ useSessions: props.useSessions,
3107
+ useSessionStatus: props.useSessionStatus,
3108
+ openChild: props.openChild,
3109
+ openChildAside: props.openChildAside,
3110
+ refreshProjection: props.refreshProjection,
3111
+ t: props.hostT,
3112
+ routeT
2458
3113
  };
3114
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3115
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css$1 }),
3116
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CatalogDropdown, {
3117
+ rootSessionId: parentId,
3118
+ currentSessionId: props.lineageSessionId,
3119
+ variant: "switcher",
3120
+ displayTitle: props.displayTitle,
3121
+ ...props.openTitle === void 0 ? {} : { openTitle: props.openTitle },
3122
+ ...shared
3123
+ }),
3124
+ props.openTitle === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CatalogDropdown, {
3125
+ rootSessionId: props.lineageSessionId,
3126
+ variant: "count",
3127
+ ...shared
3128
+ }) : null
3129
+ ] });
3130
+ }
3131
+ //#endregion
3132
+ //#region src/client.tsx
3133
+ const name = "dsh-subagent-models-client";
3134
+ const inject = [
3135
+ "slots",
3136
+ "connection",
3137
+ ...MODEL_CATALOG_INJECT,
3138
+ "locale",
3139
+ "sessions",
3140
+ "uiWorkspace",
3141
+ "sidebarRight"
3142
+ ];
3143
+ const CHANNEL = "/dsh-model-hub";
3144
+ const BUNDLE = "@klarkxy/dsh-model-hub";
3145
+ function localeActive(locale) {
3146
+ return String(typeof locale === "string" ? locale : locale?.getSnapshot?.()?.active ?? "en");
3147
+ }
3148
+ const css = `${officialUiCss("dsh-model-hub")}${modelMenuCss}`;
3149
+ function SubagentModelsPage({ api, locale }) {
2459
3150
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2460
3151
  className: "dsh-model-hub dsh-ui-page",
2461
- lang: t.language,
2462
- "aria-label": t("title"),
2463
- "aria-busy": writing !== "" || refreshing,
2464
- children: [
2465
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Style, {}),
2466
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
2467
- value: tab,
2468
- onChange: setTab,
2469
- label: t("tabsLabel"),
2470
- className: "hub-tabs",
2471
- items: [{
2472
- value: "fields",
2473
- label: t("tabFields"),
2474
- id: "dsh-model-hub-tab-fields",
2475
- panelId: "dsh-model-hub-panel-fields"
2476
- }, {
2477
- value: "cast",
2478
- label: t("tabCast"),
2479
- id: "dsh-model-hub-tab-cast",
2480
- panelId: "dsh-model-hub-panel-cast"
2481
- }]
2482
- }),
2483
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2484
- role: "tabpanel",
2485
- id: "dsh-model-hub-panel-fields",
2486
- "aria-labelledby": "dsh-model-hub-tab-fields",
2487
- hidden: tab !== "fields",
2488
- className: "dsh-ui-stack",
2489
- children: [
2490
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2491
- className: "hub-toolbar",
2492
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
2493
- className: "hub-search",
2494
- type: "search",
2495
- "aria-label": t("search"),
2496
- placeholder: t("search"),
2497
- value: query,
2498
- onChange: (event) => setQuery(event.target.value)
2499
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2500
- variant: "ghost",
2501
- disabled: writing !== "" || refreshing,
2502
- onClick: refreshNow,
2503
- children: refreshing ? t("refreshing") : t("refresh")
2504
- })]
2505
- }),
2506
- listError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2507
- role: "alert",
2508
- className: "dsh-ui-error dsh-ui-wrap",
2509
- children: t(saved ? "savedRefreshFailed" : "failed", hubMessage(listError, t.language))
2510
- }) : null,
2511
- syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2512
- role: "alert",
2513
- className: "dsh-ui-error dsh-ui-wrap",
2514
- children: t("syncFailed", hubMessage(syncError, t.language))
2515
- }) : null,
2516
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
2517
- state: catalogStatus,
2518
- locale: t.language
2519
- }),
2520
- writeError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2521
- role: "alert",
2522
- className: "dsh-ui-error dsh-ui-wrap",
2523
- children: t("writeFailed", hubMessage(writeError, t.language))
2524
- }) : null,
2525
- writing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2526
- role: "status",
2527
- className: "dsh-ui-help",
2528
- children: t("saving")
2529
- }) : null,
2530
- saved && !listError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2531
- role: "status",
2532
- className: "dsh-ui-help",
2533
- children: t("saved")
2534
- }) : null,
2535
- entries === null && !listError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2536
- className: "dsh-ui-loading",
2537
- children: t("loading")
2538
- }) : null,
2539
- entries !== null && groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2540
- className: "dsh-ui-help",
2541
- children: t("empty")
2542
- }) : null,
2543
- groups.length > 0 && visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2544
- className: "dsh-ui-help",
2545
- children: t("noFields")
2546
- }) : null,
2547
- visible.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2548
- className: "hub-group",
2549
- "aria-label": pluginTitle(entry, t),
2550
- children: [
2551
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2552
- className: "hub-group-head",
2553
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2554
- className: "hub-group-title",
2555
- children: pluginTitle(entry, t)
2556
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2557
- className: "hub-namespace",
2558
- children: entry.ns
2559
- })]
2560
- }),
2561
- entry.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2562
- role: "alert",
2563
- className: "dsh-ui-error dsh-ui-wrap",
2564
- children: t("failed", hubMessage(hubText(entry.error, t.language), t.language))
2565
- }) : null,
2566
- entry.catalogError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2567
- role: "alert",
2568
- className: "dsh-ui-error dsh-ui-wrap",
2569
- children: t("catalogFailed", hubMessage(hubText(entry.catalogError, t.language), t.language))
2570
- }) : null,
2571
- entry.catalog !== void 0 ? catalogFailures(entry.catalog).map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2572
- role: "alert",
2573
- className: "dsh-ui-error dsh-ui-wrap",
2574
- children: t("catalogFailed", hubMessage(failure, t.language))
2575
- }, index)) : null,
2576
- entry.fields.map((field) => {
2577
- const key = `${entry.ns}:${field.path.join(".")}`;
2578
- const label = fieldLabel(field, t);
2579
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2580
- className: "hub-row",
2581
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2582
- className: "hub-row-copy",
2583
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2584
- className: "hub-row-label",
2585
- children: label
2586
- }), field.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2587
- className: "hub-row-description",
2588
- children: hubText(field.description, t.language)
2589
- }) : null]
2590
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2591
- className: "hub-editor",
2592
- children: field.editable === false ? field.description ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2593
- className: "dsh-ui-help",
2594
- children: t("collectionTemplate")
2595
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldMenu, {
2596
- t,
2597
- catalog: entry.catalog !== void 0 ? entry.catalog : catalog,
2598
- current: field.current,
2599
- label,
2600
- allowFollow: field.allowFollow,
2601
- followLabel: followLabel(field, t),
2602
- reasoningEffort: field.supportsReasoning === false ? false : field.marker?.reasoningEffort,
2603
- disabled: disabled || !!entry.error || !!entry.catalogError || entry.catalog === void 0 && catalog === null,
2604
- onSet: (route) => setRoute(entry, field, route)
2605
- })
2606
- })]
2607
- }, key);
2608
- })
2609
- ]
2610
- }, entry.ns))
2611
- ]
2612
- }),
2613
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2614
- role: "tabpanel",
2615
- id: "dsh-model-hub-panel-cast",
2616
- "aria-labelledby": "dsh-model-hub-tab-cast",
2617
- hidden: tab !== "cast",
2618
- children: tab === "cast" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CastPage, {
2619
- api,
2620
- locale
2621
- }) : null
2622
- })
2623
- ]
3152
+ "aria-label": localeActive(locale).toLowerCase().startsWith("zh") ? "子代理" : "Subagents",
3153
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CastPage, {
3154
+ api,
3155
+ locale
3156
+ })]
3157
+ });
3158
+ }
3159
+ function subagentChatAddress(address) {
3160
+ const query = new URLSearchParams({
3161
+ parent: address.parentSessionId,
3162
+ mode: address.mode
2624
3163
  });
3164
+ return `dsh-resource://subagentchat/session/${encodeURIComponent(address.childSessionId)}?${query}`;
2625
3165
  }
2626
3166
  function apply(ctx) {
2627
3167
  const client = ctx;
2628
3168
  mountModelSettingsChanges(ctx, BUNDLE);
2629
- const changes = createModelSettingsSubscriber(ctx, BUNDLE, modelSettingsChangesIdentity(BUNDLE), () => true);
3169
+ const changes = createModelSettingsSubscriber(ctx, BUNDLE, modelSettingsChangesIdentity(BUNDLE), (namespace) => CAST_WATCH_NAMESPACES.some((own) => own === namespace));
2630
3170
  const call = async (endpoint, payload) => {
2631
3171
  const result = await client.connection.rpc.call(CHANNEL, endpoint, payload);
2632
3172
  if (!result?.ok) throw new Error(result?.error?.message || "Request failed");
2633
3173
  return result.value;
2634
3174
  };
2635
3175
  const api = Object.freeze({
2636
- list: () => call("fields.list", {}),
2637
- setField: (payload) => call("fields.set", payload),
2638
3176
  modelCatalog: () => readModelCatalog(client),
2639
3177
  subscribeChanges: changes.subscribe,
2640
3178
  getCast: () => call("cast.get", {}),
@@ -2643,24 +3181,58 @@ function apply(ctx) {
2643
3181
  setCastRoute: (payload) => call("cast.setRoute", payload),
2644
3182
  removeCastRoute: (payload) => call("cast.removeRoute", payload),
2645
3183
  setCastTendency: (payload) => call("cast.setTendency", payload),
2646
- listRunning: () => call("cast.running", {})
3184
+ setCastRoutePreferences: (payload) => call("cast.setPreferences", payload)
2647
3185
  });
2648
- client.slots.inject("plugins.bundle.config", () => client.slots.register({
2649
- name: "plugins.bundle.config",
2650
- key: BUNDLE,
3186
+ const locale = client.locale;
3187
+ client.slots.inject("settings.section", () => [client.slots.register({
3188
+ name: "settings.section",
3189
+ id: "model-hub",
3190
+ order: 180,
3191
+ label: () => localeActive(locale).toLowerCase().startsWith("zh") ? "子代理" : "Subagents",
2651
3192
  inject: () => ({
2652
3193
  api,
2653
- locale: ctx.locale
3194
+ locale
2654
3195
  })
2655
- }, HubPage));
3196
+ }, SubagentModelsPage), registerSettingsSectionIcon(ctx, "model-hub", 180, IconModelNetworkSettings)]);
3197
+ const sidebarChatAvailable = Boolean(client.get?.("resources"));
3198
+ const catalogActions = (_parentSessionId) => ({
3199
+ openChild: (address) => {
3200
+ client.uiWorkspace.openSession(address);
3201
+ },
3202
+ openChildAside: (address) => {
3203
+ if (!sidebarChatAvailable) {
3204
+ client.uiWorkspace.openSession(address);
3205
+ return;
3206
+ }
3207
+ client.sidebarRight.openResource(subagentChatAddress(address), {
3208
+ kind: "subagentchat",
3209
+ preferNewPane: true
3210
+ });
3211
+ },
3212
+ refreshProjection: (sessionId) => {
3213
+ Promise.resolve(client.sessions.refreshProjections(sessionId)).catch(() => {});
3214
+ },
3215
+ hostT: locale.bind("subagent"),
3216
+ locale
3217
+ });
3218
+ client.slots.inject("conversation.session.header.lineage", () => client.slots.register({
3219
+ name: "conversation.session.header.lineage",
3220
+ priority: -10,
3221
+ inject: catalogActions
3222
+ }, SubagentHeaderLineage));
3223
+ client.slots.inject("conversation.session.header.actions", () => client.slots.register({
3224
+ name: "conversation.session.header.actions",
3225
+ id: "subagent-catalog",
3226
+ order: 10,
3227
+ priority: -10,
3228
+ inject: catalogActions
3229
+ }, SubagentCatalogAction));
2656
3230
  }
2657
3231
  //#endregion
2658
3232
  exports.BUNDLE = BUNDLE;
2659
3233
  exports.CHANNEL = CHANNEL;
2660
- exports.FieldMenu = FieldMenu;
2661
- exports.HubPage = HubPage;
3234
+ exports.SubagentModelsPage = SubagentModelsPage;
2662
3235
  exports.apply = apply;
2663
- exports.catalogFailures = catalogFailures;
2664
3236
  exports.inject = inject;
2665
3237
  exports.name = name;
2666
3238