dsh-daruma 0.1.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,497 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-daruma",
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 react_jsx_runtime = require("react/jsx-runtime");
9
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ //#region lib/client/locales.js
11
+ /** zh/en dictionaries for the daruma client UI. */
12
+ const zh = {
13
+ status: "渠道状态",
14
+ current: "当前",
15
+ backup: "备用",
16
+ backupNone: "未设置",
17
+ backupManage: "备用渠道",
18
+ backupManageHint: "从候选列表选择备用渠道",
19
+ failoverCount: "已切换",
20
+ lastFailover: "上次切换",
21
+ healthy: "健康",
22
+ cooldown: "冷却中",
23
+ probe: "探测中",
24
+ refresh: "刷新",
25
+ close: "关闭",
26
+ clearBackup: "清除备用",
27
+ provider: "提供方",
28
+ providerPlaceholder: "provider,如 mt",
29
+ loadCandidates: "加载候选",
30
+ loading: "加载中…",
31
+ candidates: "候选模型",
32
+ setBackup: "设为备用",
33
+ currentBackup: "当前备用",
34
+ currentBackupMark: "备用",
35
+ noCandidates: "未加载到候选模型",
36
+ empty: "暂无渠道数据"
37
+ };
38
+ const en = {
39
+ status: "Channel status",
40
+ current: "Current",
41
+ backup: "Backup",
42
+ backupNone: "none",
43
+ backupManage: "Backup channel",
44
+ backupManageHint: "Pick a backup channel from the candidate list",
45
+ failoverCount: "failovers",
46
+ lastFailover: "last failover",
47
+ healthy: "healthy",
48
+ cooldown: "cooling",
49
+ probe: "probing",
50
+ refresh: "Refresh",
51
+ close: "Close",
52
+ clearBackup: "Clear backup",
53
+ provider: "Provider",
54
+ providerPlaceholder: "provider, e.g. mt",
55
+ loadCandidates: "Load candidates",
56
+ loading: "Loading…",
57
+ candidates: "Candidate models",
58
+ setBackup: "Set as backup",
59
+ currentBackup: "Current backup",
60
+ currentBackupMark: "backup",
61
+ noCandidates: "No candidates loaded",
62
+ empty: "No channel data yet"
63
+ };
64
+ //#endregion
65
+ //#region lib/client/api.js
66
+ /**
67
+ * Client-side wire types and RPC wrapper for the `/dsh-daruma` channel.
68
+ * Structural types only — no monorepo-internal imports.
69
+ */
70
+ function createApi(rpc) {
71
+ const call = async (endpoint, payload) => {
72
+ const result = await rpc(endpoint, payload);
73
+ if (!result.ok) throw new Error(result.error?.message ?? `daruma rpc ${endpoint} failed`);
74
+ return result.value;
75
+ };
76
+ return {
77
+ status: () => call("status"),
78
+ listCandidates: (provider) => call("listCandidates", { provider }),
79
+ setBackup: async (provider, model) => {
80
+ await call("setBackup", {
81
+ provider,
82
+ model
83
+ });
84
+ },
85
+ clearBackup: async () => {
86
+ await call("clearBackup");
87
+ }
88
+ };
89
+ }
90
+ //#endregion
91
+ //#region lib/client/BackupPanel.js
92
+ /**
93
+ * BackupChannelPanel: list candidate models for a provider and pick one as
94
+ * the failover backup. Rendered inside a primitives Modal.
95
+ *
96
+ * No probing happens here anymore — the panel is a plain picker. Channel
97
+ * health still updates from real traffic via the recovery engine.
98
+ */
99
+ function deriveProvider(status) {
100
+ if (status === null) return "";
101
+ if (status.current !== null) return status.current.split("::")[0] ?? "";
102
+ if (status.backup !== null) return status.backup.provider;
103
+ return status.providers[0] ?? "";
104
+ }
105
+ const sectionStyle = {
106
+ display: "flex",
107
+ flexDirection: "column",
108
+ gap: "8px",
109
+ padding: "12px 0",
110
+ fontSize: "13px"
111
+ };
112
+ const rowStyle = {
113
+ display: "flex",
114
+ alignItems: "center",
115
+ gap: "8px"
116
+ };
117
+ const noteStyle = {
118
+ color: "var(--dsw-text-tertiary, #999)",
119
+ fontSize: "12px"
120
+ };
121
+ /** Clamp a long provider/model name so the row never wraps. */
122
+ const backupPillStyle = {
123
+ flex: "1 1 auto",
124
+ minWidth: 0,
125
+ overflow: "hidden"
126
+ };
127
+ const ellipsisStyle = {
128
+ minWidth: 0,
129
+ overflow: "hidden",
130
+ textOverflow: "ellipsis",
131
+ whiteSpace: "nowrap"
132
+ };
133
+ const listStyle = {
134
+ maxHeight: 300,
135
+ overflowY: "auto",
136
+ display: "flex",
137
+ flexDirection: "column",
138
+ gap: 4,
139
+ contain: "content",
140
+ willChange: "scroll-position",
141
+ overscrollBehavior: "contain"
142
+ };
143
+ /** Skip rendering rows outside the viewport while scrolling. */
144
+ const candidateRowStyle = {
145
+ ...rowStyle,
146
+ contentVisibility: "auto",
147
+ containIntrinsicSize: "28px"
148
+ };
149
+ /** One candidate row, memoized so selection changes only re-render it. */
150
+ const CandidateRow = (0, react.memo)(function CandidateRow(props) {
151
+ const { candidate, isBackup, t, onSetBackup } = props;
152
+ return (0, react_jsx_runtime.jsxs)("div", {
153
+ style: candidateRowStyle,
154
+ children: [
155
+ (0, react_jsx_runtime.jsx)("span", { style: {
156
+ width: 14,
157
+ flexShrink: 0
158
+ } }),
159
+ (0, react_jsx_runtime.jsx)("span", {
160
+ style: {
161
+ flex: 1,
162
+ minWidth: 0,
163
+ overflow: "hidden",
164
+ textOverflow: "ellipsis",
165
+ whiteSpace: "nowrap"
166
+ },
167
+ children: candidate.model
168
+ }),
169
+ isBackup && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
170
+ active: true,
171
+ children: t("currentBackupMark")
172
+ }),
173
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
174
+ size: "sm",
175
+ variant: "ghost",
176
+ disabled: isBackup,
177
+ onClick: () => onSetBackup(candidate.model),
178
+ children: t("setBackup")
179
+ })
180
+ ]
181
+ });
182
+ });
183
+ function BackupPanel(props) {
184
+ const { api, t, status, onClose } = props;
185
+ const [provider, setProvider] = (0, react.useState)(deriveProvider(status));
186
+ const [candidates, setCandidates] = (0, react.useState)(null);
187
+ const [loading, setLoading] = (0, react.useState)(false);
188
+ const [busy, setBusy] = (0, react.useState)(null);
189
+ const [error, setError] = (0, react.useState)(null);
190
+ const [backupOverride, setBackupOverride] = (0, react.useState)(void 0);
191
+ const backupShown = backupOverride !== void 0 ? backupOverride : status?.backup ?? null;
192
+ const backupModel = (0, react.useMemo)(() => backupShown !== null && backupShown.provider === provider ? backupShown.model : null, [backupShown, provider]);
193
+ const loadCandidates = async () => {
194
+ if (provider === "") return;
195
+ setLoading(true);
196
+ setError(null);
197
+ try {
198
+ setCandidates(await api.listCandidates(provider));
199
+ } catch (cause) {
200
+ setError(cause instanceof Error ? cause.message : String(cause));
201
+ } finally {
202
+ setLoading(false);
203
+ }
204
+ };
205
+ (0, react.useEffect)(() => {
206
+ if (provider !== "") loadCandidates();
207
+ }, [provider]);
208
+ (0, react.useEffect)(() => {
209
+ setBackupOverride(void 0);
210
+ }, [status]);
211
+ const setBackup = async (model) => {
212
+ setBusy(model);
213
+ setError(null);
214
+ try {
215
+ await api.setBackup(provider, model);
216
+ setBackupOverride({
217
+ provider,
218
+ model
219
+ });
220
+ } catch (cause) {
221
+ setError(cause instanceof Error ? cause.message : String(cause));
222
+ } finally {
223
+ setBusy(null);
224
+ }
225
+ };
226
+ const clearBackup = async () => {
227
+ setError(null);
228
+ try {
229
+ await api.clearBackup();
230
+ setBackupOverride(null);
231
+ } catch (cause) {
232
+ setError(cause instanceof Error ? cause.message : String(cause));
233
+ }
234
+ };
235
+ const handleSetBackup = (0, react.useCallback)((model) => void setBackup(model), [provider]);
236
+ return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
237
+ open: true,
238
+ onClose,
239
+ title: t("backupManage"),
240
+ className: "daruma-wide-dialog",
241
+ closeLabel: t("close"),
242
+ description: t("backupManageHint"),
243
+ footer: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
244
+ variant: "primary",
245
+ onClick: onClose,
246
+ children: t("close")
247
+ }),
248
+ children: (0, react_jsx_runtime.jsxs)("div", {
249
+ style: sectionStyle,
250
+ children: [
251
+ status !== null && status.failoverCount > 0 && (0, react_jsx_runtime.jsxs)("div", {
252
+ style: noteStyle,
253
+ children: [
254
+ t("failoverCount"),
255
+ ": ",
256
+ status.failoverCount,
257
+ status.failoverHistory.length > 0 && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
258
+ " · ",
259
+ t("lastFailover"),
260
+ ": ",
261
+ status.failoverHistory.map((h) => `${h.from} → ${h.to}`).join(" · ")
262
+ ] })
263
+ ]
264
+ }),
265
+ backupShown !== null && (0, react_jsx_runtime.jsxs)("div", {
266
+ style: rowStyle,
267
+ children: [
268
+ (0, react_jsx_runtime.jsxs)("span", {
269
+ style: { flexShrink: 0 },
270
+ children: [t("currentBackup"), ":"]
271
+ }),
272
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
273
+ active: true,
274
+ style: backupPillStyle,
275
+ title: `${backupShown.provider}/${backupShown.model}`,
276
+ children: (0, react_jsx_runtime.jsxs)("span", {
277
+ style: ellipsisStyle,
278
+ children: [
279
+ backupShown.provider,
280
+ "/",
281
+ backupShown.model
282
+ ]
283
+ })
284
+ }),
285
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
286
+ size: "sm",
287
+ variant: "ghost",
288
+ style: { flexShrink: 0 },
289
+ onClick: () => void clearBackup(),
290
+ children: t("clearBackup")
291
+ })
292
+ ]
293
+ }),
294
+ (0, react_jsx_runtime.jsxs)("div", {
295
+ style: rowStyle,
296
+ children: [(status?.providers ?? []).length > 0 ? (0, react_jsx_runtime.jsx)("select", {
297
+ value: provider,
298
+ disabled: loading,
299
+ onChange: (event) => setProvider(event.currentTarget.value),
300
+ style: {
301
+ flex: 1,
302
+ padding: "4px 8px",
303
+ fontSize: 13
304
+ },
305
+ children: status?.providers.map((name) => (0, react_jsx_runtime.jsx)("option", {
306
+ value: name,
307
+ children: name
308
+ }, name))
309
+ }) : (0, react_jsx_runtime.jsx)("input", {
310
+ value: provider,
311
+ placeholder: t("providerPlaceholder"),
312
+ onChange: (event) => setProvider(event.currentTarget.value),
313
+ style: {
314
+ flex: 1,
315
+ padding: "4px 8px",
316
+ fontSize: 13
317
+ }
318
+ }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
319
+ size: "sm",
320
+ variant: "outline",
321
+ onClick: () => void loadCandidates(),
322
+ disabled: provider === "" || loading,
323
+ children: loading ? t("loading") : t("loadCandidates")
324
+ })]
325
+ }),
326
+ error !== null && (0, react_jsx_runtime.jsx)("div", {
327
+ style: { color: "var(--dsw-text-danger, #e5484d)" },
328
+ children: error
329
+ }),
330
+ candidates !== null && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
331
+ (0, react_jsx_runtime.jsx)("div", {
332
+ style: rowStyle,
333
+ children: (0, react_jsx_runtime.jsxs)("span", { children: [
334
+ t("candidates"),
335
+ " (",
336
+ candidates.length,
337
+ ")"
338
+ ] })
339
+ }),
340
+ candidates.length === 0 && (0, react_jsx_runtime.jsx)("div", {
341
+ style: noteStyle,
342
+ children: t("noCandidates")
343
+ }),
344
+ (0, react_jsx_runtime.jsx)("div", {
345
+ style: listStyle,
346
+ children: candidates.map((candidate) => (0, react_jsx_runtime.jsx)(CandidateRow, {
347
+ candidate,
348
+ isBackup: candidate.model === backupModel,
349
+ t,
350
+ onSetBackup: handleSetBackup
351
+ }, candidate.model))
352
+ })
353
+ ] })
354
+ ]
355
+ })
356
+ });
357
+ }
358
+ //#endregion
359
+ //#region lib/client/StatusDock.js
360
+ /**
361
+ * DarumaStatus: a compact control at the right end of the composer tool row,
362
+ * next to the model selector. Shows overall channel health and the backup
363
+ * selection; clicking it opens the backup-channel panel.
364
+ */
365
+ const controlStyle = {
366
+ display: "inline-flex",
367
+ alignItems: "center",
368
+ gap: 6,
369
+ maxWidth: 220,
370
+ overflow: "hidden",
371
+ textOverflow: "ellipsis",
372
+ whiteSpace: "nowrap"
373
+ };
374
+ /** Overall health: any cooling channel → error, any probe → warning, else done. */
375
+ function overallState(status) {
376
+ if (status === null) return "ongoing";
377
+ if (status.channels.some((c) => c.state === "COOLDOWN")) return "error";
378
+ if (status.channels.some((c) => c.state === "PROBE")) return "warning";
379
+ return "done";
380
+ }
381
+ const iconColor = {
382
+ done: "var(--dsw-text-success, #30a46c)",
383
+ warning: "var(--dsw-text-warning, #f5a524)",
384
+ error: "var(--dsw-text-danger, #e5484d)",
385
+ ongoing: "var(--dsw-text-secondary, #888)"
386
+ };
387
+ /** Status glyph: a check when healthy, a warning otherwise. */
388
+ function StatusIcon(props) {
389
+ return (0, react_jsx_runtime.jsx)("span", {
390
+ style: {
391
+ display: "inline-flex",
392
+ color: iconColor[props.state],
393
+ flexShrink: 0
394
+ },
395
+ children: props.state === "done" ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 14 }) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, { size: 14 })
396
+ });
397
+ }
398
+ function StatusDock(props) {
399
+ const [status, setStatus] = (0, react.useState)(null);
400
+ const [panelOpen, setPanelOpen] = (0, react.useState)(false);
401
+ const load = (0, react.useCallback)(async () => {
402
+ try {
403
+ const value = await props.api.status();
404
+ setStatus(value);
405
+ } catch {}
406
+ }, [props.api]);
407
+ (0, react.useEffect)(() => {
408
+ load();
409
+ const timer = setInterval(() => void load(), 3e4);
410
+ return () => clearInterval(timer);
411
+ }, [load]);
412
+ const t = props.t;
413
+ const backupLabel = status?.backup !== null && status?.backup !== void 0 ? `${status.backup.provider}/${status.backup.model}` : t("backupNone");
414
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Button, {
415
+ size: "sm",
416
+ variant: "ghost",
417
+ title: t("backupManageHint"),
418
+ onClick: () => setPanelOpen(true),
419
+ style: controlStyle,
420
+ children: [(0, react_jsx_runtime.jsx)(StatusIcon, { state: overallState(status) }), (0, react_jsx_runtime.jsxs)("span", {
421
+ style: {
422
+ minWidth: 0,
423
+ overflow: "hidden",
424
+ textOverflow: "ellipsis",
425
+ whiteSpace: "nowrap"
426
+ },
427
+ children: [
428
+ t("backup"),
429
+ ": ",
430
+ backupLabel
431
+ ]
432
+ })]
433
+ }), panelOpen && (0, react_jsx_runtime.jsx)(BackupPanel, {
434
+ api: props.api,
435
+ t,
436
+ status,
437
+ onClose: () => {
438
+ setPanelOpen(false);
439
+ load();
440
+ }
441
+ })] });
442
+ }
443
+ //#endregion
444
+ //#region lib/client/index.js
445
+ /**
446
+ * dsh-daruma client: registers a compact channel-status dock above the
447
+ * composer (conversation.input.dock) with a "backup channel" panel that
448
+ * lists candidate models and picks a backup. Built by tsdown into the
449
+ * __ModuleLoader__ factory bundle at lib/client.js.
450
+ */
451
+ const NS = "daruma";
452
+ const name = "dsh-daruma";
453
+ const inject = ["slots", "locale"];
454
+ function apply(ctx) {
455
+ if (typeof document !== "undefined") {
456
+ const tagId = "daruma-panel-style";
457
+ if (document.querySelector(`style[data-daruma="${tagId}"]`) === null) {
458
+ const style = document.createElement("style");
459
+ style.dataset.daruma = tagId;
460
+ style.textContent = ".daruma-wide-dialog.daruma-wide-dialog { width: min(560px, 100%); }";
461
+ document.head.appendChild(style);
462
+ }
463
+ }
464
+ ctx.effect(() => ctx.locale.register(NS, {
465
+ zh,
466
+ en
467
+ }), "dsh-daruma: dictionaries");
468
+ const t = ctx.locale.bind(NS);
469
+ const connection = ctx.get("connection");
470
+ if (connection === void 0) {
471
+ console.warn("[dsh-daruma] web transport unavailable — client UI disabled");
472
+ return;
473
+ }
474
+ const rpc = (endpoint, payload) => connection.rpc.call("/dsh-daruma", endpoint, payload ?? {});
475
+ const api = createApi(rpc);
476
+ ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
477
+ name: "conversation.input.right",
478
+ id: "daruma-status",
479
+ order: 0,
480
+ inject: () => ({
481
+ api,
482
+ t
483
+ })
484
+ }, () => (0, react.createElement)(StatusDock, {
485
+ api,
486
+ t
487
+ })));
488
+ }
489
+ //#endregion
490
+ exports.apply = apply;
491
+ exports.inject = inject;
492
+ exports.name = name;
493
+ return module.exports;
494
+ }
495
+ });
496
+
497
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":["memo","_jsxs","_jsx","Pill","Button","useState","useMemo","useCallback","Modal","_Fragment","_jsx","IconCheckOutline16","IconWarningOutline16","useState","useCallback","_jsxs","_Fragment","Button","h"],"sources":["client/locales.js","client/api.js","client/BackupPanel.js","client/StatusDock.js","client/index.js"],"sourcesContent":["/** zh/en dictionaries for the daruma client UI. */\nexport const zh = {\n status: '渠道状态',\n current: '当前',\n backup: '备用',\n backupNone: '未设置',\n backupManage: '备用渠道',\n backupManageHint: '从候选列表选择备用渠道',\n failoverCount: '已切换',\n lastFailover: '上次切换',\n healthy: '健康',\n cooldown: '冷却中',\n probe: '探测中',\n refresh: '刷新',\n close: '关闭',\n clearBackup: '清除备用',\n provider: '提供方',\n providerPlaceholder: 'provider,如 mt',\n loadCandidates: '加载候选',\n loading: '加载中…',\n candidates: '候选模型',\n setBackup: '设为备用',\n currentBackup: '当前备用',\n currentBackupMark: '备用',\n noCandidates: '未加载到候选模型',\n empty: '暂无渠道数据',\n};\nexport const en = {\n status: 'Channel status',\n current: 'Current',\n backup: 'Backup',\n backupNone: 'none',\n backupManage: 'Backup channel',\n backupManageHint: 'Pick a backup channel from the candidate list',\n failoverCount: 'failovers',\n lastFailover: 'last failover',\n healthy: 'healthy',\n cooldown: 'cooling',\n probe: 'probing',\n refresh: 'Refresh',\n close: 'Close',\n clearBackup: 'Clear backup',\n provider: 'Provider',\n providerPlaceholder: 'provider, e.g. mt',\n loadCandidates: 'Load candidates',\n loading: 'Loading…',\n candidates: 'Candidate models',\n setBackup: 'Set as backup',\n currentBackup: 'Current backup',\n currentBackupMark: 'backup',\n noCandidates: 'No candidates loaded',\n empty: 'No channel data yet',\n};\n//# sourceMappingURL=locales.js.map","/**\n * Client-side wire types and RPC wrapper for the `/dsh-daruma` channel.\n * Structural types only — no monorepo-internal imports.\n */\nexport function createApi(rpc) {\n const call = async (endpoint, payload) => {\n const result = (await rpc(endpoint, payload));\n if (!result.ok) {\n throw new Error(result.error?.message ?? `daruma rpc ${endpoint} failed`);\n }\n return result.value;\n };\n return {\n status: () => call('status'),\n listCandidates: (provider) => call('listCandidates', { provider }),\n setBackup: async (provider, model) => {\n await call('setBackup', { provider, model });\n },\n clearBackup: async () => {\n await call('clearBackup');\n },\n };\n}\n//# sourceMappingURL=api.js.map","import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from \"react/jsx-runtime\";\n/**\n * BackupChannelPanel: list candidate models for a provider and pick one as\n * the failover backup. Rendered inside a primitives Modal.\n *\n * No probing happens here anymore — the panel is a plain picker. Channel\n * health still updates from real traffic via the recovery engine.\n */\nimport { memo, useCallback, useEffect, useMemo, useState } from 'react';\nimport { Button, Modal, Pill, } from '@deepseek-ai/dsh-client-ui-primitives';\nfunction deriveProvider(status) {\n if (status === null)\n return '';\n if (status.current !== null)\n return status.current.split('::')[0] ?? '';\n if (status.backup !== null)\n return status.backup.provider;\n return status.providers[0] ?? '';\n}\nconst sectionStyle = {\n display: 'flex',\n flexDirection: 'column',\n gap: '8px',\n padding: '12px 0',\n fontSize: '13px',\n};\nconst rowStyle = {\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n};\nconst noteStyle = { color: 'var(--dsw-text-tertiary, #999)', fontSize: '12px' };\n/** Clamp a long provider/model name so the row never wraps. */\nconst backupPillStyle = {\n flex: '1 1 auto',\n minWidth: 0,\n overflow: 'hidden',\n};\nconst ellipsisStyle = {\n minWidth: 0,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n};\nconst listStyle = {\n maxHeight: 300,\n overflowY: 'auto',\n display: 'flex',\n flexDirection: 'column',\n gap: 4,\n contain: 'content',\n willChange: 'scroll-position',\n overscrollBehavior: 'contain',\n};\n/** Skip rendering rows outside the viewport while scrolling. */\nconst candidateRowStyle = {\n ...rowStyle,\n contentVisibility: 'auto',\n containIntrinsicSize: '28px',\n};\n/** One candidate row, memoized so selection changes only re-render it. */\nconst CandidateRow = memo(function CandidateRow(props) {\n const { candidate, isBackup, t, onSetBackup } = props;\n return (_jsxs(\"div\", { style: candidateRowStyle, children: [_jsx(\"span\", { style: { width: 14, flexShrink: 0 } }), _jsx(\"span\", { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: candidate.model }), isBackup && _jsx(Pill, { active: true, children: t('currentBackupMark') }), _jsx(Button, { size: \"sm\", variant: \"ghost\", disabled: isBackup, onClick: () => onSetBackup(candidate.model), children: t('setBackup') })] }));\n});\nexport function BackupPanel(props) {\n const { api, t, status, onClose } = props;\n const [provider, setProvider] = useState(deriveProvider(status));\n const [candidates, setCandidates] = useState(null);\n const [loading, setLoading] = useState(false);\n const [busy, setBusy] = useState(null);\n const [error, setError] = useState(null);\n // Local overlay on the status-prop backup: applied optimistically after\n // set/clear so the row reflects the change without waiting for the parent\n // to re-fetch status. Cleared whenever a fresh status arrives.\n const [backupOverride, setBackupOverride] = useState(undefined);\n // The effective backup shown in the panel: local override → status prop.\n const backupShown = backupOverride !== undefined ? backupOverride : status?.backup ?? null;\n // O(1) lookup for the current backup model; re-built only when the effective backup changes.\n const backupModel = useMemo(() => (backupShown !== null && backupShown.provider === provider ? backupShown.model : null), [backupShown, provider]);\n const loadCandidates = async () => {\n if (provider === '')\n return;\n setLoading(true);\n setError(null);\n try {\n setCandidates(await api.listCandidates(provider));\n }\n catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause));\n }\n finally {\n setLoading(false);\n }\n };\n useEffect(() => {\n if (provider !== '')\n void loadCandidates();\n }, [provider]);\n // A fresh status from the parent supersedes the local override.\n useEffect(() => {\n setBackupOverride(undefined);\n }, [status]);\n const setBackup = async (model) => {\n setBusy(model);\n setError(null);\n try {\n await api.setBackup(provider, model);\n setBackupOverride({ provider, model });\n }\n catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause));\n }\n finally {\n setBusy(null);\n }\n };\n const clearBackup = async () => {\n setError(null);\n try {\n await api.clearBackup();\n setBackupOverride(null);\n }\n catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause));\n }\n };\n // Stable callback so memoized CandidateRow props don't change every render.\n const handleSetBackup = useCallback((model) => void setBackup(model), [provider]);\n return (_jsx(Modal, { open: true, onClose: onClose, title: t('backupManage'), className: \"daruma-wide-dialog\", closeLabel: t('close'), description: t('backupManageHint'), footer: _jsx(Button, { variant: \"primary\", onClick: onClose, children: t('close') }), children: _jsxs(\"div\", { style: sectionStyle, children: [status !== null && status.failoverCount > 0 && (_jsxs(\"div\", { style: noteStyle, children: [t('failoverCount'), \": \", status.failoverCount, status.failoverHistory.length > 0 && (_jsxs(_Fragment, { children: [' · ', t('lastFailover'), \": \", status.failoverHistory.map((h) => `${h.from} → ${h.to}`).join(' · ')] }))] })), backupShown !== null && (_jsxs(\"div\", { style: rowStyle, children: [_jsxs(\"span\", { style: { flexShrink: 0 }, children: [t('currentBackup'), \":\"] }), _jsx(Pill, { active: true, style: backupPillStyle, title: `${backupShown.provider}/${backupShown.model}`, children: _jsxs(\"span\", { style: ellipsisStyle, children: [backupShown.provider, \"/\", backupShown.model] }) }), _jsx(Button, { size: \"sm\", variant: \"ghost\", style: { flexShrink: 0 }, onClick: () => void clearBackup(), children: t('clearBackup') })] })), _jsxs(\"div\", { style: rowStyle, children: [(status?.providers ?? []).length > 0 ? (_jsx(\"select\", { value: provider, disabled: loading, onChange: (event) => setProvider(event.currentTarget.value), style: { flex: 1, padding: '4px 8px', fontSize: 13 }, children: status?.providers.map((name) => (_jsx(\"option\", { value: name, children: name }, name))) })) : (_jsx(\"input\", { value: provider, placeholder: t('providerPlaceholder'), onChange: (event) => setProvider(event.currentTarget.value), style: { flex: 1, padding: '4px 8px', fontSize: 13 } })), _jsx(Button, { size: \"sm\", variant: \"outline\", onClick: () => void loadCandidates(), disabled: provider === '' || loading, children: loading ? t('loading') : t('loadCandidates') })] }), error !== null && _jsx(\"div\", { style: { color: 'var(--dsw-text-danger, #e5484d)' }, children: error }), candidates !== null && (_jsxs(_Fragment, { children: [_jsx(\"div\", { style: rowStyle, children: _jsxs(\"span\", { children: [t('candidates'), \" (\", candidates.length, \")\"] }) }), candidates.length === 0 && _jsx(\"div\", { style: noteStyle, children: t('noCandidates') }), _jsx(\"div\", { style: listStyle, children: candidates.map((candidate) => (_jsx(CandidateRow, { candidate: candidate, isBackup: candidate.model === backupModel, t: t, onSetBackup: handleSetBackup }, candidate.model))) })] }))] }) }));\n}\n//# sourceMappingURL=BackupPanel.js.map","import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from \"react/jsx-runtime\";\n/**\n * DarumaStatus: a compact control at the right end of the composer tool row,\n * next to the model selector. Shows overall channel health and the backup\n * selection; clicking it opens the backup-channel panel.\n */\nimport { useCallback, useEffect, useState } from 'react';\nimport { Button, IconCheckOutline16, IconWarningOutline16 } from '@deepseek-ai/dsh-client-ui-primitives';\nimport { BackupPanel } from \"./BackupPanel.js\";\nconst controlStyle = {\n display: 'inline-flex',\n alignItems: 'center',\n gap: 6,\n maxWidth: 220,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n};\n/** Overall health: any cooling channel → error, any probe → warning, else done. */\nfunction overallState(status) {\n if (status === null)\n return 'ongoing';\n if (status.channels.some((c) => c.state === 'COOLDOWN'))\n return 'error';\n if (status.channels.some((c) => c.state === 'PROBE'))\n return 'warning';\n return 'done';\n}\nconst iconColor = {\n done: 'var(--dsw-text-success, #30a46c)',\n warning: 'var(--dsw-text-warning, #f5a524)',\n error: 'var(--dsw-text-danger, #e5484d)',\n ongoing: 'var(--dsw-text-secondary, #888)',\n};\n/** Status glyph: a check when healthy, a warning otherwise. */\nfunction StatusIcon(props) {\n return (_jsx(\"span\", { style: { display: 'inline-flex', color: iconColor[props.state], flexShrink: 0 }, children: props.state === 'done'\n ? _jsx(IconCheckOutline16, { size: 14 })\n : _jsx(IconWarningOutline16, { size: 14 }) }));\n}\nexport function StatusDock(props) {\n const [status, setStatus] = useState(null);\n const [panelOpen, setPanelOpen] = useState(false);\n const load = useCallback(async () => {\n try {\n const value = await props.api.status();\n setStatus(value);\n }\n catch {\n // keep last good snapshot; the control stays quiet on RPC failure\n }\n }, [props.api]);\n useEffect(() => {\n void load();\n const timer = setInterval(() => void load(), 30_000);\n return () => clearInterval(timer);\n }, [load]);\n const t = props.t;\n const backupLabel = status?.backup !== null && status?.backup !== undefined\n ? `${status.backup.provider}/${status.backup.model}`\n : t('backupNone');\n return (_jsxs(_Fragment, { children: [_jsxs(Button, { size: \"sm\", variant: \"ghost\", title: t('backupManageHint'), onClick: () => setPanelOpen(true), style: controlStyle, children: [_jsx(StatusIcon, { state: overallState(status) }), _jsxs(\"span\", { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: [t('backup'), \": \", backupLabel] })] }), panelOpen && (_jsx(BackupPanel, { api: props.api, t: t, status: status, onClose: () => {\n setPanelOpen(false);\n void load();\n } }))] }));\n}\n//# sourceMappingURL=StatusDock.js.map","/**\n * dsh-daruma client: registers a compact channel-status dock above the\n * composer (conversation.input.dock) with a \"backup channel\" panel that\n * lists candidate models and picks a backup. Built by tsdown into the\n * __ModuleLoader__ factory bundle at lib/client.js.\n */\nimport { createElement as h } from 'react';\nimport { en, zh } from \"./locales.js\";\nimport { createApi } from \"./api.js\";\nimport { StatusDock } from \"./StatusDock.js\";\nconst NS = 'daruma';\nexport const name = 'dsh-daruma';\nexport const inject = ['slots', 'locale'];\nexport function apply(ctx) {\n // Widen the backup-panel dialog: the primitives Modal card is fixed at\n // min(380px, 100%), which truncates long provider/model names. A global\n // class (not a CSS module) outranks the module-scoped `.dialog` rule.\n if (typeof document !== 'undefined') {\n const tagId = 'daruma-panel-style';\n if (document.querySelector(`style[data-daruma=\"${tagId}\"]`) === null) {\n const style = document.createElement('style');\n style.dataset.daruma = tagId;\n style.textContent = '.daruma-wide-dialog.daruma-wide-dialog { width: min(560px, 100%); }';\n document.head.appendChild(style);\n }\n }\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-daruma: dictionaries');\n const t = ctx.locale.bind(NS);\n const connection = ctx.get('connection');\n if (connection === undefined) {\n console.warn('[dsh-daruma] web transport unavailable — client UI disabled');\n return;\n }\n const rpc = (endpoint, payload) => connection.rpc.call('/dsh-daruma', endpoint, payload ?? {});\n const api = createApi(rpc);\n ctx.slots.inject('conversation.input.right', () => ctx.slots.register({\n name: 'conversation.input.right',\n id: 'daruma-status',\n order: 0,\n inject: () => ({ api, t }),\n }, () => h(StatusDock, { api, t })));\n}\n//# sourceMappingURL=index.js.map"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Plugin config parsing: cordis.patch.yml `config` → daruma-core config.
3
+ */
4
+ import { type RecoveryPolicyConfig } from 'daruma-core';
5
+ export interface ChannelEntry {
6
+ readonly provider: string;
7
+ readonly model: string;
8
+ }
9
+ export interface PluginConfig {
10
+ readonly channels?: readonly ChannelEntry[];
11
+ readonly failureBudget?: number;
12
+ readonly cooldownMs?: number;
13
+ readonly giveUpBudget?: number;
14
+ readonly stateFile?: string;
15
+ }
16
+ export interface ResolvedConfig extends RecoveryPolicyConfig {
17
+ readonly stateFile: string;
18
+ }
19
+ export declare function defaultStateFile(): string;
20
+ export declare function resolveConfig(raw?: PluginConfig): ResolvedConfig;
package/lib/config.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Plugin config parsing: cordis.patch.yml `config` → daruma-core config.
3
+ */
4
+ import { homedir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { modelId } from 'daruma-core';
7
+ import { channelIdOf } from "./mapping.js";
8
+ export function defaultStateFile() {
9
+ return join(homedir(), '.dsh', 'daruma', 'channel-health.json');
10
+ }
11
+ export function resolveConfig(raw = {}) {
12
+ const channels = (raw.channels ?? []).map((entry) => ({
13
+ id: channelIdOf(entry.provider, entry.model),
14
+ provider: entry.provider,
15
+ model: modelId(entry.model),
16
+ }));
17
+ return {
18
+ channels,
19
+ failureBudget: raw.failureBudget ?? 3,
20
+ cooldownMs: raw.cooldownMs ?? 30_000,
21
+ giveUpBudget: raw.giveUpBudget ?? 8,
22
+ stateFile: raw.stateFile || defaultStateFile(),
23
+ };
24
+ }
25
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,OAAO,EAA2C,MAAM,aAAa,CAAA;AAC9E,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAmB1C,MAAM,UAAU,gBAAgB;IAC9B,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,qBAAqB,CAAC,CAAA;AACjE,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAoB,EAAE;IAClD,MAAM,QAAQ,GAAc,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC/D,EAAE,EAAE,WAAW,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC;QAC5C,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;KAC5B,CAAC,CAAC,CAAA;IAEH,OAAO;QACL,QAAQ;QACR,aAAa,EAAE,GAAG,CAAC,aAAa,IAAI,CAAC;QACrC,UAAU,EAAE,GAAG,CAAC,UAAU,IAAI,MAAM;QACpC,YAAY,EAAE,GAAG,CAAC,YAAY,IAAI,CAAC;QACnC,SAAS,EAAE,GAAG,CAAC,SAAS,IAAI,gBAAgB,EAAE;KAC/C,CAAA;AACH,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * RecoveryEngine — the adapter-side state holder that wraps the pure
3
+ * `decide()` from daruma-core.
4
+ *
5
+ * It owns the mutable channel-health map, the per-session failover counter,
6
+ * and persistence through a `ChannelHealthStore`. All decision logic stays in
7
+ * daruma-core; this class only threads state and I/O.
8
+ */
9
+ import { type Channel, type ChannelHealthStore, type ChannelId, type ChannelState, type Clock, type FailureSignal, type RecoveryPlan, type RecoveryPolicyConfig } from 'daruma-core';
10
+ export interface ChannelHealthView {
11
+ readonly channel: ChannelId;
12
+ readonly state: ChannelState;
13
+ readonly failures: number;
14
+ }
15
+ export interface FailoverRecord {
16
+ readonly from: string;
17
+ readonly to: string;
18
+ readonly reason: string;
19
+ readonly at: number;
20
+ }
21
+ export declare class RecoveryEngine {
22
+ private readonly config;
23
+ private readonly store;
24
+ private readonly healths;
25
+ private failoverTotal;
26
+ private readonly failoverHistory;
27
+ private readonly clock;
28
+ constructor(config: RecoveryPolicyConfig, store: ChannelHealthStore, clock?: Clock);
29
+ /** Load every configured channel's persisted health into memory. */
30
+ private preload;
31
+ /** Feed one failure signal and persist the resulting state. */
32
+ onFailure(signal: FailureSignal, preferred?: Channel): RecoveryPlan;
33
+ /** Snapshot of every tracked channel's health, for the status UI. */
34
+ listHealth(): ChannelHealthView[];
35
+ get failoverCount(): number;
36
+ /** Configured failover channels (for provider discovery). */
37
+ get channels(): readonly Channel[];
38
+ /** Recent failovers, newest last (bounded). */
39
+ get history(): readonly FailoverRecord[];
40
+ }