@klarkxy/dsh-model-hub 0.1.8 → 0.2.1

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