@henryqw/pi-herdr-rename 0.2.0 → 0.3.0

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
 
@@ -11,24 +11,37 @@ const WIDGET_KEY = "pi-herdr-rename";
11
11
  const WIDGET_RESULT_MS = 2_000;
12
12
  const MAX_MESSAGE_CHARS = 1_000;
13
13
  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.";
14
+ const DEFAULT_MAX_WORDS = 4;
15
+ const DEFAULT_MAX_CHARS = 40;
16
16
  const configPath = () => join(getAgentDir(), "config", "pi-herdr-rename.json");
17
17
 
18
- async function configuredModel(): Promise<string | undefined> {
18
+ type RenameConfig = { model?: string; maxWords: number; maxChars: number };
19
+
20
+ class RenameModelError extends Error {}
21
+
22
+ const positiveInteger = (value: unknown, fallback: number) =>
23
+ typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
24
+
25
+ async function configured(): Promise<RenameConfig> {
19
26
  try {
20
27
  const config: unknown = JSON.parse(await readFile(configPath(), "utf8"));
21
28
  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;
29
+ const values = config as { model?: unknown; maxWords?: unknown; maxChars?: unknown };
30
+ return {
31
+ model: typeof values.model === "string" && /^[^\s/]+\/\S+$/.test(values.model) ? values.model : undefined,
32
+ maxWords: positiveInteger(values.maxWords, DEFAULT_MAX_WORDS),
33
+ maxChars: positiveInteger(values.maxChars, DEFAULT_MAX_CHARS),
34
+ };
24
35
  }
25
36
  } catch {}
37
+ return { maxWords: DEFAULT_MAX_WORDS, maxChars: DEFAULT_MAX_CHARS };
26
38
  }
27
39
 
28
40
  async function saveModel(model: string): Promise<void> {
29
41
  const path = configPath();
42
+ const { maxWords, maxChars } = await configured();
30
43
  await mkdir(dirname(path), { recursive: true });
31
- await writeFile(path, `${JSON.stringify({ model }, null, 2)}\n`, "utf8");
44
+ await writeFile(path, `${JSON.stringify({ model, maxWords, maxChars }, null, 2)}\n`, "utf8");
32
45
  }
33
46
 
34
47
  function messageText(content: unknown): string {
@@ -85,7 +98,7 @@ function recentConversation(ctx: ExtensionContext, fallback?: string): string |
85
98
  }
86
99
 
87
100
  async function generateTitle(text: string, ctx: ExtensionContext, signal: AbortSignal): Promise<string> {
88
- const key = await configuredModel();
101
+ const { model: key, maxWords, maxChars } = await configured();
89
102
  if (!key) throw new Error("Rename model is not configured. Run /rename-model.");
90
103
  const separator = key.indexOf("/");
91
104
  const provider = key.slice(0, separator);
@@ -98,11 +111,14 @@ async function generateTitle(text: string, ctx: ExtensionContext, signal: AbortS
98
111
  const response = await ctx.modelRegistry.complete(
99
112
  model,
100
113
  {
101
- systemPrompt: TITLE_PROMPT,
114
+ 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
115
  messages: [{ role: "user", content: text.slice(0, MAX_CONTEXT_CHARS), timestamp: Date.now() }],
103
116
  },
104
117
  { signal, maxRetries: 0, maxTokens: 64 },
105
118
  );
119
+ if (response.stopReason === "error") {
120
+ throw new RenameModelError(response.errorMessage || "Rename model failed.");
121
+ }
106
122
  if (response.stopReason !== "stop") throw new Error("Rename model did not return a complete title.");
107
123
 
108
124
  const title = response.content
@@ -112,7 +128,7 @@ async function generateTitle(text: string, ctx: ExtensionContext, signal: AbortS
112
128
  .trim()
113
129
  .toLowerCase()
114
130
  .replace(/\s+/g, " ");
115
- if (!title || title.length > 60 || title.split(" ").length > 5) {
131
+ if (!title || title.length > maxChars || title.split(" ").length > maxWords) {
116
132
  throw new Error("Rename model returned an invalid title.");
117
133
  }
118
134
  return title;
@@ -180,7 +196,7 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
180
196
  await applyHerdr(title, request, controller);
181
197
  return title;
182
198
  } catch (error) {
183
- if (manual && isCurrent(request, controller)) {
199
+ if (isCurrent(request, controller) && (manual || error instanceof RenameModelError)) {
184
200
  ctx.ui.notify(error instanceof Error ? error.message : "Rename failed.", "warning");
185
201
  }
186
202
  return undefined;
@@ -202,7 +218,7 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
202
218
  active = undefined;
203
219
  sequence++;
204
220
  latestUserText = latestSessionUserText(ctx);
205
- if (!(await configuredModel())) {
221
+ if (!(await configured()).model) {
206
222
  ctx.ui.notify("Run /rename-model to configure chat title generation.", "warning");
207
223
  }
208
224
  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.0",
4
4
  "description": "Generate short Pi chat titles and rename the current Herdr location.",
5
5
  "keywords": [
6
6
  "pi-package",