@cortexkit/aft-opencode 0.42.0 → 0.43.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 +35 -0
- package/dist/bg-notifications.d.ts.map +1 -1
- package/dist/config.d.ts +7 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/configure-warnings.d.ts +3 -3
- package/dist/configure-warnings.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6092 -9528
- package/dist/notifications.d.ts +2 -2
- package/dist/notifications.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +6 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-notifications.d.ts +29 -15
- package/dist/shared/rpc-notifications.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts +10 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/subc-tool-schemas.d.ts +1 -1
- package/dist/subc-tool-schemas.d.ts.map +1 -1
- package/dist/tools/_shared.d.ts +12 -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.map +1 -1
- package/dist/tools/bash_watch.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/imports.d.ts.map +1 -1
- package/dist/tools/inspect.d.ts +0 -1
- package/dist/tools/inspect.d.ts.map +1 -1
- package/dist/tools/navigation.d.ts.map +1 -1
- package/dist/tools/permissions.d.ts +23 -13
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/reading.d.ts +0 -1
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/refactoring.d.ts.map +1 -1
- package/dist/tools/safety.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/notification-socket.d.ts +41 -0
- package/dist/tui/notification-socket.d.ts.map +1 -0
- package/dist/tui.js +2460 -187
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -9
- package/src/shared/rpc-client.ts +9 -0
- package/src/shared/rpc-notifications.ts +97 -41
- package/src/shared/rpc-server.ts +286 -67
- package/src/tui/index.tsx +77 -125
- package/src/tui/notification-socket.ts +421 -0
- package/src/tui/sidebar.tsx +38 -54
- package/dist/patch-parser.d.ts +0 -33
- package/dist/patch-parser.d.ts.map +0 -1
- package/dist/shared/opencode-config-dir.d.ts +0 -16
- package/dist/shared/opencode-config-dir.d.ts.map +0 -1
- package/dist/shared/pty-cache.d.ts +0 -18
- package/dist/shared/pty-cache.d.ts.map +0 -1
- package/dist/shared/tui-config.d.ts +0 -2
- package/dist/shared/tui-config.d.ts.map +0 -1
- package/src/shared/opencode-config-dir.ts +0 -46
- package/src/shared/pty-cache.ts +0 -113
- package/src/shared/tui-config.ts +0 -58
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { AftRpcClient, type AftRpcEndpoint } from "../shared/rpc-client";
|
|
2
|
+
import { resolveCortexKitStorageRoot } from "../shared/storage-paths";
|
|
3
|
+
|
|
4
|
+
export interface SocketNotification {
|
|
5
|
+
id: number;
|
|
6
|
+
type: string;
|
|
7
|
+
payload: Record<string, unknown>;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface StatusInvalidation {
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TuiSocketOptions {
|
|
16
|
+
getDirectory: () => string | null | undefined;
|
|
17
|
+
getSessionId: () => string | null;
|
|
18
|
+
onNotification: (notification: SocketNotification) => boolean | Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface WebSocketLike {
|
|
22
|
+
readyState: number;
|
|
23
|
+
addEventListener(
|
|
24
|
+
type: "open" | "message" | "close" | "error",
|
|
25
|
+
listener: (event: unknown) => void,
|
|
26
|
+
): void;
|
|
27
|
+
send(data: string): void;
|
|
28
|
+
close(): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type WebSocketFactory = new (url: string) => WebSocketLike;
|
|
32
|
+
type RpcClientLike = Pick<AftRpcClient, "resolveEndpoint" | "reset">;
|
|
33
|
+
|
|
34
|
+
interface SocketDeps {
|
|
35
|
+
createClient: (directory: string) => RpcClientLike;
|
|
36
|
+
WebSocketCtor: WebSocketFactory | null;
|
|
37
|
+
setTimeout: typeof setTimeout;
|
|
38
|
+
clearTimeout: typeof clearTimeout;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SocketScope {
|
|
42
|
+
directory: string;
|
|
43
|
+
sessionId: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const RECONNECT_BASE_MS = 500;
|
|
47
|
+
const RECONNECT_MAX_MS = 10_000;
|
|
48
|
+
const WEB_SOCKET_OPEN = 1;
|
|
49
|
+
const clients = new Map<string, AftRpcClient>();
|
|
50
|
+
const statusInvalidationListeners = new Set<(event: StatusInvalidation) => void>();
|
|
51
|
+
const lastHandledIdBySession = new Map<string, number>();
|
|
52
|
+
|
|
53
|
+
function defaultWebSocketCtor(): WebSocketFactory | null {
|
|
54
|
+
const ctor = (globalThis as typeof globalThis & { WebSocket?: WebSocketFactory }).WebSocket;
|
|
55
|
+
return ctor ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function defaultClient(directory: string): AftRpcClient {
|
|
59
|
+
let client = clients.get(directory);
|
|
60
|
+
if (!client) {
|
|
61
|
+
client = new AftRpcClient(resolveCortexKitStorageRoot(), directory);
|
|
62
|
+
clients.set(directory, client);
|
|
63
|
+
}
|
|
64
|
+
return client;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let deps: SocketDeps = {
|
|
68
|
+
createClient: defaultClient,
|
|
69
|
+
WebSocketCtor: defaultWebSocketCtor(),
|
|
70
|
+
setTimeout,
|
|
71
|
+
clearTimeout,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let opts: TuiSocketOptions | null = null;
|
|
75
|
+
let socket: WebSocketLike | null = null;
|
|
76
|
+
let socketScope: SocketScope | null = null;
|
|
77
|
+
let helloedScope: SocketScope | null = null;
|
|
78
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | undefined;
|
|
79
|
+
let reconnectAttempt = 0;
|
|
80
|
+
let closed = true;
|
|
81
|
+
let generation = 0;
|
|
82
|
+
let connectingGeneration: number | null = null;
|
|
83
|
+
let connectingScope: SocketScope | null = null;
|
|
84
|
+
|
|
85
|
+
export function startAftTuiSocket(options: TuiSocketOptions): void {
|
|
86
|
+
opts = options;
|
|
87
|
+
closed = false;
|
|
88
|
+
generation += 1;
|
|
89
|
+
void reconcileSocketScope();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function stopAftTuiSocket(): void {
|
|
93
|
+
closed = true;
|
|
94
|
+
generation += 1;
|
|
95
|
+
connectingGeneration = null;
|
|
96
|
+
connectingScope = null;
|
|
97
|
+
if (reconnectTimer) {
|
|
98
|
+
deps.clearTimeout(reconnectTimer);
|
|
99
|
+
reconnectTimer = undefined;
|
|
100
|
+
}
|
|
101
|
+
closeCurrentSocket(false);
|
|
102
|
+
for (const client of clients.values()) client.reset();
|
|
103
|
+
clients.clear();
|
|
104
|
+
reconnectAttempt = 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function refreshAftTuiSocketScope(): void {
|
|
108
|
+
if (closed) return;
|
|
109
|
+
void reconcileSocketScope();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function subscribeStatusInvalidations(
|
|
113
|
+
listener: (event: StatusInvalidation) => void,
|
|
114
|
+
): () => void {
|
|
115
|
+
statusInvalidationListeners.add(listener);
|
|
116
|
+
return () => {
|
|
117
|
+
statusInvalidationListeners.delete(listener);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function createDebouncedStatusRefresh(
|
|
122
|
+
refresh: () => void | Promise<void>,
|
|
123
|
+
delayMs: number,
|
|
124
|
+
): { schedule: () => void; dispose: () => void } {
|
|
125
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
126
|
+
let disposed = false;
|
|
127
|
+
return {
|
|
128
|
+
schedule() {
|
|
129
|
+
if (disposed) return;
|
|
130
|
+
if (timer) deps.clearTimeout(timer);
|
|
131
|
+
timer = deps.setTimeout(() => {
|
|
132
|
+
timer = undefined;
|
|
133
|
+
void refresh();
|
|
134
|
+
}, delayMs);
|
|
135
|
+
},
|
|
136
|
+
dispose() {
|
|
137
|
+
disposed = true;
|
|
138
|
+
if (timer) {
|
|
139
|
+
deps.clearTimeout(timer);
|
|
140
|
+
timer = undefined;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function currentScope(): SocketScope | null {
|
|
147
|
+
const directory = opts?.getDirectory() ?? "";
|
|
148
|
+
const sessionId = opts?.getSessionId() ?? "";
|
|
149
|
+
if (!directory || !sessionId) return null;
|
|
150
|
+
return { directory, sessionId };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sameScope(a: SocketScope | null, b: SocketScope | null): boolean {
|
|
154
|
+
return a?.directory === b?.directory && a?.sessionId === b?.sessionId;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function reconcileSocketScope(): Promise<void> {
|
|
158
|
+
if (closed || !opts) return;
|
|
159
|
+
const scope = currentScope();
|
|
160
|
+
if (!scope) {
|
|
161
|
+
generation += 1;
|
|
162
|
+
connectingGeneration = null;
|
|
163
|
+
connectingScope = null;
|
|
164
|
+
closeCurrentSocket(false);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (socket && socketScope?.directory === scope.directory) {
|
|
169
|
+
if (!sameScope(socketScope, scope) && socket.readyState !== WEB_SOCKET_OPEN) {
|
|
170
|
+
generation += 1;
|
|
171
|
+
closeCurrentSocket(false);
|
|
172
|
+
await connect(scope, generation);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (socket.readyState === WEB_SOCKET_OPEN && !sameScope(helloedScope, scope)) {
|
|
176
|
+
await sendHelloWithFreshToken(socket, scope, generation);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (sameScope(connectingScope, scope)) return;
|
|
182
|
+
|
|
183
|
+
generation += 1;
|
|
184
|
+
connectingGeneration = null;
|
|
185
|
+
connectingScope = null;
|
|
186
|
+
closeCurrentSocket(false);
|
|
187
|
+
await connect(scope, generation);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function connect(scope: SocketScope, connectGeneration: number): Promise<void> {
|
|
191
|
+
if (closed || socket || connectingGeneration !== null) return;
|
|
192
|
+
const WebSocketCtor = deps.WebSocketCtor ?? defaultWebSocketCtor();
|
|
193
|
+
if (!WebSocketCtor) {
|
|
194
|
+
scheduleReconnect();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
connectingGeneration = connectGeneration;
|
|
199
|
+
connectingScope = scope;
|
|
200
|
+
let endpoint: AftRpcEndpoint | null = null;
|
|
201
|
+
try {
|
|
202
|
+
endpoint = await deps.createClient(scope.directory).resolveEndpoint();
|
|
203
|
+
} catch {
|
|
204
|
+
endpoint = null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (connectingGeneration === connectGeneration) {
|
|
208
|
+
connectingGeneration = null;
|
|
209
|
+
connectingScope = null;
|
|
210
|
+
}
|
|
211
|
+
if (closed || connectGeneration !== generation || !sameScope(currentScope(), scope)) return;
|
|
212
|
+
if (!endpoint) {
|
|
213
|
+
scheduleReconnect();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let ws: WebSocketLike;
|
|
218
|
+
try {
|
|
219
|
+
ws = new WebSocketCtor(`ws://127.0.0.1:${endpoint.port}/ws`);
|
|
220
|
+
} catch {
|
|
221
|
+
scheduleReconnect();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
socket = ws;
|
|
226
|
+
socketScope = scope;
|
|
227
|
+
helloedScope = null;
|
|
228
|
+
|
|
229
|
+
ws.addEventListener("open", () => {
|
|
230
|
+
if (socket !== ws || connectGeneration !== generation) return;
|
|
231
|
+
reconnectAttempt = 0;
|
|
232
|
+
sendHello(ws, scope, endpoint.token);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
ws.addEventListener("message", (event) => {
|
|
236
|
+
if (socket !== ws || connectGeneration !== generation) return;
|
|
237
|
+
const data =
|
|
238
|
+
typeof (event as { data?: unknown }).data === "string"
|
|
239
|
+
? (event as { data: string }).data
|
|
240
|
+
: String((event as { data?: unknown }).data ?? "");
|
|
241
|
+
void handleSocketMessage(ws, data, connectGeneration);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const onDown = () => {
|
|
245
|
+
if (socket !== ws) return;
|
|
246
|
+
socket = null;
|
|
247
|
+
socketScope = null;
|
|
248
|
+
helloedScope = null;
|
|
249
|
+
generation += 1;
|
|
250
|
+
scheduleReconnect();
|
|
251
|
+
};
|
|
252
|
+
ws.addEventListener("close", onDown);
|
|
253
|
+
ws.addEventListener("error", () => {
|
|
254
|
+
try {
|
|
255
|
+
ws.close();
|
|
256
|
+
} catch {
|
|
257
|
+
// best-effort
|
|
258
|
+
}
|
|
259
|
+
onDown();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function closeCurrentSocket(schedule: boolean): void {
|
|
264
|
+
const ws = socket;
|
|
265
|
+
socket = null;
|
|
266
|
+
socketScope = null;
|
|
267
|
+
helloedScope = null;
|
|
268
|
+
if (ws) {
|
|
269
|
+
try {
|
|
270
|
+
ws.close();
|
|
271
|
+
} catch {
|
|
272
|
+
// best-effort
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (schedule) scheduleReconnect();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function scheduleReconnect(): void {
|
|
279
|
+
if (closed || reconnectTimer) return;
|
|
280
|
+
const scope = currentScope();
|
|
281
|
+
if (!scope) return;
|
|
282
|
+
const delay = Math.min(RECONNECT_BASE_MS * 2 ** reconnectAttempt, RECONNECT_MAX_MS);
|
|
283
|
+
reconnectAttempt += 1;
|
|
284
|
+
reconnectTimer = deps.setTimeout(() => {
|
|
285
|
+
reconnectTimer = undefined;
|
|
286
|
+
const nextScope = currentScope();
|
|
287
|
+
if (!nextScope) return;
|
|
288
|
+
void connect(nextScope, generation);
|
|
289
|
+
}, delay);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function sendHelloWithFreshToken(
|
|
293
|
+
ws: WebSocketLike,
|
|
294
|
+
scope: SocketScope,
|
|
295
|
+
expectedGeneration: number,
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
let endpoint: AftRpcEndpoint | null = null;
|
|
298
|
+
try {
|
|
299
|
+
endpoint = await deps.createClient(scope.directory).resolveEndpoint();
|
|
300
|
+
} catch {
|
|
301
|
+
endpoint = null;
|
|
302
|
+
}
|
|
303
|
+
if (
|
|
304
|
+
closed ||
|
|
305
|
+
socket !== ws ||
|
|
306
|
+
expectedGeneration !== generation ||
|
|
307
|
+
!sameScope(currentScope(), scope)
|
|
308
|
+
) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
sendHello(ws, scope, endpoint?.token ?? null);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function sendHello(ws: WebSocketLike, scope: SocketScope, token: string | null): void {
|
|
315
|
+
helloedScope = scope;
|
|
316
|
+
if (socket === ws) socketScope = scope;
|
|
317
|
+
const lastReceivedId = lastHandledIdBySession.get(scope.sessionId) ?? 0;
|
|
318
|
+
ws.send(
|
|
319
|
+
JSON.stringify({
|
|
320
|
+
type: "hello",
|
|
321
|
+
token: token ?? "",
|
|
322
|
+
sessionId: scope.sessionId,
|
|
323
|
+
lastReceivedId,
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function handleSocketMessage(
|
|
329
|
+
ws: WebSocketLike,
|
|
330
|
+
raw: string,
|
|
331
|
+
messageGeneration: number,
|
|
332
|
+
): Promise<void> {
|
|
333
|
+
let msg: {
|
|
334
|
+
type?: string;
|
|
335
|
+
notification?: SocketNotification;
|
|
336
|
+
sessionId?: string;
|
|
337
|
+
error?: string;
|
|
338
|
+
};
|
|
339
|
+
try {
|
|
340
|
+
msg = JSON.parse(raw);
|
|
341
|
+
} catch {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (msg.type === "status-changed") {
|
|
346
|
+
for (const listener of statusInvalidationListeners) {
|
|
347
|
+
try {
|
|
348
|
+
listener({ sessionId: msg.sessionId });
|
|
349
|
+
} catch {
|
|
350
|
+
// One sidebar/dialog listener must not block the others.
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (msg.type === "notification" && msg.notification) {
|
|
357
|
+
const notification = msg.notification;
|
|
358
|
+
const active = opts?.getSessionId() ?? null;
|
|
359
|
+
if (!active) return;
|
|
360
|
+
if (notification.sessionId && notification.sessionId !== active) return;
|
|
361
|
+
|
|
362
|
+
let consumed = false;
|
|
363
|
+
try {
|
|
364
|
+
consumed = await Promise.resolve(opts?.onNotification(notification) ?? false);
|
|
365
|
+
} catch {
|
|
366
|
+
consumed = false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (
|
|
370
|
+
socket !== ws ||
|
|
371
|
+
messageGeneration !== generation ||
|
|
372
|
+
(opts?.getSessionId() ?? null) !== active
|
|
373
|
+
) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (consumed && notification.id > (lastHandledIdBySession.get(active) ?? 0)) {
|
|
377
|
+
lastHandledIdBySession.set(active, notification.id);
|
|
378
|
+
try {
|
|
379
|
+
ws.send(JSON.stringify({ type: "ack", lastReceivedId: notification.id }));
|
|
380
|
+
} catch {
|
|
381
|
+
// best-effort; reconnect hello replays the cursor.
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (msg.type === "error") {
|
|
388
|
+
try {
|
|
389
|
+
ws.close();
|
|
390
|
+
} catch {
|
|
391
|
+
// best-effort
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function __setAftTuiSocketDepsForTest(overrides: Partial<SocketDeps>): () => void {
|
|
397
|
+
const previous = deps;
|
|
398
|
+
deps = { ...deps, ...overrides };
|
|
399
|
+
return () => {
|
|
400
|
+
deps = previous;
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function __resetAftTuiSocketForTest(): void {
|
|
405
|
+
stopAftTuiSocket();
|
|
406
|
+
opts = null;
|
|
407
|
+
statusInvalidationListeners.clear();
|
|
408
|
+
lastHandledIdBySession.clear();
|
|
409
|
+
clients.clear();
|
|
410
|
+
reconnectAttempt = 0;
|
|
411
|
+
generation = 0;
|
|
412
|
+
connectingGeneration = null;
|
|
413
|
+
connectingScope = null;
|
|
414
|
+
closed = true;
|
|
415
|
+
deps = {
|
|
416
|
+
createClient: defaultClient,
|
|
417
|
+
WebSocketCtor: defaultWebSocketCtor(),
|
|
418
|
+
setTimeout,
|
|
419
|
+
clearTimeout,
|
|
420
|
+
};
|
|
421
|
+
}
|
package/src/tui/sidebar.tsx
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
// @ts-nocheck
|
|
3
3
|
|
|
4
|
-
// AFT sidebar slot
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// magic-context, so the panel stays current without polling.
|
|
4
|
+
// AFT sidebar slot. Header with "AFT" badge + version, then live status of search and semantic
|
|
5
|
+
// indexes plus their on-disk size. Refreshes on mount/session change and on
|
|
6
|
+
// server-pushed status invalidations with a small debounce, so the panel stays
|
|
7
|
+
// current without polling.
|
|
9
8
|
|
|
10
9
|
import { canonicalizeProjectRoot } from "@cortexkit/aft-bridge";
|
|
11
10
|
import type { TuiPluginApi, TuiSlotPlugin, TuiThemeCurrent } from "@opencode-ai/plugin/tui";
|
|
@@ -20,6 +19,11 @@ import {
|
|
|
20
19
|
type StatusCompression,
|
|
21
20
|
} from "../shared/status";
|
|
22
21
|
import { resolveCortexKitStorageRoot } from "../shared/storage-paths";
|
|
22
|
+
import {
|
|
23
|
+
createDebouncedStatusRefresh,
|
|
24
|
+
refreshAftTuiSocketScope,
|
|
25
|
+
subscribeStatusInvalidations,
|
|
26
|
+
} from "./notification-socket";
|
|
23
27
|
import {
|
|
24
28
|
type AftTuiPrefs,
|
|
25
29
|
computeEffectiveOrder,
|
|
@@ -35,10 +39,6 @@ import {
|
|
|
35
39
|
|
|
36
40
|
const SINGLE_BORDER = { type: "single" } as any;
|
|
37
41
|
const REFRESH_DEBOUNCE_MS = 200;
|
|
38
|
-
// The sidebar polls the bridge as a backstop because not every state change
|
|
39
|
-
// (e.g. semantic index transitioning from "loading" → "ready" mid-session)
|
|
40
|
-
// emits a session/message event. 1.5s matches the /aft-status dialog cadence.
|
|
41
|
-
const POLL_INTERVAL_MS = 1500;
|
|
42
42
|
|
|
43
43
|
function formatBytes(n: number): string {
|
|
44
44
|
if (!Number.isFinite(n) || n <= 0) return "—";
|
|
@@ -277,7 +277,7 @@ export function scopedSidebarSnapshot(
|
|
|
277
277
|
* mid-respawn after a binary swap, or a momentary session-dir key miss) arrives
|
|
278
278
|
* over RPC as `success: true`, so a naive `setStatus` would overwrite a good
|
|
279
279
|
* snapshot and collapse the panel to the lazy-bridge placeholder — the blank
|
|
280
|
-
* flicker that recovers on the next
|
|
280
|
+
* flicker that recovers on the next refresh. Suppress the downgrade only when we
|
|
281
281
|
* already hold initialized data for the same context; never blocks the first
|
|
282
282
|
* real snapshot, and a genuine context switch clears separately.
|
|
283
283
|
*/
|
|
@@ -353,8 +353,6 @@ const SidebarContent = (props: {
|
|
|
353
353
|
sessionID: string;
|
|
354
354
|
} | null = null;
|
|
355
355
|
let generation = 0;
|
|
356
|
-
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
357
|
-
let pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
358
356
|
|
|
359
357
|
const currentDirectory = () => props.api.state.path.directory ?? "";
|
|
360
358
|
const requestRender = () => {
|
|
@@ -443,6 +441,22 @@ const SidebarContent = (props: {
|
|
|
443
441
|
current.sessionID === sid &&
|
|
444
442
|
current.snapshot.cache_role !== "not_initialized";
|
|
445
443
|
if (shouldSuppressUninitializedDowngrade(snapshot.cache_role, haveGoodForContext)) return;
|
|
444
|
+
// Equality gate: a pushed invalidation can still produce the same
|
|
445
|
+
// snapshot (for example, a session-scoped status frame that does not
|
|
446
|
+
// affect this sidebar's visible fields). Minting a new status object
|
|
447
|
+
// would run SolidJS reactivity and schedule a host frame for no visible
|
|
448
|
+
// change. Skip the update when the freshly-fetched snapshot is
|
|
449
|
+
// byte-identical to what we already show for this exact context.
|
|
450
|
+
// JSON.stringify is sound here because the snapshot is a plain object
|
|
451
|
+
// coerced from the status RPC's JSON.
|
|
452
|
+
if (
|
|
453
|
+
current !== null &&
|
|
454
|
+
current.directory === directory &&
|
|
455
|
+
current.sessionID === sid &&
|
|
456
|
+
JSON.stringify(current.snapshot) === JSON.stringify(snapshot)
|
|
457
|
+
) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
446
460
|
setStatus({ directory, sessionID: sid, snapshot });
|
|
447
461
|
requestRender();
|
|
448
462
|
}
|
|
@@ -456,13 +470,8 @@ const SidebarContent = (props: {
|
|
|
456
470
|
}
|
|
457
471
|
};
|
|
458
472
|
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
debounceTimer = setTimeout(() => {
|
|
462
|
-
debounceTimer = undefined;
|
|
463
|
-
void refresh();
|
|
464
|
-
}, REFRESH_DEBOUNCE_MS);
|
|
465
|
-
};
|
|
473
|
+
const statusDebouncer = createDebouncedStatusRefresh(refresh, REFRESH_DEBOUNCE_MS);
|
|
474
|
+
const scheduleRefresh = () => statusDebouncer.schedule();
|
|
466
475
|
|
|
467
476
|
const reloadPrefs = async () => {
|
|
468
477
|
const root = await readTuiPreferencesFile();
|
|
@@ -481,58 +490,33 @@ const SidebarContent = (props: {
|
|
|
481
490
|
unwatchPrefs();
|
|
482
491
|
generation++;
|
|
483
492
|
abortInflight();
|
|
484
|
-
|
|
485
|
-
if (pollTimer) clearInterval(pollTimer);
|
|
493
|
+
statusDebouncer.dispose();
|
|
486
494
|
});
|
|
487
495
|
|
|
488
496
|
// Refresh on session id change + initial load
|
|
489
497
|
createEffect(
|
|
490
498
|
on(props.sessionID, () => {
|
|
499
|
+
refreshAftTuiSocketScope();
|
|
491
500
|
void refresh();
|
|
492
501
|
}),
|
|
493
502
|
);
|
|
494
503
|
|
|
495
|
-
// Wire live updates:
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
// recompute disk usage on every keystroke.
|
|
504
|
+
// Wire live updates: the server pushes a lightweight invalidation whenever
|
|
505
|
+
// the bridge reports a status change. The sidebar coalesces bursts into one
|
|
506
|
+
// trailing status fetch and stays completely idle when no backend state changes.
|
|
499
507
|
createEffect(
|
|
500
508
|
on(
|
|
501
509
|
props.sessionID,
|
|
502
510
|
(sessionID) => {
|
|
503
511
|
if (!sessionID) return;
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}),
|
|
509
|
-
props.api.event.on("session.updated", (event) => {
|
|
510
|
-
if (event.properties?.info?.id !== sessionID) return;
|
|
511
|
-
scheduleRefresh();
|
|
512
|
-
}),
|
|
513
|
-
];
|
|
514
|
-
// Background poller for state that doesn't emit session events
|
|
515
|
-
// (semantic index `loading` → `ready`, disk size growth during
|
|
516
|
-
// a background indexer rebuild). Self-cancelling on cleanup.
|
|
517
|
-
if (!pollTimer) {
|
|
518
|
-
pollTimer = setInterval(() => {
|
|
519
|
-
scheduleRefresh();
|
|
520
|
-
}, POLL_INTERVAL_MS);
|
|
521
|
-
}
|
|
512
|
+
const unsubscribe = subscribeStatusInvalidations((event) => {
|
|
513
|
+
if (event.sessionId && event.sessionId !== props.sessionID()) return;
|
|
514
|
+
scheduleRefresh();
|
|
515
|
+
});
|
|
522
516
|
onCleanup(() => {
|
|
523
|
-
|
|
524
|
-
try {
|
|
525
|
-
unsub();
|
|
526
|
-
} catch {
|
|
527
|
-
// best effort
|
|
528
|
-
}
|
|
529
|
-
}
|
|
517
|
+
unsubscribe();
|
|
530
518
|
generation++;
|
|
531
519
|
abortInflight();
|
|
532
|
-
if (pollTimer) {
|
|
533
|
-
clearInterval(pollTimer);
|
|
534
|
-
pollTimer = undefined;
|
|
535
|
-
}
|
|
536
520
|
});
|
|
537
521
|
},
|
|
538
522
|
{ defer: false },
|
package/dist/patch-parser.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal patch parser for the opencode `*** Begin Patch` format.
|
|
3
|
-
* Ported from opencode's internal Patch module.
|
|
4
|
-
*/
|
|
5
|
-
export interface AddHunk {
|
|
6
|
-
type: "add";
|
|
7
|
-
path: string;
|
|
8
|
-
contents: string;
|
|
9
|
-
}
|
|
10
|
-
export interface DeleteHunk {
|
|
11
|
-
type: "delete";
|
|
12
|
-
path: string;
|
|
13
|
-
}
|
|
14
|
-
export interface UpdateFileChunk {
|
|
15
|
-
old_lines: string[];
|
|
16
|
-
new_lines: string[];
|
|
17
|
-
change_context?: string;
|
|
18
|
-
is_end_of_file?: boolean;
|
|
19
|
-
}
|
|
20
|
-
export interface UpdateHunk {
|
|
21
|
-
type: "update";
|
|
22
|
-
path: string;
|
|
23
|
-
move_path?: string;
|
|
24
|
-
chunks: UpdateFileChunk[];
|
|
25
|
-
}
|
|
26
|
-
export type Hunk = AddHunk | DeleteHunk | UpdateHunk;
|
|
27
|
-
export declare function parsePatch(patchText: string): Hunk[];
|
|
28
|
-
/**
|
|
29
|
-
* Apply update chunks to the original file content, producing new content.
|
|
30
|
-
* Ported from opencode's deriveNewContentsFromChunks.
|
|
31
|
-
*/
|
|
32
|
-
export declare function applyUpdateChunks(originalContent: string, filePath: string, chunks: UpdateFileChunk[]): string;
|
|
33
|
-
//# sourceMappingURL=patch-parser.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"patch-parser.d.ts","sourceRoot":"","sources":["../src/patch-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,KAAK,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,UAAU,GAAG,UAAU,CAAC;AAqHrD,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,CAqDpD;AAwQD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,eAAe,EAAE,GACxB,MAAM,CAmHR"}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export type OpenCodeBinaryType = "opencode" | "opencode-desktop";
|
|
2
|
-
export interface OpenCodeConfigDirOptions {
|
|
3
|
-
binary: OpenCodeBinaryType;
|
|
4
|
-
version?: string | null;
|
|
5
|
-
checkExisting?: boolean;
|
|
6
|
-
}
|
|
7
|
-
export interface OpenCodeConfigPaths {
|
|
8
|
-
configDir: string;
|
|
9
|
-
configJson: string;
|
|
10
|
-
configJsonc: string;
|
|
11
|
-
packageJson: string;
|
|
12
|
-
omoConfig: string;
|
|
13
|
-
}
|
|
14
|
-
export declare function getOpenCodeConfigDir(_options: OpenCodeConfigDirOptions): string;
|
|
15
|
-
export declare function getOpenCodeConfigPaths(options: OpenCodeConfigDirOptions): OpenCodeConfigPaths;
|
|
16
|
-
//# sourceMappingURL=opencode-config-dir.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"opencode-config-dir.d.ts","sourceRoot":"","sources":["../../src/shared/opencode-config-dir.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,kBAAkB,CAAC;AAEjE,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAeD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,wBAAwB,GAAG,MAAM,CAE/E;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,wBAAwB,GAAG,mBAAmB,CAS7F"}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
|
-
import { Terminal } from "@xterm/headless";
|
|
3
|
-
export interface PtyTerminalState {
|
|
4
|
-
terminal: Terminal;
|
|
5
|
-
fileHandle: fs.FileHandle;
|
|
6
|
-
offset: number;
|
|
7
|
-
rows: number;
|
|
8
|
-
cols: number;
|
|
9
|
-
lastAccessMs: number;
|
|
10
|
-
}
|
|
11
|
-
export declare function getOrCreatePtyTerminal(key: string, outputPath: string, rows?: number, cols?: number): Promise<PtyTerminalState>;
|
|
12
|
-
export declare function readPtyBytes(state: PtyTerminalState): Promise<Buffer>;
|
|
13
|
-
export declare function disposePtyTerminal(key: string): Promise<void>;
|
|
14
|
-
export declare function disposeAllPtyTerminals(): Promise<void>;
|
|
15
|
-
export declare function renderScreen(state: PtyTerminalState, rows?: number, cols?: number): string;
|
|
16
|
-
export declare function __resetPtyCacheForTests(): void;
|
|
17
|
-
export declare function __ptyCacheSizeForTests(): number;
|
|
18
|
-
//# sourceMappingURL=pty-cache.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pty-cache.d.ts","sourceRoot":"","sources":["../../src/shared/pty-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAM3C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAID,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,IAAI,SAAe,EACnB,IAAI,SAAe,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAuB3B;AAED,wBAAsB,YAAY,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAa3E;AAED,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMnE;AAED,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAE5D;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,gBAAgB,EACvB,IAAI,SAAa,EACjB,IAAI,SAAa,GAChB,MAAM,CAiBR;AAED,wBAAgB,uBAAuB,IAAI,IAAI,CAM9C;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tui-config.d.ts","sourceRoot":"","sources":["../../src/shared/tui-config.ts"],"names":[],"mappings":"AAmBA,wBAAgB,oBAAoB,IAAI,OAAO,CAsC9C"}
|