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.
- package/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5268 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +384 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- 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 =
|
|
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 = [
|
|
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)
|
|
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(
|
|
39
|
-
.
|
|
40
|
-
|
|
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 =
|
|
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(
|
|
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 = () =>
|
|
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
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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)
|
|
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))
|
|
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({
|
|
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)
|
|
86
|
-
|
|
87
|
-
if (process.platform === "
|
|
88
|
-
|
|
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(
|
|
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 {
|
|
98
|
-
|
|
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 {
|
|
231
|
+
try {
|
|
232
|
+
dirs = fs.readdirSync(root);
|
|
233
|
+
} catch {}
|
|
112
234
|
for (const dir of dirs) {
|
|
113
235
|
let opened;
|
|
114
|
-
try {
|
|
115
|
-
|
|
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 (
|
|
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], {
|
|
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
|
|
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(
|
|
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 {
|
|
145
|
-
|
|
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": {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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 {
|
|
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(
|
|
378
|
+
export function editorSessionWorkspace(
|
|
379
|
+
agent,
|
|
380
|
+
sessionId,
|
|
381
|
+
folder,
|
|
382
|
+
window = folder
|
|
383
|
+
) {
|
|
189
384
|
const resume = AGENTS[agent]?.resume;
|
|
190
|
-
if (
|
|
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 =
|
|
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 =
|
|
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(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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 (
|
|
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 = () =>
|
|
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"]
|
|
227
|
-
.map((
|
|
228
|
-
.
|
|
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(
|
|
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) =>
|
|
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"))
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
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 {
|
|
260
|
-
|
|
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) => {
|
|
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"], {
|
|
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(
|
|
272
|
-
|
|
273
|
-
|
|
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"))
|
|
590
|
+
if (!dir || !t3Tables(home).has("orchestration_v2_projection_threads"))
|
|
591
|
+
return null;
|
|
284
592
|
const project = t3Project(folder, home);
|
|
285
|
-
const rows = (
|
|
286
|
-
|
|
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 =
|
|
289
|
-
|
|
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 (
|
|
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"))
|
|
303
|
-
|
|
304
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
321
|
-
|
|
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 =
|
|
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"))
|
|
337
|
-
|
|
338
|
-
|
|
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 {
|
|
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 {
|
|
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) => {
|
|
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 =
|
|
362
|
-
|
|
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 = {
|
|
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({
|
|
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 = () =>
|
|
383
|
-
|
|
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(
|
|
386
|
-
|
|
387
|
-
|
|
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)
|
|
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 = {
|
|
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 || "")
|
|
396
|
-
|
|
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))
|
|
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")
|
|
402
|
-
|
|
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) =>
|
|
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}`;
|