@cortexkit/aft-opencode 0.17.3 → 0.18.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.
- package/dist/bg-notifications.d.ts +41 -0
- package/dist/bg-notifications.d.ts.map +1 -0
- package/dist/bridge.d.ts +9 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/config.d.ts +27 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1240 -493
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/patch-parser.d.ts.map +1 -1
- package/dist/shared/status.d.ts +2 -2
- package/dist/shared/status.d.ts.map +1 -1
- package/dist/tools/_shared.d.ts +2 -2
- package/dist/tools/_shared.d.ts.map +1 -1
- package/dist/tools/ast.d.ts.map +1 -1
- package/dist/tools/bash.d.ts +4 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tui.js +377 -11
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/shared/status.ts +10 -8
- package/src/tui/index.tsx +13 -0
- package/src/tui/sidebar.tsx +331 -0
package/src/tui/index.tsx
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// @ts-nocheck
|
|
3
3
|
|
|
4
4
|
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
5
|
+
import packageJson from "../../package.json";
|
|
5
6
|
import { AftRpcClient } from "../shared/rpc-client";
|
|
6
7
|
import { coerceAftStatus, formatStatusDialogMessage } from "../shared/status";
|
|
8
|
+
import { createAftSidebarSlot } from "./sidebar";
|
|
7
9
|
|
|
8
10
|
// The TUI talks to the server plugin via AftRpcClient. The client reads the
|
|
9
11
|
// JSON port file written by AftRpcServer ({ port, token }) and includes that
|
|
@@ -190,6 +192,17 @@ async function showStartupNotifications(api: TuiPluginApi): Promise<void> {
|
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
const tui: TuiPlugin = async (api) => {
|
|
195
|
+
// Sidebar slot: live status of search index, semantic index, and disk
|
|
196
|
+
// usage. See ./sidebar.tsx for the panel itself. Registered before the
|
|
197
|
+
// command palette entry so the sidebar is available immediately when the
|
|
198
|
+
// user opens their first session.
|
|
199
|
+
try {
|
|
200
|
+
api.slots.register(createAftSidebarSlot(api, packageJson.version));
|
|
201
|
+
} catch {
|
|
202
|
+
// Older OpenCode TUI hosts may not implement api.slots; fall through
|
|
203
|
+
// and keep the slash command working.
|
|
204
|
+
}
|
|
205
|
+
|
|
193
206
|
api.command.register(() => [
|
|
194
207
|
{
|
|
195
208
|
title: "AFT: Status",
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
|
|
4
|
+
// AFT sidebar slot — mirrors opencode-magic-context's sidebar pattern.
|
|
5
|
+
// Header with "AFT" badge + version, then live status of search and semantic
|
|
6
|
+
// indexes plus their on-disk size. Refreshes on session change and on
|
|
7
|
+
// session.updated/message.updated events with a small debounce, same as
|
|
8
|
+
// magic-context, so the panel stays current without polling.
|
|
9
|
+
|
|
10
|
+
import type { TuiPluginApi, TuiSlotPlugin, TuiThemeCurrent } from "@opencode-ai/plugin/tui";
|
|
11
|
+
import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js";
|
|
12
|
+
|
|
13
|
+
import { AftRpcClient } from "../shared/rpc-client";
|
|
14
|
+
import { type AftStatusSnapshot, coerceAftStatus } from "../shared/status";
|
|
15
|
+
|
|
16
|
+
const SINGLE_BORDER = { type: "single" } as any;
|
|
17
|
+
const REFRESH_DEBOUNCE_MS = 200;
|
|
18
|
+
// The sidebar polls the bridge as a backstop because not every state change
|
|
19
|
+
// (e.g. semantic index transitioning from "loading" → "ready" mid-session)
|
|
20
|
+
// emits a session/message event. 1.5s matches the /aft-status dialog cadence.
|
|
21
|
+
const POLL_INTERVAL_MS = 1500;
|
|
22
|
+
|
|
23
|
+
function formatBytes(n: number): string {
|
|
24
|
+
if (!Number.isFinite(n) || n <= 0) return "—";
|
|
25
|
+
if (n >= 1_073_741_824) return `${(n / 1_073_741_824).toFixed(1)} GB`;
|
|
26
|
+
if (n >= 1_048_576) return `${(n / 1_048_576).toFixed(1)} MB`;
|
|
27
|
+
if (n >= 1_024) return `${Math.round(n / 1_024)} KB`;
|
|
28
|
+
return `${n} B`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatCount(n: number | null | undefined): string {
|
|
32
|
+
if (n == null || !Number.isFinite(n)) return "—";
|
|
33
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
34
|
+
if (n >= 1_000) return `${Math.round(n / 1_000)}K`;
|
|
35
|
+
return String(n);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Map index status → (label, theme color name). The label is what we want
|
|
39
|
+
// the user to see; the color encodes severity so the eye lands on warnings.
|
|
40
|
+
function statusDisplay(status: string): { label: string; tone: "ok" | "warn" | "err" | "muted" } {
|
|
41
|
+
switch (status) {
|
|
42
|
+
case "ready":
|
|
43
|
+
return { label: "ready", tone: "ok" };
|
|
44
|
+
case "loading":
|
|
45
|
+
case "building":
|
|
46
|
+
return { label: status, tone: "warn" };
|
|
47
|
+
case "failed":
|
|
48
|
+
case "error":
|
|
49
|
+
return { label: status, tone: "err" };
|
|
50
|
+
case "disabled":
|
|
51
|
+
return { label: "disabled", tone: "muted" };
|
|
52
|
+
default:
|
|
53
|
+
return { label: status || "unknown", tone: "muted" };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const StatRow = (props: {
|
|
58
|
+
theme: TuiThemeCurrent;
|
|
59
|
+
label: string;
|
|
60
|
+
value: string;
|
|
61
|
+
tone?: "ok" | "warn" | "err" | "muted" | "accent";
|
|
62
|
+
}) => {
|
|
63
|
+
const fg = createMemo(() => {
|
|
64
|
+
switch (props.tone) {
|
|
65
|
+
case "ok":
|
|
66
|
+
return props.theme.success ?? props.theme.accent;
|
|
67
|
+
case "warn":
|
|
68
|
+
return props.theme.warning;
|
|
69
|
+
case "err":
|
|
70
|
+
return props.theme.error;
|
|
71
|
+
case "muted":
|
|
72
|
+
return props.theme.textMuted;
|
|
73
|
+
case "accent":
|
|
74
|
+
return props.theme.accent;
|
|
75
|
+
default:
|
|
76
|
+
return props.theme.text;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<box width="100%" flexDirection="row" justifyContent="space-between">
|
|
82
|
+
<text fg={props.theme.textMuted}>{props.label}</text>
|
|
83
|
+
<text fg={fg()}>
|
|
84
|
+
<b>{props.value}</b>
|
|
85
|
+
</text>
|
|
86
|
+
</box>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const SectionHeader = (props: { theme: TuiThemeCurrent; title: string; marginTop?: number }) => (
|
|
91
|
+
<box width="100%" marginTop={props.marginTop ?? 1}>
|
|
92
|
+
<text fg={props.theme.text}>
|
|
93
|
+
<b>{props.title}</b>
|
|
94
|
+
</text>
|
|
95
|
+
</box>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// One RPC client per project directory — same pattern as the /aft-status
|
|
99
|
+
// dialog handler in tui/index.tsx. Sharing the map avoids opening a second
|
|
100
|
+
// connection just for the sidebar.
|
|
101
|
+
const sidebarClients = new Map<string, AftRpcClient>();
|
|
102
|
+
function getClient(directory: string): AftRpcClient {
|
|
103
|
+
let client = sidebarClients.get(directory);
|
|
104
|
+
if (client) return client;
|
|
105
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
106
|
+
const dataHome = process.env.XDG_DATA_HOME || `${home}/.local/share`;
|
|
107
|
+
const storageDir = `${dataHome}/opencode/storage/plugin/aft`;
|
|
108
|
+
client = new AftRpcClient(storageDir, directory);
|
|
109
|
+
sidebarClients.set(directory, client);
|
|
110
|
+
return client;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const SidebarContent = (props: {
|
|
114
|
+
api: TuiPluginApi;
|
|
115
|
+
sessionID: () => string;
|
|
116
|
+
theme: TuiThemeCurrent;
|
|
117
|
+
pluginVersion: string;
|
|
118
|
+
}) => {
|
|
119
|
+
const [status, setStatus] = createSignal<AftStatusSnapshot | null>(null);
|
|
120
|
+
// Once a request is in flight, suppress any overlapping refresh so we
|
|
121
|
+
// don't open a thundering herd of RPCs on rapid event bursts.
|
|
122
|
+
let inflight = false;
|
|
123
|
+
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
124
|
+
let pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
125
|
+
|
|
126
|
+
const refresh = async () => {
|
|
127
|
+
const sid = props.sessionID();
|
|
128
|
+
if (!sid) return;
|
|
129
|
+
if (inflight) return;
|
|
130
|
+
const directory = props.api.state.path.directory ?? "";
|
|
131
|
+
if (!directory) return;
|
|
132
|
+
|
|
133
|
+
inflight = true;
|
|
134
|
+
try {
|
|
135
|
+
const client = getClient(directory);
|
|
136
|
+
const response = await client.call("status", { sessionID: sid });
|
|
137
|
+
if (response && (response as Record<string, unknown>).success !== false) {
|
|
138
|
+
const snapshot = coerceAftStatus(response as Record<string, unknown>);
|
|
139
|
+
setStatus(snapshot);
|
|
140
|
+
try {
|
|
141
|
+
props.api.renderer.requestRender();
|
|
142
|
+
} catch {
|
|
143
|
+
// renderer may not be available during teardown; safe to ignore
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// RPC server may not be ready yet, or the bridge may be respawning
|
|
148
|
+
// after a binary swap — leave the previous snapshot visible rather
|
|
149
|
+
// than blanking the sidebar.
|
|
150
|
+
} finally {
|
|
151
|
+
inflight = false;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const scheduleRefresh = () => {
|
|
156
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
157
|
+
debounceTimer = setTimeout(() => {
|
|
158
|
+
debounceTimer = undefined;
|
|
159
|
+
void refresh();
|
|
160
|
+
}, REFRESH_DEBOUNCE_MS);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
onCleanup(() => {
|
|
164
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
165
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Refresh on session id change + initial load
|
|
169
|
+
createEffect(
|
|
170
|
+
on(props.sessionID, () => {
|
|
171
|
+
void refresh();
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Wire live updates: session/message events are cheap signals that
|
|
176
|
+
// *something* AFT-relevant probably changed (formatted edit, lsp activity,
|
|
177
|
+
// index pre-warm completion). The status RPC is debounced so we don't
|
|
178
|
+
// recompute disk usage on every keystroke.
|
|
179
|
+
createEffect(
|
|
180
|
+
on(
|
|
181
|
+
props.sessionID,
|
|
182
|
+
(sessionID) => {
|
|
183
|
+
if (!sessionID) return;
|
|
184
|
+
const unsubs = [
|
|
185
|
+
props.api.event.on("message.updated", (event) => {
|
|
186
|
+
if (event.properties?.info?.sessionID !== sessionID) return;
|
|
187
|
+
scheduleRefresh();
|
|
188
|
+
}),
|
|
189
|
+
props.api.event.on("session.updated", (event) => {
|
|
190
|
+
if (event.properties?.info?.id !== sessionID) return;
|
|
191
|
+
scheduleRefresh();
|
|
192
|
+
}),
|
|
193
|
+
];
|
|
194
|
+
// Background poller for state that doesn't emit session events
|
|
195
|
+
// (semantic index `loading` → `ready`, disk size growth during
|
|
196
|
+
// a background indexer rebuild). Self-cancelling on cleanup.
|
|
197
|
+
if (!pollTimer) {
|
|
198
|
+
pollTimer = setInterval(() => {
|
|
199
|
+
scheduleRefresh();
|
|
200
|
+
}, POLL_INTERVAL_MS);
|
|
201
|
+
}
|
|
202
|
+
onCleanup(() => {
|
|
203
|
+
for (const unsub of unsubs) {
|
|
204
|
+
try {
|
|
205
|
+
unsub();
|
|
206
|
+
} catch {
|
|
207
|
+
// best effort
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (pollTimer) {
|
|
211
|
+
clearInterval(pollTimer);
|
|
212
|
+
pollTimer = undefined;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
{ defer: false },
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const s = createMemo(() => status());
|
|
221
|
+
|
|
222
|
+
// Pre-compute display values so the JSX stays readable. createMemo for
|
|
223
|
+
// each derived field would be overkill — these are cheap derivations.
|
|
224
|
+
const searchStatus = () => statusDisplay(s()?.search_index?.status ?? "disabled");
|
|
225
|
+
const semanticStatus = () => statusDisplay(s()?.semantic_index?.status ?? "disabled");
|
|
226
|
+
const trigramBytes = () => s()?.disk?.trigram_disk_bytes ?? 0;
|
|
227
|
+
const semanticBytes = () => s()?.disk?.semantic_disk_bytes ?? 0;
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<box
|
|
231
|
+
width="100%"
|
|
232
|
+
flexDirection="column"
|
|
233
|
+
border={SINGLE_BORDER}
|
|
234
|
+
borderColor={props.theme.borderActive}
|
|
235
|
+
paddingTop={1}
|
|
236
|
+
paddingBottom={1}
|
|
237
|
+
paddingLeft={1}
|
|
238
|
+
paddingRight={1}
|
|
239
|
+
>
|
|
240
|
+
{/* Header: AFT badge + binary version */}
|
|
241
|
+
<box flexDirection="row" justifyContent="space-between" alignItems="center">
|
|
242
|
+
<box paddingLeft={1} paddingRight={1} backgroundColor={props.theme.accent}>
|
|
243
|
+
<text fg={props.theme.background}>
|
|
244
|
+
<b>AFT</b>
|
|
245
|
+
</text>
|
|
246
|
+
</box>
|
|
247
|
+
<text fg={props.theme.textMuted}>v{s()?.version ?? props.pluginVersion}</text>
|
|
248
|
+
</box>
|
|
249
|
+
|
|
250
|
+
{/* Search index */}
|
|
251
|
+
<SectionHeader theme={props.theme} title="Search Index" />
|
|
252
|
+
<StatRow
|
|
253
|
+
theme={props.theme}
|
|
254
|
+
label="Status"
|
|
255
|
+
value={searchStatus().label}
|
|
256
|
+
tone={searchStatus().tone}
|
|
257
|
+
/>
|
|
258
|
+
{(s()?.search_index?.files ?? null) != null && (
|
|
259
|
+
<StatRow
|
|
260
|
+
theme={props.theme}
|
|
261
|
+
label="Files"
|
|
262
|
+
value={formatCount(s()!.search_index.files)}
|
|
263
|
+
tone="muted"
|
|
264
|
+
/>
|
|
265
|
+
)}
|
|
266
|
+
<StatRow theme={props.theme} label="Disk" value={formatBytes(trigramBytes())} tone="muted" />
|
|
267
|
+
|
|
268
|
+
{/* Semantic index */}
|
|
269
|
+
<SectionHeader theme={props.theme} title="Semantic Index" />
|
|
270
|
+
<StatRow
|
|
271
|
+
theme={props.theme}
|
|
272
|
+
label="Status"
|
|
273
|
+
value={semanticStatus().label}
|
|
274
|
+
tone={semanticStatus().tone}
|
|
275
|
+
/>
|
|
276
|
+
{/* When loading, magic-context-style progress hint helps users see
|
|
277
|
+
background work is making progress instead of stuck. */}
|
|
278
|
+
{s()?.semantic_index?.status === "loading" &&
|
|
279
|
+
s()?.semantic_index?.entries_total != null &&
|
|
280
|
+
s()!.semantic_index.entries_total! > 0 && (
|
|
281
|
+
<StatRow
|
|
282
|
+
theme={props.theme}
|
|
283
|
+
label="Progress"
|
|
284
|
+
value={`${formatCount(s()!.semantic_index.entries_done)} / ${formatCount(
|
|
285
|
+
s()!.semantic_index.entries_total,
|
|
286
|
+
)}`}
|
|
287
|
+
tone="warn"
|
|
288
|
+
/>
|
|
289
|
+
)}
|
|
290
|
+
{(s()?.semantic_index?.entries ?? null) != null && (
|
|
291
|
+
<StatRow
|
|
292
|
+
theme={props.theme}
|
|
293
|
+
label="Entries"
|
|
294
|
+
value={formatCount(s()!.semantic_index.entries)}
|
|
295
|
+
tone="muted"
|
|
296
|
+
/>
|
|
297
|
+
)}
|
|
298
|
+
<StatRow theme={props.theme} label="Disk" value={formatBytes(semanticBytes())} tone="muted" />
|
|
299
|
+
|
|
300
|
+
{/* Surface failures clearly so users know to act (install ONNX,
|
|
301
|
+
fix config, etc.) rather than silently leaving the panel "off". */}
|
|
302
|
+
{s()?.semantic_index?.status === "failed" && s()?.semantic_index?.error && (
|
|
303
|
+
<box marginTop={1} width="100%">
|
|
304
|
+
<text fg={props.theme.error}>⚠ {s()!.semantic_index.error}</text>
|
|
305
|
+
</box>
|
|
306
|
+
)}
|
|
307
|
+
</box>
|
|
308
|
+
);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export function createAftSidebarSlot(api: TuiPluginApi, pluginVersion: string): TuiSlotPlugin {
|
|
312
|
+
return {
|
|
313
|
+
// 150 matches magic-context's order — chosen so AFT renders below
|
|
314
|
+
// higher-priority panels but above default plugin slots. If both
|
|
315
|
+
// plugins are loaded together, magic-context will appear first.
|
|
316
|
+
order: 160,
|
|
317
|
+
slots: {
|
|
318
|
+
sidebar_content: (ctx, value) => {
|
|
319
|
+
const theme = createMemo(() => (ctx as any).theme.current);
|
|
320
|
+
return (
|
|
321
|
+
<SidebarContent
|
|
322
|
+
api={api}
|
|
323
|
+
sessionID={() => value.session_id}
|
|
324
|
+
theme={theme()}
|
|
325
|
+
pluginVersion={pluginVersion}
|
|
326
|
+
/>
|
|
327
|
+
);
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|