@cortexkit/aft-opencode 0.29.0 → 0.30.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.
@@ -19,6 +19,16 @@
19
19
  * UI uses, so the active session's `SessionRunState` is the one that
20
20
  * resolves `ensureRunning` and overlapping turns coalesce correctly.
21
21
  *
22
+ * The workaround only works when the live HTTP listener is actually
23
+ * reachable. OpenCode Desktop (Electron+Node) and TUI launched with
24
+ * `opencode --port 0` bind a real API listener; plain TUI binds an
25
+ * internal-only listener that 404s for `/session/*`. We probe once at
26
+ * plugin init and cache the result. When the listener is unreachable
27
+ * the wake path silently uses the in-process `input.client.session.promptAsync`,
28
+ * which keeps wakes flowing (at the cost of the upstream duplicate-runner
29
+ * bug) instead of producing no notification at all or nagging the user
30
+ * to relaunch with a different flag.
31
+ *
22
32
  * Tracked upstream as anomalyco/opencode#28202. When OpenCode fixes the
23
33
  * runtime split, this helper and its single consumer in `bg-notifications.ts`
24
34
  * can be deleted and the wake path can go back to `input.client`.
@@ -38,23 +48,32 @@ export type LiveServerClient = ReturnType<typeof createOpencodeClient>;
38
48
  export declare function getLiveServerClient(serverUrl: string, directory: string): LiveServerClient;
39
49
  /** Test helper — drop the cache between cases so each test starts clean. */
40
50
  export declare function __resetLiveServerClientCacheForTests(): void;
41
- /**
42
- * True when the current runtime is Bun. OpenCode's TUI ships with Bun;
43
- * the Electron Desktop app ships with Node. We use this to gate the
44
- * `--port 0` nudge: Desktop is unaffected by anomalyco/opencode#28202
45
- * because Node's webidl polyfill is complete, so undici v8 (and the live
46
- * HTTP server) work without additional flags.
47
- */
48
- export declare function isBunRuntime(): boolean;
49
51
  /**
50
52
  * Probe whether `serverUrl` accepts a connection within `timeoutMs`.
51
53
  * Returns `true` for any HTTP response (including 4xx / 5xx) since the
52
54
  * goal is to confirm the listener exists. Returns `false` on connection
53
55
  * refused, DNS failure, timeout, or undefined URL.
54
56
  *
55
- * Used at plugin init under Bun to detect TUI sessions started without
56
- * `opencode --port 0` those bind an internal-only listener that 404s
57
- * for `/session/...` endpoints and breaks AFT's wake path.
57
+ * Used at plugin init to decide whether bg-notifications should use the
58
+ * live-server wake transport (workaround for anomalyco/opencode#28202)
59
+ * or fall back to the in-process `input.client.session.promptAsync`
60
+ * path. Plain TUI (no `--port 0`) binds an internal-only listener that
61
+ * 404s for `/session/...`, so this returns false there; OpenCode
62
+ * Desktop, `opencode run`, and `opencode --port 0` TUI return true.
58
63
  */
59
64
  export declare function probeServerReachable(serverUrl: string | undefined, timeoutMs?: number): Promise<boolean>;
65
+ /**
66
+ * Record the probe result. Idempotent; if you record twice, the latest
67
+ * value wins. The wake path reads through `useLiveServerWake()`.
68
+ */
69
+ export declare function setLiveServerWakeAvailable(available: boolean): void;
70
+ /**
71
+ * Read the cached probe decision. `true` means the wake path should use
72
+ * `getLiveServerClient(serverUrl, directory)` and POST through the live
73
+ * HTTP listener. `false` means fall back to the in-process client passed
74
+ * via plugin context (`input.client`).
75
+ */
76
+ export declare function useLiveServerWake(): boolean;
77
+ /** Test helper — reset the decision cache between cases. */
78
+ export declare function __resetLiveServerWakeForTests(): void;
60
79
  //# sourceMappingURL=live-server-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"live-server-client.d.ts","sourceRoot":"","sources":["../../src/shared/live-server-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AA6BvE;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAY1F;AAED,4EAA4E;AAC5E,wBAAgB,oCAAoC,IAAI,IAAI,CAE3D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,SAAO,GACf,OAAO,CAAC,OAAO,CAAC,CAqBlB"}
