my-pi 0.0.13 → 0.1.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.
@@ -0,0 +1,234 @@
1
+ // Session name — AI-powered session naming
2
+ // Adapted from Thomas Lopes' pi dotfiles
3
+
4
+ import { complete, type Message } from '@mariozechner/pi-ai';
5
+ import type {
6
+ ExtensionAPI,
7
+ SessionEntry,
8
+ } from '@mariozechner/pi-coding-agent';
9
+ import {
10
+ BorderedLoader,
11
+ convertToLlm,
12
+ serializeConversation,
13
+ } from '@mariozechner/pi-coding-agent';
14
+
15
+ const SYSTEM_PROMPT = `You are a session naming assistant. Given a conversation history, generate a short, descriptive session name (2-5 words) that captures the main topic or task.
16
+
17
+ Guidelines:
18
+ - Be concise but specific
19
+ - Use kebab-case or natural language
20
+ - Focus on the core task/question
21
+ - Avoid generic names like "discussion" or "conversation"
22
+ - No quotes, no punctuation at the end
23
+
24
+ Examples:
25
+ - "fix auth bug" -> "fix-auth-bug" or "authentication fix"
26
+ - "how do I deploy to vercel" -> "vercel deployment"
27
+ - "explain react hooks" -> "react hooks explanation"
28
+ - "optimize database queries" -> "db query optimization"
29
+
30
+ Output ONLY the session name, nothing else.`;
31
+
32
+ const AUTO_NAME_THRESHOLD = 1;
33
+ const MAX_CHARS = 4000;
34
+ const MAX_NAME_LEN = 50;
35
+
36
+ function clean_name(value: string): string {
37
+ return value
38
+ .replace(/^["']|["']$/g, '')
39
+ .replace(/\n/g, ' ')
40
+ .replace(/\s+/g, ' ')
41
+ .trim()
42
+ .slice(0, MAX_NAME_LEN);
43
+ }
44
+
45
+ function truncate_conversation(value: string): string {
46
+ return value.length > MAX_CHARS
47
+ ? value.slice(0, MAX_CHARS) + '\n...'
48
+ : value;
49
+ }
50
+
51
+ async function generate_session_name(
52
+ ctx: {
53
+ modelRegistry: {
54
+ getApiKeyAndHeaders: (
55
+ model: NonNullable<
56
+ Parameters<
57
+ Parameters<ExtensionAPI['registerCommand']>[1]['handler']
58
+ >[1]['model']
59
+ >,
60
+ ) => Promise<any>;
61
+ };
62
+ },
63
+ model: NonNullable<
64
+ Parameters<
65
+ Parameters<ExtensionAPI['registerCommand']>[1]['handler']
66
+ >[1]['model']
67
+ >,
68
+ conversation_text: string,
69
+ signal?: AbortSignal,
70
+ ): Promise<string | null> {
71
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
72
+ if (!auth.ok || !auth.apiKey) {
73
+ throw new Error(
74
+ auth.ok ? `No API key for ${model.provider}` : auth.error,
75
+ );
76
+ }
77
+
78
+ const user_message: Message = {
79
+ role: 'user',
80
+ content: [
81
+ {
82
+ type: 'text',
83
+ text: `## Conversation History\n\n${truncate_conversation(conversation_text)}\n\nGenerate a concise session name for this conversation.`,
84
+ },
85
+ ],
86
+ timestamp: Date.now(),
87
+ };
88
+
89
+ const response = await complete(
90
+ model,
91
+ { systemPrompt: SYSTEM_PROMPT, messages: [user_message] },
92
+ { apiKey: auth.apiKey, headers: auth.headers, signal },
93
+ );
94
+
95
+ if (response.stopReason === 'aborted') {
96
+ return null;
97
+ }
98
+
99
+ return clean_name(
100
+ response.content
101
+ .filter(
102
+ (c): c is { type: 'text'; text: string } => c.type === 'text',
103
+ )
104
+ .map((c) => c.text.trim())
105
+ .join(' '),
106
+ );
107
+ }
108
+
109
+ export default async function session_name(pi: ExtensionAPI) {
110
+ let auto_named_attempted = false;
111
+
112
+ pi.on('agent_end', async (_event, ctx) => {
113
+ if (!ctx.hasUI || !ctx.model) return;
114
+ if (pi.getSessionName() || auto_named_attempted) return;
115
+
116
+ const branch = ctx.sessionManager.getBranch();
117
+ const user_messages = branch.filter(
118
+ (entry): entry is SessionEntry & { type: 'message' } =>
119
+ entry.type === 'message' && entry.message.role === 'user',
120
+ );
121
+ if (user_messages.length < AUTO_NAME_THRESHOLD) return;
122
+
123
+ auto_named_attempted = true;
124
+ const messages = branch
125
+ .filter(
126
+ (entry): entry is SessionEntry & { type: 'message' } =>
127
+ entry.type === 'message',
128
+ )
129
+ .map((entry) => entry.message);
130
+ if (messages.length === 0) return;
131
+
132
+ const conversation_text = serializeConversation(
133
+ convertToLlm(messages),
134
+ );
135
+
136
+ generate_session_name(ctx, ctx.model, conversation_text)
137
+ .then((name) => {
138
+ if (!name) return;
139
+ pi.setSessionName(name);
140
+ ctx.ui.notify(`Auto-named: ${name}`, 'info');
141
+ })
142
+ .catch((err) => {
143
+ console.error('Auto-naming failed:', err);
144
+ });
145
+ });
146
+
147
+ pi.on('session_start', async () => {
148
+ auto_named_attempted = false;
149
+ });
150
+
151
+ pi.registerCommand('session-name', {
152
+ description:
153
+ 'Set, show, or auto-generate the current session name',
154
+ handler: async (args, ctx) => {
155
+ const trimmed = args.trim();
156
+
157
+ if (!trimmed) {
158
+ const current = pi.getSessionName();
159
+ ctx.ui.notify(
160
+ current ? `Session: ${current}` : 'No session name set',
161
+ 'info',
162
+ );
163
+ return;
164
+ }
165
+
166
+ if (trimmed === '--auto' || trimmed === '-a') {
167
+ if (!ctx.hasUI || !ctx.model) {
168
+ ctx.ui.notify(
169
+ 'Auto-naming requires interactive mode and a selected model',
170
+ 'error',
171
+ );
172
+ return;
173
+ }
174
+
175
+ const branch = ctx.sessionManager.getBranch();
176
+ const messages = branch
177
+ .filter(
178
+ (entry): entry is SessionEntry & { type: 'message' } =>
179
+ entry.type === 'message',
180
+ )
181
+ .map((entry) => entry.message);
182
+ if (messages.length === 0) {
183
+ ctx.ui.notify('No conversation to analyze', 'error');
184
+ return;
185
+ }
186
+
187
+ const conversation_text = serializeConversation(
188
+ convertToLlm(messages),
189
+ );
190
+
191
+ const result = await ctx.ui.custom<string | null>(
192
+ (tui, theme, _kb, done) => {
193
+ const loader = new BorderedLoader(
194
+ tui,
195
+ theme,
196
+ 'Generating session name...',
197
+ );
198
+ loader.onAbort = () => done(null);
199
+
200
+ generate_session_name(
201
+ ctx,
202
+ ctx.model!,
203
+ conversation_text,
204
+ loader.signal,
205
+ )
206
+ .then(done)
207
+ .catch((err) => {
208
+ console.error('Auto-naming failed:', err);
209
+ done(null);
210
+ });
211
+
212
+ return loader;
213
+ },
214
+ );
215
+
216
+ if (result === null) {
217
+ ctx.ui.notify('Auto-naming cancelled', 'info');
218
+ return;
219
+ }
220
+ if (!result) {
221
+ ctx.ui.notify('Failed to generate name', 'error');
222
+ return;
223
+ }
224
+
225
+ pi.setSessionName(result);
226
+ ctx.ui.notify(`Session named: ${result}`, 'info');
227
+ return;
228
+ }
229
+
230
+ pi.setSessionName(clean_name(trimmed));
231
+ ctx.ui.notify(`Session named: ${clean_name(trimmed)}`, 'info');
232
+ },
233
+ });
234
+ }