@cortexkit/opencode-magic-context 0.31.1 → 0.31.3

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,1052 @@
1
+ import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid";
2
+ import { createTextNode as _$createTextNode } from "opentui:runtime-module:%40opentui%2Fsolid";
3
+ import { effect as _$effect } from "opentui:runtime-module:%40opentui%2Fsolid";
4
+ import { insertNode as _$insertNode } from "opentui:runtime-module:%40opentui%2Fsolid";
5
+ import { memo as _$memo } from "opentui:runtime-module:%40opentui%2Fsolid";
6
+ import { insert as _$insert } from "opentui:runtime-module:%40opentui%2Fsolid";
7
+ import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid";
8
+ import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
9
+ /** @jsxImportSource @opentui/solid */
10
+ import { Show, createEffect, createMemo, createSignal, on, onCleanup } from "opentui:runtime-module:solid-js";
11
+ import packageJson from "../../../package.json";
12
+ import { badgeTextColor } from "../badge-contrast";
13
+ import { loadSidebarSnapshot } from "../data/context-db";
14
+ import { formatThresholdPercent } from "../../shared/format-threshold";
15
+ import { computeEffectiveOrder, DEFAULT_SLOT_ORDER, PLUGIN_KEY, queueTuiPreferenceUpdate, readTuiPreferencesFile, readTuiPreferencesFileSync, resolveMagicContextPrefs, watchTuiPreferences } from "../../shared/tui-preferences";
16
+
17
+ // Module-level hook so the upgrade/recomp dialog can kick the sidebar into its
18
+ // fast recomp self-poll the INSTANT the user confirms — without waiting for a
19
+ // parent-session message event (the RPC upgrade/recomp call fires none). The
20
+ // mounted SidebarContent registers its refresh here.
21
+ let activeRecompPollKick = null;
22
+ export function kickRecompProgressRefresh() {
23
+ activeRecompPollKick?.();
24
+ }
25
+ const SINGLE_BORDER = {
26
+ type: "single"
27
+ };
28
+ const REFRESH_DEBOUNCE_MS = 150;
29
+ // The TUI may unmount and remount sidebar_content when the user switches views
30
+ // (main -> subagent -> main). A remount re-runs the component body, so a signal
31
+ // created inside the component would reset to its seed. The controller lives in
32
+ // the slot-factory closure (plugin/process lifetime) and owns the durable
33
+ // prefs/collapse signals plus the single shared file watcher, so collapse state
34
+ // and live pref reloads survive remounts. No Solid effects/memos here — those
35
+ // need an owner; the poll-interval effect stays inside the component.
36
+ function createSidebarController(initialPrefs) {
37
+ const [prefs, setPrefs] = createSignal(initialPrefs);
38
+ const seedCollapsed = initialPrefs.rememberCollapsed && initialPrefs.collapsed != null ? initialPrefs.collapsed : initialPrefs.startCollapsed;
39
+ const [collapsed, setCollapsed] = createSignal(seedCollapsed);
40
+ let lastPersistedCollapsed = initialPrefs.collapsed;
41
+ let lastApplied = JSON.stringify(initialPrefs);
42
+
43
+ // Watcher lives for the process lifetime — intentionally never disposed.
44
+ // Collapse echo guard: lastPersistedCollapsed advances only once our own
45
+ // write lands, so a watcher echo of the value we just wrote is rejected by
46
+ // the `!==` check and cannot revert a user click.
47
+ watchTuiPreferences(() => {
48
+ void (async () => {
49
+ const next = resolveMagicContextPrefs(await readTuiPreferencesFile());
50
+ const serialized = JSON.stringify(next);
51
+ if (serialized === lastApplied) return;
52
+ lastApplied = serialized;
53
+ setPrefs(next);
54
+ if (next.rememberCollapsed && next.collapsed != null && next.collapsed !== lastPersistedCollapsed) {
55
+ lastPersistedCollapsed = next.collapsed;
56
+ setCollapsed(next.collapsed);
57
+ }
58
+ })();
59
+ });
60
+ function toggleCollapsed() {
61
+ const next = !collapsed();
62
+ setCollapsed(next);
63
+ if (prefs().rememberCollapsed) {
64
+ void queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], next).then(() => {
65
+ lastPersistedCollapsed = next;
66
+ });
67
+ }
68
+ }
69
+ return {
70
+ prefs,
71
+ collapsed,
72
+ toggleCollapsed
73
+ };
74
+ }
75
+ function compactTokens(value) {
76
+ if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
77
+ if (value >= 1_000) return `${(value / 1_000).toFixed(0)}K`;
78
+ return String(value);
79
+ }
80
+ function relativeTime(ms) {
81
+ const diff = Date.now() - ms;
82
+ if (diff < 60_000) return "just now";
83
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
84
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
85
+ return `${Math.floor(diff / 86_400_000)}d ago`;
86
+ }
87
+
88
+ // Text progress bar, e.g. [██████░░░░] for the recomp/upgrade live indicator.
89
+ function progressBar(fraction, width = 14) {
90
+ const clamped = Math.max(0, Math.min(1, fraction));
91
+ const filled = Math.round(clamped * width);
92
+ return `[${"█".repeat(filled)}${"░".repeat(width - filled)}]`;
93
+ }
94
+
95
+ // Token breakdown segment colors (hardcoded hex values)
96
+ const COLORS = {
97
+ // Cool / structured — injected by the plugin into message[0]
98
+ system: "#c084fc",
99
+ // Purple
100
+ docs: "#22d3ee",
101
+ // Cyan — <project-docs>
102
+ compartments: "#60a5fa",
103
+ // Blue
104
+ facts: "#fbbf24",
105
+ // Yellow/orange
106
+ memories: "#34d399",
107
+ // Green
108
+ profile: "#a3e635",
109
+ // Lime — <user-profile>
110
+ // Warm / user-facing — regular chat and tool traffic. Grouped visually
111
+ // by hue family so the user reads them as a related block.
112
+ conversation: "#f87171",
113
+ // Red
114
+ toolCalls: "#fb923c",
115
+ // Orange
116
+ toolDefs: "#f472b6" // Pink
117
+ };
118
+ // Segmented token breakdown bar with legend
119
+ const TokenBreakdown = props => {
120
+ // The bar is rendered as a flex row of colored boxes, each with
121
+ // flexGrow=tokens and flexBasis=0. opentui distributes the parent
122
+ // container's full width proportionally, so the bar always fills the
123
+ // sidebar regardless of terminal size. No hardcoded width is needed —
124
+ // this fixes both the over-wide bar that wrapped onto a second line on
125
+ // narrow sidebars (issue #90) and the under-wide bar that left empty
126
+ // space on the right on wide sidebars.
127
+ const segments = createMemo(() => {
128
+ const s = props.snapshot;
129
+ const total = s.inputTokens || 1;
130
+ const result = [];
131
+
132
+ // System Prompt (purple)
133
+ if (s.systemPromptTokens > 0) {
134
+ result.push({
135
+ key: "sys",
136
+ tokens: s.systemPromptTokens,
137
+ color: COLORS.system,
138
+ label: "System"
139
+ });
140
+ }
141
+
142
+ // Docs (cyan) — injected <project-docs> block (ARCHITECTURE/STRUCTURE)
143
+ if (s.docsTokens > 0) {
144
+ result.push({
145
+ key: "docs",
146
+ tokens: s.docsTokens,
147
+ color: COLORS.docs,
148
+ label: "Docs"
149
+ });
150
+ }
151
+
152
+ // Compartments (blue)
153
+ if (s.compartmentTokens > 0) {
154
+ result.push({
155
+ key: "comp",
156
+ tokens: s.compartmentTokens,
157
+ color: COLORS.compartments,
158
+ label: "Compartments"
159
+ });
160
+ }
161
+
162
+ // Facts (yellow/orange)
163
+ if (s.factTokens > 0) {
164
+ result.push({
165
+ key: "fact",
166
+ tokens: s.factTokens,
167
+ color: COLORS.facts,
168
+ label: "Facts"
169
+ });
170
+ }
171
+
172
+ // Memories (green)
173
+ if (s.memoryTokens > 0) {
174
+ result.push({
175
+ key: "mem",
176
+ tokens: s.memoryTokens,
177
+ color: COLORS.memories,
178
+ label: "Memories"
179
+ });
180
+ }
181
+
182
+ // User Profile (lime) — injected <user-profile> block (promoted user memories)
183
+ if (s.profileTokens > 0) {
184
+ result.push({
185
+ key: "profile",
186
+ tokens: s.profileTokens,
187
+ color: COLORS.profile,
188
+ label: "User Profile"
189
+ });
190
+ }
191
+
192
+ // Conversation = real user/assistant text/reasoning/images
193
+ // (excludes injected session-history and excludes tool call I/O).
194
+ //
195
+ // Always show this row even when conversationTokens === 0. The
196
+ // calibrator's residual-distribution math (tokenizer-calibration.ts)
197
+ // can round it down to zero when toolCallsLocal massively dominates
198
+ // conversationLocal — that's a calibration artifact, not a real
199
+ // "zero conversation". Suppressing the row leaves the legend looking
200
+ // truncated, which is more confusing than showing a 0% line. The
201
+ // segment is also skipped in the bar at 0 width because the segment
202
+ // builder uses `Math.max(1, ...)` only when tokens > 0 (see
203
+ // segmentWidths), so the visual bar stays correct either way.
204
+ result.push({
205
+ key: "conv",
206
+ tokens: s.conversationTokens,
207
+ color: COLORS.conversation,
208
+ label: "Conversation"
209
+ });
210
+
211
+ // Tool Calls = tool_use/tool_result/tool/tool-invocation parts in messages
212
+ // (actionable — users can reduce via ctx_reduce)
213
+ if (s.toolCallTokens > 0) {
214
+ result.push({
215
+ key: "tool-calls",
216
+ tokens: s.toolCallTokens,
217
+ color: COLORS.toolCalls,
218
+ label: "Tool Calls"
219
+ });
220
+ }
221
+
222
+ // Tool Definitions = measured description + JSON-schema parameters for
223
+ // each tool OpenCode sends in the `tools` request parameter, populated
224
+ // by the `tool.definition` plugin hook keyed by {provider, model, agent}.
225
+ // Zero until the first turn measures the active agent's tool set.
226
+ if (s.toolDefinitionTokens > 0) {
227
+ result.push({
228
+ key: "tool-defs",
229
+ tokens: s.toolDefinitionTokens,
230
+ color: COLORS.toolDefs,
231
+ label: "Tool Defs"
232
+ });
233
+ }
234
+ return result;
235
+ });
236
+ const totalTokens = createMemo(() => props.snapshot.inputTokens || 1);
237
+
238
+ // Render-time segments for the bar. Zero-token segments are filtered out
239
+ // entirely (no flex weight, no rendered box) so they don't claim any
240
+ // width. Non-zero segments still get a Math.max(1, ...) floor on
241
+ // flexGrow so very small contributions remain visible as a thin sliver.
242
+ // The legend rows below show every segment (including zeros) for table
243
+ // stability — only the bar prunes them.
244
+ const barSegments = createMemo(() => segments().filter(seg => seg.tokens > 0));
245
+ return (() => {
246
+ var _el$ = _$createElement("box"),
247
+ _el$2 = _$createElement("box");
248
+ _$insertNode(_el$, _el$2);
249
+ _$setProp(_el$, "width", "100%");
250
+ _$setProp(_el$, "flexDirection", "column");
251
+ _$setProp(_el$2, "width", "100%");
252
+ _$setProp(_el$2, "flexDirection", "row");
253
+ _$setProp(_el$2, "height", 1);
254
+ _$insert(_el$2, () => barSegments().map(seg => (() => {
255
+ var _el$3 = _$createElement("box");
256
+ _$setProp(_el$3, "flexBasis", 0);
257
+ _$setProp(_el$3, "height", 1);
258
+ _$effect(_p$ => {
259
+ var _v$ = seg.key,
260
+ _v$2 = Math.max(1, seg.tokens),
261
+ _v$3 = seg.color;
262
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$3, "key", _v$, _p$.e));
263
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$3, "flexGrow", _v$2, _p$.t));
264
+ _v$3 !== _p$.a && (_p$.a = _$setProp(_el$3, "backgroundColor", _v$3, _p$.a));
265
+ return _p$;
266
+ }, {
267
+ e: undefined,
268
+ t: undefined,
269
+ a: undefined
270
+ });
271
+ return _el$3;
272
+ })()));
273
+ _$insert(_el$, (() => {
274
+ var _c$ = _$memo(() => !!!props.collapsed);
275
+ return () => _c$() && (() => {
276
+ var _el$4 = _$createElement("box");
277
+ _$setProp(_el$4, "flexDirection", "column");
278
+ _$setProp(_el$4, "marginTop", 0);
279
+ _$insert(_el$4, () => segments().map(seg => {
280
+ const pct = (seg.tokens / totalTokens() * 100).toFixed(0);
281
+ return (() => {
282
+ var _el$5 = _$createElement("box"),
283
+ _el$6 = _$createElement("text"),
284
+ _el$7 = _$createElement("text"),
285
+ _el$8 = _$createTextNode(` (`),
286
+ _el$9 = _$createTextNode(`%)`);
287
+ _$insertNode(_el$5, _el$6);
288
+ _$insertNode(_el$5, _el$7);
289
+ _$setProp(_el$5, "width", "100%");
290
+ _$setProp(_el$5, "flexDirection", "row");
291
+ _$setProp(_el$5, "justifyContent", "space-between");
292
+ _$insert(_el$6, () => seg.label);
293
+ _$insertNode(_el$7, _el$8);
294
+ _$insertNode(_el$7, _el$9);
295
+ _$insert(_el$7, () => compactTokens(seg.tokens), _el$8);
296
+ _$insert(_el$7, pct, _el$9);
297
+ _$effect(_p$ => {
298
+ var _v$4 = seg.key,
299
+ _v$5 = seg.color,
300
+ _v$6 = props.theme.textMuted;
301
+ _v$4 !== _p$.e && (_p$.e = _$setProp(_el$5, "key", _v$4, _p$.e));
302
+ _v$5 !== _p$.t && (_p$.t = _$setProp(_el$6, "fg", _v$5, _p$.t));
303
+ _v$6 !== _p$.a && (_p$.a = _$setProp(_el$7, "fg", _v$6, _p$.a));
304
+ return _p$;
305
+ }, {
306
+ e: undefined,
307
+ t: undefined,
308
+ a: undefined
309
+ });
310
+ return _el$5;
311
+ })();
312
+ }));
313
+ return _el$4;
314
+ })();
315
+ })(), null);
316
+ return _el$;
317
+ })();
318
+ };
319
+ const StatRow = props => {
320
+ const fg = createMemo(() => {
321
+ if (props.warning) return props.theme.warning;
322
+ if (props.accent) return props.theme.accent;
323
+ if (props.dim) return props.theme.textMuted;
324
+ return props.theme.text;
325
+ });
326
+ return (() => {
327
+ var _el$0 = _$createElement("box"),
328
+ _el$1 = _$createElement("text"),
329
+ _el$10 = _$createElement("text"),
330
+ _el$11 = _$createElement("b");
331
+ _$insertNode(_el$0, _el$1);
332
+ _$insertNode(_el$0, _el$10);
333
+ _$setProp(_el$0, "width", "100%");
334
+ _$setProp(_el$0, "flexDirection", "row");
335
+ _$setProp(_el$0, "justifyContent", "space-between");
336
+ _$insert(_el$1, () => props.label);
337
+ _$insertNode(_el$10, _el$11);
338
+ _$insert(_el$11, () => props.value);
339
+ _$effect(_p$ => {
340
+ var _v$7 = props.theme.textMuted,
341
+ _v$8 = fg();
342
+ _v$7 !== _p$.e && (_p$.e = _$setProp(_el$1, "fg", _v$7, _p$.e));
343
+ _v$8 !== _p$.t && (_p$.t = _$setProp(_el$10, "fg", _v$8, _p$.t));
344
+ return _p$;
345
+ }, {
346
+ e: undefined,
347
+ t: undefined
348
+ });
349
+ return _el$0;
350
+ })();
351
+ };
352
+ const SectionHeader = props => (() => {
353
+ var _el$12 = _$createElement("box"),
354
+ _el$13 = _$createElement("text"),
355
+ _el$14 = _$createElement("b");
356
+ _$insertNode(_el$12, _el$13);
357
+ _$setProp(_el$12, "width", "100%");
358
+ _$setProp(_el$12, "marginTop", 1);
359
+ _$insertNode(_el$13, _el$14);
360
+ _$insert(_el$14, () => props.title);
361
+ _$effect(_$p => _$setProp(_el$13, "fg", props.theme.text, _$p));
362
+ return _el$12;
363
+ })();
364
+
365
+ // Live recomp / session-upgrade progress. Renders while an upgrade runs (and
366
+ // briefly after it finishes) so a multi-minute rebuild is visible instead of a
367
+ // single missed toast (dogfood 2026-05-30).
368
+ const RecompProgressSection = props => {
369
+ // CRITICAL: read `props.progress` reactively on every access — do NOT
370
+ // destructure it into a local `const p = props.progress` at creation time.
371
+ // The parent keeps THIS component instance mounted as the phase advances
372
+ // (recomp → migration → done), so a frozen `p` would render the
373
+ // creation-time phase forever — the sidebar stuck on "upgrading / Running
374
+ // historian (pass 1)…" even though the upgrade finished. Each accessor below
375
+ // tracks the parent signal so the label/bar/note update live (root cause of
376
+ // the dogfood 2026-05-30 "recomp upgrading stays" freeze).
377
+ const phase = () => props.progress.phase;
378
+ const fraction = () => props.progress.totalMessages > 0 ? props.progress.processedMessages / props.progress.totalMessages : 0;
379
+ const pct = () => Math.round(fraction() * 100);
380
+
381
+ // "Recomp" vs "Upgrade" vs "Embed" wording follows the flow that started this
382
+ // run, so a plain /ctx-recomp never renders as an "Upgrade" (dogfood 2026-06-04).
383
+ const verb = () => props.progress.kind === "upgrade" ? "Upgrade" : props.progress.kind === "embed" ? "Embed" : props.progress.kind === "wrapup" ? "Wrapup" : "Recomp";
384
+ const activeText = () => props.progress.kind === "upgrade" ? "upgrading ⟳" : props.progress.kind === "embed" ? "embedding ⟳" : props.progress.kind === "wrapup" ? "wrapping ⟳" : "comparting ⟳";
385
+ const label = createMemo(() => {
386
+ switch (props.progress.phase) {
387
+ case "recomp":
388
+ return {
389
+ text: activeText(),
390
+ color: props.theme.warning
391
+ };
392
+ case "migration":
393
+ return {
394
+ text: "Migrating memories ⟳",
395
+ color: props.theme.warning
396
+ };
397
+ case "done":
398
+ return {
399
+ text: `✓ ${verb()} complete`,
400
+ color: props.theme.success ?? props.theme.accent
401
+ };
402
+ case "skipped":
403
+ // Neutral terse status next to the bold verb header; the full,
404
+ // self-contained reason (lease-busy "try again shortly" vs a
405
+ // partial-stall "run /ctx-embed start again") renders on its own
406
+ // line below. Don't re-prepend verb here (it's already the bold
407
+ // header — doing so read as "EmbedEmbed"), and don't hardcode
408
+ // "retry shortly" (wrong for a partial stall).
409
+ return {
410
+ text: "stopped",
411
+ color: props.theme.textMuted
412
+ };
413
+ case "failed":
414
+ return {
415
+ text: `✗ ${verb()} failed`,
416
+ color: props.theme.error
417
+ };
418
+ }
419
+ });
420
+ return [(() => {
421
+ var _el$15 = _$createElement("box"),
422
+ _el$16 = _$createElement("text"),
423
+ _el$17 = _$createElement("b"),
424
+ _el$18 = _$createElement("text");
425
+ _$insertNode(_el$15, _el$16);
426
+ _$insertNode(_el$15, _el$18);
427
+ _$setProp(_el$15, "width", "100%");
428
+ _$setProp(_el$15, "marginTop", 1);
429
+ _$setProp(_el$15, "flexDirection", "row");
430
+ _$setProp(_el$15, "justifyContent", "space-between");
431
+ _$insertNode(_el$16, _el$17);
432
+ _$insert(_el$17, verb);
433
+ _$insert(_el$18, () => label().text);
434
+ _$effect(_p$ => {
435
+ var _v$9 = props.theme.text,
436
+ _v$0 = label().color;
437
+ _v$9 !== _p$.e && (_p$.e = _$setProp(_el$16, "fg", _v$9, _p$.e));
438
+ _v$0 !== _p$.t && (_p$.t = _$setProp(_el$18, "fg", _v$0, _p$.t));
439
+ return _p$;
440
+ }, {
441
+ e: undefined,
442
+ t: undefined
443
+ });
444
+ return _el$15;
445
+ })(), _$memo(() => _$memo(() => !!(phase() === "recomp" && props.progress.totalMessages > 0))() && (() => {
446
+ var _el$19 = _$createElement("box"),
447
+ _el$20 = _$createElement("text"),
448
+ _el$21 = _$createElement("text"),
449
+ _el$22 = _$createTextNode(`%`);
450
+ _$insertNode(_el$19, _el$20);
451
+ _$insertNode(_el$19, _el$21);
452
+ _$setProp(_el$19, "width", "100%");
453
+ _$setProp(_el$19, "flexDirection", "row");
454
+ _$setProp(_el$19, "justifyContent", "space-between");
455
+ _$insert(_el$20, () => progressBar(fraction()));
456
+ _$insertNode(_el$21, _el$22);
457
+ _$insert(_el$21, pct, _el$22);
458
+ _$effect(_p$ => {
459
+ var _v$1 = props.theme.accent,
460
+ _v$10 = props.theme.textMuted;
461
+ _v$1 !== _p$.e && (_p$.e = _$setProp(_el$20, "fg", _v$1, _p$.e));
462
+ _v$10 !== _p$.t && (_p$.t = _$setProp(_el$21, "fg", _v$10, _p$.t));
463
+ return _p$;
464
+ }, {
465
+ e: undefined,
466
+ t: undefined
467
+ });
468
+ return _el$19;
469
+ })()), _$memo(() => _$memo(() => !!((phase() === "recomp" || phase() === "migration") && props.progress.note))() && (() => {
470
+ var _el$23 = _$createElement("text");
471
+ _$insert(_el$23, () => props.progress.note);
472
+ _$effect(_$p => _$setProp(_el$23, "fg", props.theme.textMuted, _$p));
473
+ return _el$23;
474
+ })()), _$memo(() => _$memo(() => !!(phase() === "recomp" && props.progress.kind !== "embed"))() && _$createComponent(StatRow, {
475
+ get theme() {
476
+ return props.theme;
477
+ },
478
+ label: "Compartments",
479
+ get value() {
480
+ return `${props.progress.compartmentsCreated} (${props.progress.passCount} pass${props.progress.passCount === 1 ? "" : "es"})`;
481
+ },
482
+ dim: true
483
+ })), _$memo(() => _$memo(() => !!(phase() === "recomp" && props.progress.kind === "embed"))() && _$createComponent(StatRow, {
484
+ get theme() {
485
+ return props.theme;
486
+ },
487
+ label: "Compartments",
488
+ get value() {
489
+ return `${props.progress.processedMessages}/${props.progress.totalMessages} embedded`;
490
+ },
491
+ dim: true
492
+ })), _$memo(() => _$memo(() => !!((phase() === "failed" || phase() === "skipped") && props.progress.message))() && (() => {
493
+ var _el$24 = _$createElement("text");
494
+ _$insert(_el$24, () => props.progress.message);
495
+ _$effect(_$p => _$setProp(_el$24, "fg", props.theme.textMuted, _$p));
496
+ return _el$24;
497
+ })())];
498
+ };
499
+ const SidebarContent = props => {
500
+ const [snapshot, setSnapshot] = createSignal(null);
501
+ // Collapse state + section visibility prefs live in the controller (plugin
502
+ // closure), so they survive view-switch remounts and persist across restarts
503
+ // via ~/.config/opencode/tui-preferences.jsonc. Read reactively.
504
+ const collapsed = props.controller.collapsed;
505
+ const sections = () => props.controller.prefs().sections;
506
+ const headerLabel = () => props.controller.prefs().header.label;
507
+ let refreshTimer;
508
+ // Self-sustaining poll while a recomp/upgrade is running. Recomp work
509
+ // happens in CHILD sessions whose message events are filtered out of the
510
+ // subscription below, so without this the progress bar would freeze until
511
+ // the next parent-session message. Active only during recomp/migration;
512
+ // stops itself once the phase goes terminal/absent (dogfood 2026-05-30).
513
+ let recompPollTimer;
514
+ const RECOMP_POLL_MS = 1200;
515
+ // Robust recomp poll state. The loop MUST survive a failed/slow snapshot
516
+ // fetch — the server is busy doing the historian LLM call during a recomp,
517
+ // so a poll can reject or return a stale (pre-recomp) cached snapshot. The
518
+ // OLD loop reattached the next timer only inside `.then()`, so any rejection
519
+ // killed it and the bar froze mid-pass (dogfood 2026-05-30). This version
520
+ // reschedules on BOTH success and failure, keyed on `recompActive`, and only
521
+ // stops on a terminal phase, a bounded "never started" probe window, or the
522
+ // entry vanishing after we'd seen it active.
523
+ let recompActive = false;
524
+ let recompSawPhase = false;
525
+ let recompPollCount = 0;
526
+ let recompConsecutiveAbsent = 0;
527
+ const RECOMP_PROBE_MAX = 12; // ~15s for the server's "Starting…" to land
528
+ // After we've SEEN an active phase, a momentarily absent snapshot is almost
529
+ // always transient — the server's sticky cache serves a pre-recomp snapshot
530
+ // (no recompProgress) during the token-quiet recomp window, or a concurrent
531
+ // BEGIN-IMMEDIATE publish makes the snapshot DB read throw → bare empty. The
532
+ // entry is held until terminal + a 30s grace, so we keep polling through many
533
+ // absents and only give up after a long run of them (entry truly gone but we
534
+ // somehow missed "done"). This was the freeze: the old logic stopped on the
535
+ // FIRST absent-after-active (dogfood 2026-05-30).
536
+ const RECOMP_ABSENT_GIVEUP = 40; // ~48s of continuous absence → stop
537
+ const RECOMP_MAX_POLLS = 1500; // ~30min absolute safety cap
538
+
539
+ const refresh = () => {
540
+ const sid = props.sessionID();
541
+ if (!sid) return;
542
+ const directory = props.api.state.path.directory ?? "";
543
+ void loadSidebarSnapshot(sid, directory).then(data => {
544
+ // Guard against a session switch while this load was in flight:
545
+ // painting session A's snapshot into the now-active session B shows
546
+ // the wrong session's numbers until B's own refresh resolves.
547
+ if (props.sessionID() !== sid) return;
548
+ setSnapshot(data);
549
+ try {
550
+ props.api.renderer.requestRender();
551
+ } catch {
552
+ // Ignore render errors
553
+ }
554
+ // If a recomp/upgrade is running (detected via any refresh, e.g.
555
+ // a /ctx-recomp command not started from the dialog), make sure
556
+ // the dedicated poll loop is running.
557
+ const phase = data?.recompProgress?.phase;
558
+ if ((phase === "recomp" || phase === "migration") && !recompActive) {
559
+ kickRecompPoll();
560
+ }
561
+ }).catch(() => {
562
+ // one-shot refresh failure is non-fatal; the recomp loop (if any)
563
+ // has its own resilient retry.
564
+ });
565
+ };
566
+ const scheduleRefresh = () => {
567
+ if (refreshTimer) clearTimeout(refreshTimer);
568
+ refreshTimer = setTimeout(() => {
569
+ refreshTimer = undefined;
570
+ refresh();
571
+ }, REFRESH_DEBOUNCE_MS);
572
+ };
573
+ const scheduleRecompTick = () => {
574
+ if (!recompActive) return;
575
+ if (recompPollTimer) clearTimeout(recompPollTimer);
576
+ recompPollTimer = setTimeout(recompTick, RECOMP_POLL_MS);
577
+ };
578
+ function recompTick() {
579
+ if (!recompActive) return;
580
+ recompPollCount += 1;
581
+ if (recompPollCount > RECOMP_MAX_POLLS) {
582
+ recompActive = false;
583
+ return;
584
+ }
585
+ const sid = props.sessionID();
586
+ if (!sid) {
587
+ recompActive = false;
588
+ return;
589
+ }
590
+ const directory = props.api.state.path.directory ?? "";
591
+ void loadSidebarSnapshot(sid, directory).then(data => {
592
+ const phase = data?.recompProgress?.phase;
593
+ // While a recomp is known-active, a transient snapshot that lost
594
+ // recompProgress (sticky cache / busy-DB empty) must NOT wipe the
595
+ // visible bar — carry the last good progress forward so it stays
596
+ // stable until a real update or the terminal state lands.
597
+ const prevProgress = snapshot()?.recompProgress;
598
+ const merged = !phase && recompSawPhase && prevProgress ? {
599
+ ...data,
600
+ recompProgress: prevProgress
601
+ } : data;
602
+ setSnapshot(merged);
603
+ try {
604
+ props.api.renderer.requestRender();
605
+ } catch {
606
+ // ignore render errors
607
+ }
608
+ if (phase === "recomp" || phase === "migration") {
609
+ recompSawPhase = true;
610
+ recompConsecutiveAbsent = 0;
611
+ scheduleRecompTick();
612
+ } else if (phase === "done" || phase === "failed" || phase === "skipped") {
613
+ // Terminal state rendered — stop. The server keeps "done"/
614
+ // "skipped" for a grace window and "failed" until the next run,
615
+ // so the outcome stays visible without further polling.
616
+ recompActive = false;
617
+ } else {
618
+ // Phase absent this poll.
619
+ recompConsecutiveAbsent += 1;
620
+ if (!recompSawPhase) {
621
+ // Still waiting for the server's first "Starting…".
622
+ if (recompPollCount < RECOMP_PROBE_MAX) scheduleRecompTick();else {
623
+ recompActive = false;
624
+ }
625
+ } else if (recompConsecutiveAbsent < RECOMP_ABSENT_GIVEUP) {
626
+ // Seen it active — absent is almost certainly the sticky
627
+ // cache / a transient snapshot read. Keep polling so we
628
+ // still catch the terminal state. DON'T overwrite the
629
+ // last good progress snapshot with this transient empty.
630
+ scheduleRecompTick();
631
+ } else {
632
+ // Long continuous absence — the entry is genuinely gone.
633
+ recompActive = false;
634
+ }
635
+ }
636
+ }).catch(err => {
637
+ // CRITICAL: a failed/slow fetch must NOT kill the loop — keep
638
+ // polling while active so we still catch the terminal state.
639
+ scheduleRecompTick();
640
+ });
641
+ }
642
+
643
+ // Kick the resilient recomp poll loop on dialog confirm (or when a refresh
644
+ // first detects an active recomp). The server emits an immediate "Starting…"
645
+ // entry; the probe window covers the brief RPC race before it lands.
646
+ function kickRecompPoll() {
647
+ if (recompActive) return; // already running
648
+ recompActive = true;
649
+ recompSawPhase = false;
650
+ recompPollCount = 0;
651
+ recompConsecutiveAbsent = 0;
652
+ recompTick();
653
+ }
654
+ activeRecompPollKick = kickRecompPoll;
655
+ onCleanup(() => {
656
+ if (refreshTimer) clearTimeout(refreshTimer);
657
+ if (recompPollTimer) clearTimeout(recompPollTimer);
658
+ recompActive = false;
659
+ if (activeRecompPollKick === kickRecompPoll) activeRecompPollKick = null;
660
+ });
661
+
662
+ // Refresh on session change
663
+ createEffect(on(props.sessionID, () => {
664
+ refresh();
665
+ }));
666
+
667
+ // Subscribe to events for live updates
668
+ createEffect(on(props.sessionID, sessionID => {
669
+ const unsubs = [props.api.event.on("message.updated", event => {
670
+ if (event.properties.info.sessionID !== sessionID) return;
671
+ scheduleRefresh();
672
+ }), props.api.event.on("session.updated", event => {
673
+ if (event.properties.info.id !== sessionID) return;
674
+ scheduleRefresh();
675
+ }), props.api.event.on("message.removed", event => {
676
+ if (event.properties.sessionID !== sessionID) return;
677
+ scheduleRefresh();
678
+ })];
679
+ onCleanup(() => {
680
+ for (const unsub of unsubs) unsub();
681
+ });
682
+ }, {
683
+ defer: false
684
+ }));
685
+ const s = createMemo(() => snapshot());
686
+ const contextSummaryColor = createMemo(() => {
687
+ const usage = s()?.usagePercentage ?? 0;
688
+ if (usage >= 80) return props.theme.error;
689
+ if (usage >= 65) return props.theme.warning;
690
+ return props.theme.accent;
691
+ });
692
+ return (() => {
693
+ var _el$25 = _$createElement("box"),
694
+ _el$26 = _$createElement("box"),
695
+ _el$27 = _$createElement("box"),
696
+ _el$28 = _$createElement("text"),
697
+ _el$29 = _$createElement("b"),
698
+ _el$30 = _$createElement("text"),
699
+ _el$31 = _$createTextNode(`v`);
700
+ _$insertNode(_el$25, _el$26);
701
+ _$setProp(_el$25, "width", "100%");
702
+ _$setProp(_el$25, "flexDirection", "column");
703
+ _$setProp(_el$25, "border", SINGLE_BORDER);
704
+ _$setProp(_el$25, "paddingTop", 1);
705
+ _$setProp(_el$25, "paddingBottom", 1);
706
+ _$setProp(_el$25, "paddingLeft", 1);
707
+ _$setProp(_el$25, "paddingRight", 1);
708
+ _$insertNode(_el$26, _el$27);
709
+ _$insertNode(_el$26, _el$30);
710
+ _$setProp(_el$26, "flexDirection", "row");
711
+ _$setProp(_el$26, "justifyContent", "space-between");
712
+ _$setProp(_el$26, "alignItems", "center");
713
+ _$setProp(_el$26, "onMouseDown", () => props.controller.toggleCollapsed());
714
+ _$insertNode(_el$27, _el$28);
715
+ _$setProp(_el$27, "paddingLeft", 1);
716
+ _$setProp(_el$27, "paddingRight", 1);
717
+ _$insertNode(_el$28, _el$29);
718
+ _$insert(_el$29, () => collapsed() ? "▶ " : "▼ ", null);
719
+ _$insert(_el$29, headerLabel, null);
720
+ _$insertNode(_el$30, _el$31);
721
+ _$insert(_el$30, () => packageJson.version, null);
722
+ _$insert(_el$25, (() => {
723
+ var _c$2 = _$memo(() => !!(s() && s().inputTokens > 0));
724
+ return () => _c$2() && (() => {
725
+ var _el$32 = _$createElement("box");
726
+ _$setProp(_el$32, "flexDirection", "column");
727
+ _$insert(_el$32, (() => {
728
+ var _c$5 = _$memo(() => (s()?.contextLimit ?? 0) > 0);
729
+ return () => _c$5() && (() => {
730
+ var _el$33 = _$createElement("box"),
731
+ _el$34 = _$createElement("text"),
732
+ _el$35 = _$createElement("b"),
733
+ _el$36 = _$createTextNode(`%`),
734
+ _el$37 = _$createTextNode(` / `),
735
+ _el$38 = _$createTextNode(`%`),
736
+ _el$39 = _$createElement("text"),
737
+ _el$40 = _$createTextNode(` / `);
738
+ _$insertNode(_el$33, _el$34);
739
+ _$insertNode(_el$33, _el$39);
740
+ _$setProp(_el$33, "width", "100%");
741
+ _$setProp(_el$33, "flexDirection", "row");
742
+ _$setProp(_el$33, "justifyContent", "space-between");
743
+ _$insertNode(_el$34, _el$35);
744
+ _$insertNode(_el$34, _el$37);
745
+ _$insertNode(_el$34, _el$38);
746
+ _$insertNode(_el$35, _el$36);
747
+ _$insert(_el$35, () => s().usagePercentage.toFixed(1), _el$36);
748
+ _$insert(_el$34, () => formatThresholdPercent(s().executeThreshold), _el$38);
749
+ _$insertNode(_el$39, _el$40);
750
+ _$insert(_el$39, () => compactTokens(s().inputTokens), _el$40);
751
+ _$insert(_el$39, () => compactTokens(s().contextLimit), null);
752
+ _$effect(_p$ => {
753
+ var _v$15 = contextSummaryColor(),
754
+ _v$16 = contextSummaryColor();
755
+ _v$15 !== _p$.e && (_p$.e = _$setProp(_el$34, "fg", _v$15, _p$.e));
756
+ _v$16 !== _p$.t && (_p$.t = _$setProp(_el$39, "fg", _v$16, _p$.t));
757
+ return _p$;
758
+ }, {
759
+ e: undefined,
760
+ t: undefined
761
+ });
762
+ return _el$33;
763
+ })();
764
+ })(), null);
765
+ _$insert(_el$32, _$createComponent(TokenBreakdown, {
766
+ get theme() {
767
+ return props.theme;
768
+ },
769
+ get snapshot() {
770
+ return s();
771
+ },
772
+ get collapsed() {
773
+ return collapsed();
774
+ }
775
+ }), null);
776
+ _$effect(_$p => _$setProp(_el$32, "marginTop", collapsed() ? 0 : 1, _$p));
777
+ return _el$32;
778
+ })();
779
+ })(), null);
780
+ _$insert(_el$25, (() => {
781
+ var _c$3 = _$memo(() => !!collapsed());
782
+ return () => _c$3() && (() => {
783
+ var _el$41 = _$createElement("box"),
784
+ _el$42 = _$createElement("box"),
785
+ _el$43 = _$createElement("text"),
786
+ _el$45 = _$createElement("box"),
787
+ _el$46 = _$createElement("text"),
788
+ _el$48 = _$createElement("text"),
789
+ _el$49 = _$createElement("box"),
790
+ _el$50 = _$createElement("text"),
791
+ _el$52 = _$createElement("text"),
792
+ _el$53 = _$createTextNode(`C:`),
793
+ _el$54 = _$createTextNode(` Q:`),
794
+ _el$55 = _$createTextNode(` N:`);
795
+ _$insertNode(_el$41, _el$42);
796
+ _$insertNode(_el$41, _el$45);
797
+ _$insertNode(_el$41, _el$49);
798
+ _$setProp(_el$41, "width", "100%");
799
+ _$setProp(_el$41, "flexDirection", "column");
800
+ _$insertNode(_el$42, _el$43);
801
+ _$setProp(_el$42, "width", "100%");
802
+ _$setProp(_el$42, "flexDirection", "row");
803
+ _$setProp(_el$42, "justifyContent", "space-between");
804
+ _$insertNode(_el$43, _$createTextNode(`Historian`));
805
+ _$insert(_el$42, (() => {
806
+ var _c$6 = _$memo(() => !!s()?.historianRunning);
807
+ return () => _c$6() ? (() => {
808
+ var _el$56 = _$createElement("text");
809
+ _$insertNode(_el$56, _$createTextNode(`comparting ⟳`));
810
+ _$effect(_$p => _$setProp(_el$56, "fg", props.theme.warning, _$p));
811
+ return _el$56;
812
+ })() : (() => {
813
+ var _el$58 = _$createElement("text");
814
+ _$insertNode(_el$58, _$createTextNode(`idle`));
815
+ _$effect(_$p => _$setProp(_el$58, "fg", props.theme.textMuted, _$p));
816
+ return _el$58;
817
+ })();
818
+ })(), null);
819
+ _$insertNode(_el$45, _el$46);
820
+ _$insertNode(_el$45, _el$48);
821
+ _$setProp(_el$45, "width", "100%");
822
+ _$setProp(_el$45, "flexDirection", "row");
823
+ _$setProp(_el$45, "justifyContent", "space-between");
824
+ _$insertNode(_el$46, _$createTextNode(`Memories`));
825
+ _$insert(_el$48, (() => {
826
+ var _c$7 = _$memo(() => (s()?.memoryBlockCount ?? 0) > 0);
827
+ return () => _c$7() ? `${s().memoryBlockCount}/${s()?.memoryCount ?? 0}` : String(s()?.memoryCount ?? 0);
828
+ })());
829
+ _$insertNode(_el$49, _el$50);
830
+ _$insertNode(_el$49, _el$52);
831
+ _$setProp(_el$49, "width", "100%");
832
+ _$setProp(_el$49, "flexDirection", "row");
833
+ _$setProp(_el$49, "justifyContent", "space-between");
834
+ _$insertNode(_el$50, _$createTextNode(`Status`));
835
+ _$insertNode(_el$52, _el$53);
836
+ _$insertNode(_el$52, _el$54);
837
+ _$insertNode(_el$52, _el$55);
838
+ _$insert(_el$52, () => s()?.compartmentCount ?? 0, _el$54);
839
+ _$insert(_el$52, () => s()?.pendingOpsCount ?? 0, _el$55);
840
+ _$insert(_el$52, () => s()?.sessionNoteCount ?? 0, null);
841
+ _$insert(_el$41, _$createComponent(Show, {
842
+ get when() {
843
+ return s()?.recompProgress;
844
+ },
845
+ children: progress => _$createComponent(RecompProgressSection, {
846
+ get theme() {
847
+ return props.theme;
848
+ },
849
+ get progress() {
850
+ return progress();
851
+ }
852
+ })
853
+ }), null);
854
+ _$effect(_p$ => {
855
+ var _v$17 = props.theme.textMuted,
856
+ _v$18 = props.theme.textMuted,
857
+ _v$19 = props.theme.textMuted,
858
+ _v$20 = props.theme.textMuted,
859
+ _v$21 = props.theme.textMuted;
860
+ _v$17 !== _p$.e && (_p$.e = _$setProp(_el$43, "fg", _v$17, _p$.e));
861
+ _v$18 !== _p$.t && (_p$.t = _$setProp(_el$46, "fg", _v$18, _p$.t));
862
+ _v$19 !== _p$.a && (_p$.a = _$setProp(_el$48, "fg", _v$19, _p$.a));
863
+ _v$20 !== _p$.o && (_p$.o = _$setProp(_el$50, "fg", _v$20, _p$.o));
864
+ _v$21 !== _p$.i && (_p$.i = _$setProp(_el$52, "fg", _v$21, _p$.i));
865
+ return _p$;
866
+ }, {
867
+ e: undefined,
868
+ t: undefined,
869
+ a: undefined,
870
+ o: undefined,
871
+ i: undefined
872
+ });
873
+ return _el$41;
874
+ })();
875
+ })(), null);
876
+ _$insert(_el$25, (() => {
877
+ var _c$4 = _$memo(() => !!!collapsed());
878
+ return () => _c$4() && [_$memo(() => _$memo(() => !!sections().historian)() && [(() => {
879
+ var _el$60 = _$createElement("box"),
880
+ _el$61 = _$createElement("text"),
881
+ _el$62 = _$createElement("b");
882
+ _$insertNode(_el$60, _el$61);
883
+ _$setProp(_el$60, "width", "100%");
884
+ _$setProp(_el$60, "marginTop", 1);
885
+ _$setProp(_el$60, "flexDirection", "row");
886
+ _$setProp(_el$60, "justifyContent", "space-between");
887
+ _$insertNode(_el$61, _el$62);
888
+ _$insertNode(_el$62, _$createTextNode(`Historian`));
889
+ _$insert(_el$60, (() => {
890
+ var _c$8 = _$memo(() => !!s()?.historianRunning);
891
+ return () => _c$8() ? (() => {
892
+ var _el$64 = _$createElement("text");
893
+ _$insertNode(_el$64, _$createTextNode(`comparting ⟳`));
894
+ _$effect(_$p => _$setProp(_el$64, "fg", props.theme.warning, _$p));
895
+ return _el$64;
896
+ })() : (() => {
897
+ var _el$66 = _$createElement("text");
898
+ _$insertNode(_el$66, _$createTextNode(`idle`));
899
+ _$effect(_$p => _$setProp(_el$66, "fg", props.theme.textMuted, _$p));
900
+ return _el$66;
901
+ })();
902
+ })(), null);
903
+ _$effect(_$p => _$setProp(_el$61, "fg", props.theme.text, _$p));
904
+ return _el$60;
905
+ })(), _$createComponent(StatRow, {
906
+ get theme() {
907
+ return props.theme;
908
+ },
909
+ label: "Compartments",
910
+ get value() {
911
+ return String(s()?.compartmentCount ?? 0);
912
+ }
913
+ }), _$createComponent(Show, {
914
+ get when() {
915
+ return s()?.recompProgress;
916
+ },
917
+ children: progress => _$createComponent(RecompProgressSection, {
918
+ get theme() {
919
+ return props.theme;
920
+ },
921
+ get progress() {
922
+ return progress();
923
+ }
924
+ })
925
+ })]), _$memo(() => _$memo(() => !!sections().memory)() && [_$createComponent(SectionHeader, {
926
+ get theme() {
927
+ return props.theme;
928
+ },
929
+ title: "Memory"
930
+ }), _$createComponent(StatRow, {
931
+ get theme() {
932
+ return props.theme;
933
+ },
934
+ label: "Memories",
935
+ get value() {
936
+ return String(s()?.memoryCount ?? 0);
937
+ },
938
+ accent: true
939
+ }), _$memo(() => _$memo(() => (s()?.memoryBlockCount ?? 0) > 0)() && _$createComponent(StatRow, {
940
+ get theme() {
941
+ return props.theme;
942
+ },
943
+ label: "Injected",
944
+ get value() {
945
+ return String(s().memoryBlockCount);
946
+ },
947
+ dim: true
948
+ }))]), _$memo(() => _$memo(() => !!(sections().status && ((s()?.pendingOpsCount ?? 0) > 0 || (s()?.sessionNoteCount ?? 0) > 0 || (s()?.readySmartNoteCount ?? 0) > 0)))() && [_$createComponent(SectionHeader, {
949
+ get theme() {
950
+ return props.theme;
951
+ },
952
+ title: "Status"
953
+ }), _$memo(() => _$memo(() => (s()?.pendingOpsCount ?? 0) > 0)() && _$createComponent(StatRow, {
954
+ get theme() {
955
+ return props.theme;
956
+ },
957
+ label: "Queue",
958
+ get value() {
959
+ return `${s().pendingOpsCount} pending`;
960
+ },
961
+ warning: true
962
+ })), _$memo(() => _$memo(() => (s()?.sessionNoteCount ?? 0) > 0)() && _$createComponent(StatRow, {
963
+ get theme() {
964
+ return props.theme;
965
+ },
966
+ label: "Notes",
967
+ get value() {
968
+ return String(s().sessionNoteCount);
969
+ }
970
+ })), _$memo(() => _$memo(() => (s()?.readySmartNoteCount ?? 0) > 0)() && _$createComponent(StatRow, {
971
+ get theme() {
972
+ return props.theme;
973
+ },
974
+ label: "Smart Notes",
975
+ get value() {
976
+ return `${s().readySmartNoteCount} ready`;
977
+ },
978
+ accent: true
979
+ }))]), _$memo(() => _$memo(() => !!(sections().dreamer && s()?.lastDreamerRunAt))() && [_$createComponent(SectionHeader, {
980
+ get theme() {
981
+ return props.theme;
982
+ },
983
+ title: "Dreamer"
984
+ }), _$createComponent(StatRow, {
985
+ get theme() {
986
+ return props.theme;
987
+ },
988
+ label: "Last run",
989
+ get value() {
990
+ return relativeTime(s().lastDreamerRunAt);
991
+ },
992
+ dim: true
993
+ })]), _$memo(() => _$memo(() => !!(sections().stats && s()?.totalInputTokens != null))() && [_$createComponent(SectionHeader, {
994
+ get theme() {
995
+ return props.theme;
996
+ },
997
+ title: "Stats"
998
+ }), _$createComponent(StatRow, {
999
+ get theme() {
1000
+ return props.theme;
1001
+ },
1002
+ label: "Total tokens",
1003
+ get value() {
1004
+ return compactTokens(s().totalInputTokens ?? 0);
1005
+ },
1006
+ dim: true
1007
+ })])];
1008
+ })(), null);
1009
+ _$effect(_p$ => {
1010
+ var _v$11 = props.theme.borderActive,
1011
+ _v$12 = props.theme.accent,
1012
+ _v$13 = badgeTextColor(props.theme.accent, props.theme.background),
1013
+ _v$14 = props.theme.textMuted;
1014
+ _v$11 !== _p$.e && (_p$.e = _$setProp(_el$25, "borderColor", _v$11, _p$.e));
1015
+ _v$12 !== _p$.t && (_p$.t = _$setProp(_el$27, "backgroundColor", _v$12, _p$.t));
1016
+ _v$13 !== _p$.a && (_p$.a = _$setProp(_el$28, "fg", _v$13, _p$.a));
1017
+ _v$14 !== _p$.o && (_p$.o = _$setProp(_el$30, "fg", _v$14, _p$.o));
1018
+ return _p$;
1019
+ }, {
1020
+ e: undefined,
1021
+ t: undefined,
1022
+ a: undefined,
1023
+ o: undefined
1024
+ });
1025
+ return _el$25;
1026
+ })();
1027
+ };
1028
+ export function createSidebarContentSlot(api) {
1029
+ // Seed synchronously at slot construction so the sidebar renders at its
1030
+ // final collapse state + order on the first paint (no async flicker). The
1031
+ // controller lives here in the factory closure for the plugin lifetime, so
1032
+ // collapse state and live pref reloads survive sidebar_content remounts.
1033
+ const seedRoot = readTuiPreferencesFileSync();
1034
+ const controller = createSidebarController(resolveMagicContextPrefs(seedRoot));
1035
+ const effectiveOrder = computeEffectiveOrder(seedRoot, PLUGIN_KEY, DEFAULT_SLOT_ORDER);
1036
+ return {
1037
+ order: effectiveOrder,
1038
+ slots: {
1039
+ sidebar_content: (ctx, value) => {
1040
+ const theme = createMemo(() => ctx.theme.current);
1041
+ return _$createComponent(SidebarContent, {
1042
+ api: api,
1043
+ sessionID: () => value.session_id,
1044
+ get theme() {
1045
+ return theme();
1046
+ },
1047
+ controller: controller
1048
+ });
1049
+ }
1050
+ }
1051
+ };
1052
+ }