@henryqw/pi-herdr-rename 0.2.0 → 0.3.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/README.md CHANGED
@@ -17,9 +17,9 @@ pi remove npm:@henryqw/pi-herdr-rename
17
17
  ## Behavior
18
18
 
19
19
  - On the first real, non-empty text prompt in a new session, title generation starts in the background and does not delay the main Pi response. Extension-injected prompts, empty prompts, and image-only input are ignored.
20
- - Successful titles are lowercase, at most five words, and at most 60 characters. The first 1,000 characters of user text are sent to the rename model; prompt content is never logged.
20
+ - Successful titles are lowercase and, by default, at most four words and 40 characters. The first 1,000 characters of user text are sent to the rename model; prompt content is never logged.
21
21
  - A successful title updates the Pi session name and current Herdr pane. The enclosing Herdr tab is updated only when the current tab has one pane. Outside Herdr, only the Pi session name changes.
22
- - Resuming a named session reapplies its saved title without another rename-model request. Automatic failures stay quiet and do not change labels; there is no local fallback or retry.
22
+ - Resuming a named session reapplies its saved title without another rename-model request. Automatic rename-model errors show a warning; other automatic failures stay quiet. Failures do not change labels, fall back locally, or retry.
23
23
 
24
24
  ## Manual rename
25
25
 
@@ -35,11 +35,13 @@ The selection is saved in:
35
35
 
36
36
  ```json
37
37
  {
38
- "model": "provider/model"
38
+ "model": "provider/model",
39
+ "maxWords": 4,
40
+ "maxChars": 40
39
41
  }
40
42
  ```
41
43
 
42
- Missing, malformed, or unavailable configuration never falls back to another model.
44
+ `maxWords` and `maxChars` accept positive integers and default to 4 and 40. Invalid limits use their defaults. Missing, malformed, or unavailable model selection never falls back to another model.
43
45
 
44
46
  ## Development
45
47
 
@@ -6,29 +6,43 @@ import {
6
6
  type ExtensionAPI,
7
7
  type ExtensionContext,
8
8
  } from "@earendil-works/pi-coding-agent";
9
+ import { createHerdrClient } from "@henryqw/pi-herdr";
9
10
 
10
11
  const WIDGET_KEY = "pi-herdr-rename";
11
12
  const WIDGET_RESULT_MS = 2_000;
12
13
  const MAX_MESSAGE_CHARS = 1_000;
13
14
  const MAX_CONTEXT_CHARS = 4_000;
14
- const TITLE_PROMPT =
15
- "Return only a short chat title for the current conversation topic, prioritizing the most recent user intent: lowercase, at most five words, and at most 60 characters.";
15
+ const DEFAULT_MAX_WORDS = 4;
16
+ const DEFAULT_MAX_CHARS = 40;
16
17
  const configPath = () => join(getAgentDir(), "config", "pi-herdr-rename.json");
17
18
 
