@henryqw/pi-herdr-rename 0.1.0 → 0.2.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
@@ -23,11 +23,11 @@ pi remove npm:@henryqw/pi-herdr-rename
23
23
 
24
24
  ## Manual rename
25
25
 
26
- Run `/rename` to generate a title from up to the three most recent user/assistant rounds. It uses text only, caps each message at 1,000 characters and the complete context at 4,000 characters, and applies the same Pi and Herdr rules. The command warns without changing anything when no user text exists or generation fails. If requests overlap, the latest rename request wins.
26
+ Run `/rename` to generate a title from up to the three most recent user/assistant rounds. It uses text only, caps each message at 1,000 characters and the complete context at 4,000 characters, and applies the same Pi and Herdr rules. An animated widget shows `renaming...`, briefly changes to `renamed to <title>`, then disappears. The command warns without changing anything when no user text exists or generation fails. If requests overlap, the latest rename request wins.
27
27
 
28
28
  ## Rename model
29
29
 
30
- Run `/rename-model` to choose an available authenticated text model with Pi's native selector. The default is `openai-codex/gpt-5.6-luna`.
30
+ Run `/rename-model` to choose the model from Pi's available authenticated text models. Until a valid selection is saved, every session start prompts you to run the command and title generation remains disabled.
31
31
 
32
32
  The selection is saved in:
33
33
 
@@ -39,7 +39,7 @@ The selection is saved in:
39
39
  }
40
40
  ```
41
41
 
42
- Missing or malformed configuration uses the default. An unavailable configured model is not silently replaced; run `/rename-model` to choose another model.
42
+ Missing, malformed, or unavailable configuration never falls back to another model.
43
43
 
44
44
  ## Development
45
45
 
@@ -1,29 +1,28 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import {
4
+ BorderedLoader,
4
5
  getAgentDir,
5
6
  type ExtensionAPI,
6
7
  type ExtensionContext,
7
8
  } from "@earendil-works/pi-coding-agent";
8
9
 
9
- const DEFAULT_MODEL = "openai-codex/gpt-5.6-luna";
10
+ const WIDGET_KEY = "pi-herdr-rename";
11
+ const WIDGET_RESULT_MS = 2_000;
10
12
  const MAX_MESSAGE_CHARS = 1_000;
11
13
  const MAX_CONTEXT_CHARS = 4_000;
12
14
  const TITLE_PROMPT =
13
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
16
  const configPath = () => join(getAgentDir(), "config", "pi-herdr-rename.json");
15
17
 
16
- async function configuredModel(): Promise<string> {
18
+ async function configuredModel(): Promise<string | undefined> {
17
19
  try {
18
20
  const config: unknown = JSON.parse(await readFile(configPath(), "utf8"));
19
21
  if (config && typeof config === "object" && !Array.isArray(config)) {
20
22
  const model = (config as { model?: unknown }).model;
21
23
  if (typeof model === "string" && /^[^\s/]+\/\S+$/.test(model)) return model;
22
24
  }
23
- } catch {
24
- // Missing or malformed config uses the default model.
25
- }
26
- return DEFAULT_MODEL;
25
+ } catch {}
27
26
  }
28
27
 
29
28
  async function saveModel(model: string): Promise<void> {
@@ -87,6 +86,7 @@ function recentConversation(ctx: ExtensionContext, fallback?: string): string |
87
86
 
88
87
  async function generateTitle(text: string, ctx: ExtensionContext, signal: AbortSignal): Promise<string> {
89
88
  const key = await configuredModel();
89
+ if (!key) throw new Error("Rename model is not configured. Run /rename-model.");
90
90
  const separator = key.indexOf("/");
91
91
  const provider = key.slice(0, separator);
92
92
  const id = key.slice(separator + 1);
@@ -131,6 +131,8 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
131
131
  let automaticStarted = false;
132
132
  let sequence = 0;
133
133
  let active: AbortController | undefined;
134
+ let widgetSequence = 0;
135
+ let widgetTimer: ReturnType<typeof setTimeout> | undefined;
134
136
 
135
137
  const isCurrent = (request: number, controller: AbortController) =>
136
138
  request === sequence && active === controller && !controller.signal.aborted;
@@ -169,27 +171,40 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
169
171
  if (isCurrent(request, controller)) active = undefined;
170
172
  };
171
173
 
172
- const rename = async (text: string, ctx: ExtensionContext, manual: boolean): Promise<void> => {
174
+ const rename = async (text: string, ctx: ExtensionContext, manual: boolean): Promise<string | undefined> => {
173
175
  const { request, controller } = begin();
174
176
  try {
175
177
  const title = await generateTitle(text, ctx, controller.signal);
176
178
  if (!isCurrent(request, controller)) return;
177
179
  pi.setSessionName(title);
178
180
  await applyHerdr(title, request, controller);
181
+ return title;
179
182
  } catch (error) {
180
183
  if (manual && isCurrent(request, controller)) {
181
184
  ctx.ui.notify(error instanceof Error ? error.message : "Rename failed.", "warning");
182
185
  }
186
+ return undefined;
183
187
  } finally {
184
188
  finish(request, controller);
185
189
  }
186
190
  };
187
191
 
188
- pi.on("session_start", (_event, ctx) => {
192
+ const clearWidget = (ctx: ExtensionContext) => {
193
+ widgetSequence++;
194
+ if (widgetTimer) clearTimeout(widgetTimer);
195
+ widgetTimer = undefined;
196
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
197
+ };
198
+
199
+ pi.on("session_start", async (_event, ctx) => {
200
+ clearWidget(ctx);
189
201
  active?.abort();
190
202
  active = undefined;
191
203
  sequence++;
192
204
  latestUserText = latestSessionUserText(ctx);
205
+ if (!(await configuredModel())) {
206
+ ctx.ui.notify("Run /rename-model to configure chat title generation.", "warning");
207
+ }
193
208
  const title = pi.getSessionName();
194
209
  automaticStarted = Boolean(title || latestUserText);
195
210
  if (!title) return;
@@ -210,7 +225,8 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
210
225
  return { action: "continue" };
211
226
  });
212
227
 
213
- pi.on("session_shutdown", () => {
228
+ pi.on("session_shutdown", (_event, ctx) => {
229
+ clearWidget(ctx);
214
230
  active?.abort();
215
231
  active = undefined;
216
232
  sequence++;
@@ -224,7 +240,26 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
224
240
  ctx.ui.notify("No user text is available to rename this chat.", "warning");
225
241
  return;
226
242
  }
227
- await rename(context, ctx, true);
243
+ if (widgetTimer) clearTimeout(widgetTimer);
244
+ widgetTimer = undefined;
245
+ const widgetRequest = ++widgetSequence;
246
+ ctx.ui.setWidget(
247
+ WIDGET_KEY,
248
+ (tui, theme) => new BorderedLoader(tui, theme, "renaming...", { cancellable: false }),
249
+ );
250
+ const title = await rename(context, ctx, true);
251
+ if (widgetRequest !== widgetSequence) return;
252
+ if (!title) {
253
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
254
+ return;
255
+ }
256
+ ctx.ui.setWidget(WIDGET_KEY, [`renamed to ${title}`]);
257
+ widgetTimer = setTimeout(() => {
258
+ if (widgetRequest !== widgetSequence) return;
259
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
260
+ widgetTimer = undefined;
261
+ }, WIDGET_RESULT_MS);
262
+ widgetTimer.unref?.();
228
263
  },
229
264
  });
230
265
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-herdr-rename",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Generate short Pi chat titles and rename the current Herdr location.",
5
5
  "keywords": [
6
6
  "pi-package",