@cortexkit/aft-opencode 0.62.0 → 0.63.4

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 (85) hide show
  1. package/dist/bash-wait-detach.d.ts +1 -0
  2. package/dist/bash-wait-detach.d.ts.map +1 -1
  3. package/dist/bg-notifications.d.ts +30 -11
  4. package/dist/bg-notifications.d.ts.map +1 -1
  5. package/dist/bridge-bootstrap.d.ts +1 -1
  6. package/dist/bridge-bootstrap.d.ts.map +1 -1
  7. package/dist/config-live-reload.d.ts +14 -2
  8. package/dist/config-live-reload.d.ts.map +1 -1
  9. package/dist/config.d.ts +37 -8
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/entry/server.js +4015 -1382
  12. package/dist/hooks/auto-update-checker/index.d.ts +5 -1
  13. package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3294 -1243
  16. package/dist/logger.d.ts +6 -10
  17. package/dist/logger.d.ts.map +1 -1
  18. package/dist/lsp-auto-install.d.ts.map +1 -1
  19. package/dist/lsp.d.ts +1 -0
  20. package/dist/lsp.d.ts.map +1 -1
  21. package/dist/shared/rpc-client.d.ts.map +1 -1
  22. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  23. package/dist/shared/rpc-server.d.ts +2 -0
  24. package/dist/shared/rpc-server.d.ts.map +1 -1
  25. package/dist/shared/session-cleanup.d.ts +6 -0
  26. package/dist/shared/session-cleanup.d.ts.map +1 -0
  27. package/dist/shared/session-directory.d.ts +24 -0
  28. package/dist/shared/session-directory.d.ts.map +1 -1
  29. package/dist/shared/status.d.ts +12 -1
  30. package/dist/shared/status.d.ts.map +1 -1
  31. package/dist/shared/subagent-detect.d.ts +24 -8
  32. package/dist/shared/subagent-detect.d.ts.map +1 -1
  33. package/dist/shared/wake-admission.d.ts +68 -0
  34. package/dist/shared/wake-admission.d.ts.map +1 -0
  35. package/dist/shutdown-hooks.d.ts +2 -0
  36. package/dist/shutdown-hooks.d.ts.map +1 -1
  37. package/dist/subc-tool-schemas.d.ts.map +1 -1
  38. package/dist/tools/_shared.d.ts +5 -1
  39. package/dist/tools/_shared.d.ts.map +1 -1
  40. package/dist/tools/bash.d.ts +1 -0
  41. package/dist/tools/bash.d.ts.map +1 -1
  42. package/dist/tools/bash_watch.d.ts +4 -0
  43. package/dist/tools/bash_watch.d.ts.map +1 -1
  44. package/dist/tools/definitions/v2.d.ts +2 -1
  45. package/dist/tools/definitions/v2.d.ts.map +1 -1
  46. package/dist/tools/hoisted.d.ts.map +1 -1
  47. package/dist/tools/inspect.d.ts.map +1 -1
  48. package/dist/tools/metadata.d.ts +7 -0
  49. package/dist/tools/metadata.d.ts.map +1 -0
  50. package/dist/tools/outside-permissions.d.ts +35 -0
  51. package/dist/tools/outside-permissions.d.ts.map +1 -0
  52. package/dist/tools/permissions.d.ts +9 -11
  53. package/dist/tools/permissions.d.ts.map +1 -1
  54. package/dist/tools/reading.d.ts.map +1 -1
  55. package/dist/tools/search.d.ts.map +1 -1
  56. package/dist/tools/semantic.d.ts.map +1 -1
  57. package/dist/tui/notification-socket.d.ts.map +1 -1
  58. package/dist/tui/preferences.d.ts +11 -0
  59. package/dist/tui/preferences.d.ts.map +1 -1
  60. package/dist/v2-session-lifecycle.d.ts +7 -0
  61. package/dist/v2-session-lifecycle.d.ts.map +1 -1
  62. package/dist/v2-tool-hooks.d.ts.map +1 -1
  63. package/dist/wakes/runtime-consumer.d.ts +9 -3
  64. package/dist/wakes/runtime-consumer.d.ts.map +1 -1
  65. package/dist/wakes/session-delivery.d.ts +22 -4
  66. package/dist/wakes/session-delivery.d.ts.map +1 -1
  67. package/package.json +8 -8
  68. package/src/entry/server-runtime.mjs +53 -18
  69. package/src/logger.ts +12 -5
  70. package/src/shared/rpc-client.ts +35 -28
  71. package/src/shared/rpc-notifications.ts +5 -3
  72. package/src/shared/rpc-server.ts +65 -7
  73. package/src/shared/session-cleanup.ts +13 -0
  74. package/src/shared/session-directory.ts +59 -3
  75. package/src/shared/status.ts +32 -2
  76. package/src/shared/subagent-detect.ts +53 -25
  77. package/src/shared/wake-admission.ts +340 -0
  78. package/src/tui/notification-socket.ts +30 -4
  79. package/src/tui/preferences.ts +66 -29
  80. package/src/tui/sidebar-view.tsx +1 -0
  81. package/src/tui/v2.tsx +49 -1
  82. package/src/tui-compiled/notification-socket.ts +30 -4
  83. package/src/tui-compiled/preferences.ts +66 -29
  84. package/src/tui-compiled/sidebar-view.tsx +2 -1
  85. package/src/tui-compiled/v2.tsx +54 -1
