dsh-usage-state 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/LICENSE +21 -0
- package/README.en.md +126 -0
- package/README.md +158 -0
- package/cordis.patch.yml +7 -0
- package/docs/adapters.md +100 -0
- package/lib/client.js +1828 -0
- package/lib/index.js +1764 -0
- package/lib/typert.js +120 -0
- package/package.json +92 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1828 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-usage-state",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
//#region src/shared/config.ts
|
|
11
|
+
const DEFAULT_CONFIG = {
|
|
12
|
+
models: [],
|
|
13
|
+
order: [],
|
|
14
|
+
providers: {},
|
|
15
|
+
sources: {},
|
|
16
|
+
refresh: {
|
|
17
|
+
intervalMinutes: 5,
|
|
18
|
+
turnEndDelayMs: 2e3,
|
|
19
|
+
minIntervalSeconds: 60
|
|
20
|
+
},
|
|
21
|
+
display: {
|
|
22
|
+
thresholdWarnPercent: 80,
|
|
23
|
+
thresholdCriticalPercent: 95,
|
|
24
|
+
progressBar: true
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const MODES = [
|
|
28
|
+
"api",
|
|
29
|
+
"coding-plan",
|
|
30
|
+
"hidden"
|
|
31
|
+
];
|
|
32
|
+
const PROVIDER_MODES = [
|
|
33
|
+
"api",
|
|
34
|
+
"coding-plan",
|
|
35
|
+
"hidden",
|
|
36
|
+
"auto"
|
|
37
|
+
];
|
|
38
|
+
function asRecord(value) {
|
|
39
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
40
|
+
}
|
|
41
|
+
function cleanString(value) {
|
|
42
|
+
return typeof value === "string" ? value.trim() : "";
|
|
43
|
+
}
|
|
44
|
+
function clampInt(value, minimum, maximum, fallback) {
|
|
45
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
46
|
+
return Math.min(maximum, Math.max(minimum, Math.round(value)));
|
|
47
|
+
}
|
|
48
|
+
function normalizeModels(value) {
|
|
49
|
+
if (!Array.isArray(value)) return [];
|
|
50
|
+
const models = [];
|
|
51
|
+
const seen = /* @__PURE__ */ new Set();
|
|
52
|
+
for (const raw of value) {
|
|
53
|
+
const entry = asRecord(raw);
|
|
54
|
+
if (entry === void 0) continue;
|
|
55
|
+
const provider = cleanString(entry.provider);
|
|
56
|
+
const model = cleanString(entry.model);
|
|
57
|
+
if (provider === "" || model === "") continue;
|
|
58
|
+
const identity = `${provider}\u0000${model}`;
|
|
59
|
+
if (seen.has(identity)) continue;
|
|
60
|
+
seen.add(identity);
|
|
61
|
+
const rawMode = cleanString(entry.mode);
|
|
62
|
+
const mode = MODES.includes(rawMode) ? rawMode : "hidden";
|
|
63
|
+
const sourceId = cleanString(entry.sourceId);
|
|
64
|
+
models.push({
|
|
65
|
+
provider,
|
|
66
|
+
model,
|
|
67
|
+
sourceId: sourceId === "" ? null : sourceId,
|
|
68
|
+
mode
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return models;
|
|
72
|
+
}
|
|
73
|
+
function normalizeSources(value) {
|
|
74
|
+
const root = asRecord(value);
|
|
75
|
+
if (root === void 0) return {};
|
|
76
|
+
const sources = {};
|
|
77
|
+
for (const [id, raw] of Object.entries(root)) {
|
|
78
|
+
const entry = asRecord(raw);
|
|
79
|
+
if (entry === void 0) continue;
|
|
80
|
+
const config = {};
|
|
81
|
+
const apiKeyRef = cleanString(entry.apiKeyRef);
|
|
82
|
+
if (apiKeyRef !== "") config.apiKeyRef = apiKeyRef;
|
|
83
|
+
const baseUrl = cleanString(entry.baseUrl);
|
|
84
|
+
if (baseUrl !== "") config.baseUrl = baseUrl;
|
|
85
|
+
if (config.apiKeyRef !== void 0 || config.baseUrl !== void 0) sources[id] = config;
|
|
86
|
+
}
|
|
87
|
+
return sources;
|
|
88
|
+
}
|
|
89
|
+
function normalizeRefresh(value) {
|
|
90
|
+
const root = asRecord(value) ?? {};
|
|
91
|
+
const defaults = DEFAULT_CONFIG.refresh;
|
|
92
|
+
return {
|
|
93
|
+
intervalMinutes: clampInt(root.intervalMinutes, 1, 1440, defaults.intervalMinutes),
|
|
94
|
+
turnEndDelayMs: clampInt(root.turnEndDelayMs, 0, 6e4, defaults.turnEndDelayMs),
|
|
95
|
+
minIntervalSeconds: clampInt(root.minIntervalSeconds, 0, 3600, defaults.minIntervalSeconds)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function normalizeDisplay(value) {
|
|
99
|
+
const root = asRecord(value) ?? {};
|
|
100
|
+
const defaults = DEFAULT_CONFIG.display;
|
|
101
|
+
const progressBar = typeof root.progressBar === "boolean" ? root.progressBar : defaults.progressBar;
|
|
102
|
+
const thresholdWarnPercent = clampInt(root.thresholdWarnPercent, 1, 100, defaults.thresholdWarnPercent);
|
|
103
|
+
const thresholdCriticalPercent = clampInt(root.thresholdCriticalPercent, 1, 100, defaults.thresholdCriticalPercent);
|
|
104
|
+
if (thresholdWarnPercent >= thresholdCriticalPercent) return {
|
|
105
|
+
...defaults,
|
|
106
|
+
progressBar
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
thresholdWarnPercent,
|
|
110
|
+
thresholdCriticalPercent,
|
|
111
|
+
progressBar
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function normalizeProviders(value) {
|
|
115
|
+
const root = asRecord(value);
|
|
116
|
+
if (root === void 0) return {};
|
|
117
|
+
const providers = {};
|
|
118
|
+
for (const [id, raw] of Object.entries(root)) {
|
|
119
|
+
const entry = asRecord(raw);
|
|
120
|
+
if (entry === void 0) continue;
|
|
121
|
+
const rawMode = cleanString(entry.mode);
|
|
122
|
+
const normalized = { mode: PROVIDER_MODES.includes(rawMode) ? rawMode : "auto" };
|
|
123
|
+
const sourceId = cleanString(entry.sourceId);
|
|
124
|
+
if (sourceId !== "") normalized.sourceId = sourceId;
|
|
125
|
+
const baseUrl = cleanString(entry.baseUrl);
|
|
126
|
+
if (baseUrl !== "") normalized.baseUrl = baseUrl;
|
|
127
|
+
const apiKeyRef = cleanString(entry.apiKeyRef);
|
|
128
|
+
if (apiKeyRef !== "") normalized.apiKeyRef = apiKeyRef;
|
|
129
|
+
providers[id] = normalized;
|
|
130
|
+
}
|
|
131
|
+
return providers;
|
|
132
|
+
}
|
|
133
|
+
/** Keep the stored order sane and append providers it does not mention. */
|
|
134
|
+
function normalizeOrder(value, providers) {
|
|
135
|
+
const order = [];
|
|
136
|
+
if (Array.isArray(value)) for (const raw of value) {
|
|
137
|
+
const id = cleanString(raw);
|
|
138
|
+
if (id === "" || order.includes(id)) continue;
|
|
139
|
+
order.push(id);
|
|
140
|
+
}
|
|
141
|
+
for (const id of Object.keys(providers)) if (!order.includes(id)) order.push(id);
|
|
142
|
+
return order;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Turn whatever the hand-editable settings document contains into a usable
|
|
146
|
+
* config. Deliberately never throws: the settings provider calls the schema
|
|
147
|
+
* synchronously at registration time, and a dirty section must not block the
|
|
148
|
+
* plugin from loading. Malformed pieces fall back to defaults instead.
|
|
149
|
+
*
|
|
150
|
+
* Legacy per-model entries are migrated into provider entries so an existing
|
|
151
|
+
* document keeps the choices its owner already made.
|
|
152
|
+
*/
|
|
153
|
+
function normalizeConfig(raw) {
|
|
154
|
+
const root = asRecord(raw) ?? {};
|
|
155
|
+
const models = normalizeModels(root.models);
|
|
156
|
+
const providers = normalizeProviders(root.providers);
|
|
157
|
+
for (const entry of models) {
|
|
158
|
+
if (providers[entry.provider] !== void 0) continue;
|
|
159
|
+
providers[entry.provider] = {
|
|
160
|
+
mode: entry.mode,
|
|
161
|
+
...entry.sourceId === null ? {} : { sourceId: entry.sourceId }
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
models,
|
|
166
|
+
order: normalizeOrder(root.order, providers),
|
|
167
|
+
providers,
|
|
168
|
+
sources: normalizeSources(root.sources),
|
|
169
|
+
refresh: normalizeRefresh(root.refresh),
|
|
170
|
+
display: normalizeDisplay(root.display)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const PROVIDER_HINTS = [
|
|
174
|
+
{
|
|
175
|
+
sourceId: "zai",
|
|
176
|
+
pattern: /(zai|zhipu|bigmodel|glm)/
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
sourceId: "kimi",
|
|
180
|
+
pattern: /(kimi|moonshot)/
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
sourceId: "sub2api",
|
|
184
|
+
pattern: /sub-?2-?api/
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
sourceId: "opencode",
|
|
188
|
+
pattern: /opencode/
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
sourceId: "deepseek",
|
|
192
|
+
pattern: /deepseek/
|
|
193
|
+
}
|
|
194
|
+
];
|
|
195
|
+
const HOST_HINTS = [
|
|
196
|
+
{
|
|
197
|
+
sourceId: "deepseek",
|
|
198
|
+
pattern: /(^|\.)api\.deepseek\.com$/
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
sourceId: "zai",
|
|
202
|
+
pattern: /(^|\.)(api\.z\.ai|open\.bigmodel\.cn|bigmodel\.cn)$/
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
sourceId: "kimi",
|
|
206
|
+
pattern: /(^|\.)(api\.kimi\.com|api\.moonshot\.cn|moonshot\.cn)$/
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
sourceId: "opencode",
|
|
210
|
+
pattern: /(^|\.)opencode\.ai$/
|
|
211
|
+
}
|
|
212
|
+
];
|
|
213
|
+
/**
|
|
214
|
+
* Best guess at which data source backs a DSH provider. Provider id first (cheap
|
|
215
|
+
* and usually right), then the endpoint host. Because any unrecognised endpoint is
|
|
216
|
+
* most likely a self-hosted gateway, that is the last resort rather than `undefined`.
|
|
217
|
+
*/
|
|
218
|
+
function suggestSourceId(providerId, baseUrl) {
|
|
219
|
+
const normalized = providerId.trim().toLowerCase();
|
|
220
|
+
for (const hint of PROVIDER_HINTS) if (hint.pattern.test(normalized)) return hint.sourceId;
|
|
221
|
+
if (baseUrl === void 0) return void 0;
|
|
222
|
+
let host;
|
|
223
|
+
try {
|
|
224
|
+
host = new URL(baseUrl.trim()).host.toLowerCase();
|
|
225
|
+
} catch {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
for (const hint of HOST_HINTS) if (hint.pattern.test(host)) return hint.sourceId;
|
|
229
|
+
return "sub2api";
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/shared/providers.ts
|
|
233
|
+
/**
|
|
234
|
+
* The scheme+host of a declared endpoint. A DSH provider profile's `baseURL` is an
|
|
235
|
+
* *API* base (`https://api.z.ai/api/paas/v4`), while every adapter here appends its
|
|
236
|
+
* own path to a bare origin, so only the origin is usable as our endpoint.
|
|
237
|
+
*/
|
|
238
|
+
function originOf(url) {
|
|
239
|
+
if (url === void 0) return void 0;
|
|
240
|
+
try {
|
|
241
|
+
const parsed = new URL(url.trim());
|
|
242
|
+
return parsed.origin === "null" ? void 0 : parsed.origin;
|
|
243
|
+
} catch {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function withTarget(resolution) {
|
|
248
|
+
if (resolution.sourceId === null || resolution.mode === null) return resolution;
|
|
249
|
+
return {
|
|
250
|
+
...resolution,
|
|
251
|
+
key: `${resolution.sourceId}:${resolution.mode}`
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Resolve one provider. `auto` picks the source the provider id (or its endpoint)
|
|
256
|
+
* suggests and that source's primary mode, so a provider DSH already has a key
|
|
257
|
+
* for starts working without touching the settings page.
|
|
258
|
+
*/
|
|
259
|
+
function resolveProvider(input) {
|
|
260
|
+
const entry = input.config.providers[input.provider];
|
|
261
|
+
const declared = originOf(input.endpointHint);
|
|
262
|
+
const baseUrl = entry?.baseUrl ?? declared;
|
|
263
|
+
const base = {
|
|
264
|
+
provider: input.provider,
|
|
265
|
+
...baseUrl === void 0 ? {} : { baseUrl },
|
|
266
|
+
...entry?.baseUrl === void 0 ? {} : { baseUrlPinned: true },
|
|
267
|
+
...entry?.apiKeyRef === void 0 ? {} : { apiKeyRef: entry.apiKeyRef }
|
|
268
|
+
};
|
|
269
|
+
if (entry?.mode === "hidden") return {
|
|
270
|
+
...base,
|
|
271
|
+
sourceId: null,
|
|
272
|
+
mode: null,
|
|
273
|
+
reason: "hidden"
|
|
274
|
+
};
|
|
275
|
+
const sourceId = entry?.sourceId ?? suggestSourceId(input.provider, input.endpointHint) ?? null;
|
|
276
|
+
if (sourceId === null) return {
|
|
277
|
+
...base,
|
|
278
|
+
sourceId: null,
|
|
279
|
+
mode: null,
|
|
280
|
+
reason: "unknown-source"
|
|
281
|
+
};
|
|
282
|
+
const source = input.catalog.find((candidate) => candidate.id === sourceId);
|
|
283
|
+
if (source === void 0) return {
|
|
284
|
+
...base,
|
|
285
|
+
sourceId,
|
|
286
|
+
mode: null,
|
|
287
|
+
reason: "unknown-source"
|
|
288
|
+
};
|
|
289
|
+
if (entry?.mode === "api" || entry?.mode === "coding-plan") {
|
|
290
|
+
if (!source.modes.includes(entry.mode)) return {
|
|
291
|
+
...base,
|
|
292
|
+
sourceId,
|
|
293
|
+
mode: null,
|
|
294
|
+
reason: "unsupported"
|
|
295
|
+
};
|
|
296
|
+
return withTarget({
|
|
297
|
+
...base,
|
|
298
|
+
sourceId,
|
|
299
|
+
mode: entry.mode,
|
|
300
|
+
reason: "configured"
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (source.requiresBaseUrl && baseUrl === void 0) return {
|
|
304
|
+
...base,
|
|
305
|
+
sourceId,
|
|
306
|
+
mode: null,
|
|
307
|
+
reason: "needs-endpoint"
|
|
308
|
+
};
|
|
309
|
+
const mode = source.modes[0];
|
|
310
|
+
if (mode === void 0) return {
|
|
311
|
+
...base,
|
|
312
|
+
sourceId,
|
|
313
|
+
mode: null,
|
|
314
|
+
reason: "unsupported"
|
|
315
|
+
};
|
|
316
|
+
return withTarget({
|
|
317
|
+
...base,
|
|
318
|
+
sourceId,
|
|
319
|
+
mode,
|
|
320
|
+
reason: "auto"
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Display order: the configured order first, then any provider the caller knows
|
|
325
|
+
* about that the order does not mention yet (so a newly configured provider never
|
|
326
|
+
* disappears from the page).
|
|
327
|
+
*/
|
|
328
|
+
function orderProviders(providers, config) {
|
|
329
|
+
const ordered = config.order.filter((provider) => providers.includes(provider));
|
|
330
|
+
const missing = providers.filter((provider) => !ordered.includes(provider));
|
|
331
|
+
return [...ordered, ...missing];
|
|
332
|
+
}
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/client/provider-rows.ts
|
|
335
|
+
/**
|
|
336
|
+
* Whether a stored entry that the model catalog does not list still deserves a row.
|
|
337
|
+
* A provider DSH has but whose model list failed is real, just unreadable, so its row
|
|
338
|
+
* stays; one that is routable with zero models looks deleted in DSH's own model list,
|
|
339
|
+
* so its row goes with it.
|
|
340
|
+
*/
|
|
341
|
+
function stillConfigured(registry, provider) {
|
|
342
|
+
return registry.routable.includes(provider) && registry.failed.includes(provider);
|
|
343
|
+
}
|
|
344
|
+
function buildProviderRows(input) {
|
|
345
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
346
|
+
for (const model of input.models) {
|
|
347
|
+
const existing = grouped.get(model.provider);
|
|
348
|
+
if (existing === void 0) grouped.set(model.provider, {
|
|
349
|
+
providerName: model.providerName,
|
|
350
|
+
models: [{
|
|
351
|
+
id: model.model,
|
|
352
|
+
name: model.name
|
|
353
|
+
}]
|
|
354
|
+
});
|
|
355
|
+
else existing.models.push({
|
|
356
|
+
id: model.model,
|
|
357
|
+
name: model.name
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
for (const provider of [...input.config.order, ...Object.keys(input.config.providers)]) {
|
|
361
|
+
if (grouped.has(provider)) continue;
|
|
362
|
+
if (input.registry !== void 0 && !stillConfigured(input.registry, provider)) continue;
|
|
363
|
+
grouped.set(provider, {
|
|
364
|
+
providerName: provider,
|
|
365
|
+
models: []
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return orderProviders([...grouped.keys()], input.config).map((provider) => {
|
|
369
|
+
const group = grouped.get(provider);
|
|
370
|
+
const entry = input.config.providers[provider];
|
|
371
|
+
const endpointHint = input.endpointHints?.[provider];
|
|
372
|
+
const sourceId = entry?.sourceId ?? suggestSourceId(provider, endpointHint) ?? null;
|
|
373
|
+
const modes = [
|
|
374
|
+
"auto",
|
|
375
|
+
...(sourceId === null ? void 0 : input.catalog.find((candidate) => candidate.id === sourceId))?.modes ?? ["api", "coding-plan"],
|
|
376
|
+
"hidden"
|
|
377
|
+
];
|
|
378
|
+
return {
|
|
379
|
+
provider,
|
|
380
|
+
providerName: group?.providerName ?? provider,
|
|
381
|
+
models: group?.models ?? [],
|
|
382
|
+
resolution: resolveProvider({
|
|
383
|
+
provider,
|
|
384
|
+
config: input.config,
|
|
385
|
+
catalog: input.catalog,
|
|
386
|
+
...endpointHint === void 0 ? {} : { endpointHint }
|
|
387
|
+
}),
|
|
388
|
+
selected: entry?.mode ?? "auto",
|
|
389
|
+
modes: [...new Set(modes)]
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/** Set one provider's mode, preserving its other overrides and every other provider. */
|
|
394
|
+
function setProviderMode(providers, provider, mode) {
|
|
395
|
+
const existing = providers[provider];
|
|
396
|
+
return {
|
|
397
|
+
...providers,
|
|
398
|
+
[provider]: {
|
|
399
|
+
...existing ?? {},
|
|
400
|
+
mode
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/** Swap a provider with its neighbour; `undefined` when the move is impossible. */
|
|
405
|
+
function reorderProviders(order, provider, delta) {
|
|
406
|
+
const index = order.indexOf(provider);
|
|
407
|
+
if (index < 0) return void 0;
|
|
408
|
+
const target = index + delta;
|
|
409
|
+
if (target < 0 || target >= order.length) return void 0;
|
|
410
|
+
const next = [...order];
|
|
411
|
+
const moved = next[index];
|
|
412
|
+
const displaced = next[target];
|
|
413
|
+
if (moved === void 0 || displaced === void 0) return void 0;
|
|
414
|
+
next[index] = displaced;
|
|
415
|
+
next[target] = moved;
|
|
416
|
+
return next;
|
|
417
|
+
}
|
|
418
|
+
//#endregion
|
|
419
|
+
//#region src/client/hooks.ts
|
|
420
|
+
/** Re-render on every store publication. */
|
|
421
|
+
function useStoreState(store) {
|
|
422
|
+
const [state, setState] = (0, react.useState)(() => store.getSnapshot());
|
|
423
|
+
(0, react.useEffect)(() => store.subscribe(() => setState(store.getSnapshot())), [store]);
|
|
424
|
+
return state;
|
|
425
|
+
}
|
|
426
|
+
/** A settings snapshot, kept current with the host document. */
|
|
427
|
+
function useSettingsValue(scope) {
|
|
428
|
+
const [snapshot, setSnapshot] = (0, react.useState)(() => scope.getSnapshot());
|
|
429
|
+
(0, react.useEffect)(() => scope.subscribe(() => setSnapshot(scope.getSnapshot())), [scope]);
|
|
430
|
+
return snapshot;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* A ticking clock, so reset countdowns stay honest without re-rendering on
|
|
434
|
+
* every frame. The client composition has no timer service, so this is a plain
|
|
435
|
+
* interval cleaned up with the effect.
|
|
436
|
+
*/
|
|
437
|
+
function useNow(intervalMs = 3e4) {
|
|
438
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
439
|
+
(0, react.useEffect)(() => {
|
|
440
|
+
const timer = setInterval(() => setNow(Date.now()), intervalMs);
|
|
441
|
+
return () => clearInterval(timer);
|
|
442
|
+
}, [intervalMs]);
|
|
443
|
+
return now;
|
|
444
|
+
}
|
|
445
|
+
//#endregion
|
|
446
|
+
//#region src/client/SettingsSection.tsx
|
|
447
|
+
const CARD = {
|
|
448
|
+
border: "1px solid var(--dsw-alias-border-l2, var(--dsw-alias-border-l3))",
|
|
449
|
+
borderRadius: "12px",
|
|
450
|
+
background: "var(--dsw-alias-bg-layer-3)",
|
|
451
|
+
padding: "12px 14px",
|
|
452
|
+
display: "flex",
|
|
453
|
+
flexDirection: "column",
|
|
454
|
+
gap: "10px"
|
|
455
|
+
};
|
|
456
|
+
const ROW = {
|
|
457
|
+
display: "flex",
|
|
458
|
+
alignItems: "center",
|
|
459
|
+
gap: "8px",
|
|
460
|
+
flexWrap: "wrap",
|
|
461
|
+
minWidth: 0
|
|
462
|
+
};
|
|
463
|
+
const MUTED = {
|
|
464
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
465
|
+
fontSize: "12px"
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* The card header is a two-column grid, never a wrapping flex row: the name
|
|
469
|
+
* column absorbs the squeeze (and ellipsizes) so the controls keep the same
|
|
470
|
+
* right edge on every card, whatever the provider is called.
|
|
471
|
+
*/
|
|
472
|
+
const HEADER = {
|
|
473
|
+
display: "grid",
|
|
474
|
+
gridTemplateColumns: "minmax(0, 1fr) auto",
|
|
475
|
+
alignItems: "center",
|
|
476
|
+
gap: "8px",
|
|
477
|
+
minWidth: 0
|
|
478
|
+
};
|
|
479
|
+
/** The name is already truncated by the time it is painted; the tooltip keeps it readable. */
|
|
480
|
+
const NAME = {
|
|
481
|
+
minWidth: 0,
|
|
482
|
+
overflow: "hidden",
|
|
483
|
+
textOverflow: "ellipsis",
|
|
484
|
+
whiteSpace: "nowrap"
|
|
485
|
+
};
|
|
486
|
+
const CONTROLS = {
|
|
487
|
+
display: "flex",
|
|
488
|
+
alignItems: "center",
|
|
489
|
+
gap: "8px",
|
|
490
|
+
flexShrink: 0
|
|
491
|
+
};
|
|
492
|
+
/** Models are listed for orientation only, so the line stays short. */
|
|
493
|
+
const MAX_MODELS_SHOWN = 6;
|
|
494
|
+
function modeLabel(mode, t) {
|
|
495
|
+
if (mode === "api") return t("modeApi");
|
|
496
|
+
if (mode === "coding-plan") return t("modeCodingPlan");
|
|
497
|
+
if (mode === "hidden") return t("modeHidden");
|
|
498
|
+
return t("modeAuto");
|
|
499
|
+
}
|
|
500
|
+
function windowModeLabel(mode, t) {
|
|
501
|
+
return modeLabel(mode, t);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* A text field that keeps a local draft and writes once, on blur or Enter.
|
|
505
|
+
* Committing on every keystroke would mean one settings revision per character.
|
|
506
|
+
*/
|
|
507
|
+
function DraftInput(props) {
|
|
508
|
+
const [draft, setDraft] = (0, react.useState)(props.value);
|
|
509
|
+
const [editing, setEditing] = (0, react.useState)(false);
|
|
510
|
+
const shown = editing ? draft : props.value;
|
|
511
|
+
const commit = () => {
|
|
512
|
+
setEditing(false);
|
|
513
|
+
const next = draft.trim();
|
|
514
|
+
if (next !== props.value.trim()) props.onCommit(next);
|
|
515
|
+
};
|
|
516
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
517
|
+
type: props.type ?? "text",
|
|
518
|
+
value: shown,
|
|
519
|
+
placeholder: props.placeholder,
|
|
520
|
+
disabled: props.disabled,
|
|
521
|
+
onChange: (event) => {
|
|
522
|
+
setEditing(true);
|
|
523
|
+
setDraft(event.target.value);
|
|
524
|
+
},
|
|
525
|
+
onBlur: commit,
|
|
526
|
+
onKeyDown: (event) => {
|
|
527
|
+
if (event.key === "Enter") event.target.blur();
|
|
528
|
+
},
|
|
529
|
+
style: { maxWidth: props.width ?? "280px" }
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Credential status and writes for one account. The candidate names are shown on
|
|
534
|
+
* purpose: the user has to know what to call the environment variable or stored
|
|
535
|
+
* credential — and when DSH already provides one, there is nothing to do here.
|
|
536
|
+
*/
|
|
537
|
+
function CredentialPanel(props) {
|
|
538
|
+
const { t } = props;
|
|
539
|
+
const [draft, setDraft] = (0, react.useState)("");
|
|
540
|
+
const [note, setNote] = (0, react.useState)(void 0);
|
|
541
|
+
const candidates = props.description?.candidates ?? [];
|
|
542
|
+
const configuredRef = props.description?.ref ?? candidates.find((candidate) => candidate.configured)?.ref ?? props.refs[0];
|
|
543
|
+
const writable = props.description?.writable !== false;
|
|
544
|
+
const save = async () => {
|
|
545
|
+
if (props.credentials === void 0 || configuredRef === void 0 || draft.trim() === "") return;
|
|
546
|
+
const result = await props.credentials.set(configuredRef, draft.trim());
|
|
547
|
+
if (!result.ok) {
|
|
548
|
+
setNote(t("credentialFailed", { message: result.error.message }));
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
setDraft("");
|
|
552
|
+
setNote(t("credentialSaved"));
|
|
553
|
+
props.onChanged();
|
|
554
|
+
};
|
|
555
|
+
const clear = async () => {
|
|
556
|
+
if (props.credentials === void 0 || configuredRef === void 0) return;
|
|
557
|
+
const result = await props.credentials.unset(configuredRef);
|
|
558
|
+
if (!result.ok) {
|
|
559
|
+
setNote(t("credentialFailed", { message: result.error.message }));
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
setNote(t("credentialSaved"));
|
|
563
|
+
props.onChanged();
|
|
564
|
+
};
|
|
565
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
566
|
+
style: {
|
|
567
|
+
display: "flex",
|
|
568
|
+
flexDirection: "column",
|
|
569
|
+
gap: "6px"
|
|
570
|
+
},
|
|
571
|
+
children: [
|
|
572
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
573
|
+
style: ROW,
|
|
574
|
+
children: [
|
|
575
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
576
|
+
style: MUTED,
|
|
577
|
+
children: t("credential")
|
|
578
|
+
}),
|
|
579
|
+
(candidates.length === 0 ? props.refs.map((ref) => ({
|
|
580
|
+
ref,
|
|
581
|
+
configured: false
|
|
582
|
+
})) : candidates).map((candidate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
583
|
+
style: ROW,
|
|
584
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
585
|
+
style: { fontSize: "11px" },
|
|
586
|
+
children: candidate.ref
|
|
587
|
+
}), candidate.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
|
|
588
|
+
tone: "success",
|
|
589
|
+
children: t("credentialConfigured", { source: candidate.source ?? "" })
|
|
590
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
|
|
591
|
+
tone: "neutral",
|
|
592
|
+
children: t("credentialMissing")
|
|
593
|
+
})]
|
|
594
|
+
}, candidate.ref)),
|
|
595
|
+
writable ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
596
|
+
style: MUTED,
|
|
597
|
+
children: t("credentialLocked")
|
|
598
|
+
})
|
|
599
|
+
]
|
|
600
|
+
}),
|
|
601
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
602
|
+
style: ROW,
|
|
603
|
+
children: [
|
|
604
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
605
|
+
type: "password",
|
|
606
|
+
autoComplete: "off",
|
|
607
|
+
value: draft,
|
|
608
|
+
disabled: !writable || props.credentials === void 0,
|
|
609
|
+
placeholder: t("credentialPlaceholder"),
|
|
610
|
+
onChange: (event) => setDraft(event.target.value),
|
|
611
|
+
style: { maxWidth: "280px" }
|
|
612
|
+
}),
|
|
613
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
614
|
+
size: "sm",
|
|
615
|
+
variant: "primary",
|
|
616
|
+
disabled: draft.trim() === "",
|
|
617
|
+
onClick: () => void save(),
|
|
618
|
+
children: t("credentialSave")
|
|
619
|
+
}),
|
|
620
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
621
|
+
size: "sm",
|
|
622
|
+
variant: "outline",
|
|
623
|
+
disabled: configuredRef === void 0 || props.description?.configured !== true,
|
|
624
|
+
onClick: () => void clear(),
|
|
625
|
+
children: t("credentialClear")
|
|
626
|
+
})
|
|
627
|
+
]
|
|
628
|
+
}),
|
|
629
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
630
|
+
style: MUTED,
|
|
631
|
+
children: note ?? t("credentialHint")
|
|
632
|
+
})
|
|
633
|
+
]
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
/** What this row currently resolves to, in one short phrase. */
|
|
637
|
+
function resolutionLabel(row, catalog, t) {
|
|
638
|
+
const { resolution } = row;
|
|
639
|
+
if (resolution.reason === "hidden") return t("modeHidden");
|
|
640
|
+
if (resolution.reason === "needs-endpoint") return t("needsEndpoint");
|
|
641
|
+
if (resolution.reason === "unknown-source") return t("unknownSource");
|
|
642
|
+
if (resolution.reason === "unsupported" || resolution.mode === null || resolution.sourceId === null) return t("modeUnsupported");
|
|
643
|
+
const target = `${catalog.find((entry) => entry.id === resolution.sourceId)?.displayName ?? resolution.sourceId} · ${windowModeLabel(resolution.mode, t)}`;
|
|
644
|
+
return resolution.reason === "auto" ? t("detectedAs", { target }) : t("showsAs", { target });
|
|
645
|
+
}
|
|
646
|
+
function ProviderCard(props) {
|
|
647
|
+
const { t, row } = props;
|
|
648
|
+
const entry = props.config.providers[row.provider];
|
|
649
|
+
const resolvedSource = row.resolution.sourceId === null ? void 0 : props.catalog.find((candidate) => candidate.id === row.resolution.sourceId);
|
|
650
|
+
const refMode = row.resolution.mode ?? resolvedSource?.modes[0];
|
|
651
|
+
const refs = refMode === void 0 ? [] : [...resolvedSource?.credentialRefs[refMode] ?? []];
|
|
652
|
+
const modelNames = row.models.map((model) => model.name);
|
|
653
|
+
const listed = modelNames.slice(0, MAX_MODELS_SHOWN).join(" · ");
|
|
654
|
+
const suffix = modelNames.length > MAX_MODELS_SHOWN ? ", …" : "";
|
|
655
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
656
|
+
style: {
|
|
657
|
+
display: "flex",
|
|
658
|
+
flexDirection: "column",
|
|
659
|
+
gap: "6px",
|
|
660
|
+
paddingBottom: "6px"
|
|
661
|
+
},
|
|
662
|
+
children: [
|
|
663
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
664
|
+
style: HEADER,
|
|
665
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
666
|
+
style: NAME,
|
|
667
|
+
title: row.providerName === row.provider ? row.providerName : `${row.providerName} · ${row.provider}`,
|
|
668
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: row.providerName }), row.providerName === row.provider ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
669
|
+
style: MUTED,
|
|
670
|
+
children: [" ", row.provider]
|
|
671
|
+
})]
|
|
672
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
673
|
+
style: CONTROLS,
|
|
674
|
+
children: [
|
|
675
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
676
|
+
size: "sm",
|
|
677
|
+
variant: "ghost",
|
|
678
|
+
"aria-label": t("moveUp"),
|
|
679
|
+
disabled: props.index === 0,
|
|
680
|
+
onClick: () => props.onMove(-1),
|
|
681
|
+
children: "↑"
|
|
682
|
+
}),
|
|
683
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
684
|
+
size: "sm",
|
|
685
|
+
variant: "ghost",
|
|
686
|
+
"aria-label": t("moveDown"),
|
|
687
|
+
disabled: props.index === props.total - 1,
|
|
688
|
+
onClick: () => props.onMove(1),
|
|
689
|
+
children: "↓"
|
|
690
|
+
}),
|
|
691
|
+
row.modes.map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
692
|
+
size: "sm",
|
|
693
|
+
variant: row.selected === mode ? "primary" : "outline",
|
|
694
|
+
onClick: () => props.onMode(mode),
|
|
695
|
+
children: modeLabel(mode, t)
|
|
696
|
+
}, mode))
|
|
697
|
+
]
|
|
698
|
+
})]
|
|
699
|
+
}),
|
|
700
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
701
|
+
style: MUTED,
|
|
702
|
+
children: resolutionLabel(row, props.catalog, t)
|
|
703
|
+
}),
|
|
704
|
+
props.failure === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
705
|
+
style: MUTED,
|
|
706
|
+
children: [
|
|
707
|
+
"⚠ ",
|
|
708
|
+
t(`error.${props.failure.kind}`),
|
|
709
|
+
props.failure.detail === void 0 ? "" : ` — ${props.failure.detail}`
|
|
710
|
+
]
|
|
711
|
+
}),
|
|
712
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
713
|
+
style: MUTED,
|
|
714
|
+
children: modelNames.length === 0 ? t("noModels") : t("modelsPrefix", { list: `${listed}${suffix}` })
|
|
715
|
+
}),
|
|
716
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", {
|
|
717
|
+
style: {
|
|
718
|
+
...MUTED,
|
|
719
|
+
cursor: "pointer"
|
|
720
|
+
},
|
|
721
|
+
children: t("advanced")
|
|
722
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
723
|
+
style: {
|
|
724
|
+
display: "flex",
|
|
725
|
+
flexDirection: "column",
|
|
726
|
+
gap: "8px",
|
|
727
|
+
paddingTop: "8px"
|
|
728
|
+
},
|
|
729
|
+
children: [
|
|
730
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
731
|
+
style: ROW,
|
|
732
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
733
|
+
style: MUTED,
|
|
734
|
+
children: t("sourceLabel")
|
|
735
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
736
|
+
value: entry?.sourceId ?? "",
|
|
737
|
+
onChange: (event) => {
|
|
738
|
+
const value = event.target.value;
|
|
739
|
+
if (value === "") props.onClearField([
|
|
740
|
+
"providers",
|
|
741
|
+
row.provider,
|
|
742
|
+
"sourceId"
|
|
743
|
+
]);
|
|
744
|
+
else props.onField([
|
|
745
|
+
"providers",
|
|
746
|
+
row.provider,
|
|
747
|
+
"sourceId"
|
|
748
|
+
], value);
|
|
749
|
+
},
|
|
750
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
751
|
+
value: "",
|
|
752
|
+
children: t("sourceAuto")
|
|
753
|
+
}), props.catalog.map((candidate) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
754
|
+
value: candidate.id,
|
|
755
|
+
children: candidate.displayName
|
|
756
|
+
}, candidate.id))]
|
|
757
|
+
})]
|
|
758
|
+
}),
|
|
759
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
760
|
+
style: ROW,
|
|
761
|
+
children: [
|
|
762
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
763
|
+
style: MUTED,
|
|
764
|
+
children: t("baseUrl")
|
|
765
|
+
}),
|
|
766
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DraftInput, {
|
|
767
|
+
value: entry?.baseUrl ?? "",
|
|
768
|
+
placeholder: row.resolution.mode === null ? t("baseUrlPlaceholder") : resolvedSource?.defaultBaseUrl[row.resolution.mode] ?? t("baseUrlPlaceholder"),
|
|
769
|
+
width: "320px",
|
|
770
|
+
onCommit: (value) => {
|
|
771
|
+
if (value === "") props.onClearField([
|
|
772
|
+
"providers",
|
|
773
|
+
row.provider,
|
|
774
|
+
"baseUrl"
|
|
775
|
+
]);
|
|
776
|
+
else props.onField([
|
|
777
|
+
"providers",
|
|
778
|
+
row.provider,
|
|
779
|
+
"baseUrl"
|
|
780
|
+
], value);
|
|
781
|
+
}
|
|
782
|
+
}),
|
|
783
|
+
resolvedSource?.requiresBaseUrl === true && entry?.baseUrl === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
|
|
784
|
+
tone: "warning",
|
|
785
|
+
children: t("baseUrlRequired")
|
|
786
|
+
}) : null
|
|
787
|
+
]
|
|
788
|
+
}),
|
|
789
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
790
|
+
style: ROW,
|
|
791
|
+
children: [
|
|
792
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
793
|
+
style: MUTED,
|
|
794
|
+
children: t("apiKeyRef")
|
|
795
|
+
}),
|
|
796
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DraftInput, {
|
|
797
|
+
value: entry?.apiKeyRef ?? "",
|
|
798
|
+
width: "220px",
|
|
799
|
+
onCommit: (value) => {
|
|
800
|
+
if (value === "") props.onClearField([
|
|
801
|
+
"providers",
|
|
802
|
+
row.provider,
|
|
803
|
+
"apiKeyRef"
|
|
804
|
+
]);
|
|
805
|
+
else props.onField([
|
|
806
|
+
"providers",
|
|
807
|
+
row.provider,
|
|
808
|
+
"apiKeyRef"
|
|
809
|
+
], value);
|
|
810
|
+
}
|
|
811
|
+
}),
|
|
812
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
813
|
+
style: MUTED,
|
|
814
|
+
children: t("apiKeyRefHint")
|
|
815
|
+
})
|
|
816
|
+
]
|
|
817
|
+
}),
|
|
818
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialPanel, {
|
|
819
|
+
t,
|
|
820
|
+
refs,
|
|
821
|
+
description: props.description,
|
|
822
|
+
credentials: props.credentials,
|
|
823
|
+
onChanged: props.onCredentialChanged
|
|
824
|
+
})
|
|
825
|
+
]
|
|
826
|
+
})] })
|
|
827
|
+
]
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* The plugin's settings page: one row per provider. The reading is account-level,
|
|
832
|
+
* so per-model configuration was both long and redundant. Everything the plugin
|
|
833
|
+
* can work out on its own — which source, which mode, which key — is left to
|
|
834
|
+
* "auto"; only deviations are written to the settings document.
|
|
835
|
+
*/
|
|
836
|
+
function SettingsSection(props) {
|
|
837
|
+
const { t } = props;
|
|
838
|
+
const snapshot = useSettingsValue(props.settings);
|
|
839
|
+
const state = useStoreState(props.usageState);
|
|
840
|
+
(0, react.useEffect)(() => {
|
|
841
|
+
props.usageState.refreshModels();
|
|
842
|
+
props.usageState.refreshCredentials();
|
|
843
|
+
}, [props.usageState]);
|
|
844
|
+
if (snapshot.status === "unavailable") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
845
|
+
style: MUTED,
|
|
846
|
+
children: t("unavailable")
|
|
847
|
+
});
|
|
848
|
+
const config = normalizeConfig(snapshot.value ?? {});
|
|
849
|
+
const rows = buildProviderRows({
|
|
850
|
+
models: state.models,
|
|
851
|
+
config,
|
|
852
|
+
catalog: state.catalog,
|
|
853
|
+
registry: state.modelRegistry
|
|
854
|
+
});
|
|
855
|
+
const write = (path, value) => {
|
|
856
|
+
props.settings.mutate([{
|
|
857
|
+
op: "set",
|
|
858
|
+
path,
|
|
859
|
+
value
|
|
860
|
+
}]);
|
|
861
|
+
};
|
|
862
|
+
const clear = (path) => {
|
|
863
|
+
props.settings.mutate([{
|
|
864
|
+
op: "unset",
|
|
865
|
+
path
|
|
866
|
+
}]);
|
|
867
|
+
};
|
|
868
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
869
|
+
style: {
|
|
870
|
+
display: "flex",
|
|
871
|
+
flexDirection: "column",
|
|
872
|
+
gap: "14px",
|
|
873
|
+
maxWidth: "760px"
|
|
874
|
+
},
|
|
875
|
+
children: [
|
|
876
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
877
|
+
style: {
|
|
878
|
+
display: "flex",
|
|
879
|
+
flexDirection: "column",
|
|
880
|
+
gap: "6px"
|
|
881
|
+
},
|
|
882
|
+
children: [
|
|
883
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
884
|
+
style: { margin: 0 },
|
|
885
|
+
children: t("title")
|
|
886
|
+
}),
|
|
887
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
888
|
+
style: {
|
|
889
|
+
...MUTED,
|
|
890
|
+
margin: 0
|
|
891
|
+
},
|
|
892
|
+
children: t("intro")
|
|
893
|
+
}),
|
|
894
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
895
|
+
style: ROW,
|
|
896
|
+
children: [
|
|
897
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
898
|
+
size: "sm",
|
|
899
|
+
variant: "outline",
|
|
900
|
+
onClick: () => void props.usageState.refresh(true),
|
|
901
|
+
children: t("refreshNow")
|
|
902
|
+
}),
|
|
903
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
904
|
+
style: MUTED,
|
|
905
|
+
children: state.checkedAt === void 0 ? t("neverChecked") : t("lastChecked", { time: new Date(state.checkedAt).toLocaleTimeString() })
|
|
906
|
+
}),
|
|
907
|
+
state.error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
908
|
+
style: MUTED,
|
|
909
|
+
children: t("refreshFailed", { message: state.error })
|
|
910
|
+
}),
|
|
911
|
+
state.modelsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
912
|
+
style: MUTED,
|
|
913
|
+
children: state.modelsError
|
|
914
|
+
})
|
|
915
|
+
]
|
|
916
|
+
})
|
|
917
|
+
]
|
|
918
|
+
}),
|
|
919
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
920
|
+
style: CARD,
|
|
921
|
+
children: [
|
|
922
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sectionProviders") }),
|
|
923
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
924
|
+
style: MUTED,
|
|
925
|
+
children: t("sectionProvidersHint")
|
|
926
|
+
}),
|
|
927
|
+
rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
928
|
+
style: MUTED,
|
|
929
|
+
children: t("empty")
|
|
930
|
+
}) : null,
|
|
931
|
+
rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderCard, {
|
|
932
|
+
t,
|
|
933
|
+
row,
|
|
934
|
+
config,
|
|
935
|
+
catalog: state.catalog,
|
|
936
|
+
credentials: props.credentials,
|
|
937
|
+
description: row.resolution.key === void 0 ? void 0 : state.credentials[row.resolution.key],
|
|
938
|
+
failure: row.resolution.key === void 0 ? void 0 : state.snapshots[row.resolution.key]?.error,
|
|
939
|
+
index,
|
|
940
|
+
total: rows.length,
|
|
941
|
+
onMode: (mode) => write(["providers"], setProviderMode(config.providers, row.provider, mode)),
|
|
942
|
+
onMove: (delta) => {
|
|
943
|
+
const next = reorderProviders(config.order, row.provider, delta);
|
|
944
|
+
if (next !== void 0) write(["order"], next);
|
|
945
|
+
},
|
|
946
|
+
onField: (path, value) => write(path, value),
|
|
947
|
+
onClearField: (path) => clear(path),
|
|
948
|
+
onCredentialChanged: () => void props.usageState.refreshCredentials()
|
|
949
|
+
}, row.provider))
|
|
950
|
+
]
|
|
951
|
+
}),
|
|
952
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
953
|
+
style: CARD,
|
|
954
|
+
children: [
|
|
955
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sectionDisplay") }),
|
|
956
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
957
|
+
style: ROW,
|
|
958
|
+
children: [
|
|
959
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
960
|
+
style: MUTED,
|
|
961
|
+
children: t("thresholdWarn")
|
|
962
|
+
}),
|
|
963
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DraftInput, {
|
|
964
|
+
type: "number",
|
|
965
|
+
value: String(config.display.thresholdWarnPercent),
|
|
966
|
+
width: "90px",
|
|
967
|
+
onCommit: (value) => write(["display", "thresholdWarnPercent"], Number(value))
|
|
968
|
+
}),
|
|
969
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
970
|
+
style: MUTED,
|
|
971
|
+
children: t("thresholdCritical")
|
|
972
|
+
}),
|
|
973
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DraftInput, {
|
|
974
|
+
type: "number",
|
|
975
|
+
value: String(config.display.thresholdCriticalPercent),
|
|
976
|
+
width: "90px",
|
|
977
|
+
onCommit: (value) => write(["display", "thresholdCriticalPercent"], Number(value))
|
|
978
|
+
})
|
|
979
|
+
]
|
|
980
|
+
}),
|
|
981
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
982
|
+
style: ROW,
|
|
983
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
984
|
+
style: MUTED,
|
|
985
|
+
children: t("intervalMinutes")
|
|
986
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DraftInput, {
|
|
987
|
+
type: "number",
|
|
988
|
+
value: String(config.refresh.intervalMinutes),
|
|
989
|
+
width: "90px",
|
|
990
|
+
onCommit: (value) => write(["refresh", "intervalMinutes"], Number(value))
|
|
991
|
+
})]
|
|
992
|
+
}),
|
|
993
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
|
|
994
|
+
checked: config.display.progressBar,
|
|
995
|
+
label: t("progressBar"),
|
|
996
|
+
onChange: (next) => write(["display", "progressBar"], next)
|
|
997
|
+
})
|
|
998
|
+
]
|
|
999
|
+
}),
|
|
1000
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1001
|
+
style: ROW,
|
|
1002
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1003
|
+
size: "sm",
|
|
1004
|
+
variant: "ghost",
|
|
1005
|
+
onClick: props.close,
|
|
1006
|
+
children: t("close")
|
|
1007
|
+
})
|
|
1008
|
+
})
|
|
1009
|
+
]
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
//#endregion
|
|
1013
|
+
//#region src/shared/display.ts
|
|
1014
|
+
const CURRENCY_SYMBOLS = {
|
|
1015
|
+
CNY: "¥",
|
|
1016
|
+
USD: "$"
|
|
1017
|
+
};
|
|
1018
|
+
const PROGRESS_WIDTH = 8;
|
|
1019
|
+
/** `¥66.28`, `$6.80`, `EUR 1.50`; an unknown code keeps its numeric form. */
|
|
1020
|
+
function formatBalance(balance) {
|
|
1021
|
+
const amount = balance.amount.toFixed(2);
|
|
1022
|
+
const currency = balance.currency.trim().toUpperCase();
|
|
1023
|
+
if (currency === "") return amount;
|
|
1024
|
+
const symbol = CURRENCY_SYMBOLS[currency];
|
|
1025
|
+
return symbol === void 0 ? `${currency} ${amount}` : `${symbol}${amount}`;
|
|
1026
|
+
}
|
|
1027
|
+
/** One decimal only when it carries information: `42%`, `42.5%`. */
|
|
1028
|
+
function formatPercent(percent) {
|
|
1029
|
+
const rounded = Math.round(percent * 10) / 10;
|
|
1030
|
+
return Number.isInteger(rounded) ? `${rounded}%` : `${rounded.toFixed(1)}%`;
|
|
1031
|
+
}
|
|
1032
|
+
/** Compact, language-neutral age of a kept reading: `45s`, `12m`, `4h12m`, `3d`. */
|
|
1033
|
+
function formatAge(since, now) {
|
|
1034
|
+
return formatCountdown(now, since) ?? "0s";
|
|
1035
|
+
}
|
|
1036
|
+
/** Compact, language-neutral remaining time: `5d`, `3d4h`, `4h12m`, `12m`, `45s`. */
|
|
1037
|
+
function formatCountdown(resetsAt, now) {
|
|
1038
|
+
if (resetsAt === void 0) return void 0;
|
|
1039
|
+
const seconds = Math.floor(Math.max(0, resetsAt - now) / 1e3);
|
|
1040
|
+
const days = Math.floor(seconds / 86400);
|
|
1041
|
+
if (days >= 1) {
|
|
1042
|
+
const hours = Math.floor(seconds % 86400 / 3600);
|
|
1043
|
+
return hours === 0 ? `${days}d` : `${days}d${hours}h`;
|
|
1044
|
+
}
|
|
1045
|
+
const hours = Math.floor(seconds / 3600);
|
|
1046
|
+
if (hours >= 1) return `${hours}h${Math.floor(seconds % 3600 / 60)}m`;
|
|
1047
|
+
const minutes = Math.floor(seconds / 60);
|
|
1048
|
+
return minutes >= 1 ? `${minutes}m` : `${seconds}s`;
|
|
1049
|
+
}
|
|
1050
|
+
function progressBar(percent, width = PROGRESS_WIDTH) {
|
|
1051
|
+
const filled = Math.min(width, Math.max(0, Math.round(percent / 100 * width)));
|
|
1052
|
+
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
1053
|
+
}
|
|
1054
|
+
function severityOf(usedPercent, display) {
|
|
1055
|
+
if (usedPercent >= display.thresholdCriticalPercent) return "critical";
|
|
1056
|
+
if (usedPercent >= display.thresholdWarnPercent) return "warn";
|
|
1057
|
+
return "normal";
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* The status line's content, as data: numbers formatted, states named, nothing
|
|
1061
|
+
* localized. The browser turns `state` and window ids into copy.
|
|
1062
|
+
*
|
|
1063
|
+
* A stale reading stays visible (with the label flagged) because hiding it would
|
|
1064
|
+
* look like "no usage"; a failure with no previous reading shows the error state
|
|
1065
|
+
* instead of a misleading zero.
|
|
1066
|
+
*/
|
|
1067
|
+
function describeStatus(input) {
|
|
1068
|
+
const { status, snapshot, display, now } = input;
|
|
1069
|
+
if (status.kind === "hidden") return [];
|
|
1070
|
+
if (status.kind === "unconfigured") return [{
|
|
1071
|
+
kind: "state",
|
|
1072
|
+
state: "unconfigured"
|
|
1073
|
+
}];
|
|
1074
|
+
if (status.kind === "needs-endpoint") return [{
|
|
1075
|
+
kind: "state",
|
|
1076
|
+
state: "needs-endpoint"
|
|
1077
|
+
}];
|
|
1078
|
+
if (status.kind === "unsupported") return [{
|
|
1079
|
+
kind: "state",
|
|
1080
|
+
state: "unsupported"
|
|
1081
|
+
}];
|
|
1082
|
+
const label = snapshot?.stale === true ? {
|
|
1083
|
+
kind: "label",
|
|
1084
|
+
text: input.sourceLabel,
|
|
1085
|
+
stale: true,
|
|
1086
|
+
...snapshot.fetchedAt > 0 ? { staleSince: snapshot.fetchedAt } : {}
|
|
1087
|
+
} : {
|
|
1088
|
+
kind: "label",
|
|
1089
|
+
text: input.sourceLabel
|
|
1090
|
+
};
|
|
1091
|
+
if (snapshot === void 0) return [label, {
|
|
1092
|
+
kind: "state",
|
|
1093
|
+
state: "loading"
|
|
1094
|
+
}];
|
|
1095
|
+
if (snapshot.balances.length === 0 && snapshot.windows.length === 0) return [label, snapshot.error === void 0 ? {
|
|
1096
|
+
kind: "state",
|
|
1097
|
+
state: "loading"
|
|
1098
|
+
} : {
|
|
1099
|
+
kind: "state",
|
|
1100
|
+
state: "error",
|
|
1101
|
+
errorKind: snapshot.error.kind,
|
|
1102
|
+
...snapshot.error.detail === void 0 ? {} : { errorDetail: snapshot.error.detail }
|
|
1103
|
+
}];
|
|
1104
|
+
const segments = [label];
|
|
1105
|
+
for (const balance of snapshot.balances) segments.push({
|
|
1106
|
+
kind: "balance",
|
|
1107
|
+
amount: formatBalance(balance),
|
|
1108
|
+
currency: balance.currency,
|
|
1109
|
+
...balance.granted === void 0 ? {} : { granted: balance.granted },
|
|
1110
|
+
...balance.toppedUp === void 0 ? {} : { toppedUp: balance.toppedUp }
|
|
1111
|
+
});
|
|
1112
|
+
for (const window of snapshot.windows) {
|
|
1113
|
+
const segment = {
|
|
1114
|
+
kind: "window",
|
|
1115
|
+
windowId: window.id,
|
|
1116
|
+
percent: formatPercent(window.usedPercent),
|
|
1117
|
+
severity: severityOf(window.usedPercent, display)
|
|
1118
|
+
};
|
|
1119
|
+
if (window.resetsAt !== void 0 && window.resetsAt > now) segment.resetsAt = window.resetsAt;
|
|
1120
|
+
if (display.progressBar) segment.bar = progressBar(window.usedPercent);
|
|
1121
|
+
segments.push(segment);
|
|
1122
|
+
}
|
|
1123
|
+
return segments;
|
|
1124
|
+
}
|
|
1125
|
+
//#endregion
|
|
1126
|
+
//#region src/client/status-text.ts
|
|
1127
|
+
/**
|
|
1128
|
+
* Window ids are data, not copy: `5h` is the same in every language. When the
|
|
1129
|
+
* dictionary has no entry the raw id is shown rather than the lookup key.
|
|
1130
|
+
*/
|
|
1131
|
+
function windowLabel(id, t) {
|
|
1132
|
+
const key = `window.${id}`;
|
|
1133
|
+
const text = t(key);
|
|
1134
|
+
return text === key ? id : text;
|
|
1135
|
+
}
|
|
1136
|
+
/** Join the pieces of a tooltip, dropping the ones that do not apply. */
|
|
1137
|
+
function tooltipOf(pieces) {
|
|
1138
|
+
const present = pieces.filter((piece) => piece !== void 0 && piece !== "");
|
|
1139
|
+
return present.length === 0 ? void 0 : present.join(" · ");
|
|
1140
|
+
}
|
|
1141
|
+
function localTime(epochMs) {
|
|
1142
|
+
return new Date(epochMs).toLocaleTimeString();
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Turn the host's language-neutral segments into render-ready parts.
|
|
1146
|
+
*
|
|
1147
|
+
* Every part also carries a tooltip: the line has room for one number per window,
|
|
1148
|
+
* while the useful context (which source and mode this reading belongs to, when it
|
|
1149
|
+
* was last refreshed, when a window resets, how a balance splits) does not fit.
|
|
1150
|
+
*/
|
|
1151
|
+
function statusParts(input) {
|
|
1152
|
+
const { segments, t, now, sourceLabel, modeLabel } = input;
|
|
1153
|
+
const context = tooltipOf([sourceLabel === void 0 ? void 0 : t("tipSource", { source: sourceLabel }), modeLabel === void 0 ? void 0 : t("tipMode", { mode: modeLabel })]);
|
|
1154
|
+
const parts = [];
|
|
1155
|
+
for (const segment of segments) switch (segment.kind) {
|
|
1156
|
+
case "label": {
|
|
1157
|
+
const staleHint = segment.stale === true ? t("staleHint") : void 0;
|
|
1158
|
+
const refreshHint = segment.staleSince === void 0 ? void 0 : t("tipUpdated", { age: formatAge(segment.staleSince, now) });
|
|
1159
|
+
parts.push({
|
|
1160
|
+
kind: "label",
|
|
1161
|
+
text: segment.text,
|
|
1162
|
+
stale: segment.stale === true,
|
|
1163
|
+
...tooltipOf([
|
|
1164
|
+
staleHint,
|
|
1165
|
+
refreshHint,
|
|
1166
|
+
context
|
|
1167
|
+
]) === void 0 ? {} : { tooltip: tooltipOf([
|
|
1168
|
+
staleHint,
|
|
1169
|
+
refreshHint,
|
|
1170
|
+
context
|
|
1171
|
+
]) }
|
|
1172
|
+
});
|
|
1173
|
+
if (segment.staleSince !== void 0) parts.push({
|
|
1174
|
+
kind: "age",
|
|
1175
|
+
text: t("staleAgo", { age: formatAge(segment.staleSince, now) }),
|
|
1176
|
+
...staleHint === void 0 ? {} : { tooltip: staleHint }
|
|
1177
|
+
});
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
case "balance": {
|
|
1181
|
+
const split = segment.granted === void 0 && segment.toppedUp === void 0 ? void 0 : t("tipBalanceSplit", {
|
|
1182
|
+
granted: segment.granted === void 0 ? "—" : String(segment.granted),
|
|
1183
|
+
toppedUp: segment.toppedUp === void 0 ? "—" : String(segment.toppedUp)
|
|
1184
|
+
});
|
|
1185
|
+
parts.push({
|
|
1186
|
+
kind: "balance",
|
|
1187
|
+
text: segment.amount,
|
|
1188
|
+
currency: segment.currency,
|
|
1189
|
+
...tooltipOf([split, context]) === void 0 ? {} : { tooltip: tooltipOf([split, context]) }
|
|
1190
|
+
});
|
|
1191
|
+
break;
|
|
1192
|
+
}
|
|
1193
|
+
case "window": {
|
|
1194
|
+
const countdown = formatCountdown(segment.resetsAt, now);
|
|
1195
|
+
const resetHint = segment.resetsAt === void 0 ? void 0 : t("tipResets", { time: localTime(segment.resetsAt) });
|
|
1196
|
+
parts.push({
|
|
1197
|
+
kind: "window",
|
|
1198
|
+
id: segment.windowId,
|
|
1199
|
+
text: `${windowLabel(segment.windowId, t)} ${segment.percent}`,
|
|
1200
|
+
percent: segment.percent,
|
|
1201
|
+
severity: segment.severity,
|
|
1202
|
+
...countdown === void 0 ? {} : { countdown },
|
|
1203
|
+
...segment.bar === void 0 ? {} : { bar: segment.bar },
|
|
1204
|
+
...tooltipOf([resetHint, context]) === void 0 ? {} : { tooltip: tooltipOf([resetHint, context]) }
|
|
1205
|
+
});
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
case "state": {
|
|
1209
|
+
const reason = segment.errorKind === void 0 ? void 0 : segment.errorDetail === void 0 ? t(`error.${segment.errorKind}`) : `${t(`error.${segment.errorKind}`)}: ${segment.errorDetail}`;
|
|
1210
|
+
parts.push({
|
|
1211
|
+
kind: "state",
|
|
1212
|
+
state: segment.state,
|
|
1213
|
+
text: t(`state.${segment.state}`),
|
|
1214
|
+
...segment.errorKind === void 0 ? {} : { errorKind: segment.errorKind },
|
|
1215
|
+
...segment.errorDetail === void 0 ? {} : { errorDetail: segment.errorDetail },
|
|
1216
|
+
...tooltipOf([reason, context]) === void 0 ? {} : { tooltip: tooltipOf([reason, context]) }
|
|
1217
|
+
});
|
|
1218
|
+
break;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return parts;
|
|
1222
|
+
}
|
|
1223
|
+
//#endregion
|
|
1224
|
+
//#region src/client/StatusLine.tsx
|
|
1225
|
+
/**
|
|
1226
|
+
* The composer mount point: one line directly under the platform's stats row, so
|
|
1227
|
+
* it reads as that row's second line rather than a stray block.
|
|
1228
|
+
*/
|
|
1229
|
+
const DOCK_STYLE = {
|
|
1230
|
+
display: "flex",
|
|
1231
|
+
alignItems: "center",
|
|
1232
|
+
justifyContent: "center",
|
|
1233
|
+
gap: "8px",
|
|
1234
|
+
fontSize: "var(--dsh-content-font-size-secondary, 13px)",
|
|
1235
|
+
lineHeight: "calc(20px + var(--dsh-content-font-delta-secondary, 0px))",
|
|
1236
|
+
fontVariantNumeric: "tabular-nums",
|
|
1237
|
+
whiteSpace: "nowrap",
|
|
1238
|
+
overflow: "hidden",
|
|
1239
|
+
textOverflow: "ellipsis",
|
|
1240
|
+
width: "100%",
|
|
1241
|
+
maxWidth: "var(--dsh-chat-content-width)",
|
|
1242
|
+
margin: "0 auto",
|
|
1243
|
+
padding: "4px calc(var(--dsh-composer-side-clearance) + 16px) 0"
|
|
1244
|
+
};
|
|
1245
|
+
const LABEL_STYLE = { color: "var(--dsw-alias-label-tertiary)" };
|
|
1246
|
+
const SEPARATOR_STYLE = { color: "var(--dsw-alias-separator-primary, var(--dsw-alias-label-dimmed))" };
|
|
1247
|
+
function severityColor(severity) {
|
|
1248
|
+
if (severity === "critical") return "var(--dsw-alias-state-error-primary)";
|
|
1249
|
+
if (severity === "warn") return "var(--dsw-alias-state-warn-primary)";
|
|
1250
|
+
return "var(--dsw-alias-label-secondary)";
|
|
1251
|
+
}
|
|
1252
|
+
/** Wrap one part in the platform tooltip when it has something more to say. */
|
|
1253
|
+
function withTooltip(node, tooltip, key) {
|
|
1254
|
+
if (tooltip === void 0) return node;
|
|
1255
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
1256
|
+
label: tooltip,
|
|
1257
|
+
side: "top",
|
|
1258
|
+
children: node
|
|
1259
|
+
}, `tip-${key}`);
|
|
1260
|
+
}
|
|
1261
|
+
function renderPart(part, t, key) {
|
|
1262
|
+
switch (part.kind) {
|
|
1263
|
+
case "label": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1264
|
+
style: LABEL_STYLE,
|
|
1265
|
+
children: [part.stale ? "⚠ " : "", part.text]
|
|
1266
|
+
}, key);
|
|
1267
|
+
case "age": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1268
|
+
style: LABEL_STYLE,
|
|
1269
|
+
children: part.text
|
|
1270
|
+
}, key);
|
|
1271
|
+
case "balance": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1272
|
+
style: { color: "var(--dsw-alias-label-secondary)" },
|
|
1273
|
+
children: part.text
|
|
1274
|
+
}, key);
|
|
1275
|
+
case "window": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1276
|
+
style: { color: severityColor(part.severity) },
|
|
1277
|
+
children: [
|
|
1278
|
+
part.text,
|
|
1279
|
+
part.countdown === void 0 ? "" : ` (${part.countdown})`,
|
|
1280
|
+
part.bar === void 0 ? "" : ` ${part.bar}`
|
|
1281
|
+
]
|
|
1282
|
+
}, key);
|
|
1283
|
+
case "state": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1284
|
+
style: LABEL_STYLE,
|
|
1285
|
+
children: part.text
|
|
1286
|
+
}, key);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
/** Map a provider resolution onto what the line can say about it. */
|
|
1290
|
+
function statusOf(resolution) {
|
|
1291
|
+
switch (resolution.reason) {
|
|
1292
|
+
case "hidden": return { kind: "hidden" };
|
|
1293
|
+
case "needs-endpoint": return { kind: "needs-endpoint" };
|
|
1294
|
+
case "unknown-source":
|
|
1295
|
+
case "unsupported": return resolution.sourceId === null ? { kind: "unconfigured" } : { kind: "unsupported" };
|
|
1296
|
+
case "auto":
|
|
1297
|
+
case "configured":
|
|
1298
|
+
if (resolution.key === void 0 || resolution.sourceId === null || resolution.mode === null) return { kind: "unconfigured" };
|
|
1299
|
+
return {
|
|
1300
|
+
kind: "ready",
|
|
1301
|
+
key: resolution.key,
|
|
1302
|
+
sourceId: resolution.sourceId,
|
|
1303
|
+
mode: resolution.mode
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* One read-only usage line: balance in API mode, 5h/7d quota in coding-plan mode.
|
|
1309
|
+
*
|
|
1310
|
+
* It reports the account's **current** reading, so it has exactly one home: the
|
|
1311
|
+
* line under the composer. There is no per-turn copy — an account-level number
|
|
1312
|
+
* cannot honestly describe a single turn (see `slots.ts`).
|
|
1313
|
+
*/
|
|
1314
|
+
function StatusLine(props) {
|
|
1315
|
+
const t = props.t;
|
|
1316
|
+
const now = useNow(3e4);
|
|
1317
|
+
const state = useStoreState(props.usageState);
|
|
1318
|
+
const config = useSettingsValue(props.settings).value;
|
|
1319
|
+
const selection = props.useProjection?.("modelSelection");
|
|
1320
|
+
const current = selection?.next ?? selection?.lastUsed ?? null;
|
|
1321
|
+
if (config === void 0 || current === null) return null;
|
|
1322
|
+
const status = statusOf(resolveProvider({
|
|
1323
|
+
provider: current.provider,
|
|
1324
|
+
config,
|
|
1325
|
+
catalog: state.catalog
|
|
1326
|
+
}));
|
|
1327
|
+
const snapshot = status.kind === "ready" ? state.snapshots[status.key] : void 0;
|
|
1328
|
+
const sourceLabel = status.kind === "ready" ? state.catalog.find((entry) => entry.id === status.sourceId)?.displayName ?? status.sourceId : "";
|
|
1329
|
+
const fullParts = statusParts({
|
|
1330
|
+
segments: describeStatus({
|
|
1331
|
+
sourceLabel,
|
|
1332
|
+
status,
|
|
1333
|
+
snapshot,
|
|
1334
|
+
display: config.display,
|
|
1335
|
+
now
|
|
1336
|
+
}),
|
|
1337
|
+
t,
|
|
1338
|
+
now,
|
|
1339
|
+
...sourceLabel === "" ? {} : { sourceLabel },
|
|
1340
|
+
...status.kind === "ready" ? { modeLabel: status.mode === "api" ? t("modeApi") : t("modeCodingPlan") } : {}
|
|
1341
|
+
});
|
|
1342
|
+
if (fullParts.length === 0) return null;
|
|
1343
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1344
|
+
"data-usage-state": "dock",
|
|
1345
|
+
style: DOCK_STYLE,
|
|
1346
|
+
children: fullParts.map((part, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [index > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1347
|
+
style: SEPARATOR_STYLE,
|
|
1348
|
+
"aria-hidden": "true",
|
|
1349
|
+
children: "·"
|
|
1350
|
+
}) : null, withTooltip(renderPart(part, t, index), part.tooltip, index)] }, index))
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
//#endregion
|
|
1354
|
+
//#region src/client/locales.ts
|
|
1355
|
+
const LOCALE_NS = "usage-state";
|
|
1356
|
+
const zh = {
|
|
1357
|
+
nav: "用量状态",
|
|
1358
|
+
title: "用量状态",
|
|
1359
|
+
intro: "为每个模型选择显示账户余额(API 模式)或套餐额度(Coding Plan 模式)。数据由宿主定时刷新,密钥存放在 DSH 凭据库中,本插件不保存明文。",
|
|
1360
|
+
refreshNow: "立即刷新",
|
|
1361
|
+
refreshing: "刷新中…",
|
|
1362
|
+
lastChecked: "更新于 {time}",
|
|
1363
|
+
neverChecked: "尚未获取",
|
|
1364
|
+
close: "关闭",
|
|
1365
|
+
unavailable: "当前连接不支持读写设置。",
|
|
1366
|
+
empty: "还没有发现任何供应商。请先在 DSH 里配置模型供应商。",
|
|
1367
|
+
sectionProviders: "供应商",
|
|
1368
|
+
sectionProvidersHint: "余额与额度是账户级的,所以每个供应商只需设置一次:默认「自动」会识别出该用哪个数据源并直接生效。",
|
|
1369
|
+
sectionDisplay: "显示",
|
|
1370
|
+
modeAuto: "自动",
|
|
1371
|
+
modeApi: "API 余额",
|
|
1372
|
+
modeCodingPlan: "Coding Plan",
|
|
1373
|
+
modeHidden: "隐藏",
|
|
1374
|
+
modeUnsupported: "该数据源不支持此模式",
|
|
1375
|
+
moveUp: "上移",
|
|
1376
|
+
moveDown: "下移",
|
|
1377
|
+
sourceLabel: "数据源",
|
|
1378
|
+
sourceAuto: "自动",
|
|
1379
|
+
detectedAs: "自动识别为 {target}",
|
|
1380
|
+
showsAs: "显示 {target}",
|
|
1381
|
+
needsEndpoint: "需要先填写接口地址",
|
|
1382
|
+
unknownSource: "没有可用的数据源",
|
|
1383
|
+
notConfigured: "未配置",
|
|
1384
|
+
advanced: "高级",
|
|
1385
|
+
modelsPrefix: "模型:{list}",
|
|
1386
|
+
noModels: "(目录里没有模型)",
|
|
1387
|
+
baseUrl: "接口地址",
|
|
1388
|
+
baseUrlPlaceholder: "https://…",
|
|
1389
|
+
baseUrlRequired: "此数据源必须填写你自己实例的接口地址",
|
|
1390
|
+
apiKeyRef: "凭据名(可选)",
|
|
1391
|
+
apiKeyRefHint: "留空则自动探测;填写后优先使用该凭据名。",
|
|
1392
|
+
credential: "密钥",
|
|
1393
|
+
credentialConfigured: "已配置(来源:{source})",
|
|
1394
|
+
credentialMissing: "未配置",
|
|
1395
|
+
credentialLocked: "由环境变量提供,无法在此修改",
|
|
1396
|
+
credentialPlaceholder: "粘贴密钥…",
|
|
1397
|
+
credentialSave: "保存",
|
|
1398
|
+
credentialClear: "清除",
|
|
1399
|
+
credentialSaved: "已保存",
|
|
1400
|
+
credentialFailed: "保存失败:{message}",
|
|
1401
|
+
credentialHint: "密钥写入 DSH 凭据库(~/.dsh/.credentials.yaml),不会存进本插件。",
|
|
1402
|
+
intervalMinutes: "空闲刷新间隔(分钟)",
|
|
1403
|
+
thresholdWarn: "黄色阈值(已用 %)",
|
|
1404
|
+
thresholdCritical: "红色阈值(已用 %)",
|
|
1405
|
+
progressBar: "显示进度条",
|
|
1406
|
+
refreshFailed: "刷新失败:{message}",
|
|
1407
|
+
"state.loading": "读取中…",
|
|
1408
|
+
"state.unconfigured": "未配置",
|
|
1409
|
+
"state.unsupported": "模式不支持",
|
|
1410
|
+
"state.needs-endpoint": "需要先填写接口地址",
|
|
1411
|
+
"state.error": "获取失败",
|
|
1412
|
+
"error.config": "缺少密钥或接口地址",
|
|
1413
|
+
"error.auth": "密钥无效",
|
|
1414
|
+
"error.http": "接口返回错误",
|
|
1415
|
+
"error.network": "网络不可达",
|
|
1416
|
+
"error.parse": "返回内容无法解析",
|
|
1417
|
+
"error.unknown": "未知错误",
|
|
1418
|
+
staleHint: "当前显示的是上一次成功获取的值",
|
|
1419
|
+
staleAgo: "{age} 前",
|
|
1420
|
+
tipSource: "数据源 {source}",
|
|
1421
|
+
tipMode: "模式 {mode}",
|
|
1422
|
+
tipUpdated: "上次成功更新 {age} 前",
|
|
1423
|
+
tipResets: "重置于 {time}",
|
|
1424
|
+
tipBalanceSplit: "赠送 {granted} · 充值 {toppedUp}",
|
|
1425
|
+
tipMore: "悬停查看详情",
|
|
1426
|
+
"window.5h": "5h",
|
|
1427
|
+
"window.1d": "1d",
|
|
1428
|
+
"window.7d": "7d",
|
|
1429
|
+
"window.30d": "30d"
|
|
1430
|
+
};
|
|
1431
|
+
const en = {
|
|
1432
|
+
nav: "Usage state",
|
|
1433
|
+
title: "Usage state",
|
|
1434
|
+
intro: "Choose what to show for each model: account balance (API mode) or coding-plan quota (Coding Plan mode). The host refreshes on a timer, and keys live in the DSH credential store — this plugin never keeps a plaintext copy.",
|
|
1435
|
+
refreshNow: "Refresh now",
|
|
1436
|
+
refreshing: "Refreshing…",
|
|
1437
|
+
lastChecked: "Updated {time}",
|
|
1438
|
+
neverChecked: "Not fetched yet",
|
|
1439
|
+
close: "Close",
|
|
1440
|
+
unavailable: "This connection does not serve settings.",
|
|
1441
|
+
empty: "No providers found yet. Configure an LLM provider in DSH first.",
|
|
1442
|
+
sectionProviders: "Providers",
|
|
1443
|
+
sectionProvidersHint: "Balance and quota are account-level, so one setting per provider is enough. “Auto” detects which data source to use and works right away.",
|
|
1444
|
+
sectionDisplay: "Display",
|
|
1445
|
+
modeAuto: "Auto",
|
|
1446
|
+
modeApi: "API balance",
|
|
1447
|
+
modeCodingPlan: "Coding plan",
|
|
1448
|
+
modeHidden: "Hidden",
|
|
1449
|
+
modeUnsupported: "This data source does not serve that mode",
|
|
1450
|
+
moveUp: "Move up",
|
|
1451
|
+
moveDown: "Move down",
|
|
1452
|
+
sourceLabel: "Source",
|
|
1453
|
+
sourceAuto: "Auto",
|
|
1454
|
+
detectedAs: "Detected {target}",
|
|
1455
|
+
showsAs: "Shows {target}",
|
|
1456
|
+
needsEndpoint: "Needs an endpoint first",
|
|
1457
|
+
unknownSource: "No data source available",
|
|
1458
|
+
notConfigured: "Not configured",
|
|
1459
|
+
advanced: "Advanced",
|
|
1460
|
+
modelsPrefix: "Models: {list}",
|
|
1461
|
+
noModels: "(no models in the catalog)",
|
|
1462
|
+
baseUrl: "Endpoint",
|
|
1463
|
+
baseUrlPlaceholder: "https://…",
|
|
1464
|
+
baseUrlRequired: "This data source needs the endpoint of your own instance",
|
|
1465
|
+
apiKeyRef: "Credential name (optional)",
|
|
1466
|
+
apiKeyRefHint: "Leave blank to auto-detect; when set, this ref is tried first.",
|
|
1467
|
+
credential: "API key",
|
|
1468
|
+
credentialConfigured: "Configured ({source})",
|
|
1469
|
+
credentialMissing: "Not configured",
|
|
1470
|
+
credentialLocked: "Provided by an environment variable; not editable here",
|
|
1471
|
+
credentialPlaceholder: "Paste the key…",
|
|
1472
|
+
credentialSave: "Save",
|
|
1473
|
+
credentialClear: "Clear",
|
|
1474
|
+
credentialSaved: "Saved",
|
|
1475
|
+
credentialFailed: "Could not save: {message}",
|
|
1476
|
+
credentialHint: "The key is written to the DSH credential store (~/.dsh/.credentials.yaml), not to this plugin.",
|
|
1477
|
+
intervalMinutes: "Idle refresh interval (minutes)",
|
|
1478
|
+
thresholdWarn: "Amber threshold (used %)",
|
|
1479
|
+
thresholdCritical: "Red threshold (used %)",
|
|
1480
|
+
progressBar: "Show progress bar",
|
|
1481
|
+
refreshFailed: "Refresh failed: {message}",
|
|
1482
|
+
"state.loading": "Reading…",
|
|
1483
|
+
"state.unconfigured": "Not configured",
|
|
1484
|
+
"state.unsupported": "Mode not supported",
|
|
1485
|
+
"state.needs-endpoint": "Needs an endpoint first",
|
|
1486
|
+
"state.error": "Unavailable",
|
|
1487
|
+
"error.config": "Missing key or endpoint",
|
|
1488
|
+
"error.auth": "Key rejected",
|
|
1489
|
+
"error.http": "The endpoint returned an error",
|
|
1490
|
+
"error.network": "Network unreachable",
|
|
1491
|
+
"error.parse": "The response could not be parsed",
|
|
1492
|
+
"error.unknown": "Unknown error",
|
|
1493
|
+
staleHint: "Showing the last value that was fetched successfully",
|
|
1494
|
+
staleAgo: "{age} ago",
|
|
1495
|
+
tipSource: "Source {source}",
|
|
1496
|
+
tipMode: "Mode {mode}",
|
|
1497
|
+
tipUpdated: "Last successful update {age} ago",
|
|
1498
|
+
tipResets: "Resets at {time}",
|
|
1499
|
+
tipBalanceSplit: "Granted {granted} · Topped up {toppedUp}",
|
|
1500
|
+
tipMore: "Hover for details",
|
|
1501
|
+
"window.5h": "5h",
|
|
1502
|
+
"window.1d": "1d",
|
|
1503
|
+
"window.7d": "7d",
|
|
1504
|
+
"window.30d": "30d"
|
|
1505
|
+
};
|
|
1506
|
+
//#endregion
|
|
1507
|
+
//#region src/client/remote.ts
|
|
1508
|
+
/**
|
|
1509
|
+
* Reading a remote namespace off the client context.
|
|
1510
|
+
*
|
|
1511
|
+
* `ctx.remote.<namespace>` is a service proxy: plain property access throws
|
|
1512
|
+
* `cannot get property "remote.<ns>" without inject` unless the name is declared
|
|
1513
|
+
* in the plugin's `inject` list. That cannot work for the namespace this plugin
|
|
1514
|
+
* contributes itself (`remote.usageState`) — it only exists after `$mount`, so
|
|
1515
|
+
* declaring it would deadlock. `ctx.get()` is the inject-free accessor and returns
|
|
1516
|
+
* `undefined` while the namespace is absent.
|
|
1517
|
+
*/
|
|
1518
|
+
function remoteService(ctx, name) {
|
|
1519
|
+
try {
|
|
1520
|
+
const service = ctx.get(name);
|
|
1521
|
+
return service === void 0 ? void 0 : service;
|
|
1522
|
+
} catch {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
//#endregion
|
|
1527
|
+
//#region src/client/slots.ts
|
|
1528
|
+
const STATUS_LINE_SLOTS = [{
|
|
1529
|
+
name: "conversation.composer.dock",
|
|
1530
|
+
id: "usage-state",
|
|
1531
|
+
order: 1,
|
|
1532
|
+
locale: "usage-state"
|
|
1533
|
+
}];
|
|
1534
|
+
//#endregion
|
|
1535
|
+
//#region src/client/store.ts
|
|
1536
|
+
/** Flatten the DSH model catalog into the rows the settings page lists. */
|
|
1537
|
+
function flattenCatalog(catalog) {
|
|
1538
|
+
if (catalog === void 0) return [];
|
|
1539
|
+
const rows = [];
|
|
1540
|
+
for (const group of catalog.groups) for (const model of group.models) rows.push({
|
|
1541
|
+
provider: group.id,
|
|
1542
|
+
providerName: group.name,
|
|
1543
|
+
model: model.id,
|
|
1544
|
+
name: model.name
|
|
1545
|
+
});
|
|
1546
|
+
return rows;
|
|
1547
|
+
}
|
|
1548
|
+
function messageOf(error) {
|
|
1549
|
+
return error instanceof Error ? error.message : String(error);
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* The registry view of a catalog result, or `undefined` when it does not carry one
|
|
1553
|
+
* (a host older than `routableProviders`). Unknown is not the same as empty: the
|
|
1554
|
+
* settings page keeps stored entries visible while the registry is unknown, so a
|
|
1555
|
+
* failed or unavailable catalog can never hide configuration that needs clearing.
|
|
1556
|
+
*/
|
|
1557
|
+
function registryOf(catalog) {
|
|
1558
|
+
if (catalog?.routableProviders === void 0) return void 0;
|
|
1559
|
+
return {
|
|
1560
|
+
routable: [...catalog.routableProviders],
|
|
1561
|
+
failed: (catalog.failures ?? []).map((failure) => failure.id)
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* The browser's mirror of the host's readings.
|
|
1566
|
+
*
|
|
1567
|
+
* Two rules keep the status line honest: a failed refresh never clears what is
|
|
1568
|
+
* already displayed (it only records why), and concurrent callers share one
|
|
1569
|
+
* in-flight call so a component re-render cannot multiply RPC traffic.
|
|
1570
|
+
*/
|
|
1571
|
+
var UsageStateClientStore = class {
|
|
1572
|
+
deps;
|
|
1573
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1574
|
+
state = {
|
|
1575
|
+
status: "idle",
|
|
1576
|
+
catalog: [],
|
|
1577
|
+
snapshots: {},
|
|
1578
|
+
credentials: {},
|
|
1579
|
+
checkedAt: void 0,
|
|
1580
|
+
models: [],
|
|
1581
|
+
modelRegistry: void 0,
|
|
1582
|
+
error: void 0,
|
|
1583
|
+
credentialsError: void 0,
|
|
1584
|
+
modelsError: void 0
|
|
1585
|
+
};
|
|
1586
|
+
inflight;
|
|
1587
|
+
constructor(deps) {
|
|
1588
|
+
this.deps = deps;
|
|
1589
|
+
}
|
|
1590
|
+
getSnapshot() {
|
|
1591
|
+
return this.state;
|
|
1592
|
+
}
|
|
1593
|
+
subscribe(listener) {
|
|
1594
|
+
this.listeners.add(listener);
|
|
1595
|
+
return () => {
|
|
1596
|
+
this.listeners.delete(listener);
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
publish(patch) {
|
|
1600
|
+
this.state = {
|
|
1601
|
+
...this.state,
|
|
1602
|
+
...patch
|
|
1603
|
+
};
|
|
1604
|
+
for (const listener of [...this.listeners]) listener();
|
|
1605
|
+
}
|
|
1606
|
+
refresh(force = false) {
|
|
1607
|
+
if (this.inflight !== void 0) return this.inflight;
|
|
1608
|
+
this.publish({ status: "loading" });
|
|
1609
|
+
const run = (async () => {
|
|
1610
|
+
try {
|
|
1611
|
+
const result = await this.deps.getState(force);
|
|
1612
|
+
if (!result.ok) {
|
|
1613
|
+
this.publish({
|
|
1614
|
+
status: "error",
|
|
1615
|
+
error: result.error.message
|
|
1616
|
+
});
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
this.publish({
|
|
1620
|
+
status: "ready",
|
|
1621
|
+
catalog: result.value.sources,
|
|
1622
|
+
snapshots: result.value.snapshots,
|
|
1623
|
+
checkedAt: result.value.checkedAt,
|
|
1624
|
+
error: void 0
|
|
1625
|
+
});
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
this.publish({
|
|
1628
|
+
status: "error",
|
|
1629
|
+
error: messageOf(error)
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
})().finally(() => {
|
|
1633
|
+
this.inflight = void 0;
|
|
1634
|
+
});
|
|
1635
|
+
this.inflight = run;
|
|
1636
|
+
return run;
|
|
1637
|
+
}
|
|
1638
|
+
/** Load the model catalog once per settings-page visit. */
|
|
1639
|
+
async refreshModels() {
|
|
1640
|
+
if (this.deps.modelCatalog === void 0) return;
|
|
1641
|
+
try {
|
|
1642
|
+
const result = await this.deps.modelCatalog();
|
|
1643
|
+
if (!result.ok) {
|
|
1644
|
+
this.publish({
|
|
1645
|
+
modelsError: result.error.message,
|
|
1646
|
+
modelRegistry: void 0
|
|
1647
|
+
});
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
this.publish({
|
|
1651
|
+
models: flattenCatalog(result.value),
|
|
1652
|
+
modelRegistry: registryOf(result.value),
|
|
1653
|
+
modelsError: void 0
|
|
1654
|
+
});
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
this.publish({
|
|
1657
|
+
modelsError: messageOf(error),
|
|
1658
|
+
modelRegistry: void 0
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
async refreshCredentials() {
|
|
1663
|
+
try {
|
|
1664
|
+
const result = await this.deps.describeCredentials();
|
|
1665
|
+
if (!result.ok) {
|
|
1666
|
+
this.publish({ credentialsError: result.error.message });
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
this.publish({
|
|
1670
|
+
credentials: result.value.credentials,
|
|
1671
|
+
credentialsError: void 0
|
|
1672
|
+
});
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
this.publish({ credentialsError: messageOf(error) });
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
//#endregion
|
|
1679
|
+
//#region src/client/index.tsx
|
|
1680
|
+
const inject = [
|
|
1681
|
+
"slots",
|
|
1682
|
+
"locale",
|
|
1683
|
+
"settingsScope",
|
|
1684
|
+
"remote",
|
|
1685
|
+
"remote.session",
|
|
1686
|
+
"remote.credentials"
|
|
1687
|
+
];
|
|
1688
|
+
const USAGE_STATE_NS = "usage-state";
|
|
1689
|
+
const POLL_INTERVAL_MS = 3e4;
|
|
1690
|
+
/** Hand-rolled codecs: the browser bundle must not carry zod. */
|
|
1691
|
+
const booleanOrUndefined = {
|
|
1692
|
+
mode: "strict",
|
|
1693
|
+
typeSymbol: "dsh-usage-state#Force",
|
|
1694
|
+
schema: { parse: (value) => value === void 0 ? void 0 : value === true }
|
|
1695
|
+
};
|
|
1696
|
+
const srcJson = { mode: "src-json" };
|
|
1697
|
+
/** Must mirror `src/host/typert.ts`: the wire endpoint is `<namespace>/<method>`. */
|
|
1698
|
+
const CONTRIBUTION = {
|
|
1699
|
+
package: "dsh-usage-state",
|
|
1700
|
+
descriptors: [{
|
|
1701
|
+
id: "dsh-usage-state#usageState/getState",
|
|
1702
|
+
service: "usageState",
|
|
1703
|
+
namespace: "usageState",
|
|
1704
|
+
method: "getState",
|
|
1705
|
+
invocation: { kind: "direct" },
|
|
1706
|
+
parameters: [{
|
|
1707
|
+
name: "force",
|
|
1708
|
+
wire: "force",
|
|
1709
|
+
source: "json",
|
|
1710
|
+
acceptsUndefined: true,
|
|
1711
|
+
codec: booleanOrUndefined
|
|
1712
|
+
}],
|
|
1713
|
+
result: srcJson
|
|
1714
|
+
}, {
|
|
1715
|
+
id: "dsh-usage-state#usageState/describeCredentials",
|
|
1716
|
+
service: "usageState",
|
|
1717
|
+
namespace: "usageState",
|
|
1718
|
+
method: "describeCredentials",
|
|
1719
|
+
invocation: { kind: "direct" },
|
|
1720
|
+
parameters: [],
|
|
1721
|
+
result: srcJson
|
|
1722
|
+
}]
|
|
1723
|
+
};
|
|
1724
|
+
function apply(ctx) {
|
|
1725
|
+
ctx.effect(() => ctx.locale.register(LOCALE_NS, {
|
|
1726
|
+
zh,
|
|
1727
|
+
en
|
|
1728
|
+
}), "dsh-usage-state: dictionaries");
|
|
1729
|
+
const t = ctx.locale.bind(LOCALE_NS);
|
|
1730
|
+
const settings = ctx.settingsScope.bind({
|
|
1731
|
+
namespace: USAGE_STATE_NS,
|
|
1732
|
+
decode: (section) => normalizeConfig(section)
|
|
1733
|
+
});
|
|
1734
|
+
const usageStateRemote = () => remoteService(ctx, "remote.usageState");
|
|
1735
|
+
const store = new UsageStateClientStore({
|
|
1736
|
+
getState: async (force) => {
|
|
1737
|
+
const service = usageStateRemote();
|
|
1738
|
+
if (service === void 0) return {
|
|
1739
|
+
ok: false,
|
|
1740
|
+
error: { message: "remote not mounted" }
|
|
1741
|
+
};
|
|
1742
|
+
return service.getState(force);
|
|
1743
|
+
},
|
|
1744
|
+
describeCredentials: async () => {
|
|
1745
|
+
const service = usageStateRemote();
|
|
1746
|
+
if (service === void 0) return {
|
|
1747
|
+
ok: false,
|
|
1748
|
+
error: { message: "remote not mounted" }
|
|
1749
|
+
};
|
|
1750
|
+
return service.describeCredentials();
|
|
1751
|
+
},
|
|
1752
|
+
modelCatalog: async () => {
|
|
1753
|
+
const session = remoteService(ctx, "remote.session");
|
|
1754
|
+
if (session === void 0) return {
|
|
1755
|
+
ok: false,
|
|
1756
|
+
error: { message: "model catalog unavailable" }
|
|
1757
|
+
};
|
|
1758
|
+
return session.modelCatalog();
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
ctx.effect(() => {
|
|
1762
|
+
let dispose;
|
|
1763
|
+
let cancelled = false;
|
|
1764
|
+
ctx.remote.$mount(CONTRIBUTION).then((off) => {
|
|
1765
|
+
if (cancelled) {
|
|
1766
|
+
off();
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
dispose = off;
|
|
1770
|
+
store.refresh(false);
|
|
1771
|
+
store.refreshModels();
|
|
1772
|
+
store.refreshCredentials();
|
|
1773
|
+
}, () => void 0);
|
|
1774
|
+
return () => {
|
|
1775
|
+
cancelled = true;
|
|
1776
|
+
dispose?.();
|
|
1777
|
+
};
|
|
1778
|
+
}, "dsh-usage-state: remote contribution");
|
|
1779
|
+
ctx.effect(() => {
|
|
1780
|
+
const timer = setInterval(() => {
|
|
1781
|
+
store.refresh(false);
|
|
1782
|
+
if (store.getSnapshot().models.length === 0) store.refreshModels();
|
|
1783
|
+
}, POLL_INTERVAL_MS);
|
|
1784
|
+
const disposers = [
|
|
1785
|
+
ctx.on("api-session/status", (...args) => {
|
|
1786
|
+
if (args[1] === false) store.refresh(false);
|
|
1787
|
+
}),
|
|
1788
|
+
ctx.on("connection/reset", () => {
|
|
1789
|
+
store.refresh(true);
|
|
1790
|
+
}),
|
|
1791
|
+
settings.subscribe(() => {
|
|
1792
|
+
store.refresh(false);
|
|
1793
|
+
})
|
|
1794
|
+
];
|
|
1795
|
+
return () => {
|
|
1796
|
+
clearInterval(timer);
|
|
1797
|
+
for (const dispose of disposers) dispose();
|
|
1798
|
+
};
|
|
1799
|
+
}, "dsh-usage-state: refresh loop");
|
|
1800
|
+
const seat = () => ({
|
|
1801
|
+
usageState: store,
|
|
1802
|
+
settings
|
|
1803
|
+
});
|
|
1804
|
+
for (const slot of STATUS_LINE_SLOTS) ctx.slots.inject(slot.name, () => ctx.slots.register({
|
|
1805
|
+
name: slot.name,
|
|
1806
|
+
id: slot.id,
|
|
1807
|
+
order: slot.order,
|
|
1808
|
+
locale: slot.locale,
|
|
1809
|
+
inject: seat
|
|
1810
|
+
}, (props) => (0, react.createElement)(StatusLine, props)));
|
|
1811
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1812
|
+
name: "settings.section",
|
|
1813
|
+
id: "usage-state",
|
|
1814
|
+
order: 200,
|
|
1815
|
+
label: () => t("nav"),
|
|
1816
|
+
locale: LOCALE_NS,
|
|
1817
|
+
inject: () => ({
|
|
1818
|
+
...seat(),
|
|
1819
|
+
credentials: remoteService(ctx, "remote.credentials")
|
|
1820
|
+
})
|
|
1821
|
+
}, SettingsSection));
|
|
1822
|
+
}
|
|
1823
|
+
//#endregion
|
|
1824
|
+
exports.apply = apply;
|
|
1825
|
+
exports.inject = inject;
|
|
1826
|
+
return module.exports;
|
|
1827
|
+
}
|
|
1828
|
+
});
|