@henryqw/pi-herdr-rename 5.0.3 → 5.0.5
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 +3 -3
- package/extensions/rename.ts +48 -19
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ In a new, untitled session, the first real user prompt generates a title in the
|
|
|
35
35
|
|
|
36
36
|
### Title and branch rules
|
|
37
37
|
|
|
38
|
-
A semantic branch is a Git-safe branch name made from a task
|
|
38
|
+
A semantic branch is a Git-safe branch name made from a task kind and the display-title words. The model replies with one JSON object holding `kind` and `subject`; that classification stays internal. For example, `{"kind":"refactor","subject":"update task logic"}` displays as `Update task logic` and maps to `refactor/update-task-logic`. A reply that does not fit the shape is sent back once with the rejection before the route gives up on it, so a valid title still comes from the first route.
|
|
39
39
|
|
|
40
40
|
- In a linked worktree, a detached checkout or Herdr `worktree/...` branch is renamed. An existing non-generated branch stays.
|
|
41
41
|
- A linked worktree's generated workspace label, such as `worktree-brave-meadow-4aa8`, becomes the display title automatically. A manual `/rename` can also replace a custom workspace name.
|
|
@@ -46,7 +46,7 @@ A semantic branch is a Git-safe branch name made from a task type and the displa
|
|
|
46
46
|
|
|
47
47
|
The task `pi-herdr-rename/rename` uses `fast` only when it has no explicit assignment. To use another configured profile, assign the task to that profile in `/task-models`; the shared [`pi-task-models` config](https://pi.henry.wang/extensions/pi-task-models#config) stores the assignment under `tasks` at `~/.pi/agent/config/pi-task-models/config.json`.
|
|
48
48
|
|
|
49
|
-
The extension tries the assigned profile's primary route, then its fallback, while honoring the configured thinking level. It never substitutes the current session model.
|
|
49
|
+
The extension tries the assigned profile's primary route, then its fallback, while honoring the configured thinking level. Within one route a reply that fails the title shape is retried once with the rejected text; only a second failure moves on to the fallback. It never substitutes the current session model.
|
|
50
50
|
|
|
51
51
|
## State and storage
|
|
52
52
|
|
|
@@ -54,7 +54,7 @@ The Pi session stores each generated display title and semantic branch in a `pi-
|
|
|
54
54
|
|
|
55
55
|
## Limits and recovery
|
|
56
56
|
|
|
57
|
-
Display titles are natural task phrases, preferably
|
|
57
|
+
Display titles are natural task phrases, preferably two or three words, and always at most four words and 20 characters. Models that answer in another language still produce English titles.
|
|
58
58
|
|
|
59
59
|
A missing shared task-model config warns once at session start. Run `/task-models` to configure rename routing. No viable route leaves titles unchanged. `/rename` warns if the session has no user text to rename.
|
|
60
60
|
|
package/extensions/rename.ts
CHANGED
|
@@ -22,6 +22,9 @@ const MAX_CONTEXT_MESSAGES = 5;
|
|
|
22
22
|
const DISPLAY_MAX_WORDS = 4;
|
|
23
23
|
const DISPLAY_MAX_CHARS = 20;
|
|
24
24
|
const SEMANTIC_TYPE_MAX_CHARS = 12;
|
|
25
|
+
const TITLE_SHAPE = `{"kind":"<one lowercase English word, at most ${SEMANTIC_TYPE_MAX_CHARS} letters>","subject":"<2 or 3 lowercase English words, letters and digits only, single spaces, at most ${DISPLAY_MAX_CHARS} characters>"}`;
|
|
26
|
+
const TITLE_PROMPT = `You label a coding session with one short English title. Reply with one JSON object and nothing else: ${TITLE_SHAPE}. Use English even when the user writes in another language. Example reply: {"kind":"fix","subject":"login crash"}`;
|
|
27
|
+
const TITLE_REPAIR_PROMPT = `Your previous reply was rejected. Reply only with one JSON object shaped ${TITLE_SHAPE}, where kind is one lowercase English word of at most ${SEMANTIC_TYPE_MAX_CHARS} letters and subject is 2 or 3 lowercase English words of letters and digits with single spaces, at most ${DISPLAY_MAX_CHARS} characters.`;
|
|
25
28
|
export const RENAME_TASK = {
|
|
26
29
|
id: "pi-herdr-rename/rename",
|
|
27
30
|
label: "Conversation rename",
|
|
@@ -63,16 +66,35 @@ function isDisplayTitle(value: unknown): value is string {
|
|
|
63
66
|
return validSubject(subject) && value === subject[0].toUpperCase() + subject.slice(1);
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
function
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
function shapedTitle(kind: string, subject: string): GeneratedTitle | undefined {
|
|
70
|
+
const normalizedKind = kind.trim().toLowerCase();
|
|
71
|
+
const normalizedSubject = subject.trim().toLowerCase().replace(/\s+/g, " ");
|
|
72
|
+
if (!/^[a-z][a-z0-9-]*$/.test(normalizedKind) || normalizedKind.length > SEMANTIC_TYPE_MAX_CHARS) return undefined;
|
|
73
|
+
if (!validSubject(normalizedSubject)) return undefined;
|
|
70
74
|
return {
|
|
71
|
-
display:
|
|
72
|
-
branch: `${
|
|
75
|
+
display: normalizedSubject[0].toUpperCase() + normalizedSubject.slice(1),
|
|
76
|
+
branch: `${normalizedKind}/${normalizedSubject.replaceAll(" ", "-")}`,
|
|
73
77
|
};
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
function parseGeneratedTitle(response: string): GeneratedTitle | undefined {
|
|
81
|
+
for (const candidate of response.match(/\{[^{}]*\}/gs) ?? []) {
|
|
82
|
+
let value: unknown;
|
|
83
|
+
try {
|
|
84
|
+
value = JSON.parse(candidate);
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
89
|
+
const { kind, subject } = value as { kind?: unknown; subject?: unknown };
|
|
90
|
+
if (typeof kind !== "string" || typeof subject !== "string") continue;
|
|
91
|
+
const parsed = shapedTitle(kind, subject);
|
|
92
|
+
if (parsed) return parsed;
|
|
93
|
+
}
|
|
94
|
+
const legacy = /^([a-z][a-z0-9-]*): (.+)$/.exec(response.trim().toLowerCase().replace(/\s+/g, " "));
|
|
95
|
+
return legacy ? shapedTitle(legacy[1], legacy[2]) : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
76
98
|
function savedTitle(ctx: ExtensionContext): GeneratedTitle | undefined {
|
|
77
99
|
const entry = [...ctx.sessionManager.getBranch()]
|
|
78
100
|
.reverse()
|
|
@@ -151,14 +173,11 @@ function recentUserMessages(ctx: ExtensionContext, fallback?: string): string |
|
|
|
151
173
|
}
|
|
152
174
|
|
|
153
175
|
async function generateTitle(text: string, ctx: ExtensionContext, signal: AbortSignal): Promise<GeneratedTitle> {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
messages: [{ role: "user" as const, content: text.slice(0, MAX_CONTEXT_CHARS), timestamp: Date.now() }],
|
|
157
|
-
};
|
|
158
|
-
const complete = async (route: ResolvedTaskRoute) => {
|
|
176
|
+
const prompt = text.slice(0, MAX_CONTEXT_CHARS);
|
|
177
|
+
const complete = async (route: ResolvedTaskRoute, messages: Array<{ role: "user"; content: string; timestamp: number }>) => {
|
|
159
178
|
let response;
|
|
160
179
|
try {
|
|
161
|
-
response = await ctx.modelRegistry.streamSimple(route.model,
|
|
180
|
+
response = await ctx.modelRegistry.streamSimple(route.model, { systemPrompt: TITLE_PROMPT, messages }, {
|
|
162
181
|
signal,
|
|
163
182
|
maxRetries: 0,
|
|
164
183
|
...(route.thinkingLevel === "off" ? {} : { reasoning: route.thinkingLevel }),
|
|
@@ -175,17 +194,27 @@ async function generateTitle(text: string, ctx: ExtensionContext, signal: AbortS
|
|
|
175
194
|
return executeTaskRoutes(
|
|
176
195
|
configuredRenameRoutes(ctx),
|
|
177
196
|
async (route) => {
|
|
178
|
-
const
|
|
197
|
+
const firstMessages = [{ role: "user" as const, content: prompt, timestamp: Date.now() }];
|
|
198
|
+
const response = await complete(route, firstMessages);
|
|
179
199
|
const title = response.content
|
|
180
200
|
.filter((part) => part.type === "text")
|
|
181
201
|
.map((part) => part.text)
|
|
182
|
-
.join(" ")
|
|
183
|
-
.trim()
|
|
184
|
-
.toLowerCase()
|
|
185
|
-
.replace(/\s+/g, " ");
|
|
202
|
+
.join(" ");
|
|
186
203
|
const generated = parseGeneratedTitle(title);
|
|
187
|
-
if (
|
|
188
|
-
|
|
204
|
+
if (generated) return generated;
|
|
205
|
+
|
|
206
|
+
signal.throwIfAborted();
|
|
207
|
+
const repaired = await complete(route, [
|
|
208
|
+
...firstMessages,
|
|
209
|
+
{ role: "user", content: `${JSON.stringify(title.trim().slice(0, MAX_MESSAGE_CHARS))} ${TITLE_REPAIR_PROMPT}`, timestamp: Date.now() },
|
|
210
|
+
]);
|
|
211
|
+
const repairedTitle = repaired.content
|
|
212
|
+
.filter((part) => part.type === "text")
|
|
213
|
+
.map((part) => part.text)
|
|
214
|
+
.join(" ");
|
|
215
|
+
const repairedGenerated = parseGeneratedTitle(repairedTitle);
|
|
216
|
+
if (!repairedGenerated) throw new RenameModelError("Rename task model returned an invalid title.");
|
|
217
|
+
return repairedGenerated;
|
|
189
218
|
},
|
|
190
219
|
{
|
|
191
220
|
signal,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-herdr-rename",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.5",
|
|
4
4
|
"description": "Generate short Pi display titles and rename the current Herdr location.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"pack:check": "npm pack --dry-run"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@earendil-works/pi-coding-agent": ">=0.86.0 <0.
|
|
28
|
+
"@earendil-works/pi-coding-agent": ">=0.86.0 <0.100.0"
|
|
29
29
|
},
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|
|
@@ -48,6 +48,6 @@
|
|
|
48
48
|
"@henryqw/pi-task-models": "^7.0.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
51
|
+
"@earendil-works/pi-coding-agent": "0.99.1"
|
|
52
52
|
}
|
|
53
53
|
}
|