@cueloop/daemon 0.1.0-alpha.80 → 0.1.0-alpha.82

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.
@@ -0,0 +1,398 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { Thread } from "@cueloop/schema";
6
+ import {
7
+ insideGhostty,
8
+ openGhosttyThreadSurface as openActualGhosttyThreadSurface,
9
+ resolveCueloopLaunchCommand,
10
+ supportsGhosttyAppleScript,
11
+ GHOSTTY_OPEN_APPLESCRIPT,
12
+ type GhosttyThreadSurfacePersistence,
13
+ } from "./ghostty-thread-surface";
14
+ import type { GhosttyThreadSurfaceHandle } from "./ghostty-thread-surface-store";
15
+
16
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-ghostty-thread-surface-"));
17
+ const openGhosttyThreadSurface: typeof openActualGhosttyThreadSurface = (
18
+ reviewed,
19
+ persistence,
20
+ env,
21
+ mode,
22
+ binPath,
23
+ platform,
24
+ ) =>
25
+ openActualGhosttyThreadSurface(
26
+ reviewed,
27
+ persistence,
28
+ env,
29
+ mode,
30
+ binPath,
31
+ platform,
32
+ "/usr/local/bin/cueloop",
33
+ );
34
+
35
+ afterAll(() => rmSync(dir, { recursive: true, force: true }));
36
+
37
+ function thread(): Thread {
38
+ return {
39
+ schemaVersion: "1",
40
+ id: "ses_ghostty1",
41
+ workspace: { repoRoot: "/repo", branch: "main" },
42
+ artifact: { type: "plan", content: "# Plan", meta: { cwd: "/repo/work" } },
43
+ revisions: [{ revision: 1, content: "# Plan", submittedAt: "now" }],
44
+ annotations: [],
45
+ message: null,
46
+ status: "pending",
47
+ createdAt: "now",
48
+ };
49
+ }
50
+
51
+ function persistence(initial: GhosttyThreadSurfaceHandle | null = null) {
52
+ let saved = initial;
53
+ let claimed = false;
54
+ const port: GhosttyThreadSurfacePersistence = {
55
+ ghosttyClaimThreadSurface: async () => {
56
+ if (claimed) return false;
57
+ claimed = true;
58
+
59
+ return true;
60
+ },
61
+ ghosttyReleaseThreadSurface: async () => {
62
+ claimed = false;
63
+ },
64
+ ghosttyGetThreadSurface: async () => saved,
65
+ ghosttySetThreadSurface: async (_id, handle) => {
66
+ saved = handle;
67
+ },
68
+ };
69
+
70
+ return { port, saved: () => saved };
71
+ }
72
+
73
+ function stub(
74
+ name: string,
75
+ focusResult = "focused",
76
+ version = "1.3.1",
77
+ openFails = false,
78
+ closeFails = false,
79
+ ) {
80
+ const binPath = join(dir, `${name}.sh`);
81
+ const logPath = join(dir, `${name}.log`);
82
+ const scriptPath = join(dir, `${name}.applescript`);
83
+ const shellPath = join(dir, `${name}.shell-command`);
84
+
85
+ writeFileSync(
86
+ binPath,
87
+ `#!/bin/sh
88
+ if [ "$2" = 'tell application "Ghostty" to get version' ]; then
89
+ printf 'version\\n' >> "${logPath}"
90
+ printf '${version}\\n'
91
+ exit 0
92
+ fi
93
+ printf '%s\\n' "$2" > "${scriptPath}"
94
+ case "$2" in
95
+ *"close candidate"*)
96
+ printf 'close:%s\\n' "$3" >> "${logPath}"
97
+ ${closeFails ? "exit 1" : ""}
98
+ printf 'closed\\n'
99
+ exit 0
100
+ ;;
101
+ esac
102
+ if [ "$3" = "ses_previous" ] || [ "$3" = "term-1" ]; then
103
+ printf 'focus:%s\\n' "$3" >> "${logPath}"
104
+ printf '${focusResult}\\n'
105
+ exit 0
106
+ fi
107
+ printf 'open:%s:%s\\n' "$3" "$4" >> "${logPath}"
108
+ printf '%s\\n' "$5" > "${shellPath}"
109
+ ${openFails ? "exit 1" : "printf 'term-1\\n'"}
110
+ `,
111
+ );
112
+ chmodSync(binPath, 0o755);
113
+
114
+ return { binPath, logPath, scriptPath, shellPath };
115
+ }
116
+
117
+ function log(path: string): string[] {
118
+ return readFileSync(path, "utf8").trim().split("\n");
119
+ }
120
+
121
+ describe("Ghostty AppleScript capability", () => {
122
+ test.skipIf(process.platform !== "darwin" || !existsSync("/Applications/Ghostty.app"))(
123
+ "open script compiles against the installed Ghostty dictionary",
124
+ () => {
125
+ const compiled = Bun.spawnSync(
126
+ ["osacompile", "-e", GHOSTTY_OPEN_APPLESCRIPT, "-o", join(dir, "ghostty-open.scpt")],
127
+ { stdout: "pipe", stderr: "pipe" },
128
+ );
129
+
130
+ expect(compiled.stderr.toString()).toBe("");
131
+ expect(compiled.exitCode).toBe(0);
132
+ },
133
+ );
134
+
135
+ test("requires the documented 1.3 API", () => {
136
+ expect(supportsGhosttyAppleScript("1.2.3")).toBe(false);
137
+ expect(supportsGhosttyAppleScript("1.3.0")).toBe(true);
138
+ expect(supportsGhosttyAppleScript("1.3.1")).toBe(true);
139
+ expect(supportsGhosttyAppleScript(null)).toBe(false);
140
+ });
141
+
142
+ test("detects Ghostty's own environment, not TERM alone", () => {
143
+ expect(insideGhostty({ TERM_PROGRAM: "ghostty" })).toBe(true);
144
+ expect(insideGhostty({ GHOSTTY_RESOURCES_DIR: "/Applications/Ghostty.app" })).toBe(true);
145
+ expect(insideGhostty({ TERM_PROGRAM: "other" })).toBe(false);
146
+ });
147
+
148
+ test("source CLI invocation uses its absolute entry when no cueloop binary is installed", () => {
149
+ const entry = join(import.meta.dir, "../../cli/src/main.ts");
150
+
151
+ expect(resolveCueloopLaunchCommand(process.execPath, entry, "")).toBe(
152
+ `'${process.execPath}' run '${entry}'`,
153
+ );
154
+ expect(resolveCueloopLaunchCommand(process.execPath, "/other", "")).toBeNull();
155
+ });
156
+ });
157
+
158
+ describe("openGhosttyThreadSurface", () => {
159
+ test.each(["tab", "pane", "window"] as const)("opens a focused %s", async (mode) => {
160
+ const native = stub(`open-${mode}`);
161
+ const store = persistence();
162
+
163
+ expect(
164
+ await openGhosttyThreadSurface(
165
+ thread(),
166
+ store.port,
167
+ { TERM_PROGRAM: "ghostty" },
168
+ mode,
169
+ native.binPath,
170
+ "darwin",
171
+ ),
172
+ ).toBe("opened");
173
+ expect(store.saved()).toEqual({ terminalId: "term-1" });
174
+ expect(log(native.logPath)).toEqual(["version", `open:${mode}:/repo/work`]);
175
+ const script = readFileSync(native.scriptPath, "utf8");
176
+
177
+ expect(script).toContain(
178
+ "set initial working directory of surfaceConfiguration to threadDirectory",
179
+ );
180
+ expect(script).toContain("input text shellCommand to createdTerminal");
181
+ expect(script).toContain("set targetWindow to front window");
182
+ expect(script).toContain(
183
+ "split sourceTerminal direction right with configuration surfaceConfiguration",
184
+ );
185
+ expect(script).toContain('perform action "new_tab" on sourceTerminal');
186
+ expect(script).toContain("focus createdTerminal");
187
+ expect(readFileSync(native.shellPath, "utf8").trim()).toBe(
188
+ "cd -- '/repo/work' && '/usr/local/bin/cueloop' 'ses_ghostty1'",
189
+ );
190
+ });
191
+
192
+ test("focuses a live terminal without creating another", async () => {
193
+ const native = stub("focus");
194
+ const store = persistence({ terminalId: "ses_previous" });
195
+
196
+ expect(
197
+ await openGhosttyThreadSurface(
198
+ thread(),
199
+ store.port,
200
+ { TERM_PROGRAM: "ghostty" },
201
+ "tab",
202
+ native.binPath,
203
+ "darwin",
204
+ ),
205
+ ).toBe("focused");
206
+ expect(log(native.logPath)).toEqual(["version", "focus:ses_previous"]);
207
+ });
208
+
209
+ test("focuses a live terminal even if cueloop is no longer on PATH", async () => {
210
+ const native = stub("focus-no-binary");
211
+ const store = persistence({ terminalId: "ses_previous" });
212
+
213
+ expect(
214
+ await openActualGhosttyThreadSurface(
215
+ thread(),
216
+ store.port,
217
+ { TERM_PROGRAM: "ghostty" },
218
+ "tab",
219
+ native.binPath,
220
+ "darwin",
221
+ null,
222
+ ),
223
+ ).toBe("focused");
224
+ expect(log(native.logPath)).toEqual(["version", "focus:ses_previous"]);
225
+ });
226
+
227
+ test("quotes a workspace path before entering it in a Ghostty shell", async () => {
228
+ const native = stub("quoted-cwd");
229
+ const reviewed = thread();
230
+
231
+ reviewed.artifact.meta.cwd = "/repo/it's $(unsafe)";
232
+ expect(
233
+ await openGhosttyThreadSurface(
234
+ reviewed,
235
+ persistence().port,
236
+ { TERM_PROGRAM: "ghostty" },
237
+ "tab",
238
+ native.binPath,
239
+ "darwin",
240
+ ),
241
+ ).toBe("opened");
242
+ expect(readFileSync(native.shellPath, "utf8").trim()).toBe(
243
+ "cd -- '/repo/it'\\''s $(unsafe)' && '/usr/local/bin/cueloop' 'ses_ghostty1'",
244
+ );
245
+ });
246
+
247
+ test("reopens a closed terminal and replaces its handle", async () => {
248
+ const native = stub("closed", "closed");
249
+ const store = persistence({ terminalId: "ses_previous" });
250
+
251
+ expect(
252
+ await openGhosttyThreadSurface(
253
+ thread(),
254
+ store.port,
255
+ { TERM_PROGRAM: "ghostty" },
256
+ "pane",
257
+ native.binPath,
258
+ "darwin",
259
+ ),
260
+ ).toBe("opened");
261
+ expect(store.saved()).toEqual({ terminalId: "term-1" });
262
+ expect(log(native.logPath)).toEqual(["version", "focus:ses_previous", "open:pane:/repo/work"]);
263
+ });
264
+
265
+ test("closes an untracked terminal when daemon persistence fails", async () => {
266
+ const native = stub("persist-failed");
267
+ const store: GhosttyThreadSurfacePersistence = {
268
+ ghosttyClaimThreadSurface: async () => true,
269
+ ghosttyReleaseThreadSurface: async () => {},
270
+ ghosttyGetThreadSurface: async () => null,
271
+ ghosttySetThreadSurface: async () => {
272
+ throw new Error("daemon unavailable");
273
+ },
274
+ };
275
+
276
+ expect(
277
+ await openGhosttyThreadSurface(
278
+ thread(),
279
+ store,
280
+ { TERM_PROGRAM: "ghostty" },
281
+ "tab",
282
+ native.binPath,
283
+ "darwin",
284
+ ),
285
+ ).toBe("failed");
286
+ expect(log(native.logPath)).toEqual(["version", "open:tab:/repo/work", "close:term-1"]);
287
+ });
288
+
289
+ test("does not open another tab when native identity lookup fails", async () => {
290
+ const native = stub("lookup-failed");
291
+ const store: GhosttyThreadSurfacePersistence = {
292
+ ghosttyClaimThreadSurface: async () => true,
293
+ ghosttyReleaseThreadSurface: async () => {},
294
+ ghosttyGetThreadSurface: async () => {
295
+ throw new Error("daemon unavailable");
296
+ },
297
+ ghosttySetThreadSurface: async () => {},
298
+ };
299
+
300
+ expect(
301
+ await openGhosttyThreadSurface(
302
+ thread(),
303
+ store,
304
+ { TERM_PROGRAM: "ghostty" },
305
+ "tab",
306
+ native.binPath,
307
+ "darwin",
308
+ ),
309
+ ).toBe("failed");
310
+ expect(log(native.logPath)).toEqual(["version"]);
311
+ });
312
+
313
+ test("retains the claim if an untracked native terminal cannot be closed", async () => {
314
+ const native = stub("close-failed", "focused", "1.3.1", false, true);
315
+ let claimed = false;
316
+ const store: GhosttyThreadSurfacePersistence = {
317
+ ghosttyClaimThreadSurface: async () => {
318
+ if (claimed) return false;
319
+ claimed = true;
320
+
321
+ return true;
322
+ },
323
+ ghosttyReleaseThreadSurface: async () => {
324
+ claimed = false;
325
+ },
326
+ ghosttyGetThreadSurface: async () => null,
327
+ ghosttySetThreadSurface: async () => {
328
+ throw new Error("daemon write failed");
329
+ },
330
+ };
331
+ const launch = () =>
332
+ openGhosttyThreadSurface(
333
+ thread(),
334
+ store,
335
+ { TERM_PROGRAM: "ghostty" },
336
+ "tab",
337
+ native.binPath,
338
+ "darwin",
339
+ );
340
+
341
+ expect(await launch()).toBe("failed");
342
+ expect(await launch()).toBe("failed");
343
+ expect(log(native.logPath).filter((line) => line.startsWith("open:"))).toEqual([
344
+ "open:tab:/repo/work",
345
+ ]);
346
+ });
347
+
348
+ test("concurrent callers cannot open duplicate live surfaces", async () => {
349
+ const native = stub("concurrent");
350
+ const store = persistence();
351
+ const launch = () =>
352
+ openGhosttyThreadSurface(
353
+ thread(),
354
+ store.port,
355
+ { TERM_PROGRAM: "ghostty" },
356
+ "tab",
357
+ native.binPath,
358
+ "darwin",
359
+ );
360
+
361
+ expect((await Promise.all([launch(), launch()])).toSorted()).toEqual(["failed", "opened"]);
362
+ expect(log(native.logPath).filter((line) => line.startsWith("open:"))).toEqual([
363
+ "open:tab:/repo/work",
364
+ ]);
365
+ });
366
+
367
+ test("missing capability, denied automation, and disabled placement never resolve a Thread", async () => {
368
+ const old = stub("old", "focused", "1.2.3");
369
+ const denied = stub("denied", "focused", "1.3.1", true);
370
+ const store = persistence();
371
+ const env = { TERM_PROGRAM: "ghostty" };
372
+
373
+ expect(
374
+ await openGhosttyThreadSurface(thread(), store.port, env, "tab", old.binPath, "darwin"),
375
+ ).toBe("failed");
376
+ expect(
377
+ await openGhosttyThreadSurface(thread(), store.port, env, "tab", denied.binPath, "darwin"),
378
+ ).toBe("failed");
379
+ expect(
380
+ await openGhosttyThreadSurface(thread(), store.port, env, "none", denied.binPath, "darwin"),
381
+ ).toBe("disabled");
382
+ expect(
383
+ await openGhosttyThreadSurface(thread(), store.port, {}, "tab", denied.binPath, "darwin"),
384
+ ).toBe("unavailable");
385
+ expect(
386
+ await openActualGhosttyThreadSurface(
387
+ thread(),
388
+ store.port,
389
+ env,
390
+ "tab",
391
+ denied.binPath,
392
+ "darwin",
393
+ null,
394
+ ),
395
+ ).toBe("failed");
396
+ expect(store.saved()).toBeNull();
397
+ });
398
+ });
@@ -0,0 +1,267 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { accessSync, constants, existsSync, statSync } from "node:fs";
3
+ import { basename, delimiter, isAbsolute, join } from "node:path";
4
+ import type { Thread, ThreadSurfaceOpenStatus } from "@cueloop/schema";
5
+ import type { GhosttyThreadSurfaceHandle } from "./ghostty-thread-surface-store";
6
+ import { loadGhosttyThreadSurface, type GhosttyThreadSurface } from "./thread-surface-config";
7
+
8
+ const APPLESCRIPT_TIMEOUT_MS = 10000;
9
+ const VERSION_SCRIPT = 'tell application "Ghostty" to get version';
10
+ export const GHOSTTY_OPEN_APPLESCRIPT = `on run argv
11
+ set placement to item 1 of argv
12
+ set threadDirectory to item 2 of argv
13
+ set shellCommand to item 3 of argv
14
+ tell application "Ghostty"
15
+ if placement is "tab" then
16
+ set targetWindow to front window
17
+ set beforeCount to count of tabs of targetWindow
18
+ set beforeIds to id of every tab of targetWindow
19
+ set sourceTerminal to focused terminal of selected tab of targetWindow
20
+ if not (perform action "new_tab" on sourceTerminal) then error "Ghostty rejected new_tab"
21
+ repeat 50 times
22
+ if (count of tabs of targetWindow) > beforeCount then exit repeat
23
+ delay 0.1
24
+ end repeat
25
+ if (count of tabs of targetWindow) <= beforeCount then error "Ghostty did not create a tab"
26
+ set newTabs to {}
27
+ repeat with candidateTab in tabs of targetWindow
28
+ if (id of candidateTab) is not in beforeIds then set end of newTabs to candidateTab
29
+ end repeat
30
+ if (count of newTabs) is not 1 then error "Ghostty tab identity is ambiguous"
31
+ set createdTab to item 1 of newTabs
32
+ set createdTerminal to focused terminal of createdTab
33
+ else
34
+ set surfaceConfiguration to new surface configuration
35
+ set initial working directory of surfaceConfiguration to threadDirectory
36
+ if placement is "window" then
37
+ set createdWindow to new window with configuration surfaceConfiguration
38
+ set createdTerminal to focused terminal of selected tab of createdWindow
39
+ else
40
+ set targetWindow to front window
41
+ set sourceTerminal to focused terminal of selected tab of targetWindow
42
+ set createdTerminal to split sourceTerminal direction right with configuration surfaceConfiguration
43
+ end if
44
+ end if
45
+ try
46
+ repeat 50 times
47
+ if (name of createdTerminal) is not "👻" and (name of createdTerminal) is not "" then exit repeat
48
+ delay 0.1
49
+ end repeat
50
+ if (name of createdTerminal) is "👻" or (name of createdTerminal) is "" then error "Ghostty surface has no shell"
51
+ input text shellCommand to createdTerminal
52
+ send key "enter" to createdTerminal
53
+ focus createdTerminal
54
+ return id of createdTerminal
55
+ on error
56
+ if placement is "tab" then
57
+ close tab createdTab
58
+ else if placement is "window" then
59
+ close window createdWindow
60
+ else
61
+ close createdTerminal
62
+ end if
63
+ error "Ghostty surface did not launch the Thread"
64
+ end try
65
+ end tell
66
+ end run`;
67
+ const FOCUS_SCRIPT = `on run argv
68
+ set targetId to item 1 of argv
69
+ tell application "Ghostty"
70
+ repeat with candidate in terminals
71
+ if (id of candidate) is targetId then
72
+ focus candidate
73
+ return "focused"
74
+ end if
75
+ end repeat
76
+ end tell
77
+ return "closed"
78
+ end run`;
79
+ const CLOSE_SCRIPT = `on run argv
80
+ set targetId to item 1 of argv
81
+ tell application "Ghostty"
82
+ repeat with candidate in terminals
83
+ if (id of candidate) is targetId then
84
+ close candidate
85
+ return "closed"
86
+ end if
87
+ end repeat
88
+ end tell
89
+ return "absent"
90
+ end run`;
91
+
92
+ export interface GhosttyThreadSurfacePersistence {
93
+ ghosttyClaimThreadSurface(threadId: string): Promise<boolean>;
94
+ ghosttyReleaseThreadSurface(threadId: string): Promise<void>;
95
+ ghosttyGetThreadSurface(threadId: string): Promise<GhosttyThreadSurfaceHandle | null>;
96
+ ghosttySetThreadSurface(threadId: string, handle: GhosttyThreadSurfaceHandle): Promise<void>;
97
+ }
98
+
99
+ export interface GhosttyEnv {
100
+ [name: string]: string | undefined;
101
+ TERM_PROGRAM?: string;
102
+ GHOSTTY_RESOURCES_DIR?: string;
103
+ }
104
+
105
+ interface GhosttyLaunchResult {
106
+ status: ThreadSurfaceOpenStatus;
107
+ retainClaim: boolean;
108
+ }
109
+
110
+ function runAppleScript(binPath: string, script: string, args: string[] = []): string | null {
111
+ try {
112
+ const result = spawnSync(binPath, ["-e", script, ...args], {
113
+ encoding: "utf8",
114
+ stdio: ["ignore", "pipe", "ignore"],
115
+ timeout: APPLESCRIPT_TIMEOUT_MS,
116
+ });
117
+
118
+ return result.status === 0 ? result.stdout.trim() : null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ export function resolveCueloopLaunchCommand(
125
+ executable = process.execPath,
126
+ entry = process.argv[1],
127
+ path = process.env.PATH,
128
+ ): string | null {
129
+ if (basename(executable) === "cueloop") return shellQuote(executable);
130
+
131
+ if (
132
+ entry &&
133
+ isAbsolute(entry) &&
134
+ entry.endsWith(join("packages", "cli", "src", "main.ts")) &&
135
+ existsSync(entry)
136
+ ) {
137
+ return `${shellQuote(executable)} run ${shellQuote(entry)}`;
138
+ }
139
+
140
+ for (const directory of path?.split(delimiter) ?? []) {
141
+ if (!isAbsolute(directory)) continue;
142
+ const candidate = join(directory, "cueloop");
143
+
144
+ try {
145
+ accessSync(candidate, constants.X_OK);
146
+ if (statSync(candidate).isFile()) return shellQuote(candidate);
147
+ } catch {
148
+ // A PATH entry without an executable cannot launch the Thread.
149
+ }
150
+ }
151
+
152
+ return null;
153
+ }
154
+
155
+ function shellQuote(value: string): string {
156
+ return `'${value.replaceAll("'", "'\\''")}'`;
157
+ }
158
+
159
+ /** Ghostty 1.3.0 introduced the native AppleScript commands used here. */
160
+ export function supportsGhosttyAppleScript(version: string | null): boolean {
161
+ const parsed = version?.match(/^(\d+)\.(\d+)\.(\d+)/);
162
+
163
+ if (!parsed) return false;
164
+ const major = Number(parsed[1]);
165
+ const minor = Number(parsed[2]);
166
+
167
+ return major > 1 || (major === 1 && minor >= 3);
168
+ }
169
+
170
+ /** Herdr selection happens before this adapter, even when it runs inside Ghostty. */
171
+ export function insideGhostty(env: GhosttyEnv = process.env): boolean {
172
+ return env.TERM_PROGRAM?.toLowerCase() === "ghostty" || Boolean(env.GHOSTTY_RESOURCES_DIR);
173
+ }
174
+
175
+ /** Reuse a live Ghostty terminal, reopen a closed one, or leave the Thread pending. */
176
+ export async function openGhosttyThreadSurface(
177
+ thread: Thread,
178
+ persistence: GhosttyThreadSurfacePersistence,
179
+ env: GhosttyEnv = process.env,
180
+ mode: GhosttyThreadSurface = loadGhosttyThreadSurface(),
181
+ binPath = "osascript",
182
+ platform: NodeJS.Platform = process.platform,
183
+ cueloopBinPath?: string | null,
184
+ ): Promise<ThreadSurfaceOpenStatus> {
185
+ if (platform !== "darwin" || !insideGhostty(env)) return "unavailable";
186
+ if (mode === "none") return "disabled";
187
+ if (!supportsGhosttyAppleScript(runAppleScript(binPath, VERSION_SCRIPT))) return "failed";
188
+
189
+ try {
190
+ if (!(await persistence.ghosttyClaimThreadSurface(thread.id))) return "failed";
191
+ } catch {
192
+ return "failed";
193
+ }
194
+
195
+ let result: GhosttyLaunchResult;
196
+
197
+ try {
198
+ result = await openClaimedGhosttyThreadSurface(
199
+ thread,
200
+ persistence,
201
+ mode,
202
+ binPath,
203
+ cueloopBinPath,
204
+ );
205
+ } catch {
206
+ result = { status: "failed", retainClaim: true };
207
+ }
208
+
209
+ if (result.retainClaim) return "failed";
210
+
211
+ try {
212
+ await persistence.ghosttyReleaseThreadSurface(thread.id);
213
+ } catch {
214
+ return "failed";
215
+ }
216
+
217
+ return result.status;
218
+ }
219
+
220
+ async function openClaimedGhosttyThreadSurface(
221
+ thread: Thread,
222
+ persistence: GhosttyThreadSurfacePersistence,
223
+ mode: Exclude<GhosttyThreadSurface, "none">,
224
+ binPath: string,
225
+ cueloopBinPath?: string | null,
226
+ ): Promise<GhosttyLaunchResult> {
227
+ let recorded: GhosttyThreadSurfaceHandle | null;
228
+
229
+ try {
230
+ recorded = await persistence.ghosttyGetThreadSurface(thread.id);
231
+ } catch {
232
+ return { status: "failed", retainClaim: false };
233
+ }
234
+
235
+ if (recorded) {
236
+ const result = runAppleScript(binPath, FOCUS_SCRIPT, [recorded.terminalId]);
237
+
238
+ if (result === "focused") return { status: "focused", retainClaim: false };
239
+ if (result !== "closed") return { status: "failed", retainClaim: false };
240
+ }
241
+
242
+ const launchCommand =
243
+ cueloopBinPath === undefined
244
+ ? resolveCueloopLaunchCommand()
245
+ : cueloopBinPath && shellQuote(cueloopBinPath);
246
+
247
+ if (!launchCommand) return { status: "failed", retainClaim: false };
248
+
249
+ const cwd = thread.artifact.meta.cwd ?? thread.workspace.repoRoot;
250
+ const terminalId = runAppleScript(binPath, GHOSTTY_OPEN_APPLESCRIPT, [
251
+ mode,
252
+ cwd,
253
+ `cd -- ${shellQuote(cwd)} && ${launchCommand} ${shellQuote(thread.id)}`,
254
+ ]);
255
+
256
+ if (!terminalId) return { status: "failed", retainClaim: false };
257
+
258
+ try {
259
+ await persistence.ghosttySetThreadSurface(thread.id, { terminalId });
260
+ } catch {
261
+ const closed = runAppleScript(binPath, CLOSE_SCRIPT, [terminalId]);
262
+
263
+ return { status: "failed", retainClaim: closed !== "closed" && closed !== "absent" };
264
+ }
265
+
266
+ return { status: "opened", retainClaim: false };
267
+ }
@@ -34,6 +34,21 @@ function createPlan(instance: DaemonCore) {
34
34
  }
35
35
 
36
36
  describe("durable harness delivery", () => {
37
+ test("reload finds only bindings for the active harness conversation", () => {
38
+ const first = core();
39
+ const thread = createPlan(first);
40
+ const current = first.harnessBind({
41
+ threadId: thread.id,
42
+ harness: "pi",
43
+ harnessSessionId: "pi-current",
44
+ });
45
+
46
+ first.harnessBind({ threadId: thread.id, harness: "pi", harnessSessionId: "pi-other" });
47
+ first.harnessBind({ threadId: thread.id, harness: "codex", harnessSessionId: "pi-current" });
48
+
49
+ expect(core().harnessBindingsForSession("pi", "pi-current")).toEqual([current]);
50
+ });
51
+
37
52
  test("redelivers after reload until the harness acknowledges the Message", () => {
38
53
  const first = core();
39
54
  const thread = createPlan(first);