tryharness 0.7.23 → 0.7.26

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 (61) hide show
  1. package/assets/opencode-plugin.js +48 -11
  2. package/assets/pi-extension.ts +105 -26
  3. package/cli.mjs +1238 -322
  4. package/lib/activity.mjs +197 -40
  5. package/lib/adapters.mjs +337 -54
  6. package/lib/agentnode.mjs +175 -35
  7. package/lib/atlas.mjs +383 -83
  8. package/lib/cards.mjs +174 -40
  9. package/lib/claudemem.mjs +74 -18
  10. package/lib/commands.mjs +13 -5
  11. package/lib/context.mjs +409 -71
  12. package/lib/control-client.mjs +5 -2
  13. package/lib/coordinator.mjs +274 -57
  14. package/lib/daemon.mjs +5268 -1024
  15. package/lib/desktop.mjs +149 -35
  16. package/lib/digest.mjs +54 -13
  17. package/lib/environment.mjs +103 -21
  18. package/lib/files.mjs +246 -13
  19. package/lib/firewall.mjs +169 -35
  20. package/lib/folders.mjs +179 -36
  21. package/lib/frames.mjs +214 -40
  22. package/lib/inference.mjs +463 -126
  23. package/lib/learn.mjs +78 -16
  24. package/lib/live.mjs +286 -61
  25. package/lib/localmodels.mjs +424 -83
  26. package/lib/mcp-stdio.mjs +868 -151
  27. package/lib/media.mjs +138 -28
  28. package/lib/members.json +1 -1
  29. package/lib/members.mjs +35 -9
  30. package/lib/models.mjs +342 -80
  31. package/lib/network.mjs +201 -42
  32. package/lib/notify.mjs +49 -12
  33. package/lib/nudge.mjs +184 -33
  34. package/lib/ocr.mjs +223 -60
  35. package/lib/onboard.mjs +321 -70
  36. package/lib/parsers.mjs +132 -41
  37. package/lib/paths.mjs +33 -10
  38. package/lib/personal.mjs +51 -13
  39. package/lib/phones.mjs +384 -60
  40. package/lib/places.mjs +555 -107
  41. package/lib/provenance.mjs +19 -4
  42. package/lib/receipts.mjs +51 -11
  43. package/lib/redact.mjs +512 -116
  44. package/lib/runner.mjs +345 -55
  45. package/lib/runtime-install.mjs +55 -14
  46. package/lib/screens.mjs +154 -38
  47. package/lib/sealed.mjs +157 -28
  48. package/lib/secrets.mjs +18 -5
  49. package/lib/service.mjs +170 -34
  50. package/lib/snippets.mjs +94 -18
  51. package/lib/speech.mjs +688 -175
  52. package/lib/store.mjs +1654 -307
  53. package/lib/swarm.mjs +596 -117
  54. package/lib/t3.mjs +400 -79
  55. package/lib/trails.mjs +285 -61
  56. package/lib/ui.mjs +95 -23
  57. package/lib/vectors.mjs +5 -2
  58. package/lib/wakes.mjs +284 -68
  59. package/lib/watches.mjs +247 -48
  60. package/lib/worktrees.mjs +186 -38
  61. package/package.json +3 -3
package/lib/places.mjs CHANGED
@@ -19,14 +19,33 @@ import { mainCheckout } from "./worktrees.mjs";
19
19
  // `reopen` is optional: a page it serves on this computer opens directly; a command is only ever copied for the person
20
20
  // to paste, never run. Only PLACE_ENV is kept of `env`.
21
21
 
22
- const PS = ["/bin/ps", "/usr/bin/ps"].find((file) => fs.existsSync(file)) || "ps";
22
+ const PS =
23
+ ["/bin/ps", "/usr/bin/ps"].find((file) => fs.existsSync(file)) || "ps";
23
24
 
24
25
  /** The names a place is read from: nothing else of a session's environment is kept. */
25
- export const PLACE_ENV = ["__CFBundleIdentifier", "TERM_PROGRAM", "TMUX", "TMUX_PANE", "WEZTERM_PANE", "KITTY_WINDOW_ID", "KITTY_LISTEN_ON", "WT_SESSION", "VSCODE_GIT_ASKPASS_NODE", "CLAUDE_CODE_ENTRYPOINT", "T3CODE_CLI_PATH"];
26
+ export const PLACE_ENV = [
27
+ "__CFBundleIdentifier",
28
+ "TERM_PROGRAM",
29
+ "TMUX",
30
+ "TMUX_PANE",
31
+ "WEZTERM_PANE",
32
+ "KITTY_WINDOW_ID",
33
+ "KITTY_LISTEN_ON",
34
+ "WT_SESSION",
35
+ "VSCODE_GIT_ASKPASS_NODE",
36
+ "CLAUDE_CODE_ENTRYPOINT",
37
+ "T3CODE_CLI_PATH",
38
+ ];
26
39
 
27
40
  export function placeEnv(env) {
28
41
  const picked = {};
29
- for (const name of PLACE_ENV) if (typeof env?.[name] === "string" && env[name] && env[name].length <= 1024) picked[name] = env[name];
42
+ for (const name of PLACE_ENV)
43
+ if (
44
+ typeof env?.[name] === "string" &&
45
+ env[name] &&
46
+ env[name].length <= 1024
47
+ )
48
+ picked[name] = env[name];
30
49
  return picked;
31
50
  }
32
51
 
@@ -35,31 +54,88 @@ export function processEnv(pid) {
35
54
  if (!Number.isInteger(pid) || pid <= 1) return {};
36
55
  if (process.platform === "linux") {
37
56
  try {
38
- return placeEnv(Object.fromEntries(fs.readFileSync(`/proc/${pid}/environ`, "utf8").split("\0").filter((entry) => entry.includes("="))
39
- .map((entry) => [entry.slice(0, entry.indexOf("=")), entry.slice(entry.indexOf("=") + 1)])));
40
- } catch { return {}; }
57
+ return placeEnv(
58
+ Object.fromEntries(
59
+ fs
60
+ .readFileSync(`/proc/${pid}/environ`, "utf8")
61
+ .split("\0")
62
+ .filter((entry) => entry.includes("="))
63
+ .map((entry) => [
64
+ entry.slice(0, entry.indexOf("=")),
65
+ entry.slice(entry.indexOf("=") + 1),
66
+ ])
67
+ )
68
+ );
69
+ } catch {
70
+ return {};
71
+ }
41
72
  }
42
73
  if (process.platform !== "darwin") return {};
43
74
  // `ps eww` prints the command, then each variable as NAME=value, space separated (values may hold spaces).
