@qadanm/opencode-smart-router 0.4.0 → 0.6.1

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/dist/tui.js DELETED
@@ -1,587 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __esm = (fn, res, err) => function __init() {
4
- if (err) throw err[0];
5
- try {
6
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
- } catch (e) {
8
- throw err = [e], e;
9
- }
10
- };
11
- var __export = (target, all) => {
12
- for (var name in all)
13
- __defProp(target, name, { get: all[name], enumerable: true });
14
- };
15
-
16
- // src/gui/viewmodel.ts
17
- var viewmodel_exports = {};
18
- __export(viewmodel_exports, {
19
- formatPercent: () => formatPercent,
20
- formatReset: () => formatReset,
21
- formatUsd: () => formatUsd,
22
- formatWindowUsed: () => formatWindowUsed,
23
- keyOf: () => keyOf,
24
- orderCandidateRows: () => orderCandidateRows,
25
- providerGroups: () => providerGroups,
26
- rankingAdjustments: () => rankingAdjustments,
27
- statusLine: () => statusLine,
28
- summariseDecision: () => summariseDecision,
29
- toCandidateRow: () => toCandidateRow,
30
- toProviderRow: () => toProviderRow
31
- });
32
- var sourceLabelOf, windowLabels, formatReset, toWindow, toProviderRow, formatUsd, formatPercent, formatWindowUsed, rankingAdjustments, keyOf, toCandidateRow, verdictRank, orderCandidateRows, summariseDecision, statusLine, contextLabel, providerGroups;
33
- var init_viewmodel = __esm({
34
- "src/gui/viewmodel.ts"() {
35
- "use strict";
36
- sourceLabelOf = (source) => {
37
- switch (source) {
38
- case "provider":
39
- return "verified by provider";
40
- case "estimate":
41
- return "estimated \u2014 lower bound";
42
- default:
43
- return "not reported";
44
- }
45
- };
46
- windowLabels = {
47
- fiveHour: "5h",
48
- weekly: "7d",
49
- monthly: "Monthly"
50
- };
51
- formatReset = (at, now = Date.now()) => {
52
- if (at === void 0) return void 0;
53
- const delta = at - now;
54
- if (delta <= 0) return "resetting";
55
- const minutes = Math.round(delta / 6e4);
56
- if (minutes < 60) return `in ${minutes}m`;
57
- const hours = Math.round(minutes / 60);
58
- if (hours < 36) return `in ${hours}h`;
59
- const days = Math.round(hours / 24);
60
- return `in ${days}d`;
61
- };
62
- toWindow = (id, window, now) => {
63
- const source = window?.source ?? "unknown";
64
- const usedPct = window?.usedPct ?? 0;
65
- const reported = source !== "unknown" && (window?.capUsd !== void 0 || usedPct > 0);
66
- return {
67
- id,
68
- label: windowLabels[id],
69
- usedPct,
70
- remainingPct: window?.remainingPct ?? Math.max(0, 100 - usedPct),
71
- ...window?.usedUsd !== void 0 ? { usedUsd: window.usedUsd } : {},
72
- ...window?.capUsd !== void 0 ? { capUsd: window.capUsd } : {},
73
- ...window?.resetsAt !== void 0 ? { resetsAt: window.resetsAt } : {},
74
- ...formatReset(window?.resetsAt, now) ? { resetLabel: formatReset(window?.resetsAt, now) } : {},
75
- source,
76
- sourceLabel: sourceLabelOf(source),
77
- exceeded: Boolean(window?.exceeded),
78
- reported,
79
- lowerBound: source === "estimate"
80
- };
81
- };
82
- toProviderRow = (snapshot, now = Date.now()) => {
83
- const source = snapshot.monthly?.source === "provider" ? "provider" : snapshot.monthly?.source === "estimate" ? "estimate" : "unknown";
84
- const windows = [
85
- toWindow("fiveHour", snapshot.rolling, now),
86
- toWindow("weekly", snapshot.weekly, now),
87
- toWindow("monthly", snapshot.monthly, now)
88
- ];
89
- const exhaustedWindow = windows.find((window) => window.exceeded);
90
- const pools = (snapshot.pools ?? []).map((pool) => ({
91
- id: pool.id,
92
- label: pool.label,
93
- source: pool.source,
94
- sourceLabel: sourceLabelOf(pool.source),
95
- monthlyUsd: pool.monthlyUsd,
96
- usedUsd: pool.usedUsd,
97
- remainingUsd: pool.remainingUsd,
98
- usedPct: pool.usedPct,
99
- ...pool.weeklyUsedPct !== void 0 ? { weeklyPct: pool.weeklyUsedPct } : {},
100
- ...pool.fiveHourUsedPct !== void 0 ? { fiveHourPct: pool.fiveHourUsedPct } : {},
101
- ...formatReset(pool.resetsAt, now) ? { resetLabel: formatReset(pool.resetsAt, now) } : {},
102
- note: pool.note
103
- }));
104
- return {
105
- providerID: snapshot.providerID,
106
- label: snapshot.label ?? snapshot.providerID,
107
- plan: snapshot.plan ?? "",
108
- source,
109
- sourceLabel: sourceLabelOf(source),
110
- ...snapshot.quotaState ? { quotaState: snapshot.quotaState } : {},
111
- stale: Boolean(snapshot.stale),
112
- fetchedAt: snapshot.fetchedAt ?? now,
113
- exhausted: Boolean(exhaustedWindow),
114
- ...exhaustedWindow ? { exhaustedWindow: exhaustedWindow.label } : {},
115
- windows,
116
- pools,
117
- ...snapshot.monthlyRemainingUsd !== void 0 ? { monthlyRemainingUsd: snapshot.monthlyRemainingUsd } : {},
118
- ...snapshot.extraCredits ? { extraCreditsUsd: snapshot.extraCredits.totalUsd } : {},
119
- ...snapshot.error ? { error: snapshot.error } : {},
120
- notes: snapshot.notes ?? [],
121
- ...source === "estimate" ? { caveat: (snapshot.notes ?? [])[0] } : {}
122
- };
123
- };
124
- formatUsd = (value) => {
125
- if (!Number.isFinite(value)) return "\u2014";
126
- if (value === 0) return "$0";
127
- if (value < 0.01) return `$${value.toFixed(4)}`;
128
- if (value < 1) return `$${value.toFixed(3)}`;
129
- return `$${value.toFixed(2)}`;
130
- };
131
- formatPercent = (value) => Number.isFinite(value) ? `${Math.round(value)}%` : "\u2014";
132
- formatWindowUsed = (window) => {
133
- if (!window.reported) return "\u2014";
134
- const value = formatPercent(window.usedPct);
135
- return window.lowerBound ? `\u2265${value}` : value;
136
- };
137
- rankingAdjustments = (candidate) => {
138
- const notes = [];
139
- const credit = candidate.creditMultiplier;
140
- if (credit !== void 0 && credit !== 1) notes.push(`${credit}x plan credits`);
141
- const speed = candidate.speedFactor;
142
- if (speed !== void 0 && speed !== 1) notes.push(`${speed}x measured speed`);
143
- const marginal = candidate.marginalPriceFactor;
144
- if (marginal !== void 0 && marginal !== 1) notes.push(`${marginal}x expiring allowance`);
145
- if (candidate.switchPenaltyUsd > 0) notes.push(`${formatUsd(candidate.switchPenaltyUsd)} switch cost`);
146
- return notes;
147
- };
148
- keyOf = (model) => `${model.providerID}/${model.modelID}`;
149
- toCandidateRow = (candidate, chosen) => {
150
- const key = keyOf(candidate.model);
151
- return {
152
- key,
153
- name: candidate.name,
154
- tier: candidate.tier,
155
- verdict: key === chosen && candidate.eligible ? "chosen" : candidate.eligible ? "eligible" : "blocked",
156
- reason: candidate.reason,
157
- estimatedUsd: candidate.estimatedUsd,
158
- effectiveCost: candidate.effectiveCost,
159
- successProbability: candidate.successProbability,
160
- adjustments: rankingAdjustments(candidate)
161
- };
162
- };
163
- verdictRank = { chosen: 0, eligible: 1, blocked: 2 };
164
- orderCandidateRows = (rows) => [...rows].sort((left, right) => {
165
- const byVerdict = verdictRank[left.verdict] - verdictRank[right.verdict];
166
- if (byVerdict !== 0) return byVerdict;
167
- if (left.verdict === "blocked") return left.key.localeCompare(right.key);
168
- return left.effectiveCost - right.effectiveCost || left.key.localeCompare(right.key);
169
- });
170
- summariseDecision = (decision) => {
171
- if (!decision) return void 0;
172
- const chosenKey = decision.chosen ? `${decision.chosen.providerID}/${decision.chosen.id}` : void 0;
173
- const rows = orderCandidateRows(decision.candidates.map((candidate) => toCandidateRow(candidate, chosenKey)));
174
- return {
175
- id: decision.id,
176
- chosen: chosenKey,
177
- switched: decision.switched,
178
- switchReason: decision.switchReason,
179
- taskKind: decision.task?.kind ?? "unknown",
180
- taskTier: decision.task?.tier ?? 0,
181
- contextTokens: decision.contextTokens,
182
- estimatedUsd: decision.estimatedUsd,
183
- reasons: decision.reasons ?? [],
184
- warnings: decision.warnings ?? [],
185
- selfheal: decision.selfheal?.rejected ? `rejected ${decision.selfheal.rejected} \xB7 ${decision.selfheal.action === "noted" ? "noted for the next prompt" : decision.selfheal.action}` : void 0,
186
- eligibleCount: rows.filter((row) => row.verdict !== "blocked").length,
187
- blockedCount: rows.filter((row) => row.verdict === "blocked").length,
188
- rows
189
- };
190
- };
191
- statusLine = (input) => {
192
- const parts = [input.runtime ?? "unknown", input.mode ?? "unknown"];
193
- parts.push(input.current ? `${input.current.providerID}/${input.current.id}` : "no model yet");
194
- return parts.join(" \xB7 ");
195
- };
196
- contextLabel = (tokens) => {
197
- if (!Number.isFinite(tokens) || tokens <= 0) return "unknown";
198
- return tokens >= 1e6 ? `${Number((tokens / 1e6).toFixed(1))}M` : `${Math.round(tokens / 1e3)}k`;
199
- };
200
- providerGroups = (input) => {
201
- const byProvider = /* @__PURE__ */ new Map();
202
- for (const model of input.models) {
203
- const state = model.excluded ? "excluded" : model.approved ? "approved" : "not approved";
204
- const row = {
205
- key: model.key,
206
- providerID: model.providerID,
207
- name: model.name,
208
- tier: model.tier,
209
- intelligence: model.intelligence,
210
- design: model.design,
211
- contextLabel: contextLabel(model.context),
212
- costLabel: `${formatUsd(model.costInput)} / ${formatUsd(model.costOutput)} per 1M`,
213
- vision: model.vision,
214
- state
215
- };
216
- const list = byProvider.get(model.providerID) ?? [];
217
- list.push(row);
218
- byProvider.set(model.providerID, list);
219
- }
220
- const quotaLabel = new Map(input.quota.map((entry) => [entry.providerID, entry.label ?? entry.providerID]));
221
- return [...byProvider.entries()].map(([providerID, models]) => {
222
- const active = input.activeProviders.includes(providerID);
223
- const approvedCount = models.filter((model) => model.state === "approved").length;
224
- return {
225
- providerID,
226
- label: quotaLabel.get(providerID) ?? providerID,
227
- state: active ? "active" : "not configured",
228
- // Approved first, then by tier and cost: the order a routing decision
229
- // would consider them.
230
- models: [...models].sort((left, right) => {
231
- const rank = (row) => row.state === "approved" ? 0 : row.state === "excluded" ? 2 : 1;
232
- return rank(left) - rank(right) || left.tier - right.tier || left.key.localeCompare(right.key);
233
- }),
234
- approvedCount,
235
- totalCount: models.length,
236
- ...active ? {} : { note: "Not routing here: no credential found for this provider on this machine." }
237
- };
238
- }).sort((left, right) => {
239
- if (left.state !== right.state) return left.state === "active" ? -1 : 1;
240
- return left.label.localeCompare(right.label);
241
- });
242
- };
243
- }
244
- });
245
-
246
- // src/tui/index.tsx
247
- import { Plugin } from "@opencode/plugin/tui";
248
-
249
- // src/rpc.ts
250
- import { Rpc } from "@opencode/plugin/rpc";
251
- var objectOutput = { type: "object", additionalProperties: true };
252
- var emptyInput = { type: "object", properties: {}, additionalProperties: false };
253
- var sessionInput = {
254
- type: "object",
255
- properties: { sessionID: { type: "string" } },
256
- required: ["sessionID"],
257
- additionalProperties: false
258
- };
259
- var method = (input) => ({ input, output: objectOutput });
260
- var SmartRouterRpc = Rpc.define({
261
- id: "smart-router",
262
- methods: {
263
- status: method({
264
- type: "object",
265
- properties: { sessionID: { type: "string" } },
266
- additionalProperties: false
267
- }),
268
- preview: method({
269
- type: "object",
270
- properties: {
271
- sessionID: { type: "string" },
272
- text: { type: "string" },
273
- files: { type: "array", items: { type: "object", additionalProperties: true } }
274
- },
275
- required: ["sessionID", "text"],
276
- additionalProperties: false
277
- }),
278
- setMode: method({
279
- type: "object",
280
- properties: { mode: { type: "string", enum: ["efficiency", "balanced", "premium"] } },
281
- required: ["mode"],
282
- additionalProperties: false
283
- }),
284
- setPaused: method({
285
- type: "object",
286
- properties: { paused: { type: "boolean" } },
287
- required: ["paused"],
288
- additionalProperties: false
289
- }),
290
- /**
291
- * Turn automatic routing on or off for one session.
292
- *
293
- * Not the same as disabling the plugin or pausing every session: the router
294
- * stays loaded, keeps reporting quota and decisions, and simply stops
295
- * changing the model. The user keeps whatever model they have.
296
- */
297
- setSessionAuto: method({
298
- type: "object",
299
- properties: { sessionID: { type: "string" }, auto: { type: "boolean" } },
300
- required: ["sessionID", "auto"],
301
- additionalProperties: false
302
- }),
303
- setDryRun: method({
304
- type: "object",
305
- properties: { dryRun: { type: "boolean" } },
306
- required: ["dryRun"],
307
- additionalProperties: false
308
- }),
309
- resumeAuto: method(sessionInput),
310
- pin: method({
311
- type: "object",
312
- properties: {
313
- sessionID: { type: "string" },
314
- providerID: { type: "string" },
315
- modelID: { type: "string" },
316
- variant: { type: "string" }
317
- },
318
- required: ["sessionID", "providerID", "modelID"],
319
- additionalProperties: false
320
- }),
321
- approvePremium: method(sessionInput),
322
- denyPremium: method(sessionInput),
323
- excludeModel: method({
324
- type: "object",
325
- properties: {
326
- providerID: { type: "string" },
327
- modelID: { type: "string" },
328
- excluded: { type: "boolean" }
329
- },
330
- required: ["providerID", "modelID", "excluded"],
331
- additionalProperties: false
332
- }),
333
- setPremiumBudget: method({
334
- type: "object",
335
- properties: { usd: { type: "number", minimum: 0, maximum: 1e5 } },
336
- required: ["usd"],
337
- additionalProperties: false
338
- }),
339
- setReserve: method({
340
- type: "object",
341
- properties: {
342
- providerID: { type: "string" },
343
- monthlyPct: { type: "number", minimum: 0, maximum: 90 },
344
- weeklyPct: { type: "number", minimum: 0, maximum: 90 },
345
- fiveHourPct: { type: "number", minimum: 0, maximum: 90 }
346
- },
347
- required: ["providerID"],
348
- additionalProperties: false
349
- }),
350
- /**
351
- * The intelligence floor in force, and why.
352
- *
353
- * Calibration can only be trusted if its result is visible: a number with
354
- * no explanation is indistinguishable from the hand-picked one it replaced.
355
- * This previews without enabling anything.
356
- */
357
- floor: method(emptyInput),
358
- setCalibrateFloor: method({
359
- type: "object",
360
- properties: { enabled: { type: "boolean" } },
361
- required: ["enabled"],
362
- additionalProperties: false
363
- }),
364
- refreshQuota: method(emptyInput),
365
- /**
366
- * What this machine has, for the first-run screen.
367
- *
368
- * Onboarding must show evidence, not a questionnaire. The router already
369
- * detects credentials and imports history at startup; this reports what it
370
- * found so the first thing a new user sees is the truth about their own
371
- * machine rather than a form asking them to type it in.
372
- */
373
- onboarding: method(emptyInput),
374
- /** Mark the first run as seen, so it does not reappear. */
375
- completeOnboarding: method(emptyInput),
376
- /**
377
- * Record what the user's account page showed, so the estimate reflects the
378
- * whole account rather than only the traffic this router routed.
379
- *
380
- * Takes the percentage the page displays, not a dollar figure: asking a user
381
- * to divide by a cap they may not know is where the arithmetic mistakes come
382
- * from. The server converts it against the window the page was describing.
383
- */
384
- /**
385
- * Record the account's balance page, pasted whole.
386
- *
387
- * One field, because that is the actual task: select the page, copy, paste.
388
- * The server reads the plan, the percentage, which window it belongs to, the
389
- * reset time, the billing period and the wallet out of the text. A form with
390
- * four boxes asks the user to do that parsing by hand and get it right.
391
- */
392
- recordPlanPage: method({
393
- type: "object",
394
- properties: {
395
- /** The pasted page text. */
396
- page: { type: "string" },
397
- /** Apply it without previewing. Defaults to false: the user confirms first. */
398
- confirm: { type: "boolean" }
399
- },
400
- required: ["page"],
401
- additionalProperties: false
402
- }),
403
- report: method(emptyInput),
404
- usage: method(emptyInput),
405
- getConfig: method(emptyInput),
406
- resetOverrides: method(emptyInput),
407
- resetMetrics: method(emptyInput),
408
- refreshIndicator: method(sessionInput),
409
- diagnostics: method(emptyInput),
410
- setTitleIndicator: method({
411
- type: "object",
412
- properties: { enabled: { type: "boolean" } },
413
- required: ["enabled"],
414
- additionalProperties: false
415
- })
416
- },
417
- events: {
418
- decision: {
419
- schema: {
420
- type: "object",
421
- properties: {
422
- decision: { type: "object", additionalProperties: true }
423
- },
424
- required: ["decision"],
425
- additionalProperties: false
426
- }
427
- },
428
- status: {
429
- schema: {
430
- type: "object",
431
- properties: {
432
- sessionID: { type: "string" },
433
- summary: { type: "object", additionalProperties: true }
434
- },
435
- required: ["summary"],
436
- additionalProperties: false
437
- }
438
- },
439
- quota: {
440
- schema: {
441
- type: "object",
442
- properties: {
443
- providers: { type: "array", items: { type: "object", additionalProperties: true } }
444
- },
445
- required: ["providers"],
446
- additionalProperties: false
447
- }
448
- }
449
- }
450
- });
451
-
452
- // src/tui/index.tsx
453
- var tick = (ok) => ok ? "\u2713" : "\xB7";
454
- var formatDecision = (status, view) => {
455
- if (!status) return "Smart Router: no response from the router.";
456
- const head = `${status.runtime ?? "unknown"} \xB7 ${status.mode ?? "unknown"} \xB7 ${status.current ? `${status.current.providerID}/${status.current.id}` : "no model yet"}`;
457
- if (!view) return `${head}
458
-
459
- No decision yet in this session.`;
460
- const lines = [head, "", `Chosen ${view.chosen ?? "nothing eligible"}`];
461
- lines.push(
462
- ` ${view.taskKind} \xB7 tier ${view.taskTier} \xB7 ${Math.round(view.contextTokens / 1e3)}k context`
463
- );
464
- if (view.reasons.length > 0) {
465
- lines.push("", "Why");
466
- for (const reason of view.reasons.slice(0, 5)) lines.push(` \u2022 ${reason}`);
467
- }
468
- if (view.rows.length > 0) {
469
- lines.push("", `Candidates (${view.eligibleCount} eligible, ${view.blockedCount} blocked)`);
470
- for (const row of view.rows.slice(0, 10)) {
471
- lines.push(` ${tick(row.verdict !== "blocked")} ${row.key}`);
472
- lines.push(` ${row.reason}`);
473
- }
474
- }
475
- if (view.warnings.length > 0) {
476
- lines.push("", "Warnings");
477
- for (const warning of view.warnings.slice(0, 3)) lines.push(` ! ${warning}`);
478
- }
479
- return lines.join("\n");
480
- };
481
- var formatProviders = (providers) => {
482
- if (providers.length === 0) return "No providers reported.";
483
- const lines = [];
484
- for (const provider of providers) {
485
- const windows = provider.windows.filter((window) => window.reported).map((window) => `${window.label} ${window.lowerBound ? "\u2265" : ""}${window.usedPct}%`);
486
- lines.push(`${provider.label}${provider.exhausted ? " (exhausted)" : ""}`);
487
- lines.push(` ${windows.length > 0 ? windows.join(" \xB7 ") : "no windows reported"} \xB7 ${provider.sourceLabel}`);
488
- if (provider.caveat) lines.push(` ! ${provider.caveat}`);
489
- }
490
- return lines.join("\n");
491
- };
492
- var index_default = Plugin.define({
493
- /**
494
- * Deliberately NOT nested under the server part's id.
495
- *
496
- * The GUI docs are explicit: if a plugin's id has another id nested under it,
497
- * or nests under one, its GUI does not load. The TUI previously used
498
- * "opencode-smart-router.tui", which nests under the server part's
499
- * "opencode-smart-router" — so adding a TUI silently disabled the desktop
500
- * panel, with no error beyond a generic request failure.
501
- *
502
- * A sibling id avoids the namespace collision entirely.
503
- */
504
- id: "opencode-smart-router-tui",
505
- setup(context) {
506
- const stop = context.data.on("session.execution.succeeded", () => {
507
- void refresh();
508
- });
509
- const refresh = async () => {
510
- try {
511
- const result = await context.client.rpc(SmartRouterRpc).status({});
512
- setFooter(`${result?.runtime ?? "?"} \xB7 ${result?.mode ?? "?"}`);
513
- } catch {
514
- setFooter("router unreachable");
515
- }
516
- };
517
- let setFooter = (_text) => {
518
- };
519
- context.ui.slot({
520
- append: "prompt.footer.status",
521
- render: () => {
522
- const [text, set] = createTerminalSignal("router");
523
- setFooter = set;
524
- void refresh();
525
- return /* @__PURE__ */ React.createElement("text", null, text());
526
- }
527
- });
528
- context.keymap.layer(() => ({
529
- mode: "global",
530
- commands: [
531
- {
532
- id: "router.decision",
533
- title: "Router: why this model",
534
- slash: { name: "router-why" },
535
- palette: true,
536
- run: async () => {
537
- try {
538
- const result = await context.client.rpc(SmartRouterRpc).status({});
539
- const { summariseDecision: summariseDecision2, toProviderRow: toProviderRow2 } = await Promise.resolve().then(() => (init_viewmodel(), viewmodel_exports));
540
- const body = formatDecision(result, summariseDecision2(result?.lastDecision));
541
- await context.ui.dialog.alert({ title: "Smart Router", message: body });
542
- } catch (cause) {
543
- await context.ui.dialog.alert({
544
- title: "Smart Router",
545
- message: `Could not read router status: ${cause instanceof Error ? cause.message : String(cause)}`
546
- });
547
- }
548
- }
549
- },
550
- {
551
- id: "router.providers",
552
- title: "Router: providers and allowances",
553
- slash: { name: "router-quota" },
554
- palette: true,
555
- run: async () => {
556
- try {
557
- const result = await context.client.rpc(SmartRouterRpc).status({});
558
- const { toProviderRow: toProviderRow2 } = await Promise.resolve().then(() => (init_viewmodel(), viewmodel_exports));
559
- const rows = (result?.quota ?? []).map((snapshot) => toProviderRow2(snapshot));
560
- await context.ui.dialog.alert({ title: "Providers", message: formatProviders(rows) });
561
- } catch (cause) {
562
- await context.ui.dialog.alert({
563
- title: "Providers",
564
- message: `Could not read provider status: ${cause instanceof Error ? cause.message : String(cause)}`
565
- });
566
- }
567
- }
568
- }
569
- ],
570
- bindings: ["router.decision", "router.providers"]
571
- }));
572
- return () => stop();
573
- }
574
- });
575
- function createTerminalSignal(initial) {
576
- let value = initial;
577
- const listeners = /* @__PURE__ */ new Set();
578
- return [() => value, (next) => {
579
- value = next;
580
- for (const listener of listeners) listener(next);
581
- }];
582
- }
583
- export {
584
- index_default as default,
585
- formatDecision,
586
- formatProviders
587
- };