@@ -49,6 +49,14 @@ const WEB_SOCKET_OPEN = 1;
49
49
  const clients = new Map<string, AftRpcClient>();
50
50
  const statusInvalidationListeners = new Set<(event: StatusInvalidation) => void>();
51
51
  const lastHandledIdBySession = new Map<string, number>();
52
+ const socketEndpoints = new WeakMap<WebSocketLike, AftRpcEndpoint>();
53
+ const CURSOR_CACHE_LIMIT = 200;
54
+
55
+ function cursorKey(ws: WebSocketLike, sessionId: string): string | null {
56
+ const endpoint = socketEndpoints.get(ws);
57
+ // The authentication token distinguishes server instances, even when a new instance reuses the port.
58
+ return endpoint?.token ? `${endpoint.token}\0${sessionId}` : null;
59
+ }
52
60
 
53
61
  function defaultWebSocketCtor(): WebSocketFactory | null {
54
62
  const ctor = (globalThis as typeof globalThis & { WebSocket?: WebSocketFactory }).WebSocket;
@@ -128,6 +136,7 @@ export function stopAftTuiSocket(): void {
128
136
  closeCurrentSocket(false);
129
137
  for (const client of clients.values()) client.reset();
130
138
  clients.clear();
139
+ lastHandledIdBySession.clear();
131
140
  reconnectAttempt = 0;
132
141
  }
133
142
 
@@ -250,6 +259,7 @@ async function connect(scope: SocketScope, connectGeneration: number): Promise<v
250
259
  }
251
260
 
252
261
  socket = ws;
262
+ socketEndpoints.set(ws, endpoint);
253
263
  socketScope = scope;
254
264
  helloedScope = null;
255
265
 
@@ -335,13 +345,21 @@ async function sendHelloWithFreshToken(
335
345
  ) {
336
346
  return;
337
347
  }
338
- sendHello(ws, scope, endpoint?.token ?? null);
348
+ const connected = socketEndpoints.get(ws);
349
+ if (!endpoint || endpoint.port !== connected?.port || endpoint.token !== connected.token) {
350
+ generation += 1;
351
+ closeCurrentSocket(false);
352
+ void connect(scope, generation);
353
+ return;
354
+ }
355
+ sendHello(ws, scope, connected.token);
339
356
  }
340
357
 
