@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 +6 -4
- package/extensions/rename.ts +27 -11
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
package/extensions/rename.ts
CHANGED
|
@@ -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
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
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
|
|
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:
|
|
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 >
|
|
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 (
|
|
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
|
|
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();
|