@cortexkit/opencode-anthropic-auth 2.1.2 → 2.2.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.
@@ -0,0 +1,1253 @@
1
+ import { memo as _$memo } from "opentui:runtime-module:%40opentui%2Fsolid";
2
+ import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid";
3
+ import { createTextNode as _$createTextNode } from "opentui:runtime-module:%40opentui%2Fsolid";
4
+ import { use as _$use } from "opentui:runtime-module:%40opentui%2Fsolid";
5
+ import { effect as _$effect } from "opentui:runtime-module:%40opentui%2Fsolid";
6
+ import { insertNode as _$insertNode } from "opentui:runtime-module:%40opentui%2Fsolid";
7
+ import { insert as _$insert } from "opentui:runtime-module:%40opentui%2Fsolid";
8
+ import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid";
9
+ import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
10
+ /** @jsxImportSource @opentui/solid */
11
+
12
+ // The CLAUDE quota sidebar, shared by the OpenCode 1 and OpenCode 2 TUIs.
13
+ // Each host supplies the sidebar state, a flat theme token set and the
14
+ // preference controller; this module only renders.
15
+
16
+ import { readFileSync } from "node:fs";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import { formatCreditAmounts, quotaDisplayProjection, quotaWindowParts } from "@cortexkit/anthropic-auth-core";
20
+ import { TextAttributes } from "opentui:runtime-module:%40opentui%2Fcore";
21
+ import { createEffect, createMemo, createSignal, For, Index, onCleanup, Show } from "opentui:runtime-module:solid-js";
22
+ import { computeQuotaPacing, DEFAULT_SIDEBAR_STATE, FIVE_HOUR_MS, formatPrimeAccountValue, formatQuotaRowLabel, getCollapsedAccounts, getCollapsedQuotaSummary, getFableRecoverySummary, resolveActiveAccount, SEVEN_DAY_MS } from "../sidebar-state.js";
23
+ import { PLUGIN_KEY, queueTuiPreferenceUpdate, readTuiPreferencesFile, resolveAnthropicAuthPrefs, watchTuiPreferences } from "../tui-preferences.js";
24
+
25
+ // Read package metadata from either the raw src/ entry or its generated
26
+ // src/tui-compiled/tui/ counterpart. Avoid a JSON import because package.json sits
27
+ // outside the declaration build's rootDir.
28
+ const PLUGIN_VERSION = (() => {
29
+ const here = dirname(fileURLToPath(import.meta.url));
30
+ for (const packageFile of [join(here, '..', '..', 'package.json'), join(here, '..', '..', '..', 'package.json')]) {
31
+ try {
32
+ const raw = readFileSync(packageFile, 'utf8');
33
+ const version = JSON.parse(raw).version;
34
+ if (version) return version;
35
+ } catch {
36
+ // Try the path for the other TUI entry layout.
37
+ }
38
+ }
39
+ return '';
40
+ })();
41
+
42
+ // biome-ignore lint/suspicious/noExplicitAny: opentui border prop not typed in plugin tui surface
43
+ const SINGLE_BORDER = {
44
+ type: 'single'
45
+ };
46
+
47
+ /** The flat theme tokens the sidebar draws with; OpenCode 1's `theme.current` has them all. */
48
+
49
+ // Tone -> theme token. Theme tokens only — never hardcoded colors. Fallbacks
50
+ // resolve to other theme tokens so the sidebar always tracks the active theme.
51
+ function toneColor(theme, tone) {
52
+ switch (tone) {
53
+ case 'ok':
54
+ return theme.success ?? theme.accent;
55
+ case 'warn':
56
+ return theme.warning ?? theme.accent;
57
+ case 'err':
58
+ return theme.error ?? theme.accent;
59
+ case 'muted':
60
+ return theme.textMuted ?? theme.text;
61
+ case 'accent':
62
+ return theme.accent ?? theme.text;
63
+ default:
64
+ return theme.text;
65
+ }
66
+ }
67
+ function usageTone(usedPct, appearance) {
68
+ if (usedPct < appearance.warnThreshold) return 'ok';
69
+ if (usedPct < appearance.errorThreshold) return 'warn';
70
+ return 'err';
71
+ }
72
+ const PACE_RESERVE_CHAR = '\u2592';
73
+ const PACE_DEFICIT_CHAR = '\u2593';
74
+
75
+ // Variant C pacing bar: normal fill up to min(used, pace), then a pace
76
+ // segment covering the gap — headroom being banked (reserve, ok tone) or the
77
+ // overshoot itself (deficit, err tone) — then empty cells. Without pacing the
78
+ // bar renders as a single fill+empty pair, identical to the pre-pacing look.
79
+ // Empty text nodes still occupy a phantom cell in the opentui flex row, so
80
+ // zero-length segments are dropped from every return path (plain and pacing).
81
+ function quotaBarSegments(usedPct, appearance, pacing) {
82
+ const width = appearance.barWidth;
83
+ const cells = pct => Math.max(0, Math.min(Math.round(pct / 100 * width), width));
84
+ const usedCells = cells(usedPct);
85
+ const fillTone = usageTone(usedPct, appearance);
86
+ const plain = [{
87
+ text: appearance.barFilledChar.repeat(usedCells),
88
+ tone: fillTone
89
+ }, {
90
+ text: appearance.barEmptyChar.repeat(width - usedCells),
91
+ tone: fillTone
92
+ }];
93
+ if (!pacing) return plain.filter(segment => segment.text.length > 0);
94
+ const paceCells = cells(pacing.pacePercent);
95
+ const lo = Math.min(usedCells, paceCells);
96
+ const hi = Math.max(usedCells, paceCells);
97
+ if (hi === lo) return plain.filter(segment => segment.text.length > 0);
98
+ const overspent = usedCells > paceCells;
99
+ return [{
100
+ text: appearance.barFilledChar.repeat(lo),
101
+ tone: fillTone
102
+ }, {
103
+ text: (overspent ? PACE_DEFICIT_CHAR : PACE_RESERVE_CHAR).repeat(hi - lo),
104
+ tone: overspent ? 'err' : 'ok'
105
+ }, {
106
+ text: appearance.barEmptyChar.repeat(width - hi),
107
+ tone: fillTone
108
+ }].filter(segment => segment.text.length > 0);
109
+ }
110
+ function formatResetIn(resetsAt, now) {
111
+ if (!resetsAt) return '';
112
+ const ms = new Date(resetsAt).getTime() - now;
113
+ if (ms <= 0) return 'now';
114
+ const mins = Math.floor(ms / 60_000);
115
+ if (mins < 60) return `${mins}m`;
116
+ const hrs = Math.floor(mins / 60);
117
+ if (hrs < 24) {
118
+ const rm = mins % 60;
119
+ return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h`;
120
+ }
121
+ const days = Math.floor(hrs / 24);
122
+ const rh = hrs % 24;
123
+ return rh > 0 ? `${days}d${rh}h` : `${days}d`;
124
+ }
125
+ function formatUntil(until, now) {
126
+ if (!until) return '';
127
+ const ms = until - now;
128
+ if (ms <= 0) return 'now';
129
+ const mins = Math.ceil(ms / 60_000);
130
+ if (mins < 60) return `${mins}m`;
131
+ const hrs = Math.floor(mins / 60);
132
+ const rm = mins % 60;
133
+ return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h`;
134
+ }
135
+
136
+ // --- Reusable components (aft-style) ---------------------------------------
137
+
138
+ function SectionHeader(props) {
139
+ return (() => {
140
+ var _el$ = _$createElement("box"),
141
+ _el$2 = _$createElement("text"),
142
+ _el$3 = _$createElement("b");
143
+ _$insertNode(_el$, _el$2);
144
+ _$setProp(_el$, "width", '100%');
145
+ _$insertNode(_el$2, _el$3);
146
+ _$insert(_el$3, () => props.title);
147
+ _$effect(_p$ => {
148
+ var _v$ = props.marginTop ?? 1,
149
+ _v$2 = props.theme.text;
150
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$, "marginTop", _v$, _p$.e));
151
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "fg", _v$2, _p$.t));
152
+ return _p$;
153
+ }, {
154
+ e: undefined,
155
+ t: undefined
156
+ });
157
+ return _el$;
158
+ })();
159
+ }
160
+ function StatRow(props) {
161
+ return (() => {
162
+ var _el$4 = _$createElement("box"),
163
+ _el$5 = _$createElement("text"),
164
+ _el$6 = _$createElement("text"),
165
+ _el$7 = _$createElement("b");
166
+ _$insertNode(_el$4, _el$5);
167
+ _$insertNode(_el$4, _el$6);
168
+ _$setProp(_el$4, "width", '100%');
169
+ _$setProp(_el$4, "flexDirection", 'row');
170
+ _$setProp(_el$4, "justifyContent", 'space-between');
171
+ _$insert(_el$5, () => props.label);
172
+ _$insertNode(_el$6, _el$7);
173
+ _$insert(_el$7, () => props.value);
174
+ _$effect(_p$ => {
175
+ var _v$3 = props.theme.textMuted,
176
+ _v$4 = toneColor(props.theme, props.tone ?? 'text');
177
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$5, "fg", _v$3, _p$.e));
178
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$6, "fg", _v$4, _p$.t));
179
+ return _p$;
180
+ }, {
181
+ e: undefined,
182
+ t: undefined
183
+ });
184
+ return _el$4;
185
+ })();
186
+ }
187
+
188
+ // Compact row for the collapsed view: muted label left, caller-provided value
189
+ // right. Mirrors StatRow's layout so columns line up.
190
+ function CollapsedRow(props) {
191
+ return (() => {
192
+ var _el$8 = _$createElement("box"),
193
+ _el$9 = _$createElement("text");
194
+ _$insertNode(_el$8, _el$9);
195
+ _$setProp(_el$8, "width", '100%');
196
+ _$setProp(_el$8, "flexDirection", 'row');
197
+ _$setProp(_el$8, "justifyContent", 'space-between');
198
+ _$insert(_el$9, () => props.label);
199
+ _$insert(_el$8, () => props.children, null);
200
+ _$effect(_$p => _$setProp(_el$9, "fg", props.theme.textMuted, _$p));
201
+ return _el$8;
202
+ })();
203
+ }
204
+ function CollapsedAccountRow(props) {
205
+ const [width, setWidth] = createSignal(40);
206
+ let row;
207
+ const summary = () => getCollapsedQuotaSummary(props.account.quota, props.now);
208
+ const value = () => summary().text ?? '—';
209
+ const labelWidth = () => Math.max(0, width() - Bun.stringWidth(value()) - 1);
210
+ const dim = () => !props.account.active || props.account.quotaDated || summary().stale;
211
+ const attributes = () => dim() ? TextAttributes.DIM : TextAttributes.BOLD;
212
+ const color = () => dim() ? props.theme.textMuted : toneColor(props.theme, props.tone);
213
+ return (() => {
214
+ var _el$0 = _$createElement("box"),
215
+ _el$12 = _$createElement("text");
216
+ _$insertNode(_el$0, _el$12);
217
+ _$use(element => {
218
+ row = element;
219
+ setWidth(element.width || 40);
220
+ }, _el$0);
221
+ _$setProp(_el$0, "width", '100%');
222
+ _$setProp(_el$0, "height", 1);
223
+ _$setProp(_el$0, "flexDirection", 'row');
224
+ _$setProp(_el$0, "onSizeChange", () => setWidth(row?.width || 40));
225
+ _$insert(_el$0, _$createComponent(Show, {
226
+ get when() {
227
+ return labelWidth() > 0;
228
+ },
229
+ get children() {
230
+ return [(() => {
231
+ var _el$1 = _$createElement("text");
232
+ _$setProp(_el$1, "flexShrink", 0);
233
+ _$setProp(_el$1, "wrapMode", 'none');
234
+ _$insert(_el$1, () => formatQuotaRowLabel(props.account.name, labelWidth()));
235
+ _$effect(_p$ => {
236
+ var _v$5 = labelWidth(),
237
+ _v$6 = color(),
238
+ _v$7 = attributes();
239
+ _v$5 !== _p$.e && (_p$.e = _$setProp(_el$1, "width", _v$5, _p$.e));
240
+ _v$6 !== _p$.t && (_p$.t = _$setProp(_el$1, "fg", _v$6, _p$.t));
241
+ _v$7 !== _p$.a && (_p$.a = _$setProp(_el$1, "attributes", _v$7, _p$.a));
242
+ return _p$;
243
+ }, {
244
+ e: undefined,
245
+ t: undefined,
246
+ a: undefined
247
+ });
248
+ return _el$1;
249
+ })(), (() => {
250
+ var _el$10 = _$createElement("text");
251
+ _$insertNode(_el$10, _$createTextNode(` `));
252
+ _$setProp(_el$10, "width", 1);
253
+ _$setProp(_el$10, "flexShrink", 0);
254
+ return _el$10;
255
+ })()];
256
+ }
257
+ }), _el$12);
258
+ _$setProp(_el$12, "flexShrink", 0);
259
+ _$setProp(_el$12, "wrapMode", 'none');
260
+ _$insert(_el$12, value);
261
+ _$effect(_p$ => {
262
+ var _v$8 = Bun.stringWidth(value()),
263
+ _v$9 = color(),
264
+ _v$0 = attributes();
265
+ _v$8 !== _p$.e && (_p$.e = _$setProp(_el$12, "width", _v$8, _p$.e));
266
+ _v$9 !== _p$.t && (_p$.t = _$setProp(_el$12, "fg", _v$9, _p$.t));
267
+ _v$0 !== _p$.a && (_p$.a = _$setProp(_el$12, "attributes", _v$0, _p$.a));
268
+ return _p$;
269
+ }, {
270
+ e: undefined,
271
+ t: undefined,
272
+ a: undefined
273
+ });
274
+ return _el$0;
275
+ })();
276
+ }
277
+
278
+ // Quota window row: muted label left, tone-colored bar + shared figure right,
279
+ // with an optional muted reset suffix. When pacing data is present, the bar
280
+ // gains a pace segment and an off-pace window adds a muted subline with the
281
+ // reserve/deficit delta and the projected runout.
282
+ function QuotaRow(props) {
283
+ const used = () => props.usedPercent ?? 0;
284
+ const reset = () => props.part?.resetIn;
285
+ const paceLine = () => {
286
+ const pacing = props.pacing;
287
+ if (!pacing || pacing.state === 'on-pace') return null;
288
+ const pct = Math.round(Math.abs(pacing.deltaPercent));
289
+ if (pacing.state === 'reserve') return `reserve ${pct}% \u00b7 lasts`;
290
+ return pacing.runsOutAt ? `deficit ${pct}% \u00b7 out in ${formatResetIn(pacing.runsOutAt, props.now)}` : `deficit ${pct}% \u00b7 lasts`;
291
+ };
292
+ return _$createComponent(Show, {
293
+ get when() {
294
+ return props.part;
295
+ },
296
+ get fallback() {
297
+ return (() => {
298
+ var _el$21 = _$createElement("box"),
299
+ _el$22 = _$createElement("text"),
300
+ _el$23 = _$createElement("text");
301
+ _$insertNode(_el$21, _el$22);
302
+ _$insertNode(_el$21, _el$23);
303
+ _$setProp(_el$21, "width", '100%');
304
+ _$setProp(_el$21, "flexDirection", 'row');
305
+ _$insert(_el$22, () => `${formatQuotaRowLabel(props.label)} `);
306
+ _$insertNode(_el$23, _$createTextNode(`—`));
307
+ _$effect(_p$ => {
308
+ var _v$13 = props.theme.textMuted,
309
+ _v$14 = props.theme.textMuted;
310
+ _v$13 !== _p$.e && (_p$.e = _$setProp(_el$22, "fg", _v$13, _p$.e));
311
+ _v$14 !== _p$.t && (_p$.t = _$setProp(_el$23, "fg", _v$14, _p$.t));
312
+ return _p$;
313
+ }, {
314
+ e: undefined,
315
+ t: undefined
316
+ });
317
+ return _el$21;
318
+ })();
319
+ },
320
+ get children() {
321
+ return [(() => {
322
+ var _el$13 = _$createElement("box"),
323
+ _el$14 = _$createElement("box"),
324
+ _el$15 = _$createElement("text"),
325
+ _el$16 = _$createElement("text");
326
+ _$insertNode(_el$13, _el$14);
327
+ _$setProp(_el$13, "width", '100%');
328
+ _$setProp(_el$13, "flexDirection", 'row');
329
+ _$setProp(_el$13, "justifyContent", 'space-between');
330
+ _$insertNode(_el$14, _el$15);
331
+ _$insertNode(_el$14, _el$16);
332
+ _$setProp(_el$14, "flexDirection", 'row');
333
+ _$insert(_el$15, () => `${formatQuotaRowLabel(props.label)} `);
334
+ _$insert(_el$14, _$createComponent(Index, {
335
+ get each() {
336
+ return quotaBarSegments(used(), props.appearance, props.pacing);
337
+ },
338
+ children: segment => (() => {
339
+ var _el$25 = _$createElement("text");
340
+ _$insert(_el$25, () => segment().text);
341
+ _$effect(_$p => _$setProp(_el$25, "fg", toneColor(props.theme, segment().tone), _$p));
342
+ return _el$25;
343
+ })()
344
+ }), _el$16);
345
+ _$insert(_el$16, () => ` ${props.part?.figure}${props.binding ? ' •' : ''}`);
346
+ _$insert(_el$13, _$createComponent(Show, {
347
+ get when() {
348
+ return reset();
349
+ },
350
+ get children() {
351
+ var _el$17 = _$createElement("text");
352
+ _$insert(_el$17, reset);
353
+ _$effect(_$p => _$setProp(_el$17, "fg", props.theme.textMuted, _$p));
354
+ return _el$17;
355
+ }
356
+ }), null);
357
+ _$effect(_p$ => {
358
+ var _v$1 = props.theme.textMuted,
359
+ _v$10 = toneColor(props.theme, usageTone(used(), props.appearance));
360
+ _v$1 !== _p$.e && (_p$.e = _$setProp(_el$15, "fg", _v$1, _p$.e));
361
+ _v$10 !== _p$.t && (_p$.t = _$setProp(_el$16, "fg", _v$10, _p$.t));
362
+ return _p$;
363
+ }, {
364
+ e: undefined,
365
+ t: undefined
366
+ });
367
+ return _el$13;
368
+ })(), _$createComponent(Show, {
369
+ get when() {
370
+ return paceLine();
371
+ },
372
+ get children() {
373
+ var _el$18 = _$createElement("box"),
374
+ _el$19 = _$createElement("text"),
375
+ _el$20 = _$createElement("text");
376
+ _$insertNode(_el$18, _el$19);
377
+ _$insertNode(_el$18, _el$20);
378
+ _$setProp(_el$18, "width", '100%');
379
+ _$setProp(_el$18, "flexDirection", 'row');
380
+ _$insert(_el$19, () => formatQuotaRowLabel('', 11));
381
+ _$insert(_el$20, paceLine);
382
+ _$effect(_p$ => {
383
+ var _v$11 = props.theme.textMuted,
384
+ _v$12 = toneColor(props.theme, props.pacing?.state === 'deficit' ? 'warn' : 'muted');
385
+ _v$11 !== _p$.e && (_p$.e = _$setProp(_el$19, "fg", _v$11, _p$.e));
386
+ _v$12 !== _p$.t && (_p$.t = _$setProp(_el$20, "fg", _v$12, _p$.t));
387
+ return _p$;
388
+ }, {
389
+ e: undefined,
390
+ t: undefined
391
+ });
392
+ return _el$18;
393
+ }
394
+ })];
395
+ }
396
+ });
397
+ }
398
+
399
+ // The shared credit figure followed by the spent and limit amounts:
400
+ // `credits 75% left · $25.00/$100.00`. When that line is wider than the
401
+ // measured width, the amounts drop their cents (`$1,250/$5,000`) instead of
402
+ // wrapping onto a second line.
403
+ function creditLine(figure, extraUsage, width) {
404
+ const amounts = formatCreditAmounts(extraUsage);
405
+ if (!amounts) return figure;
406
+ const line = `${figure} · ${amounts}`;
407
+ if (width === undefined || Bun.stringWidth(line) <= width) return line;
408
+ return `${figure} · ${formatCreditAmounts(extraUsage, {
409
+ wholeUnits: true
410
+ })}`;
411
+ }
412
+
413
+ // Account block: header row (name + status word) then per-window quota rows.
414
+ // Pacing is computed here — per window, against the wall clock at render time
415
+ // (re-evaluated on every state poll) — and disabled by passing false.
416
+ export function AccountBlock(props) {
417
+ const statusWord = () => props.quotaDated && props.quota ? 'dated' : props.needsReauth ? 're-login' : props.vaultReauth ? 'needs sign-in in the vault' : props.active ? 'active' : 'idle';
418
+ const statusTone = () => props.needsReauth || props.vaultReauth || props.stateLine ? 'warn' : props.active ? 'ok' : 'muted';
419
+ const pacingFor = (window, windowMs) => props.pacingEnabled && !props.quotaDated && window ? computeQuotaPacing(window, windowMs, props.now) : null;
420
+ const parts = () => quotaWindowParts(quotaDisplayProjection(props.quota, props.now), {
421
+ now: props.now
422
+ });
423
+ const partFor = name => parts().windows.find(part => part.name === name);
424
+ return (() => {
425
+ var _el$26 = _$createElement("box"),
426
+ _el$27 = _$createElement("box"),
427
+ _el$28 = _$createElement("text"),
428
+ _el$29 = _$createElement("b"),
429
+ _el$30 = _$createElement("text"),
430
+ _el$31 = _$createElement("b");
431
+ _$insertNode(_el$26, _el$27);
432
+ _$setProp(_el$26, "width", '100%');
433
+ _$setProp(_el$26, "flexDirection", 'column');
434
+ _$insertNode(_el$27, _el$28);
435
+ _$insertNode(_el$27, _el$30);
436
+ _$setProp(_el$27, "width", '100%');
437
+ _$setProp(_el$27, "flexDirection", 'row');
438
+ _$setProp(_el$27, "justifyContent", 'space-between');
439
+ _$insertNode(_el$28, _el$29);
440
+ _$insert(_el$29, () => `${props.name}${props.tierLabel ? ` · ${props.tierLabel}` : ''}`);
441
+ _$insertNode(_el$30, _el$31);
442
+ _$insert(_el$31, statusWord);
443
+ _$insert(_el$26, _$createComponent(Show, {
444
+ get when() {
445
+ return props.stateLine;
446
+ },
447
+ get children() {
448
+ var _el$32 = _$createElement("text");
449
+ _$insert(_el$32, () => props.stateLine);
450
+ _$effect(_$p => _$setProp(_el$32, "fg", props.theme.textMuted, _$p));
451
+ return _el$32;
452
+ }
453
+ }), null);
454
+ _$insert(_el$26, _$createComponent(Show, {
455
+ get when() {
456
+ return props.quota;
457
+ },
458
+ get fallback() {
459
+ return (() => {
460
+ var _el$36 = _$createElement("text");
461
+ _$insertNode(_el$36, _$createTextNode(`checking…`));
462
+ _$effect(_$p => _$setProp(_el$36, "fg", props.theme.textMuted, _$p));
463
+ return _el$36;
464
+ })();
465
+ },
466
+ get children() {
467
+ return [_$createComponent(QuotaRow, {
468
+ get theme() {
469
+ return props.theme;
470
+ },
471
+ get appearance() {
472
+ return props.appearance;
473
+ },
474
+ label: "5h",
475
+ get part() {
476
+ return partFor('5h');
477
+ },
478
+ get usedPercent() {
479
+ return props.quota?.five_hour?.usedPercent;
480
+ },
481
+ get pacing() {
482
+ return pacingFor(props.quota?.five_hour, FIVE_HOUR_MS);
483
+ },
484
+ get binding() {
485
+ return props.quota?.bindingWindow === 'five_hour';
486
+ },
487
+ get now() {
488
+ return props.now;
489
+ }
490
+ }), _$createComponent(QuotaRow, {
491
+ get theme() {
492
+ return props.theme;
493
+ },
494
+ get appearance() {
495
+ return props.appearance;
496
+ },
497
+ label: "7d",
498
+ get part() {
499
+ return partFor('7d');
500
+ },
501
+ get usedPercent() {
502
+ return props.quota?.seven_day?.usedPercent;
503
+ },
504
+ get pacing() {
505
+ return pacingFor(props.quota?.seven_day, SEVEN_DAY_MS);
506
+ },
507
+ get binding() {
508
+ return props.quota?.bindingWindow === 'seven_day';
509
+ },
510
+ get now() {
511
+ return props.now;
512
+ }
513
+ }), _$createComponent(For, {
514
+ get each() {
515
+ return props.quota?.scoped ?? [];
516
+ },
517
+ children: window => _$createComponent(QuotaRow, {
518
+ get theme() {
519
+ return props.theme;
520
+ },
521
+ get appearance() {
522
+ return props.appearance;
523
+ },
524
+ get label() {
525
+ return partFor(window.title)?.name ?? window.title;
526
+ },
527
+ get part() {
528
+ return partFor(window.title);
529
+ },
530
+ get usedPercent() {
531
+ return window.usedPercent;
532
+ },
533
+ get pacing() {
534
+ return pacingFor(window, SEVEN_DAY_MS);
535
+ },
536
+ get binding() {
537
+ return props.quota?.bindingWindow === window.id;
538
+ },
539
+ get now() {
540
+ return props.now;
541
+ }
542
+ })
543
+ }), _$createComponent(Show, {
544
+ get when() {
545
+ return partFor('credits');
546
+ },
547
+ children: budget => {
548
+ // The line box's laid-out width, read when it mounts and on each
549
+ // resize. It is undefined (amounts keep their cents) until layout.
550
+ const [width, setWidth] = createSignal();
551
+ let line;
552
+ return (() => {
553
+ var _el$38 = _$createElement("box"),
554
+ _el$39 = _$createElement("text");
555
+ _$insertNode(_el$38, _el$39);
556
+ _$use(element => {
557
+ line = element;
558
+ setWidth(element.width || undefined);
559
+ }, _el$38);
560
+ _$setProp(_el$38, "width", '100%');
561
+ _$setProp(_el$38, "onSizeChange", () => setWidth(line?.width || undefined));
562
+ _$insert(_el$39, () => creditLine(budget().figure, props.quota?.extraUsage, width()));
563
+ _$effect(_$p => _$setProp(_el$39, "fg", toneColor(props.theme, props.quota?.extraUsage?.severity === 'critical' ? 'err' : props.quota?.extraUsage?.severity === 'warning' || props.quota?.extraUsage?.exhausted ? 'warn' : 'muted'), _$p));
564
+ return _el$38;
565
+ })();
566
+ }
567
+ }), _$createComponent(Show, {
568
+ get when() {
569
+ return parts().age;
570
+ },
571
+ get children() {
572
+ var _el$33 = _$createElement("text");
573
+ _$insert(_el$33, () => parts().age);
574
+ _$effect(_$p => _$setProp(_el$33, "fg", props.theme.textMuted, _$p));
575
+ return _el$33;
576
+ }
577
+ }), _$createComponent(Show, {
578
+ get when() {
579
+ return props.quota?.fallbackAdvised === true;
580
+ },
581
+ get children() {
582
+ var _el$34 = _$createElement("text");
583
+ _$insertNode(_el$34, _$createTextNode(`→ fallback advised`));
584
+ _$effect(_$p => _$setProp(_el$34, "fg", props.theme.textMuted, _$p));
585
+ return _el$34;
586
+ }
587
+ })];
588
+ }
589
+ }), null);
590
+ _$effect(_p$ => {
591
+ var _v$15 = props.marginTop ?? 0,
592
+ _v$16 = props.theme.text,
593
+ _v$17 = toneColor(props.theme, statusTone());
594
+ _v$15 !== _p$.e && (_p$.e = _$setProp(_el$26, "marginTop", _v$15, _p$.e));
595
+ _v$16 !== _p$.t && (_p$.t = _$setProp(_el$28, "fg", _v$16, _p$.t));
596
+ _v$17 !== _p$.a && (_p$.a = _$setProp(_el$30, "fg", _v$17, _p$.a));
597
+ return _p$;
598
+ }, {
599
+ e: undefined,
600
+ t: undefined,
601
+ a: undefined
602
+ });
603
+ return _el$26;
604
+ })();
605
+ }
606
+ // The TUI may unmount and remount sidebar_content when the user switches
607
+ // views (e.g. main -> subagent -> main). A remount re-runs the component
608
+ // body, so any signal created inside the component would reset to its
609
+ // seed. The controller lives in the plugin closure (process lifetime)
610
+ // and owns the durable prefs/collapse signals plus the single shared
611
+ // watcher subscription, so collapse and live pref reloads survive the
612
+ // remount. No effects or memos here — those need a Solid owner, so the
613
+ // poll-interval createEffect stays inside the component.
614
+ export function createSidebarController(initialPrefs) {
615
+ const [prefs, setPrefs] = createSignal(initialPrefs);
616
+ const seedCollapsed = initialPrefs.rememberCollapsed && initialPrefs.collapsed != null ? initialPrefs.collapsed : initialPrefs.startCollapsed;
617
+ const [collapsed, setCollapsed] = createSignal(seedCollapsed);
618
+ let lastPersistedCollapsed = initialPrefs.collapsed;
619
+ let lastApplied = JSON.stringify(initialPrefs);
620
+
621
+ // OpenCode 1 keeps the watcher for the process lifetime; OpenCode 2 calls
622
+ // dispose when the plugin unloads. Collapse guard mirrors the race-fix in toggleCollapsed:
623
+ // lastPersistedCollapsed is advanced only once our own write lands, so
624
+ // watcher echoes of the previous persisted value are rejected by the
625
+ // `!==` check and cannot revert a user click.
626
+ const dispose = watchTuiPreferences(() => {
627
+ void (async () => {
628
+ const next = resolveAnthropicAuthPrefs(await readTuiPreferencesFile());
629
+ const serialized = JSON.stringify(next);
630
+ if (serialized === lastApplied) return;
631
+ lastApplied = serialized;
632
+ setPrefs(next);
633
+ if (next.rememberCollapsed && next.collapsed != null && next.collapsed !== lastPersistedCollapsed) {
634
+ lastPersistedCollapsed = next.collapsed;
635
+ setCollapsed(next.collapsed);
636
+ }
637
+ })();
638
+ });
639
+ function toggleCollapsed() {
640
+ const next = !collapsed();
641
+ setCollapsed(next);
642
+ if (prefs().rememberCollapsed) {
643
+ void queueTuiPreferenceUpdate(PLUGIN_KEY, ['collapsed'], next).then(() => {
644
+ lastPersistedCollapsed = next;
645
+ });
646
+ }
647
+ }
648
+ return {
649
+ prefs,
650
+ collapsed,
651
+ toggleCollapsed,
652
+ dispose
653
+ };
654
+ }
655
+
656
+ /** The sidebar panel for one session; the host owns polling and the theme. */
657
+ export function ClaudeSidebar(props) {
658
+ const prefs = props.controller.prefs;
659
+ const collapsed = props.controller.collapsed;
660
+ const state = props.state;
661
+ const theme = props.theme;
662
+ // Countdowns, reading ages and pacing read this clock, so they advance on
663
+ // each tick and state change without a new answer or rebuilt rows.
664
+ const [tick, setTick] = createSignal(0);
665
+ createEffect(() => {
666
+ const timer = setInterval(() => setTick(n => n + 1), prefs().pollMs);
667
+ onCleanup(() => clearInterval(timer));
668
+ });
669
+ const now = createMemo(() => {
670
+ tick();
671
+ state();
672
+ return Date.now();
673
+ });
674
+ const enabledFallbacks = () => (state().fallbacks ?? []).filter(f => f.enabled || f.custodyState && f.custodyState !== 'off');
675
+ const hasData = () => state().main?.quota != null || state().main?.stateLine != null || enabledFallbacks().length > 0;
676
+ const headerLabel = () => {
677
+ const name = prefs().header.label;
678
+ return !hasData() ? name : collapsed() ? `\u25b6 ${name}` : `\u25bc ${name}`;
679
+ };
680
+ const activeAccount = () => resolveActiveAccount(state());
681
+ const activeQuotaSummary = () => getCollapsedQuotaSummary(activeAccount().quota, now());
682
+ const activePacingDeficit = () => {
683
+ if (!prefs().sections.pacing) return false;
684
+ if (activeAccount().quotaDated) return null;
685
+ const quota = activeAccount().quota;
686
+ if (!quota) return false;
687
+ const windows = [[quota.five_hour, FIVE_HOUR_MS], [quota.seven_day, SEVEN_DAY_MS], ...(quota.scoped ?? []).map(window => [window, SEVEN_DAY_MS])];
688
+ return windows.some(([w, ms]) => w != null && computeQuotaPacing(w, ms, now())?.state === 'deficit');
689
+ };
690
+ const activeQuotaTone = () => {
691
+ const summary = activeQuotaSummary();
692
+ const values = [summary.fiveHourUsedPercent, summary.sevenDayUsedPercent, ...summary.scopedUsedPercents].filter(value => value != null);
693
+ // Pacing deficit is an advisory projection, not actual quota exhaustion,
694
+ // so it can only BUMP the usage tone up to warn at most — never soften a
695
+ // real warn/err usage reading and never paint a true red.
696
+ const base = values.length > 0 ? usageTone(Math.max(...values), prefs().appearance) : 'muted';
697
+ if (!activePacingDeficit()) return base;
698
+ return base === 'ok' || base === 'muted' ? 'warn' : base;
699
+ };
700
+ const quotaBackedOff = () => state().main?.quotaBackedOff === true;
701
+ const refreshBackedOff = () => state().main?.refreshBackedOff === true;
702
+ const needsReauth = () => enabledFallbacks().some(f => f.needsReauth);
703
+ const vaultReauth = () => enabledFallbacks().some(f => f.vaultReauth);
704
+ const degraded = () => quotaBackedOff() || refreshBackedOff() || needsReauth() || vaultReauth();
705
+ const fableRecoverySummary = () => getFableRecoverySummary(state(), props.sessionId);
706
+ const fableRecoveryTone = () => state().fableRecoveries?.find(recovery => recovery.sessionId === props.sessionId)?.mode === 'fable' ? 'ok' : 'warn';
707
+ const cacheKeep = () => state().cacheKeep;
708
+ const showCache = () => prefs().sections.cache && cacheKeep() != null && cacheKeep()?.window != null;
709
+ const prime = () => state().prime;
710
+ const showPrime = () => prime() != null;
711
+ const primeHasError = () => prime()?.accounts.some(account => account.lastResult === 'error') ?? false;
712
+ const relayValue = () => {
713
+ const r = state().relay;
714
+ if (!r) return '\u2014';
715
+ return `${r.transport} \u00b7 ${r.enabled ? 'on' : 'off'}`;
716
+ };
717
+ return (() => {
718
+ var _el$40 = _$createElement("box"),
719
+ _el$41 = _$createElement("box"),
720
+ _el$42 = _$createElement("box"),
721
+ _el$43 = _$createElement("text"),
722
+ _el$44 = _$createElement("b");
723
+ _$insertNode(_el$40, _el$41);
724
+ _$setProp(_el$40, "width", '100%');
725
+ _$setProp(_el$40, "flexDirection", 'column');
726
+ _$setProp(_el$40, "border", SINGLE_BORDER);
727
+ _$setProp(_el$40, "paddingTop", 1);
728
+ _$setProp(_el$40, "paddingBottom", 1);
729
+ _$setProp(_el$40, "paddingLeft", 1);
730
+ _$setProp(_el$40, "paddingRight", 1);
731
+ _$insertNode(_el$41, _el$42);
732
+ _$setProp(_el$41, "width", '100%');
733
+ _$setProp(_el$41, "flexDirection", 'row');
734
+ _$setProp(_el$41, "justifyContent", 'space-between');
735
+ _$setProp(_el$41, "alignItems", 'center');
736
+ _$setProp(_el$41, "onMouseDown", () => {
737
+ if (hasData()) props.controller.toggleCollapsed();
738
+ });
739
+ _$insertNode(_el$42, _el$43);
740
+ _$setProp(_el$42, "paddingLeft", 1);
741
+ _$setProp(_el$42, "paddingRight", 1);
742
+ _$insertNode(_el$43, _el$44);
743
+ _$insert(_el$44, headerLabel);
744
+ _$insert(_el$41, _$createComponent(Show, {
745
+ get when() {
746
+ return degraded();
747
+ },
748
+ get fallback() {
749
+ return _$createComponent(Show, {
750
+ get when() {
751
+ return prefs().header.showVersion && PLUGIN_VERSION !== '';
752
+ },
753
+ get children() {
754
+ var _el$52 = _$createElement("text");
755
+ _$insert(_el$52, `v${PLUGIN_VERSION}`);
756
+ _$effect(_$p => _$setProp(_el$52, "fg", theme().textMuted, _$p));
757
+ return _el$52;
758
+ }
759
+ });
760
+ },
761
+ get children() {
762
+ var _el$45 = _$createElement("box"),
763
+ _el$46 = _$createElement("text"),
764
+ _el$47 = _$createElement("b");
765
+ _$insertNode(_el$45, _el$46);
766
+ _$setProp(_el$45, "paddingLeft", 1);
767
+ _$setProp(_el$45, "paddingRight", 1);
768
+ _$insertNode(_el$46, _el$47);
769
+ _$insertNode(_el$47, _$createTextNode(`LIMITED`));
770
+ _$effect(_p$ => {
771
+ var _v$18 = theme().warning,
772
+ _v$19 = theme().background;
773
+ _v$18 !== _p$.e && (_p$.e = _$setProp(_el$45, "backgroundColor", _v$18, _p$.e));
774
+ _v$19 !== _p$.t && (_p$.t = _$setProp(_el$46, "fg", _v$19, _p$.t));
775
+ return _p$;
776
+ }, {
777
+ e: undefined,
778
+ t: undefined
779
+ });
780
+ return _el$45;
781
+ }
782
+ }), null);
783
+ _$insert(_el$40, _$createComponent(Show, {
784
+ get when() {
785
+ return _$memo(() => !!collapsed())() && hasData();
786
+ },
787
+ get children() {
788
+ return [_$createComponent(Index, {
789
+ get each() {
790
+ return getCollapsedAccounts(state(), prefs().sections.fallbackAccounts);
791
+ },
792
+ children: account => _$createComponent(CollapsedAccountRow, {
793
+ get theme() {
794
+ return theme();
795
+ },
796
+ get account() {
797
+ return account();
798
+ },
799
+ get tone() {
800
+ return activeQuotaTone();
801
+ },
802
+ get now() {
803
+ return now();
804
+ }
805
+ })
806
+ }), _$createComponent(Show, {
807
+ get when() {
808
+ return state().fastMode;
809
+ },
810
+ get children() {
811
+ return _$createComponent(CollapsedRow, {
812
+ get theme() {
813
+ return theme();
814
+ },
815
+ label: "Mode",
816
+ get children() {
817
+ var _el$49 = _$createElement("text"),
818
+ _el$50 = _$createElement("b");
819
+ _$insertNode(_el$49, _el$50);
820
+ _$insertNode(_el$50, _$createTextNode(`fast`));
821
+ _$effect(_$p => _$setProp(_el$49, "fg", toneColor(theme(), 'accent'), _$p));
822
+ return _el$49;
823
+ }
824
+ });
825
+ }
826
+ }), _$createComponent(Show, {
827
+ get when() {
828
+ return fableRecoverySummary();
829
+ },
830
+ children: summary => _$createComponent(CollapsedRow, {
831
+ get theme() {
832
+ return theme();
833
+ },
834
+ label: "Recovery",
835
+ get children() {
836
+ var _el$53 = _$createElement("text"),
837
+ _el$54 = _$createElement("b");
838
+ _$insertNode(_el$53, _el$54);
839
+ _$insert(_el$54, summary);
840
+ _$effect(_$p => _$setProp(_el$53, "fg", toneColor(theme(), fableRecoveryTone()), _$p));
841
+ return _el$53;
842
+ }
843
+ })
844
+ })];
845
+ }
846
+ }), null);
847
+ _$insert(_el$40, _$createComponent(Show, {
848
+ get when() {
849
+ return !collapsed() || !hasData();
850
+ },
851
+ get children() {
852
+ return [_$createComponent(Show, {
853
+ get when() {
854
+ return hasData();
855
+ },
856
+ get fallback() {
857
+ return (() => {
858
+ var _el$55 = _$createElement("box"),
859
+ _el$56 = _$createElement("text");
860
+ _$insertNode(_el$55, _el$56);
861
+ _$setProp(_el$55, "marginTop", 1);
862
+ _$setProp(_el$55, "width", '100%');
863
+ _$insertNode(_el$56, _$createTextNode(`Waiting for quota…`));
864
+ _$effect(_$p => _$setProp(_el$56, "fg", theme().textMuted, _$p));
865
+ return _el$55;
866
+ })();
867
+ },
868
+ get children() {
869
+ return _$createComponent(Show, {
870
+ get when() {
871
+ return prefs().sections.quota;
872
+ },
873
+ get children() {
874
+ return [_$createComponent(SectionHeader, {
875
+ get theme() {
876
+ return theme();
877
+ },
878
+ title: "Quota"
879
+ }), _$createComponent(AccountBlock, {
880
+ get theme() {
881
+ return theme();
882
+ },
883
+ get appearance() {
884
+ return prefs().appearance;
885
+ },
886
+ name: "main",
887
+ get quota() {
888
+ return state().main?.quota;
889
+ },
890
+ get active() {
891
+ return state().activeId === 'main';
892
+ },
893
+ get pacingEnabled() {
894
+ return prefs().sections.pacing;
895
+ },
896
+ get tierLabel() {
897
+ return state().main?.tierLabel;
898
+ },
899
+ get stateLine() {
900
+ return state().main?.stateLine;
901
+ },
902
+ get quotaDated() {
903
+ return state().main?.quotaDated;
904
+ },
905
+ get now() {
906
+ return now();
907
+ }
908
+ }), _$createComponent(Show, {
909
+ get when() {
910
+ return prefs().sections.fallbackAccounts;
911
+ },
912
+ get children() {
913
+ return _$createComponent(For, {
914
+ get each() {
915
+ return enabledFallbacks();
916
+ },
917
+ children: fb => _$createComponent(AccountBlock, {
918
+ get theme() {
919
+ return theme();
920
+ },
921
+ get appearance() {
922
+ return prefs().appearance;
923
+ },
924
+ get name() {
925
+ return fb.label ?? fb.id;
926
+ },
927
+ get quota() {
928
+ return fb.quota;
929
+ },
930
+ get active() {
931
+ return state().activeId === fb.id;
932
+ },
933
+ get pacingEnabled() {
934
+ return prefs().sections.pacing;
935
+ },
936
+ get tierLabel() {
937
+ return fb.tierLabel;
938
+ },
939
+ get needsReauth() {
940
+ return fb.needsReauth;
941
+ },
942
+ get vaultReauth() {
943
+ return fb.vaultReauth;
944
+ },
945
+ get stateLine() {
946
+ return fb.stateLine;
947
+ },
948
+ get quotaDated() {
949
+ return fb.quotaDated;
950
+ },
951
+ marginTop: 1,
952
+ get now() {
953
+ return now();
954
+ }
955
+ })
956
+ });
957
+ }
958
+ })];
959
+ }
960
+ });
961
+ }
962
+ }), _$createComponent(Show, {
963
+ get when() {
964
+ return prefs().sections.routing;
965
+ },
966
+ get children() {
967
+ return [_$createComponent(SectionHeader, {
968
+ get theme() {
969
+ return theme();
970
+ },
971
+ title: "Routing"
972
+ }), _$createComponent(StatRow, {
973
+ get theme() {
974
+ return theme();
975
+ },
976
+ label: "Route",
977
+ get value() {
978
+ return state().route;
979
+ },
980
+ tone: "accent"
981
+ }), _$createComponent(StatRow, {
982
+ get theme() {
983
+ return theme();
984
+ },
985
+ label: "Mode",
986
+ get value() {
987
+ return state().fastMode ? 'fast' : 'std';
988
+ },
989
+ get tone() {
990
+ return state().fastMode ? 'accent' : 'muted';
991
+ }
992
+ }), _$createComponent(StatRow, {
993
+ get theme() {
994
+ return theme();
995
+ },
996
+ label: "Relay",
997
+ get value() {
998
+ return relayValue();
999
+ },
1000
+ get tone() {
1001
+ return state().relay?.enabled ? 'ok' : 'muted';
1002
+ }
1003
+ })];
1004
+ }
1005
+ }), _$createComponent(Show, {
1006
+ get when() {
1007
+ return fableRecoverySummary();
1008
+ },
1009
+ children: summary => _$createComponent(StatRow, {
1010
+ get theme() {
1011
+ return theme();
1012
+ },
1013
+ label: "Recovery",
1014
+ get value() {
1015
+ return summary();
1016
+ },
1017
+ get tone() {
1018
+ return fableRecoveryTone();
1019
+ }
1020
+ })
1021
+ }), _$createComponent(Show, {
1022
+ get when() {
1023
+ return showCache();
1024
+ },
1025
+ get children() {
1026
+ return [_$createComponent(SectionHeader, {
1027
+ get theme() {
1028
+ return theme();
1029
+ },
1030
+ title: "Cache"
1031
+ }), _$createComponent(StatRow, {
1032
+ get theme() {
1033
+ return theme();
1034
+ },
1035
+ label: "1h cache",
1036
+ get value() {
1037
+ return `${cacheKeep()?.window} \u00b7 ${cacheKeep()?.enabled ? 'on' : 'off'}`;
1038
+ },
1039
+ get tone() {
1040
+ return cacheKeep()?.enabled ? 'ok' : 'muted';
1041
+ }
1042
+ }), _$createComponent(Show, {
1043
+ get when() {
1044
+ return (cacheKeep()?.trackedSessions ?? 0) > 0;
1045
+ },
1046
+ get children() {
1047
+ return _$createComponent(StatRow, {
1048
+ get theme() {
1049
+ return theme();
1050
+ },
1051
+ label: "Tracked",
1052
+ get value() {
1053
+ return String(cacheKeep()?.trackedSessions);
1054
+ },
1055
+ tone: "text"
1056
+ });
1057
+ }
1058
+ })];
1059
+ }
1060
+ }), _$createComponent(Show, {
1061
+ get when() {
1062
+ return showPrime();
1063
+ },
1064
+ get children() {
1065
+ return [_$createComponent(StatRow, {
1066
+ get theme() {
1067
+ return theme();
1068
+ },
1069
+ label: "Prime",
1070
+ get value() {
1071
+ return primeHasError() ? 'err' : 'on';
1072
+ },
1073
+ get tone() {
1074
+ return primeHasError() ? 'warn' : 'ok';
1075
+ }
1076
+ }), _$createComponent(For, {
1077
+ get each() {
1078
+ return prime()?.accounts ?? [];
1079
+ },
1080
+ children: account => {
1081
+ const value = () => formatPrimeAccountValue(account, now());
1082
+ return _$createComponent(StatRow, {
1083
+ get theme() {
1084
+ return theme();
1085
+ },
1086
+ get label() {
1087
+ return account.label;
1088
+ },
1089
+ get value() {
1090
+ return value().text;
1091
+ },
1092
+ get tone() {
1093
+ return value().hasError ? 'warn' : 'text';
1094
+ }
1095
+ });
1096
+ }
1097
+ })];
1098
+ }
1099
+ }), _$createComponent(Show, {
1100
+ get when() {
1101
+ return _$memo(() => !!degraded())() && prefs().sections.health;
1102
+ },
1103
+ get children() {
1104
+ return [_$createComponent(SectionHeader, {
1105
+ get theme() {
1106
+ return theme();
1107
+ },
1108
+ title: "Health"
1109
+ }), _$createComponent(Show, {
1110
+ get when() {
1111
+ return quotaBackedOff();
1112
+ },
1113
+ get children() {
1114
+ return _$createComponent(StatRow, {
1115
+ get theme() {
1116
+ return theme();
1117
+ },
1118
+ label: "Quota API",
1119
+ get value() {
1120
+ return `backoff ${formatUntil(state().main?.quotaBackoffUntil, now())}`;
1121
+ },
1122
+ tone: "warn"
1123
+ });
1124
+ }
1125
+ }), _$createComponent(Show, {
1126
+ get when() {
1127
+ return refreshBackedOff();
1128
+ },
1129
+ get children() {
1130
+ return _$createComponent(StatRow, {
1131
+ get theme() {
1132
+ return theme();
1133
+ },
1134
+ label: "Token refresh",
1135
+ get value() {
1136
+ return `backoff ${formatUntil(state().main?.refreshBackoffUntil, now())}`;
1137
+ },
1138
+ tone: "warn"
1139
+ });
1140
+ }
1141
+ })];
1142
+ }
1143
+ })];
1144
+ }
1145
+ }), null);
1146
+ _$effect(_p$ => {
1147
+ var _v$20 = theme().borderActive,
1148
+ _v$21 = theme().accent,
1149
+ _v$22 = theme().background;
1150
+ _v$20 !== _p$.e && (_p$.e = _$setProp(_el$40, "borderColor", _v$20, _p$.e));
1151
+ _v$21 !== _p$.t && (_p$.t = _$setProp(_el$42, "backgroundColor", _v$21, _p$.t));
1152
+ _v$22 !== _p$.a && (_p$.a = _$setProp(_el$43, "fg", _v$22, _p$.a));
1153
+ return _p$;
1154
+ }, {
1155
+ e: undefined,
1156
+ t: undefined,
1157
+ a: undefined
1158
+ });
1159
+ return _el$40;
1160
+ })();
1161
+ }
1162
+
1163
+ // --- OpenCode 2 -------------------------------------------------------------
1164
+
1165
+ /** The OpenCode 2 theme tokens the sidebar reads (`ResolvedTheme` subset). */
1166
+
1167
+ /** OpenCode 2's semantic theme as the flat tokens OpenCode 1 provides. */
1168
+ export function sidebarThemeFromHost(theme) {
1169
+ return {
1170
+ text: theme.text.base,
1171
+ textMuted: theme.text.muted,
1172
+ success: theme.text.feedback.success.base,
1173
+ warning: theme.text.feedback.warning.base,
1174
+ error: theme.text.feedback.error.base,
1175
+ accent: theme.text.action.primary.base,
1176
+ background: theme.background.base,
1177
+ borderActive: theme.border.base
1178
+ };
1179
+ }
1180
+
1181
+ /** The OpenCode 2 TUI context members the sidebar uses. */
1182
+
1183
+ /** Reads one session's sidebar state from the location's server. */
1184
+
1185
+ function HostSidebar(props) {
1186
+ const [state, setState] = createSignal(DEFAULT_SIDEBAR_STATE);
1187
+ const abort = new AbortController();
1188
+ let inFlight = false;
1189
+ let shown = '';
1190
+ const refresh = async () => {
1191
+ if (inFlight || abort.signal.aborted) return;
1192
+ inFlight = true;
1193
+ try {
1194
+ const next = await props.read(props.sessionId, abort.signal);
1195
+ // Every answer is stamped now; redraw only when its content changed.
1196
+ // Countdowns and ages advance on the sidebar's own clock.
1197
+ const content = JSON.stringify({
1198
+ ...next,
1199
+ lastUpdated: 0
1200
+ });
1201
+ if (!abort.signal.aborted && content !== shown) {
1202
+ shown = content;
1203
+ setState(next);
1204
+ }
1205
+ } catch {
1206
+ // Keep the last state; an older server without `sidebar` keeps waiting.
1207
+ } finally {
1208
+ inFlight = false;
1209
+ }
1210
+ };
1211
+ createEffect(() => {
1212
+ const timer = setInterval(refresh, props.controller.prefs().pollMs);
1213
+ onCleanup(() => clearInterval(timer));
1214
+ });
1215
+ void refresh();
1216
+ onCleanup(() => abort.abort());
1217
+ return _$createComponent(ClaudeSidebar, {
1218
+ state: state,
1219
+ theme: () => sidebarThemeFromHost(props.host.theme),
1220
+ get controller() {
1221
+ return props.controller;
1222
+ },
1223
+ get sessionId() {
1224
+ return props.sessionId;
1225
+ }
1226
+ });
1227
+ }
1228
+
1229
+ /** Claims the OpenCode 2 session sidebar; the returned function releases it. */
1230
+ export async function mountClaudeSidebar(host, read) {
1231
+ const controller = createSidebarController(resolveAnthropicAuthPrefs(await readTuiPreferencesFile()));
1232
+ let release;
1233
+ try {
1234
+ release = host.ui.slot({
1235
+ append: 'sidebar.content',
1236
+ render: input => _$createComponent(HostSidebar, {
1237
+ host: host,
1238
+ read: read,
1239
+ controller: controller,
1240
+ get sessionId() {
1241
+ return input.sessionID;
1242
+ }
1243
+ })
1244
+ });
1245
+ } catch (error) {
1246
+ controller.dispose();
1247
+ throw error;
1248
+ }
1249
+ return () => {
1250
+ release();
1251
+ controller.dispose();
1252
+ };
1253
+ }