341
358
  function sendHello(ws: WebSocketLike, scope: SocketScope, token: string | null): void {
342
359
  helloedScope = scope;
343
360
  if (socket === ws) socketScope = scope;
344
- const lastReceivedId = lastHandledIdBySession.get(scope.sessionId) ?? 0;
361
+ const key = cursorKey(ws, scope.sessionId);
362
+ const lastReceivedId = key ? (lastHandledIdBySession.get(key) ?? 0) : 0;
345
363
  ws.send(
346
364
  JSON.stringify({
347
365
  type: "hello",
@@ -400,8 +418,16 @@ async function handleSocketMessage(
400
418
  ) {
401
419
  return;
402
420
  }
403
- if (consumed && notification.id > (lastHandledIdBySession.get(active) ?? 0)) {
404
- lastHandledIdBySession.set(active, notification.id);
421
+ const key = cursorKey(ws, active);
422
+ if (consumed && notification.id > (key ? (lastHandledIdBySession.get(key) ?? 0) : 0)) {
423
+ if (key) {
424
+ lastHandledIdBySession.delete(key);
425
+ lastHandledIdBySession.set(key, notification.id);
426
+ if (lastHandledIdBySession.size > CURSOR_CACHE_LIMIT) {
427
+ const oldest = lastHandledIdBySession.keys().next().value;
428
+ if (oldest !== undefined) lastHandledIdBySession.delete(oldest);
429
+ }
430
+ }
405
431
  try {
406
432
  ws.send(JSON.stringify({ type: "ack", lastReceivedId: notification.id }));
407
433
  } catch {
@@ -39,6 +39,12 @@ export interface AftTuiPrefs {
39
39
  startCollapsed: boolean;
40
40
  rememberCollapsed: boolean;
41
41
  collapsed: boolean | null;
42
+ /**
43
+ * Whether OpenCode 2 shows AFT's health line (`AFT E0 W0 | D0 U0 C0 | T0`)
44
+ * in the prompt footer. Off hides it entirely; the sidebar and the status
45
+ * dialog are unaffected.
46
+ */
47
+ footer: boolean;
42
48
  header: {
43
49
  label: string;
44
50
  showVersion: boolean;
@@ -57,6 +63,7 @@ export const DEFAULT_PREFS: AftTuiPrefs = {
57
63
  startCollapsed: false,
58
64
  rememberCollapsed: true,
59
65
  collapsed: null,
66
+ footer: true,
60
67
  header: { label: "AFT", showVersion: true },
61
68
  sections: {
62
69
  searchIndex: true,
@@ -99,6 +106,7 @@ export function resolveAftPrefs(root: Record<string, unknown>): AftTuiPrefs {
99
106
  startCollapsed: bool(entry.startCollapsed, d.startCollapsed),
100
107
  rememberCollapsed: bool(entry.rememberCollapsed, d.rememberCollapsed),
101
108
  collapsed: typeof entry.collapsed === "boolean" ? entry.collapsed : null,
109
+ footer: bool(entry.footer, d.footer),
102
110
  header: {
103
111
  label: label(header.label, d.header.label, 20),
104
112
  showVersion: bool(header.showVersion, d.header.showVersion),
@@ -161,18 +169,14 @@ async function writePreference(pluginKey: string, path: string[], value: JsonVal
161
169
  let text: string;
162
170
  try {
163
171
  text = await readFile(file, "utf8");
164
- } catch {
165
- text = "";
166
- }
167
- if (text.trim() === "") text = TEMPLATE;
168
-
169
- let root: Record<string, unknown>;
170
- try {
171
- const parsed = parse(text);
172
- root = isRecord(parsed) ? (parsed as Record<string, unknown>) : {};
173
- } catch {
174
- root = {};
172
+ } catch (error) {
173
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
174
+ text = TEMPLATE;
175
175
  }
176
+ // Only a missing file can be initialized. Invalid existing bytes may belong to another plugin.
177
+ const parsed: unknown = parse(text);
178
+ if (!isRecord(parsed)) throw new Error("TUI preferences must contain an object");
179
+ const root = parsed;
176
180
 
177
181
  if (!isRecord(root[pluginKey])) {
178
182
  root[pluginKey] = {};
@@ -193,22 +197,52 @@ export function queueTuiPreferenceUpdate(
193
197
  path: string[],
194
198
  value: JsonValue,
195
199
  ): Promise<void> {
196
- writeChain = writeChain.then(() => writePreference(pluginKey, path, value)).catch(() => {});
197
- return writeChain;
200
+ const update = writeChain.then(() => writePreference(pluginKey, path, value));
201
+ // Keep subsequent writes usable while returning the actual failure to this caller.
202
+ writeChain = update.catch(() => {});
203
+ return update;
198
204
  }
199
205
 
200
206
  const WATCH_DEBOUNCE_MS = 150;
201
207
 
208
+ /**
209
+ * What the watcher last saw where the preferences file should be: its text,
210
+ * or FILE_ABSENT once a read has confirmed the file does not exist. A symbol
211
+ * can never equal any file text, so deleting the file always counts as a
212
+ * change, even from an empty file.
213
+ */
214
+ const FILE_ABSENT: unique symbol = Symbol("tui-preferences file absent");
215
+ type WatchedState = string | typeof FILE_ABSENT;
216
+
217
+ /**
218
+ * Read the file for the watcher. Returns FILE_ABSENT only for ENOENT; any
219
+ * other failure (a permission error, a directory at the path) returns null,
220
+ * meaning "unknown", so a file that is present but momentarily unreadable
221
+ * never makes callers fall back to defaults.
222
+ */
223
+ async function readWatchedState(file: string): Promise<WatchedState | null> {
224
+ try {
225
+ return await readFile(file, "utf8");
226
+ } catch (error) {
227
+ return (error as NodeJS.ErrnoException).code === "ENOENT" ? FILE_ABSENT : null;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Calls `onChange` after the preferences file's content changes, including
233
+ * when the file is deleted (callers then read defaults) and when it is
234
+ * created again.
235
+ */
202
236
  export function watchTuiPreferences(onChange: () => void): () => void {
203
237
  const file = getTuiPreferencesFile();
204
238
  const name = basename(file);
205
239
  let timer: ReturnType<typeof setTimeout> | null = null;
206
- let lastSeen: string | null = null;
207
- void readFile(file, "utf8")
208
- .then((text) => {
209
- if (lastSeen === null) lastSeen = text;
210
- })
211
- .catch(() => {});
240
+ // null until a read has succeeded or confirmed the file is missing.
241
+ let lastSeen: WatchedState | null = null;
242
+ let stopped = false;
243
+ void readWatchedState(file).then((state) => {
244
+ if (lastSeen === null && state !== null) lastSeen = state;
245
+ });
212
246
  try {
213
247
  const watcher = watch(dirname(file), (_event, filename) => {
214
248
  const isOurs =
@@ -217,20 +251,23 @@ export function watchTuiPreferences(onChange: () => void): () => void {
217
251
  if (timer) clearTimeout(timer);
218
252
  timer = setTimeout(() => {
219
253
  timer = null;
220
- void readFile(file, "utf8")
221
- .catch(() => null)
222
- .then((text) => {
223
- if (text === null) return;
224
- if (text === lastSeen) return;
225
- lastSeen = text;
226
- onChange();
227
- });
254
+ void readWatchedState(file).then((state) => {
255
+ if (stopped || state === null) return;
256
+ if (state === lastSeen) return;
257
+ lastSeen = state;
258
+ onChange();
259
+ });
228
260
  }, WATCH_DEBOUNCE_MS);
229
261
  });
230
- return () => {
262
+ const stop = () => {
263
+ if (stopped) return;
264
+ stopped = true;
231
265
  if (timer) clearTimeout(timer);
266
+ timer = null;
232
267
  watcher.close();
233
268
  };
269
+ watcher.on("error", stop);
270
+ return stop;
234
271
  } catch {
235
272
  return () => {};
236
273
  }
@@ -238,6 +275,6 @@ export function watchTuiPreferences(onChange: () => void): () => void {
238
275
 
239
276
  export function persistCollapsedIfEnabled(prefs: AftTuiPrefs, collapsed: boolean): void {
240
277
  if (prefs.rememberCollapsed) {
241
- void queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], collapsed);
278
+ void queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], collapsed).catch(() => {});
242
279
  }
243
280
  }
@@ -399,7 +399,8 @@ export const AftSidebarPanel = props => {
399
399
  stage: s()?.semantic_index?.stage,
400
400
  error: s()?.semantic_index?.error,
401
401
  reason: s()?.semantic_index?.reason,
402
- backend_url: s()?.semantic_index?.backend_url
402
+ backend_url: s()?.semantic_index?.backend_url,
403
+ pending_reason: s()?.semantic_index?.pending_reason
403
404
  })
404
405
  };
405
406
  };
@@ -1,4 +1,5 @@
1
1
  import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid";
2
+ import { memo as _$memo } from "opentui:runtime-module:%40opentui%2Fsolid";
2
3
  import { insert as _$insert } from "opentui:runtime-module:%40opentui%2Fsolid";
3
4
  import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
4
5
  /** @jsxImportSource @opentui/solid */
@@ -8,6 +9,7 @@ import { createEffect, createSignal, onCleanup } from "opentui:runtime-module:so
8
9
  import { version as packageVersion } from "../../package.json";
9
10
  import { coerceAftStatus, formatStatusDialogMessage } from "../shared/status";
10
11
  import { AftRpc } from "./aft-rpc";
12
+ import { readTuiPreferencesFile, resolveAftPrefs, watchTuiPreferences } from "./preferences";
11
13
  import { AftSidebarPanel, createSidebarPreferences } from "./sidebar-view";
12
14
  import { formatAftStatusSegment } from "./v2-status";
13
15
 
@@ -163,7 +165,38 @@ function useAftStatus(context, rpc, sessionID) {
163
165
  });
164
166
  return status;
165
167
  }
166
- function FooterStatus(props) {
168
+
169
+ /**
170
+ * The `footer` switch from the `aft` entry of `tui-preferences.jsonc`, kept
171
+ * current while the file is edited, as the sidebar's preferences are.
172
+ *
173
+ * It reads as hidden until the file has been read once. Starting from the
174
+ * default (shown) instead would flash the line on every prompt mount for
175
+ * someone who turned it off; the read takes milliseconds, and a missing or
176
+ * unreadable file resolves to the default, so the line still appears for
177
+ * everyone else.
178
+ */
179
+ function createFooterVisibility() {
180
+ const [visible, setVisible] = createSignal(false);
181
+ // Reads can overlap when the file changes twice in quick succession; only
182
+ // the most recently started read may set the value.
183
+ let generation = 0;
184
+ const reload = async () => {
185
+ const current = ++generation;
186
+ const prefs = resolveAftPrefs(await readTuiPreferencesFile());
187
+ if (current === generation) setVisible(prefs.footer);
188
+ };
189
+ void reload();
190
+ const unwatch = watchTuiPreferences(() => {
191
+ void reload();
192
+ });
193
+ onCleanup(() => {
194
+ generation += 1;
195
+ unwatch();
196
+ });
197
+ return visible;
198
+ }
199
+ function FooterSegment(props) {
167
200
  const status = useAftStatus(props.context, props.rpc, () => props.sessionID);
168
201
  return (() => {
169
202
  var _el$ = _$createElement("text");
@@ -171,6 +204,26 @@ function FooterStatus(props) {
171
204
  return _el$;
172
205
  })();
173
206
  }
207
+
208
+ /**
209
+ * The prompt footer claim. With `footer: false` it renders no element at all,
210
+ * so the line takes no space, and the status subscription behind it is torn
211
+ * down until the switch is turned back on.
212
+ */
213
+ function FooterStatus(props) {
214
+ const visible = createFooterVisibility();
215
+ return _$memo(() => _$memo(() => !!visible())() ? _$createComponent(FooterSegment, {
216
+ get context() {
217
+ return props.context;
218
+ },
219
+ get rpc() {
220
+ return props.rpc;
221
+ },
222
+ get sessionID() {
223
+ return props.sessionID;
224
+ }
225
+ }) : null);
226
+ }
174
227
  function SidebarStatus(props) {
175
228
  const status = useAftStatus(props.context, props.rpc, () => props.sessionID);
176
229
  const preferences = createSidebarPreferences();