@henryqw/pi-herdr-rename 2.1.7 → 2.1.8

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
@@ -34,6 +34,6 @@ Shared [`pi-task-models` config](../pi-task-models#config) at `~/.pi/agent/confi
34
34
 
35
35
  Display titles are natural task phrases, preferably three or four words and always at most four words and 20 characters. Model classification stays internal: `refactor: update task logic` displays as `Update task logic` and maps to Git branch `refactor/update-task-logic`.
36
36
 
37
- In a linked worktree, a detached checkout or Herdr `worktree/...` branch is renamed; an existing non-generated branch stays. A generated workspace label such as `worktree-brave-meadow-4aa8` becomes display title; custom workspace names stay. Enclosing Herdr tab updates only when this pane is tab's only pane. Outside Herdr, only Pi session name changes.
37
+ In a linked worktree, a detached checkout or Herdr `worktree/...` branch is renamed; an existing non-generated branch stays. A generated workspace label such as `worktree-brave-meadow-4aa8` becomes the display title automatically; `/rename` also replaces a custom workspace name. Enclosing Herdr tab updates only when this pane is tab's only pane. Outside Herdr, only Pi session name changes.
38
38
 
39
- Tries assigned profile primary, then fallback, while honoring configured thinking level. Never substitutes current session model. No viable route leaves titles unchanged. Resuming a session created by this version reapplies saved display title and semantic branch without another model request. Older titles receive no migration.
39
+ Tries assigned profile primary, then fallback, while honoring configured thinking level. Never substitutes current session model. No viable route leaves titles unchanged. Resuming a session created by this version reapplies saved display title and semantic branch without another model request. Herdr and Git synchronization failures appear as warnings; cancellation by a newer rename remains silent. Older titles receive no migration.
@@ -3,7 +3,7 @@ import {
3
3
  type ExtensionAPI,
4
4
  type ExtensionContext,
5
5
  } from "@earendil-works/pi-coding-agent";
6
- import { createHerdrClient } from "@henryqw/pi-herdr";
6
+ import { createHerdrClient, withWorktreeLock } from "@henryqw/pi-herdr";
7
7
  import {
8
8
  readTaskModelsConfig,
9
9
  resolveConfiguredTaskRoutes,
@@ -42,15 +42,22 @@ function configuredRenameRoutes(ctx: ExtensionContext): ResolvedTaskRoute[] {
42
42
  }
43
43
  }
44
44
 
45
+ function validSubject(subject: string): boolean {
46
+ return /^[a-z0-9]+(?: [a-z0-9]+)*$/.test(subject)
47
+ && subject.length <= DISPLAY_MAX_CHARS
48
+ && subject.split(" ").length <= DISPLAY_MAX_WORDS;
49
+ }
50
+
51
+ function isDisplayTitle(value: unknown): value is string {
52
+ if (typeof value !== "string" || !value) return false;
53
+ const subject = value[0].toLowerCase() + value.slice(1);
54
+ return validSubject(subject) && value === subject[0].toUpperCase() + subject.slice(1);
55
+ }
56
+
45
57
  function parseGeneratedTitle(title: string): GeneratedTitle | undefined {
46
- const match = /^([a-z][a-z0-9-]*): ([a-z0-9]+(?: [a-z0-9]+)*)$/.exec(title);
47
- if (!match) return undefined;
58
+ const match = /^([a-z][a-z0-9-]*): (.+)$/.exec(title);
59
+ if (!match || match[1].length > SEMANTIC_TYPE_MAX_CHARS || !validSubject(match[2])) return undefined;
48
60
  const subject = match[2];
49
- if (
50
- match[1].length > SEMANTIC_TYPE_MAX_CHARS ||
51
- subject.length > DISPLAY_MAX_CHARS ||
52
- subject.split(" ").length > DISPLAY_MAX_WORDS
53
- ) return undefined;
54
61
  return {
55
62
  display: subject[0].toUpperCase() + subject.slice(1),
56
63
  branch: `${match[1]}/${subject.replaceAll(" ", "-")}`,
@@ -63,7 +70,7 @@ function savedTitle(ctx: ExtensionContext): GeneratedTitle | undefined {
63
70
  .find((candidate) => candidate.type === "custom" && candidate.customType === TITLE_STATE_TYPE);
64
71
  if (entry?.type !== "custom" || !entry.data || typeof entry.data !== "object" || Array.isArray(entry.data)) return undefined;
65
72
  const { display, branch } = entry.data as { display?: unknown; branch?: unknown };
66
- return typeof display === "string" && typeof branch === "string" && SEMANTIC_BRANCH.test(branch)
73
+ return isDisplayTitle(display) && typeof branch === "string" && SEMANTIC_BRANCH.test(branch)
67
74
  ? { display, branch }
68
75
  : undefined;
69
76
  }
@@ -216,6 +223,7 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
216
223
  displayTitle: string,
217
224
  branchCandidate: string,
218
225
  previousDisplayTitle: string | undefined,
226
+ forceWorkspaceRename: boolean,
219
227
  request: number,
220
228
  controller: AbortController,
221
229
  ): Promise<void> => {
@@ -247,6 +255,16 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
247
255
  const workspaceName = workspace?.label;
248
256
  if (typeof workspaceName !== "string") throw new Error("Herdr workspace response omitted label.");
249
257
  const worktree = workspace?.worktree;
258
+ if (
259
+ workspaceName !== displayTitle &&
260
+ (forceWorkspaceRename ||
261
+ (worktree?.is_linked_worktree === true &&
262
+ (HERDR_DEFAULT_WORKTREE_NAME.test(workspaceName) || workspaceName === previousDisplayTitle))) &&
263
+ isCurrent(request, controller)
264
+ ) {
265
+ await herdr.run(["workspace", "rename", workspaceId, displayTitle], { signal: controller.signal });
266
+ }
267
+
250
268
  const checkoutPath = worktree?.checkout_path;
251
269
  if (worktree?.is_linked_worktree !== true || typeof checkoutPath !== "string" || !checkoutPath) return;
252
270
 
@@ -258,22 +276,18 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
258
276
  return result.stdout.trim();
259
277
  };
260
278
 
261
- const branch = await runGit(["branch", "--show-current"]);
262
- if (!branch || branch.startsWith("worktree/")) {
279
+ await withWorktreeLock(checkoutPath, async () => {
263
280
  if (!isCurrent(request, controller)) return;
264
- const branches = (await runGit(["for-each-ref", "--format=%(refname:short)", "refs/heads"]))
265
- .split("\n")
266
- .filter(Boolean);
267
- const semanticBranch = availableBranch(branchCandidate, branches);
268
- await runGit(branch ? ["branch", "-m", semanticBranch] : ["switch", "-c", semanticBranch]);
269
- }
270
- if (
271
- workspaceName !== displayTitle &&
272
- (HERDR_DEFAULT_WORKTREE_NAME.test(workspaceName) || workspaceName === previousDisplayTitle) &&
273
- isCurrent(request, controller)
274
- ) {
275
- await herdr.run(["workspace", "rename", workspaceId, displayTitle], { signal: controller.signal });
276
- }
281
+ const branch = await runGit(["branch", "--show-current"]);
282
+ if (!branch || branch.startsWith("worktree/")) {
283
+ if (!isCurrent(request, controller)) return;
284
+ const branches = (await runGit(["for-each-ref", "--format=%(refname:short)", "refs/heads"]))
285
+ .split("\n")
286
+ .filter(Boolean);
287
+ const semanticBranch = availableBranch(branchCandidate, branches);
288
+ await runGit(branch ? ["branch", "-m", semanticBranch] : ["switch", "-c", semanticBranch]);
289
+ }
290
+ });
277
291
  };
278
292
 
279
293
  const begin = () => {
@@ -300,10 +314,10 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
300
314
  const previousDisplayTitle = saved && pi.getSessionName() === saved.display ? saved.display : undefined;
301
315
  pi.setSessionName(title.display);
302
316
  pi.appendEntry(TITLE_STATE_TYPE, title);
303
- await applyHerdr(title.display, title.branch, previousDisplayTitle, request, controller);
317
+ await applyHerdr(title.display, title.branch, previousDisplayTitle, manual, request, controller);
304
318
  return title.display;
305
319
  } catch (error) {
306
- if (isCurrent(request, controller) && (manual || error instanceof RenameModelError)) {
320
+ if (isCurrent(request, controller)) {
307
321
  ctx.ui.notify(error instanceof Error ? error.message : "Rename failed.", "warning");
308
322
  }
309
323
  return undefined;
@@ -341,8 +355,12 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
341
355
  if (!title || title !== saved?.display) return;
342
356
 
343
357
  const { request, controller } = begin();
344
- void applyHerdr(title, saved.branch, saved.display, request, controller)
345
- .catch(() => undefined)
358
+ void applyHerdr(title, saved.branch, saved.display, false, request, controller)
359
+ .catch((error) => {
360
+ if (isCurrent(request, controller)) {
361
+ ctx.ui.notify(error instanceof Error ? error.message : "Rename failed.", "warning");
362
+ }
363
+ })
346
364
  .finally(() => finish(request, controller));
347
365
  });
348
366
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-herdr-rename",
3
- "version": "2.1.7",
3
+ "version": "2.1.8",
4
4
  "description": "Generate short Pi display titles and rename the current Herdr location.",
5
5
  "keywords": [
6
6
  "pi-package",