pi-subagents 0.80.0 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/bundle/agents.js +18 -0
- package/bundle/background-work.js +16 -0
- package/bundle/capability-ceiling.js +22 -0
- package/bundle/child-tool-plan.js +11 -0
- package/bundle/chunk-26C3WLYQ.js +8388 -0
- package/bundle/chunk-2XYWN7QJ.js +128 -0
- package/bundle/chunk-2ZSNH26H.js +4164 -0
- package/bundle/chunk-3DC5KBVP.js +235 -0
- package/bundle/chunk-3IF2PGW7.js +380 -0
- package/bundle/chunk-4QFZZV3O.js +57 -0
- package/bundle/chunk-563BHAZ5.js +1365 -0
- package/bundle/chunk-5LFLVSTM.js +260 -0
- package/bundle/chunk-62OCY2S4.js +14 -0
- package/bundle/chunk-74RT5YQE.js +326 -0
- package/bundle/chunk-7F6RKT6W.js +132 -0
- package/bundle/chunk-7J4ZXMY7.js +18 -0
- package/bundle/chunk-ARRKBB2B.js +179 -0
- package/bundle/chunk-BJN5LUQY.js +458 -0
- package/bundle/chunk-BPS2AGBZ.js +5421 -0
- package/bundle/chunk-CEKS3TFO.js +1337 -0
- package/bundle/chunk-CTPAE36Y.js +94 -0
- package/bundle/chunk-D4EHT5SJ.js +14 -0
- package/bundle/chunk-EYNJ6VL6.js +526 -0
- package/bundle/chunk-G5DKFLLI.js +1185 -0
- package/bundle/chunk-KSWQVGDW.js +1027 -0
- package/bundle/chunk-L5ZHE4L4.js +946 -0
- package/bundle/chunk-LSRUTHT7.js +57 -0
- package/bundle/chunk-N2BN7BF5.js +159 -0
- package/bundle/chunk-NF7IEKBH.js +490 -0
- package/bundle/chunk-NIXWDZZV.js +9 -0
- package/bundle/chunk-NNLASTQE.js +4082 -0
- package/bundle/chunk-TUWLXTAM.js +935 -0
- package/bundle/chunk-UKG4V45O.js +228 -0
- package/bundle/chunk-V2JWDB5U.js +753 -0
- package/bundle/chunk-V3BIUPLU.js +9163 -0
- package/bundle/chunk-V6MR5TOQ.js +270 -0
- package/bundle/chunk-VJHOELQP.js +8245 -0
- package/bundle/chunk-WSBNENFS.js +732 -0
- package/bundle/chunk-XMFLCKMQ.js +16664 -0
- package/bundle/chunk-YFCK7H2S.js +129 -0
- package/bundle/chunk-YYREO77V.js +137 -0
- package/bundle/chunk-Z7KVHQHL.js +173 -0
- package/bundle/chunk-ZBC44JJ3.js +8606 -0
- package/bundle/chunk-ZP3HODFH.js +175 -0
- package/bundle/control-channel.js +11 -0
- package/bundle/delegation.js +14 -0
- package/bundle/external-job-provider.js +20 -0
- package/bundle/external-runs.js +21 -0
- package/bundle/index.js +30 -0
- package/bundle/inspectors.js +8 -0
- package/bundle/intercom-bridge.js +11 -0
- package/bundle/preflight.js +425 -0
- package/bundle/project-panes.js +28 -0
- package/bundle/required-child-extensions.js +6 -0
- package/bundle/shared-types.js +8 -0
- package/bundle/workflow-resources.js +7 -0
- package/docs/extension-api.md +27 -0
- package/docs/observability.md +6 -2
- package/package.json +18 -18
- package/src/agents/frontmatter.d.ts +2 -0
- package/src/agents/frontmatter.js +9 -2
- package/src/api/delegation.d.ts +20 -0
- package/src/extension/index.js +27 -7
- package/src/runs/background/run-child-session.js +18 -0
- package/src/runs/background/stale-run-reconciler.js +2 -0
- package/src/runs/foreground/execution.js +39 -2
- package/src/runs/shared/structured-output.d.ts +1 -0
- package/src/runs/shared/structured-output.js +1 -0
- package/src/shared/accessible-dir.js +4 -1
- package/src/shared/provider-diagnostics.d.ts +9 -0
- package/src/shared/provider-diagnostics.js +75 -0
- package/src/shared/types.d.ts +3 -0
- package/src/slash/delegation-adapters.d.ts +3 -1
- package/src/slash/delegation-adapters.js +7 -0
- package/src/slash/prompt-template-bridge.js +3 -1
- package/src/slash/subagents-admin.d.ts +1 -0
- package/src/slash/subagents-admin.js +1 -1
- package/src/tui/fleet-layout.d.ts +20 -0
- package/src/tui/fleet-layout.js +17 -0
- package/src/tui/fleet-viewport.d.ts +12 -0
- package/src/tui/fleet-viewport.js +35 -0
- package/src/tui/fleet.d.ts +5 -6
- package/src/tui/fleet.js +69 -51
- package/src/workflows/structured-workflow-scripts.d.ts +1 -0
- package/src/workflows/structured-workflow-scripts.js +5 -4
- package/src/workflows/workflow-resources.js +2 -2
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatShellCommand
|
|
3
|
+
} from "./chunk-7J4ZXMY7.js";
|
|
4
|
+
import {
|
|
5
|
+
getProjectSubagentsDir
|
|
6
|
+
} from "./chunk-3DC5KBVP.js";
|
|
7
|
+
import {
|
|
8
|
+
getPiSpawnCommand,
|
|
9
|
+
writeAtomicJson
|
|
10
|
+
} from "./chunk-ZBC44JJ3.js";
|
|
11
|
+
|
|
12
|
+
// src/inspectors/herdr/project-panes.ts
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
|
|
16
|
+
// src/inspectors/herdr/client.ts
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
function error(code, message, details) {
|
|
19
|
+
return { ok: false, error: { code, message, ...details !== void 0 ? { details } : {} } };
|
|
20
|
+
}
|
|
21
|
+
function parseLastJson(value) {
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
if (!trimmed) return void 0;
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(trimmed);
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
for (const line of trimmed.split(/\r?\n/).reverse()) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(line);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return void 0;
|
|
35
|
+
}
|
|
36
|
+
function normalizeCode(raw) {
|
|
37
|
+
const code = String(raw ?? "").toLowerCase();
|
|
38
|
+
if (code.includes("timeout") || code.includes("timed_out")) return "TIMEOUT";
|
|
39
|
+
if (code.includes("gone")) return "PANE_GONE";
|
|
40
|
+
if (code.includes("not_found") || code.includes("not-found") || code === "no_such_pane") return "NOT_FOUND";
|
|
41
|
+
return "VALIDATION_ERROR";
|
|
42
|
+
}
|
|
43
|
+
function createHerdrClient(options = {}) {
|
|
44
|
+
const bin = options.bin ?? process.env.HERDR_BIN ?? "herdr";
|
|
45
|
+
const spawnImpl = options.spawn ?? spawn;
|
|
46
|
+
return {
|
|
47
|
+
run(args, runOptions = {}) {
|
|
48
|
+
return new Promise((resolve2) => {
|
|
49
|
+
let child;
|
|
50
|
+
try {
|
|
51
|
+
child = spawnImpl(bin, args, { shell: false, windowsHide: true, env: process.env });
|
|
52
|
+
} catch (cause) {
|
|
53
|
+
const code = cause?.code;
|
|
54
|
+
resolve2(error("HERDR_UNAVAILABLE", code === "ENOENT" ? "Herdr is not installed or is not on PATH. Install Herdr 0.7.5+ or set HERDR_BIN." : `Failed to start Herdr: ${cause instanceof Error ? cause.message : String(cause)}`));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let stdout = "";
|
|
58
|
+
let stderr = "";
|
|
59
|
+
let settled = false;
|
|
60
|
+
const finish = (result) => {
|
|
61
|
+
if (settled) return;
|
|
62
|
+
settled = true;
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
runOptions.signal?.removeEventListener("abort", abort);
|
|
65
|
+
resolve2(result);
|
|
66
|
+
};
|
|
67
|
+
const abort = () => {
|
|
68
|
+
try {
|
|
69
|
+
child.kill();
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
finish(error("TIMEOUT", `Herdr command '${args.join(" ")}' was aborted.`));
|
|
73
|
+
};
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
try {
|
|
76
|
+
child.kill();
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
finish(error("TIMEOUT", `Herdr command '${args.join(" ")}' timed out after ${runOptions.timeoutMs ?? 15e3}ms.`));
|
|
80
|
+
}, runOptions.timeoutMs ?? 15e3);
|
|
81
|
+
timer.unref?.();
|
|
82
|
+
if (runOptions.signal?.aborted) abort();
|
|
83
|
+
else runOptions.signal?.addEventListener("abort", abort, { once: true });
|
|
84
|
+
child.stdout?.on("data", (chunk) => {
|
|
85
|
+
stdout += chunk.toString();
|
|
86
|
+
});
|
|
87
|
+
child.stderr?.on("data", (chunk) => {
|
|
88
|
+
stderr += chunk.toString();
|
|
89
|
+
});
|
|
90
|
+
child.on("error", (cause) => {
|
|
91
|
+
const code = cause.code;
|
|
92
|
+
finish(error("HERDR_UNAVAILABLE", code === "ENOENT" ? "Herdr is not installed or is not on PATH. Install Herdr 0.7.5+ or set HERDR_BIN." : `Failed to run Herdr: ${cause.message}`));
|
|
93
|
+
});
|
|
94
|
+
child.on("close", (exitCode) => {
|
|
95
|
+
const parsed = parseLastJson(stdout) ?? (exitCode === 0 ? void 0 : parseLastJson(stderr));
|
|
96
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "error" in parsed) {
|
|
97
|
+
const raw = parsed.error;
|
|
98
|
+
finish(error(normalizeCode(raw?.code), String(raw?.message ?? "Herdr command failed."), raw));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (exitCode === 0) {
|
|
102
|
+
if (parsed !== void 0) {
|
|
103
|
+
const envelope = parsed;
|
|
104
|
+
finish({ ok: true, data: envelope.result ?? parsed });
|
|
105
|
+
} else if (runOptions.textOk) finish({ ok: true, data: stdout.trim() });
|
|
106
|
+
else finish({ ok: true, data: {} });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const message = stderr.split(/\r?\n/).find((line) => line.trim())?.trim() ?? `Herdr exited with code ${exitCode}.`;
|
|
110
|
+
finish(error("VALIDATION_ERROR", message, { exitCode }));
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function parseHerdrVersion(value) {
|
|
117
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(value);
|
|
118
|
+
return match ? { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) } : void 0;
|
|
119
|
+
}
|
|
120
|
+
function supportsRawPanes(version) {
|
|
121
|
+
return version.major > 0 || version.minor > 7 || version.minor === 7 && version.patch >= 5;
|
|
122
|
+
}
|
|
123
|
+
async function detectHerdr(client, signal) {
|
|
124
|
+
const result = await client.run(["--version"], { timeoutMs: 3e3, signal, textOk: true });
|
|
125
|
+
if (result.ok === false) return result;
|
|
126
|
+
const versionText = typeof result.data === "string" ? result.data : JSON.stringify(result.data);
|
|
127
|
+
const version = parseHerdrVersion(versionText);
|
|
128
|
+
if (!version) return error("VALIDATION_ERROR", `Could not parse the Herdr version from '${versionText}'.`);
|
|
129
|
+
if (!supportsRawPanes(version)) return error("HERDR_UNSUPPORTED_VERSION", `Herdr ${versionText} does not support raw inspector panes. Upgrade to Herdr 0.7.5 or newer.`);
|
|
130
|
+
return { ok: true, data: { version, versionText } };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/inspectors/herdr/focus.ts
|
|
134
|
+
function herdrPaneRecord(value) {
|
|
135
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
136
|
+
const record = value;
|
|
137
|
+
return record.pane && typeof record.pane === "object" && !Array.isArray(record.pane) ? record.pane : record;
|
|
138
|
+
}
|
|
139
|
+
function text(record, ...keys) {
|
|
140
|
+
for (const key of keys) if (typeof record[key] === "string" && record[key]) return record[key];
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
function herdrPaneFocusTarget(value) {
|
|
144
|
+
const pane = herdrPaneRecord(value);
|
|
145
|
+
if (!pane) return {};
|
|
146
|
+
return {
|
|
147
|
+
paneId: text(pane, "pane_id", "paneId", "id"),
|
|
148
|
+
tabId: text(pane, "tab_id", "tabId"),
|
|
149
|
+
workspaceId: text(pane, "workspace_id", "workspaceId")
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function focusHerdrPane(client, paneId, signal) {
|
|
153
|
+
const live = await client.run(["pane", "get", paneId], { timeoutMs: 5e3, signal });
|
|
154
|
+
if (live.ok === false) return live;
|
|
155
|
+
const target = herdrPaneFocusTarget(live.data);
|
|
156
|
+
if (!target.paneId) {
|
|
157
|
+
return { ok: false, error: { code: "INVALID_PANE_RESPONSE", message: `Herdr pane get returned no pane id for '${paneId}'.`, details: live.data } };
|
|
158
|
+
}
|
|
159
|
+
if (target.tabId) {
|
|
160
|
+
const focused = await client.run(["tab", "focus", target.tabId], { timeoutMs: 5e3, signal });
|
|
161
|
+
return focused.ok ? { ok: true, data: { paneId: target.paneId, tabId: target.tabId, ...target.workspaceId ? { workspaceId: target.workspaceId } : {} } } : focused;
|
|
162
|
+
}
|
|
163
|
+
if (target.workspaceId) {
|
|
164
|
+
const focused = await client.run(["workspace", "focus", target.workspaceId], { timeoutMs: 5e3, signal });
|
|
165
|
+
return focused.ok ? { ok: true, data: { paneId: target.paneId, workspaceId: target.workspaceId } } : focused;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
error: {
|
|
170
|
+
code: "PANE_FOCUS_UNSUPPORTED",
|
|
171
|
+
message: `Herdr pane '${paneId}' has no tab_id or workspace_id. Select it in Herdr manually, or upgrade Herdr focus support.`,
|
|
172
|
+
details: live.data
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/inspectors/herdr/project-panes.ts
|
|
178
|
+
var HERDR_PROJECT_PANE_ACTIONS = ["project.open", "project.status", "project.close"];
|
|
179
|
+
var PROJECT_PANES_API_VERSION = 1;
|
|
180
|
+
var PROJECT_PANE_TRUST_STATUS = "human-verification-required";
|
|
181
|
+
function toolResult(text2, isError = false) {
|
|
182
|
+
return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {}, details: { mode: "management", results: [] } };
|
|
183
|
+
}
|
|
184
|
+
function projectPaneError(code, message, fields = {}) {
|
|
185
|
+
return { ok: false, error: { code, message, ...fields } };
|
|
186
|
+
}
|
|
187
|
+
function formatProjectPaneError(input) {
|
|
188
|
+
return `Herdr project pane error (${input.code}): ${input.message}`;
|
|
189
|
+
}
|
|
190
|
+
function projectPaneDir(projectRoot) {
|
|
191
|
+
return path.join(getProjectSubagentsDir(projectRoot), "project-panes");
|
|
192
|
+
}
|
|
193
|
+
function projectPaneBindingPath(projectRoot) {
|
|
194
|
+
return path.join(projectPaneDir(projectRoot), "herdr.json");
|
|
195
|
+
}
|
|
196
|
+
function projectPaneRootIndexPath(ownerRoot) {
|
|
197
|
+
return path.join(projectPaneDir(ownerRoot), "herdr-roots.json");
|
|
198
|
+
}
|
|
199
|
+
function parseRootIndex(value) {
|
|
200
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid Herdr project pane root index.");
|
|
201
|
+
const input = value;
|
|
202
|
+
if (input.schemaVersion !== 1 || input.kind !== "herdr-project-pane-roots" || !Array.isArray(input.projectRoots)) throw new Error("Invalid Herdr project pane root index.");
|
|
203
|
+
if (!input.projectRoots.every((root) => typeof root === "string" && root.length > 0)) throw new Error("Invalid Herdr project pane root index.");
|
|
204
|
+
return input.projectRoots;
|
|
205
|
+
}
|
|
206
|
+
function listHerdrProjectPaneRoots(ownerRoot) {
|
|
207
|
+
try {
|
|
208
|
+
return parseRootIndex(JSON.parse(fs.readFileSync(projectPaneRootIndexPath(ownerRoot), "utf-8")));
|
|
209
|
+
} catch (error2) {
|
|
210
|
+
const code = error2.code;
|
|
211
|
+
if (code === "ENOENT" || code === "ENOTDIR") return [];
|
|
212
|
+
throw error2;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function writeHerdrProjectPaneRoot(ownerRoot, projectRoot) {
|
|
216
|
+
const projectRoots = [.../* @__PURE__ */ new Set([...listHerdrProjectPaneRoots(ownerRoot), projectRoot])].sort();
|
|
217
|
+
writeAtomicJson(projectPaneRootIndexPath(ownerRoot), {
|
|
218
|
+
schemaVersion: 1,
|
|
219
|
+
kind: "herdr-project-pane-roots",
|
|
220
|
+
projectRoots
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function removeHerdrProjectPaneRoot(ownerRoot, projectRoot) {
|
|
224
|
+
const projectRoots = listHerdrProjectPaneRoots(ownerRoot).filter((root) => root !== projectRoot);
|
|
225
|
+
const file = projectPaneRootIndexPath(ownerRoot);
|
|
226
|
+
if (projectRoots.length === 0) {
|
|
227
|
+
fs.rmSync(file, { force: true });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
writeAtomicJson(file, {
|
|
231
|
+
schemaVersion: 1,
|
|
232
|
+
kind: "herdr-project-pane-roots",
|
|
233
|
+
projectRoots
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function parseBinding(value, strict = false) {
|
|
237
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
238
|
+
const input = value;
|
|
239
|
+
if (input.schemaVersion !== 1 || input.kind !== "herdr-project-pane") return void 0;
|
|
240
|
+
if (typeof input.projectRoot !== "string" || typeof input.paneId !== "string" || typeof input.openedAt !== "string" || typeof input.command !== "string") return void 0;
|
|
241
|
+
if (strict) {
|
|
242
|
+
if (![input.projectRoot, input.paneId, input.openedAt, input.command].every((field) => field.trim().length > 0)) return void 0;
|
|
243
|
+
for (const field of ["lastFocusedAt", "herdrVersion", "startupMessage"]) {
|
|
244
|
+
if (input[field] !== void 0 && typeof input[field] !== "string") return void 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return input;
|
|
248
|
+
}
|
|
249
|
+
function readBinding(projectRoot, strict) {
|
|
250
|
+
const file = projectPaneBindingPath(projectRoot);
|
|
251
|
+
let raw;
|
|
252
|
+
try {
|
|
253
|
+
raw = fs.readFileSync(file, "utf-8");
|
|
254
|
+
} catch (cause) {
|
|
255
|
+
const code = cause.code;
|
|
256
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { state: "absent" };
|
|
257
|
+
return { state: "read-error", cause };
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const binding = parseBinding(JSON.parse(raw), strict);
|
|
261
|
+
return binding ? { state: "valid", binding } : { state: "invalid" };
|
|
262
|
+
} catch {
|
|
263
|
+
return { state: "invalid" };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function readHerdrProjectPaneBinding(projectRoot) {
|
|
267
|
+
const read = readBinding(projectRoot, false);
|
|
268
|
+
return read.state === "valid" ? read.binding : void 0;
|
|
269
|
+
}
|
|
270
|
+
function readProjectPaneBinding(projectRoot) {
|
|
271
|
+
const read = readBinding(projectRoot, true);
|
|
272
|
+
if (read.state === "absent") return { ok: true, data: void 0 };
|
|
273
|
+
if (read.state === "read-error") {
|
|
274
|
+
const bindingPath = projectPaneBindingPath(projectRoot);
|
|
275
|
+
return projectPaneError("BINDING_READ_FAILED", `Failed to read project pane binding '${bindingPath}': ${read.cause instanceof Error ? read.cause.message : String(read.cause)}`, {
|
|
276
|
+
projectRoot,
|
|
277
|
+
bindingPath,
|
|
278
|
+
details: fileSystemErrorDetails(read.cause)
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (read.state === "invalid") {
|
|
282
|
+
return projectPaneError("INVALID_BINDING", `Project pane binding '${projectPaneBindingPath(projectRoot)}' is malformed.`, {
|
|
283
|
+
projectRoot,
|
|
284
|
+
bindingPath: projectPaneBindingPath(projectRoot)
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return { ok: true, data: read.binding };
|
|
288
|
+
}
|
|
289
|
+
function herdrProjectPaneSnapshotFromBinding(binding, now = Date.now()) {
|
|
290
|
+
return {
|
|
291
|
+
projectRoot: binding.projectRoot,
|
|
292
|
+
bindingPath: projectPaneBindingPath(binding.projectRoot),
|
|
293
|
+
paneId: binding.paneId,
|
|
294
|
+
openedAt: binding.openedAt,
|
|
295
|
+
...binding.lastFocusedAt ? { lastFocusedAt: binding.lastFocusedAt } : {},
|
|
296
|
+
state: "open",
|
|
297
|
+
agentStatus: "unknown",
|
|
298
|
+
ownership: "unknown",
|
|
299
|
+
safeToClose: false,
|
|
300
|
+
refreshedAt: now
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function restoreHerdrProjectPaneSnapshots(state, projectRoots, now = Date.now()) {
|
|
304
|
+
const restored = new Map(state.herdrProjectPanes ?? []);
|
|
305
|
+
for (const projectRoot of projectRoots) {
|
|
306
|
+
const binding = readHerdrProjectPaneBinding(projectRoot);
|
|
307
|
+
if (binding) restored.set(binding.projectRoot, herdrProjectPaneSnapshotFromBinding(binding, now));
|
|
308
|
+
}
|
|
309
|
+
state.herdrProjectPanes = restored;
|
|
310
|
+
}
|
|
311
|
+
function sanitizedSummary(value) {
|
|
312
|
+
if (typeof value !== "string") return void 0;
|
|
313
|
+
const cleaned = value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
314
|
+
return cleaned ? cleaned.slice(0, 120) : void 0;
|
|
315
|
+
}
|
|
316
|
+
function nestedRecord(value, key) {
|
|
317
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
318
|
+
const child = value[key];
|
|
319
|
+
return child && typeof child === "object" && !Array.isArray(child) ? child : void 0;
|
|
320
|
+
}
|
|
321
|
+
function paneSummary(pane) {
|
|
322
|
+
return sanitizedSummary(pane.summary) ?? sanitizedSummary(pane.state_text) ?? sanitizedSummary(pane.stateText) ?? sanitizedSummary(pane.token_summary) ?? sanitizedSummary(nestedRecord(pane, "tokens")?.summary) ?? sanitizedSummary(nestedRecord(pane, "metadata")?.summary) ?? sanitizedSummary(nestedRecord(nestedRecord(pane, "metadata"), "tokens")?.summary);
|
|
323
|
+
}
|
|
324
|
+
function projectPaneRuntime(value) {
|
|
325
|
+
const pane = herdrPaneRecord(value);
|
|
326
|
+
const focusTarget = herdrPaneFocusTarget(value);
|
|
327
|
+
const paneId = focusTarget.paneId;
|
|
328
|
+
if (!pane || !paneId) return void 0;
|
|
329
|
+
const text2 = (key) => typeof pane[key] === "string" ? pane[key] : void 0;
|
|
330
|
+
const agentStatus = text2("agent_status") ?? text2("agentStatus") ?? "unknown";
|
|
331
|
+
const summary = paneSummary(pane);
|
|
332
|
+
return {
|
|
333
|
+
paneId,
|
|
334
|
+
...text2("agent") ? { agent: text2("agent") } : {},
|
|
335
|
+
agentStatus: agentStatus.toLowerCase(),
|
|
336
|
+
...text2("cwd") ? { cwd: text2("cwd") } : {},
|
|
337
|
+
...text2("foreground_cwd") || text2("foregroundCwd") ? { foregroundCwd: text2("foreground_cwd") ?? text2("foregroundCwd") } : {},
|
|
338
|
+
...typeof pane.focused === "boolean" ? { focused: pane.focused } : {},
|
|
339
|
+
...focusTarget.tabId ? { tabId: focusTarget.tabId } : {},
|
|
340
|
+
...focusTarget.workspaceId ? { workspaceId: focusTarget.workspaceId } : {},
|
|
341
|
+
...summary ? { summary } : {},
|
|
342
|
+
...text2("terminal_title_stripped") || text2("terminal_title") || text2("terminalTitle") ? { terminalTitle: text2("terminal_title_stripped") ?? text2("terminal_title") ?? text2("terminalTitle") } : {}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function herdrProjectPaneSnapshotFromStatus(data, now) {
|
|
346
|
+
if (data.state === "absent" || !data.binding) return void 0;
|
|
347
|
+
return {
|
|
348
|
+
projectRoot: data.projectRoot,
|
|
349
|
+
bindingPath: data.bindingPath,
|
|
350
|
+
paneId: data.binding.paneId,
|
|
351
|
+
openedAt: data.binding.openedAt,
|
|
352
|
+
...data.binding.lastFocusedAt ? { lastFocusedAt: data.binding.lastFocusedAt } : {},
|
|
353
|
+
state: data.state,
|
|
354
|
+
agentStatus: data.runtime?.agentStatus ?? "unknown",
|
|
355
|
+
ownership: data.ownership,
|
|
356
|
+
safeToClose: data.safeToClose,
|
|
357
|
+
refreshedAt: now,
|
|
358
|
+
...data.runtime?.summary ? { summary: data.runtime.summary } : {},
|
|
359
|
+
...data.runtime?.tabId ? { tabId: data.runtime.tabId } : {},
|
|
360
|
+
...data.runtime?.workspaceId ? { workspaceId: data.runtime.workspaceId } : {},
|
|
361
|
+
...data.runtime?.terminalTitle ? { terminalTitle: data.runtime.terminalTitle } : {},
|
|
362
|
+
...data.staleReason ? { staleReason: data.staleReason.message } : {}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function rememberProjectPane(state, data, now) {
|
|
366
|
+
if (!state) return;
|
|
367
|
+
state.herdrProjectPanes ??= /* @__PURE__ */ new Map();
|
|
368
|
+
const snapshot = herdrProjectPaneSnapshotFromStatus(data, now);
|
|
369
|
+
if (snapshot) state.herdrProjectPanes.set(data.projectRoot, snapshot);
|
|
370
|
+
else state.herdrProjectPanes.delete(data.projectRoot);
|
|
371
|
+
}
|
|
372
|
+
function forgetProjectPane(state, projectRoot) {
|
|
373
|
+
state?.herdrProjectPanes?.delete(projectRoot);
|
|
374
|
+
}
|
|
375
|
+
function resolveProjectRoot(requested) {
|
|
376
|
+
const resolved = path.resolve(requested);
|
|
377
|
+
try {
|
|
378
|
+
const stat = fs.statSync(resolved);
|
|
379
|
+
if (!stat.isDirectory()) return projectPaneError("INVALID_PROJECT_ROOT", `Project pane target '${resolved}' is not a directory.`, { projectRoot: resolved });
|
|
380
|
+
return { ok: true, data: fs.realpathSync(resolved) };
|
|
381
|
+
} catch (cause) {
|
|
382
|
+
return projectPaneError("INVALID_PROJECT_ROOT", `Project pane target '${resolved}' is unavailable: ${cause instanceof Error ? cause.message : String(cause)}`, { projectRoot: resolved });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function inspectPane(client, paneId, signal) {
|
|
386
|
+
const live = await client.run(["pane", "get", paneId], { timeoutMs: 5e3, signal });
|
|
387
|
+
if (live.ok === false) return projectPaneError(live.error.code, live.error.message, { details: live.error.details });
|
|
388
|
+
const runtime = projectPaneRuntime(live.data);
|
|
389
|
+
if (!runtime) return projectPaneError("INVALID_PANE_RESPONSE", `Herdr pane get returned no pane runtime for '${paneId}'.`, { details: live.data });
|
|
390
|
+
return { ok: true, data: runtime };
|
|
391
|
+
}
|
|
392
|
+
function projectPaneCommand(message) {
|
|
393
|
+
const args = message?.trim() ? [message.trim()] : [];
|
|
394
|
+
const command = getPiSpawnCommand(args);
|
|
395
|
+
return formatShellCommand(command.command, command.args);
|
|
396
|
+
}
|
|
397
|
+
function canonicalRuntimePath(value) {
|
|
398
|
+
if (!value) return void 0;
|
|
399
|
+
try {
|
|
400
|
+
return fs.realpathSync(path.resolve(value));
|
|
401
|
+
} catch {
|
|
402
|
+
return path.resolve(value);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function projectPaneOwnership(runtime, binding, projectRoot) {
|
|
406
|
+
if (runtime.paneId !== binding.paneId) return "mismatch";
|
|
407
|
+
const runtimeRoot = canonicalRuntimePath(runtime.cwd);
|
|
408
|
+
if (!runtimeRoot) return "unknown";
|
|
409
|
+
return runtimeRoot === projectRoot ? "verified" : "mismatch";
|
|
410
|
+
}
|
|
411
|
+
function bindingForManager(projectRoot, legacyToolCompatibility) {
|
|
412
|
+
const read = readBinding(projectRoot, !legacyToolCompatibility);
|
|
413
|
+
if (read.state === "absent") return { ok: true, data: void 0 };
|
|
414
|
+
if (read.state === "read-error") {
|
|
415
|
+
if (legacyToolCompatibility) return { ok: true, data: void 0 };
|
|
416
|
+
const bindingPath = projectPaneBindingPath(projectRoot);
|
|
417
|
+
return projectPaneError("BINDING_READ_FAILED", `Failed to read project pane binding '${bindingPath}': ${read.cause instanceof Error ? read.cause.message : String(read.cause)}`, {
|
|
418
|
+
projectRoot,
|
|
419
|
+
bindingPath,
|
|
420
|
+
details: fileSystemErrorDetails(read.cause)
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
if (read.state === "invalid") {
|
|
424
|
+
if (legacyToolCompatibility) return { ok: true, data: void 0 };
|
|
425
|
+
return projectPaneError("INVALID_BINDING", `Project pane binding '${projectPaneBindingPath(projectRoot)}' is malformed.`, {
|
|
426
|
+
projectRoot,
|
|
427
|
+
bindingPath: projectPaneBindingPath(projectRoot)
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const binding = read.binding;
|
|
431
|
+
if (!legacyToolCompatibility && canonicalRuntimePath(binding.projectRoot) !== projectRoot) {
|
|
432
|
+
return projectPaneError("INVALID_BINDING", `Project pane binding root '${binding.projectRoot}' does not match '${projectRoot}'.`, {
|
|
433
|
+
projectRoot,
|
|
434
|
+
bindingPath: projectPaneBindingPath(projectRoot),
|
|
435
|
+
details: binding
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return { ok: true, data: binding };
|
|
439
|
+
}
|
|
440
|
+
function common(projectRoot) {
|
|
441
|
+
return {
|
|
442
|
+
apiVersion: PROJECT_PANES_API_VERSION,
|
|
443
|
+
projectRoot,
|
|
444
|
+
bindingPath: projectPaneBindingPath(projectRoot),
|
|
445
|
+
trust: PROJECT_PANE_TRUST_STATUS
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function fileSystemErrorDetails(cause) {
|
|
449
|
+
if (!(cause instanceof Error)) return cause;
|
|
450
|
+
const code = cause.code;
|
|
451
|
+
return { name: cause.name, message: cause.message, ...code ? { code } : {} };
|
|
452
|
+
}
|
|
453
|
+
function removeProjectPaneBinding(projectRoot) {
|
|
454
|
+
const bindingPath = projectPaneBindingPath(projectRoot);
|
|
455
|
+
try {
|
|
456
|
+
fs.rmSync(bindingPath, { force: true });
|
|
457
|
+
return { ok: true, data: void 0 };
|
|
458
|
+
} catch (cause) {
|
|
459
|
+
return projectPaneError("BINDING_REMOVE_FAILED", `Failed to remove project pane binding '${bindingPath}': ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
460
|
+
projectRoot,
|
|
461
|
+
bindingPath,
|
|
462
|
+
details: fileSystemErrorDetails(cause)
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function createProjectPaneManagerInternal(options = {}) {
|
|
467
|
+
const client = options.client ?? createHerdrClient();
|
|
468
|
+
return {
|
|
469
|
+
async status(input) {
|
|
470
|
+
const root = resolveProjectRoot(input.cwd);
|
|
471
|
+
if (!root.ok) return root;
|
|
472
|
+
const projectRoot = root.data;
|
|
473
|
+
const bindingResult = bindingForManager(projectRoot, options.legacyToolCompatibility);
|
|
474
|
+
if (!bindingResult.ok) return bindingResult;
|
|
475
|
+
const existing = bindingResult.data;
|
|
476
|
+
if (!existing) return { ok: true, data: { ...common(projectRoot), state: "absent", ownership: "unknown", safeToClose: true } };
|
|
477
|
+
const live = await inspectPane(client, existing.paneId, input.signal);
|
|
478
|
+
if (!live.ok) {
|
|
479
|
+
if (live.error.code === "INVALID_PANE_RESPONSE" && options.legacyToolCompatibility) {
|
|
480
|
+
const runtime = { paneId: existing.paneId, agentStatus: "unknown" };
|
|
481
|
+
return { ok: true, data: { ...common(projectRoot), state: "open", binding: existing, runtime, ownership: "unknown", safeToClose: false } };
|
|
482
|
+
}
|
|
483
|
+
if (live.error.code === "NOT_FOUND" || live.error.code === "PANE_GONE") {
|
|
484
|
+
return { ok: true, data: {
|
|
485
|
+
...common(projectRoot),
|
|
486
|
+
state: "stale",
|
|
487
|
+
binding: existing,
|
|
488
|
+
ownership: "unknown",
|
|
489
|
+
safeToClose: false,
|
|
490
|
+
staleReason: { code: live.error.code, message: live.error.message }
|
|
491
|
+
} };
|
|
492
|
+
}
|
|
493
|
+
return { ok: false, error: { ...live.error, projectRoot, bindingPath: projectPaneBindingPath(projectRoot) } };
|
|
494
|
+
}
|
|
495
|
+
const ownership = projectPaneOwnership(live.data, existing, projectRoot);
|
|
496
|
+
return { ok: true, data: {
|
|
497
|
+
...common(projectRoot),
|
|
498
|
+
state: "open",
|
|
499
|
+
binding: existing,
|
|
500
|
+
runtime: live.data,
|
|
501
|
+
ownership,
|
|
502
|
+
safeToClose: live.data.agentStatus === "idle" && ownership === "verified"
|
|
503
|
+
} };
|
|
504
|
+
},
|
|
505
|
+
async focus(input) {
|
|
506
|
+
const status = await this.status(input);
|
|
507
|
+
if (!status.ok) return projectPaneError(status.error.code, status.error.message, status.error);
|
|
508
|
+
if (status.data.state !== "open" || !status.data.binding || !status.data.runtime) {
|
|
509
|
+
return projectPaneError("PANE_GONE", `No open Herdr project pane binding exists for '${status.data.projectRoot}'.`, {
|
|
510
|
+
projectRoot: status.data.projectRoot,
|
|
511
|
+
bindingPath: status.data.bindingPath
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (status.data.ownership !== "verified") {
|
|
515
|
+
return projectPaneError("PANE_OWNERSHIP_UNVERIFIED", `Project pane '${status.data.binding.paneId}' ownership is '${status.data.ownership}' for '${status.data.projectRoot}'.`, {
|
|
516
|
+
projectRoot: status.data.projectRoot,
|
|
517
|
+
bindingPath: status.data.bindingPath,
|
|
518
|
+
details: status.data.runtime
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
const focused = await focusHerdrPane(client, status.data.binding.paneId, input.signal);
|
|
522
|
+
if (!focused.ok) return projectPaneError(focused.error.code, focused.error.message, {
|
|
523
|
+
projectRoot: status.data.projectRoot,
|
|
524
|
+
bindingPath: status.data.bindingPath,
|
|
525
|
+
details: focused.error.details
|
|
526
|
+
});
|
|
527
|
+
const now = (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
528
|
+
const binding = { ...status.data.binding, lastFocusedAt: now };
|
|
529
|
+
try {
|
|
530
|
+
writeAtomicJson(status.data.bindingPath, binding);
|
|
531
|
+
} catch (cause) {
|
|
532
|
+
return projectPaneError("BINDING_WRITE_FAILED", `Failed to update project pane binding '${status.data.bindingPath}': ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
533
|
+
projectRoot: status.data.projectRoot,
|
|
534
|
+
bindingPath: status.data.bindingPath,
|
|
535
|
+
details: fileSystemErrorDetails(cause)
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return { ok: true, data: {
|
|
539
|
+
...common(status.data.projectRoot),
|
|
540
|
+
binding,
|
|
541
|
+
runtime: status.data.runtime,
|
|
542
|
+
ownership: status.data.ownership,
|
|
543
|
+
focused: focused.data
|
|
544
|
+
} };
|
|
545
|
+
},
|
|
546
|
+
async open(input) {
|
|
547
|
+
const root = resolveProjectRoot(input.cwd);
|
|
548
|
+
if (!root.ok) return root;
|
|
549
|
+
const projectRoot = root.data;
|
|
550
|
+
const detected = await detectHerdr(client, input.signal);
|
|
551
|
+
if (!detected.ok) return projectPaneError(detected.error.code, detected.error.message, { projectRoot, details: detected.error.details });
|
|
552
|
+
const bindingResult = bindingForManager(projectRoot, options.legacyToolCompatibility);
|
|
553
|
+
if (!bindingResult.ok) return bindingResult;
|
|
554
|
+
const existing = bindingResult.data;
|
|
555
|
+
if (existing) {
|
|
556
|
+
const live = await inspectPane(client, existing.paneId, input.signal);
|
|
557
|
+
if (live.ok) {
|
|
558
|
+
const ownership = projectPaneOwnership(live.data, existing, projectRoot);
|
|
559
|
+
if (!options.legacyToolCompatibility && ownership !== "verified") {
|
|
560
|
+
return projectPaneError("PANE_OWNERSHIP_UNVERIFIED", `Project pane '${existing.paneId}' ownership is '${ownership}' for '${projectRoot}'.`, {
|
|
561
|
+
projectRoot,
|
|
562
|
+
bindingPath: projectPaneBindingPath(projectRoot),
|
|
563
|
+
details: live.data
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return { ok: true, data: { ...common(projectRoot), disposition: "already-open", binding: existing, runtime: live.data } };
|
|
567
|
+
}
|
|
568
|
+
if (live.error.code === "INVALID_PANE_RESPONSE" && options.legacyToolCompatibility) {
|
|
569
|
+
return { ok: true, data: {
|
|
570
|
+
...common(projectRoot),
|
|
571
|
+
disposition: "already-open",
|
|
572
|
+
binding: existing,
|
|
573
|
+
runtime: { paneId: existing.paneId, agentStatus: "unknown" }
|
|
574
|
+
} };
|
|
575
|
+
}
|
|
576
|
+
const stale = live.error.code === "NOT_FOUND" || live.error.code === "PANE_GONE";
|
|
577
|
+
if (!stale && !options.legacyToolCompatibility) {
|
|
578
|
+
return { ok: false, error: { ...live.error, projectRoot, bindingPath: projectPaneBindingPath(projectRoot) } };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const splitArgs = ["pane", "split", "--current", "--direction", "right", "--cwd", projectRoot];
|
|
582
|
+
splitArgs.push(input.focus === true ? "--focus" : "--no-focus");
|
|
583
|
+
const split = await client.run(splitArgs, { timeoutMs: 15e3, signal: input.signal });
|
|
584
|
+
if (!split.ok) return projectPaneError(split.error.code, split.error.message, { projectRoot, details: split.error.details });
|
|
585
|
+
const paneId = herdrPaneFocusTarget(split.data).paneId;
|
|
586
|
+
if (!paneId) return projectPaneError(options.legacyToolCompatibility ? "PANE_GONE" : "INVALID_PANE_RESPONSE", "Herdr pane split returned no pane id.", { projectRoot, details: split.data });
|
|
587
|
+
const startupMessage = input.message?.trim();
|
|
588
|
+
const command = projectPaneCommand(startupMessage);
|
|
589
|
+
const started = await client.run(["pane", "run", paneId, command], { timeoutMs: 15e3, signal: input.signal });
|
|
590
|
+
if (!started.ok) {
|
|
591
|
+
await client.run(["pane", "close", paneId], { timeoutMs: 5e3 });
|
|
592
|
+
return projectPaneError(started.error.code, started.error.message, { projectRoot, details: started.error.details });
|
|
593
|
+
}
|
|
594
|
+
const now = (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
595
|
+
const binding = {
|
|
596
|
+
schemaVersion: 1,
|
|
597
|
+
kind: "herdr-project-pane",
|
|
598
|
+
projectRoot,
|
|
599
|
+
paneId,
|
|
600
|
+
openedAt: now,
|
|
601
|
+
...input.focus === true ? { lastFocusedAt: now } : {},
|
|
602
|
+
herdrVersion: detected.data.versionText,
|
|
603
|
+
command,
|
|
604
|
+
...startupMessage ? { startupMessage } : {}
|
|
605
|
+
};
|
|
606
|
+
const bindingPath = projectPaneBindingPath(projectRoot);
|
|
607
|
+
try {
|
|
608
|
+
writeAtomicJson(bindingPath, binding);
|
|
609
|
+
} catch (cause) {
|
|
610
|
+
let cleanup;
|
|
611
|
+
try {
|
|
612
|
+
const closed = await client.run(["pane", "close", paneId], { timeoutMs: 5e3 });
|
|
613
|
+
cleanup = closed.ok ? { paneClosed: true } : { paneClosed: false, error: closed.error };
|
|
614
|
+
} catch (cleanupCause) {
|
|
615
|
+
cleanup = { paneClosed: false, error: fileSystemErrorDetails(cleanupCause) };
|
|
616
|
+
}
|
|
617
|
+
const cleanupMessage = cleanup.paneClosed ? ` The newly opened pane '${paneId}' was closed.` : ` Cleanup could not close the newly opened pane '${paneId}'.`;
|
|
618
|
+
return projectPaneError("BINDING_WRITE_FAILED", `Failed to persist project pane binding '${bindingPath}': ${cause instanceof Error ? cause.message : String(cause)}.${cleanupMessage}`, {
|
|
619
|
+
projectRoot,
|
|
620
|
+
bindingPath,
|
|
621
|
+
details: { cause: fileSystemErrorDetails(cause), cleanup }
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
return { ok: true, data: { ...common(projectRoot), disposition: "opened", binding } };
|
|
625
|
+
},
|
|
626
|
+
async close(input) {
|
|
627
|
+
const root = resolveProjectRoot(input.cwd);
|
|
628
|
+
if (!root.ok) return root;
|
|
629
|
+
const projectRoot = root.data;
|
|
630
|
+
const bindingResult = bindingForManager(projectRoot, options.legacyToolCompatibility);
|
|
631
|
+
if (!bindingResult.ok) return bindingResult;
|
|
632
|
+
const existing = bindingResult.data;
|
|
633
|
+
if (!existing) return { ok: true, data: { ...common(projectRoot), disposition: "absent" } };
|
|
634
|
+
const live = await inspectPane(client, existing.paneId, input.signal);
|
|
635
|
+
if (!live.ok) {
|
|
636
|
+
if (live.error.code === "NOT_FOUND" || live.error.code === "PANE_GONE") {
|
|
637
|
+
const removed2 = removeProjectPaneBinding(projectRoot);
|
|
638
|
+
if (!removed2.ok) return removed2;
|
|
639
|
+
return { ok: true, data: { ...common(projectRoot), disposition: "stale-binding-removed", binding: existing } };
|
|
640
|
+
}
|
|
641
|
+
return { ok: false, error: { ...live.error, projectRoot, bindingPath: projectPaneBindingPath(projectRoot) } };
|
|
642
|
+
}
|
|
643
|
+
const runtime = live.data;
|
|
644
|
+
const ownership = projectPaneOwnership(runtime, existing, projectRoot);
|
|
645
|
+
if (ownership !== "verified") {
|
|
646
|
+
return projectPaneError("PANE_OWNERSHIP_UNVERIFIED", `Project pane '${existing.paneId}' ownership is '${ownership}' for '${projectRoot}'.`, {
|
|
647
|
+
projectRoot,
|
|
648
|
+
bindingPath: projectPaneBindingPath(projectRoot),
|
|
649
|
+
details: runtime
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
if (runtime.agentStatus !== "idle") {
|
|
653
|
+
return projectPaneError("PANE_NOT_IDLE", `Project pane '${existing.paneId}' is '${runtime.agentStatus}', not explicitly idle.`, {
|
|
654
|
+
projectRoot,
|
|
655
|
+
bindingPath: projectPaneBindingPath(projectRoot),
|
|
656
|
+
details: runtime
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
const closed = await client.run(["pane", "close", existing.paneId], { timeoutMs: 1e4, signal: input.signal });
|
|
660
|
+
if (!closed.ok && closed.error.code !== "NOT_FOUND" && closed.error.code !== "PANE_GONE") {
|
|
661
|
+
return projectPaneError(closed.error.code, closed.error.message, { projectRoot, bindingPath: projectPaneBindingPath(projectRoot), details: closed.error.details });
|
|
662
|
+
}
|
|
663
|
+
const disposition = closed.ok ? "closed" : "stale-binding-removed";
|
|
664
|
+
const removed = removeProjectPaneBinding(projectRoot);
|
|
665
|
+
if (!removed.ok) return removed;
|
|
666
|
+
return { ok: true, data: { ...common(projectRoot), disposition, binding: existing, ...runtime ? { runtime } : {} } };
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function createProjectPaneManager(options = {}) {
|
|
671
|
+
return createProjectPaneManagerInternal(options);
|
|
672
|
+
}
|
|
673
|
+
async function openProjectPane(options) {
|
|
674
|
+
return createProjectPaneManager().open(options);
|
|
675
|
+
}
|
|
676
|
+
async function getProjectPaneStatus(options) {
|
|
677
|
+
return createProjectPaneManager().status(options);
|
|
678
|
+
}
|
|
679
|
+
async function closeProjectPane(options) {
|
|
680
|
+
return createProjectPaneManager().close(options);
|
|
681
|
+
}
|
|
682
|
+
async function focusProjectPane(options) {
|
|
683
|
+
return createProjectPaneManager().focus(options);
|
|
684
|
+
}
|
|
685
|
+
async function handleHerdrProjectPaneAction(action, params, deps) {
|
|
686
|
+
const requested = params.cwd?.trim() || deps.cwd;
|
|
687
|
+
const ownerRoot = path.resolve(deps.cwd);
|
|
688
|
+
const manager = createProjectPaneManagerInternal({ client: deps.client, now: deps.now, legacyToolCompatibility: true });
|
|
689
|
+
const remember = (data) => rememberProjectPane(deps.state, data, deps.now?.().getTime() ?? Date.now());
|
|
690
|
+
if (action === "project.status") {
|
|
691
|
+
const status2 = await manager.status({ cwd: requested, signal: deps.signal });
|
|
692
|
+
if (!status2.ok) return toolResult(formatProjectPaneError(status2.error), true);
|
|
693
|
+
remember(status2.data);
|
|
694
|
+
if (status2.data.state === "absent") return toolResult(`No Herdr project pane binding exists for ${status2.data.projectRoot}.`);
|
|
695
|
+
if (status2.data.state === "stale") {
|
|
696
|
+
const reason = status2.data.staleReason;
|
|
697
|
+
return toolResult(`${formatProjectPaneError(reason)}
|
|
698
|
+
Binding: ${status2.data.bindingPath}`, true);
|
|
699
|
+
}
|
|
700
|
+
return toolResult(`Herdr project pane ${status2.data.binding.paneId} is open for ${status2.data.projectRoot}.
|
|
701
|
+
Binding: ${status2.data.bindingPath}`);
|
|
702
|
+
}
|
|
703
|
+
if (action === "project.close") {
|
|
704
|
+
const closed = await manager.close({ cwd: requested, requireIdle: true, signal: deps.signal });
|
|
705
|
+
if (!closed.ok) return toolResult(formatProjectPaneError(closed.error), true);
|
|
706
|
+
forgetProjectPane(deps.state, closed.data.projectRoot);
|
|
707
|
+
try {
|
|
708
|
+
removeHerdrProjectPaneRoot(ownerRoot, closed.data.projectRoot);
|
|
709
|
+
} catch (error2) {
|
|
710
|
+
return toolResult(`Herdr project pane error (BINDING_REMOVE_FAILED): Failed to remove project pane root index '${projectPaneRootIndexPath(ownerRoot)}': ${error2 instanceof Error ? error2.message : String(error2)}`, true);
|
|
711
|
+
}
|
|
712
|
+
if (closed.data.disposition === "absent") return toolResult(`No Herdr project pane binding exists for ${closed.data.projectRoot}.`);
|
|
713
|
+
return toolResult(`Closed Herdr project pane ${closed.data.binding.paneId} for ${closed.data.projectRoot}.`);
|
|
714
|
+
}
|
|
715
|
+
const opened = await manager.open({ cwd: requested, message: params.message, focus: params.focus, signal: deps.signal });
|
|
716
|
+
if (!opened.ok) return toolResult(formatProjectPaneError(opened.error), true);
|
|
717
|
+
try {
|
|
718
|
+
writeHerdrProjectPaneRoot(ownerRoot, opened.data.projectRoot);
|
|
719
|
+
} catch (error2) {
|
|
720
|
+
return toolResult(`Herdr project pane error (BINDING_WRITE_FAILED): Failed to persist project pane root index '${projectPaneRootIndexPath(ownerRoot)}': ${error2 instanceof Error ? error2.message : String(error2)}`, true);
|
|
721
|
+
}
|
|
722
|
+
const status = await manager.status({ cwd: opened.data.projectRoot, signal: deps.signal });
|
|
723
|
+
if (status.ok) remember(status.data);
|
|
724
|
+
if (opened.data.disposition === "already-open") {
|
|
725
|
+
if (params.focus) {
|
|
726
|
+
const focused = await manager.focus({ cwd: opened.data.projectRoot, signal: deps.signal });
|
|
727
|
+
if (!focused.ok) return toolResult(`Herdr project pane ${opened.data.binding.paneId} is already open for ${opened.data.projectRoot}. ${formatProjectPaneError(focused.error)}`, true);
|
|
728
|
+
const focusedStatus = await manager.status({ cwd: opened.data.projectRoot, signal: deps.signal });
|
|
729
|
+
if (focusedStatus.ok) remember(focusedStatus.data);
|
|
730
|
+
return toolResult(`Herdr project pane ${opened.data.binding.paneId} is already open for ${opened.data.projectRoot}. Focused ${focused.data.focused.tabId ? `tab ${focused.data.focused.tabId}` : `workspace ${focused.data.focused.workspaceId}`}.`);
|
|
731
|
+
}
|
|
732
|
+
return toolResult(`Herdr project pane ${opened.data.binding.paneId} is already open for ${opened.data.projectRoot}.`);
|
|
733
|
+
}
|
|
734
|
+
return toolResult(`Opened Herdr project pane ${opened.data.binding.paneId} for ${opened.data.projectRoot}. The pane runs its own Pi session; subagents launched there belong to that project.`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export {
|
|
738
|
+
createHerdrClient,
|
|
739
|
+
detectHerdr,
|
|
740
|
+
HERDR_PROJECT_PANE_ACTIONS,
|
|
741
|
+
PROJECT_PANES_API_VERSION,
|
|
742
|
+
PROJECT_PANE_TRUST_STATUS,
|
|
743
|
+
projectPaneBindingPath,
|
|
744
|
+
listHerdrProjectPaneRoots,
|
|
745
|
+
readProjectPaneBinding,
|
|
746
|
+
restoreHerdrProjectPaneSnapshots,
|
|
747
|
+
createProjectPaneManager,
|
|
748
|
+
openProjectPane,
|
|
749
|
+
getProjectPaneStatus,
|
|
750
|
+
closeProjectPane,
|
|
751
|
+
focusProjectPane,
|
|
752
|
+
handleHerdrProjectPaneAction
|
|
753
|
+
};
|