44
- const line = spawnSync(PS, ["eww", "-o", "command=", "-p", String(pid)], { encoding: "utf8", timeout: 3_000 }).stdout || "";
75
+ const line =
76
+ spawnSync(PS, ["eww", "-o", "command=", "-p", String(pid)], {
77
+ encoding: "utf8",
78
+ timeout: 3_000,
79
+ }).stdout || "";
45
80
  const env = {};
46
81
  for (const name of PLACE_ENV) {
47
- const found = line.match(new RegExp(`(?:^|\\s)${name}=(.*?)(?=\\s[A-Za-z_][A-Za-z0-9_]*=|\\s*$)`));
82
+ const found = line.match(
83
+ new RegExp(`(?:^|\\s)${name}=(.*?)(?=\\s[A-Za-z_][A-Za-z0-9_]*=|\\s*$)`)
84
+ );
48
85
  if (found?.[1]) env[name] = found[1];
49
86
  }
50
87
  return placeEnv(env);
51
88
  }
52
89
 
53
- export const appDirs = () => (process.env.HARNESS_APP_DIRS ? process.env.HARNESS_APP_DIRS.split(path.delimiter) : ["/Applications", path.join(os.homedir(), "Applications")]);
90
+ export const appDirs = () =>
91
+ process.env.HARNESS_APP_DIRS
92
+ ? process.env.HARNESS_APP_DIRS.split(path.delimiter)
93
+ : ["/Applications", path.join(os.homedir(), "Applications")];
54
94
 
55
95
  // ── Editors built on VS Code ──────────────────────────────────────────────────
56
96
 
57
97
  /** Each one's app, link scheme, command-line tool, settings folder and extensions folder. */
58
98
  const EDITORS = [
59
- { id: "cursor", name: "Cursor", bundle: "com.todesktop.230313mzl4w4u92", match: /cursor/i, scheme: "cursor", cli: "cursor", data: "Cursor", extensions: ".cursor" },
60
- { id: "windsurf", name: "Windsurf", bundle: "com.exafunction.windsurf", match: /windsurf/i, scheme: "windsurf", cli: "windsurf", data: "Windsurf", extensions: ".windsurf" },
61
- { id: "vscode-insiders", name: "Visual Studio Code - Insiders", bundle: "com.microsoft.VSCodeInsiders", match: /insiders/i, scheme: "vscode-insiders", cli: "code-insiders", data: "Code - Insiders", extensions: ".vscode-insiders" },
62
- { id: "vscode", name: "Visual Studio Code", bundle: "com.microsoft.VSCode", match: /visual studio code|microsoft vs code|[\\/]code[\\/]/i, scheme: "vscode", cli: "code", data: "Code", extensions: ".vscode" },
99
+ {
100
+ id: "cursor",
101
+ name: "Cursor",
102
+ bundle: "com.todesktop.230313mzl4w4u92",
103
+ match: /cursor/i,
104
+ scheme: "cursor",
105
+ cli: "cursor",
106
+ data: "Cursor",
107
+ extensions: ".cursor",
108
+ },
109
+ {
110
+ id: "windsurf",
111
+ name: "Windsurf",
112
+ bundle: "com.exafunction.windsurf",
113
+ match: /windsurf/i,
114
+ scheme: "windsurf",
115
+ cli: "windsurf",
116
+ data: "Windsurf",
117
+ extensions: ".windsurf",
118
+ },
119
+ {
120
+ id: "vscode-insiders",
121
+ name: "Visual Studio Code - Insiders",
122
+ bundle: "com.microsoft.VSCodeInsiders",
123
+ match: /insiders/i,
124
+ scheme: "vscode-insiders",
125
+ cli: "code-insiders",
126
+ data: "Code - Insiders",
127
+ extensions: ".vscode-insiders",
128
+ },
129
+ {
130
+ id: "vscode",
131
+ name: "Visual Studio Code",
132
+ bundle: "com.microsoft.VSCode",
133
+ match: /visual studio code|microsoft vs code|[\\/]code[\\/]/i,
134
+ scheme: "vscode",
135
+ cli: "code",
136
+ data: "Code",
137
+ extensions: ".vscode",
138
+ },
63
139
  ];
64
140
  export const EDITOR_IDS = EDITORS.map((editor) => editor.id);
65
141
  const editorById = (id) => EDITORS.find((editor) => editor.id === id) ?? null;
@@ -71,31 +147,75 @@ export function editorInstall(editorId, hint = "") {
71
147
  if (!editor) return null;
72
148
  const tries = [];
73
149
  const app = String(hint).match(/^(.*?\.app)(?:\/|$)/)?.[1];
74
- if (app) tries.push({ app, cli: path.join(app, "Contents", "Resources", "app", "bin", editor.cli) });
150
+ if (app)
151
+ tries.push({
152
+ app,
153
+ cli: path.join(app, "Contents", "Resources", "app", "bin", editor.cli),
154
+ });
75
155
  // Linux and Windows: the editor's executable sits beside its bin folder.
76
- else if (hint && path.isAbsolute(hint)) tries.push({ app: path.dirname(hint), cli: path.join(path.dirname(hint), "bin", editor.cli + (process.platform === "win32" ? ".cmd" : "")) });
156
+ else if (hint && path.isAbsolute(hint))
157
+ tries.push({
158
+ app: path.dirname(hint),
159
+ cli: path.join(
160
+ path.dirname(hint),
161
+ "bin",
162
+ editor.cli + (process.platform === "win32" ? ".cmd" : "")
163
+ ),
164
+ });
77
165
  for (const dir of appDirs()) {
78
166
  const at = path.join(dir, `${editor.name}.app`);
79
- tries.push({ app: at, cli: path.join(at, "Contents", "Resources", "app", "bin", editor.cli) });
167
+ tries.push({
168
+ app: at,
169
+ cli: path.join(at, "Contents", "Resources", "app", "bin", editor.cli),
170
+ });
80
171
  }
81
172
  return tries.find((entry) => fs.existsSync(entry.cli)) ?? null;
82
173
  }
83
174
 
84
175
  function editorData(editor) {
85
- if (process.env.HARNESS_EDITOR_ROOT) return path.join(process.env.HARNESS_EDITOR_ROOT, editor.data);
86
- if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Application Support", editor.data);
87
- if (process.platform === "win32") return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), editor.data);
88
- return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), editor.data);
176
+ if (process.env.HARNESS_EDITOR_ROOT)
177
+ return path.join(process.env.HARNESS_EDITOR_ROOT, editor.data);
178
+ if (process.platform === "darwin")
179
+ return path.join(
180
+ os.homedir(),
181
+ "Library",
182
+ "Application Support",
183
+ editor.data
184
+ );
185
+ if (process.platform === "win32")
186
+ return path.join(
187
+ process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"),
188
+ editor.data
189
+ );
190
+ return path.join(
191
+ process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"),
192
+ editor.data
193
+ );
89
194
  }
