@klarkxy/dsh-git-commit 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/docs/README.zh-CN.md +3 -0
- package/lib/client.inner.cjs +1058 -127
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +1058 -127
- package/lib/contracts.d.ts +1 -1
- package/lib/index.d.ts +4 -1
- package/lib/index.js +14 -4
- package/lib/index.js.map +1 -1
- package/package.json +21 -11
package/lib/client.inner.cjs
CHANGED
|
@@ -3,32 +3,65 @@ let react = require("react");
|
|
|
3
3
|
let react_dom = require("react-dom");
|
|
4
4
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
5
5
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
|
-
//#region ../dsh-model-route/src/route.ts
|
|
7
|
-
const KEY_SEPARATOR = "";
|
|
8
|
-
/** Stable key for one route, safe to use as a select-option value. */
|
|
9
|
-
function modelRouteKey(provider, model) {
|
|
10
|
-
return provider && model ? `${provider}${KEY_SEPARATOR}${model}` : "";
|
|
11
|
-
}
|
|
12
|
-
/** Inverse of modelRouteKey; undefined for malformed keys. */
|
|
13
|
-
function parseModelRouteKey(value) {
|
|
14
|
-
const index = value.indexOf(KEY_SEPARATOR);
|
|
15
|
-
if (index <= 0) return void 0;
|
|
16
|
-
const provider = value.slice(0, index);
|
|
17
|
-
const model = value.slice(index + 1);
|
|
18
|
-
if (!provider || !model) return void 0;
|
|
19
|
-
return {
|
|
20
|
-
provider,
|
|
21
|
-
model
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
//#endregion
|
|
25
6
|
//#region ../dsh-model-route/src/catalog.ts
|
|
7
|
+
/** Both services are required: Cordis checks the nested namespace separately. */
|
|
8
|
+
const MODEL_CATALOG_INJECT = ["remote", "remote.session"];
|
|
26
9
|
function asRecord(value) {
|
|
27
10
|
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
28
11
|
}
|
|
29
12
|
function asText(value) {
|
|
30
13
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
31
14
|
}
|
|
15
|
+
/** A useful diagnostic even when a transport rejects with a plain object. */
|
|
16
|
+
function modelCatalogErrorMessage(value) {
|
|
17
|
+
if (value instanceof Error && value.message) return value.message;
|
|
18
|
+
return asText(asRecord(value)?.message) ?? asText(value) ?? "Unable to load the model catalog.";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Validate and unwrap a successful catalog without discarding provider failures
|
|
22
|
+
* or newer host metadata. Names and metadata remain optional for older hosts.
|
|
23
|
+
* A broken response is never presented as a successfully loaded empty menu.
|
|
24
|
+
*/
|
|
25
|
+
function checkedModelCatalog(value) {
|
|
26
|
+
const envelope = asRecord(value);
|
|
27
|
+
if (envelope?.ok === false) throw new Error(modelCatalogErrorMessage(envelope.error ?? envelope));
|
|
28
|
+
const root = envelope?.ok === true ? asRecord(envelope.value) : envelope;
|
|
29
|
+
const malformed = (field) => {
|
|
30
|
+
throw new Error(`Invalid model catalog: ${field}.`);
|
|
31
|
+
};
|
|
32
|
+
if (!root || !Array.isArray(root.groups)) return malformed("groups must be an array");
|
|
33
|
+
for (const value of root.groups) {
|
|
34
|
+
const group = asRecord(value);
|
|
35
|
+
if (!group || !asText(group.id) || !Array.isArray(group.models)) return malformed("each group needs an id and models array");
|
|
36
|
+
for (const value of group.models) {
|
|
37
|
+
const model = asRecord(value);
|
|
38
|
+
if (!model || !asText(model.id)) return malformed("each model needs an id");
|
|
39
|
+
if (model.reasoning !== void 0) {
|
|
40
|
+
const reasoning = asRecord(model.reasoning);
|
|
41
|
+
if (!reasoning || !Array.isArray(reasoning.efforts) || reasoning.efforts.some((value) => !parseEffort(value))) return malformed("reasoning efforts must be an array of identified efforts");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (root.failures !== void 0 && (!Array.isArray(root.failures) || root.failures.some((value) => {
|
|
46
|
+
const failure = asRecord(value);
|
|
47
|
+
return !failure || !asText(failure.id) || typeof failure.name !== "string" || typeof failure.message !== "string";
|
|
48
|
+
}))) return malformed("failures must be an array of provider failures");
|
|
49
|
+
return root;
|
|
50
|
+
}
|
|
51
|
+
/** Read through the injected session namespace; absence and rejection are errors. */
|
|
52
|
+
async function readModelCatalog(client) {
|
|
53
|
+
const session = client.remote?.session;
|
|
54
|
+
if (typeof session?.modelCatalog !== "function") throw new Error("Model catalog unavailable: inject both remote and remote.session.");
|
|
55
|
+
return checkedModelCatalog(await session.modelCatalog());
|
|
56
|
+
}
|
|
57
|
+
/** Provider failures are partial results, not a transport failure. */
|
|
58
|
+
function modelCatalogFailures(value) {
|
|
59
|
+
try {
|
|
60
|
+
return checkedModelCatalog(value).failures ?? [];
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
32
65
|
function parseEffort(value) {
|
|
33
66
|
const row = asRecord(value);
|
|
34
67
|
const id = row ? asText(row.id) : void 0;
|
|
@@ -84,6 +117,850 @@ function modelMenuEffortOptions(choice, current) {
|
|
|
84
117
|
});
|
|
85
118
|
return listed;
|
|
86
119
|
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region ../dsh-model-route/src/menu.ts
|
|
122
|
+
/** Localize standard effort ids only when the provider has not supplied a display name. */
|
|
123
|
+
function modelMenuEffortLabel(effort, locale) {
|
|
124
|
+
if (effort.name !== effort.id) return effort.name;
|
|
125
|
+
return {
|
|
126
|
+
off: ["关闭", "Off"],
|
|
127
|
+
none: ["无", "None"],
|
|
128
|
+
minimal: ["最低", "Minimal"],
|
|
129
|
+
low: ["低", "Low"],
|
|
130
|
+
medium: ["中", "Medium"],
|
|
131
|
+
high: ["高", "High"],
|
|
132
|
+
xhigh: ["极高", "Extra high"],
|
|
133
|
+
max: ["最高", "Maximum"],
|
|
134
|
+
ultra: ["超高", "Ultra"]
|
|
135
|
+
}[effort.id]?.[locale?.toLowerCase().startsWith("zh") ? 0 : 1] ?? effort.name;
|
|
136
|
+
}
|
|
137
|
+
/** The short model label shown inside a provider group. */
|
|
138
|
+
function modelMenuShortName(choice) {
|
|
139
|
+
const sep = choice.label.indexOf(" / ");
|
|
140
|
+
return sep > 0 ? choice.label.slice(sep + 3) : choice.model;
|
|
141
|
+
}
|
|
142
|
+
/** Group catalog choices by provider and filter them by a case-insensitive query. */
|
|
143
|
+
function groupModelMenuChoices(choices, query) {
|
|
144
|
+
const needle = query.trim().toLowerCase();
|
|
145
|
+
const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
|
|
146
|
+
const groups = [];
|
|
147
|
+
for (const choice of matched) {
|
|
148
|
+
const sep = choice.label.indexOf(" / ");
|
|
149
|
+
const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
|
|
150
|
+
const last = groups.at(-1);
|
|
151
|
+
if (last && last.provider === choice.provider) last.items.push(choice);
|
|
152
|
+
else groups.push({
|
|
153
|
+
provider: choice.provider,
|
|
154
|
+
name,
|
|
155
|
+
items: [choice]
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return groups;
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region ../dsh-model-route/src/catalog-ui.tsx
|
|
162
|
+
/**
|
|
163
|
+
* Latest refresh wins, including retries while a previous request is pending.
|
|
164
|
+
* Keep load stable (for example with useCallback); changing it invalidates the
|
|
165
|
+
* previous source and fetches again, so switching clients cannot publish stale data.
|
|
166
|
+
*/
|
|
167
|
+
function useModelCatalog(load, options = {}) {
|
|
168
|
+
const enabled = options.enabled !== false;
|
|
169
|
+
const enabledRef = (0, react.useRef)(enabled);
|
|
170
|
+
enabledRef.current = enabled;
|
|
171
|
+
const live = (0, react.useRef)(false);
|
|
172
|
+
const source = (0, react.useRef)(load);
|
|
173
|
+
const revision = (0, react.useRef)(0);
|
|
174
|
+
const [state, setState] = (0, react.useState)(() => {
|
|
175
|
+
try {
|
|
176
|
+
return {
|
|
177
|
+
catalog: options.initialCatalog === void 0 ? void 0 : checkedModelCatalog(options.initialCatalog),
|
|
178
|
+
loading: enabled,
|
|
179
|
+
error: ""
|
|
180
|
+
};
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return {
|
|
183
|
+
catalog: void 0,
|
|
184
|
+
loading: enabled,
|
|
185
|
+
error: modelCatalogErrorMessage(error)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
190
|
+
if (!live.current || !enabledRef.current) return;
|
|
191
|
+
const request = ++revision.current;
|
|
192
|
+
setState((previous) => ({
|
|
193
|
+
...previous,
|
|
194
|
+
loading: true,
|
|
195
|
+
error: ""
|
|
196
|
+
}));
|
|
197
|
+
try {
|
|
198
|
+
const catalog = checkedModelCatalog(await load());
|
|
199
|
+
if (live.current && request === revision.current) setState({
|
|
200
|
+
catalog,
|
|
201
|
+
loading: false,
|
|
202
|
+
error: ""
|
|
203
|
+
});
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (live.current && request === revision.current) setState((previous) => ({
|
|
206
|
+
...previous,
|
|
207
|
+
loading: false,
|
|
208
|
+
error: modelCatalogErrorMessage(error)
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
}, [load, enabled]);
|
|
212
|
+
(0, react.useEffect)(() => {
|
|
213
|
+
live.current = true;
|
|
214
|
+
if (source.current !== load) {
|
|
215
|
+
source.current = load;
|
|
216
|
+
setState({
|
|
217
|
+
catalog: void 0,
|
|
218
|
+
loading: enabled,
|
|
219
|
+
error: ""
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (enabled) refresh();
|
|
223
|
+
else setState((previous) => ({
|
|
224
|
+
...previous,
|
|
225
|
+
loading: false
|
|
226
|
+
}));
|
|
227
|
+
return () => {
|
|
228
|
+
live.current = false;
|
|
229
|
+
++revision.current;
|
|
230
|
+
};
|
|
231
|
+
}, [enabled, refresh]);
|
|
232
|
+
(0, react.useEffect)(() => {
|
|
233
|
+
if (!enabled) return;
|
|
234
|
+
const reload = () => {
|
|
235
|
+
refresh();
|
|
236
|
+
};
|
|
237
|
+
if (typeof window !== "undefined") window.addEventListener("focus", reload);
|
|
238
|
+
let unsubscribe;
|
|
239
|
+
try {
|
|
240
|
+
unsubscribe = options.subscribe?.(reload);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
setState((previous) => ({
|
|
243
|
+
...previous,
|
|
244
|
+
error: modelCatalogErrorMessage(error)
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
return () => {
|
|
248
|
+
if (typeof window !== "undefined") window.removeEventListener("focus", reload);
|
|
249
|
+
unsubscribe?.();
|
|
250
|
+
};
|
|
251
|
+
}, [
|
|
252
|
+
enabled,
|
|
253
|
+
refresh,
|
|
254
|
+
options.subscribe
|
|
255
|
+
]);
|
|
256
|
+
const choices = (0, react.useMemo)(() => parseModelMenuChoices(state.catalog), [state.catalog]);
|
|
257
|
+
const failures = (0, react.useMemo)(() => state.catalog === void 0 ? [] : modelCatalogFailures(state.catalog), [state.catalog]);
|
|
258
|
+
return {
|
|
259
|
+
...state,
|
|
260
|
+
choices,
|
|
261
|
+
failures,
|
|
262
|
+
refresh
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** Also accepts a mapped state from a caller that owns broader async orchestration. */
|
|
266
|
+
function ModelCatalogStatus({ state, locale }) {
|
|
267
|
+
const zh = locale?.toLowerCase().startsWith("zh") === true;
|
|
268
|
+
const copy = zh ? {
|
|
269
|
+
loading: "正在加载模型…",
|
|
270
|
+
error: "模型列表加载失败",
|
|
271
|
+
partial: "部分提供商的模型未能加载",
|
|
272
|
+
retry: "重试"
|
|
273
|
+
} : {
|
|
274
|
+
loading: "Loading models…",
|
|
275
|
+
error: "Unable to load models",
|
|
276
|
+
partial: "Some providers could not load their models",
|
|
277
|
+
retry: "Retry"
|
|
278
|
+
};
|
|
279
|
+
if (!state.loading && !state.error && state.failures.length === 0) return null;
|
|
280
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
281
|
+
className: "dsh-model-catalog-status",
|
|
282
|
+
lang: zh ? "zh" : "en",
|
|
283
|
+
children: [
|
|
284
|
+
state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
285
|
+
role: "status",
|
|
286
|
+
children: copy.loading
|
|
287
|
+
}) : null,
|
|
288
|
+
state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
289
|
+
role: "alert",
|
|
290
|
+
children: [
|
|
291
|
+
copy.error,
|
|
292
|
+
": ",
|
|
293
|
+
state.error
|
|
294
|
+
]
|
|
295
|
+
}) : null,
|
|
296
|
+
state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
297
|
+
role: "status",
|
|
298
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
299
|
+
failure.name || failure.id,
|
|
300
|
+
": ",
|
|
301
|
+
failure.message
|
|
302
|
+
] }, `${failure.id}:${index}`)) })]
|
|
303
|
+
}) : null,
|
|
304
|
+
state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
305
|
+
type: "button",
|
|
306
|
+
size: "sm",
|
|
307
|
+
disabled: state.loading,
|
|
308
|
+
onClick: () => {
|
|
309
|
+
state.refresh();
|
|
310
|
+
},
|
|
311
|
+
children: copy.retry
|
|
312
|
+
}) : null
|
|
313
|
+
]
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region ../dsh-model-route/src/ui.tsx
|
|
318
|
+
/**
|
|
319
|
+
* ModelMenu: the standard editor of one model-route field — the visible half
|
|
320
|
+
* of the x-model-route contract. A feature plugin's settings page renders it
|
|
321
|
+
* for its own marked field; the model hub renders one per detected field.
|
|
322
|
+
*
|
|
323
|
+
* It is a plugin-side copy of the host's composer model picker
|
|
324
|
+
* (dsh-client-ui-model-selection's ModelSelect), rebuilt from the official
|
|
325
|
+
* primitives and the published bundle's behavior: one card that opens on the
|
|
326
|
+
* root pane (Model / Effort cells), drills into the provider-grouped,
|
|
327
|
+
* searchable model list or the effort list, and Escapes back before closing.
|
|
328
|
+
* Sticky group headings, subsequence ranking, highlight walking and focus
|
|
329
|
+
* hand-back all match the host. SYNC POINT: when the host's ModelSelect or
|
|
330
|
+
* ModelSelect.module.css changes, this copy must be re-transcribed; styles
|
|
331
|
+
* below may only reference --dsw-* theme tokens, never literal colors.
|
|
332
|
+
*
|
|
333
|
+
* It statically imports the primitives, so only browser clients should load
|
|
334
|
+
* this entry; host code and node tests use the core entry (`./index.ts`).
|
|
335
|
+
*/
|
|
336
|
+
/** Unplaced portal card: hidden but laid out at a fixed origin so measurements are real. */
|
|
337
|
+
const MEASURE_STYLE = {
|
|
338
|
+
visibility: "hidden",
|
|
339
|
+
left: 0,
|
|
340
|
+
top: 0
|
|
341
|
+
};
|
|
342
|
+
function ModelMenu(props) {
|
|
343
|
+
const language = props.locale?.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
344
|
+
const copy = language === "zh" ? {
|
|
345
|
+
model: "模型",
|
|
346
|
+
effort: "思考强度",
|
|
347
|
+
default: "默认",
|
|
348
|
+
search: "搜索模型…",
|
|
349
|
+
empty: "没有匹配的模型",
|
|
350
|
+
clear: "清除搜索"
|
|
351
|
+
} : {
|
|
352
|
+
model: "Model",
|
|
353
|
+
effort: "Reasoning",
|
|
354
|
+
default: "Default",
|
|
355
|
+
search: "Search models…",
|
|
356
|
+
empty: "No matching models",
|
|
357
|
+
clear: "Clear search"
|
|
358
|
+
};
|
|
359
|
+
const { open, onOpenChange, anchor, choices, selected, onPick, leading = [], onPickLeading, efforts = [], selectedEffort, onPickEffort, modelLabel = copy.model, modelValue = "—", effortLabel = copy.effort, effortValue, defaultEffortLabel = copy.default, searchPlaceholder = copy.search, emptyLabel = copy.empty, clearSearchLabel = copy.clear, className, listClassName } = props;
|
|
360
|
+
const rootRef = (0, react.useRef)(null);
|
|
361
|
+
const openRef = (0, react.useRef)(open);
|
|
362
|
+
openRef.current = open;
|
|
363
|
+
const pendingOpenRef = (0, react.useRef)(false);
|
|
364
|
+
const pendingRestoreFocus = (0, react.useRef)(false);
|
|
365
|
+
const [requestedOpen, setRequestedOpen] = (0, react.useState)(false);
|
|
366
|
+
const menuRef = (0, react.useRef)(null);
|
|
367
|
+
const groupsRef = (0, react.useRef)(null);
|
|
368
|
+
const searchRef = (0, react.useRef)(null);
|
|
369
|
+
const itemRefs = (0, react.useRef)([]);
|
|
370
|
+
const paneFocus = (0, react.useRef)(null);
|
|
371
|
+
const [pane, setPane] = (0, react.useState)("root");
|
|
372
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
373
|
+
const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
|
|
374
|
+
const [menuPos, setMenuPos] = (0, react.useState)(null);
|
|
375
|
+
const id = (0, react.useId)();
|
|
376
|
+
const showSearch = choices.length > 4;
|
|
377
|
+
const effortRows = (0, react.useMemo)(() => [{
|
|
378
|
+
id: "",
|
|
379
|
+
name: defaultEffortLabel
|
|
380
|
+
}, ...efforts.map((effort) => ({
|
|
381
|
+
...effort,
|
|
382
|
+
name: modelMenuEffortLabel(effort, language)
|
|
383
|
+
}))], [
|
|
384
|
+
defaultEffortLabel,
|
|
385
|
+
efforts,
|
|
386
|
+
language
|
|
387
|
+
]);
|
|
388
|
+
const filteredGroups = (0, react.useMemo)(() => groupModelMenuChoices(choices, "").map((group) => ({
|
|
389
|
+
...group,
|
|
390
|
+
items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
|
|
391
|
+
choice,
|
|
392
|
+
name: modelMenuShortName(choice),
|
|
393
|
+
label: choice.model
|
|
394
|
+
})), showSearch ? query.trim() : "")
|
|
395
|
+
})).filter((group) => group.items.length > 0), [
|
|
396
|
+
choices,
|
|
397
|
+
query,
|
|
398
|
+
showSearch
|
|
399
|
+
]);
|
|
400
|
+
const visibleModels = (0, react.useMemo)(() => filteredGroups.flatMap((group) => group.items.map((item) => item.choice)), [filteredGroups]);
|
|
401
|
+
const currentVisibleIndex = visibleModels.findIndex((choice) => choice.provider === selected?.provider && choice.model === selected.model);
|
|
402
|
+
const activeModelIndex = Math.min(highlightedIndex ?? Math.max(0, currentVisibleIndex), visibleModels.length - 1);
|
|
403
|
+
const changeOpen = (next) => {
|
|
404
|
+
if (openRef.current === next) return;
|
|
405
|
+
openRef.current = next;
|
|
406
|
+
onOpenChange(next);
|
|
407
|
+
};
|
|
408
|
+
const requestOpen = () => {
|
|
409
|
+
if (open || pendingOpenRef.current) return;
|
|
410
|
+
pendingOpenRef.current = true;
|
|
411
|
+
pendingRestoreFocus.current = false;
|
|
412
|
+
setRequestedOpen(true);
|
|
413
|
+
};
|
|
414
|
+
const close = (restoreFocus = false) => {
|
|
415
|
+
const active = document.activeElement;
|
|
416
|
+
pendingRestoreFocus.current = restoreFocus && (active === document.body || rootRef.current?.contains(active) === true || menuRef.current?.contains(active) === true);
|
|
417
|
+
changeOpen(false);
|
|
418
|
+
};
|
|
419
|
+
const drill = (next) => {
|
|
420
|
+
setQuery("");
|
|
421
|
+
setHighlightedIndex(null);
|
|
422
|
+
paneFocus.current = "drill";
|
|
423
|
+
setPane(next);
|
|
424
|
+
};
|
|
425
|
+
/** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
|
|
426
|
+
const back = (from) => {
|
|
427
|
+
paneFocus.current = from;
|
|
428
|
+
setPane("root");
|
|
429
|
+
};
|
|
430
|
+
const moveFocus = (offset) => {
|
|
431
|
+
const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
|
|
432
|
+
if (!items.length) return;
|
|
433
|
+
const active = items.findIndex((item) => item === document.activeElement);
|
|
434
|
+
items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
|
|
435
|
+
};
|
|
436
|
+
(0, react.useLayoutEffect)(() => {
|
|
437
|
+
if (open || !pendingRestoreFocus.current) return;
|
|
438
|
+
const active = document.activeElement;
|
|
439
|
+
if (active !== document.body && !rootRef.current?.contains(active)) {
|
|
440
|
+
pendingRestoreFocus.current = false;
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const trigger = rootRef.current?.querySelector("button");
|
|
444
|
+
if (!trigger || trigger.disabled) return;
|
|
445
|
+
trigger.focus();
|
|
446
|
+
if (document.activeElement === trigger) pendingRestoreFocus.current = false;
|
|
447
|
+
});
|
|
448
|
+
(0, react.useEffect)(() => {
|
|
449
|
+
if (open || !pendingRestoreFocus.current) return;
|
|
450
|
+
const cancelOutside = (event) => {
|
|
451
|
+
if (event.target instanceof Node && !rootRef.current?.contains(event.target)) pendingRestoreFocus.current = false;
|
|
452
|
+
};
|
|
453
|
+
document.addEventListener("focusin", cancelOutside);
|
|
454
|
+
document.addEventListener("pointerdown", cancelOutside);
|
|
455
|
+
return () => {
|
|
456
|
+
document.removeEventListener("focusin", cancelOutside);
|
|
457
|
+
document.removeEventListener("pointerdown", cancelOutside);
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
(0, react.useEffect)(() => {
|
|
461
|
+
if (!requestedOpen) return;
|
|
462
|
+
pendingOpenRef.current = false;
|
|
463
|
+
setRequestedOpen(false);
|
|
464
|
+
if (!open) changeOpen(true);
|
|
465
|
+
}, [requestedOpen, open]);
|
|
466
|
+
(0, react.useEffect)(() => {
|
|
467
|
+
if (!open) return;
|
|
468
|
+
setPane("root");
|
|
469
|
+
setQuery("");
|
|
470
|
+
setHighlightedIndex(null);
|
|
471
|
+
}, [open]);
|
|
472
|
+
(0, react.useEffect)(() => {
|
|
473
|
+
if (!open) return;
|
|
474
|
+
const closeOutside = (event) => {
|
|
475
|
+
const target = event.target;
|
|
476
|
+
if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
|
|
477
|
+
changeOpen(false);
|
|
478
|
+
};
|
|
479
|
+
document.addEventListener("mousedown", closeOutside);
|
|
480
|
+
return () => document.removeEventListener("mousedown", closeOutside);
|
|
481
|
+
}, [open, onOpenChange]);
|
|
482
|
+
(0, react.useEffect)(() => {
|
|
483
|
+
const intent = paneFocus.current;
|
|
484
|
+
if (!open || menuPos === null || intent === null) return;
|
|
485
|
+
if (intent !== "drill" && pane !== "root") return;
|
|
486
|
+
paneFocus.current = null;
|
|
487
|
+
if (intent === "drill") {
|
|
488
|
+
if (pane === "model" && showSearch) {
|
|
489
|
+
searchRef.current?.focus();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
(menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? itemRefs.current.find((item) => item !== null && !item.disabled))?.focus();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
itemRefs.current[intent === "effort" ? 1 : 0]?.focus();
|
|
496
|
+
}, [
|
|
497
|
+
open,
|
|
498
|
+
pane,
|
|
499
|
+
showSearch,
|
|
500
|
+
menuPos
|
|
501
|
+
]);
|
|
502
|
+
(0, react.useEffect)(() => {
|
|
503
|
+
const viewport = groupsRef.current;
|
|
504
|
+
if (viewport === null) return;
|
|
505
|
+
return (0, _deepseek_ai_dsh_client_ui_primitives.observeStickyMenuGroups)(viewport);
|
|
506
|
+
}, [
|
|
507
|
+
open,
|
|
508
|
+
pane,
|
|
509
|
+
filteredGroups
|
|
510
|
+
]);
|
|
511
|
+
(0, react.useLayoutEffect)(() => {
|
|
512
|
+
if (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
|
|
513
|
+
}, [
|
|
514
|
+
open,
|
|
515
|
+
pane,
|
|
516
|
+
activeModelIndex,
|
|
517
|
+
visibleModels,
|
|
518
|
+
leading.length
|
|
519
|
+
]);
|
|
520
|
+
(0, react.useLayoutEffect)(() => {
|
|
521
|
+
if (!open) {
|
|
522
|
+
setMenuPos(null);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const place = () => {
|
|
526
|
+
const rect = rootRef.current?.getBoundingClientRect();
|
|
527
|
+
if (!rect) return;
|
|
528
|
+
const margin = 12, lw = menuRef.current?.offsetWidth ?? 0, lh = menuRef.current?.offsetHeight ?? 0;
|
|
529
|
+
let x = rect.right - lw, y = rect.top - 8 - lh;
|
|
530
|
+
if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
|
|
531
|
+
if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
|
|
532
|
+
setMenuPos({
|
|
533
|
+
left: x,
|
|
534
|
+
top: y
|
|
535
|
+
});
|
|
536
|
+
};
|
|
537
|
+
place();
|
|
538
|
+
window.addEventListener("scroll", place, true);
|
|
539
|
+
window.addEventListener("resize", place);
|
|
540
|
+
return () => {
|
|
541
|
+
window.removeEventListener("scroll", place, true);
|
|
542
|
+
window.removeEventListener("resize", place);
|
|
543
|
+
};
|
|
544
|
+
}, [
|
|
545
|
+
open,
|
|
546
|
+
pane,
|
|
547
|
+
choices,
|
|
548
|
+
query
|
|
549
|
+
]);
|
|
550
|
+
function onKeyDown(event) {
|
|
551
|
+
if (event.defaultPrevented || event.nativeEvent.isComposing) return;
|
|
552
|
+
if (!open && [
|
|
553
|
+
"Enter",
|
|
554
|
+
" ",
|
|
555
|
+
"ArrowDown",
|
|
556
|
+
"ArrowUp"
|
|
557
|
+
].includes(event.key)) {
|
|
558
|
+
const button = event.target instanceof Element ? event.target.closest("button") : null;
|
|
559
|
+
if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
|
|
560
|
+
event.preventDefault();
|
|
561
|
+
paneFocus.current = "model";
|
|
562
|
+
requestOpen();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (event.key === "Escape" && open) {
|
|
566
|
+
event.preventDefault();
|
|
567
|
+
if (pane !== "root") back(pane);
|
|
568
|
+
else close(true);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (!open) return;
|
|
572
|
+
if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
573
|
+
event.preventDefault();
|
|
574
|
+
if (visibleModels.length > 0) {
|
|
575
|
+
setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
|
|
576
|
+
searchRef.current?.focus();
|
|
577
|
+
}
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (pane === "model" && showSearch && event.target instanceof HTMLInputElement && (event.key === "Enter" || event.key === "Tab" && !event.shiftKey)) {
|
|
581
|
+
if (event.key === "Tab" && visibleModels.length === 0) return;
|
|
582
|
+
event.preventDefault();
|
|
583
|
+
const highlighted = visibleModels[activeModelIndex];
|
|
584
|
+
if (highlighted) {
|
|
585
|
+
onPick({
|
|
586
|
+
provider: highlighted.provider,
|
|
587
|
+
model: highlighted.model
|
|
588
|
+
});
|
|
589
|
+
close(true);
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (event.key === "Tab") {
|
|
594
|
+
if (event.shiftKey) {
|
|
595
|
+
event.preventDefault();
|
|
596
|
+
if (pane !== "root") back(pane);
|
|
597
|
+
else close(true);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const focused = document.activeElement;
|
|
601
|
+
const rows = itemRefs.current.filter((item) => item !== null);
|
|
602
|
+
if (focused instanceof HTMLButtonElement && rows.includes(focused)) {
|
|
603
|
+
event.preventDefault();
|
|
604
|
+
focused.click();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (pane === "model" && showSearch) {
|
|
608
|
+
event.preventDefault();
|
|
609
|
+
setHighlightedIndex(null);
|
|
610
|
+
searchRef.current?.focus();
|
|
611
|
+
}
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
615
|
+
event.preventDefault();
|
|
616
|
+
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function onBlur(event) {
|
|
620
|
+
if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) || menuRef.current?.contains(event.relatedTarget))) return;
|
|
621
|
+
if (open) changeOpen(false);
|
|
622
|
+
}
|
|
623
|
+
itemRefs.current = [];
|
|
624
|
+
let itemIndex = 0;
|
|
625
|
+
let modelIndex = 0;
|
|
626
|
+
const itemRef = () => {
|
|
627
|
+
const at = itemIndex++;
|
|
628
|
+
return (node) => {
|
|
629
|
+
itemRefs.current[at] = node;
|
|
630
|
+
};
|
|
631
|
+
};
|
|
632
|
+
const menu = open && typeof document !== "undefined" ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
|
|
633
|
+
ref: menuRef,
|
|
634
|
+
id: `${id}-menu`,
|
|
635
|
+
className: `dsh-model-menu-menu${listClassName ? ` ${listClassName}` : ""}`,
|
|
636
|
+
style: menuPos ?? MEASURE_STYLE,
|
|
637
|
+
role: pane === "model" ? "group" : "menu",
|
|
638
|
+
"aria-label": modelLabel,
|
|
639
|
+
lang: language,
|
|
640
|
+
children: [
|
|
641
|
+
pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
642
|
+
ref: itemRef(),
|
|
643
|
+
type: "button",
|
|
644
|
+
role: "menuitem",
|
|
645
|
+
className: "dsh-model-menu-cell",
|
|
646
|
+
onClick: () => drill("model"),
|
|
647
|
+
children: [
|
|
648
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
649
|
+
className: "dsh-model-menu-cellLabel",
|
|
650
|
+
children: modelLabel
|
|
651
|
+
}),
|
|
652
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
653
|
+
className: "dsh-model-menu-cellValue",
|
|
654
|
+
children: modelValue
|
|
655
|
+
}),
|
|
656
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
|
|
657
|
+
]
|
|
658
|
+
}), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
659
|
+
ref: itemRef(),
|
|
660
|
+
type: "button",
|
|
661
|
+
role: "menuitem",
|
|
662
|
+
className: "dsh-model-menu-cell",
|
|
663
|
+
onClick: () => drill("effort"),
|
|
664
|
+
children: [
|
|
665
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
666
|
+
className: "dsh-model-menu-cellLabel",
|
|
667
|
+
children: effortLabel
|
|
668
|
+
}),
|
|
669
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
670
|
+
className: "dsh-model-menu-cellValue",
|
|
671
|
+
children: effortValue ?? effortRows.find((row) => row.id === (selectedEffort ?? ""))?.name ?? defaultEffortLabel
|
|
672
|
+
}),
|
|
673
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
|
|
674
|
+
]
|
|
675
|
+
}) : null] }) : null,
|
|
676
|
+
pane === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
677
|
+
showSearch ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
678
|
+
className: "dsh-model-menu-searchRow",
|
|
679
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
680
|
+
ref: searchRef,
|
|
681
|
+
className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
|
|
682
|
+
type: "text",
|
|
683
|
+
role: "searchbox",
|
|
684
|
+
"aria-label": searchPlaceholder,
|
|
685
|
+
"aria-controls": `${id}-models`,
|
|
686
|
+
"aria-activedescendant": activeModelIndex < 0 ? void 0 : `${id}-model-${activeModelIndex}`,
|
|
687
|
+
placeholder: searchPlaceholder,
|
|
688
|
+
value: query,
|
|
689
|
+
onChange: (event) => {
|
|
690
|
+
setQuery(event.target.value);
|
|
691
|
+
setHighlightedIndex(0);
|
|
692
|
+
}
|
|
693
|
+
}), query !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
694
|
+
type: "button",
|
|
695
|
+
className: "dsh-model-menu-searchClear",
|
|
696
|
+
"aria-label": clearSearchLabel,
|
|
697
|
+
onClick: () => {
|
|
698
|
+
setQuery("");
|
|
699
|
+
setHighlightedIndex(null);
|
|
700
|
+
searchRef.current?.focus();
|
|
701
|
+
},
|
|
702
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFillRegular, {})
|
|
703
|
+
}) : null]
|
|
704
|
+
}) : null,
|
|
705
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
706
|
+
ref: groupsRef,
|
|
707
|
+
id: `${id}-models`,
|
|
708
|
+
className: "dsh-model-menu-groups scrollable",
|
|
709
|
+
role: "menu",
|
|
710
|
+
"aria-label": modelLabel,
|
|
711
|
+
hidden: filteredGroups.length === 0,
|
|
712
|
+
children: [leading.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
713
|
+
className: "dsh-model-menu-leading",
|
|
714
|
+
children: leading.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
715
|
+
ref: itemRef(),
|
|
716
|
+
type: "button",
|
|
717
|
+
role: "menuitemradio",
|
|
718
|
+
"aria-checked": item.selected === true,
|
|
719
|
+
className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
|
|
720
|
+
onClick: () => {
|
|
721
|
+
onPickLeading?.(item.id);
|
|
722
|
+
close(true);
|
|
723
|
+
},
|
|
724
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
725
|
+
className: "dsh-model-menu-optionCopy",
|
|
726
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
727
|
+
className: "dsh-model-menu-modelName",
|
|
728
|
+
children: item.label
|
|
729
|
+
})
|
|
730
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
731
|
+
className: "dsh-model-menu-check",
|
|
732
|
+
children: item.selected === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
733
|
+
})]
|
|
734
|
+
}, item.id))
|
|
735
|
+
}) : null, filteredGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuGroup, {
|
|
736
|
+
label: group.name,
|
|
737
|
+
children: group.items.map((item) => {
|
|
738
|
+
const index = modelIndex++;
|
|
739
|
+
const choice = item.choice;
|
|
740
|
+
const checked = selected?.provider === choice.provider && selected.model === choice.model;
|
|
741
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
742
|
+
ref: itemRef(),
|
|
743
|
+
type: "button",
|
|
744
|
+
role: "menuitemradio",
|
|
745
|
+
"aria-checked": checked,
|
|
746
|
+
id: `${id}-model-${index}`,
|
|
747
|
+
tabIndex: showSearch ? -1 : 0,
|
|
748
|
+
onFocus: () => setHighlightedIndex(index),
|
|
749
|
+
"data-highlighted": index === activeModelIndex ? "" : void 0,
|
|
750
|
+
className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
|
|
751
|
+
onMouseMove: index === activeModelIndex ? void 0 : () => {
|
|
752
|
+
if (showSearch) setHighlightedIndex(index);
|
|
753
|
+
else itemRefs.current[leading.length + index]?.focus();
|
|
754
|
+
},
|
|
755
|
+
title: choice.label,
|
|
756
|
+
onClick: () => {
|
|
757
|
+
onPick({
|
|
758
|
+
provider: choice.provider,
|
|
759
|
+
model: choice.model
|
|
760
|
+
});
|
|
761
|
+
close(true);
|
|
762
|
+
},
|
|
763
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
764
|
+
className: "dsh-model-menu-optionCopy",
|
|
765
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
766
|
+
className: "dsh-model-menu-modelName",
|
|
767
|
+
children: modelMenuShortName(choice)
|
|
768
|
+
})
|
|
769
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
770
|
+
className: "dsh-model-menu-check",
|
|
771
|
+
children: checked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
772
|
+
})]
|
|
773
|
+
}, `${choice.provider}/${choice.model}`);
|
|
774
|
+
})
|
|
775
|
+
}, group.provider))]
|
|
776
|
+
}),
|
|
777
|
+
filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
778
|
+
className: "dsh-model-menu-empty",
|
|
779
|
+
role: "status",
|
|
780
|
+
children: emptyLabel
|
|
781
|
+
}) : null
|
|
782
|
+
] }) : null,
|
|
783
|
+
pane === "effort" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: effortRows.map((row) => {
|
|
784
|
+
const checked = (selectedEffort ?? "") === row.id;
|
|
785
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
786
|
+
ref: itemRef(),
|
|
787
|
+
type: "button",
|
|
788
|
+
role: "menuitemradio",
|
|
789
|
+
"aria-checked": checked,
|
|
790
|
+
className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
|
|
791
|
+
onClick: () => {
|
|
792
|
+
onPickEffort?.(row.id || void 0);
|
|
793
|
+
close(true);
|
|
794
|
+
},
|
|
795
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
796
|
+
className: "dsh-model-menu-optionCopy",
|
|
797
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
798
|
+
className: "dsh-model-menu-modelName",
|
|
799
|
+
children: row.name
|
|
800
|
+
})
|
|
801
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
802
|
+
className: "dsh-model-menu-check",
|
|
803
|
+
children: checked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
804
|
+
})]
|
|
805
|
+
}, row.id || "__default");
|
|
806
|
+
}) }) : null
|
|
807
|
+
]
|
|
808
|
+
}), document.body) : null;
|
|
809
|
+
const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
|
|
810
|
+
"aria-haspopup": "menu",
|
|
811
|
+
"aria-expanded": open,
|
|
812
|
+
"aria-controls": open ? `${id}-menu` : void 0
|
|
813
|
+
}) : anchor;
|
|
814
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
815
|
+
ref: rootRef,
|
|
816
|
+
className: `dsh-model-menu-root${className ? ` ${className}` : ""}`,
|
|
817
|
+
onKeyDown,
|
|
818
|
+
onBlur,
|
|
819
|
+
onClick: (event) => {
|
|
820
|
+
if (event.defaultPrevented) return;
|
|
821
|
+
const button = event.target instanceof Element ? event.target.closest("button") : null;
|
|
822
|
+
if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
|
|
823
|
+
requestOpen();
|
|
824
|
+
},
|
|
825
|
+
onMouseDown: (event) => {
|
|
826
|
+
if (event.target instanceof Element && menuRef.current?.contains(event.target) && event.target.closest("button") !== null) event.preventDefault();
|
|
827
|
+
},
|
|
828
|
+
children: [trigger, menu]
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* ModelMenu styles, transcribed from the official ModelSelect.module.css (the
|
|
833
|
+
* published dsh-client-ui-model-selection bundle): card capped at
|
|
834
|
+
* min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
|
|
835
|
+
* 34px root cells at 13/20, a borderless 12px search row with a clear button,
|
|
836
|
+
* sticky group headings (MenuGroup's own CSS), and the l1-stroke prominent
|
|
837
|
+
* elevation at portal z-index 1100. The trigger mirrors the composer's 28px
|
|
838
|
+
* model pill, chevron rotating open. MenuSurface paints fill and backdrop.
|
|
839
|
+
* Only --dsw-* theme tokens may appear here — a literal color is a bug.
|
|
840
|
+
*/
|
|
841
|
+
const modelMenuCss = `
|
|
842
|
+
.dsh-model-menu-root { min-width: 0; position: relative; display: inline-flex; }
|
|
843
|
+
.dsh-model-menu-trigger {
|
|
844
|
+
border-radius: var(--dsw-radius-sm);
|
|
845
|
+
min-width: 0;
|
|
846
|
+
max-width: min(360px, 45cqw);
|
|
847
|
+
height: 28px;
|
|
848
|
+
color: var(--dsw-alias-label-secondary);
|
|
849
|
+
cursor: pointer;
|
|
850
|
+
background: none;
|
|
851
|
+
border: none;
|
|
852
|
+
outline: none;
|
|
853
|
+
align-items: center;
|
|
854
|
+
gap: 4px;
|
|
855
|
+
padding: 0 4px 0 8px;
|
|
856
|
+
font-size: 13px;
|
|
857
|
+
font-weight: 400;
|
|
858
|
+
line-height: 20px;
|
|
859
|
+
display: flex;
|
|
860
|
+
}
|
|
861
|
+
.dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
862
|
+
.dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
|
|
863
|
+
.dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
|
|
864
|
+
.dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
|
|
865
|
+
.dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
|
|
866
|
+
.dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform 0.12s; }
|
|
867
|
+
.dsh-model-menu-chevronOpen { transform: rotate(180deg); }
|
|
868
|
+
.dsh-model-menu-menu {
|
|
869
|
+
z-index: 1100;
|
|
870
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
|
871
|
+
width: max-content;
|
|
872
|
+
min-width: min(240px, 100vw - 32px);
|
|
873
|
+
max-width: min(420px, 100vw - 32px);
|
|
874
|
+
max-height: min(360px, 100vh - 96px);
|
|
875
|
+
box-shadow: var(--dsw-elevation-prominent);
|
|
876
|
+
color: var(--dsw-alias-label-primary);
|
|
877
|
+
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
|
|
878
|
+
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
|
|
879
|
+
border: 0;
|
|
880
|
+
flex-direction: column;
|
|
881
|
+
padding: 4px;
|
|
882
|
+
display: flex;
|
|
883
|
+
position: fixed;
|
|
884
|
+
overflow: hidden;
|
|
885
|
+
}
|
|
886
|
+
.dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
|
|
887
|
+
.dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
|
|
888
|
+
.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; }
|
|
889
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
|
|
890
|
+
.dsh-model-menu-searchWithQuery { padding-right: 34px; }
|
|
891
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
|
|
892
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
|
|
893
|
+
.dsh-model-menu-searchClear {
|
|
894
|
+
width: 24px;
|
|
895
|
+
height: 24px;
|
|
896
|
+
color: var(--dsw-alias-label-secondary);
|
|
897
|
+
cursor: pointer;
|
|
898
|
+
background: none;
|
|
899
|
+
border: none;
|
|
900
|
+
border-radius: 50%;
|
|
901
|
+
justify-content: center;
|
|
902
|
+
align-items: center;
|
|
903
|
+
padding: 0;
|
|
904
|
+
display: inline-flex;
|
|
905
|
+
position: absolute;
|
|
906
|
+
top: 50%;
|
|
907
|
+
right: 4px;
|
|
908
|
+
transform: translateY(-50%);
|
|
909
|
+
}
|
|
910
|
+
.dsh-model-menu-searchClear:hover, .dsh-model-menu-searchClear:focus-visible { background: var(--dsw-alias-interactive-bg-hover); outline: none; }
|
|
911
|
+
.dsh-model-menu-groups { min-height: 0; overflow-y: auto; }
|
|
912
|
+
.dsh-model-menu-leading { border-bottom: 0.5px solid var(--dsw-alias-border-l2); margin-bottom: 3px; padding-bottom: 3px; }
|
|
913
|
+
.dsh-model-menu-option {
|
|
914
|
+
box-sizing: border-box;
|
|
915
|
+
border-radius: var(--dsw-radius-md);
|
|
916
|
+
width: auto;
|
|
917
|
+
min-width: 100%;
|
|
918
|
+
min-height: 34px;
|
|
919
|
+
color: inherit;
|
|
920
|
+
text-align: left;
|
|
921
|
+
cursor: pointer;
|
|
922
|
+
background: none;
|
|
923
|
+
border: none;
|
|
924
|
+
outline: none;
|
|
925
|
+
align-items: center;
|
|
926
|
+
gap: 6px;
|
|
927
|
+
padding: 5px 7px;
|
|
928
|
+
display: flex;
|
|
929
|
+
}
|
|
930
|
+
.dsh-model-menu-option:not(.dsh-model-menu-modelOption):hover:not(:disabled),
|
|
931
|
+
.dsh-model-menu-option:focus-visible,
|
|
932
|
+
.dsh-model-menu-optionActive:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
933
|
+
.dsh-model-menu-modelOption:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
934
|
+
.dsh-model-menu-selected { background: none; }
|
|
935
|
+
.dsh-model-menu-option:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
|
|
936
|
+
.dsh-model-menu-optionCopy { flex-direction: column; flex: 1; min-width: 0; display: flex; }
|
|
937
|
+
.dsh-model-menu-modelName { color: inherit; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 400; line-height: 18px; overflow: hidden; }
|
|
938
|
+
.dsh-model-menu-check { color: var(--dsw-alias-label-primary); flex: 0 0 14px; place-items: center; display: grid; }
|
|
939
|
+
.dsh-model-menu-check svg { width: 14px; height: 14px; }
|
|
940
|
+
.dsh-model-menu-cell {
|
|
941
|
+
box-sizing: border-box;
|
|
942
|
+
border-radius: var(--dsw-radius-md);
|
|
943
|
+
width: auto;
|
|
944
|
+
min-width: 100%;
|
|
945
|
+
height: 34px;
|
|
946
|
+
color: var(--dsw-alias-label-primary);
|
|
947
|
+
cursor: pointer;
|
|
948
|
+
text-align: left;
|
|
949
|
+
background: none;
|
|
950
|
+
border: none;
|
|
951
|
+
outline: none;
|
|
952
|
+
align-items: center;
|
|
953
|
+
gap: 6px;
|
|
954
|
+
padding: 0 8px;
|
|
955
|
+
font-size: 13px;
|
|
956
|
+
line-height: 20px;
|
|
957
|
+
display: flex;
|
|
958
|
+
}
|
|
959
|
+
.dsh-model-menu-cell:hover, .dsh-model-menu-cell:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
|
|
960
|
+
.dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
|
|
961
|
+
.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; }
|
|
962
|
+
.dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
|
|
963
|
+
`;
|
|
87
964
|
Object.values({
|
|
88
965
|
/** Surfaces, from the page up to the topmost nested card. */
|
|
89
966
|
surface: [
|
|
@@ -446,6 +1323,29 @@ ${scope} {
|
|
|
446
1323
|
& .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
|
|
447
1324
|
& .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
|
|
448
1325
|
|
|
1326
|
+
/* --- Native textarea. The primitives ship no textarea either, so a settings
|
|
1327
|
+
* prompt keeps the platform element and the same field shell as the select:
|
|
1328
|
+
* 0.5px l4, radius-md, layer-3, 13/1.5 type. It grows vertically instead of
|
|
1329
|
+
* locking to the select's 34px row. --- */
|
|
1330
|
+
& .dsh-ui-textarea {
|
|
1331
|
+
box-sizing: border-box;
|
|
1332
|
+
width: 100%;
|
|
1333
|
+
min-width: 0;
|
|
1334
|
+
resize: vertical;
|
|
1335
|
+
padding: 8px 12px;
|
|
1336
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
1337
|
+
border-radius: var(--dsw-radius-md);
|
|
1338
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
1339
|
+
font: inherit;
|
|
1340
|
+
font-size: 13px;
|
|
1341
|
+
line-height: 1.5;
|
|
1342
|
+
color: var(--dsw-alias-label-primary);
|
|
1343
|
+
}
|
|
1344
|
+
& .dsh-ui-textarea:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
|
|
1345
|
+
& .dsh-ui-textarea:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
|
|
1346
|
+
& .dsh-ui-textarea::placeholder { color: var(--dsw-alias-label-dimmed); }
|
|
1347
|
+
& .dsh-ui-textarea[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
|
|
1348
|
+
|
|
449
1349
|
/* Long panels scroll inside themselves instead of growing the host page. */
|
|
450
1350
|
& .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
|
451
1351
|
& .dsh-ui-panel-wide { max-width: 100%; }
|
|
@@ -458,25 +1358,18 @@ ${scope} {
|
|
|
458
1358
|
}
|
|
459
1359
|
//#endregion
|
|
460
1360
|
//#region ../dsh-plugin-kit/src/contracts.ts
|
|
461
|
-
/**
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
method: "watch",
|
|
474
|
-
mode: "stream",
|
|
475
|
-
invocation: { kind: "direct" },
|
|
476
|
-
parameters: [],
|
|
477
|
-
cancellation: { parameter: "signal" }
|
|
478
|
-
}]
|
|
479
|
-
};
|
|
1361
|
+
/** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
|
|
1362
|
+
function modelSettingsChangesIdentity(packageName) {
|
|
1363
|
+
const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
|
|
1364
|
+
const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
1365
|
+
if (!parts.length) throw new Error("Model settings package name has no RPC segment");
|
|
1366
|
+
const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
|
|
1367
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
|
|
1368
|
+
return {
|
|
1369
|
+
service: segment,
|
|
1370
|
+
namespace: segment
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
480
1373
|
//#endregion
|
|
481
1374
|
//#region ../dsh-plugin-kit/src/model-settings-client.ts
|
|
482
1375
|
/** This stream carries invalidations only; ready is never a settings snapshot. */
|
|
@@ -491,17 +1384,44 @@ function parseModelSettingsChangeFrame(value) {
|
|
|
491
1384
|
};
|
|
492
1385
|
throw new Error("Invalid model settings change frame");
|
|
493
1386
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
1387
|
+
/** One owner's contribution. Each package mounts only its own endpoint. */
|
|
1388
|
+
function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
|
|
1389
|
+
return {
|
|
1390
|
+
package: packageName,
|
|
1391
|
+
descriptors: [{
|
|
1392
|
+
id: `${identity.namespace}/watch`,
|
|
1393
|
+
service: identity.service,
|
|
1394
|
+
namespace: identity.namespace,
|
|
1395
|
+
method: "watch",
|
|
1396
|
+
mode: "stream",
|
|
1397
|
+
invocation: { kind: "direct" },
|
|
1398
|
+
parameters: [],
|
|
1399
|
+
cancellation: { parameter: "signal" },
|
|
1400
|
+
result: {
|
|
1401
|
+
mode: "strict",
|
|
1402
|
+
typeSymbol: "ModelSettings.ChangeFrame",
|
|
1403
|
+
create: () => ({ parse: parseModelSettingsChangeFrame })
|
|
1404
|
+
}
|
|
1405
|
+
}]
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
/** Mount from a synchronous apply. Disposal waits for an in-flight mount and
|
|
1409
|
+
* then withdraws it, so the contribution cannot outlive the plugin. */
|
|
1410
|
+
function mountModelSettingsChanges(ctx, packageName) {
|
|
1411
|
+
const contribution = modelSettingsChangesRemote(packageName);
|
|
1412
|
+
ctx.inject(["remote"], (injected) => {
|
|
1413
|
+
const scope = injected;
|
|
1414
|
+
const mounting = scope.remote.$mount(contribution);
|
|
1415
|
+
mounting.catch(() => {});
|
|
1416
|
+
scope.effect(() => async () => {
|
|
1417
|
+
await (await mounting.catch(() => void 0))?.();
|
|
1418
|
+
});
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
502
1421
|
/** Native RemoteStream owns carrier reconnects; no timers or browser transport.
|
|
503
1422
|
* A factory shares one logical stream while at least one subscriber remains. */
|
|
504
|
-
function createModelSettingsSubscriber(ctx,
|
|
1423
|
+
function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
|
|
1424
|
+
const endpoint = identity;
|
|
505
1425
|
const listeners = /* @__PURE__ */ new Set();
|
|
506
1426
|
let injection;
|
|
507
1427
|
let current;
|
|
@@ -527,12 +1447,18 @@ function createModelSettingsSubscriber(ctx, namespace) {
|
|
|
527
1447
|
};
|
|
528
1448
|
const start = () => {
|
|
529
1449
|
const life = ++lifecycle;
|
|
530
|
-
injection = ctx.inject(["remote",
|
|
1450
|
+
injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
|
|
531
1451
|
if (life !== lifecycle || !listeners.size) return;
|
|
532
1452
|
const scope = injected;
|
|
1453
|
+
const service = scope.remote[endpoint.namespace];
|
|
1454
|
+
if (!service || !("watch" in service) || !service.watch) {
|
|
1455
|
+
report("Model settings sync service unavailable");
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const watch = service.watch;
|
|
533
1459
|
const stream = scope.remote.$stream({
|
|
534
1460
|
name: "Model settings sync",
|
|
535
|
-
open: (signal) =>
|
|
1461
|
+
open: (signal) => watch(signal),
|
|
536
1462
|
ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
|
|
537
1463
|
});
|
|
538
1464
|
const session = {
|
|
@@ -567,7 +1493,7 @@ function createModelSettingsSubscriber(ctx, namespace) {
|
|
|
567
1493
|
invalidate();
|
|
568
1494
|
} else {
|
|
569
1495
|
if (!ready) throw new Error("Model settings sync change arrived before ready");
|
|
570
|
-
if (
|
|
1496
|
+
if (accept(frame.ns)) invalidate();
|
|
571
1497
|
}
|
|
572
1498
|
}
|
|
573
1499
|
} catch (error) {
|
|
@@ -623,8 +1549,7 @@ const name = "dsh-git-commit-client";
|
|
|
623
1549
|
const inject = [
|
|
624
1550
|
"slots",
|
|
625
1551
|
"connection",
|
|
626
|
-
|
|
627
|
-
"remote.session",
|
|
1552
|
+
...MODEL_CATALOG_INJECT,
|
|
628
1553
|
"locale"
|
|
629
1554
|
];
|
|
630
1555
|
const NS = "dsh-git-commit.action";
|
|
@@ -1101,7 +2026,7 @@ function GitCommitAction({ sessionId, useSession, call, t }) {
|
|
|
1101
2026
|
}), document.body)]
|
|
1102
2027
|
});
|
|
1103
2028
|
}
|
|
1104
|
-
const clientCss = `${officialUiCss("gcm-root")}
|
|
2029
|
+
const clientCss = `${officialUiCss("gcm-root")}${modelMenuCss}
|
|
1105
2030
|
/* Only the floating panel's geometry is this plugin's own: the material, the
|
|
1106
2031
|
* rounded edge and the scrollbar tokens all come from the shared contract.
|
|
1107
2032
|
*
|
|
@@ -1130,7 +2055,6 @@ const settingsZh = {
|
|
|
1130
2055
|
pageTitle: "Git 提交",
|
|
1131
2056
|
model: "提交规划模型",
|
|
1132
2057
|
modelDefault: "默认模型",
|
|
1133
|
-
modelEffort: "思考强度",
|
|
1134
2058
|
modelHint: "留空则使用当前会话模型,再回落到宿主默认对话模型。",
|
|
1135
2059
|
fallback: "模型不可用时仍然提交",
|
|
1136
2060
|
fallbackHint: "默认关闭:模型规划失败会取消本次提交,工作区不变。开启后全部改动会合并成一个提交,由插件代写提交信息。",
|
|
@@ -1146,7 +2070,6 @@ const settingsEn = {
|
|
|
1146
2070
|
pageTitle: "Git Commit",
|
|
1147
2071
|
model: "Commit planning model",
|
|
1148
2072
|
modelDefault: "Default model",
|
|
1149
|
-
modelEffort: "Reasoning effort",
|
|
1150
2073
|
modelHint: "Leave empty to use the current session model, then the host default chat model.",
|
|
1151
2074
|
fallback: "Commit even without a model plan",
|
|
1152
2075
|
fallbackHint: "Off by default: a failed plan cancels the run and leaves the workspace untouched. When on, every change lands in one commit with a plugin-written message.",
|
|
@@ -1165,8 +2088,12 @@ function settingsCopy(locale) {
|
|
|
1165
2088
|
function GitCommitSettingsPanel(props) {
|
|
1166
2089
|
const hostLocale = useHostLocale(props.client);
|
|
1167
2090
|
const text = settingsCopy(props.locale ?? hostLocale);
|
|
2091
|
+
const language = String(props.locale ?? hostLocale).toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
1168
2092
|
const [settings, setSettings] = (0, react.useState)(void 0);
|
|
1169
|
-
const
|
|
2093
|
+
const catalogState = useModelCatalog((0, react.useCallback)(() => readModelCatalog(props.client), [props.client]), { subscribe: (0, react.useCallback)((change) => props.client.connection.generation?.subscribe(change), [props.client]) });
|
|
2094
|
+
const { refresh: refreshCatalog } = catalogState;
|
|
2095
|
+
const choices = parseModelMenuChoices(catalogState.catalog, settings?.model);
|
|
2096
|
+
const [menuOpen, setMenuOpen] = (0, react.useState)(false);
|
|
1170
2097
|
const [error, setError] = (0, react.useState)("");
|
|
1171
2098
|
const [note, setNote] = (0, react.useState)(false);
|
|
1172
2099
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
@@ -1229,6 +2156,7 @@ function GitCommitSettingsPanel(props) {
|
|
|
1229
2156
|
const stop = props.subscribeSettings?.(() => {
|
|
1230
2157
|
if (live) {
|
|
1231
2158
|
setSyncError("");
|
|
2159
|
+
refreshCatalog();
|
|
1232
2160
|
refreshRef.current();
|
|
1233
2161
|
}
|
|
1234
2162
|
}, (message) => {
|
|
@@ -1238,27 +2166,10 @@ function GitCommitSettingsPanel(props) {
|
|
|
1238
2166
|
live = false;
|
|
1239
2167
|
stop?.();
|
|
1240
2168
|
};
|
|
1241
|
-
}, [props.subscribeSettings, props.client]);
|
|
1242
|
-
const boundProvider = settings?.model.provider ?? "";
|
|
1243
|
-
const boundModel = settings?.model.model ?? "";
|
|
1244
|
-
(0, react.useEffect)(() => {
|
|
1245
|
-
let live = true;
|
|
1246
|
-
const bound = boundProvider && boundModel ? {
|
|
1247
|
-
provider: boundProvider,
|
|
1248
|
-
model: boundModel
|
|
1249
|
-
} : void 0;
|
|
1250
|
-
props.client.remote?.session?.modelCatalog?.()?.then((value) => {
|
|
1251
|
-
if (live) setChoices(parseModelMenuChoices(value, bound));
|
|
1252
|
-
}).catch(() => {
|
|
1253
|
-
if (live) setChoices([]);
|
|
1254
|
-
});
|
|
1255
|
-
return () => {
|
|
1256
|
-
live = false;
|
|
1257
|
-
};
|
|
1258
2169
|
}, [
|
|
2170
|
+
props.subscribeSettings,
|
|
1259
2171
|
props.client,
|
|
1260
|
-
|
|
1261
|
-
boundModel
|
|
2172
|
+
refreshCatalog
|
|
1262
2173
|
]);
|
|
1263
2174
|
const save = (0, react.useCallback)((patch) => {
|
|
1264
2175
|
const state = operation.current;
|
|
@@ -1320,8 +2231,12 @@ function GitCommitSettingsPanel(props) {
|
|
|
1320
2231
|
]
|
|
1321
2232
|
});
|
|
1322
2233
|
const current = settings.model;
|
|
1323
|
-
const
|
|
1324
|
-
const
|
|
2234
|
+
const follow = !current.provider || !current.model;
|
|
2235
|
+
const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
|
|
2236
|
+
const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
|
|
2237
|
+
const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
|
|
2238
|
+
const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
|
|
2239
|
+
const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
|
|
1325
2240
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1326
2241
|
className: "gcm-root gcm-settings",
|
|
1327
2242
|
"data-testid": "git-commit-settings",
|
|
@@ -1353,57 +2268,72 @@ function GitCommitSettingsPanel(props) {
|
|
|
1353
2268
|
})
|
|
1354
2269
|
]
|
|
1355
2270
|
}),
|
|
1356
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
1357
|
-
className: "dsh-ui-field",
|
|
1358
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1359
|
-
className: "dsh-ui-label",
|
|
1360
|
-
children: text.model
|
|
1361
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1362
|
-
className: "dsh-ui-select",
|
|
1363
|
-
value: modelRouteKey(current.provider, current.model),
|
|
1364
|
-
disabled: busy,
|
|
1365
|
-
onChange: (event) => {
|
|
1366
|
-
const key = event.target.value;
|
|
1367
|
-
if (!key) {
|
|
1368
|
-
save({ model: {
|
|
1369
|
-
provider: "",
|
|
1370
|
-
model: ""
|
|
1371
|
-
} });
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
const parsed = parseModelRouteKey(key);
|
|
1375
|
-
if (parsed) save({ model: parsed });
|
|
1376
|
-
},
|
|
1377
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1378
|
-
value: "",
|
|
1379
|
-
children: text.modelDefault
|
|
1380
|
-
}), choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1381
|
-
value: modelRouteKey(choice.provider, choice.model),
|
|
1382
|
-
children: choice.label
|
|
1383
|
-
}, modelRouteKey(choice.provider, choice.model)))]
|
|
1384
|
-
})]
|
|
1385
|
-
}),
|
|
1386
|
-
selected && efforts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2271
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1387
2272
|
className: "dsh-ui-field",
|
|
1388
|
-
children: [
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
2273
|
+
children: [
|
|
2274
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2275
|
+
className: "dsh-ui-label",
|
|
2276
|
+
children: text.model
|
|
2277
|
+
}),
|
|
2278
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
|
|
2279
|
+
state: catalogState,
|
|
2280
|
+
locale: language
|
|
2281
|
+
}),
|
|
2282
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
2283
|
+
locale: language,
|
|
2284
|
+
open: menuOpen && !busy,
|
|
2285
|
+
onOpenChange: (next) => setMenuOpen(next && !busy),
|
|
2286
|
+
listClassName: "gcm-root",
|
|
2287
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2288
|
+
type: "button",
|
|
2289
|
+
className: "dsh-model-menu-trigger",
|
|
2290
|
+
disabled: busy,
|
|
2291
|
+
"aria-label": `${text.model}: ${currentLabel}`,
|
|
2292
|
+
children: [
|
|
2293
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2294
|
+
className: "dsh-model-menu-triggerLabel",
|
|
2295
|
+
children: currentLabel
|
|
2296
|
+
}),
|
|
2297
|
+
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2298
|
+
className: "dsh-model-menu-triggerEffort",
|
|
2299
|
+
children: effortName ?? text.modelDefault
|
|
2300
|
+
}) : null,
|
|
2301
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !busy ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2302
|
+
]
|
|
2303
|
+
}),
|
|
2304
|
+
choices,
|
|
2305
|
+
selected: follow ? void 0 : {
|
|
2306
|
+
provider: current.provider,
|
|
2307
|
+
model: current.model
|
|
2308
|
+
},
|
|
2309
|
+
onPick: (picked) => save({ model: {
|
|
2310
|
+
provider: picked.provider,
|
|
2311
|
+
model: picked.model
|
|
2312
|
+
} }),
|
|
2313
|
+
leading: [{
|
|
2314
|
+
id: "default",
|
|
2315
|
+
label: text.modelDefault,
|
|
2316
|
+
selected: follow
|
|
2317
|
+
}],
|
|
2318
|
+
onPickLeading: () => save({ model: {
|
|
2319
|
+
provider: "",
|
|
2320
|
+
model: ""
|
|
2321
|
+
} }),
|
|
2322
|
+
efforts,
|
|
2323
|
+
selectedEffort: current.reasoningEffort || "",
|
|
2324
|
+
onPickEffort: (effort) => save({ model: follow ? {
|
|
2325
|
+
provider: "",
|
|
2326
|
+
model: ""
|
|
2327
|
+
} : {
|
|
2328
|
+
provider: current.provider,
|
|
2329
|
+
model: current.model,
|
|
2330
|
+
...effort ? { reasoningEffort: effort } : {}
|
|
2331
|
+
} }),
|
|
2332
|
+
modelLabel: text.model,
|
|
2333
|
+
modelValue: currentLabel,
|
|
2334
|
+
defaultEffortLabel: text.modelDefault
|
|
2335
|
+
})
|
|
2336
|
+
]
|
|
1407
2337
|
}),
|
|
1408
2338
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1409
2339
|
className: "dsh-ui-meta",
|
|
@@ -1435,6 +2365,7 @@ function GitCommitSettingsPanel(props) {
|
|
|
1435
2365
|
}
|
|
1436
2366
|
function apply(ctx) {
|
|
1437
2367
|
const client = ctx;
|
|
2368
|
+
mountModelSettingsChanges(ctx, PLUGIN_NAME);
|
|
1438
2369
|
const settingsSubscriber = createModelSettingsSubscriber(ctx, PLUGIN_NAME);
|
|
1439
2370
|
const subscribeSettings = (change, error) => settingsSubscriber.subscribe(change, error);
|
|
1440
2371
|
ctx.provide(GIT_COMMIT_CLIENT_SERVICE, { active: true });
|