@agnishc/edb-auto-name-session 0.4.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/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/package.json +43 -0
- package/src/index.ts +102 -0
- package/src/title.test.ts +33 -0
- package/src/title.ts +76 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0] - 2026-04-29
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Initial release: replaces Pi's default first-message session label with a generated session title
|
|
7
|
+
- Uses the `opencode/big-pickle` model to generate a concise session title after the first user message
|
|
8
|
+
- Shows interactive notifications while auto-naming runs and when the session title is set
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agnish Chakraborty
|
|
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,49 @@
|
|
|
1
|
+
# @agnishc/edb-auto-name-session
|
|
2
|
+
|
|
3
|
+
A Pi CLI extension that replaces Pi's default first-message session label with a short generated title.
|
|
4
|
+
|
|
5
|
+
By default, Pi already shows the first user message in the session picker when no explicit session name exists. This extension watches the first real user prompt in a new unnamed session, sends that prompt to `opencode/big-pickle`, and then stores a cleaner display name with `pi.setSessionName()`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install npm:@agnishc/edb-auto-name-session
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use case
|
|
14
|
+
|
|
15
|
+
Without this extension:
|
|
16
|
+
- Pi falls back to the raw first user message as the session label
|
|
17
|
+
|
|
18
|
+
With this extension:
|
|
19
|
+
- Pi keeps the same session flow
|
|
20
|
+
- after the first user message, the fallback label is replaced with a concise generated title
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
|
|
24
|
+
- First prompt: `The next extension to build is auto-name-session. This will use the opencode big pickle model.`
|
|
25
|
+
- Generated session name: `Build Auto Name Session`
|
|
26
|
+
|
|
27
|
+
## Behavior
|
|
28
|
+
|
|
29
|
+
- Runs once per fresh unnamed session
|
|
30
|
+
- Waits until the first user message is actually recorded
|
|
31
|
+
- Uses `opencode/big-pickle` to generate a concise title
|
|
32
|
+
- Shows an interactive notification while auto-naming runs, then confirms the final title
|
|
33
|
+
- Calls `pi.setSessionName()` only if the session is still unnamed
|
|
34
|
+
- Leaves already named, resumed, or forked sessions alone
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
The extension uses the `opencode` provider and the `big-pickle` model.
|
|
39
|
+
Configure OpenCode access with either:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export OPENCODE_API_KEY=...
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
or `/login` in Pi if you store provider credentials there.
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
[MIT](LICENSE) © Agnish Chakraborty
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agnishc/edb-auto-name-session",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Pi extension: replace Pi's first-message session label with a generated title",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"edb"
|
|
9
|
+
],
|
|
10
|
+
"type": "module",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Agnish Chakraborty",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/agnishcc/pi-extention-monorepo.git",
|
|
16
|
+
"directory": "packages/edb-auto-name-session"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/agnishcc/pi-extention-monorepo/tree/main/packages/edb-auto-name-session#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/agnishcc/pi-extention-monorepo/issues"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "vitest run"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"CHANGELOG.md"
|
|
33
|
+
],
|
|
34
|
+
"pi": {
|
|
35
|
+
"extensions": [
|
|
36
|
+
"./src/index.ts"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@mariozechner/pi-ai": "*",
|
|
41
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { complete } from "@mariozechner/pi-ai";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { extractUserText, sanitizeSessionName, shouldArmAutoNaming } from "./title";
|
|
4
|
+
|
|
5
|
+
const MODEL_PROVIDER = "opencode";
|
|
6
|
+
const MODEL_ID = "big-pickle";
|
|
7
|
+
|
|
8
|
+
const SYSTEM_PROMPT = `You create short session titles for coding and technical work.
|
|
9
|
+
Return exactly one title based only on the user's first message.
|
|
10
|
+
Rules:
|
|
11
|
+
- Prefer 2 to 6 words
|
|
12
|
+
- Use Title Case
|
|
13
|
+
- Mention the task, feature, bug, or file focus when clear
|
|
14
|
+
- No quotes
|
|
15
|
+
- No markdown
|
|
16
|
+
- No labels like Title:
|
|
17
|
+
- No trailing punctuation
|
|
18
|
+
- Maximum 60 characters`;
|
|
19
|
+
|
|
20
|
+
export default function autoNameSessionExtension(pi: ExtensionAPI): void {
|
|
21
|
+
let sessionToken = 0;
|
|
22
|
+
let armed = false;
|
|
23
|
+
let pending = false;
|
|
24
|
+
|
|
25
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
26
|
+
sessionToken += 1;
|
|
27
|
+
armed = shouldArmAutoNaming(ctx.sessionManager.getBranch(), pi.getSessionName());
|
|
28
|
+
pending = false;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
pi.on("session_shutdown", async () => {
|
|
32
|
+
sessionToken += 1;
|
|
33
|
+
armed = false;
|
|
34
|
+
pending = false;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
pi.on("message_end", async (event, ctx) => {
|
|
38
|
+
if (!armed || pending || pi.getSessionName()) return;
|
|
39
|
+
if (event.message.role !== "user") return;
|
|
40
|
+
|
|
41
|
+
const prompt = extractUserText(event.message.content);
|
|
42
|
+
armed = false;
|
|
43
|
+
if (!prompt) return;
|
|
44
|
+
|
|
45
|
+
pending = true;
|
|
46
|
+
const token = sessionToken;
|
|
47
|
+
|
|
48
|
+
if (ctx.hasUI) {
|
|
49
|
+
ctx.ui.notify("Auto-naming session…", "info");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const name = await generateSessionName(prompt, ctx);
|
|
54
|
+
if (!name) return;
|
|
55
|
+
if (token !== sessionToken) return;
|
|
56
|
+
if (pi.getSessionName()) return;
|
|
57
|
+
pi.setSessionName(name);
|
|
58
|
+
if (ctx.hasUI) {
|
|
59
|
+
ctx.ui.notify(`Session named: ${name}`, "info");
|
|
60
|
+
}
|
|
61
|
+
} catch (error: unknown) {
|
|
62
|
+
console.error("[edb-auto-name-session] Failed to generate session name:", error);
|
|
63
|
+
} finally {
|
|
64
|
+
if (token === sessionToken) pending = false;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function generateSessionName(prompt: string, ctx: ExtensionContext): Promise<string | undefined> {
|
|
70
|
+
const model = ctx.modelRegistry.find(MODEL_PROVIDER, MODEL_ID);
|
|
71
|
+
if (!model) return undefined;
|
|
72
|
+
|
|
73
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
74
|
+
if (!auth.ok || !auth.apiKey) return undefined;
|
|
75
|
+
|
|
76
|
+
const response = await complete(
|
|
77
|
+
model,
|
|
78
|
+
{
|
|
79
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
80
|
+
messages: [
|
|
81
|
+
{
|
|
82
|
+
role: "user",
|
|
83
|
+
content: [{ type: "text", text: prompt }],
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
apiKey: auth.apiKey,
|
|
90
|
+
headers: auth.headers,
|
|
91
|
+
maxTokens: 256,
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const text = response.content
|
|
96
|
+
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
|
97
|
+
.map((part) => part.text)
|
|
98
|
+
.join("\n")
|
|
99
|
+
.trim();
|
|
100
|
+
|
|
101
|
+
return sanitizeSessionName(text);
|
|
102
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { countUserMessages, extractUserText, sanitizeSessionName, shouldArmAutoNaming } from "./title";
|
|
3
|
+
|
|
4
|
+
describe("title helpers", () => {
|
|
5
|
+
it("extracts only text parts from user content", () => {
|
|
6
|
+
expect(
|
|
7
|
+
extractUserText([
|
|
8
|
+
{ type: "text", text: "Build an auto name session extension" },
|
|
9
|
+
{ type: "image", source: { type: "base64", data: "abc" } },
|
|
10
|
+
{ type: "text", text: "Use big pickle" },
|
|
11
|
+
]),
|
|
12
|
+
).toBe("Build an auto name session extension\nUse big pickle");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("sanitizes labels, quotes, and punctuation", () => {
|
|
16
|
+
expect(sanitizeSessionName('Title: "Auto Name Session".')).toBe("Auto Name Session");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("counts only user messages", () => {
|
|
20
|
+
const entries = [
|
|
21
|
+
{ type: "message", message: { role: "user", content: [{ type: "text", text: "one" }] } },
|
|
22
|
+
{ type: "message", message: { role: "assistant", content: [{ type: "text", text: "two" }] } },
|
|
23
|
+
{ type: "custom", customType: "x", data: {} },
|
|
24
|
+
] as any;
|
|
25
|
+
|
|
26
|
+
expect(countUserMessages(entries)).toBe(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("arms only for empty unnamed sessions", () => {
|
|
30
|
+
expect(shouldArmAutoNaming([], undefined)).toBe(true);
|
|
31
|
+
expect(shouldArmAutoNaming([], "Already Named")).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
package/src/title.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { SessionEntry } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
const MAX_PROMPT_CHARS = 4_000;
|
|
4
|
+
const MAX_TITLE_CHARS = 60;
|
|
5
|
+
|
|
6
|
+
type TextPart = {
|
|
7
|
+
type?: string;
|
|
8
|
+
text?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function shouldArmAutoNaming(entries: SessionEntry[], currentName: string | undefined): boolean {
|
|
12
|
+
return !currentName?.trim() && countUserMessages(entries) === 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function countUserMessages(entries: SessionEntry[]): number {
|
|
16
|
+
return entries.filter(isUserMessageEntry).length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function extractUserText(content: unknown): string {
|
|
20
|
+
if (typeof content === "string") return normalizePrompt(content);
|
|
21
|
+
if (!Array.isArray(content)) return "";
|
|
22
|
+
|
|
23
|
+
return normalizePrompt(
|
|
24
|
+
content
|
|
25
|
+
.filter(isTextPart)
|
|
26
|
+
.map((part) => part.text)
|
|
27
|
+
.join("\n"),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function sanitizeSessionName(value: string): string | undefined {
|
|
32
|
+
const firstLine = value
|
|
33
|
+
.replace(/^```[a-z0-9_-]*\s*/i, "")
|
|
34
|
+
.replace(/```$/g, "")
|
|
35
|
+
.split(/\r?\n/)
|
|
36
|
+
.map((line) => line.trim())
|
|
37
|
+
.find(Boolean);
|
|
38
|
+
|
|
39
|
+
if (!firstLine) return undefined;
|
|
40
|
+
|
|
41
|
+
let title = firstLine
|
|
42
|
+
.replace(/^(title|session name)\s*:\s*/i, "")
|
|
43
|
+
.replace(/^[-*]\s*/, "")
|
|
44
|
+
.replace(/[.?!:;,]+$/g, "")
|
|
45
|
+
.replace(/^['"`]+|['"`]+$/g, "")
|
|
46
|
+
.replace(/\s+/g, " ")
|
|
47
|
+
.trim();
|
|
48
|
+
|
|
49
|
+
if (!title) return undefined;
|
|
50
|
+
if (title.length <= MAX_TITLE_CHARS) return title;
|
|
51
|
+
|
|
52
|
+
title = title.slice(0, MAX_TITLE_CHARS).trimEnd();
|
|
53
|
+
const lastSpace = title.lastIndexOf(" ");
|
|
54
|
+
if (lastSpace > 20) title = title.slice(0, lastSpace);
|
|
55
|
+
return title.trim() || undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isUserMessageEntry(entry: SessionEntry): entry is SessionEntry & {
|
|
59
|
+
type: "message";
|
|
60
|
+
message: { role: "user"; content: unknown };
|
|
61
|
+
} {
|
|
62
|
+
return entry.type === "message" && entry.message.role === "user";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isTextPart(value: unknown): value is TextPart & { type: "text"; text: string } {
|
|
66
|
+
return Boolean(
|
|
67
|
+
value &&
|
|
68
|
+
typeof value === "object" &&
|
|
69
|
+
(value as TextPart).type === "text" &&
|
|
70
|
+
typeof (value as TextPart).text === "string",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizePrompt(value: string): string {
|
|
75
|
+
return value.replace(/\r\n/g, "\n").trim().slice(0, MAX_PROMPT_CHARS);
|
|
76
|
+
}
|