@henryqw/pi-herdr-clone 0.2.1 → 0.2.2
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/README.md +5 -3
- package/extensions/clone-tab.ts +76 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,9 +26,11 @@ Both commands wait until Pi is idle, then validate the current Herdr pane (`HERD
|
|
|
26
26
|
### `/clone-worktree` behavior
|
|
27
27
|
|
|
28
28
|
1. Creates a Git worktree-backed workspace with `herdr worktree create --workspace <current-workspace> --no-focus`; Herdr creates the branch from `HEAD` unless the name exists, checks out the worktree under its configured `worktrees.directory`, and opens it as a grouped workspace.
|
|
29
|
-
2.
|
|
30
|
-
3.
|
|
31
|
-
4.
|
|
29
|
+
2. Waits briefly for any worktree-layout plugin (for example `herdr-plus`) to start its own agent in the new workspace's root pane.
|
|
30
|
+
3. If the root pane is occupied, creates an additional unfocused tab in the new workspace with the checkout as its working directory, so the plugin's agent and the clone coexist. Otherwise the clone uses the root pane.
|
|
31
|
+
4. Copies the active path into a clone session stamped with the fresh checkout path as its working directory.
|
|
32
|
+
5. Starts Pi in the chosen pane with `--session <absolute-clone-file>`.
|
|
33
|
+
6. Focuses the clone's tab after Pi starts successfully.
|
|
32
34
|
|
|
33
35
|
### Failure semantics
|
|
34
36
|
|
package/extensions/clone-tab.ts
CHANGED
|
@@ -105,6 +105,46 @@ async function createBranchedClone(
|
|
|
105
105
|
return cloneFile;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// A worktree layout plugin (e.g. herdr-plus) can start its own agent in a new
|
|
109
|
+
// worktree's root pane right after creation. Give it a moment to appear so the
|
|
110
|
+
// clone can move to its own tab instead of failing with agent_pane_busy.
|
|
111
|
+
async function waitForRootPaneAgent(
|
|
112
|
+
herdr: HerdrClient<{ cwd: string }>,
|
|
113
|
+
ctx: ExtensionCommandContext,
|
|
114
|
+
paneId: string,
|
|
115
|
+
): Promise<string | undefined> {
|
|
116
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
117
|
+
const response = await herdr.json(["pane", "get", paneId], { cwd: ctx.cwd });
|
|
118
|
+
const agent = (response as { result?: { pane?: { agent?: unknown } } }).result?.pane?.agent;
|
|
119
|
+
if (typeof agent === "string" && agent.trim()) return agent;
|
|
120
|
+
await delay(250);
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseCreatedTab(createdTab: HerdrExecResult, workspaceId: string): { tabId: string; rootPaneId: string } {
|
|
126
|
+
let tabId: string | undefined;
|
|
127
|
+
let rootPaneId: string | undefined;
|
|
128
|
+
try {
|
|
129
|
+
const response: unknown = JSON.parse(createdTab.stdout);
|
|
130
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) {
|
|
131
|
+
throw new Error("Herdr tab create returned invalid JSON");
|
|
132
|
+
}
|
|
133
|
+
const result = (response as { result?: { tab?: { tab_id?: unknown }; root_pane?: { pane_id?: unknown } } }).result;
|
|
134
|
+
tabId = typeof result?.tab?.tab_id === "string" && result.tab.tab_id.trim() ? result.tab.tab_id : undefined;
|
|
135
|
+
rootPaneId = typeof result?.root_pane?.pane_id === "string" && result.root_pane.pane_id.trim()
|
|
136
|
+
? result.root_pane.pane_id
|
|
137
|
+
: undefined;
|
|
138
|
+
const missing = [!tabId && "tab_id", !rootPaneId && "root_pane.pane_id"].filter(Boolean).join(", ");
|
|
139
|
+
if (missing) throw new Error(`Herdr tab create response is missing ${missing}.`);
|
|
140
|
+
return { tabId: tabId!, rootPaneId: rootPaneId! };
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const retained = [`workspace ${workspaceId}`, tabId && `tab ${tabId}`, rootPaneId && `root pane ${rootPaneId}`]
|
|
143
|
+
.filter(Boolean).join(", ");
|
|
144
|
+
throw new Error(`Herdr tab create response could not be parsed; retained ${retained}: ${errorMessage(error)}`, { cause: error });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
108
148
|
async function discardCloneOrAggregate(cloneFile: string, error: Error): Promise<never> {
|
|
109
149
|
try {
|
|
110
150
|
await unlink(cloneFile);
|
|
@@ -123,16 +163,22 @@ async function launchCloneAgent(
|
|
|
123
163
|
rootPaneId: string,
|
|
124
164
|
cloneFile: string,
|
|
125
165
|
retained: string,
|
|
166
|
+
onPaneBusy?: () => Promise<{ rootPaneId: string; retained: string }>,
|
|
126
167
|
): Promise<string> {
|
|
127
168
|
const agentName = `clone-${randomUUID().replaceAll("-", "").slice(0, 24)}`;
|
|
128
|
-
const startArgs = [
|
|
129
|
-
"agent", "start", agentName, "--kind", "pi", "--pane", rootPaneId,
|
|
130
|
-
"--", "--session", cloneFile,
|
|
131
|
-
];
|
|
132
169
|
try {
|
|
133
170
|
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
171
|
+
const startArgs = [
|
|
172
|
+
"agent", "start", agentName, "--kind", "pi", "--pane", rootPaneId,
|
|
173
|
+
"--", "--session", cloneFile,
|
|
174
|
+
];
|
|
134
175
|
const result = await herdr.exec(startArgs, { cwd: ctx.cwd });
|
|
135
176
|
if (result.code === 0 && !result.killed) return agentName;
|
|
177
|
+
if (hasHerdrErrorCode(result, "agent_pane_busy") && onPaneBusy) {
|
|
178
|
+
({ rootPaneId, retained } = await onPaneBusy());
|
|
179
|
+
onPaneBusy = undefined;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
136
182
|
if (!hasHerdrErrorCode(result, "agent_pane_busy") || attempt === 5) {
|
|
137
183
|
throw new Error(herdrCommandFailure(startArgs, result));
|
|
138
184
|
}
|
|
@@ -275,6 +321,20 @@ export default function herdrCloneExtension(pi: ExtensionAPI): void {
|
|
|
275
321
|
);
|
|
276
322
|
}
|
|
277
323
|
|
|
324
|
+
// A layout plugin may have claimed the root pane; park the clone in its
|
|
325
|
+
// own tab so both agents coexist.
|
|
326
|
+
let targetTabId = tabId!;
|
|
327
|
+
let targetPaneId = rootPaneId!;
|
|
328
|
+
const createCloneTab = async (): Promise<void> => {
|
|
329
|
+
const cloneTabArgs = ["tab", "create", "--workspace", workspaceId!, "--cwd", checkoutPath!, "--no-focus"] as const;
|
|
330
|
+
const createdTab = await herdr.exec(cloneTabArgs, { cwd: ctx.cwd });
|
|
331
|
+
if (createdTab.code !== 0 || createdTab.killed) {
|
|
332
|
+
throw new Error(herdrCommandFailure(cloneTabArgs, createdTab));
|
|
333
|
+
}
|
|
334
|
+
({ tabId: targetTabId, rootPaneId: targetPaneId } = parseCreatedTab(createdTab, workspaceId!));
|
|
335
|
+
};
|
|
336
|
+
if (await waitForRootPaneAgent(herdr, ctx, rootPaneId!)) await createCloneTab();
|
|
337
|
+
|
|
278
338
|
let cloneFile: string;
|
|
279
339
|
try {
|
|
280
340
|
cloneFile = source.checkout
|
|
@@ -287,21 +347,28 @@ export default function herdrCloneExtension(pi: ExtensionAPI): void {
|
|
|
287
347
|
);
|
|
288
348
|
}
|
|
289
349
|
const agentName = await launchCloneAgent(
|
|
290
|
-
herdr, ctx,
|
|
291
|
-
`Herdr workspace ${workspaceId}, tab ${
|
|
350
|
+
herdr, ctx, targetPaneId, cloneFile,
|
|
351
|
+
`Herdr workspace ${workspaceId}, tab ${targetTabId}, root pane ${targetPaneId}, and session ${cloneFile}`,
|
|
352
|
+
targetPaneId === rootPaneId ? async () => {
|
|
353
|
+
await createCloneTab();
|
|
354
|
+
return {
|
|
355
|
+
rootPaneId: targetPaneId,
|
|
356
|
+
retained: `Herdr workspace ${workspaceId}, tab ${targetTabId}, root pane ${targetPaneId}, and session ${cloneFile}`,
|
|
357
|
+
};
|
|
358
|
+
} : undefined,
|
|
292
359
|
);
|
|
293
360
|
|
|
294
361
|
try {
|
|
295
|
-
await herdr.run(["tab", "focus",
|
|
362
|
+
await herdr.run(["tab", "focus", targetTabId], { cwd: ctx.cwd });
|
|
296
363
|
} catch (error) {
|
|
297
364
|
ctx.ui.notify(
|
|
298
|
-
`Clone agent ${agentName} started in Herdr worktree workspace ${workspaceId} (tab ${
|
|
365
|
+
`Clone agent ${agentName} started in Herdr worktree workspace ${workspaceId} (tab ${targetTabId}, checkout ${checkoutPath}), but focus failed: ${errorMessage(error)}`,
|
|
299
366
|
"warning",
|
|
300
367
|
);
|
|
301
368
|
return;
|
|
302
369
|
}
|
|
303
370
|
ctx.ui.notify(
|
|
304
|
-
`Cloned current conversation into Herdr worktree workspace ${workspaceId} (tab ${
|
|
371
|
+
`Cloned current conversation into Herdr worktree workspace ${workspaceId} (tab ${targetTabId}, checkout ${checkoutPath}, agent ${agentName}).`,
|
|
305
372
|
"info",
|
|
306
373
|
);
|
|
307
374
|
},
|