@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.
- package/dist/config/schema/magic-context.d.ts +13 -0
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/magic-context/memory/verification-paths.d.ts +16 -0
- package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/package.json +9 -21
- package/src/tui/entry.mjs +16 -0
- package/src/tui-compiled/badge-contrast.ts +84 -0
- package/src/tui-compiled/data/context-db.ts +353 -0
- package/src/tui-compiled/data/notification-socket.ts +229 -0
- package/src/tui-compiled/index.tsx +1204 -0
- package/src/tui-compiled/slots/sidebar-content.tsx +1052 -0
- package/src/tui-compiled/types/opencode-plugin-tui.d.ts +232 -0
|
@@ -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
|
+
};
|