@klarkxy/dsh-current-title 0.3.1 → 0.3.3
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 +7 -4
- package/docs/README.zh-CN.md +8 -5
- package/lib/client.inner.cjs +965 -280
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +965 -280
- package/lib/contracts.d.ts +53 -3
- package/lib/contracts.js +2 -55
- package/lib/contracts2.d.ts +2 -0
- package/lib/contracts2.js +105 -0
- package/lib/contracts2.js.map +1 -0
- package/lib/index.d.ts +5 -3
- package/lib/index.js +904 -11
- package/lib/index.js.map +1 -1
- package/package.json +8 -5
- package/lib/contracts.js.map +0 -1
package/lib/client.js
CHANGED
|
@@ -9,6 +9,206 @@ let react = require("react");
|
|
|
9
9
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
10
|
let react_dom = require("react-dom");
|
|
11
11
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
12
|
+
//#region ../dsh-model-route/src/catalog.ts
|
|
13
|
+
/** Both services are required: Cordis checks the nested namespace separately. */
|
|
14
|
+
const MODEL_CATALOG_INJECT = ["remote", "remote.session"];
|
|
15
|
+
function asRecord(value) {
|
|
16
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
17
|
+
}
|
|
18
|
+
function asText(value) {
|
|
19
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
20
|
+
}
|
|
21
|
+
/** A useful diagnostic even when a transport rejects with a plain object. */
|
|
22
|
+
function modelCatalogErrorMessage(value) {
|
|
23
|
+
if (value instanceof Error && value.message) return value.message;
|
|
24
|
+
return asText(asRecord(value)?.message) ?? asText(value) ?? "Unable to load the model catalog.";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate and unwrap a successful catalog without discarding provider failures
|
|
28
|
+
* or newer host metadata. Names and metadata remain optional for older hosts.
|
|
29
|
+
* A broken response is never presented as a successfully loaded empty menu.
|
|
30
|
+
*/
|
|
31
|
+
function checkedModelCatalog(value) {
|
|
32
|
+
const envelope = asRecord(value);
|
|
33
|
+
if (envelope?.ok === false) throw new Error(modelCatalogErrorMessage(envelope.error ?? envelope));
|
|
34
|
+
const root = envelope?.ok === true ? asRecord(envelope.value) : envelope;
|
|
35
|
+
const malformed = (field) => {
|
|
36
|
+
throw new Error(`Invalid model catalog: ${field}.`);
|
|
37
|
+
};
|
|
38
|
+
if (!root || !Array.isArray(root.groups)) return malformed("groups must be an array");
|
|
39
|
+
for (const value of root.groups) {
|
|
40
|
+
const group = asRecord(value);
|
|
41
|
+
if (!group || !asText(group.id) || !Array.isArray(group.models)) return malformed("each group needs an id and models array");
|
|
42
|
+
for (const value of group.models) {
|
|
43
|
+
const model = asRecord(value);
|
|
44
|
+
if (!model || !asText(model.id)) return malformed("each model needs an id");
|
|
45
|
+
if (model.reasoning !== void 0) {
|
|
46
|
+
const reasoning = asRecord(model.reasoning);
|
|
47
|
+
if (!reasoning || !Array.isArray(reasoning.efforts) || reasoning.efforts.some((value) => !parseEffort(value))) return malformed("reasoning efforts must be an array of identified efforts");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (root.failures !== void 0 && (!Array.isArray(root.failures) || root.failures.some((value) => {
|
|
52
|
+
const failure = asRecord(value);
|
|
53
|
+
return !failure || !asText(failure.id) || typeof failure.name !== "string" || typeof failure.message !== "string";
|
|
54
|
+
}))) return malformed("failures must be an array of provider failures");
|
|
55
|
+
return root;
|
|
56
|
+
}
|
|
57
|
+
/** Read through the injected session namespace; absence and rejection are errors. */
|
|
58
|
+
async function readModelCatalog(client) {
|
|
59
|
+
const session = client.remote?.session;
|
|
60
|
+
if (typeof session?.modelCatalog !== "function") throw new Error("Model catalog unavailable: inject both remote and remote.session.");
|
|
61
|
+
return checkedModelCatalog(await session.modelCatalog());
|
|
62
|
+
}
|
|
63
|
+
/** Provider failures are partial results, not a transport failure. */
|
|
64
|
+
function modelCatalogFailures(value) {
|
|
65
|
+
try {
|
|
66
|
+
return checkedModelCatalog(value).failures ?? [];
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function parseEffort(value) {
|
|
72
|
+
const row = asRecord(value);
|
|
73
|
+
const id = row ? asText(row.id) : void 0;
|
|
74
|
+
if (!id) return void 0;
|
|
75
|
+
return {
|
|
76
|
+
id,
|
|
77
|
+
name: row ? asText(row.name) ?? id : id
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Parse the host session catalog into flat choices, keeping a saved route selectable. */
|
|
81
|
+
function parseModelMenuChoices(value, bound) {
|
|
82
|
+
const envelope = asRecord(value);
|
|
83
|
+
const root = (envelope && envelope.ok === true ? asRecord(envelope.value) : void 0) ?? envelope ?? {};
|
|
84
|
+
const groups = Array.isArray(root.groups) ? root.groups : [];
|
|
85
|
+
const choices = [];
|
|
86
|
+
for (const group of groups) {
|
|
87
|
+
const groupRow = asRecord(group);
|
|
88
|
+
const provider = groupRow ? asText(groupRow.id) : void 0;
|
|
89
|
+
if (!provider || !groupRow || !Array.isArray(groupRow.models)) continue;
|
|
90
|
+
const providerName = asText(groupRow.name) ?? provider;
|
|
91
|
+
for (const entry of groupRow.models) {
|
|
92
|
+
const modelRow = asRecord(entry);
|
|
93
|
+
const model = modelRow ? asText(modelRow.id) : void 0;
|
|
94
|
+
if (!model || !modelRow) continue;
|
|
95
|
+
const name = asText(modelRow.name) ?? model;
|
|
96
|
+
const reasoning = asRecord(modelRow.reasoning);
|
|
97
|
+
const efforts = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts.flatMap((effort) => {
|
|
98
|
+
const parsed = parseEffort(effort);
|
|
99
|
+
return parsed ? [parsed] : [];
|
|
100
|
+
}) : [];
|
|
101
|
+
choices.push({
|
|
102
|
+
provider,
|
|
103
|
+
model,
|
|
104
|
+
label: `${providerName} / ${name}`,
|
|
105
|
+
efforts
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
|
|
110
|
+
provider: bound.provider,
|
|
111
|
+
model: bound.model,
|
|
112
|
+
label: `${bound.provider} / ${bound.model}`,
|
|
113
|
+
efforts: []
|
|
114
|
+
});
|
|
115
|
+
return choices;
|
|
116
|
+
}
|
|
117
|
+
/** Reasoning-effort options for one choice; the saved value stays selectable. */
|
|
118
|
+
function modelMenuEffortOptions(choice, current) {
|
|
119
|
+
const listed = (choice?.efforts ?? []).map((item) => ({ ...item }));
|
|
120
|
+
if (current && !listed.some((item) => item.id === current)) listed.push({
|
|
121
|
+
id: current,
|
|
122
|
+
name: current
|
|
123
|
+
});
|
|
124
|
+
return listed;
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region ../dsh-model-route/src/menu.ts
|
|
128
|
+
/** Localize standard effort ids only when the provider has not supplied a display name. */
|
|
129
|
+
function modelMenuEffortLabel(effort, locale) {
|
|
130
|
+
if (effort.name !== effort.id) return effort.name;
|
|
131
|
+
return {
|
|
132
|
+
off: ["关闭", "Off"],
|
|
133
|
+
none: ["无", "None"],
|
|
134
|
+
minimal: ["最低", "Minimal"],
|
|
135
|
+
low: ["低", "Low"],
|
|
136
|
+
medium: ["中", "Medium"],
|
|
137
|
+
high: ["高", "High"],
|
|
138
|
+
xhigh: ["极高", "Extra high"],
|
|
139
|
+
max: ["最高", "Maximum"],
|
|
140
|
+
ultra: ["超高", "Ultra"]
|
|
141
|
+
}[effort.id]?.[locale?.toLowerCase().startsWith("zh") ? 0 : 1] ?? effort.name;
|
|
142
|
+
}
|
|
143
|
+
/** The short model label shown inside a provider group. */
|
|
144
|
+
function modelMenuShortName(choice) {
|
|
145
|
+
const sep = choice.label.indexOf(" / ");
|
|
146
|
+
return sep > 0 ? choice.label.slice(sep + 3) : choice.model;
|
|
147
|
+
}
|
|
148
|
+
/** Group catalog choices by provider and filter them by a case-insensitive query. */
|
|
149
|
+
function groupModelMenuChoices(choices, query) {
|
|
150
|
+
const needle = query.trim().toLowerCase();
|
|
151
|
+
const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
|
|
152
|
+
const groups = [];
|
|
153
|
+
for (const choice of matched) {
|
|
154
|
+
const sep = choice.label.indexOf(" / ");
|
|
155
|
+
const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
|
|
156
|
+
const last = groups.at(-1);
|
|
157
|
+
if (last && last.provider === choice.provider) last.items.push(choice);
|
|
158
|
+
else groups.push({
|
|
159
|
+
provider: choice.provider,
|
|
160
|
+
name,
|
|
161
|
+
items: [choice]
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return groups;
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region ../dsh-plugin-kit/src/contracts.ts
|
|
168
|
+
/** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
|
|
169
|
+
function modelSettingsChangesIdentity(packageName) {
|
|
170
|
+
const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
|
|
171
|
+
const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
172
|
+
if (!parts.length) throw new Error("Model settings package name has no RPC segment");
|
|
173
|
+
const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
|
|
174
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
|
|
175
|
+
return {
|
|
176
|
+
service: segment,
|
|
177
|
+
namespace: segment
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Shared "Session Management" settings section id. The section is provided by
|
|
182
|
+
* shared infrastructure (see `registerSessionSettingsItem` in `client-utils`),
|
|
183
|
+
* not by any one business plugin: every participant registers only a row into
|
|
184
|
+
* {@link SESSION_SETTINGS_ITEM_SLOT}, so any enabled subset yields the same page.
|
|
185
|
+
*/
|
|
186
|
+
const SESSION_SETTINGS_SECTION_ID = "session-manager";
|
|
187
|
+
/**
|
|
188
|
+
* Child list slot declared by the shared Session Management page. Neutral
|
|
189
|
+
* naming on purpose: participants never import each other, they only
|
|
190
|
+
* contribute `{id, order}` rows into this slot while the page is alive.
|
|
191
|
+
*/
|
|
192
|
+
const SESSION_SETTINGS_ITEM_SLOT = "dsh.session-settings.item";
|
|
193
|
+
/**
|
|
194
|
+
* Host-owned nav icon seat: a root-scoped list where each entry's `id` must
|
|
195
|
+
* equal the `settings.section` id it decorates. Owner props are
|
|
196
|
+
* `{size, active}`; icons stay `aria-hidden` so the section label remains the
|
|
197
|
+
* accessible name. Contributors into a shared section may register the same
|
|
198
|
+
* icon `id` with different priorities — the ledger keeps every entry while the
|
|
199
|
+
* host renders one icon per section.
|
|
200
|
+
*/
|
|
201
|
+
const SETTINGS_SECTION_ICON_SLOT = "settings.section.icon";
|
|
202
|
+
/** Authenticated host-RPC channel for get/set of the shared default. */
|
|
203
|
+
const AUXILIARY_MODEL_CHANNEL = "/dsh-auxiliary-model";
|
|
204
|
+
/** Model-settings namespace under which the shared default is published. */
|
|
205
|
+
const AUXILIARY_MODEL_NS = "@klarkxy/dsh-plugin-kit/auxiliary-model";
|
|
206
|
+
/**
|
|
207
|
+
* Typert identity of the shared change stream. Mounted on the root context
|
|
208
|
+
* with ownerNs filtering, so frames only carry {@link AUXILIARY_MODEL_NS}.
|
|
209
|
+
*/
|
|
210
|
+
const AUXILIARY_MODEL_CHANGES_IDENTITY = modelSettingsChangesIdentity("@klarkxy/dsh-plugin-kit");
|
|
211
|
+
//#endregion
|
|
12
212
|
//#region ../dsh-plugin-kit/src/client-utils.ts
|
|
13
213
|
/** Fixed pause for failing background reads; manual work is never replayed. */
|
|
14
214
|
function createPluginReadGate(now = Date.now) {
|
|
@@ -122,6 +322,120 @@ function useFeatureRefresh(client, sessionId, refresh, working = false, enabled
|
|
|
122
322
|
enabled
|
|
123
323
|
]);
|
|
124
324
|
}
|
|
325
|
+
/** One `<svg>` with the shared icon chrome; children are plain elements. */
|
|
326
|
+
function lineIcon(children) {
|
|
327
|
+
return function SettingsSectionIcon(props) {
|
|
328
|
+
return (0, react.createElement)("svg", {
|
|
329
|
+
"aria-hidden": "true",
|
|
330
|
+
width: props.size ?? 16,
|
|
331
|
+
height: props.size ?? 16,
|
|
332
|
+
viewBox: "0 0 16 16",
|
|
333
|
+
fill: "none",
|
|
334
|
+
stroke: "currentColor",
|
|
335
|
+
strokeWidth: 1,
|
|
336
|
+
strokeLinecap: "round",
|
|
337
|
+
strokeLinejoin: "round"
|
|
338
|
+
}, ...children.map(([tag, attrs], index) => (0, react.createElement)(tag, {
|
|
339
|
+
key: index,
|
|
340
|
+
...attrs
|
|
341
|
+
})));
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/** Chat bubble for the shared Session Management section. */
|
|
345
|
+
const IconSessionSettings = lineIcon([["path", { d: "M8.5 13a5.5 5.5 0 0 0 5.5-5.5A5.5 5.5 0 0 0 8.5 2 5.5 5.5 0 0 0 3 7.5a5.48 5.48 0 0 0 .9 3L2.8 12.6a.5.5 0 0 0 .62.62l2.9-.98a5.48 5.48 0 0 0 2.18.76Z" }]]);
|
|
346
|
+
/** Private Cordis rendezvous; every bundled copy of this helper resolves the same root service. */
|
|
347
|
+
const SESSION_SETTINGS_PAGE_SERVICE = "dshSessionSettingsPage";
|
|
348
|
+
/** The page component renders only its declared child list: items paint their own titles and styles. */
|
|
349
|
+
function SessionSettingsPageView(props) {
|
|
350
|
+
return props.renderSlot(SESSION_SETTINGS_ITEM_SLOT, {});
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Acquire the root-owned page service, creating and providing it on first use.
|
|
354
|
+
* One counter covers participants, the section wait and the provider: the last
|
|
355
|
+
* release removes the wait and then the provider synchronously (Cordis deletes
|
|
356
|
+
* the impl before its async disposer awaits), so a later acquire sees a clean
|
|
357
|
+
* get() miss and can re-establish the page.
|
|
358
|
+
*/
|
|
359
|
+
function acquireSessionSettingsPage(root) {
|
|
360
|
+
const existing = root.get(SESSION_SETTINGS_PAGE_SERVICE, false);
|
|
361
|
+
if (existing) return existing;
|
|
362
|
+
const slots = root.slots;
|
|
363
|
+
let users = 0;
|
|
364
|
+
let stopPage;
|
|
365
|
+
let unprovide;
|
|
366
|
+
const page = {
|
|
367
|
+
retain() {
|
|
368
|
+
users += 1;
|
|
369
|
+
if (!stopPage) stopPage = slots.inject("settings.section", () => slots.register({
|
|
370
|
+
name: "settings.section",
|
|
371
|
+
id: SESSION_SETTINGS_SECTION_ID,
|
|
372
|
+
order: 30,
|
|
373
|
+
label: () => {
|
|
374
|
+
const locale = root.get("locale", false);
|
|
375
|
+
return String(locale?.getSnapshot?.().active ?? "").startsWith("zh") ? "会话管理" : "Session Management";
|
|
376
|
+
},
|
|
377
|
+
children: { [SESSION_SETTINGS_ITEM_SLOT]: {
|
|
378
|
+
kind: "list",
|
|
379
|
+
scope: "root"
|
|
380
|
+
} }
|
|
381
|
+
}, SessionSettingsPageView));
|
|
382
|
+
},
|
|
383
|
+
release() {
|
|
384
|
+
if (users <= 0) return;
|
|
385
|
+
users -= 1;
|
|
386
|
+
if (users > 0) return;
|
|
387
|
+
stopPage?.();
|
|
388
|
+
stopPage = void 0;
|
|
389
|
+
unprovide?.();
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
unprovide = root.provide(SESSION_SETTINGS_PAGE_SERVICE, page);
|
|
393
|
+
return page;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Contribute one row to the shared Session Management settings page.
|
|
397
|
+
*
|
|
398
|
+
* `ctx` is the plugin's own context: the row's slot wait unloads with the
|
|
399
|
+
* plugin, while the page itself is retained on the shared root context for as
|
|
400
|
+
* long as any contributor lives. There is no fallback seat — when every
|
|
401
|
+
* contributor is gone the section disappears with the page. Returns a disposer.
|
|
402
|
+
* Shared root-owned infrastructure (the auxiliary-model card) calls this on
|
|
403
|
+
* `ctx.root` so its row, icon and page share are owned by the root lifetime
|
|
404
|
+
* and released by the returned disposer, not by any plugin fiber.
|
|
405
|
+
*/
|
|
406
|
+
function registerSessionSettingsItem(ctx, item, component) {
|
|
407
|
+
const slots = ctx.slots;
|
|
408
|
+
const root = ctx.root;
|
|
409
|
+
const page = acquireSessionSettingsPage(root);
|
|
410
|
+
let stopItem;
|
|
411
|
+
let stopIcon;
|
|
412
|
+
try {
|
|
413
|
+
page.retain();
|
|
414
|
+
stopItem = slots.inject(SESSION_SETTINGS_ITEM_SLOT, () => slots.register({
|
|
415
|
+
name: SESSION_SETTINGS_ITEM_SLOT,
|
|
416
|
+
...item
|
|
417
|
+
}, component));
|
|
418
|
+
stopIcon = slots.inject(SETTINGS_SECTION_ICON_SLOT, () => slots.register({
|
|
419
|
+
name: SETTINGS_SECTION_ICON_SLOT,
|
|
420
|
+
id: SESSION_SETTINGS_SECTION_ID,
|
|
421
|
+
order: item.order,
|
|
422
|
+
priority: item.order
|
|
423
|
+
}, IconSessionSettings));
|
|
424
|
+
} catch (error) {
|
|
425
|
+
stopIcon?.();
|
|
426
|
+
stopItem?.();
|
|
427
|
+
page.release();
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
let disposed = false;
|
|
431
|
+
return () => {
|
|
432
|
+
if (disposed) return;
|
|
433
|
+
disposed = true;
|
|
434
|
+
stopIcon?.();
|
|
435
|
+
stopItem?.();
|
|
436
|
+
page.release();
|
|
437
|
+
};
|
|
438
|
+
}
|
|
125
439
|
Object.values({
|
|
126
440
|
/** Surfaces, from the page up to the topmost nested card. */
|
|
127
441
|
surface: [
|
|
@@ -532,20 +846,6 @@ ${scope} {
|
|
|
532
846
|
`.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?=\s*\{)/gm, (_match, className) => `&${className}, & ${className}`);
|
|
533
847
|
}
|
|
534
848
|
//#endregion
|
|
535
|
-
//#region ../dsh-plugin-kit/src/contracts.ts
|
|
536
|
-
/** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
|
|
537
|
-
function modelSettingsChangesIdentity(packageName) {
|
|
538
|
-
const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
|
|
539
|
-
const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
540
|
-
if (!parts.length) throw new Error("Model settings package name has no RPC segment");
|
|
541
|
-
const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
|
|
542
|
-
if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
|
|
543
|
-
return {
|
|
544
|
-
service: segment,
|
|
545
|
-
namespace: segment
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
//#endregion
|
|
549
849
|
//#region ../dsh-plugin-kit/src/model-settings-client.ts
|
|
550
850
|
/** This stream carries invalidations only; ready is never a settings snapshot. */
|
|
551
851
|
function parseModelSettingsChangeFrame(value) {
|
|
@@ -608,265 +908,110 @@ function createModelSettingsSubscriber(ctx, packageName, identity = modelSetting
|
|
|
608
908
|
listener.error?.(message);
|
|
609
909
|
} catch {}
|
|
610
910
|
};
|
|
611
|
-
const invalidate = () => {
|
|
612
|
-
for (const listener of [...listeners]) {
|
|
613
|
-
if (!listeners.has(listener)) continue;
|
|
614
|
-
try {
|
|
615
|
-
listener.change();
|
|
616
|
-
} catch (error) {
|
|
617
|
-
try {
|
|
618
|
-
listener.error?.(error instanceof Error ? error.message : String(error));
|
|
619
|
-
} catch {}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
};
|
|
623
|
-
const start = () => {
|
|
624
|
-
const life = ++lifecycle;
|
|
625
|
-
injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
|
|
626
|
-
if (life !== lifecycle || !listeners.size) return;
|
|
627
|
-
const scope = injected;
|
|
628
|
-
const service = scope.remote[endpoint.namespace];
|
|
629
|
-
if (!service || !("watch" in service) || !service.watch) {
|
|
630
|
-
report("Model settings sync service unavailable");
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
const watch = service.watch;
|
|
634
|
-
const stream = scope.remote.$stream({
|
|
635
|
-
name: "Model settings sync",
|
|
636
|
-
open: (signal) => watch(signal),
|
|
637
|
-
ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
|
|
638
|
-
});
|
|
639
|
-
const session = {
|
|
640
|
-
stream,
|
|
641
|
-
active: true
|
|
642
|
-
};
|
|
643
|
-
current = session;
|
|
644
|
-
scope.effect(() => () => {
|
|
645
|
-
session.active = false;
|
|
646
|
-
if (current === session) {
|
|
647
|
-
current = void 0;
|
|
648
|
-
if (life === lifecycle && listeners.size) report("Model settings sync service unavailable");
|
|
649
|
-
}
|
|
650
|
-
return stream.dispose();
|
|
651
|
-
});
|
|
652
|
-
(async () => {
|
|
653
|
-
let generation;
|
|
654
|
-
let ready = false;
|
|
655
|
-
try {
|
|
656
|
-
for await (const item of stream) {
|
|
657
|
-
if (!session.active || life !== lifecycle || !listeners.size) break;
|
|
658
|
-
if (item.signal.aborted) continue;
|
|
659
|
-
if (generation !== item.generation) {
|
|
660
|
-
generation = item.generation;
|
|
661
|
-
ready = false;
|
|
662
|
-
}
|
|
663
|
-
const frame = parseModelSettingsChangeFrame(item.value);
|
|
664
|
-
if (frame.kind === "ready") {
|
|
665
|
-
ready = true;
|
|
666
|
-
failureMessage = void 0;
|
|
667
|
-
item.accept();
|
|
668
|
-
invalidate();
|
|
669
|
-
} else {
|
|
670
|
-
if (!ready) throw new Error("Model settings sync change arrived before ready");
|
|
671
|
-
if (accept(frame.ns)) invalidate();
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
} catch (error) {
|
|
675
|
-
if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error));
|
|
676
|
-
} finally {
|
|
677
|
-
if (current === session) current = void 0;
|
|
678
|
-
session.active = false;
|
|
679
|
-
await stream.dispose().catch(() => {});
|
|
680
|
-
}
|
|
681
|
-
})();
|
|
682
|
-
});
|
|
683
|
-
queueMicrotask(() => {
|
|
684
|
-
if (life === lifecycle && listeners.size && !current) report("Model settings sync service unavailable");
|
|
685
|
-
});
|
|
686
|
-
};
|
|
687
|
-
return Object.freeze({ subscribe(onChange, onError) {
|
|
688
|
-
const listener = {
|
|
689
|
-
change: onChange,
|
|
690
|
-
error: onError
|
|
691
|
-
};
|
|
692
|
-
listeners.add(listener);
|
|
693
|
-
if (!injection) start();
|
|
694
|
-
else if (failureMessage) queueMicrotask(() => {
|
|
695
|
-
if (listeners.has(listener) && failureMessage) try {
|
|
696
|
-
listener.error?.(failureMessage);
|
|
697
|
-
} catch {}
|
|
698
|
-
});
|
|
699
|
-
return () => {
|
|
700
|
-
if (!listeners.delete(listener) || listeners.size) return;
|
|
701
|
-
lifecycle++;
|
|
702
|
-
if (current) {
|
|
703
|
-
current.active = false;
|
|
704
|
-
current.stream.dispose().catch(() => {});
|
|
705
|
-
}
|
|
706
|
-
current = void 0;
|
|
707
|
-
failureMessage = void 0;
|
|
708
|
-
const previous = injection;
|
|
709
|
-
injection = void 0;
|
|
710
|
-
try {
|
|
711
|
-
Promise.resolve(previous?.dispose()).catch(() => {});
|
|
712
|
-
} catch {}
|
|
713
|
-
};
|
|
714
|
-
} });
|
|
715
|
-
}
|
|
716
|
-
//#endregion
|
|
717
|
-
//#region ../dsh-model-route/src/catalog.ts
|
|
718
|
-
/** Both services are required: Cordis checks the nested namespace separately. */
|
|
719
|
-
const MODEL_CATALOG_INJECT = ["remote", "remote.session"];
|
|
720
|
-
function asRecord(value) {
|
|
721
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
722
|
-
}
|
|
723
|
-
function asText(value) {
|
|
724
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
725
|
-
}
|
|
726
|
-
/** A useful diagnostic even when a transport rejects with a plain object. */
|
|
727
|
-
function modelCatalogErrorMessage(value) {
|
|
728
|
-
if (value instanceof Error && value.message) return value.message;
|
|
729
|
-
return asText(asRecord(value)?.message) ?? asText(value) ?? "Unable to load the model catalog.";
|
|
730
|
-
}
|
|
731
|
-
/**
|
|
732
|
-
* Validate and unwrap a successful catalog without discarding provider failures
|
|
733
|
-
* or newer host metadata. Names and metadata remain optional for older hosts.
|
|
734
|
-
* A broken response is never presented as a successfully loaded empty menu.
|
|
735
|
-
*/
|
|
736
|
-
function checkedModelCatalog(value) {
|
|
737
|
-
const envelope = asRecord(value);
|
|
738
|
-
if (envelope?.ok === false) throw new Error(modelCatalogErrorMessage(envelope.error ?? envelope));
|
|
739
|
-
const root = envelope?.ok === true ? asRecord(envelope.value) : envelope;
|
|
740
|
-
const malformed = (field) => {
|
|
741
|
-
throw new Error(`Invalid model catalog: ${field}.`);
|
|
742
|
-
};
|
|
743
|
-
if (!root || !Array.isArray(root.groups)) return malformed("groups must be an array");
|
|
744
|
-
for (const value of root.groups) {
|
|
745
|
-
const group = asRecord(value);
|
|
746
|
-
if (!group || !asText(group.id) || !Array.isArray(group.models)) return malformed("each group needs an id and models array");
|
|
747
|
-
for (const value of group.models) {
|
|
748
|
-
const model = asRecord(value);
|
|
749
|
-
if (!model || !asText(model.id)) return malformed("each model needs an id");
|
|
750
|
-
if (model.reasoning !== void 0) {
|
|
751
|
-
const reasoning = asRecord(model.reasoning);
|
|
752
|
-
if (!reasoning || !Array.isArray(reasoning.efforts) || reasoning.efforts.some((value) => !parseEffort(value))) return malformed("reasoning efforts must be an array of identified efforts");
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
if (root.failures !== void 0 && (!Array.isArray(root.failures) || root.failures.some((value) => {
|
|
757
|
-
const failure = asRecord(value);
|
|
758
|
-
return !failure || !asText(failure.id) || typeof failure.name !== "string" || typeof failure.message !== "string";
|
|
759
|
-
}))) return malformed("failures must be an array of provider failures");
|
|
760
|
-
return root;
|
|
761
|
-
}
|
|
762
|
-
/** Read through the injected session namespace; absence and rejection are errors. */
|
|
763
|
-
async function readModelCatalog(client) {
|
|
764
|
-
const session = client.remote?.session;
|
|
765
|
-
if (typeof session?.modelCatalog !== "function") throw new Error("Model catalog unavailable: inject both remote and remote.session.");
|
|
766
|
-
return checkedModelCatalog(await session.modelCatalog());
|
|
767
|
-
}
|
|
768
|
-
/** Provider failures are partial results, not a transport failure. */
|
|
769
|
-
function modelCatalogFailures(value) {
|
|
770
|
-
try {
|
|
771
|
-
return checkedModelCatalog(value).failures ?? [];
|
|
772
|
-
} catch {
|
|
773
|
-
return [];
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
function parseEffort(value) {
|
|
777
|
-
const row = asRecord(value);
|
|
778
|
-
const id = row ? asText(row.id) : void 0;
|
|
779
|
-
if (!id) return void 0;
|
|
780
|
-
return {
|
|
781
|
-
id,
|
|
782
|
-
name: row ? asText(row.name) ?? id : id
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
/** Parse the host session catalog into flat choices, keeping a saved route selectable. */
|
|
786
|
-
function parseModelMenuChoices(value, bound) {
|
|
787
|
-
const envelope = asRecord(value);
|
|
788
|
-
const root = (envelope && envelope.ok === true ? asRecord(envelope.value) : void 0) ?? envelope ?? {};
|
|
789
|
-
const groups = Array.isArray(root.groups) ? root.groups : [];
|
|
790
|
-
const choices = [];
|
|
791
|
-
for (const group of groups) {
|
|
792
|
-
const groupRow = asRecord(group);
|
|
793
|
-
const provider = groupRow ? asText(groupRow.id) : void 0;
|
|
794
|
-
if (!provider || !groupRow || !Array.isArray(groupRow.models)) continue;
|
|
795
|
-
const providerName = asText(groupRow.name) ?? provider;
|
|
796
|
-
for (const entry of groupRow.models) {
|
|
797
|
-
const modelRow = asRecord(entry);
|
|
798
|
-
const model = modelRow ? asText(modelRow.id) : void 0;
|
|
799
|
-
if (!model || !modelRow) continue;
|
|
800
|
-
const name = asText(modelRow.name) ?? model;
|
|
801
|
-
const reasoning = asRecord(modelRow.reasoning);
|
|
802
|
-
const efforts = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts.flatMap((effort) => {
|
|
803
|
-
const parsed = parseEffort(effort);
|
|
804
|
-
return parsed ? [parsed] : [];
|
|
805
|
-
}) : [];
|
|
806
|
-
choices.push({
|
|
807
|
-
provider,
|
|
808
|
-
model,
|
|
809
|
-
label: `${providerName} / ${name}`,
|
|
810
|
-
efforts
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
|
|
815
|
-
provider: bound.provider,
|
|
816
|
-
model: bound.model,
|
|
817
|
-
label: `${bound.provider} / ${bound.model}`,
|
|
818
|
-
efforts: []
|
|
819
|
-
});
|
|
820
|
-
return choices;
|
|
821
|
-
}
|
|
822
|
-
/** Reasoning-effort options for one choice; the saved value stays selectable. */
|
|
823
|
-
function modelMenuEffortOptions(choice, current) {
|
|
824
|
-
const listed = (choice?.efforts ?? []).map((item) => ({ ...item }));
|
|
825
|
-
if (current && !listed.some((item) => item.id === current)) listed.push({
|
|
826
|
-
id: current,
|
|
827
|
-
name: current
|
|
828
|
-
});
|
|
829
|
-
return listed;
|
|
830
|
-
}
|
|
831
|
-
//#endregion
|
|
832
|
-
//#region ../dsh-model-route/src/menu.ts
|
|
833
|
-
/** Localize standard effort ids only when the provider has not supplied a display name. */
|
|
834
|
-
function modelMenuEffortLabel(effort, locale) {
|
|
835
|
-
if (effort.name !== effort.id) return effort.name;
|
|
836
|
-
return {
|
|
837
|
-
off: ["关闭", "Off"],
|
|
838
|
-
none: ["无", "None"],
|
|
839
|
-
minimal: ["最低", "Minimal"],
|
|
840
|
-
low: ["低", "Low"],
|
|
841
|
-
medium: ["中", "Medium"],
|
|
842
|
-
high: ["高", "High"],
|
|
843
|
-
xhigh: ["极高", "Extra high"],
|
|
844
|
-
max: ["最高", "Maximum"],
|
|
845
|
-
ultra: ["超高", "Ultra"]
|
|
846
|
-
}[effort.id]?.[locale?.toLowerCase().startsWith("zh") ? 0 : 1] ?? effort.name;
|
|
847
|
-
}
|
|
848
|
-
/** The short model label shown inside a provider group. */
|
|
849
|
-
function modelMenuShortName(choice) {
|
|
850
|
-
const sep = choice.label.indexOf(" / ");
|
|
851
|
-
return sep > 0 ? choice.label.slice(sep + 3) : choice.model;
|
|
852
|
-
}
|
|
853
|
-
/** Group catalog choices by provider and filter them by a case-insensitive query. */
|
|
854
|
-
function groupModelMenuChoices(choices, query) {
|
|
855
|
-
const needle = query.trim().toLowerCase();
|
|
856
|
-
const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
|
|
857
|
-
const groups = [];
|
|
858
|
-
for (const choice of matched) {
|
|
859
|
-
const sep = choice.label.indexOf(" / ");
|
|
860
|
-
const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
|
|
861
|
-
const last = groups.at(-1);
|
|
862
|
-
if (last && last.provider === choice.provider) last.items.push(choice);
|
|
863
|
-
else groups.push({
|
|
864
|
-
provider: choice.provider,
|
|
865
|
-
name,
|
|
866
|
-
items: [choice]
|
|
911
|
+
const invalidate = () => {
|
|
912
|
+
for (const listener of [...listeners]) {
|
|
913
|
+
if (!listeners.has(listener)) continue;
|
|
914
|
+
try {
|
|
915
|
+
listener.change();
|
|
916
|
+
} catch (error) {
|
|
917
|
+
try {
|
|
918
|
+
listener.error?.(error instanceof Error ? error.message : String(error));
|
|
919
|
+
} catch {}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
const start = () => {
|
|
924
|
+
const life = ++lifecycle;
|
|
925
|
+
injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
|
|
926
|
+
if (life !== lifecycle || !listeners.size) return;
|
|
927
|
+
const scope = injected;
|
|
928
|
+
const service = scope.remote[endpoint.namespace];
|
|
929
|
+
if (!service || !("watch" in service) || !service.watch) {
|
|
930
|
+
report("Model settings sync service unavailable");
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const watch = service.watch;
|
|
934
|
+
const stream = scope.remote.$stream({
|
|
935
|
+
name: "Model settings sync",
|
|
936
|
+
open: (signal) => watch(signal),
|
|
937
|
+
ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
|
|
938
|
+
});
|
|
939
|
+
const session = {
|
|
940
|
+
stream,
|
|
941
|
+
active: true
|
|
942
|
+
};
|
|
943
|
+
current = session;
|
|
944
|
+
scope.effect(() => () => {
|
|
945
|
+
session.active = false;
|
|
946
|
+
if (current === session) {
|
|
947
|
+
current = void 0;
|
|
948
|
+
if (life === lifecycle && listeners.size) report("Model settings sync service unavailable");
|
|
949
|
+
}
|
|
950
|
+
return stream.dispose();
|
|
951
|
+
});
|
|
952
|
+
(async () => {
|
|
953
|
+
let generation;
|
|
954
|
+
let ready = false;
|
|
955
|
+
try {
|
|
956
|
+
for await (const item of stream) {
|
|
957
|
+
if (!session.active || life !== lifecycle || !listeners.size) break;
|
|
958
|
+
if (item.signal.aborted) continue;
|
|
959
|
+
if (generation !== item.generation) {
|
|
960
|
+
generation = item.generation;
|
|
961
|
+
ready = false;
|
|
962
|
+
}
|
|
963
|
+
const frame = parseModelSettingsChangeFrame(item.value);
|
|
964
|
+
if (frame.kind === "ready") {
|
|
965
|
+
ready = true;
|
|
966
|
+
failureMessage = void 0;
|
|
967
|
+
item.accept();
|
|
968
|
+
invalidate();
|
|
969
|
+
} else {
|
|
970
|
+
if (!ready) throw new Error("Model settings sync change arrived before ready");
|
|
971
|
+
if (accept(frame.ns)) invalidate();
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
} catch (error) {
|
|
975
|
+
if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error));
|
|
976
|
+
} finally {
|
|
977
|
+
if (current === session) current = void 0;
|
|
978
|
+
session.active = false;
|
|
979
|
+
await stream.dispose().catch(() => {});
|
|
980
|
+
}
|
|
981
|
+
})();
|
|
867
982
|
});
|
|
868
|
-
|
|
869
|
-
|
|
983
|
+
queueMicrotask(() => {
|
|
984
|
+
if (life === lifecycle && listeners.size && !current) report("Model settings sync service unavailable");
|
|
985
|
+
});
|
|
986
|
+
};
|
|
987
|
+
return Object.freeze({ subscribe(onChange, onError) {
|
|
988
|
+
const listener = {
|
|
989
|
+
change: onChange,
|
|
990
|
+
error: onError
|
|
991
|
+
};
|
|
992
|
+
listeners.add(listener);
|
|
993
|
+
if (!injection) start();
|
|
994
|
+
else if (failureMessage) queueMicrotask(() => {
|
|
995
|
+
if (listeners.has(listener) && failureMessage) try {
|
|
996
|
+
listener.error?.(failureMessage);
|
|
997
|
+
} catch {}
|
|
998
|
+
});
|
|
999
|
+
return () => {
|
|
1000
|
+
if (!listeners.delete(listener) || listeners.size) return;
|
|
1001
|
+
lifecycle++;
|
|
1002
|
+
if (current) {
|
|
1003
|
+
current.active = false;
|
|
1004
|
+
current.stream.dispose().catch(() => {});
|
|
1005
|
+
}
|
|
1006
|
+
current = void 0;
|
|
1007
|
+
failureMessage = void 0;
|
|
1008
|
+
const previous = injection;
|
|
1009
|
+
injection = void 0;
|
|
1010
|
+
try {
|
|
1011
|
+
Promise.resolve(previous?.dispose()).catch(() => {});
|
|
1012
|
+
} catch {}
|
|
1013
|
+
};
|
|
1014
|
+
} });
|
|
870
1015
|
}
|
|
871
1016
|
//#endregion
|
|
872
1017
|
//#region ../dsh-model-route/src/catalog-ui.tsx
|
|
@@ -1695,6 +1840,488 @@ const modelMenuCss = `
|
|
|
1695
1840
|
.dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
|
|
1696
1841
|
`;
|
|
1697
1842
|
//#endregion
|
|
1843
|
+
//#region ../dsh-plugin-kit/src/auxiliary-model-client.tsx
|
|
1844
|
+
const AUXILIARY_MODEL_PACKAGE = "@klarkxy/dsh-plugin-kit";
|
|
1845
|
+
const AUXILIARY_MODEL_SEAT_ID = "auxiliary-model";
|
|
1846
|
+
/** Private Cordis rendezvous; every bundled copy resolves the same root service. */
|
|
1847
|
+
const AUXILIARY_MODEL_CARD_SERVICE = "dshAuxiliaryModelSettingsCard";
|
|
1848
|
+
/** Theme-token-only stylesheet: officialUiCss scope plus the shared ModelMenu recipe. */
|
|
1849
|
+
const auxiliaryModelCss = `${officialUiCss("ams-root")}${modelMenuCss}
|
|
1850
|
+
.ams-root .ams-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
|
|
1851
|
+
`;
|
|
1852
|
+
const auxiliaryCopy = {
|
|
1853
|
+
zh: {
|
|
1854
|
+
title: "默认辅助模型",
|
|
1855
|
+
help: "各辅助功能(如标题生成、摘要)没有单独指定模型时使用的模型;仅影响声明使用它的功能。",
|
|
1856
|
+
model: "辅助模型",
|
|
1857
|
+
modelDefault: "跟随会话或宿主默认",
|
|
1858
|
+
modelHint: "不选择模型时,各功能按自己的规则决定对话模型。",
|
|
1859
|
+
effortDefault: "跟随模型默认",
|
|
1860
|
+
loading: "正在读取设置…",
|
|
1861
|
+
failed: "无法读取模型设置。",
|
|
1862
|
+
stale: "设置已被其他窗口更新,请刷新后重试。",
|
|
1863
|
+
saveFailed: "保存失败,原设置未更改。请重试。",
|
|
1864
|
+
saved: "已保存。",
|
|
1865
|
+
retry: "重试",
|
|
1866
|
+
refresh: "刷新设置",
|
|
1867
|
+
syncUnavailable: "自动刷新不可用,请手动刷新设置。"
|
|
1868
|
+
},
|
|
1869
|
+
en: {
|
|
1870
|
+
title: "Default Auxiliary Model",
|
|
1871
|
+
help: "The model used by auxiliary features (such as title generation or summarizing) that do not name their own model; only features that opted in are affected.",
|
|
1872
|
+
model: "Auxiliary model",
|
|
1873
|
+
modelDefault: "Follow session or host default",
|
|
1874
|
+
modelHint: "With no model selected, each feature follows its own rules for the conversation model.",
|
|
1875
|
+
effortDefault: "Follow model default",
|
|
1876
|
+
loading: "Loading settings…",
|
|
1877
|
+
failed: "Could not load model settings.",
|
|
1878
|
+
stale: "Settings were changed elsewhere; refresh and retry.",
|
|
1879
|
+
saveFailed: "Save failed; the previous setting was not changed. Try again.",
|
|
1880
|
+
saved: "Saved.",
|
|
1881
|
+
retry: "Retry",
|
|
1882
|
+
refresh: "Refresh settings",
|
|
1883
|
+
syncUnavailable: "Automatic refresh unavailable; refresh settings manually."
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
/** A failed RPC keeps its code so the card can translate known rejections. */
|
|
1887
|
+
var AuxiliaryRpcError = class extends Error {
|
|
1888
|
+
code;
|
|
1889
|
+
constructor(code, message) {
|
|
1890
|
+
super(message);
|
|
1891
|
+
this.code = code;
|
|
1892
|
+
this.name = "AuxiliaryRpcError";
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
/** Host UI language, following live switches with the host's English fallback. */
|
|
1896
|
+
function useAuxiliaryLocale(client) {
|
|
1897
|
+
const subscribe = (0, react.useCallback)((listener) => client.locale?.subscribe(listener) ?? (() => {}), [client]);
|
|
1898
|
+
const snapshot = (0, react.useCallback)(() => client.locale?.getSnapshot().active ?? "en", [client]);
|
|
1899
|
+
return String((0, react.useSyncExternalStore)(subscribe, snapshot, snapshot)).startsWith("zh") ? "zh" : "en";
|
|
1900
|
+
}
|
|
1901
|
+
function isAbort(error) {
|
|
1902
|
+
return error instanceof Error && (error.name === "AbortError" || /abort|cancel/i.test(error.message));
|
|
1903
|
+
}
|
|
1904
|
+
function isStaleRejection(error) {
|
|
1905
|
+
if (!(error instanceof AuxiliaryRpcError)) return false;
|
|
1906
|
+
return error.code === "stale" || error.code === "auxiliary-model/conflict" || /changed|revision|reload|stale|conflict/i.test(error.message);
|
|
1907
|
+
}
|
|
1908
|
+
async function callAuxiliaryRpc(client, endpoint, payload = {}) {
|
|
1909
|
+
const result = await client.connection.rpc.call(AUXILIARY_MODEL_CHANNEL, endpoint, payload);
|
|
1910
|
+
if (!result || result.ok !== true) {
|
|
1911
|
+
const error = result && "error" in result ? result.error : void 0;
|
|
1912
|
+
throw new AuxiliaryRpcError(error?.code ?? "unknown", error?.message ?? `RPC ${endpoint} failed`);
|
|
1913
|
+
}
|
|
1914
|
+
return result.value;
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* The shared card. All state comes from the shared host service over the
|
|
1918
|
+
* plugin-kit RPC channel; the card only renders the stored row.
|
|
1919
|
+
*/
|
|
1920
|
+
function AuxiliaryModelCard({ client, subscribe }) {
|
|
1921
|
+
const language = useAuxiliaryLocale(client);
|
|
1922
|
+
const text = auxiliaryCopy[language];
|
|
1923
|
+
const [settings, setSettings] = (0, react.useState)();
|
|
1924
|
+
const catalogState = useModelCatalog((0, react.useCallback)(() => readModelCatalog(client), [client]), { subscribe: (0, react.useCallback)((change) => client.connection.generation?.subscribe(change), [client]) });
|
|
1925
|
+
const { refresh: refreshCatalog } = catalogState;
|
|
1926
|
+
const choices = parseModelMenuChoices(catalogState.catalog, settings?.model ?? void 0);
|
|
1927
|
+
const [menuOpen, setMenuOpen] = (0, react.useState)(false);
|
|
1928
|
+
const [error, setError] = (0, react.useState)("");
|
|
1929
|
+
const [note, setNote] = (0, react.useState)(false);
|
|
1930
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1931
|
+
const [syncError, setSyncError] = (0, react.useState)("");
|
|
1932
|
+
const operation = (0, react.useRef)({
|
|
1933
|
+
epoch: 0,
|
|
1934
|
+
active: false,
|
|
1935
|
+
reading: false,
|
|
1936
|
+
writing: false,
|
|
1937
|
+
pending: false
|
|
1938
|
+
});
|
|
1939
|
+
const refreshRef = (0, react.useRef)(() => {});
|
|
1940
|
+
const refresh = () => {
|
|
1941
|
+
const state = operation.current;
|
|
1942
|
+
if (!state.active) return;
|
|
1943
|
+
if (state.reading || state.writing) {
|
|
1944
|
+
state.pending = true;
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
state.pending = false;
|
|
1948
|
+
state.reading = true;
|
|
1949
|
+
setBusy(true);
|
|
1950
|
+
const epoch = state.epoch;
|
|
1951
|
+
callAuxiliaryRpc(client, "defaults.get").then((next) => {
|
|
1952
|
+
if (state.active && state.epoch === epoch && !state.pending) {
|
|
1953
|
+
setSettings(next);
|
|
1954
|
+
setError("");
|
|
1955
|
+
}
|
|
1956
|
+
}).catch((reason) => {
|
|
1957
|
+
if (state.active && state.epoch === epoch && !state.pending && !isAbort(reason)) setError("failed");
|
|
1958
|
+
}).finally(() => {
|
|
1959
|
+
if (!state.active || state.epoch !== epoch) return;
|
|
1960
|
+
state.reading = false;
|
|
1961
|
+
setBusy(false);
|
|
1962
|
+
if (state.pending) refreshRef.current();
|
|
1963
|
+
});
|
|
1964
|
+
};
|
|
1965
|
+
refreshRef.current = refresh;
|
|
1966
|
+
(0, react.useEffect)(() => {
|
|
1967
|
+
const state = operation.current;
|
|
1968
|
+
state.epoch++;
|
|
1969
|
+
state.active = true;
|
|
1970
|
+
state.reading = false;
|
|
1971
|
+
state.writing = false;
|
|
1972
|
+
state.pending = false;
|
|
1973
|
+
setBusy(false);
|
|
1974
|
+
setSettings(void 0);
|
|
1975
|
+
setError("");
|
|
1976
|
+
setNote(false);
|
|
1977
|
+
refreshRef.current();
|
|
1978
|
+
return () => {
|
|
1979
|
+
state.active = false;
|
|
1980
|
+
state.epoch++;
|
|
1981
|
+
};
|
|
1982
|
+
}, [client]);
|
|
1983
|
+
(0, react.useEffect)(() => {
|
|
1984
|
+
setSyncError("");
|
|
1985
|
+
let live = true;
|
|
1986
|
+
let stop;
|
|
1987
|
+
try {
|
|
1988
|
+
stop = subscribe?.(() => {
|
|
1989
|
+
if (live) {
|
|
1990
|
+
setSyncError("");
|
|
1991
|
+
refreshCatalog();
|
|
1992
|
+
refreshRef.current();
|
|
1993
|
+
}
|
|
1994
|
+
}, (message) => {
|
|
1995
|
+
if (live) setSyncError(message);
|
|
1996
|
+
});
|
|
1997
|
+
} catch {
|
|
1998
|
+
if (live) setSyncError("Model settings sync service unavailable");
|
|
1999
|
+
}
|
|
2000
|
+
return () => {
|
|
2001
|
+
live = false;
|
|
2002
|
+
try {
|
|
2003
|
+
stop?.();
|
|
2004
|
+
} catch {}
|
|
2005
|
+
};
|
|
2006
|
+
}, [subscribe, refreshCatalog]);
|
|
2007
|
+
const save = (0, react.useCallback)((model) => {
|
|
2008
|
+
const state = operation.current;
|
|
2009
|
+
if (!settings || !state.active || state.writing || state.reading) return;
|
|
2010
|
+
state.writing = true;
|
|
2011
|
+
const epoch = state.epoch;
|
|
2012
|
+
setBusy(true);
|
|
2013
|
+
setNote(false);
|
|
2014
|
+
setError("");
|
|
2015
|
+
callAuxiliaryRpc(client, "defaults.set", {
|
|
2016
|
+
model: model ?? {
|
|
2017
|
+
provider: "",
|
|
2018
|
+
model: ""
|
|
2019
|
+
},
|
|
2020
|
+
expectedRevision: settings.revision
|
|
2021
|
+
}).then((next) => {
|
|
2022
|
+
if (state.active && state.epoch === epoch) {
|
|
2023
|
+
setSettings(next);
|
|
2024
|
+
setNote(true);
|
|
2025
|
+
refreshCatalog();
|
|
2026
|
+
}
|
|
2027
|
+
}).catch((reason) => {
|
|
2028
|
+
if (state.active && state.epoch === epoch) setError(isStaleRejection(reason) ? "stale" : "saveFailed");
|
|
2029
|
+
}).finally(() => {
|
|
2030
|
+
if (!state.active || state.epoch !== epoch) return;
|
|
2031
|
+
state.writing = false;
|
|
2032
|
+
setBusy(false);
|
|
2033
|
+
if (state.pending) refreshRef.current();
|
|
2034
|
+
});
|
|
2035
|
+
}, [
|
|
2036
|
+
client,
|
|
2037
|
+
settings,
|
|
2038
|
+
refreshCatalog
|
|
2039
|
+
]);
|
|
2040
|
+
const locked = busy;
|
|
2041
|
+
if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2042
|
+
className: "ams-root dsh-ui-card",
|
|
2043
|
+
"data-testid": "auxiliary-model-settings",
|
|
2044
|
+
children: [
|
|
2045
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2046
|
+
className: "dsh-ui-heading",
|
|
2047
|
+
children: text.title
|
|
2048
|
+
}),
|
|
2049
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2050
|
+
className: "dsh-ui-help",
|
|
2051
|
+
children: text.help
|
|
2052
|
+
}),
|
|
2053
|
+
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2054
|
+
role: "alert",
|
|
2055
|
+
className: "dsh-ui-error",
|
|
2056
|
+
children: text[error]
|
|
2057
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2058
|
+
role: "status",
|
|
2059
|
+
className: "dsh-ui-hint",
|
|
2060
|
+
children: text.loading
|
|
2061
|
+
}),
|
|
2062
|
+
syncError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2063
|
+
role: "status",
|
|
2064
|
+
className: "dsh-ui-hint",
|
|
2065
|
+
children: text.syncUnavailable
|
|
2066
|
+
}),
|
|
2067
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2068
|
+
className: "dsh-ui-actions",
|
|
2069
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2070
|
+
variant: "ghost",
|
|
2071
|
+
size: "sm",
|
|
2072
|
+
disabled: locked,
|
|
2073
|
+
onClick: () => refreshRef.current(),
|
|
2074
|
+
children: text.retry
|
|
2075
|
+
})
|
|
2076
|
+
})
|
|
2077
|
+
]
|
|
2078
|
+
});
|
|
2079
|
+
const current = settings.model ?? {
|
|
2080
|
+
provider: "",
|
|
2081
|
+
model: ""
|
|
2082
|
+
};
|
|
2083
|
+
const follow = !current.provider || !current.model;
|
|
2084
|
+
const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
|
|
2085
|
+
const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
|
|
2086
|
+
const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
|
|
2087
|
+
const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
|
|
2088
|
+
const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
|
|
2089
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2090
|
+
className: "ams-root dsh-ui-card",
|
|
2091
|
+
"data-testid": "auxiliary-model-settings",
|
|
2092
|
+
children: [
|
|
2093
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2094
|
+
className: "dsh-ui-heading",
|
|
2095
|
+
children: text.title
|
|
2096
|
+
}),
|
|
2097
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2098
|
+
className: "dsh-ui-help",
|
|
2099
|
+
children: text.help
|
|
2100
|
+
}),
|
|
2101
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2102
|
+
className: "dsh-ui-field",
|
|
2103
|
+
children: [
|
|
2104
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2105
|
+
className: "dsh-ui-label",
|
|
2106
|
+
children: text.model
|
|
2107
|
+
}),
|
|
2108
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
|
|
2109
|
+
state: catalogState,
|
|
2110
|
+
locale: language
|
|
2111
|
+
}),
|
|
2112
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
2113
|
+
locale: language,
|
|
2114
|
+
open: menuOpen && !locked,
|
|
2115
|
+
onOpenChange: (next) => setMenuOpen(next && !locked),
|
|
2116
|
+
listClassName: "ams-root ams-menu-portal",
|
|
2117
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2118
|
+
type: "button",
|
|
2119
|
+
className: "dsh-model-menu-trigger",
|
|
2120
|
+
disabled: locked,
|
|
2121
|
+
"aria-label": `${text.model}: ${currentLabel}`,
|
|
2122
|
+
children: [
|
|
2123
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2124
|
+
className: "dsh-model-menu-triggerLabel",
|
|
2125
|
+
children: currentLabel
|
|
2126
|
+
}),
|
|
2127
|
+
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2128
|
+
className: "dsh-model-menu-triggerEffort",
|
|
2129
|
+
children: effortName ?? text.effortDefault
|
|
2130
|
+
}) : null,
|
|
2131
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2132
|
+
]
|
|
2133
|
+
}),
|
|
2134
|
+
choices,
|
|
2135
|
+
selected: follow ? void 0 : {
|
|
2136
|
+
provider: current.provider,
|
|
2137
|
+
model: current.model
|
|
2138
|
+
},
|
|
2139
|
+
onPick: (picked) => save({
|
|
2140
|
+
provider: picked.provider,
|
|
2141
|
+
model: picked.model
|
|
2142
|
+
}),
|
|
2143
|
+
leading: [{
|
|
2144
|
+
id: "default",
|
|
2145
|
+
label: text.modelDefault,
|
|
2146
|
+
selected: follow
|
|
2147
|
+
}],
|
|
2148
|
+
onPickLeading: () => save(null),
|
|
2149
|
+
efforts,
|
|
2150
|
+
selectedEffort: current.reasoningEffort || "",
|
|
2151
|
+
onPickEffort: (effort) => save(follow ? null : {
|
|
2152
|
+
provider: current.provider,
|
|
2153
|
+
model: current.model,
|
|
2154
|
+
...effort ? { reasoningEffort: effort } : {}
|
|
2155
|
+
}),
|
|
2156
|
+
modelLabel: text.model,
|
|
2157
|
+
modelValue: currentLabel,
|
|
2158
|
+
defaultEffortLabel: text.effortDefault
|
|
2159
|
+
}),
|
|
2160
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2161
|
+
className: "dsh-ui-hint",
|
|
2162
|
+
children: text.modelHint
|
|
2163
|
+
})
|
|
2164
|
+
]
|
|
2165
|
+
}),
|
|
2166
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2167
|
+
className: "ams-row",
|
|
2168
|
+
children: [
|
|
2169
|
+
syncError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2170
|
+
role: "status",
|
|
2171
|
+
className: "dsh-ui-hint",
|
|
2172
|
+
children: text.syncUnavailable
|
|
2173
|
+
}),
|
|
2174
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2175
|
+
role: "alert",
|
|
2176
|
+
className: "dsh-ui-error",
|
|
2177
|
+
children: text[error]
|
|
2178
|
+
}),
|
|
2179
|
+
note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2180
|
+
role: "status",
|
|
2181
|
+
className: "dsh-ui-notice",
|
|
2182
|
+
children: text.saved
|
|
2183
|
+
}),
|
|
2184
|
+
error || syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2185
|
+
variant: "ghost",
|
|
2186
|
+
size: "sm",
|
|
2187
|
+
disabled: locked,
|
|
2188
|
+
onClick: () => refreshRef.current(),
|
|
2189
|
+
children: text.refresh
|
|
2190
|
+
}) : null
|
|
2191
|
+
]
|
|
2192
|
+
})
|
|
2193
|
+
]
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
const AUXILIARY_MODEL_WATCH_COORDINATOR = "dshAuxiliaryModelWatchCoordinator";
|
|
2197
|
+
function acquireWatchCoordinator(root) {
|
|
2198
|
+
const existing = root.get(AUXILIARY_MODEL_WATCH_COORDINATOR, false);
|
|
2199
|
+
if (existing) return existing;
|
|
2200
|
+
let unprovide;
|
|
2201
|
+
const coordinator = {
|
|
2202
|
+
tail: Promise.resolve(),
|
|
2203
|
+
active: 0,
|
|
2204
|
+
mount() {
|
|
2205
|
+
coordinator.active += 1;
|
|
2206
|
+
const previousTail = coordinator.tail;
|
|
2207
|
+
const lease = {
|
|
2208
|
+
closed: false,
|
|
2209
|
+
done() {
|
|
2210
|
+
if (lease.closed) return;
|
|
2211
|
+
lease.closed = true;
|
|
2212
|
+
coordinator.active -= 1;
|
|
2213
|
+
const owned = lease.fiber;
|
|
2214
|
+
lease.fiber = void 0;
|
|
2215
|
+
coordinator.tail = coordinator.tail.then(() => Promise.resolve(owned?.dispose()).catch(() => {}));
|
|
2216
|
+
coordinator.tail.then(() => {
|
|
2217
|
+
if (coordinator.active > 0) return;
|
|
2218
|
+
try {
|
|
2219
|
+
if (root.get(AUXILIARY_MODEL_WATCH_COORDINATOR, false) === coordinator) Promise.resolve(unprovide?.()).catch(() => {});
|
|
2220
|
+
} catch {}
|
|
2221
|
+
});
|
|
2222
|
+
},
|
|
2223
|
+
fiber: void 0
|
|
2224
|
+
};
|
|
2225
|
+
previousTail.then(() => {
|
|
2226
|
+
if (lease.closed) return;
|
|
2227
|
+
try {
|
|
2228
|
+
lease.fiber = root.inject(["remote"], (injected) => {
|
|
2229
|
+
const scope = injected;
|
|
2230
|
+
const mounting = scope.remote.$mount(modelSettingsChangesRemote(AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY));
|
|
2231
|
+
mounting.catch(() => {});
|
|
2232
|
+
scope.effect(() => async () => {
|
|
2233
|
+
await (await mounting.catch(() => void 0))?.();
|
|
2234
|
+
});
|
|
2235
|
+
});
|
|
2236
|
+
} catch {}
|
|
2237
|
+
});
|
|
2238
|
+
return lease;
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
unprovide = root.provide(AUXILIARY_MODEL_WATCH_COORDINATOR, coordinator);
|
|
2242
|
+
return coordinator;
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Acquire the root-owned card service, creating it on first use. One counter
|
|
2246
|
+
* covers participants, the seat wait and the change-stream mount: the last
|
|
2247
|
+
* release removes the wait, then the watch contribution, then the provider
|
|
2248
|
+
* itself synchronously (Cordis deletes the impl before its async disposer
|
|
2249
|
+
* awaits), so a later acquire sees a clean get() miss and can re-establish.
|
|
2250
|
+
*/
|
|
2251
|
+
function acquireAuxiliaryModelCard(root, ctx) {
|
|
2252
|
+
const existing = root.get(AUXILIARY_MODEL_CARD_SERVICE, false);
|
|
2253
|
+
if (existing) return existing;
|
|
2254
|
+
let users = 0;
|
|
2255
|
+
let styleElement;
|
|
2256
|
+
let stopItem;
|
|
2257
|
+
let unprovide;
|
|
2258
|
+
const watchLease = acquireWatchCoordinator(root).mount();
|
|
2259
|
+
const subscriber = createModelSettingsSubscriber(ctx.root, AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY, (ns) => ns === AUXILIARY_MODEL_NS);
|
|
2260
|
+
const render = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
|
|
2261
|
+
client: root,
|
|
2262
|
+
subscribe: subscriber.subscribe
|
|
2263
|
+
});
|
|
2264
|
+
const service = {
|
|
2265
|
+
retain() {
|
|
2266
|
+
users += 1;
|
|
2267
|
+
if (styleElement === void 0 && typeof document !== "undefined") {
|
|
2268
|
+
styleElement = document.createElement("style");
|
|
2269
|
+
styleElement.setAttribute("data-plugin", "dsh-auxiliary-model-shared");
|
|
2270
|
+
styleElement.textContent = auxiliaryModelCss;
|
|
2271
|
+
document.head.appendChild(styleElement);
|
|
2272
|
+
}
|
|
2273
|
+
if (!stopItem) stopItem = registerSessionSettingsItem(ctx.root, {
|
|
2274
|
+
id: AUXILIARY_MODEL_SEAT_ID,
|
|
2275
|
+
order: 55
|
|
2276
|
+
}, render);
|
|
2277
|
+
},
|
|
2278
|
+
release() {
|
|
2279
|
+
if (users <= 0) return;
|
|
2280
|
+
users -= 1;
|
|
2281
|
+
if (users > 0) return;
|
|
2282
|
+
stopItem?.();
|
|
2283
|
+
stopItem = void 0;
|
|
2284
|
+
styleElement?.remove();
|
|
2285
|
+
styleElement = void 0;
|
|
2286
|
+
watchLease.done();
|
|
2287
|
+
try {
|
|
2288
|
+
Promise.resolve(unprovide?.()).catch(() => {});
|
|
2289
|
+
} catch {}
|
|
2290
|
+
unprovide = void 0;
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
unprovide = root.provide(AUXILIARY_MODEL_CARD_SERVICE, service);
|
|
2294
|
+
return service;
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* Show the shared "default auxiliary model" card while the calling plugin is
|
|
2298
|
+
* enabled.
|
|
2299
|
+
*
|
|
2300
|
+
* `ctx` is the plugin's own client context: the card itself is retained on the
|
|
2301
|
+
* shared root context for as long as any participant lives, so an arbitrary
|
|
2302
|
+
* subset of enabled plugins yields the same single card. The mount's slot wait
|
|
2303
|
+
* and the change-stream contribution are owned by the root service — the first
|
|
2304
|
+
* participant's unload cannot take the card down. Returns a disposer; it may
|
|
2305
|
+
* be invoked by both ctx.effect and an explicit caller and releases at most
|
|
2306
|
+
* once, so a double call cannot unbalance the shared refcount.
|
|
2307
|
+
*/
|
|
2308
|
+
function registerAuxiliaryModelSettings(ctx) {
|
|
2309
|
+
const root = ctx.root;
|
|
2310
|
+
const service = acquireAuxiliaryModelCard(root, ctx);
|
|
2311
|
+
try {
|
|
2312
|
+
service.retain();
|
|
2313
|
+
} catch (error) {
|
|
2314
|
+
service.release();
|
|
2315
|
+
throw error;
|
|
2316
|
+
}
|
|
2317
|
+
let disposed = false;
|
|
2318
|
+
return () => {
|
|
2319
|
+
if (disposed) return;
|
|
2320
|
+
disposed = true;
|
|
2321
|
+
service.release();
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
//#endregion
|
|
1698
2325
|
//#region src/contracts.ts
|
|
1699
2326
|
const RPC_CHANNEL = "/dsh-current-title";
|
|
1700
2327
|
const TITLE_CLIENT_SERVICE = "dshCurrentTitleClient";
|
|
@@ -1703,6 +2330,7 @@ const TITLE_CLIENT_SERVICE = "dshCurrentTitleClient";
|
|
|
1703
2330
|
const titleCopy = {
|
|
1704
2331
|
zh: {
|
|
1705
2332
|
label: "自动标题",
|
|
2333
|
+
groupHint: "标题模型、提示词与更新节奏",
|
|
1706
2334
|
session: "当前会话",
|
|
1707
2335
|
noSession: "没有打开的会话。打开一个会话后,可以在这里查看并重新生成标题。",
|
|
1708
2336
|
pinned: "已手动命名",
|
|
@@ -1714,7 +2342,8 @@ const titleCopy = {
|
|
|
1714
2342
|
promptHint: "留空用内置说明,仍返回类型和摘要;离开输入框即保存。",
|
|
1715
2343
|
loading: "正在读取标题设置…",
|
|
1716
2344
|
model: "标题模型",
|
|
1717
|
-
modelDefault: "
|
|
2345
|
+
modelDefault: "默认辅助模型",
|
|
2346
|
+
modelHint: "使用全局默认辅助模型;未设置时跟随当前会话模型,再回落到宿主默认对话模型。",
|
|
1718
2347
|
cadence: "更新节奏",
|
|
1719
2348
|
cadenceAll: "跟随当前任务(每条消息)",
|
|
1720
2349
|
cadenceFirst: "仅首条消息",
|
|
@@ -1729,6 +2358,7 @@ const titleCopy = {
|
|
|
1729
2358
|
},
|
|
1730
2359
|
en: {
|
|
1731
2360
|
label: "Auto Title",
|
|
2361
|
+
groupHint: "Title model, prompt and update cadence",
|
|
1732
2362
|
session: "Current session",
|
|
1733
2363
|
noSession: "No session is open. Open one to see its title and regenerate it here.",
|
|
1734
2364
|
pinned: "Renamed manually",
|
|
@@ -1740,7 +2370,8 @@ const titleCopy = {
|
|
|
1740
2370
|
promptHint: "Empty uses the built-in instruction; a type and summary are still returned. Saves on blur.",
|
|
1741
2371
|
loading: "Loading title settings…",
|
|
1742
2372
|
model: "Title model",
|
|
1743
|
-
modelDefault: "
|
|
2373
|
+
modelDefault: "Default auxiliary model",
|
|
2374
|
+
modelHint: "Uses the global default auxiliary model; when none is set, the current conversation model, then the host default chat model.",
|
|
1744
2375
|
cadence: "Update cadence",
|
|
1745
2376
|
cadenceAll: "Track the current task (every message)",
|
|
1746
2377
|
cadenceFirst: "First message only",
|
|
@@ -2117,7 +2748,7 @@ function TitleSettings(props) {
|
|
|
2117
2748
|
});
|
|
2118
2749
|
}
|
|
2119
2750
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2120
|
-
className: "current-title-settings ct-root
|
|
2751
|
+
className: "current-title-settings ct-root",
|
|
2121
2752
|
lang: seat.locale,
|
|
2122
2753
|
"data-testid": "current-title-settings",
|
|
2123
2754
|
children: [
|
|
@@ -2354,6 +2985,10 @@ function TitleSettings(props) {
|
|
|
2354
2985
|
modelLabel: text.model,
|
|
2355
2986
|
modelValue: currentLabel,
|
|
2356
2987
|
defaultEffortLabel: text.modelDefault
|
|
2988
|
+
}),
|
|
2989
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2990
|
+
className: "dsh-ui-meta",
|
|
2991
|
+
children: text.modelHint
|
|
2357
2992
|
})
|
|
2358
2993
|
]
|
|
2359
2994
|
}),
|
|
@@ -2383,8 +3018,56 @@ function TitleSettings(props) {
|
|
|
2383
3018
|
]
|
|
2384
3019
|
});
|
|
2385
3020
|
}
|
|
3021
|
+
/**
|
|
3022
|
+
* The settings row: a named native details group. The content mounts
|
|
3023
|
+
* on the first expand only — the shared page must not fire every plugin's
|
|
3024
|
+
* model/status requests — and stays mounted afterwards so a collapsed group
|
|
3025
|
+
* never drops an unsaved prompt draft or an in-flight write.
|
|
3026
|
+
*/
|
|
3027
|
+
function TitleSettingsItem(props) {
|
|
3028
|
+
const seat = useNativeSeat(props.client);
|
|
3029
|
+
const text = titleCopy[seat.locale];
|
|
3030
|
+
const [visited, setVisited] = (0, react.useState)(false);
|
|
3031
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
3032
|
+
className: "ct-root ct-item",
|
|
3033
|
+
lang: seat.locale,
|
|
3034
|
+
"data-testid": "current-title-settings-item",
|
|
3035
|
+
onToggle: (event) => {
|
|
3036
|
+
if (event.currentTarget.open) setVisited(true);
|
|
3037
|
+
},
|
|
3038
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
|
|
3039
|
+
className: "ct-item-summary",
|
|
3040
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3041
|
+
className: "ct-item-title",
|
|
3042
|
+
children: text.label
|
|
3043
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3044
|
+
className: "ct-item-desc",
|
|
3045
|
+
children: text.groupHint
|
|
3046
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {
|
|
3047
|
+
className: "ct-item-chevron",
|
|
3048
|
+
"aria-hidden": "true"
|
|
3049
|
+
})]
|
|
3050
|
+
}), visited ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3051
|
+
className: "ct-item-body",
|
|
3052
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettings, {
|
|
3053
|
+
client: props.client,
|
|
3054
|
+
marker: props.marker,
|
|
3055
|
+
subscribeSettings: props.subscribeSettings
|
|
3056
|
+
})
|
|
3057
|
+
}) : null]
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
2386
3060
|
const currentTitleOwnCss = `
|
|
2387
3061
|
.ct-root .ct-prompt { min-height: 96px; }
|
|
3062
|
+
.ct-root.ct-item { padding: 0; border-bottom: 0.5px solid var(--dsw-alias-border-l2); }
|
|
3063
|
+
.ct-root .ct-item-summary { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 16px 0; list-style: none; cursor: pointer; }
|
|
3064
|
+
.ct-root .ct-item-summary::-webkit-details-marker { display: none; }
|
|
3065
|
+
.ct-root .ct-item-summary > span:first-child { min-width: 0; }
|
|
3066
|
+
.ct-root .ct-item-title { display: block; font-size: 14px; line-height: 20px; color: var(--dsw-alias-label-primary); }
|
|
3067
|
+
.ct-root .ct-item-desc { display: block; margin-top: 4px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
|
|
3068
|
+
.ct-root .ct-item-chevron { width: 14px; height: 14px; color: var(--dsw-alias-label-secondary); transition: transform 0.15s ease; }
|
|
3069
|
+
.ct-root.ct-item[open] > .ct-item-summary .ct-item-chevron { transform: rotate(180deg); }
|
|
3070
|
+
.ct-root .ct-item-body { padding: 0 0 16px; }
|
|
2388
3071
|
`;
|
|
2389
3072
|
const currentTitleStyles = `${officialUiCss("ct-root")}${modelMenuCss}${currentTitleOwnCss}`;
|
|
2390
3073
|
const styles = currentTitleStyles;
|
|
@@ -2404,14 +3087,15 @@ function apply(ctx) {
|
|
|
2404
3087
|
document.head.appendChild(style);
|
|
2405
3088
|
return () => style.remove();
|
|
2406
3089
|
}, "current-title.styles");
|
|
2407
|
-
ctx.effect(() =>
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
}, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
3090
|
+
ctx.effect(() => registerSessionSettingsItem(ctx, {
|
|
3091
|
+
id: "current-title",
|
|
3092
|
+
order: 70
|
|
3093
|
+
}, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettingsItem, {
|
|
2411
3094
|
client,
|
|
2412
3095
|
marker,
|
|
2413
3096
|
subscribeSettings
|
|
2414
|
-
}))
|
|
3097
|
+
})), "current-title.settings");
|
|
3098
|
+
ctx.effect(() => registerAuxiliaryModelSettings(ctx), "current-title.auxiliary-model");
|
|
2415
3099
|
ctx.effect(() => {
|
|
2416
3100
|
let live = true;
|
|
2417
3101
|
client.connection.rpc.call(RPC_CHANNEL, "status", {}).then((result) => {
|
|
@@ -2429,6 +3113,7 @@ function apply(ctx) {
|
|
|
2429
3113
|
}
|
|
2430
3114
|
//#endregion
|
|
2431
3115
|
exports.TitleSettings = TitleSettings;
|
|
3116
|
+
exports.TitleSettingsItem = TitleSettingsItem;
|
|
2432
3117
|
exports.apply = apply;
|
|
2433
3118
|
exports.applyMarkerFromStatus = applyMarkerFromStatus;
|
|
2434
3119
|
exports.createTitleClientMarker = createTitleClientMarker;
|