pi-agent-replicate 0.0.0-stage → 1.4.1
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/AGENTS.md +27 -0
- package/CHANGELOG.md +152 -0
- package/CONTEXT.md +63 -0
- package/LICENSE +21 -0
- package/README.md +68 -2
- package/RELEASING.md +132 -0
- package/assets/banner.png +0 -0
- package/config.json.example +6 -0
- package/docs/README.md +23 -0
- package/docs/adr/0003-installable-role-packs.md +29 -0
- package/docs/adr/0008-adopt-pi-only-subagent-execution.md +22 -0
- package/docs/adr/0009-durable-recursive-subagent-ownership.md +23 -0
- package/docs/adr/0010-public-subagent-tree-registry.md +16 -0
- package/docs/configuration.md +14 -0
- package/docs/development.md +27 -0
- package/docs/public-subagent-tree-api.md +58 -0
- package/docs/retained-roles.md +71 -0
- package/docs/settled-lifecycle-contract.md +71 -0
- package/docs/worktree-subagents.md +58 -0
- package/examples/role-pack/extension.ts +18 -0
- package/examples/role-pack/package.json +16 -0
- package/examples/role-pack/roles/example-reviewer.md +12 -0
- package/index.d.ts +116 -0
- package/index.js +1064 -0
- package/index.ts +155 -0
- package/package.json +70 -4
- package/pi-extension/subagents/activity.js +575 -0
- package/pi-extension/subagents/activity.ts +980 -0
- package/pi-extension/subagents/completion.js +184 -0
- package/pi-extension/subagents/completion.ts +229 -0
- package/pi-extension/subagents/delivery-drain.js +15 -0
- package/pi-extension/subagents/delivery-drain.ts +20 -0
- package/pi-extension/subagents/delivery-files.js +86 -0
- package/pi-extension/subagents/delivery-files.ts +105 -0
- package/pi-extension/subagents/delivery-log.js +51 -0
- package/pi-extension/subagents/delivery-log.ts +51 -0
- package/pi-extension/subagents/delivery-payload.ts +55 -0
- package/pi-extension/subagents/delivery.ts +219 -0
- package/pi-extension/subagents/execution-owner.js +30 -0
- package/pi-extension/subagents/execution-owner.ts +38 -0
- package/pi-extension/subagents/herdr.js +645 -0
- package/pi-extension/subagents/herdr.ts +927 -0
- package/pi-extension/subagents/index.ts +3400 -0
- package/pi-extension/subagents/launch.js +774 -0
- package/pi-extension/subagents/launch.ts +1124 -0
- package/pi-extension/subagents/lifecycle.js +477 -0
- package/pi-extension/subagents/lifecycle.ts +582 -0
- package/pi-extension/subagents/lineage.js +806 -0
- package/pi-extension/subagents/lineage.ts +841 -0
- package/pi-extension/subagents/replica-background.js +32 -0
- package/pi-extension/subagents/replica-background.ts +38 -0
- package/pi-extension/subagents/replica-branch.ts +45 -0
- package/pi-extension/subagents/replica-directory-settings.ts +51 -0
- package/pi-extension/subagents/replica-history.js +22 -0
- package/pi-extension/subagents/replica-history.ts +21 -0
- package/pi-extension/subagents/replica-work.js +326 -0
- package/pi-extension/subagents/replica-work.ts +336 -0
- package/pi-extension/subagents/replica-worker.js +74 -0
- package/pi-extension/subagents/replica-worker.ts +71 -0
- package/pi-extension/subagents/replication.ts +99 -0
- package/pi-extension/subagents/result-delivery-queue.ts +33 -0
- package/pi-extension/subagents/role-catalog.js +123 -0
- package/pi-extension/subagents/role-catalog.ts +24 -0
- package/pi-extension/subagents/runtime-routing.js +154 -0
- package/pi-extension/subagents/runtime-routing.ts +239 -0
- package/pi-extension/subagents/session-binding.js +346 -0
- package/pi-extension/subagents/session-binding.ts +339 -0
- package/pi-extension/subagents/session-work.js +219 -0
- package/pi-extension/subagents/session-work.ts +167 -0
- package/pi-extension/subagents/session.js +318 -0
- package/pi-extension/subagents/session.ts +448 -0
- package/pi-extension/subagents/settled-contract.js +47 -0
- package/pi-extension/subagents/settled-contract.ts +175 -0
- package/pi-extension/subagents/source-state.js +180 -0
- package/pi-extension/subagents/source-state.ts +184 -0
- package/pi-extension/subagents/status.ts +493 -0
- package/pi-extension/subagents/subagent-done.js +805 -0
- package/pi-extension/subagents/subagent-done.ts +878 -0
- package/pi-extension/subagents/terminal.js +111 -0
- package/pi-extension/subagents/terminal.ts +208 -0
- package/pi-extension/subagents/watchdog.js +122 -0
- package/pi-extension/subagents/watchdog.ts +171 -0
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
import { execFile, execSync, execFileSync } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
const commandAvailability = new Map<string, boolean>();
|
|
7
|
+
|
|
8
|
+
const surfaceTabs = new Map<string, string>();
|
|
9
|
+
function hasCommand(command: string): boolean {
|
|
10
|
+
if (commandAvailability.has(command)) {
|
|
11
|
+
return commandAvailability.get(command)!;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let available = false;
|
|
15
|
+
if (process.platform === "win32") {
|
|
16
|
+
try {
|
|
17
|
+
execFileSync("where.exe", [command], { stdio: "ignore" });
|
|
18
|
+
available = true;
|
|
19
|
+
} catch {
|
|
20
|
+
try {
|
|
21
|
+
execSync(`command -v ${command}`, { stdio: "ignore" });
|
|
22
|
+
available = true;
|
|
23
|
+
} catch {
|
|
24
|
+
available = false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
try {
|
|
29
|
+
execSync(`command -v ${command}`, { stdio: "ignore" });
|
|
30
|
+
available = true;
|
|
31
|
+
} catch {
|
|
32
|
+
available = false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
commandAvailability.set(command, available);
|
|
37
|
+
return available;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isHerdrAvailable(): boolean {
|
|
41
|
+
return process.env.HERDR_ENV === "1" && hasCommand("herdr");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseHerdrJson(value: string): unknown {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(value);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractHerdrPaneId(output: string, context: string): string {
|
|
53
|
+
const parsed = parseHerdrJson(output);
|
|
54
|
+
const paneId = (parsed as { result?: { pane?: { pane_id?: unknown } } })
|
|
55
|
+
?.result?.pane?.pane_id;
|
|
56
|
+
if (typeof paneId !== "string" || !paneId) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return paneId;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extractHerdrRootPaneId(output: string, context: string): string {
|
|
65
|
+
const parsed = parseHerdrJson(output);
|
|
66
|
+
const paneId = (parsed as { result?: { root_pane?: { pane_id?: unknown } } })
|
|
67
|
+
?.result?.root_pane?.pane_id;
|
|
68
|
+
if (typeof paneId !== "string" || !paneId) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return paneId;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
function extractHerdrTabSurface(
|
|
78
|
+
output: string,
|
|
79
|
+
context: string,
|
|
80
|
+
): { paneId: string; tabId: string } {
|
|
81
|
+
const parsed = parseHerdrJson(output) as {
|
|
82
|
+
result?: {
|
|
83
|
+
tab?: { tab_id?: unknown };
|
|
84
|
+
root_pane?: { pane_id?: unknown; tab_id?: unknown };
|
|
85
|
+
};
|
|
86
|
+
} | null;
|
|
87
|
+
const paneId = parsed?.result?.root_pane?.pane_id;
|
|
88
|
+
const tabId =
|
|
89
|
+
parsed?.result?.tab?.tab_id ?? parsed?.result?.root_pane?.tab_id;
|
|
90
|
+
if (
|
|
91
|
+
typeof paneId !== "string" ||
|
|
92
|
+
!paneId ||
|
|
93
|
+
typeof tabId !== "string" ||
|
|
94
|
+
!tabId
|
|
95
|
+
) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return { paneId, tabId };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Return the remembered owning tab for a Herdr surface created by this process. */
|
|
104
|
+
export function getHerdrSurfaceTabId(surface: string): string | undefined {
|
|
105
|
+
return surfaceTabs.get(surface);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Restore a durable surface-to-tab association before retrying cleanup. */
|
|
109
|
+
export function rememberHerdrSurfaceTab(surface: string, tabId: string): void {
|
|
110
|
+
if (surface && tabId) surfaceTabs.set(surface, tabId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function forgetHerdrTab(tabId: string): void {
|
|
114
|
+
for (const [surface, rememberedTab] of surfaceTabs) {
|
|
115
|
+
if (rememberedTab === tabId) surfaceTabs.delete(surface);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export interface HerdrWorktreeSurface {
|
|
119
|
+
path: string;
|
|
120
|
+
branch: string;
|
|
121
|
+
workspaceId: string;
|
|
122
|
+
paneId: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function extractHerdrWorktree(output: string): HerdrWorktreeSurface {
|
|
126
|
+
const parsed = parseHerdrJson(output) as {
|
|
127
|
+
result?: {
|
|
128
|
+
type?: unknown;
|
|
129
|
+
workspace?: { workspace_id?: unknown };
|
|
130
|
+
root_pane?: { pane_id?: unknown };
|
|
131
|
+
worktree?: { path?: unknown; branch?: unknown };
|
|
132
|
+
};
|
|
133
|
+
} | null;
|
|
134
|
+
const result = parsed?.result;
|
|
135
|
+
if (
|
|
136
|
+
result?.type !== "worktree_created" ||
|
|
137
|
+
typeof result.workspace?.workspace_id !== "string" ||
|
|
138
|
+
!result.workspace.workspace_id ||
|
|
139
|
+
typeof result.root_pane?.pane_id !== "string" ||
|
|
140
|
+
!result.root_pane.pane_id ||
|
|
141
|
+
typeof result.worktree?.path !== "string" ||
|
|
142
|
+
!result.worktree.path ||
|
|
143
|
+
typeof result.worktree.branch !== "string" ||
|
|
144
|
+
!result.worktree.branch
|
|
145
|
+
) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Unexpected herdr worktree create output: ${output.trim() || "(empty)"}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
path: result.worktree.path,
|
|
152
|
+
branch: result.worktree.branch,
|
|
153
|
+
workspaceId: result.workspace.workspace_id,
|
|
154
|
+
paneId: result.root_pane.pane_id,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function herdrExec(args: string[]): string {
|
|
159
|
+
return execFileSync("herdr", args, {
|
|
160
|
+
encoding: "utf8",
|
|
161
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function herdrExecAsync(args: string[]): Promise<string> {
|
|
166
|
+
const { stdout } = await execFileAsync("herdr", args, { encoding: "utf8" });
|
|
167
|
+
return stdout;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getHerdrParentPaneId(): string {
|
|
171
|
+
const paneId = process.env.HERDR_PANE_ID;
|
|
172
|
+
if (!paneId) {
|
|
173
|
+
throw new Error("HERDR_PANE_ID not set");
|
|
174
|
+
}
|
|
175
|
+
return paneId;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildCurrentPaneArgs(): string[] {
|
|
179
|
+
return ["pane", "current", "--current"];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getHerdrCurrentPaneInfo(run = herdrExec): {
|
|
183
|
+
pane_id: string;
|
|
184
|
+
tab_id: string;
|
|
185
|
+
workspace_id: string;
|
|
186
|
+
} {
|
|
187
|
+
// Inherited IDs can outlive a pane move; Herdr resolves the caller identity.
|
|
188
|
+
const output = run(buildCurrentPaneArgs());
|
|
189
|
+
const parsed = parseHerdrJson(output);
|
|
190
|
+
const pane = (parsed as { result?: { pane?: unknown } } | null)?.result?.pane as
|
|
191
|
+
| { pane_id?: string; tab_id?: string; workspace_id?: string }
|
|
192
|
+
| undefined;
|
|
193
|
+
if (!pane?.pane_id || !pane?.tab_id || !pane?.workspace_id) {
|
|
194
|
+
throw new Error(`Unexpected herdr pane current output: ${output.trim() || "(empty)"}`);
|
|
195
|
+
}
|
|
196
|
+
return { pane_id: pane.pane_id, tab_id: pane.tab_id, workspace_id: pane.workspace_id };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Read the current pane's manual name; unavailable metadata must not block a launch. */
|
|
200
|
+
export function getHerdrCurrentPaneTitle(run = herdrExec): string | undefined {
|
|
201
|
+
try {
|
|
202
|
+
const parsed = parseHerdrJson(run(buildCurrentPaneArgs())) as { result?: { pane?: { title?: unknown; label?: unknown } } } | null;
|
|
203
|
+
const pane = parsed?.result?.pane;
|
|
204
|
+
return [pane?.title, pane?.label].find((value): value is string => typeof value === "string" && !!value.trim());
|
|
205
|
+
} catch {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function buildTabCreateArgs(
|
|
211
|
+
name: string,
|
|
212
|
+
cwd: string,
|
|
213
|
+
workspaceId: string,
|
|
214
|
+
): string[] {
|
|
215
|
+
return [
|
|
216
|
+
"tab",
|
|
217
|
+
"create",
|
|
218
|
+
"--workspace",
|
|
219
|
+
workspaceId,
|
|
220
|
+
"--label",
|
|
221
|
+
name,
|
|
222
|
+
"--cwd",
|
|
223
|
+
cwd,
|
|
224
|
+
"--no-focus",
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function buildWorktreeCreateArgs(
|
|
229
|
+
name: string,
|
|
230
|
+
cwd: string,
|
|
231
|
+
branch: string,
|
|
232
|
+
base: string,
|
|
233
|
+
): string[] {
|
|
234
|
+
return [
|
|
235
|
+
"worktree",
|
|
236
|
+
"create",
|
|
237
|
+
"--cwd",
|
|
238
|
+
cwd,
|
|
239
|
+
"--branch",
|
|
240
|
+
branch,
|
|
241
|
+
"--base",
|
|
242
|
+
base,
|
|
243
|
+
"--label",
|
|
244
|
+
name,
|
|
245
|
+
"--no-focus",
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function createHerdrSurfaceInWorkspace(
|
|
250
|
+
name: string,
|
|
251
|
+
cwd: string,
|
|
252
|
+
workspaceId: string,
|
|
253
|
+
): string {
|
|
254
|
+
const output = herdrExec(buildTabCreateArgs(name, cwd, workspaceId));
|
|
255
|
+
const { paneId, tabId } = extractHerdrTabSurface(output, "tab create");
|
|
256
|
+
rememberHerdrSurfaceTab(paneId, tabId);
|
|
257
|
+
try {
|
|
258
|
+
herdrExec(["pane", "rename", paneId, name]);
|
|
259
|
+
} catch {
|
|
260
|
+
// Optional — pane label is cosmetic.
|
|
261
|
+
}
|
|
262
|
+
return paneId;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function createHerdrSurface(name: string): string {
|
|
266
|
+
// Create a new tab per subagent so parallel spawns each get a full tab
|
|
267
|
+
// instead of ever-narrower splits of the parent pane. Target the current
|
|
268
|
+
// workspace explicitly because Herdr's implicit default may be another space.
|
|
269
|
+
const { workspace_id: workspaceId } = getHerdrCurrentPaneInfo();
|
|
270
|
+
return createHerdrSurfaceInWorkspace(name, process.cwd(), workspaceId);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Branch-backed worktree records returned by `herdr worktree list`. */
|
|
274
|
+
export interface HerdrWorktreeInfo {
|
|
275
|
+
branch: string;
|
|
276
|
+
path: string;
|
|
277
|
+
label?: string;
|
|
278
|
+
workspaceId?: string;
|
|
279
|
+
isLinkedWorktree: boolean;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export class HerdrWorktreeCreateError extends Error {
|
|
283
|
+
readonly recoveredWorktree: Pick<
|
|
284
|
+
HerdrWorktreeInfo,
|
|
285
|
+
"path" | "branch" | "workspaceId"
|
|
286
|
+
>;
|
|
287
|
+
|
|
288
|
+
constructor(
|
|
289
|
+
message: string,
|
|
290
|
+
recoveredWorktree: Pick<
|
|
291
|
+
HerdrWorktreeInfo,
|
|
292
|
+
"path" | "branch" | "workspaceId"
|
|
293
|
+
>,
|
|
294
|
+
) {
|
|
295
|
+
super(message);
|
|
296
|
+
this.name = "HerdrWorktreeCreateError";
|
|
297
|
+
this.recoveredWorktree = recoveredWorktree;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function parseHerdrWorktreeList(output: string): HerdrWorktreeInfo[] {
|
|
302
|
+
const parsed = parseHerdrJson(output) as {
|
|
303
|
+
result?: {
|
|
304
|
+
type?: unknown;
|
|
305
|
+
worktrees?: Array<{
|
|
306
|
+
branch?: unknown;
|
|
307
|
+
path?: unknown;
|
|
308
|
+
label?: unknown;
|
|
309
|
+
open_workspace_id?: unknown;
|
|
310
|
+
is_linked_worktree?: unknown;
|
|
311
|
+
is_detached?: unknown;
|
|
312
|
+
}>;
|
|
313
|
+
};
|
|
314
|
+
} | null;
|
|
315
|
+
const worktrees = parsed?.result?.worktrees;
|
|
316
|
+
if (parsed?.result?.type !== "worktree_list" || !Array.isArray(worktrees)) {
|
|
317
|
+
throw new Error("Unexpected herdr worktree list output");
|
|
318
|
+
}
|
|
319
|
+
// Detached checkouts have no branch and cannot match an owned replica.
|
|
320
|
+
return worktrees.filter((worktree) => !(
|
|
321
|
+
worktree.is_detached === true && worktree.branch === undefined && typeof worktree.path === "string"
|
|
322
|
+
)).map((worktree) => {
|
|
323
|
+
if (
|
|
324
|
+
typeof worktree.branch !== "string" ||
|
|
325
|
+
typeof worktree.path !== "string"
|
|
326
|
+
) {
|
|
327
|
+
throw new Error("Unexpected herdr worktree list entry");
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
branch: worktree.branch,
|
|
331
|
+
path: worktree.path,
|
|
332
|
+
...(typeof worktree.label === "string" ? { label: worktree.label } : {}),
|
|
333
|
+
...(typeof worktree.open_workspace_id === "string"
|
|
334
|
+
? { workspaceId: worktree.open_workspace_id }
|
|
335
|
+
: {}),
|
|
336
|
+
isLinkedWorktree: worktree.is_linked_worktree === true,
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function listHerdrWorktrees(cwd?: string): HerdrWorktreeInfo[] {
|
|
342
|
+
const args = ["worktree", "list"];
|
|
343
|
+
if (cwd) args.push("--cwd", cwd);
|
|
344
|
+
return parseHerdrWorktreeList(herdrExec(args));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function parseHerdrPaneList(output: string, workspaceId: string): string[] {
|
|
348
|
+
const parsed = parseHerdrJson(output) as {
|
|
349
|
+
result?: {
|
|
350
|
+
type?: unknown;
|
|
351
|
+
panes?: Array<{ pane_id?: unknown; workspace_id?: unknown }>;
|
|
352
|
+
};
|
|
353
|
+
} | null;
|
|
354
|
+
if (
|
|
355
|
+
parsed?.result?.type !== "pane_list" ||
|
|
356
|
+
!Array.isArray(parsed.result.panes)
|
|
357
|
+
) {
|
|
358
|
+
throw new Error("Unexpected herdr pane list output");
|
|
359
|
+
}
|
|
360
|
+
return parsed.result.panes
|
|
361
|
+
.filter((pane) => pane.workspace_id === workspaceId)
|
|
362
|
+
.map((pane) => pane.pane_id)
|
|
363
|
+
.filter((paneId): paneId is string => typeof paneId === "string");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function recoverHerdrWorktree(
|
|
367
|
+
cwd: string,
|
|
368
|
+
branch: string,
|
|
369
|
+
): HerdrWorktreeSurface | HerdrWorktreeInfo | undefined {
|
|
370
|
+
const matches = listHerdrWorktrees(cwd).filter(
|
|
371
|
+
(worktree) => worktree.branch === branch,
|
|
372
|
+
);
|
|
373
|
+
if (matches.length !== 1) return undefined;
|
|
374
|
+
const worktree = matches[0];
|
|
375
|
+
if (!worktree.workspaceId) return worktree;
|
|
376
|
+
const panes = parseHerdrPaneList(
|
|
377
|
+
herdrExec(["pane", "list", "--workspace", worktree.workspaceId]),
|
|
378
|
+
worktree.workspaceId,
|
|
379
|
+
);
|
|
380
|
+
if (panes.length !== 1) return worktree;
|
|
381
|
+
return {
|
|
382
|
+
path: worktree.path,
|
|
383
|
+
branch: worktree.branch,
|
|
384
|
+
workspaceId: worktree.workspaceId,
|
|
385
|
+
paneId: panes[0],
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function createHerdrWorktree(
|
|
390
|
+
name: string,
|
|
391
|
+
cwd: string,
|
|
392
|
+
branch: string,
|
|
393
|
+
base: string,
|
|
394
|
+
): HerdrWorktreeSurface {
|
|
395
|
+
const output = herdrExec(buildWorktreeCreateArgs(name, cwd, branch, base));
|
|
396
|
+
try {
|
|
397
|
+
return extractHerdrWorktree(output);
|
|
398
|
+
} catch (parseError) {
|
|
399
|
+
let recovered: HerdrWorktreeSurface | HerdrWorktreeInfo | undefined;
|
|
400
|
+
try {
|
|
401
|
+
recovered = recoverHerdrWorktree(cwd, branch);
|
|
402
|
+
} catch {
|
|
403
|
+
throw parseError;
|
|
404
|
+
}
|
|
405
|
+
if (recovered?.workspaceId && "paneId" in recovered) return recovered;
|
|
406
|
+
if (recovered) {
|
|
407
|
+
throw new HerdrWorktreeCreateError(
|
|
408
|
+
`Herdr created branch ${branch}, but its workspace response was incomplete`,
|
|
409
|
+
recovered,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
throw parseError;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function createHerdrSurfaceSplit(
|
|
417
|
+
name: string,
|
|
418
|
+
direction: "right" | "down",
|
|
419
|
+
): string {
|
|
420
|
+
const parentPaneId = getHerdrParentPaneId();
|
|
421
|
+
const output = herdrExec([
|
|
422
|
+
"pane",
|
|
423
|
+
"split",
|
|
424
|
+
parentPaneId,
|
|
425
|
+
"--direction",
|
|
426
|
+
direction,
|
|
427
|
+
"--no-focus",
|
|
428
|
+
"--cwd",
|
|
429
|
+
process.cwd(),
|
|
430
|
+
]);
|
|
431
|
+
const paneId = extractHerdrPaneId(output, "pane split");
|
|
432
|
+
try {
|
|
433
|
+
herdrExec(["pane", "rename", paneId, name]);
|
|
434
|
+
} catch {
|
|
435
|
+
// Optional.
|
|
436
|
+
}
|
|
437
|
+
return paneId;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function readHerdrScreen(surface: string, lines = 50): string {
|
|
441
|
+
// `visible` is reliable for freshly created panes where herdr's `recent`
|
|
442
|
+
// scrollback may not be populated yet.
|
|
443
|
+
return herdrExec([
|
|
444
|
+
"pane",
|
|
445
|
+
"read",
|
|
446
|
+
surface,
|
|
447
|
+
"--source",
|
|
448
|
+
"visible",
|
|
449
|
+
"--lines",
|
|
450
|
+
String(lines),
|
|
451
|
+
]);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export async function readHerdrScreenAsync(
|
|
455
|
+
surface: string,
|
|
456
|
+
lines = 50,
|
|
457
|
+
source: "visible" | "recent-unwrapped" = "visible",
|
|
458
|
+
): Promise<string> {
|
|
459
|
+
return herdrExecAsync([
|
|
460
|
+
"pane",
|
|
461
|
+
"read",
|
|
462
|
+
surface,
|
|
463
|
+
"--source",
|
|
464
|
+
source,
|
|
465
|
+
"--lines",
|
|
466
|
+
String(lines),
|
|
467
|
+
]);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export type { PaneInspection, HerdrAgentStatus } from "./lifecycle.ts";
|
|
471
|
+
|
|
472
|
+
type PaneInspectionResult =
|
|
473
|
+
| {
|
|
474
|
+
kind: "present";
|
|
475
|
+
agent?: string;
|
|
476
|
+
agentStatus: "idle" | "working" | "blocked" | "done" | "unknown";
|
|
477
|
+
}
|
|
478
|
+
| { kind: "missing"; error?: string }
|
|
479
|
+
| { kind: "unavailable"; error: string };
|
|
480
|
+
|
|
481
|
+
function parsePaneGetOutput(
|
|
482
|
+
output: string,
|
|
483
|
+
surface: string,
|
|
484
|
+
): PaneInspectionResult {
|
|
485
|
+
const parsed = parseHerdrJson(output) as {
|
|
486
|
+
result?: { pane?: unknown };
|
|
487
|
+
error?: { code?: unknown; message?: unknown };
|
|
488
|
+
} | null;
|
|
489
|
+
const errorObj = parsed?.error;
|
|
490
|
+
if (errorObj?.code === "pane_not_found" || errorObj?.code === "not_found") {
|
|
491
|
+
return {
|
|
492
|
+
kind: "missing",
|
|
493
|
+
error:
|
|
494
|
+
typeof errorObj.message === "string"
|
|
495
|
+
? errorObj.message
|
|
496
|
+
: "pane not found",
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const pane = parsed?.result?.pane;
|
|
500
|
+
if (!pane || typeof pane !== "object")
|
|
501
|
+
return { kind: "unavailable", error: "pane get returned no pane record" };
|
|
502
|
+
const record = pane as {
|
|
503
|
+
pane_id?: unknown;
|
|
504
|
+
agent?: unknown;
|
|
505
|
+
agent_status?: unknown;
|
|
506
|
+
};
|
|
507
|
+
if (record.pane_id !== surface)
|
|
508
|
+
return { kind: "unavailable", error: "pane id mismatch" };
|
|
509
|
+
const agent = typeof record.agent === "string" ? record.agent : undefined;
|
|
510
|
+
const rawStatus =
|
|
511
|
+
typeof record.agent_status === "string" ? record.agent_status : "unknown";
|
|
512
|
+
const agentStatus =
|
|
513
|
+
rawStatus === "idle" ||
|
|
514
|
+
rawStatus === "working" ||
|
|
515
|
+
rawStatus === "blocked" ||
|
|
516
|
+
rawStatus === "done" ||
|
|
517
|
+
rawStatus === "unknown"
|
|
518
|
+
? rawStatus
|
|
519
|
+
: "unknown";
|
|
520
|
+
return { kind: "present", ...(agent ? { agent } : {}), agentStatus };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function parsePaneGetError(error: any): PaneInspectionResult {
|
|
524
|
+
for (const raw of [error?.stderr, error?.stdout]) {
|
|
525
|
+
if (typeof raw !== "string" || !raw.trim()) continue;
|
|
526
|
+
try {
|
|
527
|
+
const parsed = parsePaneGetOutput(raw, "");
|
|
528
|
+
if (parsed.kind === "missing") return parsed;
|
|
529
|
+
} catch {
|
|
530
|
+
// A CLI may emit plain diagnostics on one stream and structured JSON on
|
|
531
|
+
// the other. Parse each stream independently before giving up.
|
|
532
|
+
}
|
|
533
|
+
// Older/alternate Herdr builds may print the stable error code as plain
|
|
534
|
+
// text rather than JSON. Only match explicit identifiers, not generic
|
|
535
|
+
// prose such as "pane unavailable".
|
|
536
|
+
if (/\b(?:pane_not_found|not_found)\b/.test(raw)) {
|
|
537
|
+
return { kind: "missing", error: raw.trim() };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const message = error?.message
|
|
541
|
+
? String(error.message)
|
|
542
|
+
: "herdr pane get failed";
|
|
543
|
+
return { kind: "unavailable", error: message };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Structured pane query.
|
|
548
|
+
* - present: pane is reachable; agent/agentStatus may be present when detected
|
|
549
|
+
* - missing: server responded, pane is gone
|
|
550
|
+
* - unavailable: server command failed; caller should keep polling
|
|
551
|
+
*/
|
|
552
|
+
export async function inspectHerdrPane(
|
|
553
|
+
surface: string,
|
|
554
|
+
): Promise<PaneInspectionResult> {
|
|
555
|
+
try {
|
|
556
|
+
return parsePaneGetOutput(
|
|
557
|
+
await herdrExecAsync(["pane", "get", surface]),
|
|
558
|
+
surface,
|
|
559
|
+
);
|
|
560
|
+
} catch (error: any) {
|
|
561
|
+
return parsePaneGetError(error);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export interface HerdrForegroundProcess {
|
|
566
|
+
pid: number;
|
|
567
|
+
name?: string;
|
|
568
|
+
argv0?: string;
|
|
569
|
+
argv?: string[];
|
|
570
|
+
cwd?: string;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export interface HerdrPaneProcessInfo {
|
|
574
|
+
paneId: string;
|
|
575
|
+
shellPid?: number;
|
|
576
|
+
foregroundProcessGroupId?: number;
|
|
577
|
+
pids: number[];
|
|
578
|
+
foregroundProcesses: HerdrForegroundProcess[];
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function parsePaneProcessInfo(
|
|
582
|
+
output: string,
|
|
583
|
+
paneId: string,
|
|
584
|
+
): HerdrPaneProcessInfo {
|
|
585
|
+
const parsed = parseHerdrJson(output) as {
|
|
586
|
+
result?: {
|
|
587
|
+
process_info?: {
|
|
588
|
+
pane_id?: unknown;
|
|
589
|
+
shell_pid?: unknown;
|
|
590
|
+
foreground_process_group_id?: unknown;
|
|
591
|
+
foreground_processes?: Array<{
|
|
592
|
+
pid?: unknown;
|
|
593
|
+
name?: unknown;
|
|
594
|
+
argv0?: unknown;
|
|
595
|
+
argv?: unknown;
|
|
596
|
+
cwd?: unknown;
|
|
597
|
+
}>;
|
|
598
|
+
};
|
|
599
|
+
};
|
|
600
|
+
} | null;
|
|
601
|
+
const info = parsed?.result?.process_info;
|
|
602
|
+
if (!info || typeof info !== "object") {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`Unexpected herdr pane process-info output: ${output.trim() || "(empty)"}`,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
if (typeof info.pane_id === "string" && info.pane_id !== paneId) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
`herdr pane process-info pane id mismatch: ${info.pane_id} != ${paneId}`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
const pids = new Set<number>();
|
|
613
|
+
if (
|
|
614
|
+
typeof info.shell_pid === "number" &&
|
|
615
|
+
Number.isInteger(info.shell_pid) &&
|
|
616
|
+
info.shell_pid > 0
|
|
617
|
+
) {
|
|
618
|
+
pids.add(info.shell_pid);
|
|
619
|
+
}
|
|
620
|
+
if (
|
|
621
|
+
typeof info.foreground_process_group_id === "number" &&
|
|
622
|
+
Number.isInteger(info.foreground_process_group_id) &&
|
|
623
|
+
info.foreground_process_group_id > 0
|
|
624
|
+
) {
|
|
625
|
+
pids.add(info.foreground_process_group_id);
|
|
626
|
+
}
|
|
627
|
+
const foregroundProcesses: HerdrForegroundProcess[] = [];
|
|
628
|
+
for (const process of info.foreground_processes ?? []) {
|
|
629
|
+
if (
|
|
630
|
+
typeof process?.pid === "number" &&
|
|
631
|
+
Number.isInteger(process.pid) &&
|
|
632
|
+
process.pid > 0
|
|
633
|
+
) {
|
|
634
|
+
pids.add(process.pid);
|
|
635
|
+
foregroundProcesses.push({
|
|
636
|
+
pid: process.pid,
|
|
637
|
+
...(typeof process.name === "string" ? { name: process.name } : {}),
|
|
638
|
+
...(typeof process.argv0 === "string" ? { argv0: process.argv0 } : {}),
|
|
639
|
+
...(Array.isArray(process.argv) &&
|
|
640
|
+
process.argv.every((value) => typeof value === "string")
|
|
641
|
+
? { argv: process.argv as string[] }
|
|
642
|
+
: {}),
|
|
643
|
+
...(typeof process.cwd === "string" ? { cwd: process.cwd } : {}),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
paneId,
|
|
649
|
+
...(typeof info.shell_pid === "number" ? { shellPid: info.shell_pid } : {}),
|
|
650
|
+
...(typeof info.foreground_process_group_id === "number"
|
|
651
|
+
? { foregroundProcessGroupId: info.foreground_process_group_id }
|
|
652
|
+
: {}),
|
|
653
|
+
pids: [...pids],
|
|
654
|
+
foregroundProcesses,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function getHerdrPaneProcessInfo(surface: string): HerdrPaneProcessInfo {
|
|
659
|
+
return parsePaneProcessInfo(
|
|
660
|
+
herdrExec(["pane", "process-info", "--pane", surface]),
|
|
661
|
+
surface,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function getHerdrPaneProcessInfoAsync(
|
|
666
|
+
surface: string,
|
|
667
|
+
): Promise<HerdrPaneProcessInfo> {
|
|
668
|
+
return parsePaneProcessInfo(
|
|
669
|
+
await herdrExecAsync(["pane", "process-info", "--pane", surface]),
|
|
670
|
+
surface,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function isHerdrShellReady(info: HerdrPaneProcessInfo): boolean {
|
|
675
|
+
return (
|
|
676
|
+
info.shellPid != null && info.foregroundProcessGroupId === info.shellPid
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function isExpectedPiProcess(
|
|
681
|
+
process: HerdrForegroundProcess,
|
|
682
|
+
sessionFile: string,
|
|
683
|
+
cwd: string,
|
|
684
|
+
): boolean {
|
|
685
|
+
const sessionIndex = process.argv?.indexOf("--session") ?? -1;
|
|
686
|
+
return (
|
|
687
|
+
(process.name === "pi" || process.argv0?.split("/").pop() === "pi") &&
|
|
688
|
+
sessionIndex >= 0 &&
|
|
689
|
+
process.argv?.[sessionIndex + 1] === sessionFile &&
|
|
690
|
+
process.cwd === cwd
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export async function waitForHerdrPiReady(
|
|
695
|
+
surface: string,
|
|
696
|
+
sessionFile: string,
|
|
697
|
+
cwd: string,
|
|
698
|
+
options: { timeoutMs?: number; intervalMs?: number } = {},
|
|
699
|
+
): Promise<void> {
|
|
700
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
701
|
+
const intervalMs = options.intervalMs ?? 50;
|
|
702
|
+
const deadline = Date.now() + timeoutMs;
|
|
703
|
+
let lastError = "expected Pi process not observed";
|
|
704
|
+
|
|
705
|
+
while (Date.now() <= deadline) {
|
|
706
|
+
try {
|
|
707
|
+
const info = await getHerdrPaneProcessInfoAsync(surface);
|
|
708
|
+
if (
|
|
709
|
+
info.foregroundProcesses.some((process) =>
|
|
710
|
+
isExpectedPiProcess(process, sessionFile, cwd),
|
|
711
|
+
)
|
|
712
|
+
) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
} catch (error) {
|
|
716
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
717
|
+
}
|
|
718
|
+
if (Date.now() >= deadline) break;
|
|
719
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
720
|
+
}
|
|
721
|
+
throw new Error(
|
|
722
|
+
`Timed out waiting for Pi session ${sessionFile} in Herdr pane ${surface}: ${lastError}`,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export async function waitForHerdrShellReady(
|
|
727
|
+
surface: string,
|
|
728
|
+
options: {
|
|
729
|
+
timeoutMs?: number;
|
|
730
|
+
intervalMs?: number;
|
|
731
|
+
signal?: AbortSignal;
|
|
732
|
+
} = {},
|
|
733
|
+
): Promise<void> {
|
|
734
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
735
|
+
const intervalMs = options.intervalMs ?? 50;
|
|
736
|
+
const deadline = Date.now() + timeoutMs;
|
|
737
|
+
let lastError = "no interactive shell foreground process";
|
|
738
|
+
|
|
739
|
+
while (Date.now() <= deadline) {
|
|
740
|
+
if (options.signal?.aborted)
|
|
741
|
+
throw new Error("Shell readiness wait cancelled.");
|
|
742
|
+
try {
|
|
743
|
+
if (isHerdrShellReady(await getHerdrPaneProcessInfoAsync(surface)))
|
|
744
|
+
return;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
747
|
+
}
|
|
748
|
+
if (Date.now() >= deadline) break;
|
|
749
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
750
|
+
}
|
|
751
|
+
throw new Error(
|
|
752
|
+
`Timed out waiting for interactive shell in Herdr pane ${surface}: ${lastError}`,
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export function isProcessAlive(pid: number): boolean {
|
|
757
|
+
try {
|
|
758
|
+
process.kill(pid, 0);
|
|
759
|
+
return true;
|
|
760
|
+
} catch (error) {
|
|
761
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export async function waitForProcessesExit(
|
|
766
|
+
pids: readonly number[],
|
|
767
|
+
options: {
|
|
768
|
+
timeoutMs?: number;
|
|
769
|
+
intervalMs?: number;
|
|
770
|
+
isAlive?: (pid: number) => boolean;
|
|
771
|
+
} = {},
|
|
772
|
+
): Promise<number[]> {
|
|
773
|
+
const isAlive = options.isAlive ?? isProcessAlive;
|
|
774
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
775
|
+
const intervalMs = options.intervalMs ?? 50;
|
|
776
|
+
const remaining = new Set(
|
|
777
|
+
pids.filter((pid) => Number.isInteger(pid) && pid > 0 && isAlive(pid)),
|
|
778
|
+
);
|
|
779
|
+
const deadline = Date.now() + timeoutMs;
|
|
780
|
+
while (remaining.size > 0 && Date.now() < deadline) {
|
|
781
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
782
|
+
for (const pid of remaining) {
|
|
783
|
+
if (!isAlive(pid)) remaining.delete(pid);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return [...remaining];
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
export async function waitForHerdrPaneAbsence(
|
|
790
|
+
surface: string,
|
|
791
|
+
options: {
|
|
792
|
+
timeoutMs?: number;
|
|
793
|
+
intervalMs?: number;
|
|
794
|
+
inspect?: (surface: string) => Promise<PaneInspectionResult>;
|
|
795
|
+
} = {},
|
|
796
|
+
): Promise<boolean> {
|
|
797
|
+
const inspect = options.inspect ?? inspectHerdrPane;
|
|
798
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
799
|
+
const intervalMs = options.intervalMs ?? 50;
|
|
800
|
+
const deadline = Date.now() + timeoutMs;
|
|
801
|
+
while (Date.now() <= deadline) {
|
|
802
|
+
const inspection = await inspect(surface);
|
|
803
|
+
if (inspection.kind === "missing") return true;
|
|
804
|
+
if (Date.now() >= deadline) break;
|
|
805
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
806
|
+
}
|
|
807
|
+
const finalInspection = await inspect(surface);
|
|
808
|
+
return finalInspection.kind === "missing";
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export function sendHerdrCommand(surface: string, command: string): void {
|
|
812
|
+
// pane run sends the text and Enter in a single socket request, avoiding
|
|
813
|
+
// a race where Enter could arrive before the text is fully processed.
|
|
814
|
+
herdrExec(["pane", "run", surface, command]);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export function sendHerdrEscape(surface: string): void {
|
|
818
|
+
herdrExec(["pane", "send-keys", surface, "Escape"]);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function hasHerdrErrorCode(error: any, codes: readonly string[]): boolean {
|
|
822
|
+
const accepted = new Set(codes);
|
|
823
|
+
for (const raw of [error?.stderr, error?.stdout, error?.message]) {
|
|
824
|
+
if (typeof raw !== "string" || !raw.trim()) continue;
|
|
825
|
+
const parsed = parseHerdrJson(raw) as {
|
|
826
|
+
error?: { code?: unknown };
|
|
827
|
+
} | null;
|
|
828
|
+
if (
|
|
829
|
+
typeof parsed?.error?.code === "string" &&
|
|
830
|
+
accepted.has(parsed.error.code)
|
|
831
|
+
) {
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
for (const code of accepted) {
|
|
835
|
+
if (new RegExp(`\\b${code}\\b`).test(raw)) return true;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function lookupHerdrSurfaceTab(surface: string): string | undefined {
|
|
842
|
+
try {
|
|
843
|
+
const output = herdrExec(["pane", "get", surface]);
|
|
844
|
+
const parsed = parseHerdrJson(output) as {
|
|
845
|
+
result?: { pane?: { pane_id?: unknown; tab_id?: unknown } };
|
|
846
|
+
error?: { code?: unknown };
|
|
847
|
+
} | null;
|
|
848
|
+
if (
|
|
849
|
+
parsed?.error?.code === "pane_not_found" ||
|
|
850
|
+
parsed?.error?.code === "not_found"
|
|
851
|
+
) {
|
|
852
|
+
return undefined;
|
|
853
|
+
}
|
|
854
|
+
const pane = parsed?.result?.pane;
|
|
855
|
+
if (
|
|
856
|
+
pane?.pane_id !== surface ||
|
|
857
|
+
typeof pane.tab_id !== "string" ||
|
|
858
|
+
!pane.tab_id
|
|
859
|
+
) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
`Unexpected herdr pane get output: ${output.trim() || "(empty)"}`,
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
rememberHerdrSurfaceTab(surface, pane.tab_id);
|
|
865
|
+
return pane.tab_id;
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (hasHerdrErrorCode(error, ["pane_not_found", "not_found"])) {
|
|
868
|
+
return undefined;
|
|
869
|
+
}
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/** Close the dedicated tab for a surface; an already missing target is success. */
|
|
875
|
+
export function closeHerdrSurface(surface: string): void {
|
|
876
|
+
const rememberedTab = getHerdrSurfaceTabId(surface);
|
|
877
|
+
const tabId = rememberedTab ?? lookupHerdrSurfaceTab(surface);
|
|
878
|
+
if (!tabId) return;
|
|
879
|
+
try {
|
|
880
|
+
herdrExec(["tab", "close", tabId]);
|
|
881
|
+
forgetHerdrTab(tabId);
|
|
882
|
+
} catch (error) {
|
|
883
|
+
if (
|
|
884
|
+
hasHerdrErrorCode(error, [
|
|
885
|
+
"tab_not_found",
|
|
886
|
+
"pane_not_found",
|
|
887
|
+
"not_found",
|
|
888
|
+
])
|
|
889
|
+
) {
|
|
890
|
+
forgetHerdrTab(tabId);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
throw error;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
export function renameHerdrTab(title: string): void {
|
|
898
|
+
const { tab_id: tabId } = getHerdrCurrentPaneInfo();
|
|
899
|
+
herdrExec(["tab", "rename", tabId, title]);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export function renameHerdrWorkspace(title: string): void {
|
|
903
|
+
const { workspace_id: workspaceId } = getHerdrCurrentPaneInfo();
|
|
904
|
+
herdrExec(["workspace", "rename", workspaceId, title]);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export function focusHerdrWorkspace(workspaceId: string): void {
|
|
908
|
+
herdrExec(["workspace", "focus", workspaceId]);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
export const __herdrTest__ = {
|
|
912
|
+
buildCurrentPaneArgs,
|
|
913
|
+
getHerdrCurrentPaneInfo,
|
|
914
|
+
buildTabCreateArgs,
|
|
915
|
+
buildWorktreeCreateArgs,
|
|
916
|
+
parseHerdrJson,
|
|
917
|
+
extractHerdrPaneId,
|
|
918
|
+
extractHerdrRootPaneId,
|
|
919
|
+
extractHerdrWorktree,
|
|
920
|
+
parseHerdrWorktreeList,
|
|
921
|
+
parseHerdrPaneList,
|
|
922
|
+
parsePaneGetOutput,
|
|
923
|
+
parsePaneGetError,
|
|
924
|
+
parsePaneProcessInfo,
|
|
925
|
+
isHerdrShellReady,
|
|
926
|
+
isExpectedPiProcess,
|
|
927
|
+
};
|