@cortexkit/aft-opencode 0.42.0 → 0.43.1

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 (59) hide show
  1. package/dist/bg-notifications.d.ts +35 -0
  2. package/dist/bg-notifications.d.ts.map +1 -1
  3. package/dist/config.d.ts +7 -3
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/configure-warnings.d.ts +3 -3
  6. package/dist/configure-warnings.d.ts.map +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +6092 -9528
  9. package/dist/notifications.d.ts +2 -2
  10. package/dist/notifications.d.ts.map +1 -1
  11. package/dist/shared/rpc-client.d.ts +6 -0
  12. package/dist/shared/rpc-client.d.ts.map +1 -1
  13. package/dist/shared/rpc-notifications.d.ts +29 -15
  14. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  15. package/dist/shared/rpc-server.d.ts +10 -1
  16. package/dist/shared/rpc-server.d.ts.map +1 -1
  17. package/dist/subc-tool-schemas.d.ts +1 -1
  18. package/dist/subc-tool-schemas.d.ts.map +1 -1
  19. package/dist/tools/_shared.d.ts +12 -2
  20. package/dist/tools/_shared.d.ts.map +1 -1
  21. package/dist/tools/ast.d.ts.map +1 -1
  22. package/dist/tools/bash.d.ts.map +1 -1
  23. package/dist/tools/bash_watch.d.ts.map +1 -1
  24. package/dist/tools/hoisted.d.ts.map +1 -1
  25. package/dist/tools/imports.d.ts.map +1 -1
  26. package/dist/tools/inspect.d.ts +0 -1
  27. package/dist/tools/inspect.d.ts.map +1 -1
  28. package/dist/tools/navigation.d.ts.map +1 -1
  29. package/dist/tools/permissions.d.ts +23 -13
  30. package/dist/tools/permissions.d.ts.map +1 -1
  31. package/dist/tools/reading.d.ts +0 -1
  32. package/dist/tools/reading.d.ts.map +1 -1
  33. package/dist/tools/refactoring.d.ts.map +1 -1
  34. package/dist/tools/safety.d.ts.map +1 -1
  35. package/dist/tools/search.d.ts.map +1 -1
  36. package/dist/tools/semantic.d.ts.map +1 -1
  37. package/dist/tui/notification-socket.d.ts +41 -0
  38. package/dist/tui/notification-socket.d.ts.map +1 -0
  39. package/dist/tui.js +2460 -187
  40. package/dist/types.d.ts +4 -2
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +8 -9
  43. package/src/shared/rpc-client.ts +9 -0
  44. package/src/shared/rpc-notifications.ts +97 -41
  45. package/src/shared/rpc-server.ts +286 -67
  46. package/src/tui/index.tsx +77 -125
  47. package/src/tui/notification-socket.ts +421 -0
  48. package/src/tui/sidebar.tsx +38 -54
  49. package/dist/patch-parser.d.ts +0 -33
  50. package/dist/patch-parser.d.ts.map +0 -1
  51. package/dist/shared/opencode-config-dir.d.ts +0 -16
  52. package/dist/shared/opencode-config-dir.d.ts.map +0 -1
  53. package/dist/shared/pty-cache.d.ts +0 -18
  54. package/dist/shared/pty-cache.d.ts.map +0 -1
  55. package/dist/shared/tui-config.d.ts +0 -2
  56. package/dist/shared/tui-config.d.ts.map +0 -1
  57. package/src/shared/opencode-config-dir.ts +0 -46
  58. package/src/shared/pty-cache.ts +0 -113
  59. package/src/shared/tui-config.ts +0 -58
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BridgePool } from "@cortexkit/aft-bridge";
1
+ import type { AftTransportPool } from "@cortexkit/aft-bridge";
2
2
  import type { PluginInput } from "@opencode-ai/plugin";
3
3
  import type { AftConfig } from "./config.js";
