@cortexkit/aft-opencode 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/config.d.ts +0 -22
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/hooks/auto-update-checker/types.d.ts +0 -3
  4. package/dist/hooks/auto-update-checker/types.d.ts.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +347 -127
  7. package/dist/lsp-github-table.d.ts +0 -8
  8. package/dist/lsp-github-table.d.ts.map +1 -1
  9. package/dist/notifications.d.ts +2 -9
  10. package/dist/notifications.d.ts.map +1 -1
  11. package/dist/shared/ignored-message.d.ts +9 -7
  12. package/dist/shared/ignored-message.d.ts.map +1 -1
  13. package/dist/shared/rpc-client.d.ts +23 -1
  14. package/dist/shared/rpc-client.d.ts.map +1 -1
  15. package/dist/subc-tool-schemas.d.ts.map +1 -1
  16. package/dist/tools/_shared.d.ts +0 -7
  17. package/dist/tools/_shared.d.ts.map +1 -1
  18. package/dist/tools/hoisted.d.ts.map +1 -1
  19. package/dist/tools/permissions.d.ts +7 -0
  20. package/dist/tools/permissions.d.ts.map +1 -1
  21. package/dist/tools/semantic.d.ts.map +1 -1
  22. package/dist/tui/notification-socket.d.ts.map +1 -1
  23. package/package.json +17 -14
  24. package/src/shared/ignored-message.ts +38 -19
  25. package/src/shared/rpc-client.ts +116 -4
  26. package/src/tui/entry.mjs +16 -0
  27. package/src/tui/notification-socket.ts +28 -1
  28. package/src/tui-compiled/badge-contrast.ts +43 -0
  29. package/src/tui-compiled/index.tsx +992 -0
  30. package/src/tui-compiled/notification-socket.ts +448 -0
  31. package/src/tui-compiled/preferences.ts +243 -0
  32. package/src/tui-compiled/sidebar.tsx +936 -0
  33. package/src/tui-compiled/types/opencode-plugin-tui.d.ts +239 -0
  34. package/dist/tui.js +0 -13462
