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/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
+ });