4
4
  interface ShellEnvPluginHost {
@@ -19,12 +19,14 @@ interface ShellEnvPluginHost {
19
19
  * Bundles the binary bridge, the OpenCode SDK client, and plugin config.
20
20
  */
21
21
  export interface PluginContext {
22
- pool: BridgePool;
22
+ pool: AftTransportPool;
23
23
  client: PluginInput["client"];
24
24
  plugin?: ShellEnvPluginHost;
25
25
  config: AftConfig;
26
26
  /** Absolute path to AFT's storage dir (e.g. ~/.local/share/opencode/storage/plugin/aft) */
27
27
  storageDir: string;
28
+ /** Returns false when user and project config merge to `enabled: false` for this project. */
29
+ isProjectEnabled?: (projectRoot: string) => boolean;
28
30
  }
29
31
  export {};
30
32
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,UAAU,kBAAkB;IAC1B,OAAO,CAAC,EAAE,CACR,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,EAC7D,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,KACnC,OAAO,CAAC;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC,GAAG;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;CACnF;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,2FAA2F;IAC3F,UAAU,EAAE,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,UAAU,kBAAkB;IAC1B,OAAO,CAAC,EAAE,CACR,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,EAC7D,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,KACnC,OAAO,CAAC;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC,GAAG;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;CACnF;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,2FAA2F;IAC3F,UAAU,EAAE,MAAM,CAAC;IACnB,6FAA6F;IAC7F,gBAAgB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC;CACrD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.42.0",
3
+ "version": "0.43.1",
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",
@@ -31,22 +31,21 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@clack/prompts": "^1.6.0",
34
- "@cortexkit/aft-bridge": "0.42.0",
34
+ "@cortexkit/aft-bridge": "0.43.1",
35
35
  "@opentui/core": "^0.4.2",
36
36
  "@opentui/solid": "^0.4.2",
37
- "@xterm/headless": "^5.5.0",
38
37
  "comment-json": "^4.6.2",
39
38
  "solid-js": "1.9.12",
40
39
  "undici": "^7.25.0",
41
40
  "zod": "^4.4.3"
42
41
  },
43
42
  "optionalDependencies": {
44
- "@cortexkit/aft-darwin-arm64": "0.42.0",
45
- "@cortexkit/aft-darwin-x64": "0.42.0",
46
- "@cortexkit/aft-linux-arm64": "0.42.0",
47
- "@cortexkit/aft-linux-x64": "0.42.0",
48
- "@cortexkit/aft-win32-arm64": "0.42.0",
49
- "@cortexkit/aft-win32-x64": "0.42.0"
43
+ "@cortexkit/aft-darwin-arm64": "0.43.1",
44
+ "@cortexkit/aft-darwin-x64": "0.43.1",
45
+ "@cortexkit/aft-linux-arm64": "0.43.1",
46
+ "@cortexkit/aft-linux-x64": "0.43.1",
47
+ "@cortexkit/aft-win32-arm64": "0.43.1",
48
+ "@cortexkit/aft-win32-x64": "0.43.1"
50
49
  },
51
50
  "devDependencies": {
52
51
  "@opencode-ai/plugin": "^1.17.11",
@@ -10,6 +10,8 @@ type PortInfoSource = "instance" | "legacy";
10
10
  type ParsedPortInfo = { port: number; token: string | null; pid?: number; started_at?: number };
11
11
  type PortInfo = ParsedPortInfo & { source: PortInfoSource; path?: string };
12
12
 
13
+ export type AftRpcEndpoint = { port: number; token: string | null };
14
+
13
15
  export interface AftRpcCallOptions {
14
16
  signal?: AbortSignal;
15
17
  /**
@@ -157,6 +159,13 @@ export class AftRpcClient {
157
159
  }
158
160
  }
159
161
 
162
+ /** Resolve the freshest live endpoint for the persistent TUI WebSocket. */
163
+ async resolveEndpoint(signal?: AbortSignal): Promise<AftRpcEndpoint | null> {
164
+ const [info] = await this.resolvePortInfos(signal);
165
+ if (!info) return null;
166
+ return { port: info.port, token: info.token };
167
+ }
168
+
160
169
  /**
161
170
  * Discover all live RPC port files for this project. Tries the per-instance
162
171
  * directory first (v0.28.2+), then the single legacy `port` file (older
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * In-memory notification queue for server → TUI push.
3
3
  *
4
- * Also tracks whether a TUI client is actively connected (polling).
5
- * The server plugin cannot use `process.env.OPENCODE_CLIENT` to detect TUI
6
- * because the server runs in a separate process from the TUI client.
4
+ * The queue is the durable at-least-once buffer; live WebSocket sinks are only
5
+ * a low-latency delivery path. A notification is pruned only after the TUI acks
6
+ * it with the highest id it handled.
7
7
  */
8
8
 
9
9
  export interface RpcNotification {
@@ -13,33 +13,80 @@ export interface RpcNotification {
13
13
  sessionId?: string;
14
14
  }
15
15
 
16
+ export interface NotificationSink {
17
+ /** The TUI session that the sink's socket authenticated for. */
18
+ sessionId?: string;
19
+ /** Deliver one queued notification over the live socket. */
20
+ send: (notification: RpcNotification) => void;
21
+ }
22
+
23
+ export interface StatusChangeSink {
24
+ /** The TUI session that the sink's socket authenticated for. */
25
+ sessionId?: string;
26
+ /** Deliver a lightweight status invalidation over the live socket. */
27
+ send: (event: RpcStatusChange) => void;
28
+ }
29
+
30
+ export interface RpcStatusChange {
31
+ sessionId?: string;
32
+ }
33
+
16
34
  let queue: RpcNotification[] = [];
17
35
  let nextNotificationId = 1;
18
- // Timestamp of last drain — used to detect if a TUI is actively polling.
19
- // The TUI polls every 500ms; we consider it connected if it polled within
20
- // the last 3 seconds (6× the poll interval, tolerates transient delays).
21
- //
22
- // PER-SESSION: a single server process can serve MANY sessions (e.g. a TUI on
23
- // session A plus an OpenCode Desktop opened on session B for the same project,
24
- // whose newer RPC server this TUI's port discovery then selects). The TUI
25
- // poller drains with ITS active session id, so a session is "TUI-connected"
26
- // only if a TUI recently drained FOR THAT session. A process-global timestamp
27
- // would make session B's producers (`/aft-status`, configure warnings, etc.)
28
- // take the TUI-dialog path because session A's TUI is polling — queuing a
29
- // B-scoped dialog action that A's poller correctly refuses to show, so B's
30
- // notice is lost (it also suppressed B's non-TUI fallback). Tracking drains per
31
- // session routes each producer to the right delivery path.
32
- const lastDrainAtBySession = new Map<string, number>();
33
- let lastDrainAtAny = 0;
34
- const TUI_CONNECTED_WINDOW_MS = 3_000;
36
+ const notificationSinks = new Set<NotificationSink>();
37
+ const statusChangeSinks = new Set<StatusChangeSink>();
38
+
39
+ /** Register a live TUI notification sink. Returns an unregister function. */
40
+ export function registerNotificationSink(sink: NotificationSink): () => void {
41
+ notificationSinks.add(sink);
42
+ return () => {
43
+ notificationSinks.delete(sink);
44
+ };
45
+ }
35
46
 
36
- /** Push a notification for the TUI to pick up via polling. */
47
+ /** Register a live TUI status-invalidation sink. Returns an unregister function. */
48
+ export function registerStatusChangeSink(sink: StatusChangeSink): () => void {
49
+ statusChangeSinks.add(sink);
50
+ return () => {
51
+ statusChangeSinks.delete(sink);
52
+ };
53
+ }
54
+
55
+ function notificationMatchesSink(
56
+ notification: RpcNotification,
57
+ sink: { sessionId?: string },
58
+ ): boolean {
59
+ return (
60
+ notification.sessionId === undefined ||
61
+ sink.sessionId === undefined ||
62
+ notification.sessionId === sink.sessionId
63
+ );
64
+ }
65
+
66
+ function statusChangeMatchesSink(event: RpcStatusChange, sink: { sessionId?: string }): boolean {
67
+ return (
68
+ event.sessionId === undefined ||
69
+ sink.sessionId === undefined ||
70
+ event.sessionId === sink.sessionId
71
+ );
72
+ }
73
+
74
+ /** Push a notification for the TUI. Fans out to live sinks and keeps the queue. */
37
75
  export function pushNotification(
38
76
  type: string,
39
77
  payload: Record<string, unknown>,
40
78
  sessionId?: string,
41
79
  ): void {
42
- queue.push({ id: nextNotificationId++, type, payload, sessionId });
80
+ const notification: RpcNotification = { id: nextNotificationId++, type, payload, sessionId };
81
+ queue.push(notification);
82
+ for (const sink of notificationSinks) {
83
+ if (!notificationMatchesSink(notification, sink)) continue;
84
+ try {
85
+ sink.send(notification);
86
+ } catch {
87
+ // A dead socket must not block other sinks; the queue replays on reconnect.
88
+ }
89
+ }
43
90
  // Cap queue size to prevent unbounded growth if a TUI is not draining.
44
91
  // Session-fair eviction: a naive `slice(-50)` drops the globally-oldest
45
92
  // items, so a noisy session could evict ANOTHER session's single unseen
@@ -64,8 +111,20 @@ export function pushNotification(
64
111
  }
65
112
  }
66
113
 
114
+ /** Push a lightweight status invalidation to matching live TUI sockets. */
115
+ export function pushStatusChange(sessionId?: string): void {
116
+ const event: RpcStatusChange = sessionId ? { sessionId } : {};
117
+ for (const sink of statusChangeSinks) {
118
+ if (!statusChangeMatchesSink(event, sink)) continue;
119
+ try {
120
+ sink.send(event);
121
+ } catch {
122
+ // A dead socket must not block other status listeners.
123
+ }
124
+ }
125
+ }
126
+
67
127
  /** Return pending notifications after acking the client's last received id.
68
- * Updates lastDrainAt so isTuiConnected() reflects recent activity.
69
128
  *
70
129
  * Session scoping: when `sessionId` is provided, only notifications tagged for
71
130
  * that session (or session-less/global ones) are returned and pruned — a
@@ -81,11 +140,9 @@ export function pushNotification(
81
140
  *
82
141
  * Delivery is at-least-once (non-destructive return + prune-on-ack): a returned
83
142
  * notification stays queued until a later call acks it via a higher
84
- * `lastReceivedId`, so a lost poll response re-delivers on the next poll. */
143
+ * `lastReceivedId`, so a dropped WS socket re-delivers the backlog on reconnect
144
+ * (the client sends its `lastReceivedId` in the hello). */
85
145
  export function drainNotifications(lastReceivedId = 0, sessionId?: string): RpcNotification[] {
86
- const now = Date.now();
87
- lastDrainAtAny = now;
88
- if (sessionId !== undefined) lastDrainAtBySession.set(sessionId, now);
89
146
  const matchesClient = (notification: RpcNotification): boolean =>
90
147
  sessionId === undefined ||
91
148
  notification.sessionId === undefined ||
@@ -102,20 +159,19 @@ export function drainNotifications(lastReceivedId = 0, sessionId?: string): RpcN
102
159
  );
103
160
  }
104
161
 
105
- /** Whether a TUI client is actively polling for notifications.
106
- * Returns true only if a TUI has drained within the last 3 seconds.
107
- *
108
- * Pass `sessionId` (preferred) to ask whether a TUI is polling FOR THAT
109
- * SESSION — this is what producers (`/aft-status`, configure warnings, etc.)
110
- * must use to decide dialog-vs-message, so a TUI on a different session in the
111
- * same process does not misroute their delivery. Omit it only for legacy/global
112
- * callers that genuinely have no session context; they fall back to "any
113
- * session recently drained" (the pre-per-session behavior). */
162
+ /** Whether a TUI client is connected via a live notification socket. */
114
163
  export function isTuiConnected(sessionId?: string): boolean {
115
- const now = Date.now();
116
- if (sessionId !== undefined) {
117
- const at = lastDrainAtBySession.get(sessionId) ?? 0;
118
- return at > 0 && now - at < TUI_CONNECTED_WINDOW_MS;
164
+ if (notificationSinks.size === 0) return false;
165
+ if (sessionId === undefined) return true;
166
+ for (const sink of notificationSinks) {
167
+ if (sink.sessionId === undefined || sink.sessionId === sessionId) return true;
119
168
  }
120
- return lastDrainAtAny > 0 && now - lastDrainAtAny < TUI_CONNECTED_WINDOW_MS;
169
+ return false;
170
+ }
171
+
172
+ export function __resetRpcNotificationsForTest(): void {
173
+ queue = [];
174
+ nextNotificationId = 1;
175
+ notificationSinks.clear();
176
+ statusChangeSinks.clear();
121
177
  }