@cortexkit/opencode-magic-context 0.31.2 → 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,1204 @@
1
+ import { memo as _$memo } 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 { insert as _$insert } from "opentui:runtime-module:%40opentui%2Fsolid";
6
+ import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid";
7
+ import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
8
+ import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid";
9
+ /** @jsxImportSource @opentui/solid */
10
+ // @ts-nocheck
11
+ import { createMemo } from "opentui:runtime-module:solid-js";
12
+ import { createSidebarContentSlot, kickRecompProgressRefresh } from "./slots/sidebar-content";
13
+ import packageJson from "../../package.json";
14
+ import { closeRpc, dismissUpgradeReminder, getAnnouncement, getCompartmentCount, getRpcGeneration, initRpcClient, loadEmbedDetail, loadStatusDetail, loadToastDurationMs, markAnnounced, requestRecomp, requestUpgrade } from "./data/context-db";
15
+ import { startNotificationSocket, stopNotificationSocket } from "./data/notification-socket";
16
+ import { formatThresholdPercent } from "../shared/format-threshold";
17
+ import { detectConflicts } from "../shared/conflict-detector";
18
+ import { fixConflicts } from "../shared/conflict-fixer";
19
+ const DEFAULT_TOAST_DURATION_MS = 5000;
20
+ let unifiedToastDurationMs = DEFAULT_TOAST_DURATION_MS;
21
+ async function refreshToastDurationMs() {
22
+ try {
23
+ const resolved = await loadToastDurationMs();
24
+ if (typeof resolved === "number" && Number.isFinite(resolved)) {
25
+ unifiedToastDurationMs = resolved;
26
+ }
27
+ } catch {
28
+ // Keep the current value; the next poll/startup can retry.
29
+ }
30
+ }
31
+ function getToastDurationMs() {
32
+ return unifiedToastDurationMs;
33
+ }
34
+ function showToast(api, input) {
35
+ const duration = typeof input.durationOverrideMs === "number" && Number.isFinite(input.durationOverrideMs) ? input.durationOverrideMs : getToastDurationMs();
36
+ // toast_duration_ms = 0 disables Magic Context toasts entirely. An explicit
37
+ // positive per-call override (e.g. restart-required) still shows; only a
38
+ // non-positive effective duration suppresses the toast.
39
+ if (!(duration > 0)) {
40
+ return;
41
+ }
42
+ api.ui.toast({
43
+ message: input.message,
44
+ variant: input.variant,
45
+ duration
46
+ });
47
+ }
48
+ function showConflictDialog(api, directory, reasons, conflicts) {
49
+ api.ui.dialog.replace(() => _$createComponent(api.ui.DialogConfirm, {
50
+ title: "\u26A0\uFE0F Magic Context Disabled",
51
+ get message() {
52
+ return `${reasons.join("\n")}\n\nFix these conflicts automatically?`;
53
+ },
54
+ onConfirm: () => {
55
+ const actions = fixConflicts(directory, conflicts);
56
+ const actionSummary = actions.length > 0 ? actions.map(a => `• ${a}`).join("\n") : "No changes needed";
57
+ // DialogConfirm calls dialog.clear() after onConfirm, so defer the next dialog
58
+ setTimeout(() => {
59
+ api.ui.dialog.replace(() => _$createComponent(api.ui.DialogAlert, {
60
+ title: "\u2705 Configuration Fixed",
61
+ message: `${actionSummary}\n\nPlease restart OpenCode for changes to take effect.`,
62
+ onConfirm: () => {
63
+ showToast(api, {
64
+ message: "Restart OpenCode to enable Magic Context",
65
+ variant: "warning",
66
+ durationOverrideMs: 10_000
67
+ });
68
+ }
69
+ }));
70
+ }, 50);
71
+ },
72
+ onCancel: () => {
73
+ showToast(api, {
74
+ message: "Magic Context remains disabled. Run: npx @cortexkit/opencode-magic-context@latest doctor",
75
+ variant: "warning"
76
+ });
77
+ }
78
+ }));
79
+ }
80
+ function fmt(n) {
81
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
82
+ if (n >= 1_000) return `${Math.round(n / 1_000)}K`;
83
+ return String(n);
84
+ }
85
+ function fmtBytes(n) {
86
+ if (n >= 1_048_576) return `${(n / 1_048_576).toFixed(1)} MB`;
87
+ if (n >= 1_024) return `${Math.round(n / 1_024)} KB`;
88
+ return `${n} B`;
89
+ }
90
+ function relTime(ms) {
91
+ const d = Date.now() - ms;
92
+ if (d < 60_000) return "just now";
93
+ if (d < 3_600_000) return `${Math.floor(d / 60_000)}m ago`;
94
+ if (d < 86_400_000) return `${Math.floor(d / 3_600_000)}h ago`;
95
+ return `${Math.floor(d / 86_400_000)}d ago`;
96
+ }
97
+ function getSessionId(api) {
98
+ try {
99
+ const route = api.route.current;
100
+ if (route?.name === "session" && route.params?.sessionID) {
101
+ return route.params.sessionID;
102
+ }
103
+ } catch {
104
+ // ignore
105
+ }
106
+ return null;
107
+ }
108
+ const R = props => (() => {
109
+ var _el$ = _$createElement("box"),
110
+ _el$2 = _$createElement("text"),
111
+ _el$3 = _$createElement("text");
112
+ _$insertNode(_el$, _el$2);
113
+ _$insertNode(_el$, _el$3);
114
+ _$setProp(_el$, "width", "100%");
115
+ _$setProp(_el$, "flexDirection", "row");
116
+ _$setProp(_el$, "justifyContent", "space-between");
117
+ _$insert(_el$2, () => props.l);
118
+ _$insert(_el$3, () => props.v);
119
+ _$effect(_p$ => {
120
+ var _v$ = props.t.textMuted,
121
+ _v$2 = props.fg ?? props.t.text;
122
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
123
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$3, "fg", _v$2, _p$.t));
124
+ return _p$;
125
+ }, {
126
+ e: undefined,
127
+ t: undefined
128
+ });
129
+ return _el$;
130
+ })();
131
+ const StatusDialog = props => {
132
+ const theme = createMemo(() => props.api.theme.current);
133
+ const t = () => theme();
134
+ const s = () => props.s;
135
+
136
+ // Prefer the RPC-provided model context limit (what the sidebar shows) so the
137
+ // two surfaces never disagree. Fall back to deriving from usage% only when the
138
+ // RPC limit is absent (0) — and that derivation is itself undefined at 0%, so
139
+ // it stays "?" rather than showing a number inconsistent with the sidebar.
140
+ const contextLimit = () => s().contextLimit > 0 ? s().contextLimit : s().usagePercentage > 0 ? Math.round(s().inputTokens / (s().usagePercentage / 100)) : 0;
141
+ const elapsed = () => s().lastResponseTime > 0 ? Date.now() - s().lastResponseTime : 0;
142
+
143
+ // Token breakdown segments — same colors as sidebar. Kept in sync with
144
+ // slots/sidebar-content.tsx so the status dialog and sidebar read identically.
145
+ const COLORS = {
146
+ // Cool / structured — injected by the plugin into message[0]
147
+ system: "#c084fc",
148
+ docs: "#22d3ee",
149
+ compartments: "#60a5fa",
150
+ facts: "#fbbf24",
151
+ memories: "#34d399",
152
+ profile: "#a3e635",
153
+ // Warm / user-facing — chat and tool traffic
154
+ conversation: "#f87171",
155
+ toolCalls: "#fb923c",
156
+ toolDefs: "#f472b6"
157
+ };
158
+ const breakdownSegments = () => {
159
+ const d = s();
160
+ const total = d.inputTokens || 1;
161
+ const segs = [];
162
+ if (d.systemPromptTokens > 0) segs.push({
163
+ label: "System",
164
+ tokens: d.systemPromptTokens,
165
+ color: COLORS.system
166
+ });
167
+ if (d.docsTokens > 0) segs.push({
168
+ label: "Docs",
169
+ tokens: d.docsTokens,
170
+ color: COLORS.docs
171
+ });
172
+ if (d.compartmentTokens > 0) segs.push({
173
+ label: "Compartments",
174
+ tokens: d.compartmentTokens,
175
+ color: COLORS.compartments,
176
+ detail: `(${d.compartmentCount})`
177
+ });
178
+ if (d.factTokens > 0) segs.push({
179
+ label: "Facts",
180
+ tokens: d.factTokens,
181
+ color: COLORS.facts
182
+ });
183
+ if (d.memoryTokens > 0) segs.push({
184
+ label: "Memories",
185
+ tokens: d.memoryTokens,
186
+ color: COLORS.memories,
187
+ detail: `(${d.memoryBlockCount})`
188
+ });
189
+ if (d.profileTokens > 0) segs.push({
190
+ label: "User Profile",
191
+ tokens: d.profileTokens,
192
+ color: COLORS.profile
193
+ });
194
+ if (d.conversationTokens > 0) segs.push({
195
+ label: "Conversation",
196
+ tokens: d.conversationTokens,
197
+ color: COLORS.conversation
198
+ });
199
+ if (d.toolCallTokens > 0) segs.push({
200
+ label: "Tool Calls",
201
+ tokens: d.toolCallTokens,
202
+ color: COLORS.toolCalls
203
+ });
204
+ if (d.toolDefinitionTokens > 0) segs.push({
205
+ label: "Tool Defs",
206
+ tokens: d.toolDefinitionTokens,
207
+ color: COLORS.toolDefs
208
+ });
209
+ return {
210
+ segs,
211
+ total
212
+ };
213
+ };
214
+
215
+ // The status-dialog breakdown bar uses flex layout (same approach as the
216
+ // sidebar breakdown). Each segment becomes a colored box with
217
+ // flexGrow=tokens and flexBasis=0, parent has width="100%", so opentui
218
+ // distributes the dialog's full width proportionally regardless of the
219
+ // dialog's actual rendered width.
220
+ const barSegments = () => breakdownSegments().segs.filter(seg => seg.tokens > 0);
221
+ return (() => {
222
+ var _el$4 = _$createElement("box"),
223
+ _el$5 = _$createElement("box"),
224
+ _el$6 = _$createElement("text"),
225
+ _el$7 = _$createElement("b"),
226
+ _el$9 = _$createElement("text"),
227
+ _el$0 = _$createTextNode(`v`),
228
+ _el$1 = _$createElement("box"),
229
+ _el$10 = _$createElement("text"),
230
+ _el$11 = _$createElement("b"),
231
+ _el$12 = _$createTextNode(`%`),
232
+ _el$13 = _$createTextNode(` / `),
233
+ _el$14 = _$createTextNode(`%`),
234
+ _el$15 = _$createElement("text"),
235
+ _el$16 = _$createTextNode(` / `),
236
+ _el$17 = _$createTextNode(` tokens`),
237
+ _el$18 = _$createElement("box"),
238
+ _el$19 = _$createElement("box"),
239
+ _el$20 = _$createElement("box"),
240
+ _el$21 = _$createElement("box"),
241
+ _el$22 = _$createElement("text"),
242
+ _el$23 = _$createElement("b"),
243
+ _el$25 = _$createElement("box"),
244
+ _el$26 = _$createElement("text"),
245
+ _el$27 = _$createElement("b"),
246
+ _el$29 = _$createElement("box"),
247
+ _el$30 = _$createElement("text"),
248
+ _el$31 = _$createElement("b"),
249
+ _el$33 = _$createElement("box"),
250
+ _el$34 = _$createElement("text"),
251
+ _el$35 = _$createElement("b"),
252
+ _el$37 = _$createElement("box"),
253
+ _el$38 = _$createElement("text"),
254
+ _el$39 = _$createElement("b"),
255
+ _el$41 = _$createElement("box"),
256
+ _el$42 = _$createElement("text"),
257
+ _el$43 = _$createElement("b"),
258
+ _el$45 = _$createElement("box"),
259
+ _el$46 = _$createElement("text"),
260
+ _el$47 = _$createElement("b"),
261
+ _el$49 = _$createElement("box"),
262
+ _el$50 = _$createElement("text");
263
+ _$insertNode(_el$4, _el$5);
264
+ _$insertNode(_el$4, _el$1);
265
+ _$insertNode(_el$4, _el$18);
266
+ _$insertNode(_el$4, _el$19);
267
+ _$insertNode(_el$4, _el$20);
268
+ _$insertNode(_el$4, _el$49);
269
+ _$setProp(_el$4, "flexDirection", "column");
270
+ _$setProp(_el$4, "width", "100%");
271
+ _$setProp(_el$4, "paddingLeft", 2);
272
+ _$setProp(_el$4, "paddingRight", 2);
273
+ _$setProp(_el$4, "paddingTop", 1);
274
+ _$setProp(_el$4, "paddingBottom", 1);
275
+ _$insertNode(_el$5, _el$6);
276
+ _$insertNode(_el$5, _el$9);
277
+ _$setProp(_el$5, "justifyContent", "center");
278
+ _$setProp(_el$5, "width", "100%");
279
+ _$setProp(_el$5, "marginBottom", 1);
280
+ _$setProp(_el$5, "flexDirection", "row");
281
+ _$setProp(_el$5, "gap", 2);
282
+ _$insertNode(_el$6, _el$7);
283
+ _$insertNode(_el$7, _$createTextNode(`⚡ Magic Context Status`));
284
+ _$insertNode(_el$9, _el$0);
285
+ _$insert(_el$9, () => packageJson.version, null);
286
+ _$insertNode(_el$1, _el$10);
287
+ _$insertNode(_el$1, _el$15);
288
+ _$setProp(_el$1, "flexDirection", "row");
289
+ _$setProp(_el$1, "justifyContent", "space-between");
290
+ _$setProp(_el$1, "width", "100%");
291
+ _$insertNode(_el$10, _el$11);
292
+ _$insertNode(_el$10, _el$13);
293
+ _$insertNode(_el$10, _el$14);
294
+ _$insertNode(_el$11, _el$12);
295
+ _$insert(_el$11, () => s().usagePercentage.toFixed(1), _el$12);
296
+ _$insert(_el$10, () => formatThresholdPercent(s().executeThreshold), _el$14);
297
+ _$insertNode(_el$15, _el$16);
298
+ _$insertNode(_el$15, _el$17);
299
+ _$insert(_el$15, () => fmt(s().inputTokens), _el$16);
300
+ _$insert(_el$15, (() => {
301
+ var _c$ = _$memo(() => contextLimit() > 0);
302
+ return () => _c$() ? fmt(contextLimit()) : "?";
303
+ })(), _el$17);
304
+ _$setProp(_el$18, "width", "100%");
305
+ _$setProp(_el$18, "flexDirection", "row");
306
+ _$setProp(_el$18, "height", 1);
307
+ _$insert(_el$18, () => barSegments().map(seg => (() => {
308
+ var _el$52 = _$createElement("box");
309
+ _$setProp(_el$52, "flexBasis", 0);
310
+ _$setProp(_el$52, "height", 1);
311
+ _$effect(_p$ => {
312
+ var _v$13 = seg.label,
313
+ _v$14 = Math.max(1, seg.tokens),
314
+ _v$15 = seg.color;
315
+ _v$13 !== _p$.e && (_p$.e = _$setProp(_el$52, "key", _v$13, _p$.e));
316
+ _v$14 !== _p$.t && (_p$.t = _$setProp(_el$52, "flexGrow", _v$14, _p$.t));
317
+ _v$15 !== _p$.a && (_p$.a = _$setProp(_el$52, "backgroundColor", _v$15, _p$.a));
318
+ return _p$;
319
+ }, {
320
+ e: undefined,
321
+ t: undefined,
322
+ a: undefined
323
+ });
324
+ return _el$52;
325
+ })()));
326
+ _$setProp(_el$19, "flexDirection", "column");
327
+ _$insert(_el$19, () => breakdownSegments().segs.map(seg => {
328
+ const pct = (seg.tokens / breakdownSegments().total * 100).toFixed(1);
329
+ return (() => {
330
+ var _el$53 = _$createElement("box"),
331
+ _el$54 = _$createElement("text"),
332
+ _el$55 = _$createTextNode(` `),
333
+ _el$56 = _$createElement("text"),
334
+ _el$57 = _$createTextNode(` (`),
335
+ _el$58 = _$createTextNode(`%)`);
336
+ _$insertNode(_el$53, _el$54);
337
+ _$insertNode(_el$53, _el$56);
338
+ _$setProp(_el$53, "width", "100%");
339
+ _$setProp(_el$53, "flexDirection", "row");
340
+ _$setProp(_el$53, "justifyContent", "space-between");
341
+ _$insertNode(_el$54, _el$55);
342
+ _$insert(_el$54, () => seg.label, _el$55);
343
+ _$insert(_el$54, () => seg.detail ?? "", null);
344
+ _$insertNode(_el$56, _el$57);
345
+ _$insertNode(_el$56, _el$58);
346
+ _$insert(_el$56, () => fmt(seg.tokens), _el$57);
347
+ _$insert(_el$56, pct, _el$58);
348
+ _$effect(_p$ => {
349
+ var _v$16 = seg.label,
350
+ _v$17 = seg.color,
351
+ _v$18 = t().textMuted;
352
+ _v$16 !== _p$.e && (_p$.e = _$setProp(_el$53, "key", _v$16, _p$.e));
353
+ _v$17 !== _p$.t && (_p$.t = _$setProp(_el$54, "fg", _v$17, _p$.t));
354
+ _v$18 !== _p$.a && (_p$.a = _$setProp(_el$56, "fg", _v$18, _p$.a));
355
+ return _p$;
356
+ }, {
357
+ e: undefined,
358
+ t: undefined,
359
+ a: undefined
360
+ });
361
+ return _el$53;
362
+ })();
363
+ }));
364
+ _$insert(_el$4, (() => {
365
+ var _c$2 = _$memo(() => !!s().recompProgress);
366
+ return () => _c$2() && (() => {
367
+ const p = s().recompProgress;
368
+ // Label follows the flow that started the run, so a plain
369
+ // /ctx-recomp never reads as an "Upgrade" (dogfood 2026-06-04).
370
+ const verb = p.kind === "upgrade" ? "Upgrade" : p.kind === "embed" ? "Embed" : "Recomp";
371
+ return (() => {
372
+ var _el$59 = _$createElement("box"),
373
+ _el$60 = _$createElement("text"),
374
+ _el$61 = _$createElement("b");
375
+ _$insertNode(_el$59, _el$60);
376
+ _$setProp(_el$59, "marginTop", 1);
377
+ _$setProp(_el$59, "width", "100%");
378
+ _$setProp(_el$59, "flexDirection", "column");
379
+ _$insertNode(_el$60, _el$61);
380
+ _$insert(_el$61, verb);
381
+ _$insert(_el$59, () => {
382
+ if (p.phase === "recomp") {
383
+ const frac = p.totalMessages > 0 ? p.processedMessages / p.totalMessages : 0;
384
+ const width = 24;
385
+ const filled = Math.round(Math.max(0, Math.min(1, frac)) * width);
386
+ const bar = p.totalMessages > 0 ? `[${"█".repeat(filled)}${"░".repeat(width - filled)}]` : "(starting…)";
387
+ const activeLabel = p.kind === "upgrade" ? "upgrading" : p.kind === "embed" ? "embedding" : "comparting";
388
+ return [_$createComponent(R, {
389
+ get t() {
390
+ return t();
391
+ },
392
+ l: activeLabel,
393
+ get v() {
394
+ return _$memo(() => p.totalMessages > 0)() ? `${bar} ${Math.round(frac * 100)}%` : bar;
395
+ },
396
+ get fg() {
397
+ return t().warning;
398
+ }
399
+ }), _$memo(() => _$memo(() => !!p.note)() ? _$createComponent(R, {
400
+ get t() {
401
+ return t();
402
+ },
403
+ l: "Status",
404
+ get v() {
405
+ return p.note;
406
+ },
407
+ get fg() {
408
+ return t().textMuted;
409
+ }
410
+ }) : null), _$memo(() => _$memo(() => p.kind === "embed")() ? _$createComponent(R, {
411
+ get t() {
412
+ return t();
413
+ },
414
+ l: "Compartments",
415
+ get v() {
416
+ return `${p.processedMessages}/${p.totalMessages} embedded`;
417
+ },
418
+ get fg() {
419
+ return t().textMuted;
420
+ }
421
+ }) : _$createComponent(R, {
422
+ get t() {
423
+ return t();
424
+ },
425
+ l: "Compartments",
426
+ get v() {
427
+ return `${p.compartmentsCreated} (${p.passCount} pass${p.passCount === 1 ? "" : "es"})`;
428
+ },
429
+ get fg() {
430
+ return t().textMuted;
431
+ }
432
+ }))];
433
+ }
434
+ if (p.phase === "migration") return _$createComponent(R, {
435
+ get t() {
436
+ return t();
437
+ },
438
+ l: "Status",
439
+ get v() {
440
+ return p.note ?? "Migrating memories ⟳";
441
+ },
442
+ get fg() {
443
+ return t().warning;
444
+ }
445
+ });
446
+ if (p.phase === "done") return _$createComponent(R, {
447
+ get t() {
448
+ return t();
449
+ },
450
+ l: "Status",
451
+ v: `✓ ${verb} complete`,
452
+ get fg() {
453
+ return t().accent;
454
+ }
455
+ });
456
+ if (p.phase === "skipped") return _$createComponent(R, {
457
+ get t() {
458
+ return t();
459
+ },
460
+ l: "Status",
461
+ get v() {
462
+ return p.message ?? `${verb} stopped early`;
463
+ },
464
+ get fg() {
465
+ return t().textMuted;
466
+ }
467
+ });
468
+ return _$createComponent(R, {
469
+ get t() {
470
+ return t();
471
+ },
472
+ l: "Status",
473
+ get v() {
474
+ return `✗ ${verb} failed${p.message ? `: ${p.message}` : ""}`;
475
+ },
476
+ get fg() {
477
+ return t().error;
478
+ }
479
+ });
480
+ }, null);
481
+ _$effect(_$p => _$setProp(_el$60, "fg", t().text, _$p));
482
+ return _el$59;
483
+ })();
484
+ })();
485
+ })(), _el$20);
486
+ _$insertNode(_el$20, _el$21);
487
+ _$insertNode(_el$20, _el$37);
488
+ _$setProp(_el$20, "flexDirection", "row");
489
+ _$setProp(_el$20, "width", "100%");
490
+ _$setProp(_el$20, "marginTop", 1);
491
+ _$setProp(_el$20, "gap", 4);
492
+ _$insertNode(_el$21, _el$22);
493
+ _$insertNode(_el$21, _el$25);
494
+ _$insertNode(_el$21, _el$29);
495
+ _$insertNode(_el$21, _el$33);
496
+ _$setProp(_el$21, "flexDirection", "column");
497
+ _$setProp(_el$21, "flexGrow", 1);
498
+ _$setProp(_el$21, "flexBasis", 0);
499
+ _$insertNode(_el$22, _el$23);
500
+ _$insertNode(_el$23, _$createTextNode(`Tags`));
501
+ _$insert(_el$21, _$createComponent(R, {
502
+ get t() {
503
+ return t();
504
+ },
505
+ l: "Active",
506
+ get v() {
507
+ return `${s().activeTags} (~${fmtBytes(s().activeBytes)})`;
508
+ }
509
+ }), _el$25);
510
+ _$insert(_el$21, _$createComponent(R, {
511
+ get t() {
512
+ return t();
513
+ },
514
+ l: "Dropped",
515
+ get v() {
516
+ return String(s().droppedTags);
517
+ }
518
+ }), _el$25);
519
+ _$insert(_el$21, _$createComponent(R, {
520
+ get t() {
521
+ return t();
522
+ },
523
+ l: "Total",
524
+ get v() {
525
+ return String(s().totalTags);
526
+ },
527
+ get fg() {
528
+ return t().textMuted;
529
+ }
530
+ }), _el$25);
531
+ _$insertNode(_el$25, _el$26);
532
+ _$setProp(_el$25, "marginTop", 1);
533
+ _$insertNode(_el$26, _el$27);
534
+ _$insertNode(_el$27, _$createTextNode(`Pending Queue`));
535
+ _$insert(_el$21, _$createComponent(R, {
536
+ get t() {
537
+ return t();
538
+ },
539
+ l: "Drops",
540
+ get v() {
541
+ return String(s().pendingOpsCount);
542
+ },
543
+ get fg() {
544
+ return _$memo(() => s().pendingOpsCount > 0)() ? t().warning : t().textMuted;
545
+ }
546
+ }), _el$29);
547
+ _$insertNode(_el$29, _el$30);
548
+ _$setProp(_el$29, "marginTop", 1);
549
+ _$insertNode(_el$30, _el$31);
550
+ _$insertNode(_el$31, _$createTextNode(`Cache TTL`));
551
+ _$insert(_el$21, _$createComponent(R, {
552
+ get t() {
553
+ return t();
554
+ },
555
+ l: "Configured",
556
+ get v() {
557
+ return s().cacheTtl;
558
+ }
559
+ }), _el$33);
560
+ _$insert(_el$21, _$createComponent(R, {
561
+ get t() {
562
+ return t();
563
+ },
564
+ l: "Last response",
565
+ get v() {
566
+ return _$memo(() => s().lastResponseTime > 0)() ? `${Math.round(elapsed() / 1000)}s ago` : "never";
567
+ }
568
+ }), _el$33);
569
+ _$insert(_el$21, _$createComponent(R, {
570
+ get t() {
571
+ return t();
572
+ },
573
+ l: "Remaining",
574
+ get v() {
575
+ return _$memo(() => !!s().cacheExpired)() ? "expired" : `${Math.round(s().cacheRemainingMs / 1000)}s`;
576
+ },
577
+ get fg() {
578
+ return _$memo(() => !!s().cacheExpired)() ? t().warning : t().textMuted;
579
+ }
580
+ }), _el$33);
581
+ _$insert(_el$21, _$createComponent(R, {
582
+ get t() {
583
+ return t();
584
+ },
585
+ l: "Auto-execute",
586
+ get v() {
587
+ return _$memo(() => !!s().cacheExpired)() ? "yes (expired)" : `at TTL or ≥${formatThresholdPercent(s().executeThreshold)}%`;
588
+ },
589
+ get fg() {
590
+ return t().textMuted;
591
+ }
592
+ }), _el$33);
593
+ _$insertNode(_el$33, _el$34);
594
+ _$setProp(_el$33, "marginTop", 1);
595
+ _$insertNode(_el$34, _el$35);
596
+ _$insertNode(_el$35, _$createTextNode(`Memory`));
597
+ _$insert(_el$21, _$createComponent(R, {
598
+ get t() {
599
+ return t();
600
+ },
601
+ l: "Active",
602
+ get v() {
603
+ return String(s().memoryCount);
604
+ },
605
+ get fg() {
606
+ return t().accent;
607
+ }
608
+ }), null);
609
+ _$insert(_el$21, _$createComponent(R, {
610
+ get t() {
611
+ return t();
612
+ },
613
+ l: "Injected",
614
+ get v() {
615
+ return String(s().memoryBlockCount);
616
+ },
617
+ get fg() {
618
+ return t().textMuted;
619
+ }
620
+ }), null);
621
+ _$insertNode(_el$37, _el$38);
622
+ _$insertNode(_el$37, _el$41);
623
+ _$insertNode(_el$37, _el$45);
624
+ _$setProp(_el$37, "flexDirection", "column");
625
+ _$setProp(_el$37, "flexGrow", 1);
626
+ _$setProp(_el$37, "flexBasis", 0);
627
+ _$insertNode(_el$38, _el$39);
628
+ _$insertNode(_el$39, _$createTextNode(`Reductions`));
629
+ _$insert(_el$37, _$createComponent(R, {
630
+ get t() {
631
+ return t();
632
+ },
633
+ l: "Execute threshold",
634
+ get v() {
635
+ return `${formatThresholdPercent(s().executeThreshold)}%`;
636
+ }
637
+ }), _el$41);
638
+ _$insert(_el$37, _$createComponent(R, {
639
+ get t() {
640
+ return t();
641
+ },
642
+ l: "Last reduce anchor",
643
+ get v() {
644
+ return `${fmt(s().lastNudgeTokens)} tok`;
645
+ }
646
+ }), _el$41);
647
+ _$insertNode(_el$41, _el$42);
648
+ _$setProp(_el$41, "marginTop", 1);
649
+ _$insertNode(_el$42, _el$43);
650
+ _$insertNode(_el$43, _$createTextNode(`Context Details`));
651
+ _$insert(_el$37, _$createComponent(R, {
652
+ get t() {
653
+ return t();
654
+ },
655
+ l: "Protected tags",
656
+ get v() {
657
+ return String(s().protectedTagCount);
658
+ },
659
+ get fg() {
660
+ return t().textMuted;
661
+ }
662
+ }), _el$45);
663
+ _$insert(_el$37, _$createComponent(R, {
664
+ get t() {
665
+ return t();
666
+ },
667
+ l: "Subagent",
668
+ get v() {
669
+ return s().isSubagent ? "yes" : "no";
670
+ },
671
+ get fg() {
672
+ return t().textMuted;
673
+ }
674
+ }), _el$45);
675
+ _$insertNode(_el$45, _el$46);
676
+ _$setProp(_el$45, "marginTop", 1);
677
+ _$insertNode(_el$46, _el$47);
678
+ _$insertNode(_el$47, _$createTextNode(`History Compression`));
679
+ _$insert(_el$37, _$createComponent(R, {
680
+ get t() {
681
+ return t();
682
+ },
683
+ l: "History block",
684
+ get v() {
685
+ return `~${fmt(s().historyBlockTokens)} tok`;
686
+ }
687
+ }), null);
688
+ _$insert(_el$37, (() => {
689
+ var _c$3 = _$memo(() => s().compressionBudget != null);
690
+ return () => _c$3() && _$createComponent(R, {
691
+ get t() {
692
+ return t();
693
+ },
694
+ l: "Budget",
695
+ get v() {
696
+ return `~${fmt(s().compressionBudget)} tok (${s().compressionUsage} used)`;
697
+ }
698
+ });
699
+ })(), null);
700
+ _$insert(_el$37, (() => {
701
+ var _c$4 = _$memo(() => !!s().lastDreamerRunAt);
702
+ return () => _c$4() && _$createComponent(R, {
703
+ get t() {
704
+ return t();
705
+ },
706
+ l: "Dreamer",
707
+ get v() {
708
+ return `last ${relTime(s().lastDreamerRunAt)}`;
709
+ },
710
+ get fg() {
711
+ return t().textMuted;
712
+ }
713
+ });
714
+ })(), null);
715
+ _$insert(_el$4, (() => {
716
+ var _c$5 = _$memo(() => !!s().lastTransformError);
717
+ return () => _c$5() && (() => {
718
+ var _el$62 = _$createElement("box"),
719
+ _el$63 = _$createElement("text"),
720
+ _el$64 = _$createTextNode(`⚠ `);
721
+ _$insertNode(_el$62, _el$63);
722
+ _$setProp(_el$62, "marginTop", 1);
723
+ _$setProp(_el$62, "width", "100%");
724
+ _$insertNode(_el$63, _el$64);
725
+ _$insert(_el$63, () => s().lastTransformError, null);
726
+ _$effect(_$p => _$setProp(_el$63, "fg", t().error, _$p));
727
+ return _el$62;
728
+ })();
729
+ })(), _el$49);
730
+ _$insertNode(_el$49, _el$50);
731
+ _$setProp(_el$49, "marginTop", 1);
732
+ _$setProp(_el$49, "justifyContent", "flex-end");
733
+ _$setProp(_el$49, "width", "100%");
734
+ _$insertNode(_el$50, _$createTextNode(`Esc to close`));
735
+ _$effect(_p$ => {
736
+ var _v$3 = t().accent,
737
+ _v$4 = t().textMuted,
738
+ _v$5 = s().usagePercentage >= 80 ? t().error : s().usagePercentage >= 65 ? t().warning : t().accent,
739
+ _v$6 = s().usagePercentage >= 80 ? t().error : s().usagePercentage >= 65 ? t().warning : t().accent,
740
+ _v$7 = t().text,
741
+ _v$8 = t().text,
742
+ _v$9 = t().text,
743
+ _v$0 = t().text,
744
+ _v$1 = t().text,
745
+ _v$10 = t().text,
746
+ _v$11 = t().text,
747
+ _v$12 = t().textMuted;
748
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$6, "fg", _v$3, _p$.e));
749
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$9, "fg", _v$4, _p$.t));
750
+ _v$5 !== _p$.a && (_p$.a = _$setProp(_el$10, "fg", _v$5, _p$.a));
751
+ _v$6 !== _p$.o && (_p$.o = _$setProp(_el$15, "fg", _v$6, _p$.o));
752
+ _v$7 !== _p$.i && (_p$.i = _$setProp(_el$22, "fg", _v$7, _p$.i));
753
+ _v$8 !== _p$.n && (_p$.n = _$setProp(_el$26, "fg", _v$8, _p$.n));
754
+ _v$9 !== _p$.s && (_p$.s = _$setProp(_el$30, "fg", _v$9, _p$.s));
755
+ _v$0 !== _p$.h && (_p$.h = _$setProp(_el$34, "fg", _v$0, _p$.h));
756
+ _v$1 !== _p$.r && (_p$.r = _$setProp(_el$38, "fg", _v$1, _p$.r));
757
+ _v$10 !== _p$.d && (_p$.d = _$setProp(_el$42, "fg", _v$10, _p$.d));
758
+ _v$11 !== _p$.l && (_p$.l = _$setProp(_el$46, "fg", _v$11, _p$.l));
759
+ _v$12 !== _p$.u && (_p$.u = _$setProp(_el$50, "fg", _v$12, _p$.u));
760
+ return _p$;
761
+ }, {
762
+ e: undefined,
763
+ t: undefined,
764
+ a: undefined,
765
+ o: undefined,
766
+ i: undefined,
767
+ n: undefined,
768
+ s: undefined,
769
+ h: undefined,
770
+ r: undefined,
771
+ d: undefined,
772
+ l: undefined,
773
+ u: undefined
774
+ });
775
+ return _el$4;
776
+ })();
777
+ };
778
+ function getModelKeyFromMessages(api, sessionId) {
779
+ try {
780
+ const msgs = api.state.session.messages(sessionId);
781
+ // Find the last assistant message with model info
782
+ // AssistantMessage has providerID/modelID as top-level fields
783
+ // UserMessage has model: { providerID, modelID }
784
+ for (let i = msgs.length - 1; i >= 0; i--) {
785
+ const msg = msgs[i];
786
+ if (msg.role === "assistant" && msg.providerID && msg.modelID) {
787
+ return `${msg.providerID}/${msg.modelID}`;
788
+ }
789
+ if (msg.role === "user") {
790
+ const model = msg.model;
791
+ if (model?.providerID && model?.modelID) {
792
+ return `${model.providerID}/${model.modelID}`;
793
+ }
794
+ }
795
+ }
796
+ } catch {
797
+ // messages not available
798
+ }
799
+ return undefined;
800
+ }
801
+ async function showRecompDialog(api, targetSessionId = getSessionId(api)) {
802
+ const sessionId = targetSessionId;
803
+ if (!sessionId) {
804
+ showToast(api, {
805
+ message: "No active session",
806
+ variant: "warning"
807
+ });
808
+ return false;
809
+ }
810
+ const count = await getCompartmentCount(sessionId);
811
+ // Ack only after the dialog is actually shown for the same active session;
812
+ // route switches while the RPC detail load is in flight must leave it pending.
813
+ if (getSessionId(api) !== sessionId) return false;
814
+ api.ui.dialog.replace(() => _$createComponent(api.ui.DialogConfirm, {
815
+ title: "\u26A0\uFE0F Recomp Confirmation",
816
+ get message() {
817
+ return [count === 0 ? "This session has no compartments yet — recomp will build them from raw history." : `You have ${count} compartments.`, "", "Recomp will regenerate all compartments and facts from raw history.", "This may take a long time and consume significant tokens.", "", "Proceed?"].join("\n");
818
+ },
819
+ onConfirm: () => {
820
+ void requestRecomp(sessionId);
821
+ kickRecompProgressRefresh();
822
+ showToast(api, {
823
+ message: "Recomp requested — historian will start shortly",
824
+ variant: "info"
825
+ });
826
+ },
827
+ onCancel: () => {
828
+ showToast(api, {
829
+ message: "Recomp cancelled",
830
+ variant: "info",
831
+ durationOverrideMs: 3000
832
+ });
833
+ }
834
+ }));
835
+ return true;
836
+ }
837
+ function showUpgradeDialog(api, resume, targetSessionId = getSessionId(api)) {
838
+ const sessionId = targetSessionId;
839
+ if (!sessionId) {
840
+ // No active session — nothing to upgrade. Silently skip (the server only
841
+ // enqueues this for sessions with legacy compartments, but the TUI may
842
+ // have switched sessions before the poller fired).
843
+ return false;
844
+ }
845
+ if (getSessionId(api) !== sessionId) return false;
846
+ const title = resume ? "🎆 Resume the interrupted upgrade?" : "🎆 Historian V2 is released!";
847
+ const message = resume ? [`An earlier upgrade to the new historian format was interrupted. ${resume.stagedCount} compartment${resume.stagedCount === 1 ? " was" : "s were"} already rebuilt (through message ${resume.stagedThrough}). Resuming continues from where it left off — nothing already rebuilt is reprocessed.`, "", "Resuming will:", "• Rebuild the remaining compartments into the new layered format", "• Re-organize this project's memories into the new taxonomy (once per project)", "", "The historian runs in the background and you can keep working. You can also resume via /ctx-session-upgrade later.", "", "Resume the upgrade now?"].join("\n") : ["This session's compartments are written by the old historian. The session is still usable with its old compartments, however it's strongly advised to upgrade them to the new format. This means every compartment needs to be reprocessed by the new historian, which might take a while depending on how big your session is.", "", "Running the upgrade will:", "• Rebuild this session's compartments into the new layered format", "• Re-organize this project's memories into the new taxonomy (once per project)", "", "The historian runs in the background and you can keep working while older compartments are reprocessed. You can also upgrade via /ctx-session-upgrade later.", "", "Run the upgrade now?"].join("\n");
848
+ api.ui.dialog.replace(() => _$createComponent(api.ui.DialogConfirm, {
849
+ title: title,
850
+ message: message,
851
+ onConfirm: () => {
852
+ // Start the sidebar's recomp self-poll immediately — the RPC
853
+ // call fires no message event, so without this the progress
854
+ // bar wouldn't appear until the upgrade finished.
855
+ kickRecompProgressRefresh();
856
+ showToast(api, {
857
+ message: resume ? "Resuming session upgrade — running in the background" : "Session upgrade started — running in the background",
858
+ variant: "info"
859
+ });
860
+ // Dismiss the durable reminder ONLY after the upgrade request
861
+ // actually started. If requestUpgrade() returns false (RPC /
862
+ // server / db / auth failure, restart race), the session stays
863
+ // legacy — dismissing first would set upgradeRemindedAt and
864
+ // suppress all future reminders for a session that never
865
+ // upgraded. (Resume prompts are staging-driven and unaffected.)
866
+ void requestUpgrade(sessionId).then(started => {
867
+ if (started) void dismissUpgradeReminder(sessionId);
868
+ });
869
+ },
870
+ onCancel: () => {
871
+ // Explicit decline → set the durable stamp so we don't re-prompt
872
+ // on every restart. The fix for stamp-on-display trapping a
873
+ // never-upgraded session (dogfood 2026-05-30) relies on THIS
874
+ // being the only place the TUI path stamps.
875
+ void dismissUpgradeReminder(sessionId);
876
+ showToast(api, {
877
+ message: "Upgrade skipped — run /ctx-session-upgrade anytime",
878
+ variant: "info",
879
+ durationOverrideMs: 4000
880
+ });
881
+ }
882
+ }));
883
+ return true;
884
+ }
885
+ async function showStatusDialog(api, targetSessionId = getSessionId(api)) {
886
+ const sessionId = targetSessionId;
887
+ if (!sessionId) {
888
+ showToast(api, {
889
+ message: "No active session",
890
+ variant: "warning"
891
+ });
892
+ return false;
893
+ }
894
+ const directory = api.state.path.directory ?? "";
895
+ const modelKey = getModelKeyFromMessages(api, sessionId);
896
+ const detail = await loadStatusDetail(sessionId, directory, modelKey);
897
+ if (getSessionId(api) !== sessionId) return false;
898
+ api.ui.dialog.replace(() => _$createComponent(StatusDialog, {
899
+ api: api,
900
+ s: detail
901
+ }));
902
+ return true;
903
+ }
904
+ const EmbedDialog = props => {
905
+ const theme = createMemo(() => props.api.theme.current);
906
+ const t = () => theme();
907
+ const lines = () => props.detail.statusText.split("\n");
908
+ return (() => {
909
+ var _el$65 = _$createElement("box"),
910
+ _el$66 = _$createElement("box"),
911
+ _el$67 = _$createElement("text"),
912
+ _el$68 = _$createElement("b");
913
+ _$insertNode(_el$65, _el$66);
914
+ _$setProp(_el$65, "flexDirection", "column");
915
+ _$setProp(_el$65, "width", "100%");
916
+ _$setProp(_el$65, "paddingLeft", 2);
917
+ _$setProp(_el$65, "paddingRight", 2);
918
+ _$setProp(_el$65, "paddingTop", 1);
919
+ _$setProp(_el$65, "paddingBottom", 1);
920
+ _$insertNode(_el$66, _el$67);
921
+ _$setProp(_el$66, "justifyContent", "center");
922
+ _$setProp(_el$66, "width", "100%");
923
+ _$setProp(_el$66, "marginBottom", 1);
924
+ _$insertNode(_el$67, _el$68);
925
+ _$insertNode(_el$68, _$createTextNode(`Embedding`));
926
+ _$insert(_el$65, () => lines().map(line => (() => {
927
+ var _el$70 = _$createElement("text");
928
+ _$insert(_el$70, line);
929
+ _$effect(_$p => _$setProp(_el$70, "fg", t().text, _$p));
930
+ return _el$70;
931
+ })()), null);
932
+ _$effect(_$p => _$setProp(_el$67, "fg", t().accent, _$p));
933
+ return _el$65;
934
+ })();
935
+ };
936
+ async function showEmbedDialog(api, targetSessionId = getSessionId(api)) {
937
+ const sessionId = targetSessionId;
938
+ if (!sessionId) {
939
+ api.ui.toast({
940
+ message: "No active session",
941
+ variant: "warning"
942
+ });
943
+ return false;
944
+ }
945
+ const directory = api.state.path.directory ?? "";
946
+ const detail = await loadEmbedDetail(sessionId, directory);
947
+ if (getSessionId(api) !== sessionId) return false;
948
+ api.ui.dialog.replace(() => _$createComponent(EmbedDialog, {
949
+ api: api,
950
+ detail: detail
951
+ }));
952
+ return true;
953
+ }
954
+ function showResultDialog(api, title, message) {
955
+ api.ui.dialog.replace(() => _$createComponent(api.ui.DialogAlert, {
956
+ title: title,
957
+ message: message,
958
+ onConfirm: () => {}
959
+ }));
960
+ return true;
961
+ }
962
+
963
+ /**
964
+ * Register Magic Context command palette entries, preferring the v1.14.42+
965
+ * `keymap.registerLayer` API and falling back to the legacy
966
+ * `api.command.register` for older hosts.
967
+ *
968
+ * The `keymap.registerLayer` shape uses `name`/`title`/`run`/`namespace`
969
+ * (see `@opencode-ai/plugin/tui` types) and is what the host's own legacy
970
+ * command-shim translates into. Calling it directly skips the deprecation
971
+ * warning and works without depending on the (now-deprecated) `api.command`
972
+ * namespace existing at all.
973
+ *
974
+ * Version coverage:
975
+ * 1.14.0–1.14.41 — `api.command.register` only
976
+ * 1.14.42–1.14.43 — both surfaces broken (api.command removed, keymap landed
977
+ * but with bugs); plugins crash on init either way
978
+ * 1.14.44+ — `api.keymap.registerLayer` canonical, `api.command` shim
979
+ */
980
+ function registerCommandPaletteEntries(api) {
981
+ const apiAny = api;
982
+ if (typeof apiAny.keymap?.registerLayer === "function") {
983
+ // Audit Finding #2 hardening: even when registerLayer exists as a
984
+ // function, the underlying keymap implementation in OpenCode TUI
985
+ // 1.14.42-1.14.43 can throw at call time. Without the try-catch the
986
+ // `return` below would propagate the throw and the legacy
987
+ // `command.register` fallback path (~20 lines down) would be
988
+ // unreachable. The cost is one debug log on the rare broken-TUI
989
+ // build; the benefit is that older command.register-only TUIs
990
+ // running alongside a partially-broken keymap surface still get
991
+ // their command palette entries.
992
+ try {
993
+ apiAny.keymap.registerLayer({
994
+ commands: [{
995
+ namespace: "palette",
996
+ name: "magic-context.status",
997
+ title: "Magic Context: Status",
998
+ category: "Magic Context",
999
+ run() {
1000
+ showStatusDialog(api);
1001
+ }
1002
+ }, {
1003
+ namespace: "palette",
1004
+ name: "magic-context.recomp",
1005
+ title: "Magic Context: Recomp",
1006
+ category: "Magic Context",
1007
+ run() {
1008
+ showRecompDialog(api);
1009
+ }
1010
+ }],
1011
+ bindings: []
1012
+ });
1013
+ return;
1014
+ } catch (err) {
1015
+ console.debug("[magic-context-tui] keymap.registerLayer threw; falling back to command.register", err);
1016
+ // Fall through to legacy registration.
1017
+ }
1018
+ }
1019
+ if (typeof apiAny.command?.register === "function") {
1020
+ apiAny.command.register(() => [{
1021
+ title: "Magic Context: Status",
1022
+ value: "magic-context.status",
1023
+ category: "Magic Context",
1024
+ onSelect() {
1025
+ showStatusDialog(api);
1026
+ }
1027
+ }, {
1028
+ title: "Magic Context: Recomp",
1029
+ value: "magic-context.recomp",
1030
+ category: "Magic Context",
1031
+ onSelect() {
1032
+ showRecompDialog(api);
1033
+ }
1034
+ }]);
1035
+ return;
1036
+ }
1037
+
1038
+ // Neither API surface is present. The TUI host can still load — we only
1039
+ // lose the command palette entry points. The sidebar (registered above
1040
+ // via api.slots.register) remains visible. Status/Recomp are still
1041
+ // reachable through the server-side `/ctx-status` and `/ctx-recomp`
1042
+ // slash commands, which the server handler bridges to the TUI dialogs
1043
+ // via RPC.
1044
+ }
1045
+
1046
+ /**
1047
+ * Show the one-shot "What's new" dialog on TUI startup if the server tells us
1048
+ * to. The server is the source of truth: it has the version + features
1049
+ * constants AND owns the persistence file. We just render and report back.
1050
+ *
1051
+ * Failure-tolerant by design — if the server isn't ready or the RPC fails,
1052
+ * we silently skip (the next TUI launch will retry).
1053
+ */
1054
+ /**
1055
+ * URLs render as plain text. Modern terminals (iTerm2, kitty, WezTerm, Ghostty,
1056
+ * recent macOS Terminal) auto-detect URLs and let users Cmd-click; older
1057
+ * terminals require manual copy. We tried opentui's `<a href>` JSX intrinsic
1058
+ * for application-level OSC 8 clickability, but it's a span-like element that
1059
+ * forced text out of opentui's word-wrap mode, causing bullets to bleed past
1060
+ * the dialog border. Pure-string children of `<text>` wrap correctly, so the
1061
+ * AFT-style DialogAlert + plain string is the right surface here.
1062
+ */
1063
+ async function showStartupAnnouncement(api) {
1064
+ try {
1065
+ const ann = await getAnnouncement();
1066
+ if (!ann.show || !ann.version || !ann.features || ann.features.length === 0) return;
1067
+ const title = `Magic Context v${ann.version}`;
1068
+ const lines = ["What's new:", "", ...ann.features.map(line => ` • ${line}`)];
1069
+ if (ann.footer && ann.footer.trim().length > 0) {
1070
+ // Blank-line separator keeps the persistent footer (Discord invite,
1071
+ // etc.) visually distinct from the version-specific bullets.
1072
+ lines.push("", ann.footer);
1073
+ }
1074
+ const message = lines.join("\n");
1075
+ api.ui.dialog.replace(() => _$createComponent(api.ui.DialogAlert, {
1076
+ title: title,
1077
+ message: message,
1078
+ onConfirm: () => {
1079
+ void markAnnounced();
1080
+ }
1081
+ }), () => {
1082
+ // User dismissed via Escape rather than confirming. Mark
1083
+ // dismissed anyway — they saw the dialog, that's the contract.
1084
+ void markAnnounced();
1085
+ });
1086
+ } catch {
1087
+ // RPC not ready yet (port file missing or transient HTTP failure) —
1088
+ // silently skip. The next TUI start re-checks.
1089
+ }
1090
+ }
1091
+ const tui = async (api, _options, meta) => {
1092
+ // Initialize RPC client for server communication
1093
+ const directory = api.state.path.directory ?? "";
1094
+ initRpcClient(directory);
1095
+ await refreshToastDurationMs();
1096
+
1097
+ // Register sidebar slot
1098
+ api.slots.register(createSidebarContentSlot(api));
1099
+
1100
+ // Register TUI command palette entries (no slash field — slash commands
1101
+ // are registered server-side so there's only one /ctx-* registration).
1102
+ // The server detects TUI mode and sends dialog requests via RPC instead
1103
+ // of sendIgnoredMessage.
1104
+ //
1105
+ // OpenCode 1.14.42 removed `api.command.register` entirely
1106
+ // (anomalyco/opencode#26053). A later patch (1.14.44+) reinstated it as
1107
+ // a deprecated shim that translates to `api.keymap.registerLayer`. To
1108
+ // work across all hosts (1.14.0–1.14.41 with command-only, the broken
1109
+ // 1.14.42–1.14.43, and 1.14.44+ where both exist), we prefer
1110
+ // `api.keymap.registerLayer` and fall back to `api.command.register`
1111
+ // only when keymap is missing.
1112
+ registerCommandPaletteEntries(api);
1113
+
1114
+ // Receive server→TUI notifications (toasts + dialog requests) over a single
1115
+ // persistent WebSocket, pushed the instant the server queues them. This
1116
+ // replaces the old 500ms HTTP poll whose new-connection-per-tick cost was the
1117
+ // source of idle TUI CPU (#200). The socket carries the active session in its
1118
+ // hello so the server scopes delivery; here we re-check the active session per
1119
+ // notification (it can change between queue and delivery) before acting.
1120
+ const handleNotification = async n => {
1121
+ const requestedSessionId = getSessionId(api);
1122
+ const generation = getRpcGeneration();
1123
+ // A session-scoped notification only applies while we're viewing that
1124
+ // session; global (session-less) ones always apply. Returning false leaves
1125
+ // it unacked so a TUI on the right session (or a later switch back) still
1126
+ // gets it.
1127
+ if (n.sessionId && requestedSessionId && n.sessionId !== requestedSessionId) {
1128
+ return false;
1129
+ }
1130
+ if (n.type === "toast") {
1131
+ const p = n.payload;
1132
+ showToast(api, {
1133
+ message: String(p.message ?? ""),
1134
+ variant: p.variant ?? "info",
1135
+ durationOverrideMs: typeof p.duration === "number" && Number.isFinite(p.duration) ? p.duration : undefined
1136
+ });
1137
+ return true;
1138
+ }
1139
+ if (n.type !== "action") return false;
1140
+ const action = n.payload?.action;
1141
+ const stillActive = () => getRpcGeneration() === generation && getSessionId(api) === requestedSessionId;
1142
+ if (action === "show-status-dialog") {
1143
+ return stillActive() && (await showStatusDialog(api, requestedSessionId));
1144
+ }
1145
+ if (action === "show-recomp-dialog") {
1146
+ return stillActive() && (await showRecompDialog(api, requestedSessionId));
1147
+ }
1148
+ if (action === "show-upgrade-dialog") {
1149
+ const resume = n.payload?.resume === true ? {
1150
+ stagedCount: Number(n.payload?.stagedCount ?? 0),
1151
+ stagedThrough: Number(n.payload?.stagedThrough ?? 0)
1152
+ } : undefined;
1153
+ return stillActive() && showUpgradeDialog(api, resume, requestedSessionId);
1154
+ }
1155
+ if (action === "show-embed-dialog") {
1156
+ return stillActive() && (await showEmbedDialog(api, requestedSessionId));
1157
+ }
1158
+ if (action === "wrapup-progress-kick") {
1159
+ // /ctx-wrapup blocks its command turn and fires no message events, so
1160
+ // the sidebar poll would never notice the run. Kick the fast progress
1161
+ // poll (same loop the recomp dialog kicks). The start toast arrives
1162
+ // separately via the ignored-message notification path.
1163
+ if (!stillActive()) return false;
1164
+ kickRecompProgressRefresh();
1165
+ return true;
1166
+ }
1167
+ if (action === "show-flush-dialog") {
1168
+ const flushMsg = String(n.payload?.message ?? "Flushed.");
1169
+ return stillActive() && showResultDialog(api, "Flush", flushMsg);
1170
+ }
1171
+ if (action === "show-result-dialog") {
1172
+ const title = String(n.payload?.title ?? "Magic Context");
1173
+ const body = String(n.payload?.message ?? "");
1174
+ return stillActive() && showResultDialog(api, title, body);
1175
+ }
1176
+ return false;
1177
+ };
1178
+ startNotificationSocket({
1179
+ getSessionId: () => getSessionId(api),
1180
+ onNotification: handleNotification
1181
+ });
1182
+
1183
+ // Clean up on dispose
1184
+ api.lifecycle.onDispose(() => {
1185
+ stopNotificationSocket();
1186
+ closeRpc();
1187
+ });
1188
+ const conflictResult = detectConflicts(directory);
1189
+ if (conflictResult.hasConflict) {
1190
+ showConflictDialog(api, directory, conflictResult.reasons, conflictResult.conflicts);
1191
+ return;
1192
+ }
1193
+
1194
+ // Show one-shot release announcement after conflict gate.
1195
+ // Fire-and-forget: if the server isn't ready or RPC fails, the next TUI
1196
+ // launch will retry. Dialog only appears once per ANNOUNCEMENT_VERSION
1197
+ // (persisted via mark-announced RPC writing last_announced_version).
1198
+ void showStartupAnnouncement(api);
1199
+ };
1200
+ const id = "opencode-magic-context";
1201
+ export default {
1202
+ id,
1203
+ tui
1204
+ };