90
195
 
91
196
  /** Whether an editor has an extension installed (and not marked for removal). */
92
197
  export function editorHas(editorId, extension) {
93
198
  const editor = editorById(editorId);
94
199
  if (!editor) return false;
95
- const dir = path.join(process.env.HARNESS_EDITOR_ROOT || os.homedir(), editor.extensions, "extensions");
200
+ const dir = path.join(
201
+ process.env.HARNESS_EDITOR_ROOT || os.homedir(),
202
+ editor.extensions,
203
+ "extensions"
204
+ );
96
205
  let removed = {};
97
- try { removed = JSON.parse(fs.readFileSync(path.join(dir, ".obsolete"), "utf8")); } catch {}
98
- try { return fs.readdirSync(dir).some((name) => name.toLowerCase().startsWith(`${extension}-`) && !removed[name]); } catch { return false; }
206
+ try {
207
+ removed = JSON.parse(fs.readFileSync(path.join(dir, ".obsolete"), "utf8"));
208
+ } catch {}
209
+ try {
210
+ return fs
211
+ .readdirSync(dir)
212
+ .some(
213
+ (name) =>
214
+ name.toLowerCase().startsWith(`${extension}-`) && !removed[name]
215
+ );
216
+ } catch {
217
+ return false;
218
+ }
99
219
  }
100
220
 
101
221
  /**
@@ -108,11 +228,25 @@ export function editorFolder(editorId, folder) {
108
228
  const root = path.join(editorData(editor), "User", "workspaceStorage");
109
229
  let best = null;
110
230
  let dirs = [];
111
- try { dirs = fs.readdirSync(root); } catch {}
231
+ try {
232
+ dirs = fs.readdirSync(root);
233
+ } catch {}
112
234
  for (const dir of dirs) {
113
235
  let opened;
114
- try { opened = fileURLToPath(JSON.parse(fs.readFileSync(path.join(root, dir, "workspace.json"), "utf8")).folder); } catch { continue; }
115
- if ((folder === opened || folder.startsWith(opened + path.sep)) && (!best || opened.length > best.length)) best = opened;
236
+ try {
237
+ opened = fileURLToPath(
238
+ JSON.parse(
239
+ fs.readFileSync(path.join(root, dir, "workspace.json"), "utf8")
240
+ ).folder
241
+ );
242
+ } catch {
243
+ continue;
244
+ }
245
+ if (
246
+ (folder === opened || folder.startsWith(opened + path.sep)) &&
247
+ (!best || opened.length > best.length)
248
+ )
249
+ best = opened;
116
250
  }
117
251
  return best ?? folder;
118
252
  }
@@ -122,13 +256,24 @@ export function editorFolder(editorId, folder) {
122
256
  * else a new one on it. No link, so the editor asks nothing; its tool brings itself forward.
123
257
  */
