@cortexkit/aft-opencode 0.30.0 → 0.30.2
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 +2 -0
- package/dist/bg-notifications.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/cache.d.ts +5 -16
- package/dist/hooks/auto-update-checker/cache.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/index.d.ts +1 -1
- package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1005 -632
- package/dist/notifications.d.ts +8 -5
- package/dist/notifications.d.ts.map +1 -1
- package/dist/shared/last-assistant-model.d.ts +5 -6
- package/dist/shared/last-assistant-model.d.ts.map +1 -1
- package/dist/shared/live-server-client.d.ts +24 -22
- package/dist/shared/live-server-client.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +10 -3
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/storage-paths.d.ts +2 -0
- package/dist/shared/storage-paths.d.ts.map +1 -0
- package/dist/tools/_shared.d.ts +39 -0
- package/dist/tools/_shared.d.ts.map +1 -1
- package/dist/tools/ast.d.ts.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash_watch.d.ts +4 -0
- package/dist/tools/bash_watch.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/lsp.d.ts.map +1 -1
- package/dist/tools/navigation.d.ts.map +1 -1
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/semantic.d.ts.map +1 -1
- package/dist/tui.js +222 -123
- package/package.json +8 -8
- package/src/shared/last-assistant-model.ts +9 -27
- package/src/shared/live-server-client.ts +80 -48
- package/src/shared/rpc-client.ts +132 -28
- package/src/shared/storage-paths.ts +22 -0
- package/src/tui/index.tsx +39 -17
- package/src/tui/sidebar.tsx +94 -27
package/dist/tui.js
CHANGED
|
@@ -1,78 +1,10 @@
|
|
|
1
1
|
// src/tui/index.tsx
|
|
2
2
|
import { createMemo as createMemo2, createSignal as createSignal2, onCleanup as onCleanup2 } from "solid-js";
|
|
3
3
|
// package.json
|
|
4
|
-
var
|
|
5
|
-
name: "@cortexkit/aft-opencode",
|
|
6
|
-
version: "0.30.0",
|
|
7
|
-
type: "module",
|
|
8
|
-
description: "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
|
|
9
|
-
main: "dist/index.js",
|
|
10
|
-
types: "dist/index.d.ts",
|
|
11
|
-
license: "MIT",
|
|
12
|
-
repository: {
|
|
13
|
-
type: "git",
|
|
14
|
-
url: "https://github.com/cortexkit/aft"
|
|
15
|
-
},
|
|
16
|
-
files: [
|
|
17
|
-
"dist",
|
|
18
|
-
"src/tui",
|
|
19
|
-
"src/shared",
|
|
20
|
-
"src/logger.ts",
|
|
21
|
-
"README.md"
|
|
22
|
-
],
|
|
23
|
-
scripts: {
|
|
24
|
-
build: "bun build src/index.ts --outdir dist --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/sdk && bun build src/tui/index.tsx --outfile dist/tui.js --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/plugin/tui --external @opencode-ai/sdk --external @opentui/solid --external solid-js && tsc --emitDeclarationOnly",
|
|
25
|
-
typecheck: "tsc --noEmit",
|
|
26
|
-
pretest: "cd ../.. && cargo build",
|
|
27
|
-
test: "bun test",
|
|
28
|
-
"test:e2e": "bun test src/__tests__/e2e/",
|
|
29
|
-
lint: "biome check src",
|
|
30
|
-
schema: "bun scripts/build-schema.ts",
|
|
31
|
-
prepublishOnly: "bun run build"
|
|
32
|
-
},
|
|
33
|
-
dependencies: {
|
|
34
|
-
"@clack/prompts": "^1.2.0",
|
|
35
|
-
"@cortexkit/aft-bridge": "0.30.0",
|
|
36
|
-
"@opencode-ai/plugin": "^1.15.5",
|
|
37
|
-
"@opencode-ai/sdk": "^1.15.5",
|
|
38
|
-
"@xterm/headless": "^5.5.0",
|
|
39
|
-
"comment-json": "^4.6.2",
|
|
40
|
-
undici: "^7.25.0",
|
|
41
|
-
zod: "^4.1.8"
|
|
42
|
-
},
|
|
43
|
-
optionalDependencies: {
|
|
44
|
-
"@cortexkit/aft-darwin-arm64": "0.30.0",
|
|
45
|
-
"@cortexkit/aft-darwin-x64": "0.30.0",
|
|
46
|
-
"@cortexkit/aft-linux-arm64": "0.30.0",
|
|
47
|
-
"@cortexkit/aft-linux-x64": "0.30.0",
|
|
48
|
-
"@cortexkit/aft-win32-arm64": "0.30.0",
|
|
49
|
-
"@cortexkit/aft-win32-x64": "0.30.0"
|
|
50
|
-
},
|
|
51
|
-
devDependencies: {
|
|
52
|
-
"@types/node": "^22.0.0",
|
|
53
|
-
typescript: "^5.8.0"
|
|
54
|
-
},
|
|
55
|
-
exports: {
|
|
56
|
-
".": {
|
|
57
|
-
types: "./dist/index.d.ts",
|
|
58
|
-
import: "./dist/index.js"
|
|
59
|
-
},
|
|
60
|
-
"./tui": {
|
|
61
|
-
types: "./src/tui/index.tsx",
|
|
62
|
-
import: "./src/tui/index.tsx"
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
"oc-plugin": [
|
|
66
|
-
"server",
|
|
67
|
-
"tui"
|
|
68
|
-
],
|
|
69
|
-
peerDependencies: {
|
|
70
|
-
"@opencode-ai/plugin": ">=1.15.5"
|
|
71
|
-
}
|
|
72
|
-
};
|
|
4
|
+
var version = "0.30.2";
|
|
73
5
|
|
|
74
6
|
// src/shared/rpc-client.ts
|
|
75
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
76
8
|
import { join as join3 } from "node:path";
|
|
77
9
|
|
|
78
10
|
// src/logger.ts
|
|
@@ -154,26 +86,55 @@ function rpcPortFileDir(storageDir, directory) {
|
|
|
154
86
|
var MAX_RETRIES = 10;
|
|
155
87
|
var RETRY_DELAY_MS = 500;
|
|
156
88
|
var REQUEST_TIMEOUT_MS = 5000;
|
|
89
|
+
function abortError(signal) {
|
|
90
|
+
const reason = signal.reason;
|
|
91
|
+
if (reason instanceof Error)
|
|
92
|
+
return reason;
|
|
93
|
+
return new Error("AFT RPC request aborted");
|
|
94
|
+
}
|
|
95
|
+
function throwIfAborted(signal) {
|
|
96
|
+
if (signal?.aborted)
|
|
97
|
+
throw abortError(signal);
|
|
98
|
+
}
|
|
99
|
+
function delay(ms, signal) {
|
|
100
|
+
throwIfAborted(signal);
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
let timer;
|
|
103
|
+
const onAbort = () => {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
reject(signal ? abortError(signal) : new Error("AFT RPC request aborted"));
|
|
106
|
+
};
|
|
107
|
+
timer = setTimeout(() => {
|
|
108
|
+
signal?.removeEventListener("abort", onAbort);
|
|
109
|
+
resolve();
|
|
110
|
+
}, ms);
|
|
111
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
157
114
|
|
|
158
115
|
class AftRpcClient {
|
|
159
116
|
port = null;
|
|
160
117
|
token = null;
|
|
161
118
|
portsDir;
|
|
162
119
|
legacyPortFile;
|
|
120
|
+
stalePortFailures = new Map;
|
|
163
121
|
constructor(storageDir, directory) {
|
|
164
122
|
this.portsDir = rpcPortFileDir(storageDir, directory);
|
|
165
123
|
this.legacyPortFile = rpcPortFilePath(storageDir, directory);
|
|
166
124
|
}
|
|
167
|
-
async call(method, params = {}) {
|
|
168
|
-
const
|
|
125
|
+
async call(method, params = {}, options = {}) {
|
|
126
|
+
const { signal } = options;
|
|
127
|
+
throwIfAborted(signal);
|
|
128
|
+
const infos = await this.resolvePortInfos(signal);
|
|
169
129
|
if (infos.length === 0) {
|
|
170
130
|
throw new Error("AFT RPC server not available");
|
|
171
131
|
}
|
|
172
132
|
let placeholder = null;
|
|
173
133
|
let lastError = null;
|
|
174
134
|
for (const info of infos) {
|
|
135
|
+
throwIfAborted(signal);
|
|
175
136
|
try {
|
|
176
|
-
const result = await this.callOne(method, params, info);
|
|
137
|
+
const result = await this.callOne(method, params, info, signal);
|
|
177
138
|
if (this.looksLikePlaceholder(result)) {
|
|
178
139
|
placeholder = result;
|
|
179
140
|
continue;
|
|
@@ -182,6 +143,7 @@ class AftRpcClient {
|
|
|
182
143
|
this.token = info.token;
|
|
183
144
|
return result;
|
|
184
145
|
} catch (err) {
|
|
146
|
+
throwIfAborted(signal);
|
|
185
147
|
lastError = err;
|
|
186
148
|
}
|
|
187
149
|
}
|
|
@@ -189,12 +151,12 @@ class AftRpcClient {
|
|
|
189
151
|
return placeholder;
|
|
190
152
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
191
153
|
}
|
|
192
|
-
async callOne(method, params, info) {
|
|
154
|
+
async callOne(method, params, info, signal) {
|
|
193
155
|
const response = await this.fetchWithTimeout(`http://127.0.0.1:${info.port}/rpc/${method}`, {
|
|
194
156
|
method: "POST",
|
|
195
157
|
headers: { "Content-Type": "application/json" },
|
|
196
158
|
body: JSON.stringify({ ...params, token: info.token })
|
|
197
|
-
});
|
|
159
|
+
}, signal);
|
|
198
160
|
if (!response.ok) {
|
|
199
161
|
const text = await response.text();
|
|
200
162
|
throw new Error(`RPC ${method} failed (${response.status}): ${text}`);
|
|
@@ -215,49 +177,60 @@ class AftRpcClient {
|
|
|
215
177
|
return false;
|
|
216
178
|
}
|
|
217
179
|
}
|
|
218
|
-
async resolvePortInfos() {
|
|
180
|
+
async resolvePortInfos(signal) {
|
|
219
181
|
for (let attempt = 0;attempt < MAX_RETRIES; attempt++) {
|
|
182
|
+
throwIfAborted(signal);
|
|
220
183
|
const infos = this.readAllPortFiles();
|
|
221
184
|
if (infos.length > 0) {
|
|
222
185
|
const alive = [];
|
|
223
186
|
for (const info of infos) {
|
|
224
|
-
|
|
187
|
+
throwIfAborted(signal);
|
|
188
|
+
if (await this.healthCheck(info.port, signal)) {
|
|
189
|
+
this.clearPortFailure(info);
|
|
225
190
|
alive.push(info);
|
|
191
|
+
} else {
|
|
192
|
+
this.recordPortFailure(info);
|
|
226
193
|
}
|
|
227
194
|
}
|
|
228
195
|
if (alive.length > 0)
|
|
229
196
|
return alive;
|
|
230
197
|
}
|
|
231
198
|
if (attempt < MAX_RETRIES - 1) {
|
|
232
|
-
await
|
|
199
|
+
await delay(RETRY_DELAY_MS, signal);
|
|
233
200
|
}
|
|
234
201
|
}
|
|
235
202
|
return [];
|
|
236
203
|
}
|
|
237
204
|
readAllPortFiles() {
|
|
238
205
|
const collected = [];
|
|
206
|
+
const seenPorts = new Set;
|
|
207
|
+
const add = (info) => {
|
|
208
|
+
if (seenPorts.has(info.port))
|
|
209
|
+
return;
|
|
210
|
+
seenPorts.add(info.port);
|
|
211
|
+
collected.push(info);
|
|
212
|
+
};
|
|
239
213
|
if (existsSync(this.portsDir)) {
|
|
240
214
|
try {
|
|
241
|
-
const entries = readdirSync(this.portsDir);
|
|
215
|
+
const entries = readdirSync(this.portsDir).sort();
|
|
242
216
|
for (const entry of entries) {
|
|
243
217
|
if (!entry.endsWith(".json"))
|
|
244
218
|
continue;
|
|
245
|
-
const
|
|
219
|
+
const filePath = join3(this.portsDir, entry);
|
|
220
|
+
const info = this.parsePortFile(filePath);
|
|
246
221
|
if (info)
|
|
247
|
-
|
|
222
|
+
add({ ...info, source: "instance", path: filePath });
|
|
248
223
|
}
|
|
249
224
|
} catch {}
|
|
250
225
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
collected.push(info);
|
|
255
|
-
}
|
|
226
|
+
const legacyInfo = this.parsePortFile(this.legacyPortFile);
|
|
227
|
+
if (legacyInfo)
|
|
228
|
+
add({ ...legacyInfo, source: "legacy", path: this.legacyPortFile });
|
|
256
229
|
return collected;
|
|
257
230
|
}
|
|
258
|
-
parsePortFile(
|
|
231
|
+
parsePortFile(filePath) {
|
|
259
232
|
try {
|
|
260
|
-
const content = readFileSync(
|
|
233
|
+
const content = readFileSync(filePath, "utf-8").trim();
|
|
261
234
|
let port;
|
|
262
235
|
let token;
|
|
263
236
|
if (content.startsWith("{")) {
|
|
@@ -277,28 +250,59 @@ class AftRpcClient {
|
|
|
277
250
|
return null;
|
|
278
251
|
}
|
|
279
252
|
}
|
|
280
|
-
|
|
253
|
+
portFailureKey(info) {
|
|
254
|
+
if (info.source !== "instance" || !info.path)
|
|
255
|
+
return null;
|
|
256
|
+
return `${info.path}\x00${info.port}\x00${info.token ?? ""}`;
|
|
257
|
+
}
|
|
258
|
+
clearPortFailure(info) {
|
|
259
|
+
const key = this.portFailureKey(info);
|
|
260
|
+
if (key)
|
|
261
|
+
this.stalePortFailures.delete(key);
|
|
262
|
+
}
|
|
263
|
+
recordPortFailure(info) {
|
|
264
|
+
const key = this.portFailureKey(info);
|
|
265
|
+
if (!key || !info.path)
|
|
266
|
+
return;
|
|
267
|
+
const failures = (this.stalePortFailures.get(key) ?? 0) + 1;
|
|
268
|
+
if (failures < 2) {
|
|
269
|
+
this.stalePortFailures.set(key, failures);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.stalePortFailures.delete(key);
|
|
281
273
|
try {
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
274
|
+
const current = this.parsePortFile(info.path);
|
|
275
|
+
if (current?.port === info.port && current.token === info.token) {
|
|
276
|
+
unlinkSync(info.path);
|
|
277
|
+
}
|
|
278
|
+
} catch {}
|
|
279
|
+
}
|
|
280
|
+
async healthCheck(port, signal) {
|
|
281
|
+
try {
|
|
282
|
+
const response = await this.fetchWithTimeout(`http://127.0.0.1:${port}/health`, { method: "GET" }, signal);
|
|
285
283
|
return response.ok;
|
|
286
284
|
} catch {
|
|
285
|
+
throwIfAborted(signal);
|
|
287
286
|
return false;
|
|
288
287
|
}
|
|
289
288
|
}
|
|
290
|
-
async fetchWithTimeout(url, options) {
|
|
289
|
+
async fetchWithTimeout(url, options, signal) {
|
|
290
|
+
throwIfAborted(signal);
|
|
291
291
|
const controller = new AbortController;
|
|
292
292
|
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
293
|
+
const onAbort = () => controller.abort();
|
|
294
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
293
295
|
try {
|
|
294
296
|
return await fetch(url, { ...options, signal: controller.signal });
|
|
295
297
|
} finally {
|
|
296
298
|
clearTimeout(timeout);
|
|
299
|
+
signal?.removeEventListener("abort", onAbort);
|
|
297
300
|
}
|
|
298
301
|
}
|
|
299
302
|
reset() {
|
|
300
303
|
this.port = null;
|
|
301
304
|
this.token = null;
|
|
305
|
+
this.stalePortFailures.clear();
|
|
302
306
|
}
|
|
303
307
|
}
|
|
304
308
|
|
|
@@ -418,6 +422,28 @@ function coerceAftStatus(response) {
|
|
|
418
422
|
|
|
419
423
|
// src/tui/sidebar.tsx
|
|
420
424
|
import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js";
|
|
425
|
+
|
|
426
|
+
// src/shared/storage-paths.ts
|
|
427
|
+
import { homedir } from "node:os";
|
|
428
|
+
import { join as join4 } from "node:path";
|
|
429
|
+
function homeDir() {
|
|
430
|
+
if (process.platform === "win32")
|
|
431
|
+
return process.env.USERPROFILE || process.env.HOME || homedir();
|
|
432
|
+
return process.env.HOME || homedir();
|
|
433
|
+
}
|
|
434
|
+
function dataHome() {
|
|
435
|
+
if (process.env.XDG_DATA_HOME)
|
|
436
|
+
return process.env.XDG_DATA_HOME;
|
|
437
|
+
if (process.platform === "win32") {
|
|
438
|
+
return process.env.LOCALAPPDATA || process.env.APPDATA || join4(homeDir(), "AppData", "Local");
|
|
439
|
+
}
|
|
440
|
+
return join4(homeDir(), ".local", "share");
|
|
441
|
+
}
|
|
442
|
+
function resolveCortexKitStorageRoot() {
|
|
443
|
+
return join4(dataHome(), "cortexkit", "aft");
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/tui/sidebar.tsx
|
|
421
447
|
import { jsxDEV, Fragment } from "@opentui/solid/jsx-dev-runtime";
|
|
422
448
|
var SINGLE_BORDER = { type: "single" };
|
|
423
449
|
var REFRESH_DEBOUNCE_MS = 200;
|
|
@@ -520,45 +546,92 @@ var SectionHeader = (props) => /* @__PURE__ */ jsxDEV("box", {
|
|
|
520
546
|
}, undefined, false, undefined, this)
|
|
521
547
|
}, undefined, false, undefined, this)
|
|
522
548
|
}, undefined, false, undefined, this);
|
|
549
|
+
function resolveTuiStorageDir() {
|
|
550
|
+
return resolveCortexKitStorageRoot();
|
|
551
|
+
}
|
|
523
552
|
var sidebarClients = new Map;
|
|
524
553
|
function getClient(directory) {
|
|
525
554
|
let client = sidebarClients.get(directory);
|
|
526
555
|
if (client)
|
|
527
556
|
return client;
|
|
528
|
-
|
|
529
|
-
const dataHome = process.env.XDG_DATA_HOME || `${home}/.local/share`;
|
|
530
|
-
const storageDir = `${dataHome}/cortexkit/aft`;
|
|
531
|
-
client = new AftRpcClient(storageDir, directory);
|
|
557
|
+
client = new AftRpcClient(resolveTuiStorageDir(), directory);
|
|
532
558
|
sidebarClients.set(directory, client);
|
|
533
559
|
return client;
|
|
534
560
|
}
|
|
561
|
+
function scopedSidebarSnapshot(scoped, directory, sessionID) {
|
|
562
|
+
if (!scoped)
|
|
563
|
+
return null;
|
|
564
|
+
if (scoped.directory !== directory || scoped.sessionID !== sessionID)
|
|
565
|
+
return null;
|
|
566
|
+
return scoped.snapshot;
|
|
567
|
+
}
|
|
535
568
|
var SidebarContent = (props) => {
|
|
536
569
|
const [status, setStatus] = createSignal(null);
|
|
537
|
-
let inflight =
|
|
570
|
+
let inflight = null;
|
|
571
|
+
let generation = 0;
|
|
538
572
|
let debounceTimer;
|
|
539
573
|
let pollTimer;
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
574
|
+
const currentDirectory = () => props.api.state.path.directory ?? "";
|
|
575
|
+
const requestRender = () => {
|
|
576
|
+
try {
|
|
577
|
+
props.api.renderer.requestRender();
|
|
578
|
+
} catch {}
|
|
579
|
+
};
|
|
580
|
+
const abortInflight = () => {
|
|
581
|
+
if (!inflight)
|
|
543
582
|
return;
|
|
544
|
-
|
|
583
|
+
inflight.controller.abort();
|
|
584
|
+
inflight = null;
|
|
585
|
+
};
|
|
586
|
+
const clearStatusForContext = (directory, sessionID) => {
|
|
587
|
+
const current = status();
|
|
588
|
+
if (!current)
|
|
545
589
|
return;
|
|
546
|
-
|
|
547
|
-
if (!directory)
|
|
590
|
+
if (current.directory === directory && current.sessionID === sessionID)
|
|
548
591
|
return;
|
|
549
|
-
|
|
592
|
+
setStatus(null);
|
|
593
|
+
requestRender();
|
|
594
|
+
};
|
|
595
|
+
const refresh = async () => {
|
|
596
|
+
const sid = props.sessionID();
|
|
597
|
+
const directory = currentDirectory();
|
|
598
|
+
if (!sid || !directory) {
|
|
599
|
+
generation++;
|
|
600
|
+
abortInflight();
|
|
601
|
+
if (status()) {
|
|
602
|
+
setStatus(null);
|
|
603
|
+
requestRender();
|
|
604
|
+
}
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
clearStatusForContext(directory, sid);
|
|
608
|
+
if (inflight) {
|
|
609
|
+
if (inflight.directory === directory && inflight.sessionID === sid)
|
|
610
|
+
return;
|
|
611
|
+
generation++;
|
|
612
|
+
abortInflight();
|
|
613
|
+
}
|
|
614
|
+
const requestGeneration = ++generation;
|
|
615
|
+
const controller = new AbortController;
|
|
616
|
+
inflight = { controller, generation: requestGeneration, directory, sessionID: sid };
|
|
550
617
|
try {
|
|
551
618
|
const client = getClient(directory);
|
|
552
|
-
const response = await client.call("status", { sessionID: sid });
|
|
619
|
+
const response = await client.call("status", { sessionID: sid }, { signal: controller.signal });
|
|
620
|
+
if (controller.signal.aborted || requestGeneration !== generation)
|
|
621
|
+
return;
|
|
622
|
+
if (currentDirectory() !== directory || props.sessionID() !== sid)
|
|
623
|
+
return;
|
|
553
624
|
if (response && response.success !== false) {
|
|
554
625
|
const snapshot = coerceAftStatus(response);
|
|
555
|
-
setStatus(snapshot);
|
|
556
|
-
|
|
557
|
-
props.api.renderer.requestRender();
|
|
558
|
-
} catch {}
|
|
626
|
+
setStatus({ directory, sessionID: sid, snapshot });
|
|
627
|
+
requestRender();
|
|
559
628
|
}
|
|
560
|
-
} catch {
|
|
561
|
-
|
|
629
|
+
} catch {
|
|
630
|
+
if (controller.signal.aborted || requestGeneration !== generation)
|
|
631
|
+
return;
|
|
632
|
+
} finally {
|
|
633
|
+
if (inflight?.generation === requestGeneration)
|
|
634
|
+
inflight = null;
|
|
562
635
|
}
|
|
563
636
|
};
|
|
564
637
|
const scheduleRefresh = () => {
|
|
@@ -570,6 +643,8 @@ var SidebarContent = (props) => {
|
|
|
570
643
|
}, REFRESH_DEBOUNCE_MS);
|
|
571
644
|
};
|
|
572
645
|
onCleanup(() => {
|
|
646
|
+
generation++;
|
|
647
|
+
abortInflight();
|
|
573
648
|
if (debounceTimer)
|
|
574
649
|
clearTimeout(debounceTimer);
|
|
575
650
|
if (pollTimer)
|
|
@@ -604,13 +679,15 @@ var SidebarContent = (props) => {
|
|
|
604
679
|
unsub();
|
|
605
680
|
} catch {}
|
|
606
681
|
}
|
|
682
|
+
generation++;
|
|
683
|
+
abortInflight();
|
|
607
684
|
if (pollTimer) {
|
|
608
685
|
clearInterval(pollTimer);
|
|
609
686
|
pollTimer = undefined;
|
|
610
687
|
}
|
|
611
688
|
});
|
|
612
689
|
}, { defer: false }));
|
|
613
|
-
const s =
|
|
690
|
+
const s = () => scopedSidebarSnapshot(status(), currentDirectory(), props.sessionID());
|
|
614
691
|
const notInitialized = () => s()?.cache_role === "not_initialized";
|
|
615
692
|
const searchStatus = () => statusDisplay(s()?.search_index?.status ?? "disabled");
|
|
616
693
|
const semanticStatus = () => statusDisplay(s()?.semantic_index?.status ?? "disabled");
|
|
@@ -821,10 +898,7 @@ function getRpcClient(directory) {
|
|
|
821
898
|
let client = rpcClients.get(directory);
|
|
822
899
|
if (client)
|
|
823
900
|
return client;
|
|
824
|
-
|
|
825
|
-
const dataHome = process.env.XDG_DATA_HOME || `${home}/.local/share`;
|
|
826
|
-
const storageDir = `${dataHome}/cortexkit/aft`;
|
|
827
|
-
client = new AftRpcClient(storageDir, directory);
|
|
901
|
+
client = new AftRpcClient(resolveTuiStorageDir(), directory);
|
|
828
902
|
rpcClients.set(directory, client);
|
|
829
903
|
return client;
|
|
830
904
|
}
|
|
@@ -902,16 +976,41 @@ var StatusDialog = (props) => {
|
|
|
902
976
|
const t = () => theme();
|
|
903
977
|
const [status, setStatus] = createSignal2(props.initial);
|
|
904
978
|
const [error, setError] = createSignal2(props.initialError);
|
|
905
|
-
|
|
979
|
+
let pollGeneration = 0;
|
|
980
|
+
let pollController = null;
|
|
981
|
+
const pollStatus = async () => {
|
|
982
|
+
if (pollController)
|
|
983
|
+
return;
|
|
984
|
+
const controller = new AbortController;
|
|
985
|
+
const requestGeneration = ++pollGeneration;
|
|
986
|
+
pollController = controller;
|
|
906
987
|
try {
|
|
907
|
-
const response = await props.client.call("status", { sessionID: props.sessionID });
|
|
988
|
+
const response = await props.client.call("status", { sessionID: props.sessionID }, { signal: controller.signal });
|
|
989
|
+
if (controller.signal.aborted || requestGeneration !== pollGeneration)
|
|
990
|
+
return;
|
|
908
991
|
if (response.success !== false) {
|
|
909
992
|
setStatus(coerceAftStatus(response));
|
|
910
993
|
setError(null);
|
|
911
994
|
}
|
|
912
|
-
} catch {
|
|
995
|
+
} catch {
|
|
996
|
+
if (controller.signal.aborted || requestGeneration !== pollGeneration)
|
|
997
|
+
return;
|
|
998
|
+
} finally {
|
|
999
|
+
if (pollController === controller)
|
|
1000
|
+
pollController = null;
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
const timer = setInterval(() => {
|
|
1004
|
+
pollStatus();
|
|
913
1005
|
}, POLL_INTERVAL_MS2);
|
|
914
|
-
onCleanup2(() =>
|
|
1006
|
+
onCleanup2(() => {
|
|
1007
|
+
clearInterval(timer);
|
|
1008
|
+
pollGeneration++;
|
|
1009
|
+
if (pollController) {
|
|
1010
|
+
pollController.abort();
|
|
1011
|
+
pollController = null;
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
915
1014
|
const cacheRoleTone = (role) => role === "main" ? "accent" : role === "worktree" ? "warn" : "muted";
|
|
916
1015
|
const compressionAggregateRows = () => formatCompressionSidebarRows(status()?.compression);
|
|
917
1016
|
return /* @__PURE__ */ jsxDEV2("box", {
|
|
@@ -939,7 +1038,7 @@ var StatusDialog = (props) => {
|
|
|
939
1038
|
fg: t().textMuted,
|
|
940
1039
|
children: [
|
|
941
1040
|
"v",
|
|
942
|
-
status()?.version ??
|
|
1041
|
+
status()?.version ?? version
|
|
943
1042
|
]
|
|
944
1043
|
}, undefined, true, undefined, this)
|
|
945
1044
|
]
|
|
@@ -1346,7 +1445,7 @@ ${featureText}`;
|
|
|
1346
1445
|
}
|
|
1347
1446
|
var tui = async (api) => {
|
|
1348
1447
|
try {
|
|
1349
|
-
api.slots.register(createAftSidebarSlot(api,
|
|
1448
|
+
api.slots.register(createAftSidebarSlot(api, version));
|
|
1350
1449
|
} catch {}
|
|
1351
1450
|
registerStatusCommand(api);
|
|
1352
1451
|
showStartupNotifications(api);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/aft-opencode",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^1.2.0",
|
|
32
|
-
"@cortexkit/aft-bridge": "0.30.
|
|
32
|
+
"@cortexkit/aft-bridge": "0.30.2",
|
|
33
33
|
"@opencode-ai/plugin": "^1.15.5",
|
|
34
34
|
"@opencode-ai/sdk": "^1.15.5",
|
|
35
35
|
"@xterm/headless": "^5.5.0",
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"zod": "^4.1.8"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
|
-
"@cortexkit/aft-darwin-arm64": "0.30.
|
|
42
|
-
"@cortexkit/aft-darwin-x64": "0.30.
|
|
43
|
-
"@cortexkit/aft-linux-arm64": "0.30.
|
|
44
|
-
"@cortexkit/aft-linux-x64": "0.30.
|
|
45
|
-
"@cortexkit/aft-win32-arm64": "0.30.
|
|
46
|
-
"@cortexkit/aft-win32-x64": "0.30.
|
|
41
|
+
"@cortexkit/aft-darwin-arm64": "0.30.2",
|
|
42
|
+
"@cortexkit/aft-darwin-x64": "0.30.2",
|
|
43
|
+
"@cortexkit/aft-linux-arm64": "0.30.2",
|
|
44
|
+
"@cortexkit/aft-linux-x64": "0.30.2",
|
|
45
|
+
"@cortexkit/aft-win32-arm64": "0.30.2",
|
|
46
|
+
"@cortexkit/aft-win32-x64": "0.30.2"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^22.0.0",
|
|
@@ -8,12 +8,11 @@
|
|
|
8
8
|
* prefix cache that the previous assistant turn warmed.
|
|
9
9
|
*
|
|
10
10
|
* APPROACH: Mirror what `opencode-xtra` does in production. Read recent
|
|
11
|
-
* messages from `client.session.messages()`,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* by UserMessage.
|
|
11
|
+
* messages from `client.session.messages()`, walk newest→oldest across
|
|
12
|
+
* roles, and MERGE field-by-field so the newest context-bearing message
|
|
13
|
+
* wins while older messages only fill fields it did not provide. Read BOTH
|
|
14
|
+
* the flat shape (`info.providerID`) used by AssistantMessage and the
|
|
15
|
+
* nested shape (`info.model.providerID`) used by UserMessage.
|
|
17
16
|
*
|
|
18
17
|
* IMPORTANT: This context is only meaningful for callers that DO trigger
|
|
19
18
|
* LLM inference (e.g. background-bash idle wakes with `noReply: false`).
|
|
@@ -49,11 +48,6 @@ function extractMessages(response: unknown): unknown[] {
|
|
|
49
48
|
return [];
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
function getRole(message: unknown): string | undefined {
|
|
53
|
-
if (!isRecord(message) || !isRecord(message.info)) return undefined;
|
|
54
|
-
return typeof message.info.role === "string" ? message.info.role : undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
51
|
function extractFromMessage(message: unknown): ResolvedPromptContext | null {
|
|
58
52
|
if (!isRecord(message) || !isRecord(message.info)) return null;
|
|
59
53
|
const info = message.info as RawInfo;
|
|
@@ -103,10 +97,10 @@ function isComplete(ctx: ResolvedPromptContext): boolean {
|
|
|
103
97
|
}
|
|
104
98
|
|
|
105
99
|
/**
|
|
106
|
-
* Read recent messages from the OpenCode session and resolve the
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
100
|
+
* Read recent messages from the OpenCode session and resolve the newest
|
|
101
|
+
* effective prompt context across roles. Merges across messages so partial
|
|
102
|
+
* fields are filled in from older messages only when the newer context did
|
|
103
|
+
* not provide them. Returns null if no usable context is found.
|
|
110
104
|
*
|
|
111
105
|
* Mirrors `resolveSessionPromptParams` in `opencode-xtra` (the working
|
|
112
106
|
* reference implementation).
|
|
@@ -153,19 +147,7 @@ export async function resolvePromptContext(
|
|
|
153
147
|
}
|
|
154
148
|
if (messages.length === 0) return null;
|
|
155
149
|
|
|
156
|
-
// Pass 1: prefer the most recent assistant, merge older assistants to
|
|
157
|
-
// fill missing fields.
|
|
158
150
|
let result: ResolvedPromptContext = {};
|
|
159
|
-
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
160
|
-
if (getRole(messages[i]) !== "assistant") continue;
|
|
161
|
-
const ctx = extractFromMessage(messages[i]);
|
|
162
|
-
if (!ctx) continue;
|
|
163
|
-
result = mergeContexts(result, ctx);
|
|
164
|
-
if (isComplete(result)) return result;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Pass 2: fall back to any role (covers user messages, which carry
|
|
168
|
-
// model nested under `info.model`).
|
|
169
151
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
170
152
|
const ctx = extractFromMessage(messages[i]);
|
|
171
153
|
if (!ctx) continue;
|