18
- async function configuredModel(): Promise<string | undefined> {
19
+ type RenameConfig = { model?: string; maxWords: number; maxChars: number };
20
+
21
+ class RenameModelError extends Error {}
22
+
23
+ const positiveInteger = (value: unknown, fallback: number) =>
24
+ typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
25
+
26
+ async function configured(): Promise<RenameConfig> {
19
27
  try {
20
28
  const config: unknown = JSON.parse(await readFile(configPath(), "utf8"));
21
29
  if (config && typeof config === "object" && !Array.isArray(config)) {
22
- const model = (config as { model?: unknown }).model;
23
- if (typeof model === "string" && /^[^\s/]+\/\S+$/.test(model)) return model;
30
+ const values = config as { model?: unknown; maxWords?: unknown; maxChars?: unknown };
31
+ return {
32
+ model: typeof values.model === "string" && /^[^\s/]+\/\S+$/.test(values.model) ? values.model : undefined,
33
+ maxWords: positiveInteger(values.maxWords, DEFAULT_MAX_WORDS),
34
+ maxChars: positiveInteger(values.maxChars, DEFAULT_MAX_CHARS),
35
+ };
24
36
  }
25
37
  } catch {}
38
+ return { maxWords: DEFAULT_MAX_WORDS, maxChars: DEFAULT_MAX_CHARS };
26
39
  }
27
40
 
28
41
  async function saveModel(model: string): Promise<void> {
29
42
  const path = configPath();
43
+ const { maxWords, maxChars } = await configured();
30
44
  await mkdir(dirname(path), { recursive: true });
31
- await writeFile(path, `${JSON.stringify({ model }, null, 2)}\n`, "utf8");
45
+ await writeFile(path, `${JSON.stringify({ model, maxWords, maxChars }, null, 2)}\n`, "utf8");
32
46
  }
33
47
 
34
48
  function messageText(content: unknown): string {
@@ -85,7 +99,7 @@ function recentConversation(ctx: ExtensionContext, fallback?: string): string |
85
99
  }
86
100
 
87
101
  async function generateTitle(text: string, ctx: ExtensionContext, signal: AbortSignal): Promise<string> {
88
- const key = await configuredModel();
102
+ const { model: key, maxWords, maxChars } = await configured();
89
103
  if (!key) throw new Error("Rename model is not configured. Run /rename-model.");
90
104
  const separator = key.indexOf("/");
91
105
  const provider = key.slice(0, separator);
@@ -98,11 +112,14 @@ async function generateTitle(text: string, ctx: ExtensionContext, signal: AbortS
98
112
  const response = await ctx.modelRegistry.complete(
99
113
  model,
100
114
  {
101
- systemPrompt: TITLE_PROMPT,
115
+ systemPrompt: `Return only a short chat title for the current conversation topic, prioritizing the most recent user intent: lowercase, at most ${maxWords} words, and at most ${maxChars} characters.`,
102
116
  messages: [{ role: "user", content: text.slice(0, MAX_CONTEXT_CHARS), timestamp: Date.now() }],
103
117
  },
104
118
  { signal, maxRetries: 0, maxTokens: 64 },
105
119
  );
120
+ if (response.stopReason === "error") {
121
+ throw new RenameModelError(response.errorMessage || "Rename model failed.");
122
+ }
106
123
  if (response.stopReason !== "stop") throw new Error("Rename model did not return a complete title.");
107
124
 
108
125
  const title = response.content
@@ -112,21 +129,15 @@ async function generateTitle(text: string, ctx: ExtensionContext, signal: AbortS
112
129
  .trim()
113
130
  .toLowerCase()
114
131
  .replace(/\s+/g, " ");
115
- if (!title || title.length > 60 || title.split(" ").length > 5) {
132
+ if (!title || title.length > maxChars || title.split(" ").length > maxWords) {
116
133
  throw new Error("Rename model returned an invalid title.");
117
134
  }
118
135
  return title;
119
136
  }
120
137
 
121
- async function herdr(pi: ExtensionAPI, args: string[], signal: AbortSignal): Promise<string> {
122
- const result = await pi.exec("herdr", args, { signal });
123
- if (result.code !== 0 || result.killed) {
124
- throw new Error(`Herdr ${args[0]} failed: ${result.stderr.trim() || `exit code ${result.code}`}`);
125
- }
126
- return result.stdout;
127
- }
128
-
129
138
  export default function herdrRenameExtension(pi: ExtensionAPI): void {
139
+ const herdr = createHerdrClient<{ signal: AbortSignal }>((command, args, options) =>
140
+ pi.exec(command, [...args], options));
130
141
  let latestUserText: string | undefined;
131
142
  let automaticStarted = false;
132
143
  let sequence = 0;
@@ -142,21 +153,19 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
142
153
  if (!paneId) return;
143
154
 
144
155
  if (!isCurrent(request, controller)) return;
145
- await herdr(pi, ["pane", "rename", paneId, title], controller.signal);
156
+ await herdr.run(["pane", "rename", paneId, title], { signal: controller.signal });
146
157
  if (!isCurrent(request, controller)) return;
147
158
 
148
- const paneResponse: unknown = JSON.parse(
149
- await herdr(pi, ["pane", "get", paneId], controller.signal),
150
- );
159
+ const paneResponse: unknown = await herdr.json(["pane", "get", paneId], { signal: controller.signal });
151
160
  const tabId = (paneResponse as { result?: { pane?: { tab_id?: unknown } } }).result?.pane?.tab_id;
152
161
  if (typeof tabId !== "string" || !tabId) throw new Error("Herdr pane response omitted tab_id.");
153
162
  if (!isCurrent(request, controller)) return;
154
163
 
155
- const tabResponse: unknown = JSON.parse(await herdr(pi, ["tab", "get", tabId], controller.signal));
164
+ const tabResponse: unknown = await herdr.json(["tab", "get", tabId], { signal: controller.signal });
156
165
  const paneCount = (tabResponse as { result?: { tab?: { pane_count?: unknown } } }).result?.tab?.pane_count;
157
166
  if (typeof paneCount !== "number") throw new Error("Herdr tab response omitted pane_count.");
158
167
  if (paneCount === 1 && isCurrent(request, controller)) {
159
- await herdr(pi, ["tab", "rename", tabId, title], controller.signal);
168
+ await herdr.run(["tab", "rename", tabId, title], { signal: controller.signal });
160
169
  }
161
170
  };
162
171
 
@@ -180,7 +189,7 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
180
189
  await applyHerdr(title, request, controller);
181
190
  return title;
182
191
  } catch (error) {
183
- if (manual && isCurrent(request, controller)) {
192
+ if (isCurrent(request, controller) && (manual || error instanceof RenameModelError)) {
184
193
  ctx.ui.notify(error instanceof Error ? error.message : "Rename failed.", "warning");
185
194
  }
186
195
  return undefined;
@@ -202,7 +211,7 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
202
211
  active = undefined;
203
212
  sequence++;
204
213
  latestUserText = latestSessionUserText(ctx);
205
- if (!(await configuredModel())) {
214
+ if (!(await configured()).model) {
206
215
  ctx.ui.notify("Run /rename-model to configure chat title generation.", "warning");
207
216
  }
208
217
  const title = pi.getSessionName();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-herdr-rename",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Generate short Pi chat titles and rename the current Herdr location.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -42,5 +42,8 @@
42
42
  "extensions": [
43
43
  "./extensions"
44
44
  ]
45
+ },
46
+ "dependencies": {
47
+ "@henryqw/pi-herdr": "^0.1.0"
45
48
  }
46
49
  }