124
258
  export function openEditorFolder(place, folder) {
125
- if (!place?.cli || !folder || !path.isAbsolute(folder) || !fs.existsSync(place.cli)) return false;
259
+ if (
260
+ !place?.cli ||
261
+ !folder ||
262
+ !path.isAbsolute(folder) ||
263
+ !fs.existsSync(place.cli)
264
+ )
265
+ return false;
126
266
  const env = { ...process.env };
127
267
  // Not inside one of its terminals: a tool started there would replace that window's folder.
128
268
  delete env.VSCODE_IPC_HOOK_CLI;
129
269
  delete env.ELECTRON_RUN_AS_NODE;
130
270
  const windows = process.platform === "win32";
131
- const child = spawn(place.cli, [windows ? `"${folder}"` : folder], { env, detached: !windows, stdio: "ignore", shell: windows });
271
+ const child = spawn(place.cli, [windows ? `"${folder}"` : folder], {
272
+ env,
273
+ detached: !windows,
274
+ stdio: "ignore",
275
+ shell: windows,
276
+ });
132
277
  child.on("error", () => {});
133
278
  child.unref();
134
279
  return true;
@@ -137,22 +282,60 @@ export function openEditorFolder(place, folder) {
137
282
  /** How many times the person started a command in an editor's terminal, by the editor's own record of its last commands. */
138
283
  export function editorTerminalStarts(editorId, command) {
139
284
  const editor = editorById(editorId);
140
- const db = editor ? path.join(editorData(editor), "User", "globalStorage", "state.vscdb") : null;
285
+ const db = editor
286
+ ? path.join(editorData(editor), "User", "globalStorage", "state.vscdb")
287
+ : null;
141
288
  if (!db || !fs.existsSync(db) || !fs.existsSync(SQLITE)) return 0;
142
- const found = spawnSync(SQLITE, ["-readonly", "-cmd", ".timeout 500", db, "select value from ItemTable where key = 'terminal.history.entries.commands';"], { encoding: "utf8", timeout: 3_000 });
289
+ const found = spawnSync(
290
+ SQLITE,
291
+ [
292
+ "-readonly",
293
+ "-cmd",
294
+ ".timeout 500",
295
+ db,
296
+ "select value from ItemTable where key = 'terminal.history.entries.commands';",
297
+ ],
298
+ { encoding: "utf8", timeout: 3_000 }
299
+ );
143
300
  let entries = [];
144
- try { entries = JSON.parse(found.stdout || "{}").entries || []; } catch { return 0; }
145
- return entries.filter((entry) => path.basename(String(entry?.key || "").trim().split(/\s+/)[0] || "") === command).length;
301
+ try {
302
+ entries = JSON.parse(found.stdout || "{}").entries || [];
303
+ } catch {
304
+ return 0;
305
+ }
306
+ return entries.filter(
307
+ (entry) =>
308
+ path.basename(
309
+ String(entry?.key || "")
310
+ .trim()
311
+ .split(/\s+/)[0] || ""
312
+ ) === command
313
+ ).length;
146
314
  }
147
315
 
148
316
  // ── The agents ────────────────────────────────────────────────────────────────
149
317
 
150
318
  /** How each agent picks a session up again; its own panel in an editor, when it has one; its name in T3 Code. */
151
319
  const AGENTS = {
152
- "claude-code": { command: "claude", resume: "claude --resume", t3: "claudeAgent",
153
- panel: { extension: "anthropic.claude-code", link: (scheme, id) => `${scheme}://anthropic.claude-code/open?session=${id}` } },
154
- codex: { command: "codex", resume: "codex resume", t3: "codex",
155
- panel: { extension: "openai.chatgpt", link: (scheme, id) => `${scheme}://openai.chatgpt/local/${id}` } },
320
+ "claude-code": {
321
+ command: "claude",
322
+ resume: "claude --resume",
323
+ t3: "claudeAgent",
324
+ panel: {
325
+ extension: "anthropic.claude-code",
326
+ link: (scheme, id) =>
327
+ `${scheme}://anthropic.claude-code/open?session=${id}`,
328
+ },
329
+ },
330
+ codex: {
331
+ command: "codex",
332
+ resume: "codex resume",
333
+ t3: "codex",
334
+ panel: {
335
+ extension: "openai.chatgpt",
336
+ link: (scheme, id) => `${scheme}://openai.chatgpt/local/${id}`,
337
+ },
338
+ },
156
339
  pi: { command: "pi", resume: "pi --session" },
157
340
  opencode: { command: "opencode", resume: "opencode --session" },
158
341
  };
@@ -175,7 +358,14 @@ export function editorRunsOpenTasks(editorId) {
175
358
  const editor = editorById(editorId);
176
359
  if (!editor) return false;
177
360
  let settings = "";
178
- try { settings = fs.readFileSync(path.join(editorData(editor), "User", "settings.json"), "utf8"); } catch { return true; }
361
+ try {
362
+ settings = fs.readFileSync(
363
+ path.join(editorData(editor), "User", "settings.json"),
364
+ "utf8"
365
+ );
366
+ } catch {
367
+ return true;
368
+ }
179
369
  return !/"task\.allowAutomaticTasks"\s*:\s*"off"/.test(settings);
180
370
  }
181
371
 
@@ -185,25 +375,71 @@ export function editorRunsOpenTasks(editorId) {
185
375
  * and to allow such tasks). Through the person's login shell, so the agent runs with their own setup. The file lives in
186
376
  * Harness's folder, never in the person's; its path, or null when the agent has no resume command.
187
377
  */
188
- export function editorSessionWorkspace(agent, sessionId, folder, window = folder) {
378
+ export function editorSessionWorkspace(
379
+ agent,
380
+ sessionId,
381
+ folder,
382
+ window = folder
383
+ ) {
189
384
  const resume = AGENTS[agent]?.resume;
190
- if (!resume || !PLAIN_ID.test(String(sessionId || "")) || !folder || !path.isAbsolute(folder)) return null;
385
+ if (
386
+ !resume ||
387
+ !PLAIN_ID.test(String(sessionId || "")) ||
388
+ !folder ||
389
+ !path.isAbsolute(folder)
390
+ )
391
+ return null;
191
392
  // Through the person's login shell (VS Code's default terminal on Windows), with a Node the agent accepts first.
192
- const shell = process.platform === "win32" ? null : process.env.SHELL || os.userInfo().shell || "/bin/sh";
393
+ const shell =
394
+ process.platform === "win32"
395
+ ? null
396
+ : process.env.SHELL || os.userInfo().shell || "/bin/sh";
193
397
  const { bin } = agentEnvironment(AGENTS[agent].command);
194
- const name = `${path.basename(window || folder)} - ${agent} ${String(sessionId).replace(/^ses_/, "").slice(0, 8)}`.replace(/[^A-Za-z0-9 ._-]/g, "-");
398
+ const name =
399
+ `${path.basename(window || folder)} - ${agent} ${String(sessionId).replace(/^ses_/, "").slice(0, 8)}`.replace(
400
+ /[^A-Za-z0-9 ._-]/g,
401
+ "-"
402
+ );
195
403
  const file = path.join(paths.home, "editor", `${name}.code-workspace`);
196
404
  fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
197
- fs.writeFileSync(file, JSON.stringify({
198
- folders: [{ path: window || folder }],
199
- tasks: { version: "2.0.0", tasks: [{
200
- label: `${agent} session ${sessionId}`, type: "shell", command: `${resume} ${sessionId}`,
201
- options: { cwd: folder, ...(shell ? { shell: { executable: shell, args: ["-l", "-i", "-c"] } } : {}), ...(bin ? { env: { PATH: `${bin}${path.delimiter}\${env:PATH}` } } : {}) },
202
- runOptions: { runOn: "folderOpen" },
203
- presentation: { reveal: "always", focus: true, panel: "dedicated", clear: true },
204
- problemMatcher: [],
205
- }] },
206
- }, null, 2), { mode: 0o600 });
405
+ fs.writeFileSync(
406
+ file,
407
+ JSON.stringify(
408
+ {
409
+ folders: [{ path: window || folder }],
410
+ tasks: {
411
+ version: "2.0.0",
412
+ tasks: [
413
+ {
414
+ label: `${agent} session ${sessionId}`,
415
+ type: "shell",
416
+ command: `${resume} ${sessionId}`,
417
+ options: {
418
+ cwd: folder,
419
+ ...(shell
420
+ ? { shell: { executable: shell, args: ["-l", "-i", "-c"] } }
421
+ : {}),
422
+ ...(bin
423
+ ? { env: { PATH: `${bin}${path.delimiter}\${env:PATH}` } }
424
+ : {}),
425
+ },
426
+ runOptions: { runOn: "folderOpen" },
427
+ presentation: {
428
+ reveal: "always",
429
+ focus: true,
430
+ panel: "dedicated",
431
+ clear: true,
432
+ },
433
+ problemMatcher: [],
434
+ },
435
+ ],
436
+ },
437
+ },
438
+ null,
439
+ 2
440
+ ),
441
+ { mode: 0o600 }
442
+ );
207
443
  return file;
208
444
  }
209
445
 
@@ -211,7 +447,13 @@ export function editorSessionWorkspace(agent, sessionId, folder, window = folder
211
447
  export function panelLink(agent, editorId, sessionId) {
212
448
  const panel = AGENTS[agent]?.panel;
213
449
  const editor = editorById(editorId);
214
- if (!panel || !editor || !UUID.test(String(sessionId || "")) || !editorHas(editorId, panel.extension)) return null;
450
+ if (
451
+ !panel ||
452
+ !editor ||
453
+ !UUID.test(String(sessionId || "")) ||
454
+ !editorHas(editorId, panel.extension)
455
+ )
456
+ return null;
215
457
  return panel.link(editor.scheme, sessionId);
216
458
  }
217
459
 
@@ -219,22 +461,41 @@ export function panelLink(agent, editorId, sessionId) {
219
461
 
220
462
  const SQLITE = "/usr/bin/sqlite3";
221
463
  const T3_BUNDLE = "com.t3tools.t3code";
222
- export const t3Home = () => process.env.T3_HOME || path.join(os.homedir(), ".t3", "userdata");
464
+ export const t3Home = () =>
465
+ process.env.T3_HOME || path.join(os.homedir(), ".t3", "userdata");
223
466
 
224
467
  /** T3 Code's state database: 0.0.46 nightlies (Orchestrator V2) keep a copy of their own, statev2.sqlite; the newer file is the one in use. */
225
468
  function t3Database(home) {
226
- const [found] = ["statev2.sqlite", "state.sqlite"].map((name) => path.join(home, name))
227
- .map((file) => { try { return { file, at: fs.statSync(file).mtimeMs }; } catch { return null; } })
228
- .filter(Boolean).sort((a, b) => b.at - a.at);
469
+ const [found] = ["statev2.sqlite", "state.sqlite"]
470
+ .map((name) => path.join(home, name))
471
+ .map((file) => {
472
+ try {
473
+ return { file, at: fs.statSync(file).mtimeMs };
474
+ } catch {
475
+ return null;
476
+ }
477
+ })
478
+ .filter(Boolean)
479
+ .sort((a, b) => b.at - a.at);
229
480
  return found?.file ?? null;
230
481
  }
231
482
  function t3Query(home, sql) {
232
483
  const db = t3Database(home);
233
484
  if (!db || !fs.existsSync(SQLITE)) return null;
234
- const found = spawnSync(SQLITE, ["-readonly", "-cmd", ".timeout 500", db, sql], { encoding: "utf8", timeout: 3_000 });
485
+ const found = spawnSync(
486
+ SQLITE,
487
+ ["-readonly", "-cmd", ".timeout 500", db, sql],
488
+ { encoding: "utf8", timeout: 3_000 }
489
+ );
235
490
  return found.status === 0 ? (found.stdout || "").trim() : null;
236
491
  }
237
- const t3Tables = (home) => new Set((t3Query(home, "select name from sqlite_master where type = 'table';") || "").split("\n"));
492
+ const t3Tables = (home) =>
493
+ new Set(
494
+ (
495
+ t3Query(home, "select name from sqlite_master where type = 'table';") ||
496
+ ""
497
+ ).split("\n")
498
+ );
238
499
 
239
500
  /**
240
501
  * The T3 Code thread that has a provider session: the thread T3 ran it in (nightlies record the session on the thread's
@@ -246,31 +507,77 @@ export function t3Thread(sessionId, home = t3Home()) {
246
507
  const tables = t3Tables(home);
247
508
  // The id is checked above: letters, digits and . _ : - only.
248
509
  const parts = [];
249
- if (tables.has("orchestration_v2_projection_provider_threads")) parts.push(`select thread_id, updated_at as at from orchestration_v2_projection_provider_threads where thread_id is not null and json_extract(payload_json, '$.nativeThreadRef.nativeId') = '${sessionId}'`);
250
- if (tables.has("orchestration_v2_projection_threads")) parts.push(`select thread_id, updated_at as at from orchestration_v2_projection_threads where deleted_at is null and thread_id in ('${sessionId}', 'import:claudeAgent:${sessionId}', 'import:codex:${sessionId}')`);
251
- else if (tables.has("provider_session_runtime")) parts.push(`select thread_id, last_seen_at as at from provider_session_runtime where thread_id like '%:${sessionId}' or json_extract(resume_cursor_json, '$.resume') = '${sessionId}'`);
510
+ if (tables.has("orchestration_v2_projection_provider_threads"))
511
+ parts.push(
512
+ `select thread_id, updated_at as at from orchestration_v2_projection_provider_threads where thread_id is not null and json_extract(payload_json, '$.nativeThreadRef.nativeId') = '${sessionId}'`
513
+ );
514
+ if (tables.has("orchestration_v2_projection_threads"))
515
+ parts.push(
516
+ `select thread_id, updated_at as at from orchestration_v2_projection_threads where deleted_at is null and thread_id in ('${sessionId}', 'import:claudeAgent:${sessionId}', 'import:codex:${sessionId}')`
517
+ );
518
+ else if (tables.has("provider_session_runtime"))
519
+ parts.push(
520
+ `select thread_id, last_seen_at as at from provider_session_runtime where thread_id like '%:${sessionId}' or json_extract(resume_cursor_json, '$.resume') = '${sessionId}'`
521
+ );
252
522
  if (!parts.length) return null;
253
- return t3Query(home, `select thread_id from (${parts.join(" union all ")}) order by at desc limit 1;`) || null;
523
+ return (
524
+ t3Query(
525
+ home,
526
+ `select thread_id from (${parts.join(" union all ")}) order by at desc limit 1;`
527
+ ) || null
528
+ );
254
529
  }
255
530
 
256
531
  /** T3 Code's own link to one of its threads (t3code://threads/<environment>/<thread>). */
257
532
  export function t3Link(threadId, home = t3Home()) {
258
533
  let environment = "";
259
- try { environment = fs.readFileSync(path.join(home, "environment-id"), "utf8").trim(); } catch { return null; }
260
- return threadId && PLAIN_ID.test(environment) ? `t3code://threads/${environment}/${encodeURIComponent(threadId)}` : null;
534
+ try {
535
+ environment = fs
536
+ .readFileSync(path.join(home, "environment-id"), "utf8")
537
+ .trim();
538
+ } catch {
539
+ return null;
540
+ }
541
+ return threadId && PLAIN_ID.test(environment)
542
+ ? `t3code://threads/${environment}/${encodeURIComponent(threadId)}`
543
+ : null;
261
544
  }
262
545
 
263
- const realDir = (dir) => { try { return fs.realpathSync(dir); } catch { return null; } };
546
+ const realDir = (dir) => {
547
+ try {
548
+ return fs.realpathSync(dir);
549
+ } catch {
550
+ return null;
551
+ }
552
+ };
264
553
 
265
554
  /** The T3 Code project for a folder: the one T3 opened on the folder itself or on the repository it is in. */
266
555
  export function t3Project(folder, home = t3Home()) {
267
556
  if (!folder || !t3Tables(home).has("projection_projects")) return null;
268
- const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: folder, encoding: "utf8" });
557
+ const top = spawnSync("git", ["rev-parse", "--show-toplevel"], {
558
+ cwd: folder,
559
+ encoding: "utf8",
560
+ });
269
561
  const root = top.status === 0 ? top.stdout.trim() : null;
270
562
  // A worktree of a project's checkout (a thread's copy here, T3's own) is that project's, as T3 counts it.
271
- const wanted = new Set([realDir(folder), root && realDir(root), root && realDir(mainCheckout(root))].filter(Boolean));
272
- const rows = (t3Query(home, "select project_id || char(9) || workspace_root from projection_projects where deleted_at is null;") || "").split("\n").map((line) => line.split("\t"));
273
- const found = rows.find(([id, root]) => id && root && wanted.has(realDir(root)));
563
+ const wanted = new Set(
564
+ [
565
+ realDir(folder),
566
+ root && realDir(root),
567
+ root && realDir(mainCheckout(root)),
568
+ ].filter(Boolean)
569
+ );
570
+ const rows = (
571
+ t3Query(
572
+ home,
573
+ "select project_id || char(9) || workspace_root from projection_projects where deleted_at is null;"
574
+ ) || ""
575
+ )
576
+ .split("\n")
577
+ .map((line) => line.split("\t"));
578
+ const found = rows.find(
579
+ ([id, root]) => id && root && wanted.has(realDir(root))
580
+ );
274
581
  return found ? { id: found[0], root: realDir(found[1]) } : null;
275
582
  }
276
583
 
@@ -280,28 +587,55 @@ export function t3Project(folder, home = t3Home()) {
280
587
  */
281
588
  export function t3ThreadIn(folder, home = t3Home()) {
282
589
  const dir = folder ? realDir(folder) : null;
283
- if (!dir || !t3Tables(home).has("orchestration_v2_projection_threads")) return null;
590
+ if (!dir || !t3Tables(home).has("orchestration_v2_projection_threads"))
591
+ return null;
284
592
  const project = t3Project(folder, home);
285
- const rows = (t3Query(home, "select thread_id || char(9) || coalesce(project_id, '') || char(9) || coalesce(json_extract(payload_json, '$.worktreePath'), '') from orchestration_v2_projection_threads where deleted_at is null and archived_at is null order by updated_at desc;") || "")
286
- .split("\n").map((line) => line.split("\t")).filter(([id]) => id);
593
+ const rows = (
594
+ t3Query(
595
+ home,
596
+ "select thread_id || char(9) || coalesce(project_id, '') || char(9) || coalesce(json_extract(payload_json, '$.worktreePath'), '') from orchestration_v2_projection_threads where deleted_at is null and archived_at is null order by updated_at desc;"
597
+ ) || ""
598
+ )
599
+ .split("\n")
600
+ .map((line) => line.split("\t"))
601
+ .filter(([id]) => id);
287
602
  const inProject = project ? rows.filter(([, id]) => id === project.id) : [];
288
- const found = rows.find(([, , tree]) => tree && realDir(tree) === dir)
289
- ?? (dir === project?.root ? inProject.find(([, , tree]) => !tree) : null) ?? inProject[0];
603
+ const found =
604
+ rows.find(([, , tree]) => tree && realDir(tree) === dir) ??
605
+ (dir === project?.root ? inProject.find(([, , tree]) => !tree) : null) ??
606
+ inProject[0];
290
607
  return found?.[0] ?? null;
291
608
  }
292
609
 
293
610
  /** Every folder T3 Code has a project for. */
294
611
  export function t3Projects(home = t3Home()) {
295
612
  if (!t3Tables(home).has("projection_projects")) return [];
296
- return (t3Query(home, "select workspace_root from projection_projects where deleted_at is null;") || "").split("\n").map(realDir).filter(Boolean);
613
+ return (
614
+ t3Query(
615
+ home,
616
+ "select workspace_root from projection_projects where deleted_at is null;"
617
+ ) || ""
618
+ )
619
+ .split("\n")
620
+ .map(realDir)
621
+ .filter(Boolean);
297
622
  }
298
623
 
299
624
  /** The model (and its options) the person last ran an agent with in T3 Code, as T3 records each thread's choice. */
300
625
  export function t3Model(agent, home = t3Home()) {
301
626
  const provider = t3Provider(agent);
302
- if (!provider || !t3Tables(home).has("orchestration_v2_projection_threads")) return null;
303
- const raw = t3Query(home, `select json_extract(payload_json, '$.modelSelection') from orchestration_v2_projection_threads where deleted_at is null and json_extract(payload_json, '$.modelSelection.instanceId') = '${provider}' order by updated_at desc limit 1;`);
304
- try { const model = JSON.parse(raw || "null"); return typeof model?.model === "string" ? model : null; } catch { return null; }
627
+ if (!provider || !t3Tables(home).has("orchestration_v2_projection_threads"))
628
+ return null;
629
+ const raw = t3Query(
630
+ home,
631
+ `select json_extract(payload_json, '$.modelSelection') from orchestration_v2_projection_threads where deleted_at is null and json_extract(payload_json, '$.modelSelection.instanceId') = '${provider}' order by updated_at desc limit 1;`
632
+ );
633
+ try {
634
+ const model = JSON.parse(raw || "null");
635
+ return typeof model?.model === "string" ? model : null;
636
+ } catch {
637
+ return null;
638
+ }
305
639
  }
306
640
 
307
641
  /**
@@ -312,18 +646,36 @@ export function t3Model(agent, home = t3Home()) {
312
646
  export function t3Working(agent, home = t3Home()) {
313
647
  const provider = t3Provider(agent);
314
648
  const tables = t3Tables(home);
315
- if (!provider || !tables.has("orchestration_v2_projection_provider_threads") || !tables.has("orchestration_v2_projection_threads")) return [];
649
+ if (
650
+ !provider ||
651
+ !tables.has("orchestration_v2_projection_provider_threads") ||
652
+ !tables.has("orchestration_v2_projection_threads")
653
+ )
654
+ return [];
316
655
  const projects = tables.has("projection_projects");
317
- const rows = t3Query(home, `select p.thread_id || char(9) || json_extract(p.payload_json, '$.nativeThreadRef.nativeId') || char(9) || coalesce(json_extract(t.payload_json, '$.worktreePath'), ${projects ? "r.workspace_root, " : ""}'') || char(9) || p.updated_at
656
+ const rows = t3Query(
657
+ home,
658
+ `select p.thread_id || char(9) || json_extract(p.payload_json, '$.nativeThreadRef.nativeId') || char(9) || coalesce(json_extract(t.payload_json, '$.worktreePath'), ${projects ? "r.workspace_root, " : ""}'') || char(9) || p.updated_at
318
659
  from orchestration_v2_projection_provider_threads p join orchestration_v2_projection_threads t on t.thread_id = p.thread_id${projects ? " left join projection_projects r on r.project_id = t.project_id" : ""}
319
- where p.provider = '${provider}' and p.status = 'active' and t.deleted_at is null;`);
320
- return (rows || "").split("\n").map((line) => line.split("\t")).filter(([thread, session]) => thread && PLAIN_ID.test(session || ""))
321
- .map(([thread, session, folder, at]) => ({ thread, sessionId: session, folder: folder || null, at: Date.parse(at) || 0 }));
660
+ where p.provider = '${provider}' and p.status = 'active' and t.deleted_at is null;`
661
+ );
662
+ return (rows || "")
663
+ .split("\n")
664
+ .map((line) => line.split("\t"))
665
+ .filter(([thread, session]) => thread && PLAIN_ID.test(session || ""))
666
+ .map(([thread, session, folder, at]) => ({
667
+ thread,
668
+ sessionId: session,
669
+ folder: folder || null,
670
+ at: Date.parse(at) || 0,
671
+ }));
322
672
  }
323
673
 
324
674
  /** True while T3 Code is open on this computer. */
325
675
  export function t3Open() {
326
- const running = spawnSync(PS, ["-axo", "comm="], { encoding: "utf8", maxBuffer: 16 << 20 }).stdout || "";
676
+ const running =
677
+ spawnSync(PS, ["-axo", "comm="], { encoding: "utf8", maxBuffer: 16 << 20 })
678
+ .stdout || "";
327
679
  return /\/T3 Code\b[^/\n]*\.app\/Contents\/MacOS\//.test(running);
328
680
  }
329
681
 
@@ -333,9 +685,19 @@ export function t3Runs(agent, sinceIso, home = t3Home()) {
333
685
  if (!provider || !/^[0-9TZ:.-]+$/.test(sinceIso)) return 0;
334
686
  const tables = t3Tables(home);
335
687
  const parts = [];
336
- if (tables.has("orchestration_v2_projection_provider_threads")) parts.push(`select thread_id from orchestration_v2_projection_provider_threads where provider = '${provider}' and updated_at > '${sinceIso}'`);
337
- if (tables.has("provider_session_runtime")) parts.push(`select thread_id from provider_session_runtime where provider_name = '${provider}' and last_seen_at > '${sinceIso}' and (thread_id not like 'import:%' or resume_cursor_json like '%turnCount%')`);
338
- return parts.length ? Number(t3Query(home, `select count(*) from (${parts.join(" union ")});`) || 0) || 0 : 0;
688
+ if (tables.has("orchestration_v2_projection_provider_threads"))
689
+ parts.push(
690
+ `select thread_id from orchestration_v2_projection_provider_threads where provider = '${provider}' and updated_at > '${sinceIso}'`
691
+ );
692
+ if (tables.has("provider_session_runtime"))
693
+ parts.push(
694
+ `select thread_id from provider_session_runtime where provider_name = '${provider}' and last_seen_at > '${sinceIso}' and (thread_id not like 'import:%' or resume_cursor_json like '%turnCount%')`
695
+ );
696
+ return parts.length
697
+ ? Number(
698
+ t3Query(home, `select count(*) from (${parts.join(" union ")});`) || 0
699
+ ) || 0
700
+ : 0;
339
701
  }
340
702
 
341
703
  // T3 Code opens thread and prompt links from other apps only in its newer builds (0.0.46 nightlies on); 0.0.45 hands
@@ -345,27 +707,62 @@ const t3LinkSupport = new Map();
345
707
  export function t3OpensLinks(app) {
346
708
  const archive = path.join(app, "Contents", "Resources", "app.asar");
347
709
  let stamp = "";
348
- try { stamp = String(fs.statSync(archive).mtimeMs); } catch { return false; }
710
+ try {
711
+ stamp = String(fs.statSync(archive).mtimeMs);
712
+ } catch {
713
+ return false;
714
+ }
349
715
  const known = t3LinkSupport.get(archive);
350
716
  if (known?.stamp === stamp) return known.ok;
351
717
  let ok = false;
352
- try { ok = fs.readFileSync(archive).includes("so T3 Code did not open it"); } catch {}
718
+ try {
719
+ ok = fs.readFileSync(archive).includes("so T3 Code did not open it");
720
+ } catch {}
353
721
  t3LinkSupport.set(archive, { stamp, ok });
354
722
  return ok;
355
723
  }
356
724
 
357
725
  /** T3 Code's app: the one running when several builds are installed (stable and nightly side by side), else one that opens links, else any. */
358
726
  export function t3App() {
359
- const apps = appDirs().flatMap((dir) => { try { return fs.readdirSync(dir).filter((entry) => /^T3 Code\b.*\.app$/.test(entry)).map((entry) => path.join(dir, entry)); } catch { return []; } });
727
+ const apps = appDirs().flatMap((dir) => {
728
+ try {
729
+ return fs
730
+ .readdirSync(dir)
731
+ .filter((entry) => /^T3 Code\b.*\.app$/.test(entry))
732
+ .map((entry) => path.join(dir, entry));
733
+ } catch {
734
+ return [];
735
+ }
736
+ });
360
737
  if (!apps.length) return null;
361
- const running = apps.length > 1 ? spawnSync(PS, ["-axo", "comm="], { encoding: "utf8", maxBuffer: 16 << 20 }).stdout || "" : "";
362
- const app = apps.find((candidate) => running.includes(`${candidate}/Contents/MacOS/`)) ?? apps.find((candidate) => t3OpensLinks(candidate)) ?? apps[0];
738
+ const running =
739
+ apps.length > 1
740
+ ? spawnSync(PS, ["-axo", "comm="], {
741
+ encoding: "utf8",
742
+ maxBuffer: 16 << 20,
743
+ }).stdout || ""
744
+ : "";
745
+ const app =
746
+ apps.find((candidate) =>
747
+ running.includes(`${candidate}/Contents/MacOS/`)
748
+ ) ??
749
+ apps.find((candidate) => t3OpensLinks(candidate)) ??
750
+ apps[0];
363
751
  return { name: path.basename(app, ".app"), app };
364
752
  }
365
753
 
366
754
  // ── A place ───────────────────────────────────────────────────────────────────
367
755
 
368
- const TERMINAL_NAMES = { Apple_Terminal: "Terminal", "iTerm.app": "iTerm", WezTerm: "WezTerm", ghostty: "Ghostty", WarpTerminal: "Warp", Hyper: "Hyper", kitty: "kitty", alacritty: "Alacritty" };
756
+ const TERMINAL_NAMES = {
757
+ Apple_Terminal: "Terminal",
758
+ "iTerm.app": "iTerm",
759
+ WezTerm: "WezTerm",
760
+ ghostty: "Ghostty",
761
+ WarpTerminal: "Warp",
762
+ Hyper: "Hyper",
763
+ kitty: "kitty",
764
+ alacritty: "Alacritty",
765
+ };
369
766
 
370
767
  /**
371
768
  * Where a session runs, from its environment and the app found up its process tree (`host`, live.mjs hostApp):
@@ -376,32 +773,83 @@ const TERMINAL_NAMES = { Apple_Terminal: "Terminal", "iTerm.app": "iTerm", WezTe
376
773
  * app any other app.
377
774
  * The environment comes first: the app a process sits under can be a different one (a terminal started from an editor).
378
775
  */
379
- export function placeFrom({ env = {}, host = null, tty = null, folder = null }) {
776
+ export function placeFrom({
777
+ env = {},
778
+ host = null,
779
+ tty = null,
780
+ folder = null,
781
+ }) {
380
782
  const bundle = env.__CFBundleIdentifier || null;
381
783
  const hostName = host?.name ?? "";
382
- const editorOf = () => EDITORS.find((editor) => editor.bundle === bundle) ?? EDITORS.find((editor) => editor.match.test(env.VSCODE_GIT_ASKPASS_NODE || ""))
383
- ?? (env.TERM_PROGRAM === "vscode" || env.CLAUDE_CODE_ENTRYPOINT === "claude-vscode" ? editorById("vscode") : null);
784
+ const editorOf = () =>
785
+ EDITORS.find((editor) => editor.bundle === bundle) ??
786
+ EDITORS.find((editor) =>
787
+ editor.match.test(env.VSCODE_GIT_ASKPASS_NODE || "")
788
+ ) ??
789
+ (env.TERM_PROGRAM === "vscode" ||
790
+ env.CLAUDE_CODE_ENTRYPOINT === "claude-vscode"
791
+ ? editorById("vscode")
792
+ : null);
384
793
  const editorPlace = (editor) => {
385
- const install = editorInstall(editor.id, env.VSCODE_GIT_ASKPASS_NODE || host?.app || "");
386
- const panel = env.CLAUDE_CODE_ENTRYPOINT === "claude-vscode" || env.TERM_PROGRAM !== "vscode";
387
- return { kind: "editor", editor: editor.id, name: editor.name, app: install?.app ?? host?.app ?? null, cli: install?.cli ?? null, surface: panel ? "panel" : "terminal", folder };
794
+ const install = editorInstall(
795
+ editor.id,
796
+ env.VSCODE_GIT_ASKPASS_NODE || host?.app || ""
797
+ );
798
+ const panel =
799
+ env.CLAUDE_CODE_ENTRYPOINT === "claude-vscode" ||
800
+ env.TERM_PROGRAM !== "vscode";
801
+ return {
802
+ kind: "editor",
803
+ editor: editor.id,
804
+ name: editor.name,
805
+ app: install?.app ?? host?.app ?? null,
806
+ cli: install?.cli ?? null,
807
+ surface: panel ? "panel" : "terminal",
808
+ folder,
809
+ };
388
810
  };
389
- if (env.T3CODE_CLI_PATH || bundle === T3_BUNDLE) return { kind: "t3", name: "T3 Code", folder };
811
+ if (env.T3CODE_CLI_PATH || bundle === T3_BUNDLE)
812
+ return { kind: "t3", name: "T3 Code", folder };
390
813
  const editor = editorOf();
391
814
  if (editor) return editorPlace(editor);
392
- const ids = { tty,
815
+ const ids = {
816
+ tty,
393
817
  tmux: env.TMUX && /^%\d+$/.test(env.TMUX_PANE || "") ? env.TMUX_PANE : null,
394
818
  wezterm: /^\d+$/.test(env.WEZTERM_PANE || "") ? env.WEZTERM_PANE : null,
395
- kitty: /^\d+$/.test(env.KITTY_WINDOW_ID || "") ? { id: env.KITTY_WINDOW_ID, socket: env.KITTY_LISTEN_ON || null } : null };
396
- if (env.TERM_PROGRAM && env.TERM_PROGRAM !== "vscode") return { kind: "terminal", name: hostName || TERMINAL_NAMES[env.TERM_PROGRAM] || env.TERM_PROGRAM, app: host?.app ?? null, ...ids, folder };
819
+ kitty: /^\d+$/.test(env.KITTY_WINDOW_ID || "")
820
+ ? { id: env.KITTY_WINDOW_ID, socket: env.KITTY_LISTEN_ON || null }
821
+ : null,
822
+ };
823
+ if (env.TERM_PROGRAM && env.TERM_PROGRAM !== "vscode")
824
+ return {
825
+ kind: "terminal",
826
+ name: hostName || TERMINAL_NAMES[env.TERM_PROGRAM] || env.TERM_PROGRAM,
827
+ app: host?.app ?? null,
828
+ ...ids,
829
+ folder,
830
+ };
397
831
  // Nothing in its environment says: the app it runs under.
398
- if (/^T3 Code\b/.test(hostName)) return { kind: "t3", name: "T3 Code", folder };
832
+ if (/^T3 Code\b/.test(hostName))
833
+ return { kind: "t3", name: "T3 Code", folder };
399
834
  const hosted = EDITORS.find((entry) => entry.name === hostName);
400
835
  if (hosted) return editorPlace(hosted);
401
- if (hostName === "Codex") return { kind: "codex-app", name: "Codex", app: host.app, folder };
402
- if (tty || ids.tmux || ids.wezterm || ids.kitty) return { kind: "terminal", name: hostName || "Terminal", app: host?.app ?? null, ...ids, folder };
836
+ if (hostName === "Codex")
837
+ return { kind: "codex-app", name: "Codex", app: host.app, folder };
838
+ if (tty || ids.tmux || ids.wezterm || ids.kitty)
839
+ return {
840
+ kind: "terminal",
841
+ name: hostName || "Terminal",
842
+ app: host?.app ?? null,
843
+ ...ids,
844
+ folder,
845
+ };
403
846
  return host ? { kind: "app", name: hostName, app: host.app, folder } : null;
404
847
  }
405
848
 
406
849
  /** One key per place the person uses an agent in: both T3 builds are T3; an editor's terminal and its panel differ. */
407
- export const placeKey = (place) => (place.kind === "editor" ? `editor:${place.editor}:${place.surface}` : place.kind === "t3" ? "t3" : `${place.kind}:${place.name}`);
850
+ export const placeKey = (place) =>
851
+ place.kind === "editor"
852
+ ? `editor:${place.editor}:${place.surface}`
853
+ : place.kind === "t3"
854
+ ? "t3"
855
+ : `${place.kind}:${place.name}`;