@henryqw/pi-herdr-rename 2.1.7 → 2.1.9

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
@@ -30,10 +30,10 @@ Requires Pi Coding Agent 0.84.3+.
30
30
 
31
31
  First real user prompt generates a title in the background after Pi expands skill and prompt-template shorthand. It does not delay main reply. Extension-injected prompts, empty prompts, and image-only input are ignored.
32
32
 
33
- Shared [`pi-task-models` config](../pi-task-models#config) at `~/.pi/agent/config/pi-task-models.json` routes task `pi-herdr-rename/rename` to the `fast` profile by default.
33
+ Shared [`pi-task-models` config](../pi-task-models#config) at `~/.pi/agent/config/pi-task-models.json` can explicitly override the local `pi-herdr-rename/rename` declaration, which defaults to `fast`.
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,10 +3,12 @@ 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
+ registerModelTask,
9
10
  resolveConfiguredTaskRoutes,
11
+ type ModelTask,
10
12
  type ResolvedTaskRoute,
11
13
  type TaskRouteError,
12
14
  } from "@henryqw/pi-task-models";
@@ -18,7 +20,12 @@ const MAX_CONTEXT_CHARS = 2_000;
18
20
  const DISPLAY_MAX_WORDS = 4;
19
21
  const DISPLAY_MAX_CHARS = 20;
20
22
  const SEMANTIC_TYPE_MAX_CHARS = 12;
21
- const RENAME_TASK = "pi-herdr-rename/rename";
23
+ export const RENAME_TASK = {
24
+ id: "pi-herdr-rename/rename",
25
+ label: "Conversation rename",
26
+ purpose: "Generate a short conversation title.",
27
+ defaultProfile: "fast",
28
+ } as const satisfies ModelTask;
22
29
  const TITLE_STATE_TYPE = "pi-herdr-rename/title";
23
30
  const HERDR_DEFAULT_WORKTREE_NAME = /^(?:worktree[-/])?(?:brave|calm|clear|green|lucky|quiet|rapid|silver)-(?:river|cloud|field|forest|harbor|meadow|stone|valley)-[0-9a-f]{4}$/;
24
31
  const SEMANTIC_BRANCH = /^[a-z][a-z0-9-]{0,11}\/[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -42,15 +49,22 @@ function configuredRenameRoutes(ctx: ExtensionContext): ResolvedTaskRoute[] {
42
49
  }
43
50
  }
44
51
 
52
+ function validSubject(subject: string): boolean {
53
+ return /^[a-z0-9]+(?: [a-z0-9]+)*$/.test(subject)
54
+ && subject.length <= DISPLAY_MAX_CHARS
55
+ && subject.split(" ").length <= DISPLAY_MAX_WORDS;
56
+ }
57
+
58
+ function isDisplayTitle(value: unknown): value is string {
59
+ if (typeof value !== "string" || !value) return false;
60
+ const subject = value[0].toLowerCase() + value.slice(1);
61
+ return validSubject(subject) && value === subject[0].toUpperCase() + subject.slice(1);
62
+ }
63
+
45
64
  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;
65
+ const match = /^([a-z][a-z0-9-]*): (.+)$/.exec(title);
66
+ if (!match || match[1].length > SEMANTIC_TYPE_MAX_CHARS || !validSubject(match[2])) return undefined;
48
67
  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
68
  return {
55
69
  display: subject[0].toUpperCase() + subject.slice(1),
56
70
  branch: `${match[1]}/${subject.replaceAll(" ", "-")}`,
@@ -63,7 +77,7 @@ function savedTitle(ctx: ExtensionContext): GeneratedTitle | undefined {
63
77
  .find((candidate) => candidate.type === "custom" && candidate.customType === TITLE_STATE_TYPE);
64
78
  if (entry?.type !== "custom" || !entry.data || typeof entry.data !== "object" || Array.isArray(entry.data)) return undefined;
65
79
  const { display, branch } = entry.data as { display?: unknown; branch?: unknown };
66
- return typeof display === "string" && typeof branch === "string" && SEMANTIC_BRANCH.test(branch)
80
+ return isDisplayTitle(display) && typeof branch === "string" && SEMANTIC_BRANCH.test(branch)
67
81
  ? { display, branch }
68
82
  : undefined;
69
83
  }
@@ -200,6 +214,7 @@ async function generateTitle(text: string, ctx: ExtensionContext, signal: AbortS
200
214
  }
201
215
 
202
216
  export default function herdrRenameExtension(pi: ExtensionAPI): void {
217
+ registerModelTask(pi, RENAME_TASK);
203
218
  const herdr = createHerdrClient<{ signal: AbortSignal }>(pi.exec.bind(pi));
204
219
  let latestUserText: string | undefined;
205
220
  let automaticStarted = false;
@@ -216,6 +231,7 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
216
231
  displayTitle: string,
217
232
  branchCandidate: string,
218
233
  previousDisplayTitle: string | undefined,
234
+ forceWorkspaceRename: boolean,
219
235
  request: number,
220
236
  controller: AbortController,
221
237
  ): Promise<void> => {
@@ -247,6 +263,16 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
247
263
  const workspaceName = workspace?.label;
248
264
  if (typeof workspaceName !== "string") throw new Error("Herdr workspace response omitted label.");
249
265
  const worktree = workspace?.worktree;
266
+ if (
267
+ workspaceName !== displayTitle &&
268
+ (forceWorkspaceRename ||
269
+ (worktree?.is_linked_worktree === true &&
270
+ (HERDR_DEFAULT_WORKTREE_NAME.test(workspaceName) || workspaceName === previousDisplayTitle))) &&
271
+ isCurrent(request, controller)
272
+ ) {
273
+ await herdr.run(["workspace", "rename", workspaceId, displayTitle], { signal: controller.signal });
274
+ }
275
+
250
276
  const checkoutPath = worktree?.checkout_path;
251
277
  if (worktree?.is_linked_worktree !== true || typeof checkoutPath !== "string" || !checkoutPath) return;
252
278
 
@@ -258,22 +284,18 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
258
284
  return result.stdout.trim();
259
285
  };
260
286
 
261
- const branch = await runGit(["branch", "--show-current"]);
262
- if (!branch || branch.startsWith("worktree/")) {
287
+ await withWorktreeLock(checkoutPath, async () => {
263
288
  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
- }
289
+ const branch = await runGit(["branch", "--show-current"]);
290
+ if (!branch || branch.startsWith("worktree/")) {
291
+ if (!isCurrent(request, controller)) return;
292
+ const branches = (await runGit(["for-each-ref", "--format=%(refname:short)", "refs/heads"]))
293
+ .split("\n")
294
+ .filter(Boolean);
295
+ const semanticBranch = availableBranch(branchCandidate, branches);
296
+ await runGit(branch ? ["branch", "-m", semanticBranch] : ["switch", "-c", semanticBranch]);
297
+ }
298
+ });
277
299
  };
278
300
 
279
301
  const begin = () => {
@@ -300,10 +322,10 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
300
322
  const previousDisplayTitle = saved && pi.getSessionName() === saved.display ? saved.display : undefined;
301
323
  pi.setSessionName(title.display);
302
324
  pi.appendEntry(TITLE_STATE_TYPE, title);
303
- await applyHerdr(title.display, title.branch, previousDisplayTitle, request, controller);
325
+ await applyHerdr(title.display, title.branch, previousDisplayTitle, manual, request, controller);
304
326
  return title.display;
305
327
  } catch (error) {
306
- if (isCurrent(request, controller) && (manual || error instanceof RenameModelError)) {
328
+ if (isCurrent(request, controller)) {
307
329
  ctx.ui.notify(error instanceof Error ? error.message : "Rename failed.", "warning");
308
330
  }
309
331
  return undefined;
@@ -328,7 +350,7 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
328
350
  latestUserText = latestSessionUserText(ctx);
329
351
  try {
330
352
  const taskModels = readTaskModelsConfig();
331
- const profileName = taskModels.tasks[RENAME_TASK];
353
+ const profileName = taskModels.tasks[RENAME_TASK.id] ?? RENAME_TASK.defaultProfile;
332
354
  if (!taskModels.profiles[profileName]) {
333
355
  ctx.ui.notify(`Configure rename task profile ${profileName} with /task-models.`, "warning");
334
356
  }
@@ -341,8 +363,12 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
341
363
  if (!title || title !== saved?.display) return;
342
364
 
343
365
  const { request, controller } = begin();
344
- void applyHerdr(title, saved.branch, saved.display, request, controller)
345
- .catch(() => undefined)
366
+ void applyHerdr(title, saved.branch, saved.display, false, request, controller)
367
+ .catch((error) => {
368
+ if (isCurrent(request, controller)) {
369
+ ctx.ui.notify(error instanceof Error ? error.message : "Rename failed.", "warning");
370
+ }
371
+ })
346
372
  .finally(() => finish(request, controller));
347
373
  });
348
374
 
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.9",
4
4
  "description": "Generate short Pi display titles and rename the current Herdr location.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -45,6 +45,6 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@henryqw/pi-herdr": "^0.3.0",
48
- "@henryqw/pi-task-models": "^1.0.0"
48
+ "@henryqw/pi-task-models": "^3.0.0"
49
49
  }
50
50
  }