@fyeeme/pi-session-name 1.0.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/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/index.ts +292 -0
- package/package.json +28 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.1] - 2025-07-15
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Race condition where auto-generated title could overwrite manual rename when `generateTitle` async call completes after user renamed the session
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `/rename [name]` command: manually rename the current session on demand. With
|
|
19
|
+
a name argument it sets that name; with no argument it auto-generates one from
|
|
20
|
+
the conversation. Invoking it locks out background auto-naming (manual
|
|
21
|
+
control), consistent with `/name`.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Title-generation prompt now favors descriptive titles (key entity + action +
|
|
26
|
+
goal, ~15-40 characters) over terse labels. Previously the prompt emphasized
|
|
27
|
+
"short/concise", which produced overly brief session names.
|
|
28
|
+
|
|
29
|
+
## [1.0.0] - 2026-07-20
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Initial release
|
|
34
|
+
- Auto-name pi sessions with a short LLM-generated title on first `agent_settled`
|
|
35
|
+
- `first` mode (default): name once, never overwrite
|
|
36
|
+
- `auto` mode: re-evaluate each turn, rename when the topic drifts (LLM returns `KEEP` or a new title)
|
|
37
|
+
- Never overwrites manual names (`/name`, `--name`, RPC, other extensions)
|
|
38
|
+
- Title follows the language of the user's first message
|
|
39
|
+
- Config via `.pi/session-name.json` and `PI_SESSION_NAME_*` env vars
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fyeeme
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# pi-session-name
|
|
2
|
+
|
|
3
|
+
Auto-name your [pi](https://pi.dev) sessions with a short, LLM-generated title so
|
|
4
|
+
`pi --resume` lists are easy to scan — instead of the raw first message.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# Global (user) install
|
|
10
|
+
pi install npm:@fyeeme/pi-session-name
|
|
11
|
+
|
|
12
|
+
# Project-local (.pi/settings.json)
|
|
13
|
+
pi install -l npm:@fyeeme/pi-session-name
|
|
14
|
+
|
|
15
|
+
# Try once without saving
|
|
16
|
+
pi -e npm:@fyeeme/pi-session-name
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires pi `>= 0.80.0` (uses the `agent_settled` / `session_info_changed` events).
|
|
20
|
+
|
|
21
|
+
## Behavior
|
|
22
|
+
|
|
23
|
+
- When the first agent run of a session **settles**, the extension asks the model
|
|
24
|
+
for a descriptive title (same language as your first message) — key entity +
|
|
25
|
+
action + goal, not a terse label — and writes it via `setSessionName`. It then
|
|
26
|
+
shows up in the resume picker immediately.
|
|
27
|
+
- **Never overwrites a name you set manually** (`/name`, `--name`, the picker's
|
|
28
|
+
rename, or any other extension). Once it detects an external rename it locks
|
|
29
|
+
itself for the rest of the session.
|
|
30
|
+
- Two modes (config `mode`):
|
|
31
|
+
- `first` (default) — name once, then leave it alone.
|
|
32
|
+
- `auto` — re-evaluate each turn; the model replies `KEEP` or a new title, so
|
|
33
|
+
the name tracks the current topic.
|
|
34
|
+
- **`/rename [name]`** — manually rename the current session on demand. With a
|
|
35
|
+
name argument it sets that name; with no argument it generates one from the
|
|
36
|
+
conversation (same descriptive prompt as auto-naming). Invoking `/rename` is a
|
|
37
|
+
manual action, so it locks out background auto-naming for the rest of the
|
|
38
|
+
session — you've taken control.
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
Create `.pi/session-name.json` in your project root:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{ "mode": "auto", "maxLength": 200, "model": { "provider": "openai", "id": "gpt-4o-mini" } }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Option | Default | Env override | Notes |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| `mode` | `"first"` | `PI_SESSION_NAME_MODE` | `first` \| `auto` |
|
|
51
|
+
| `maxLength` | `200` | `PI_SESSION_NAME_MAX_LENGTH` | Title character cap |
|
|
52
|
+
| `enabled` | `true` | `PI_SESSION_NAME_ENABLED=false` | Master switch |
|
|
53
|
+
| `model` | current session model | `PI_SESSION_NAME_MODEL_PROVIDER` + `PI_SESSION_NAME_MODEL_ID` | Override the model used to generate titles |
|
|
54
|
+
|
|
55
|
+
By default the extension uses the model you're already chatting with (`ctx.model`),
|
|
56
|
+
so no extra API key is needed.
|
|
57
|
+
|
|
58
|
+
## Failure handling
|
|
59
|
+
|
|
60
|
+
If the model is unavailable, has no API key, or returns garbage, the extension
|
|
61
|
+
stays silent — your session is never blocked. It retries on the next turn while
|
|
62
|
+
the session is still unnamed.
|
|
63
|
+
|
|
64
|
+
## Manual smoke test
|
|
65
|
+
|
|
66
|
+
1. `pi -e ./packages/extensions/pi-session-name/index.ts`, ask a question in a
|
|
67
|
+
fresh session, wait for the reply → the resume picker shows a generated title.
|
|
68
|
+
2. `/name foo`, chat a few turns → name stays `foo` (locked).
|
|
69
|
+
3. `mode: "auto"`: shift topic across turns → name updates; same topic → stays.
|
|
70
|
+
4. `/rename foo` → name becomes `foo` and stays (locked). `/rename` with no
|
|
71
|
+
argument → generates a fresh name from the conversation.
|
|
72
|
+
5. Unset the model's API key → no errors, session runs normally.
|
package/index.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
5
|
+
import { complete } from "@earendil-works/pi-ai/compat";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Types & helpers for buildConversationText
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
type ContentBlock = { type?: string; text?: string };
|
|
12
|
+
export type SessionEntry = { type: string; message?: { role?: string; content?: unknown } };
|
|
13
|
+
|
|
14
|
+
const extractText = (content: unknown): string[] => {
|
|
15
|
+
if (typeof content === "string") return [content];
|
|
16
|
+
if (!Array.isArray(content)) return [];
|
|
17
|
+
const parts: string[] = [];
|
|
18
|
+
for (const part of content) {
|
|
19
|
+
if (part && typeof part === "object") {
|
|
20
|
+
const b = part as ContentBlock;
|
|
21
|
+
if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return parts;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const truncate = (s: string, max: number): string => (s.length <= max ? s : `${s.slice(0, max)}\u2026`);
|
|
28
|
+
|
|
29
|
+
export function buildConversationText(
|
|
30
|
+
entries: SessionEntry[],
|
|
31
|
+
opts: { maxMessages?: number; maxCharsPerMessage?: number } = {},
|
|
32
|
+
): string {
|
|
33
|
+
const maxMessages = opts.maxMessages ?? 8;
|
|
34
|
+
const maxCharsPerMessage = opts.maxCharsPerMessage ?? 600;
|
|
35
|
+
const msgs = entries
|
|
36
|
+
.filter((e) => e.type === "message" && (e.message?.role === "user" || e.message?.role === "assistant"))
|
|
37
|
+
.map((e) => ({
|
|
38
|
+
role: e.message!.role as "user" | "assistant",
|
|
39
|
+
text: extractText(e.message!.content).join("\n").trim(),
|
|
40
|
+
}))
|
|
41
|
+
.filter((m) => m.text.length > 0);
|
|
42
|
+
if (msgs.length === 0) return "";
|
|
43
|
+
const selected = msgs.length > maxMessages ? [msgs[0], ...msgs.slice(-(maxMessages - 1))] : msgs;
|
|
44
|
+
return selected
|
|
45
|
+
.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${truncate(m.text, maxCharsPerMessage)}`)
|
|
46
|
+
.join("\n\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// cleanTitle — strip wrapping quotes/brackets, collapse whitespace, truncate
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
export function cleanTitle(raw: string, maxLength: number = 200): string | null {
|
|
54
|
+
if (!raw) return null;
|
|
55
|
+
let t = raw.trim();
|
|
56
|
+
t = t.replace(/^["'`\u300C\u300E\uFF08(\[]+|["'`\u300D\u300F\uFF09)\].]+$/g, "").trim();
|
|
57
|
+
t = t.replace(/\s+/g, " ");
|
|
58
|
+
t = t.replace(/[.\u3002!\uFF01?\uFF1F]+$/g, "");
|
|
59
|
+
if (!t) return null;
|
|
60
|
+
if (t.length > maxLength) t = t.slice(0, maxLength).trim();
|
|
61
|
+
return t.length > 0 ? t : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Prompt builders — first-title & auto-rename prompts
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
export function buildFirstPrompt(
|
|
69
|
+
conversationText: string,
|
|
70
|
+
opts: { maxLength?: number } = {},
|
|
71
|
+
): string {
|
|
72
|
+
const maxLength = opts.maxLength ?? 200;
|
|
73
|
+
return [
|
|
74
|
+
"You generate a descriptive title for this conversation so the user can find it later in a session list.",
|
|
75
|
+
"Rules:",
|
|
76
|
+
'- Output ONLY the title text. No quotes, no trailing punctuation, no explanation.',
|
|
77
|
+
"- Use the SAME language as the user's first message.",
|
|
78
|
+
'- Be descriptive, not terse: include the key entity (class, component, or concept), the action, and the goal — not a vague category.',
|
|
79
|
+
`- Aim for roughly 15-40 characters; never exceed ${maxLength} characters.`,
|
|
80
|
+
"",
|
|
81
|
+
"<conversation>",
|
|
82
|
+
conversationText,
|
|
83
|
+
"</conversation>",
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildAutoPrompt(
|
|
88
|
+
currentName: string,
|
|
89
|
+
conversationText: string,
|
|
90
|
+
opts: { maxLength?: number } = {},
|
|
91
|
+
): string {
|
|
92
|
+
const maxLength = opts.maxLength ?? 200;
|
|
93
|
+
return [
|
|
94
|
+
"You decide whether the session title still matches the conversation.",
|
|
95
|
+
`- Current title: ${currentName}`,
|
|
96
|
+
"If the title is still accurate, reply with exactly: KEEP",
|
|
97
|
+
"If it is inaccurate or too vague now, output a NEW descriptive title.",
|
|
98
|
+
"Rules for a new title:",
|
|
99
|
+
'- ONLY the title text. No quotes, no trailing punctuation, no explanation.',
|
|
100
|
+
"- Use the SAME language as the user's first message.",
|
|
101
|
+
'- Be descriptive, not terse: include the key entity (class, component, or concept), the action, and the goal — not a vague category.',
|
|
102
|
+
`- Aim for roughly 15-40 characters; never exceed ${maxLength} characters.`,
|
|
103
|
+
"",
|
|
104
|
+
"<conversation>",
|
|
105
|
+
conversationText,
|
|
106
|
+
"</conversation>",
|
|
107
|
+
].join("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// SessionNameConfig + loadConfig
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
export interface SessionNameConfig {
|
|
115
|
+
mode: "first" | "auto";
|
|
116
|
+
enabled: boolean;
|
|
117
|
+
maxLength: number;
|
|
118
|
+
model?: { provider: string; id: string };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const DEFAULT_CONFIG: SessionNameConfig = { mode: "first", enabled: true, maxLength: 200 };
|
|
122
|
+
|
|
123
|
+
export function loadConfig(cwd: string, env: Record<string, string | undefined> = process.env): SessionNameConfig {
|
|
124
|
+
let fileCfg: Partial<SessionNameConfig> = {};
|
|
125
|
+
const file = join(cwd, ".pi", "session-name.json");
|
|
126
|
+
try {
|
|
127
|
+
fileCfg = JSON.parse(readFileSync(file, "utf8")) as Partial<SessionNameConfig>;
|
|
128
|
+
} catch {
|
|
129
|
+
// missing or malformed config — ignore, fall back to defaults
|
|
130
|
+
}
|
|
131
|
+
const cfg: SessionNameConfig = { ...DEFAULT_CONFIG, ...fileCfg };
|
|
132
|
+
if (env.PI_SESSION_NAME_MODE === "first" || env.PI_SESSION_NAME_MODE === "auto") cfg.mode = env.PI_SESSION_NAME_MODE;
|
|
133
|
+
if (env.PI_SESSION_NAME_ENABLED === "false") cfg.enabled = false;
|
|
134
|
+
if (env.PI_SESSION_NAME_MAX_LENGTH) {
|
|
135
|
+
const n = Number(env.PI_SESSION_NAME_MAX_LENGTH);
|
|
136
|
+
if (Number.isFinite(n) && n > 0) cfg.maxLength = n;
|
|
137
|
+
}
|
|
138
|
+
const provider = env.PI_SESSION_NAME_MODEL_PROVIDER;
|
|
139
|
+
const id = env.PI_SESSION_NAME_MODEL_ID;
|
|
140
|
+
if (provider && id) cfg.model = { provider, id };
|
|
141
|
+
return cfg;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// ModelAuth + resolveModelAndAuth
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
export type ModelAuth = { model: Model<any>; apiKey: string; headers: Record<string, string> | undefined };
|
|
149
|
+
|
|
150
|
+
export async function resolveModelAndAuth(
|
|
151
|
+
ctx: ExtensionContext,
|
|
152
|
+
cfg: SessionNameConfig,
|
|
153
|
+
getModelFn?: (provider: string, id: string) => Model<any> | undefined,
|
|
154
|
+
): Promise<ModelAuth | null> {
|
|
155
|
+
const find = getModelFn ?? ((p: string, i: string) => ctx.modelRegistry.find(p, i) as Model<any> | undefined);
|
|
156
|
+
const model = cfg.model ? find(cfg.model.provider, cfg.model.id) : ctx.model;
|
|
157
|
+
if (!model) return null;
|
|
158
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
159
|
+
if (!auth?.ok || !auth.apiKey) return null;
|
|
160
|
+
return { model, apiKey: auth.apiKey, headers: auth.headers };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// generateTitle — call LLM to produce a short title
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
export async function generateTitle(
|
|
168
|
+
prompt: string,
|
|
169
|
+
auth: ModelAuth,
|
|
170
|
+
completeFn: typeof complete = complete,
|
|
171
|
+
): Promise<string> {
|
|
172
|
+
const response = await completeFn(
|
|
173
|
+
auth.model,
|
|
174
|
+
{
|
|
175
|
+
messages: [
|
|
176
|
+
{ role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() },
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
{ apiKey: auth.apiKey, headers: auth.headers },
|
|
180
|
+
);
|
|
181
|
+
return response.content
|
|
182
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
183
|
+
.map((c) => c.text)
|
|
184
|
+
.join("\n")
|
|
185
|
+
.trim();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Pi extension entry point
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
export default function (pi: ExtensionAPI): void {
|
|
193
|
+
let manuallyLocked = false;
|
|
194
|
+
let inFlight = false;
|
|
195
|
+
let lastAutoName: string | undefined;
|
|
196
|
+
let cfg: SessionNameConfig | null = null;
|
|
197
|
+
|
|
198
|
+
pi.on("session_start", () => {
|
|
199
|
+
inFlight = false;
|
|
200
|
+
lastAutoName = undefined;
|
|
201
|
+
cfg = null;
|
|
202
|
+
manuallyLocked = !!pi.getSessionName();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
pi.on("session_info_changed", (event) => {
|
|
206
|
+
if (event.name === lastAutoName) return;
|
|
207
|
+
manuallyLocked = true;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
211
|
+
if (cfg === null) cfg = loadConfig(ctx.cwd);
|
|
212
|
+
if (!cfg.enabled || manuallyLocked || inFlight) return;
|
|
213
|
+
|
|
214
|
+
const currentName = pi.getSessionName();
|
|
215
|
+
if (cfg.mode === "first" && (lastAutoName !== undefined || currentName)) return;
|
|
216
|
+
|
|
217
|
+
inFlight = true;
|
|
218
|
+
try {
|
|
219
|
+
const auth = await resolveModelAndAuth(ctx, cfg);
|
|
220
|
+
if (!auth) return;
|
|
221
|
+
|
|
222
|
+
const text = buildConversationText(ctx.sessionManager.getBranch());
|
|
223
|
+
if (!text.trim()) return;
|
|
224
|
+
|
|
225
|
+
let title: string | null;
|
|
226
|
+
if (cfg.mode === "first" || !currentName) {
|
|
227
|
+
title = cleanTitle(await generateTitle(buildFirstPrompt(text, cfg), auth), cfg.maxLength);
|
|
228
|
+
} else {
|
|
229
|
+
const verdict = await generateTitle(buildAutoPrompt(currentName, text, cfg), auth);
|
|
230
|
+
title = /^keep$/i.test(verdict.trim()) ? null : cleanTitle(verdict, cfg.maxLength);
|
|
231
|
+
}
|
|
232
|
+
if (!title) return;
|
|
233
|
+
|
|
234
|
+
// Re-check after async gap: manual rename or external setSessionName
|
|
235
|
+
// may have fired while generateTitle was in flight.
|
|
236
|
+
if (manuallyLocked || pi.getSessionName() !== currentName) return;
|
|
237
|
+
|
|
238
|
+
lastAutoName = title;
|
|
239
|
+
pi.setSessionName(title);
|
|
240
|
+
} catch {
|
|
241
|
+
// silent: failure does not block the session; first mode retries next round, auto mode skips this round
|
|
242
|
+
} finally {
|
|
243
|
+
inFlight = false;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
pi.registerCommand("rename", {
|
|
248
|
+
description: "Rename this session. Pass a name, or leave empty to auto-generate one from the conversation.",
|
|
249
|
+
handler: async (args, ctx) => {
|
|
250
|
+
// /rename is a manual action: take control and stop background auto-naming
|
|
251
|
+
manuallyLocked = true;
|
|
252
|
+
const cfg = loadConfig(ctx.cwd);
|
|
253
|
+
const name = args.trim();
|
|
254
|
+
|
|
255
|
+
if (name) {
|
|
256
|
+
const cleaned = cleanTitle(name, cfg.maxLength);
|
|
257
|
+
if (!cleaned) {
|
|
258
|
+
ctx.ui.notify("Invalid name", "warning");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
lastAutoName = cleaned;
|
|
262
|
+
pi.setSessionName(cleaned);
|
|
263
|
+
ctx.ui.notify(`Renamed to: ${cleaned}`, "info");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// no argument → generate a name from the conversation
|
|
268
|
+
const auth = await resolveModelAndAuth(ctx, cfg);
|
|
269
|
+
if (!auth) {
|
|
270
|
+
ctx.ui.notify("Cannot generate a name: model unavailable or no API key", "warning");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const text = buildConversationText(ctx.sessionManager.getBranch());
|
|
274
|
+
if (!text.trim()) {
|
|
275
|
+
ctx.ui.notify("No conversation to generate a name from yet", "warning");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const title = cleanTitle(await generateTitle(buildFirstPrompt(text, cfg), auth), cfg.maxLength);
|
|
280
|
+
if (!title) {
|
|
281
|
+
ctx.ui.notify("Could not generate a name from the model response", "warning");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
lastAutoName = title;
|
|
285
|
+
pi.setSessionName(title);
|
|
286
|
+
ctx.ui.notify(`Renamed to: ${title}`, "info");
|
|
287
|
+
} catch {
|
|
288
|
+
ctx.ui.notify("Failed to generate a name", "error");
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fyeeme/pi-session-name",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Auto-name pi sessions with a short LLM-generated title so --resume lists are easy to scan. Supports first (once) and auto (content-aware rename) modes; never overwrites manual names.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "fyeeme",
|
|
8
|
+
"publishConfig": { "access": "public" },
|
|
9
|
+
"repository": { "type": "git", "url": "https://github.com/fyeeme/pi-packages" },
|
|
10
|
+
"bugs": { "url": "https://github.com/fyeeme/pi-packages/issues" },
|
|
11
|
+
"homepage": "https://github.com/fyeeme/pi-packages#readme",
|
|
12
|
+
"engines": { "node": ">=18" },
|
|
13
|
+
"keywords": ["pi-package", "pi", "session", "auto-name", "rename", "resume"],
|
|
14
|
+
"files": ["index.ts", "README.md", "LICENSE", "CHANGELOG.md"],
|
|
15
|
+
"pi": { "extensions": ["./index.ts"] },
|
|
16
|
+
"scripts": { "test": "vitest --run", "typecheck": "tsc" },
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@earendil-works/pi-coding-agent": ">=0.80.0",
|
|
19
|
+
"@earendil-works/pi-ai": ">=0.80.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@earendil-works/pi-coding-agent": "0.80.10",
|
|
23
|
+
"@earendil-works/pi-ai": "0.80.10",
|
|
24
|
+
"@types/node": "22.19.19",
|
|
25
|
+
"typescript": "5.9.3",
|
|
26
|
+
"vitest": "3.2.7"
|
|
27
|
+
}
|
|
28
|
+
}
|