1
+ {"version":3,"file":"live-server-client.d.ts","sourceRoot":"","sources":["../../src/shared/live-server-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AA6BvE;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAY1F;AAED,4EAA4E;AAC5E,wBAAgB,oCAAoC,IAAI,IAAI,CAE3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,SAAO,GACf,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAmBD;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAEnE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED,4DAA4D;AAC5D,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
@@ -0,0 +1,18 @@
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
@@ -0,0 +1 @@
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 +1 @@
1
- {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAMvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA0EjD,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAuOjE;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAqCvE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAmBrE"}
1
+ {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAavE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA0EjD,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAmRjE;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAyBvE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAuBrE"}
@@ -0,0 +1,24 @@
1
+ import type { ToolContext, ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { PluginContext } from "../types.js";
3
+ export type BashWaitPattern = {
4
+ kind: "substring";
5
+ value: string;
6
+ } | {
7
+ kind: "regex";
8
+ value: RegExp;
9
+ source: string;
10
+ };
11
+ export type BashStatusWaited = {
12
+ reason: "matched" | "exited" | "timeout";
13
+ elapsed_ms: number;
14
+ match?: string;
15
+ match_offset?: number;
16
+ };
17
+ type BashStatusWithWait = Record<string, unknown> & {
18
+ waited?: BashStatusWaited;
19
+ };
20
+ export declare function createBashWatchTool(ctx: PluginContext): ToolDefinition;
21
+ export declare function waitForBashStatus(ctx: PluginContext, runtime: ToolContext, taskId: string, outputMode: string | undefined, waitFor: BashWaitPattern | undefined, effectiveWaitMs: number): Promise<BashStatusWithWait>;
22
+ export declare function parseWaitPattern(value: unknown): BashWaitPattern | undefined;
23
+ export {};
24
+ //# sourceMappingURL=bash_watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bash_watch.d.ts","sourceRoot":"","sources":["../../src/tools/bash_watch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAavE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AASjD,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AACrD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AACF,KAAK,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAElF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CA4FtE;AAiDD,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,EAAE,eAAe,GAAG,SAAS,EACpC,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,kBAAkB,CAAC,CAmD7B;AAsCD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,GAAG,SAAS,CAK5E"}
@@ -0,0 +1,38 @@
1
+ import type { ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { PluginContext } from "../types.js";
3
+ /**
4
+ * Write bytes to a running PTY background task.
5
+ *
6
+ * Two input shapes:
7
+ *
8
+ * - **String** — verbatim bytes written to the PTY. Use this for plain text
9
+ * such as REPL commands (`"print(1)\n"`) or when the agent specifically
10
+ * wants the literal characters `\u001b` (e.g. writing source code).
11
+ *
12
+ * - **Sequence array** — mix plain strings (text) with `{ key: "<name>" }`
13
+ * objects (named control keys). Items concatenate into one atomic write
14
+ * so the PTY sees the whole sequence as one input chunk. The agent never
15
+ * has to JSON-encode escape characters.
16
+ *
17
+ * Allowed key names: `enter`, `return`, `tab`, `space`, `backspace`,
18
+ * `esc`, `escape`, `up`, `down`, `left`, `right`, `home`, `end`,
19
+ * `page-up`, `page-down`, `delete`, `insert`, `f1`..`f12`,
20
+ * `ctrl-a`..`ctrl-z`. Names are case-insensitive.
21
+ *
22
+ * The vim "type text, exit insert, save and quit" idiom:
23
+ *
24
+ * ```ts
25
+ * bash_write({ taskId, input: [
26
+ * "iHello",
27
+ * { key: "esc" },
28
+ * ":wq",
29
+ * { key: "enter" },
30
+ * ]})
31
+ * ```
32
+ *
33
+ * Rust enforces a 1 MiB maximum on the EXPANDED byte stream. Agents should
34
+ * check `bash_status` first and only call `bash_write` when the task reports
35
+ * `mode: "pty"`.
36
+ */
37
+ export declare function createBashWriteTool(ctx: PluginContext): ToolDefinition;
38
+ //# sourceMappingURL=bash_write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bash_write.d.ts","sourceRoot":"","sources":["../../src/tools/bash_write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAKjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CA6CtE"}
@@ -20,7 +20,7 @@ export declare function createReadTool(ctx: PluginContext): ToolDefinition;
20
20
  * Returns hoisted tools keyed by opencode's built-in names.
21
21
  * Overrides: read, write, edit, apply_patch (always when hoisting is on).
22
22
  *
23
- * Bash hoisting is opt-in: `bash`, `bash_status`, and `bash_kill` are
23
+ * Bash hoisting is opt-in: `bash`, `bash_status`, `bash_write`, and `bash_kill` are
24
24
  * registered together when at least one `experimental.bash.*` flag is
25
25
  * enabled (rewrite, compress, or background). When all flags are off,
26
26
  * opencode's native bash stays in place — users without bash experimentals
@@ -1 +1 @@
1
- {"version":3,"file":"hoisted.d.ts","sourceRoot":"","sources":["../../src/tools/hoisted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAK1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAuEjD,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,GAAI,IAAI,MAAM,EAAE,QAAQ,MAAM,EAAE,OAAO,MAAM,KAAG,MAChD,CAAC;AAqRtC;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CA+JjE;AAmmCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAwB/E;AAMD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA4FnF"}
1
+ {"version":3,"file":"hoisted.d.ts","sourceRoot":"","sources":["../../src/tools/hoisted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAK1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAyEjD,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,GAAI,IAAI,MAAM,EAAE,QAAQ,MAAM,EAAE,OAAO,MAAM,KAAG,MAChD,CAAC;AAqRtC;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CA+JjE;AAmmCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA0B/E;AAMD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA8FnF"}
package/dist/tui.js CHANGED
@@ -3,7 +3,7 @@ import { createMemo as createMemo2, createSignal as createSignal2, onCleanup as
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@cortexkit/aft-opencode",
6
- version: "0.29.0",
6
+ version: "0.30.0",
7
7
  type: "module",
8
8
  description: "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
9
9
  main: "dist/index.js",
@@ -32,20 +32,21 @@ var package_default = {
32
32
  },
33
33
  dependencies: {
34
34
  "@clack/prompts": "^1.2.0",
35
- "@cortexkit/aft-bridge": "0.29.0",
35
+ "@cortexkit/aft-bridge": "0.30.0",
36
36
  "@opencode-ai/plugin": "^1.15.5",
37
37
  "@opencode-ai/sdk": "^1.15.5",
38
+ "@xterm/headless": "^5.5.0",
38
39
  "comment-json": "^4.6.2",
39
40
  undici: "^7.25.0",
40
41
  zod: "^4.1.8"
41
42
  },
42
43
  optionalDependencies: {
43
- "@cortexkit/aft-darwin-arm64": "0.29.0",
44
- "@cortexkit/aft-darwin-x64": "0.29.0",
45
- "@cortexkit/aft-linux-arm64": "0.29.0",
46
- "@cortexkit/aft-linux-x64": "0.29.0",
47
- "@cortexkit/aft-win32-arm64": "0.29.0",
48
- "@cortexkit/aft-win32-x64": "0.29.0"
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"
49
50
  },
50
51
  devDependencies: {
51
52
  "@types/node": "^22.0.0",
@@ -1 +1 @@
1
- {"version":3,"file":"workflow-hints.d.ts","sourceRoot":"","sources":["../src/workflow-hints.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,SAAS,EAAqB,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,WAAW,EAAE,SAAS,GAAG,aAAa,GAAG,KAAK,CAAC;IAC/C,4EAA4E;IAC5E,aAAa,EAAE,OAAO,CAAC;IACvB,mEAAmE;IACnE,eAAe,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,qBAAqB,EAAE,OAAO,CAAC;IAC/B,4DAA4D;IAC5D,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC5B;AAID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAmEzE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAQjG"}
1
+ {"version":3,"file":"workflow-hints.d.ts","sourceRoot":"","sources":["../src/workflow-hints.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,SAAS,EAAqB,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,WAAW,EAAE,SAAS,GAAG,aAAa,GAAG,KAAK,CAAC;IAC/C,4EAA4E;IAC5E,aAAa,EAAE,OAAO,CAAC;IACvB,mEAAmE;IACnE,eAAe,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,qBAAqB,EAAE,OAAO,CAAC;IAC/B,4DAA4D;IAC5D,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC5B;AAID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAuEzE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAQjG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
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,20 +29,21 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@clack/prompts": "^1.2.0",
32
- "@cortexkit/aft-bridge": "0.29.0",
32
+ "@cortexkit/aft-bridge": "0.30.0",
33
33
  "@opencode-ai/plugin": "^1.15.5",
34
34
  "@opencode-ai/sdk": "^1.15.5",
35
+ "@xterm/headless": "^5.5.0",
35
36
  "comment-json": "^4.6.2",
36
37
  "undici": "^7.25.0",
37
38
  "zod": "^4.1.8"
38
39
  },
39
40
  "optionalDependencies": {
40
- "@cortexkit/aft-darwin-arm64": "0.29.0",
41
- "@cortexkit/aft-darwin-x64": "0.29.0",
42
- "@cortexkit/aft-linux-arm64": "0.29.0",
43
- "@cortexkit/aft-linux-x64": "0.29.0",
44
- "@cortexkit/aft-win32-arm64": "0.29.0",
45
- "@cortexkit/aft-win32-x64": "0.29.0"
41
+ "@cortexkit/aft-darwin-arm64": "0.30.0",
42
+ "@cortexkit/aft-darwin-x64": "0.30.0",
43
+ "@cortexkit/aft-linux-arm64": "0.30.0",
44
+ "@cortexkit/aft-linux-x64": "0.30.0",
45
+ "@cortexkit/aft-win32-arm64": "0.30.0",
46
+ "@cortexkit/aft-win32-x64": "0.30.0"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@types/node": "^22.0.0",
@@ -19,6 +19,16 @@
19
19
  * UI uses, so the active session's `SessionRunState` is the one that
20
20
  * resolves `ensureRunning` and overlapping turns coalesce correctly.
21
21
  *
22
+ * The workaround only works when the live HTTP listener is actually
23
+ * reachable. OpenCode Desktop (Electron+Node) and TUI launched with
24
+ * `opencode --port 0` bind a real API listener; plain TUI binds an
25
+ * internal-only listener that 404s for `/session/*`. We probe once at
26
+ * plugin init and cache the result. When the listener is unreachable
27
+ * the wake path silently uses the in-process `input.client.session.promptAsync`,
28
+ * which keeps wakes flowing (at the cost of the upstream duplicate-runner
29
+ * bug) instead of producing no notification at all or nagging the user
30
+ * to relaunch with a different flag.
31
+ *
22
32
  * Tracked upstream as anomalyco/opencode#28202. When OpenCode fixes the
23
33
  * runtime split, this helper and its single consumer in `bg-notifications.ts`
24
34
  * can be deleted and the wake path can go back to `input.client`.
@@ -84,26 +94,18 @@ export function __resetLiveServerClientCacheForTests(): void {
84
94
  clientCache.clear();
85
95
  }
86
96
 
87
- /**
88
- * True when the current runtime is Bun. OpenCode's TUI ships with Bun;
89
- * the Electron Desktop app ships with Node. We use this to gate the
90
- * `--port 0` nudge: Desktop is unaffected by anomalyco/opencode#28202
91
- * because Node's webidl polyfill is complete, so undici v8 (and the live
92
- * HTTP server) work without additional flags.
93
- */
94
- export function isBunRuntime(): boolean {
95
- return typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
96
- }
97
-
98
97
  /**
99
98
  * Probe whether `serverUrl` accepts a connection within `timeoutMs`.
100
99
  * Returns `true` for any HTTP response (including 4xx / 5xx) since the
101
100
  * goal is to confirm the listener exists. Returns `false` on connection
102
101
  * refused, DNS failure, timeout, or undefined URL.
103
102
  *
104
- * Used at plugin init under Bun to detect TUI sessions started without
105
- * `opencode --port 0` those bind an internal-only listener that 404s
106
- * for `/session/...` endpoints and breaks AFT's wake path.
103
+ * Used at plugin init to decide whether bg-notifications should use the
104
+ * live-server wake transport (workaround for anomalyco/opencode#28202)
105
+ * or fall back to the in-process `input.client.session.promptAsync`
106
+ * path. Plain TUI (no `--port 0`) binds an internal-only listener that
107
+ * 404s for `/session/...`, so this returns false there; OpenCode
108
+ * Desktop, `opencode run`, and `opencode --port 0` TUI return true.
107
109
  */
108
110
  export async function probeServerReachable(
109
111
  serverUrl: string | undefined,
@@ -130,3 +132,43 @@ export async function probeServerReachable(
130
132
  clearTimeout(timer);
131
133
  }
132
134
  }
135
+
136
+ /**
137
+ * Per-plugin-process decision: should bg-notifications use the live-server
138
+ * wake transport (workaround for anomalyco/opencode#28202), or fall back
139
+ * to the in-process `input.client.session.promptAsync` path?
140
+ *
141
+ * Set once at plugin init from the result of `probeServerReachable()`.
142
+ * Defaults to `false` if no decision has been recorded yet — that's the
143
+ * safe direction because `input.client.session.promptAsync` is always
144
+ * available (it's part of the plugin contract), whereas the live-server
145
+ * path needs both a probe-confirmed listener and the workaround code
146
+ * itself to be live.
147
+ *
148
+ * Read at wake time (not cached on a closure) so background probes that
149
+ * complete after plugin init still take effect on the next wake.
150
+ */
151
+ let liveServerWakeAvailable = false;
152
+
153
+ /**
154
+ * Record the probe result. Idempotent; if you record twice, the latest
155
+ * value wins. The wake path reads through `useLiveServerWake()`.
156
+ */
157
+ export function setLiveServerWakeAvailable(available: boolean): void {
158
+ liveServerWakeAvailable = available;
159
+ }
160
+
161
+ /**
162
+ * Read the cached probe decision. `true` means the wake path should use
163
+ * `getLiveServerClient(serverUrl, directory)` and POST through the live
164
+ * HTTP listener. `false` means fall back to the in-process client passed
165
+ * via plugin context (`input.client`).
166
+ */
167
+ export function useLiveServerWake(): boolean {
168
+ return liveServerWakeAvailable;
169
+ }
170
+
171
+ /** Test helper — reset the decision cache between cases. */
172
+ export function __resetLiveServerWakeForTests(): void {
173
+ liveServerWakeAvailable = false;
174
+ }
@@ -0,0 +1,113 @@
1
+ import * as fs from "node:fs/promises";
2
+ import { Terminal } from "@xterm/headless";
3
+
4
+ const DEFAULT_COLS = 80;
5
+ const DEFAULT_ROWS = 24;
6
+ const READ_CHUNK_SIZE = 64 * 1024;
7
+
8
+ export interface PtyTerminalState {
9
+ terminal: Terminal;
10
+ fileHandle: fs.FileHandle;
11
+ offset: number;
12
+ rows: number;
13
+ cols: number;
14
+ lastAccessMs: number;
15
+ }
16
+
17
+ const terminals = new Map<string, PtyTerminalState>();
18
+
19
+ export async function getOrCreatePtyTerminal(
20
+ key: string,
21
+ outputPath: string,
22
+ rows = DEFAULT_ROWS,
23
+ cols = DEFAULT_COLS,
24
+ ): Promise<PtyTerminalState> {
25
+ const existing = terminals.get(key);
26
+ if (existing) {
27
+ existing.lastAccessMs = Date.now();
28
+ if (existing.rows === rows && existing.cols === cols) {
29
+ return existing;
30
+ }
31
+ terminals.delete(key);
32
+ existing.terminal.dispose();
33
+ await existing.fileHandle.close().catch(() => undefined);
34
+ }
35
+
36
+ const fileHandle = await fs.open(outputPath, "r");
37
+ const state: PtyTerminalState = {
38
+ terminal: new Terminal({ cols, rows, allowProposedApi: true }),
39
+ fileHandle,
40
+ offset: 0,
41
+ rows,
42
+ cols,
43
+ lastAccessMs: Date.now(),
44
+ };
45
+ terminals.set(key, state);
46
+ return state;
47
+ }
48
+
49
+ export async function readPtyBytes(state: PtyTerminalState): Promise<Buffer> {
50
+ const chunks: Buffer[] = [];
51
+ while (true) {
52
+ const buffer = Buffer.allocUnsafe(READ_CHUNK_SIZE);
53
+ const { bytesRead } = await state.fileHandle.read(buffer, 0, buffer.length, state.offset);
54
+ if (bytesRead === 0) break;
55
+ const chunk = buffer.subarray(0, bytesRead);
56
+ chunks.push(Buffer.from(chunk));
57
+ await writeTerminal(state.terminal, chunk);
58
+ state.offset += bytesRead;
59
+ }
60
+ state.lastAccessMs = Date.now();
61
+ return Buffer.concat(chunks);
62
+ }
63
+
64
+ export async function disposePtyTerminal(key: string): Promise<void> {
65
+ const state = terminals.get(key);
66
+ if (!state) return;
67
+ terminals.delete(key);
68
+ state.terminal.dispose();
69
+ await state.fileHandle.close().catch(() => undefined);
70
+ }
71
+
72
+ export async function disposeAllPtyTerminals(): Promise<void> {
73
+ await Promise.all([...terminals.keys()].map((key) => disposePtyTerminal(key)));
74
+ }
75
+
76
+ export function renderScreen(
77
+ state: PtyTerminalState,
78
+ rows = state.rows,
79
+ cols = state.cols,
80
+ ): string {
81
+ const active = state.terminal.buffer.active;
82
+ const lines: string[] = [];
83
+ for (let y = 0; y < rows; y++) {
84
+ const line = active.getLine(active.baseY + y);
85
+ if (!line) {
86
+ lines.push("");
87
+ continue;
88
+ }
89
+ let text = "";
90
+ for (let x = 0; x < cols; x++) {
91
+ text += line.getCell(x)?.getChars() || " ";
92
+ }
93
+ lines.push(text.trimEnd());
94
+ }
95
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
96
+ return lines.join("\n");
97
+ }
98
+
99
+ export function __resetPtyCacheForTests(): void {
100
+ for (const state of terminals.values()) {
101
+ state.terminal.dispose();
102
+ void state.fileHandle.close().catch(() => undefined);
103
+ }
104
+ terminals.clear();
105
+ }
106
+
107
+ export function __ptyCacheSizeForTests(): number {
108
+ return terminals.size;
109
+ }
110
+
111
+ function writeTerminal(terminal: Terminal, data: Uint8Array): Promise<void> {
112
+ return new Promise((resolve) => terminal.write(data, resolve));
113
+ }