@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 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. Copies the active path into a clone session stamped with the fresh checkout path as its working directory.
30
- 3. Starts Pi in the new workspace's root pane (whose shell runs inside the checkout) with `--session <absolute-clone-file>`.
31
- 4. Focuses the new tab after Pi starts successfully.
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
 
@@ -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, rootPaneId!, cloneFile,
291
- `Herdr workspace ${workspaceId}, tab ${tabId}, root pane ${rootPaneId}, and session ${cloneFile}`,
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", tabId!], { cwd: ctx.cwd });
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 ${tabId}, checkout ${checkoutPath}), but focus failed: ${errorMessage(error)}`,
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 ${tabId}, checkout ${checkoutPath}, agent ${agentName}).`,
371
+ `Cloned current conversation into Herdr worktree workspace ${workspaceId} (tab ${targetTabId}, checkout ${checkoutPath}, agent ${agentName}).`,
305
372
  "info",
306
373
  );
307
374
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-herdr-clone",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Clone the current Pi conversation path into a new Herdr tab.",
5
5
  "keywords": [
6
6
  "pi-package",