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