@klarkxy/dsh-model-hub 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -12
- package/docs/README.zh-CN.md +9 -12
- package/lib/client.inner.cjs +2093 -1521
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +2093 -1521
- package/lib/index.d.ts +1 -39
- package/lib/index.js +120 -512
- package/lib/index.js.map +1 -1
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +5 -5
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
|
-
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
*
|
|
27
|
-
*
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return
|
|
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
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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-
|
|
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
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
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
|
|
1695
|
-
*
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1698
|
-
*
|
|
1699
|
-
*
|
|
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) =>
|
|
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
|
|
1741
|
-
enabledLabel: "Enable subagent
|
|
1742
|
-
enabledHint: "
|
|
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.
|
|
1748
|
-
savedRefreshFailed: (message) => `Saved;
|
|
1749
|
-
conflict: "
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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:
|
|
2249
|
+
current: activePending,
|
|
2149
2250
|
label: t("addModel"),
|
|
2150
2251
|
allowFollow: false,
|
|
2151
2252
|
followLabel: t("addModel"),
|
|
2152
|
-
reasoningEffort:
|
|
2253
|
+
reasoningEffort: true,
|
|
2254
|
+
display: "short",
|
|
2153
2255
|
disabled: disabled || catalog === null,
|
|
2154
|
-
onSet: (picked) =>
|
|
2155
|
-
|
|
2156
|
-
|
|
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 —
|
|
2200
|
-
*
|
|
2201
|
-
*
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
2204
|
-
*
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
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
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
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
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
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
|
-
|
|
2304
|
-
|
|
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
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
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
|
-
|
|
2311
|
-
|
|
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
|
-
|
|
2314
|
-
|
|
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
|
-
|
|
2317
|
-
|
|
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
|
|
2320
|
-
const
|
|
2321
|
-
const
|
|
2322
|
-
const
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
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
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
if (
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
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 (
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
|
|
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
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
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
|
-
|
|
2385
|
-
|
|
2386
|
-
scopeRef.current = null;
|
|
2933
|
+
window.removeEventListener("resize", placeMenu);
|
|
2934
|
+
document.removeEventListener("scroll", placeMenu, true);
|
|
2387
2935
|
};
|
|
2388
|
-
}, [
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
]);
|
|
2393
|
-
const
|
|
2394
|
-
|
|
2395
|
-
if (
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
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
|
-
|
|
2423
|
-
className: "dsh-model-hub",
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
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
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
const
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
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
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
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), () =>
|
|
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
|
-
|
|
3190
|
+
setCastRoutePreferences: (payload) => call("cast.setPreferences", payload)
|
|
2653
3191
|
});
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
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
|
|
3200
|
+
locale
|
|
2660
3201
|
})
|
|
2661
|
-
},
|
|
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.
|
|
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
|
|