@@ -0,0 +1,992 @@
1
+ import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid";
2
+ import { memo as _$memo } from "opentui:runtime-module:%40opentui%2Fsolid";
3
+ import { createTextNode as _$createTextNode } from "opentui:runtime-module:%40opentui%2Fsolid";
4
+ import { effect as _$effect } from "opentui:runtime-module:%40opentui%2Fsolid";
5
+ import { insertNode as _$insertNode } from "opentui:runtime-module:%40opentui%2Fsolid";
6
+ import { insert as _$insert } from "opentui:runtime-module:%40opentui%2Fsolid";
7
+ import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid";
8
+ import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
9
+ /** @jsxImportSource @opentui/solid */
10
+ // @ts-nocheck
11
+
12
+ import { createMemo, createSignal, onCleanup } from "opentui:runtime-module:solid-js";
13
+ import { version as packageVersion } from "../../package.json";
14
+ import { AftRpcClient } from "../shared/rpc-client";
15
+ import { coerceAftStatus, formatBytes, formatSemanticIndexStatus, formatSemanticRefreshing } from "../shared/status";
16
+ import { createDebouncedStatusRefresh, refreshAftTuiSocketScope, startAftTuiSocket, stopAftTuiSocket, subscribeStatusInvalidations } from "./notification-socket";
17
+ import { createAftSidebarSlot, formatCompressionSidebarRows, isSnapshotForContext, resolveTuiStorageDir, shouldSuppressUninitializedDowngrade } from "./sidebar";
18
+
19
+ // The TUI talks to the server plugin via AftRpcClient. The client reads the
20
+ // JSON port file written by AftRpcServer ({ port, token }) and includes that
21
+ // per-server token on every RPC request; legacy integer port files are still
22
+ // tolerated for already-running older server plugins.
23
+
24
+ // RPC clients keyed by directory — one per project
25
+ const rpcClients = new Map();
26
+ function getRpcClient(directory) {
27
+ let client = rpcClients.get(directory);
28
+ if (client) return client;
29
+ client = new AftRpcClient(resolveTuiStorageDir(), directory);
30
+ rpcClients.set(directory, client);
31
+ return client;
32
+ }
33
+ function getSessionId(api) {
34
+ try {
35
+ const route = api.route.current;
36
+ if (route?.name === "session" && route.params?.sessionID) {
37
+ return route.params.sessionID;
38
+ }
39
+ } catch {
40
+ // ignore
41
+ }
42
+ return null;
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // StatusDialog — themed, two-column JSX dialog. OpenCode renders DialogAlert
47
+ // text in a proportional font with no column alignment, so the status view uses
48
+ // TUI flex primitives instead of a padded monospace string. The component
49
+ // subscribes to server-pushed invalidations so it can re-render as the status
50
+ // snapshot changes, with no parent re-mount.
51
+ // ---------------------------------------------------------------------------
52
+
53
+ function formatCountShort(value) {
54
+ if (value == null || !Number.isFinite(value)) return "—";
55
+ if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
56
+ if (value >= 1_000) return `${Math.round(value / 1_000)}K`;
57
+ return String(value);
58
+ }
59
+ function statusTone(status) {
60
+ switch (status) {
61
+ case "ready":
62
+ return "ok";
63
+ case "loading":
64
+ case "building":
65
+ return "warn";
66
+ case "failed":
67
+ case "error":
68
+ return "err";
69
+ default:
70
+ return "muted";
71
+ }
72
+ }
73
+ function pickToneColor(theme, tone) {
74
+ switch (tone) {
75
+ case "ok":
76
+ return theme.success ?? theme.accent;
77
+ case "warn":
78
+ return theme.warning;
79
+ case "err":
80
+ return theme.error;
81
+ case "muted":
82
+ return theme.textMuted;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Label/value row. Label is left-aligned and muted; value is right-aligned
88
+ * and themed. flexDirection="row" + justifyContent="space-between" replaces
89
+ * the monospace `padEnd(40)` hack from the previous string formatter.
90
+ */
91
+ const R = props => {
92
+ const fg = createMemo(() => {
93
+ if (!props.tone) return props.theme.text;
94
+ if (props.tone === "accent") return props.theme.accent;
95
+ return pickToneColor(props.theme, props.tone);
96
+ });
97
+ return (() => {
98
+ var _el$ = _$createElement("box"),
99
+ _el$2 = _$createElement("text"),
100
+ _el$3 = _$createElement("text");
101
+ _$insertNode(_el$, _el$2);
102
+ _$insertNode(_el$, _el$3);
103
+ _$setProp(_el$, "flexDirection", "row");
104
+ _$setProp(_el$, "width", "100%");
105
+ _$setProp(_el$, "justifyContent", "space-between");
106
+ _$insert(_el$2, () => props.label);
107
+ _$insert(_el$3, () => props.value);
108
+ _$effect(_p$ => {
109
+ var _v$ = props.theme.textMuted,
110
+ _v$2 = fg();
111
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
112
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$3, "fg", _v$2, _p$.t));
113
+ return _p$;
114
+ }, {
115
+ e: undefined,
116
+ t: undefined
117
+ });
118
+ return _el$;
119
+ })();
120
+ };
121
+ /**
122
+ * Shared accept-gate for status RPC calls: skip warm responses that describe
123
+ * another project (cross-project contamination from multi-project hosts —
124
+ * see isSnapshotForContext) so they can't beat the right server's response.
125
+ */
126
+ function statusAcceptGate(directory) {
127
+ return result => {
128
+ const rec = result;
129
+ if (rec?.success === false) return true; // error envelopes handled by callers
130
+ return isSnapshotForContext(coerceAftStatus(rec), directory, rec?.served_directory);
131
+ };
132
+ }
133
+ const StatusDialog = props => {
134
+ const theme = createMemo(() => props.api.theme.current);
135
+ const t = () => theme();
136
+
137
+ // Reactive status signal — the dialog re-renders on pushed status
138
+ // invalidations without remounting.
139
+ const [status, setStatus] = createSignal(props.initial);
140
+ const [error, setError] = createSignal(props.initialError);
141
+ let refreshGeneration = 0;
142
+ let refreshController = null;
143
+ const refreshStatus = async () => {
144
+ if (refreshController) return;
145
+ const controller = new AbortController();
146
+ const requestGeneration = ++refreshGeneration;
147
+ refreshController = controller;
148
+ try {
149
+ const response = await props.client.call("status", {
150
+ sessionID: props.sessionID
151
+ }, {
152
+ signal: controller.signal,
153
+ accept: statusAcceptGate(props.directory)
154
+ });
155
+ if (controller.signal.aborted || requestGeneration !== refreshGeneration) return;
156
+ if (response.success !== false) {
157
+ const snapshot = coerceAftStatus(response);
158
+ // Stale-while-revalidate: don't downgrade a good snapshot to a transient
159
+ // `not_initialized` (for example, while the bridge process respawns or
160
+ // a session directory lookup briefly misses) — it arrives as success:true
161
+ // and would blank the dialog until the next refresh.
162
+ const current = status();
163
+ if (shouldSuppressUninitializedDowngrade(snapshot.cache_role, current !== null && current.cache_role !== "not_initialized")) {
164
+ return;
165
+ }
166
+ setStatus(snapshot);
167
+ setError(null);
168
+ }
169
+ } catch {
170
+ if (controller.signal.aborted || requestGeneration !== refreshGeneration) return;
171
+ // transient — keep showing last good snapshot
172
+ } finally {
173
+ if (refreshController === controller) refreshController = null;
174
+ }
175
+ };
176
+ const statusDebouncer = createDebouncedStatusRefresh(refreshStatus, 200);
177
+ const unsubscribeStatusInvalidations = subscribeStatusInvalidations(event => {
178
+ if (event.sessionId && event.sessionId !== props.sessionID) return;
179
+ statusDebouncer.schedule();
180
+ });
181
+ onCleanup(() => {
182
+ unsubscribeStatusInvalidations();
183
+ statusDebouncer.dispose();
184
+ refreshGeneration++;
185
+ if (refreshController) {
186
+ refreshController.abort();
187
+ refreshController = null;
188
+ }
189
+ });
190
+
191
+ // Visual cache-role badge: main is accent, worktree is warning,
192
+ // not_initialized is muted. Matches the sidebar convention.
193
+ const cacheRoleTone = role => role === "main" ? "accent" : role === "worktree" ? "warn" : "muted";
194
+ // Reuse the sidebar's label/value formatter so the dialog and sidebar
195
+ // render identical text (e.g. "Session" / "-174,489 tokens, 59% reduction").
196
+ // The earlier `formatCompressionDialogRows` returned padded strings that
197
+ // looked offset against neighboring sections.
198
+ const compressionAggregateRows = () => formatCompressionSidebarRows(status()?.compression);
199
+ return (() => {
200
+ var _el$4 = _$createElement("box"),
201
+ _el$5 = _$createElement("box"),
202
+ _el$6 = _$createElement("text"),
203
+ _el$7 = _$createElement("b"),
204
+ _el$9 = _$createElement("box"),
205
+ _el$0 = _$createElement("text");
206
+ _$insertNode(_el$4, _el$5);
207
+ _$insertNode(_el$4, _el$9);
208
+ _$setProp(_el$4, "flexDirection", "column");
209
+ _$setProp(_el$4, "width", "100%");
210
+ _$setProp(_el$4, "paddingLeft", 2);
211
+ _$setProp(_el$4, "paddingRight", 2);
212
+ _$setProp(_el$4, "paddingTop", 1);
213
+ _$setProp(_el$4, "paddingBottom", 1);
214
+ _$insertNode(_el$5, _el$6);
215
+ _$setProp(_el$5, "justifyContent", "center");
216
+ _$setProp(_el$5, "width", "100%");
217
+ _$setProp(_el$5, "marginBottom", 1);
218
+ _$setProp(_el$5, "flexDirection", "row");
219
+ _$setProp(_el$5, "gap", 2);
220
+ _$insertNode(_el$6, _el$7);
221
+ _$insertNode(_el$7, _$createTextNode(`⚡ AFT Status`));
222
+ _$insert(_el$5, (() => {
223
+ var _c$ = _$memo(() => status()?.cache_role !== "not_initialized");
224
+ return () => _c$() && (() => {
225
+ var _el$10 = _$createElement("text"),
226
+ _el$11 = _$createTextNode(`v`);
227
+ _$insertNode(_el$10, _el$11);
228
+ _$insert(_el$10, () => status()?.version ?? packageVersion, null);
229
+ _$effect(_$p => _$setProp(_el$10, "fg", t().textMuted, _$p));
230
+ return _el$10;
231
+ })();
232
+ })(), null);
233
+ _$insert(_el$4, (() => {
234
+ var _c$2 = _$memo(() => !!error());
235
+ return () => _c$2() ? (() => {
236
+ var _el$12 = _$createElement("box"),
237
+ _el$13 = _$createElement("text");
238
+ _$insertNode(_el$12, _el$13);
239
+ _$setProp(_el$12, "width", "100%");
240
+ _$setProp(_el$12, "marginBottom", 1);
241
+ _$insert(_el$13, error);
242
+ _$effect(_$p => _$setProp(_el$13, "fg", t().warning, _$p));
243
+ return _el$12;
244
+ })() : null;
245
+ })(), _el$9);
246
+ _$insert(_el$4, (() => {
247
+ var _c$3 = _$memo(() => status()?.cache_role === "not_initialized");
248
+ return () => _c$3() ? (() => {
249
+ var _el$14 = _$createElement("box"),
250
+ _el$15 = _$createElement("text");
251
+ _$insertNode(_el$14, _el$15);
252
+ _$setProp(_el$14, "width", "100%");
253
+ _$setProp(_el$14, "marginTop", 1);
254
+ _$setProp(_el$14, "justifyContent", "center");
255
+ _$insert(_el$15, () => status().message || "AFT bridge is now spawned lazily, information here will be populated after first tool call.");
256
+ _$effect(_$p => _$setProp(_el$15, "fg", t().textMuted, _$p));
257
+ return _el$14;
258
+ })() : null;
259
+ })(), _el$9);
260
+ _$insert(_el$4, (() => {
261
+ var _c$4 = _$memo(() => !!(status() && status().cache_role !== "not_initialized"));
262
+ return () => _c$4() ? (() => {
263
+ var _el$16 = _$createElement("box");
264
+ _$setProp(_el$16, "flexDirection", "column");
265
+ _$setProp(_el$16, "width", "100%");
266
+ _$setProp(_el$16, "marginBottom", 1);
267
+ _$insert(_el$16, _$createComponent(R, {
268
+ get theme() {
269
+ return t();
270
+ },
271
+ label: "Project root",
272
+ get value() {
273
+ return status().project_root ?? "(not configured)";
274
+ }
275
+ }), null);
276
+ _$insert(_el$16, _$createComponent(R, {
277
+ get theme() {
278
+ return t();
279
+ },
280
+ label: "Canonical root",
281
+ get value() {
282
+ return status().canonical_root ?? "(not configured)";
283
+ }
284
+ }), null);
285
+ _$insert(_el$16, _$createComponent(R, {
286
+ get theme() {
287
+ return t();
288
+ },
289
+ label: "Cache role",
290
+ get value() {
291
+ return status().cache_role;
292
+ },
293
+ get tone() {
294
+ return cacheRoleTone(status().cache_role);
295
+ }
296
+ }), null);
297
+ return _el$16;
298
+ })() : null;
299
+ })(), _el$9);
300
+ _$insert(_el$4, (() => {
301
+ var _c$5 = _$memo(() => !!(status() && status().cache_role !== "not_initialized"));
302
+ return () => _c$5() ? (() => {
303
+ var _el$17 = _$createElement("box"),
304
+ _el$18 = _$createElement("box"),
305
+ _el$19 = _$createElement("text"),
306
+ _el$20 = _$createElement("b"),
307
+ _el$22 = _$createElement("box"),
308
+ _el$23 = _$createElement("text"),
309
+ _el$24 = _$createElement("b"),
310
+ _el$26 = _$createElement("box"),
311
+ _el$27 = _$createElement("text"),
312
+ _el$28 = _$createElement("b"),
313
+ _el$30 = _$createElement("box"),
314
+ _el$31 = _$createElement("text"),
315
+ _el$32 = _$createElement("b"),
316
+ _el$34 = _$createElement("box"),
317
+ _el$35 = _$createElement("text"),
318
+ _el$36 = _$createElement("b");
319
+ _$insertNode(_el$17, _el$18);
320
+ _$insertNode(_el$17, _el$30);
321
+ _$setProp(_el$17, "flexDirection", "row");
322
+ _$setProp(_el$17, "width", "100%");
323
+ _$setProp(_el$17, "gap", 4);
324
+ _$insertNode(_el$18, _el$19);
325
+ _$insertNode(_el$18, _el$22);
326
+ _$insertNode(_el$18, _el$26);
327
+ _$setProp(_el$18, "flexDirection", "column");
328
+ _$setProp(_el$18, "flexGrow", 1);
329
+ _$setProp(_el$18, "flexBasis", 0);
330
+ _$insertNode(_el$19, _el$20);
331
+ _$insertNode(_el$20, _$createTextNode(`Search index`));
332
+ _$insert(_el$18, _$createComponent(R, {
333
+ get theme() {
334
+ return t();
335
+ },
336
+ label: "Status",
337
+ get value() {
338
+ return status().search_index.status;
339
+ },
340
+ get tone() {
341
+ return statusTone(status().search_index.status);
342
+ }
343
+ }), _el$22);
344
+ _$insert(_el$18, _$createComponent(R, {
345
+ get theme() {
346
+ return t();
347
+ },
348
+ label: "Files",
349
+ get value() {
350
+ return formatCountShort(status().search_index.files);
351
+ }
352
+ }), _el$22);
353
+ _$insert(_el$18, _$createComponent(R, {
354
+ get theme() {
355
+ return t();
356
+ },
357
+ label: "Trigrams",
358
+ get value() {
359
+ return formatCountShort(status().search_index.trigrams);
360
+ }
361
+ }), _el$22);
362
+ _$insert(_el$18, _$createComponent(R, {
363
+ get theme() {
364
+ return t();
365
+ },
366
+ label: "Disk",
367
+ get value() {
368
+ return formatBytes(status().disk.trigram_disk_bytes);
369
+ },
370
+ tone: "muted"
371
+ }), _el$22);
372
+ _$insertNode(_el$22, _el$23);
373
+ _$setProp(_el$22, "marginTop", 1);
374
+ _$insertNode(_el$23, _el$24);
375
+ _$insertNode(_el$24, _$createTextNode(`Runtime`));
376
+ _$insert(_el$18, _$createComponent(R, {
377
+ get theme() {
378
+ return t();
379
+ },
380
+ label: "LSP servers",
381
+ get value() {
382
+ return String(status().lsp_servers);
383
+ }
384
+ }), _el$26);
385
+ _$insert(_el$18, _$createComponent(R, {
386
+ get theme() {
387
+ return t();
388
+ },
389
+ label: "Symbol cache (local)",
390
+ get value() {
391
+ return formatCountShort(status().symbol_cache.local_entries);
392
+ }
393
+ }), _el$26);
394
+ _$insert(_el$18, _$createComponent(R, {
395
+ get theme() {
396
+ return t();
397
+ },
398
+ label: "Symbol cache (warm)",
399
+ get value() {
400
+ return formatCountShort(status().symbol_cache.warm_entries);
401
+ },
402
+ tone: "muted"
403
+ }), _el$26);
404
+ _$insertNode(_el$26, _el$27);
405
+ _$setProp(_el$26, "marginTop", 1);
406
+ _$insertNode(_el$27, _el$28);
407
+ _$insertNode(_el$28, _$createTextNode(`Features`));
408
+ _$insert(_el$18, _$createComponent(R, {
409
+ get theme() {
410
+ return t();
411
+ },
412
+ label: "format_on_edit",
413
+ get value() {
414
+ return status().features.format_on_edit ? "on" : "off";
415
+ },
416
+ get tone() {
417
+ return status().features.format_on_edit ? "ok" : "muted";
418
+ }
419
+ }), null);
420
+ _$insert(_el$18, _$createComponent(R, {
421
+ get theme() {
422
+ return t();
423
+ },
424
+ label: "search_index",
425
+ get value() {
426
+ return status().features.search_index ? "on" : "off";
427
+ },
428
+ get tone() {
429
+ return status().features.search_index ? "ok" : "muted";
430
+ }
431
+ }), null);
432
+ _$insert(_el$18, _$createComponent(R, {
433
+ get theme() {
434
+ return t();
435
+ },
436
+ label: "semantic_search",
437
+ get value() {
438
+ return status().features.semantic_search ? "on" : "off";
439
+ },
440
+ get tone() {
441
+ return status().features.semantic_search ? "ok" : "muted";
442
+ }
443
+ }), null);
444
+ _$insertNode(_el$30, _el$31);
445
+ _$insertNode(_el$30, _el$34);
446
+ _$setProp(_el$30, "flexDirection", "column");
447
+ _$setProp(_el$30, "flexGrow", 1);
448
+ _$setProp(_el$30, "flexBasis", 0);
449
+ _$insertNode(_el$31, _el$32);
450
+ _$insertNode(_el$32, _$createTextNode(`Semantic index`));
451
+ _$insert(_el$30, _$createComponent(R, {
452
+ get theme() {
453
+ return t();
454
+ },
455
+ label: "Status",
456
+ get value() {
457
+ return formatSemanticIndexStatus(status().semantic_index.status, status().semantic_index.stage);
458
+ },
459
+ get tone() {
460
+ return statusTone(status().semantic_index.status);
461
+ }
462
+ }), _el$34);
463
+ _$insert(_el$30, (() => {
464
+ var _c$0 = _$memo(() => !!formatSemanticRefreshing(status().semantic_index.refreshing_count));
465
+ return () => _c$0() ? (() => {
466
+ var _el$38 = _$createElement("box"),
467
+ _el$39 = _$createElement("text");
468
+ _$insertNode(_el$38, _el$39);
469
+ _$setProp(_el$38, "width", "100%");
470
+ _$insert(_el$39, () => formatSemanticRefreshing(status().semantic_index.refreshing_count));
471
+ _$effect(_$p => _$setProp(_el$39, "fg", t().textMuted, _$p));
472
+ return _el$38;
473
+ })() : null;
474
+ })(), _el$34);
475
+ _$insert(_el$30, _$createComponent(R, {
476
+ get theme() {
477
+ return t();
478
+ },
479
+ label: "Entries",
480
+ get value() {
481
+ return formatCountShort(status().semantic_index.entries);
482
+ }
483
+ }), _el$34);
484
+ _$insert(_el$30, (() => {
485
+ var _c$1 = _$memo(() => !!status().semantic_index.backend);
486
+ return () => _c$1() ? _$createComponent(R, {
487
+ get theme() {
488
+ return t();
489
+ },
490
+ label: "Backend",
491
+ get value() {
492
+ return status().semantic_index.backend;
493
+ },
494
+ tone: "muted"
495
+ }) : null;
496
+ })(), _el$34);
497
+ _$insert(_el$30, (() => {
498
+ var _c$10 = _$memo(() => !!status().semantic_index.model);
499
+ return () => _c$10() ? _$createComponent(R, {
500
+ get theme() {
501
+ return t();
502
+ },
503
+ label: "Model",
504
+ get value() {
505
+ return status().semantic_index.model;
506
+ },
507
+ tone: "muted"
508
+ }) : null;
509
+ })(), _el$34);
510
+ _$insert(_el$30, (() => {
511
+ var _c$11 = _$memo(() => status().semantic_index.dimension != null);
512
+ return () => _c$11() ? _$createComponent(R, {
513
+ get theme() {
514
+ return t();
515
+ },
516
+ label: "Dimension",
517
+ get value() {
518
+ return String(status().semantic_index.dimension);
519
+ },
520
+ tone: "muted"
521
+ }) : null;
522
+ })(), _el$34);
523
+ _$insert(_el$30, _$createComponent(R, {
524
+ get theme() {
525
+ return t();
526
+ },
527
+ label: "Disk",
528
+ get value() {
529
+ return formatBytes(status().disk.semantic_disk_bytes);
530
+ },
531
+ tone: "muted"
532
+ }), _el$34);
533
+ _$insertNode(_el$34, _el$35);
534
+ _$setProp(_el$34, "marginTop", 1);
535
+ _$insertNode(_el$35, _el$36);
536
+ _$insertNode(_el$36, _$createTextNode(`Current session`));
537
+ _$insert(_el$30, _$createComponent(R, {
538
+ get theme() {
539
+ return t();
540
+ },
541
+ label: "Tracked files",
542
+ get value() {
543
+ return String(status().session.tracked_files);
544
+ }
545
+ }), null);
546
+ _$insert(_el$30, _$createComponent(R, {
547
+ get theme() {
548
+ return t();
549
+ },
550
+ label: "Checkpoints",
551
+ get value() {
552
+ return String(status().session.checkpoints);
553
+ }
554
+ }), null);
555
+ _$insert(_el$30, _$createComponent(R, {
556
+ get theme() {
557
+ return t();
558
+ },
559
+ label: "All-session checkpoints",
560
+ get value() {
561
+ return String(status().checkpoints_total);
562
+ },
563
+ tone: "muted"
564
+ }), null);
565
+ _$effect(_p$ => {
566
+ var _v$5 = t().text,
567
+ _v$6 = t().text,
568
+ _v$7 = t().text,
569
+ _v$8 = t().text,
570
+ _v$9 = t().text;
571
+ _v$5 !== _p$.e && (_p$.e = _$setProp(_el$19, "fg", _v$5, _p$.e));
572
+ _v$6 !== _p$.t && (_p$.t = _$setProp(_el$23, "fg", _v$6, _p$.t));
573
+ _v$7 !== _p$.a && (_p$.a = _$setProp(_el$27, "fg", _v$7, _p$.a));
574
+ _v$8 !== _p$.o && (_p$.o = _$setProp(_el$31, "fg", _v$8, _p$.o));
575
+ _v$9 !== _p$.i && (_p$.i = _$setProp(_el$35, "fg", _v$9, _p$.i));
576
+ return _p$;
577
+ }, {
578
+ e: undefined,
579
+ t: undefined,
580
+ a: undefined,
581
+ o: undefined,
582
+ i: undefined
583
+ });
584
+ return _el$17;
585
+ })() : null;
586
+ })(), _el$9);
587
+ _$insert(_el$4, (() => {
588
+ var _c$6 = _$memo(() => !!status()?.semantic_index.stage);
589
+ return () => _c$6() ? (() => {
590
+ var _el$40 = _$createElement("box"),
591
+ _el$41 = _$createElement("text"),
592
+ _el$42 = _$createElement("b");
593
+ _$insertNode(_el$40, _el$41);
594
+ _$setProp(_el$40, "flexDirection", "column");
595
+ _$setProp(_el$40, "width", "100%");
596
+ _$setProp(_el$40, "marginTop", 1);
597
+ _$insertNode(_el$41, _el$42);
598
+ _$insertNode(_el$42, _$createTextNode(`Semantic build progress`));
599
+ _$insert(_el$40, _$createComponent(R, {
600
+ get theme() {
601
+ return t();
602
+ },
603
+ label: "Stage",
604
+ get value() {
605
+ return status().semantic_index.stage;
606
+ }
607
+ }), null);
608
+ _$insert(_el$40, (() => {
609
+ var _c$12 = _$memo(() => status().semantic_index.files != null);
610
+ return () => _c$12() ? _$createComponent(R, {
611
+ get theme() {
612
+ return t();
613
+ },
614
+ label: "Files seen",
615
+ get value() {
616
+ return formatCountShort(status().semantic_index.files);
617
+ }
618
+ }) : null;
619
+ })(), null);
620
+ _$insert(_el$40, (() => {
621
+ var _c$13 = _$memo(() => !!(status().semantic_index.entries_done != null || status().semantic_index.entries_total != null));
622
+ return () => _c$13() ? _$createComponent(R, {
623
+ get theme() {
624
+ return t();
625
+ },
626
+ label: "Progress",
627
+ get value() {
628
+ return `${formatCountShort(status().semantic_index.entries_done ?? null)} / ${formatCountShort(status().semantic_index.entries_total ?? null)}`;
629
+ }
630
+ }) : null;
631
+ })(), null);
632
+ _$effect(_$p => _$setProp(_el$41, "fg", t().text, _$p));
633
+ return _el$40;
634
+ })() : null;
635
+ })(), _el$9);
636
+ _$insert(_el$4, (() => {
637
+ var _c$7 = _$memo(() => !!status()?.semantic_index.error);
638
+ return () => _c$7() ? (() => {
639
+ var _el$44 = _$createElement("box"),
640
+ _el$45 = _$createElement("text"),
641
+ _el$46 = _$createTextNode(`⚠ `);
642
+ _$insertNode(_el$44, _el$45);
643
+ _$setProp(_el$44, "marginTop", 1);
644
+ _$setProp(_el$44, "width", "100%");
645
+ _$insertNode(_el$45, _el$46);
646
+ _$insert(_el$45, () => status().semantic_index.error, null);
647
+ _$effect(_$p => _$setProp(_el$45, "fg", t().error, _$p));
648
+ return _el$44;
649
+ })() : null;
650
+ })(), _el$9);
651
+ _$insert(_el$4, (() => {
652
+ var _c$8 = _$memo(() => compressionAggregateRows().length > 0);
653
+ return () => _c$8() ? (() => {
654
+ var _el$47 = _$createElement("box"),
655
+ _el$48 = _$createElement("text"),
656
+ _el$49 = _$createElement("b");
657
+ _$insertNode(_el$47, _el$48);
658
+ _$setProp(_el$47, "flexDirection", "column");
659
+ _$setProp(_el$47, "width", "100%");
660
+ _$setProp(_el$47, "marginTop", 1);
661
+ _$insertNode(_el$48, _el$49);
662
+ _$insertNode(_el$49, _$createTextNode(`Compression`));
663
+ _$insert(_el$47, () => compressionAggregateRows().map(row => row.kind === "scope" ? (() => {
664
+ var _el$51 = _$createElement("box"),
665
+ _el$52 = _$createElement("text");
666
+ _$insertNode(_el$51, _el$52);
667
+ _$setProp(_el$51, "width", "100%");
668
+ _$insert(_el$52, () => row.label);
669
+ _$effect(_$p => _$setProp(_el$52, "fg", t().text, _$p));
670
+ return _el$51;
671
+ })() : _$createComponent(R, {
672
+ get theme() {
673
+ return t();
674
+ },
675
+ get label() {
676
+ return row.label;
677
+ },
678
+ get value() {
679
+ return row.value;
680
+ },
681
+ tone: "muted"
682
+ })), null);
683
+ _$effect(_$p => _$setProp(_el$48, "fg", t().text, _$p));
684
+ return _el$47;
685
+ })() : null;
686
+ })(), _el$9);
687
+ _$insert(_el$4, (() => {
688
+ var _c$9 = _$memo(() => !!status()?.status_bar);
689
+ return () => _c$9() ? (() => {
690
+ var _el$53 = _$createElement("box"),
691
+ _el$54 = _$createElement("text"),
692
+ _el$55 = _$createElement("b");
693
+ _$insertNode(_el$53, _el$54);
694
+ _$setProp(_el$53, "flexDirection", "column");
695
+ _$setProp(_el$53, "width", "100%");
696
+ _$setProp(_el$53, "marginTop", 1);
697
+ _$insertNode(_el$54, _el$55);
698
+ _$insert(_el$55, () => status().status_bar.tier2_stale ? "Code Health ~" : "Code Health");
699
+ _$insert(_el$53, _$createComponent(R, {
700
+ get theme() {
701
+ return t();
702
+ },
703
+ label: "Errors",
704
+ get value() {
705
+ return formatCountShort(status().status_bar.errors);
706
+ },
707
+ get tone() {
708
+ return status().status_bar.errors > 0 ? "err" : "muted";
709
+ }
710
+ }), null);
711
+ _$insert(_el$53, _$createComponent(R, {
712
+ get theme() {
713
+ return t();
714
+ },
715
+ label: "Warnings",
716
+ get value() {
717
+ return formatCountShort(status().status_bar.warnings);
718
+ },
719
+ get tone() {
720
+ return status().status_bar.warnings > 0 ? "warn" : "muted";
721
+ }
722
+ }), null);
723
+ _$insert(_el$53, _$createComponent(R, {
724
+ get theme() {
725
+ return t();
726
+ },
727
+ label: "Dead Code",
728
+ get value() {
729
+ return formatCountShort(status().status_bar.dead_code);
730
+ },
731
+ tone: "muted"
732
+ }), null);
733
+ _$insert(_el$53, _$createComponent(R, {
734
+ get theme() {
735
+ return t();
736
+ },
737
+ label: "Unused Exports",
738
+ get value() {
739
+ return formatCountShort(status().status_bar.unused_exports);
740
+ },
741
+ tone: "muted"
742
+ }), null);
743
+ _$insert(_el$53, _$createComponent(R, {
744
+ get theme() {
745
+ return t();
746
+ },
747
+ label: "Duplicates",
748
+ get value() {
749
+ return formatCountShort(status().status_bar.duplicates);
750
+ },
751
+ tone: "muted"
752
+ }), null);
753
+ _$insert(_el$53, _$createComponent(R, {
754
+ get theme() {
755
+ return t();
756
+ },
757
+ label: "TODOs",
758
+ get value() {
759
+ return formatCountShort(status().status_bar.todos);
760
+ },
761
+ tone: "muted"
762
+ }), null);
763
+ _$effect(_$p => _$setProp(_el$54, "fg", t().text, _$p));
764
+ return _el$53;
765
+ })() : null;
766
+ })(), _el$9);
767
+ _$insertNode(_el$9, _el$0);
768
+ _$setProp(_el$9, "marginTop", 1);
769
+ _$setProp(_el$9, "justifyContent", "flex-end");
770
+ _$setProp(_el$9, "width", "100%");
771
+ _$insertNode(_el$0, _$createTextNode(`Enter or Esc to close`));
772
+ _$effect(_p$ => {
773
+ var _v$3 = t().accent,
774
+ _v$4 = t().textMuted;
775
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$6, "fg", _v$3, _p$.e));
776
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$0, "fg", _v$4, _p$.t));
777
+ return _p$;
778
+ }, {
779
+ e: undefined,
780
+ t: undefined
781
+ });
782
+ return _el$4;
783
+ })();
784
+ };
785
+ async function showStatusDialog(api) {
786
+ const sessionID = getSessionId(api);
787
+ if (!sessionID) {
788
+ api.ui.toast({
789
+ message: "No active session",
790
+ variant: "warning",
791
+ duration: 5000
792
+ });
793
+ return;
794
+ }
795
+ const directory = api.state.path.directory ?? "";
796
+ if (!directory) {
797
+ api.ui.toast({
798
+ message: "No project directory",
799
+ variant: "warning",
800
+ duration: 5000
801
+ });
802
+ return;
803
+ }
804
+ const client = getRpcClient(directory);
805
+
806
+ // Prime the dialog with one initial fetch so we don't show a blank
807
+ // skeleton — the component then listens for pushed invalidations.
808
+ let initial = null;
809
+ let initialError = null;
810
+ try {
811
+ const response = await client.call("status", {
812
+ sessionID
813
+ }, {
814
+ accept: statusAcceptGate(directory)
815
+ });
816
+ if (response.success !== false) {
817
+ initial = coerceAftStatus(response);
818
+ } else {
819
+ initialError = "AFT bridge returned an error response.";
820
+ }
821
+ } catch {
822
+ initialError = "AFT is starting up. Status will refresh automatically...";
823
+ }
824
+
825
+ // The dialog host already wraps every replace()'d element in its own centered
826
+ // frame and binds Esc/Ctrl-C to close it. Rendering our own dialog frame inside
827
+ // that host frame would misplace the content and focus; pass the bare component
828
+ // so the host frames it once.
829
+ // Do not pass an onClose that calls dialog.clear(). The dialog system calls
830
+ // every entry's onClose on Esc, and clear() would re-trigger them, causing
831
+ // infinite recursion. The host itself pops the dialog; StatusDialog cleans up
832
+ // its own subscriptions in onCleanup. `replace` resets size to "medium", so
833
+ // request "large" after the call.
834
+ api.ui.dialog.replace(() => _$createComponent(StatusDialog, {
835
+ api: api,
836
+ client: client,
837
+ directory: directory,
838
+ sessionID: sessionID,
839
+ initial: initial,
840
+ initialError: initialError
841
+ }));
842
+ api.ui.dialog.setSize("large");
843
+ }
844
+
845
+ /**
846
+ * Register the AFT status palette command, preferring the v1.14.42+ keymap API
847
+ * and falling back to the legacy `api.command.register` for older hosts.
848
+ *
849
+ * No slash entry is registered here. The server config hook owns the single
850
+ * `/aft-status` slash registration, while this palette command keeps Ctrl+P
851
+ * discovery in the TUI.
852
+ */
853
+ function registerStatusCommand(api) {
854
+ const apiAny = api;
855
+ if (typeof apiAny.keymap?.registerLayer === "function") {
856
+ apiAny.keymap.registerLayer({
857
+ commands: [{
858
+ namespace: "palette",
859
+ name: "aft.status",
860
+ title: "AFT: Status",
861
+ category: "AFT",
862
+ run() {
863
+ void showStatusDialog(api);
864
+ }
865
+ }],
866
+ bindings: []
867
+ });
868
+ return;
869
+ }
870
+ if (typeof apiAny.command?.register === "function") {
871
+ apiAny.command.register(() => [{
872
+ title: "AFT: Status",
873
+ value: "aft.status",
874
+ category: "AFT",
875
+ onSelect() {
876
+ void showStatusDialog(api);
877
+ }
878
+ }]);
879
+ return;
880
+ }
881
+
882
+ // Neither API surface is present. The TUI host can still load; users can
883
+ // still see AFT status through the sidebar and server-owned slash command.
884
+ }
885
+ async function showStartupNotifications(api) {
886
+ const directory = api.state.path.directory ?? "";
887
+ if (!directory) return;
888
+ const client = getRpcClient(directory);
889
+
890
+ // Check for feature announcements
891
+ try {
892
+ const announcement = await client.call("get-announcement", {});
893
+ if (announcement.show && announcement.version && announcement.features?.length) {
894
+ const featureText = announcement.features.map(f => ` • ${f}`).join("\n");
895
+ // Blank-line separator distinguishes the persistent footer (Discord
896
+ // invite, etc.) from the version-specific bullets.
897
+ const hasFooter = typeof announcement.footer === "string" && announcement.footer.trim().length > 0;
898
+ const message = hasFooter ? `What's new:\n\n${featureText}\n\n${announcement.footer}` : `What's new:\n\n${featureText}`;
899
+ api.ui.dialog.replace(() => _$createComponent(api.ui.DialogAlert, {
900
+ get title() {
901
+ return `AFT v${announcement.version}`;
902
+ },
903
+ message: message,
904
+ onConfirm: () => {
905
+ // Mark as announced so it doesn't show again
906
+ void client.call("mark-announced", {});
907
+ }
908
+ }), () => {
909
+ void client.call("mark-announced", {});
910
+ });
911
+ return; // Show one dialog at a time
912
+ }
913
+ } catch {
914
+ // RPC server not ready yet — skip announcements
915
+ }
916
+
917
+ // Check for warnings
918
+ try {
919
+ const result = await client.call("get-warnings", {});
920
+ if (result.warnings?.length) {
921
+ const warningText = result.warnings.join("\n\n");
922
+ api.ui.dialog.replace(() => _$createComponent(api.ui.DialogAlert, {
923
+ title: "AFT Warning",
924
+ message: warningText,
925
+ onConfirm: () => {}
926
+ }), () => {});
927
+ }
928
+ } catch {
929
+ // RPC server not ready — skip warnings
930
+ }
931
+ }
932
+ const tui = async api => {
933
+ // Sidebar slot: live status of search index, semantic index, and disk
934
+ // usage. See ./sidebar.tsx for the panel itself. Registered before the
935
+ // command palette entry so the sidebar is available immediately when the
936
+ // user opens their first session.
937
+ try {
938
+ api.slots.register(await createAftSidebarSlot(api, packageVersion));
939
+ } catch {
940
+ // Older OpenCode TUI hosts may not implement api.slots; fall through and
941
+ // keep the palette command / server-owned slash command working.
942
+ }
943
+
944
+ // OpenCode 1.14.42 removed `api.command.register` entirely
945
+ // (anomalyco/opencode PR #26053). Later patches reinstated it as a
946
+ // deprecated shim that translates to `api.keymap.registerLayer`. To work
947
+ // across the whole 1.14.x line — including the brief 1.14.42 / 1.14.43
948
+ // window where neither the legacy API nor a shim was present — we prefer
949
+ // `api.keymap.registerLayer` and fall back to `api.command.register` only
950
+ // when the keymap surface is missing (older hosts that predate keymap).
951
+ // See https://github.com/cortexkit/aft/issues/33.
952
+ registerStatusCommand(api);
953
+
954
+ // Receive server → TUI dialog requests and status invalidations over one
955
+ // persistent WebSocket. This avoids repeated loopback HTTP connection setup,
956
+ // which caused idle CPU usage.
957
+ const handleNotification = async message => {
958
+ const requestedSessionId = getSessionId(api);
959
+ if (!requestedSessionId) return false;
960
+ if (message.sessionId && message.sessionId !== requestedSessionId) return false;
961
+ if (message.type !== "action") return false;
962
+ if (message.payload?.action !== "show-status-dialog") return false;
963
+ await showStatusDialog(api);
964
+ return true;
965
+ };
966
+ startAftTuiSocket({
967
+ getDirectory: () => api.state.path.directory ?? "",
968
+ getSessionId: () => getSessionId(api),
969
+ onNotification: handleNotification
970
+ });
971
+ const socketScopeUnsubs = [api.event?.on?.("message.updated", () => refreshAftTuiSocketScope()), api.event?.on?.("session.updated", () => refreshAftTuiSocketScope())].filter(Boolean);
972
+ api.lifecycle?.onDispose?.(() => {
973
+ stopAftTuiSocket();
974
+ for (const unsub of socketScopeUnsubs) {
975
+ try {
976
+ unsub();
977
+ } catch {
978
+ // Ignore unsubscribe errors during cleanup; socket shutdown is handled separately.
979
+ }
980
+ }
981
+ for (const client of rpcClients.values()) client.reset();
982
+ rpcClients.clear();
983
+ });
984
+
985
+ // Show startup notifications — RPC server is already running by the time TUI loads
986
+ void showStartupNotifications(api);
987
+ };
988
+ const id = "aft-opencode";
989
+ export default {
990
+ id,
991
+ tui
992
+ };