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.
@@ -1,4 +1,7 @@
1
- import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ } from '@mariozechner/pi-coding-agent';
2
5
  import {
3
6
  Container,
4
7
  SettingsList,
@@ -136,6 +139,113 @@ export function create_extensions_extension(
136
139
  options.force_disabled,
137
140
  );
138
141
 
142
+ async function show_manager(
143
+ ctx: ExtensionCommandContext,
144
+ ): Promise<boolean> {
145
+ if (!ctx.hasUI) return false;
146
+
147
+ const states = resolve_builtin_extension_states(force_disabled);
148
+ const initial_enabled = new Set(
149
+ states
150
+ .filter((state) => state.saved_enabled)
151
+ .map((state) => state.key),
152
+ );
153
+ const current_enabled = new Set(initial_enabled);
154
+
155
+ await ctx.ui.custom((tui, theme, _kb, done) => {
156
+ const items = states.map(to_setting_item);
157
+ const container = new Container();
158
+
159
+ container.addChild({
160
+ render: () => {
161
+ const saved_enabled = current_enabled.size;
162
+ const saved_disabled = states.length - saved_enabled;
163
+ const enabled_now = [...current_enabled].filter(
164
+ (key) => !force_disabled.has(key as BuiltinExtensionKey),
165
+ ).length;
166
+ const disabled_now = states.length - enabled_now;
167
+ return [
168
+ theme.fg('accent', theme.bold('Built-in extensions')),
169
+ theme.fg(
170
+ 'muted',
171
+ `${saved_enabled} saved enabled • ${saved_disabled} saved disabled • ${enabled_now} enabled now • ${disabled_now} disabled now`,
172
+ ),
173
+ '',
174
+ ];
175
+ },
176
+ invalidate: () => {},
177
+ });
178
+
179
+ const settings_list = new SettingsList(
180
+ items,
181
+ Math.min(Math.max(items.length + 4, 8), 16),
182
+ {
183
+ cursor: theme.fg('accent', '›'),
184
+ label: (text, selected) =>
185
+ selected ? theme.fg('accent', text) : text,
186
+ value: (text, selected) => {
187
+ const color = text === ENABLED ? 'success' : 'dim';
188
+ const rendered = theme.fg(color, text);
189
+ return selected
190
+ ? theme.bold(theme.fg('accent', rendered))
191
+ : rendered;
192
+ },
193
+ description: (text) => theme.fg('muted', text),
194
+ hint: (text) => theme.fg('dim', text),
195
+ },
196
+ (id, new_value) => {
197
+ const key = id as BuiltinExtensionKey;
198
+ const enabled = new_value === ENABLED;
199
+ if (enabled) {
200
+ current_enabled.add(key);
201
+ } else {
202
+ current_enabled.delete(key);
203
+ }
204
+ save_extension_enabled(key, enabled);
205
+ },
206
+ () => done(undefined),
207
+ { enableSearch: true },
208
+ );
209
+
210
+ container.addChild(settings_list);
211
+ container.addChild(
212
+ new Text(
213
+ theme.fg(
214
+ 'dim',
215
+ 'esc close • search filters • changes save immediately • CLI --no-* flags still win in this process',
216
+ ),
217
+ 0,
218
+ 1,
219
+ ),
220
+ );
221
+
222
+ return {
223
+ render(width: number) {
224
+ return container.render(width);
225
+ },
226
+ invalidate() {
227
+ container.invalidate();
228
+ },
229
+ handleInput(data: string) {
230
+ settings_list.handleInput(data);
231
+ tui.requestRender();
232
+ },
233
+ };
234
+ });
235
+
236
+ if (!sets_equal(initial_enabled, current_enabled)) {
237
+ ctx.ui.notify(
238
+ force_disabled.size > 0
239
+ ? 'Reloading to apply updated built-in extensions. CLI --no-* flags still force-disable some extensions in this process.'
240
+ : 'Reloading to apply updated built-in extensions...',
241
+ 'info',
242
+ );
243
+ await ctx.reload();
244
+ }
245
+
246
+ return true;
247
+ }
248
+
139
249
  return async function extensions(pi: ExtensionAPI) {
140
250
  const subs = ['list', 'enable', 'disable', 'toggle', 'search'];
141
251
 
@@ -169,113 +279,8 @@ export function create_extensions_extension(
169
279
  handler: async (args, ctx) => {
170
280
  const trimmed = args.trim();
171
281
 
172
- if (!trimmed && ctx.hasUI) {
173
- const states =
174
- resolve_builtin_extension_states(force_disabled);
175
- const initial_enabled = new Set(
176
- states
177
- .filter((state) => state.saved_enabled)
178
- .map((state) => state.key),
179
- );
180
- const current_enabled = new Set(initial_enabled);
181
-
182
- await ctx.ui.custom((tui, theme, _kb, done) => {
183
- const items = states.map(to_setting_item);
184
- const container = new Container();
185
-
186
- container.addChild({
187
- render: () => {
188
- const saved_enabled = current_enabled.size;
189
- const saved_disabled = states.length - saved_enabled;
190
- const enabled_now = [...current_enabled].filter(
191
- (key) =>
192
- !force_disabled.has(key as BuiltinExtensionKey),
193
- ).length;
194
- const disabled_now = states.length - enabled_now;
195
- return [
196
- theme.fg(
197
- 'accent',
198
- theme.bold('Built-in extensions'),
199
- ),
200
- theme.fg(
201
- 'muted',
202
- `${saved_enabled} saved enabled • ${saved_disabled} saved disabled • ${enabled_now} enabled now • ${disabled_now} disabled now`,
203
- ),
204
- '',
205
- ];
206
- },
207
- invalidate: () => {},
208
- });
209
-
210
- const settings_list = new SettingsList(
211
- items,
212
- Math.min(Math.max(items.length + 4, 8), 16),
213
- {
214
- cursor: theme.fg('accent', '›'),
215
- label: (text, selected) =>
216
- selected ? theme.fg('accent', text) : text,
217
- value: (text, selected) => {
218
- const color = text === ENABLED ? 'success' : 'dim';
219
- const rendered = theme.fg(color, text);
220
- return selected
221
- ? theme.bold(theme.fg('accent', rendered))
222
- : rendered;
223
- },
224
- description: (text) => theme.fg('muted', text),
225
- hint: (text) => theme.fg('dim', text),
226
- },
227
- (id, new_value) => {
228
- const key = id as BuiltinExtensionKey;
229
- const enabled = new_value === ENABLED;
230
- if (enabled) {
231
- current_enabled.add(key);
232
- } else {
233
- current_enabled.delete(key);
234
- }
235
- save_extension_enabled(key, enabled);
236
- },
237
- () => done(undefined),
238
- { enableSearch: true },
239
- );
240
-
241
- container.addChild(settings_list);
242
- container.addChild(
243
- new Text(
244
- theme.fg(
245
- 'dim',
246
- 'esc close • search filters • changes save immediately • CLI --no-* flags still win in this process',
247
- ),
248
- 0,
249
- 1,
250
- ),
251
- );
252
-
253
- return {
254
- render(width: number) {
255
- return container.render(width);
256
- },
257
- invalidate() {
258
- container.invalidate();
259
- },
260
- handleInput(data: string) {
261
- settings_list.handleInput(data);
262
- tui.requestRender();
263
- },
264
- };
265
- });
266
-
267
- if (!sets_equal(initial_enabled, current_enabled)) {
268
- ctx.ui.notify(
269
- force_disabled.size > 0
270
- ? 'Reloading to apply updated built-in extensions. CLI --no-* flags still force-disable some extensions in this process.'
271
- : 'Reloading to apply updated built-in extensions...',
272
- 'info',
273
- );
274
- await ctx.reload();
275
- return;
276
- }
277
-
278
- return;
282
+ if (!trimmed) {
283
+ if (await show_manager(ctx)) return;
279
284
  }
280
285
 
281
286
  const [sub, ...rest] = (trimmed || 'list').split(/\s+/);
@@ -296,6 +301,7 @@ export function create_extensions_extension(
296
301
  case 'disable':
297
302
  case 'toggle': {
298
303
  if (!arg) {
304
+ if (await show_manager(ctx)) return;
299
305
  ctx.ui.notify(
300
306
  `Usage: /extensions ${sub} <key>`,
301
307
  'warning',
@@ -1,87 +1,173 @@
1
- // Handoff extension — extract session context for a new session
2
- // Inspired by jayshah5696/pi-agent-extensions
1
+ // Handoff extension — generate a focused prompt for a new session
3
2
 
4
- import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
5
- import { writeFileSync } from 'node:fs';
6
- import { join } from 'node:path';
3
+ import { complete, type Message } from '@mariozechner/pi-ai';
4
+ import type {
5
+ ExtensionAPI,
6
+ SessionEntry,
7
+ } from '@mariozechner/pi-coding-agent';
8
+ import {
9
+ BorderedLoader,
10
+ convertToLlm,
11
+ serializeConversation,
12
+ } from '@mariozechner/pi-coding-agent';
7
13
 
8
- // Default export for Pi Package / additionalExtensionPaths loading
9
- export default async function handoff(pi: ExtensionAPI) {
10
- const history: Array<{
11
- role: string;
12
- summary: string;
13
- timestamp: number;
14
- }> = [];
15
-
16
- // Track conversation turns
17
- pi.on('message_end', async (event) => {
18
- const msg = event.message as unknown as Record<string, unknown>;
19
- if (!msg) return;
20
-
21
- const content = msg.content as
22
- | Array<{ type: string; text?: string }>
23
- | undefined;
24
- if (!Array.isArray(content)) return;
25
-
26
- const text = content
27
- .filter((c) => c.type === 'text')
28
- .map((c) => c.text || '')
29
- .join('\n');
30
-
31
- if (!text) return;
32
-
33
- const summary =
34
- text.length > 200 ? text.slice(0, 200) + '...' : text;
35
-
36
- history.push({
37
- role: (msg.role as string) || 'unknown',
38
- summary,
39
- timestamp: Date.now(),
40
- });
41
- });
14
+ const SYSTEM_PROMPT = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that:
15
+
16
+ 1. Summarizes relevant context from the conversation (decisions made, approaches taken, key findings)
17
+ 2. Lists any relevant files that were discussed or modified
18
+ 3. Clearly states the next task based on the user's goal
19
+ 4. Is self-contained - the new thread should be able to proceed without the old conversation
20
+
21
+ Format your response as a prompt the user can send to start the new thread. Be concise but include all necessary context. Do not include any preamble like "Here's the prompt" - just output the prompt itself.
22
+
23
+ Example output format:
24
+ ## Context
25
+ We've been working on X. Key decisions:
26
+ - Decision 1
27
+ - Decision 2
28
+
29
+ Files involved:
30
+ - path/to/file1.ts
31
+ - path/to/file2.ts
32
+
33
+ ## Task
34
+ [Clear description of what to do next based on user's goal]`;
42
35
 
36
+ export default async function handoff(pi: ExtensionAPI) {
43
37
  pi.registerCommand('handoff', {
44
38
  description:
45
- 'Export session context as a handoff prompt for a new session',
39
+ 'Transfer context to a new focused session with an AI-generated prompt',
46
40
  handler: async (args, ctx) => {
47
- const task = args.trim();
41
+ if (!ctx.hasUI) {
42
+ ctx.ui.notify('handoff requires interactive mode', 'error');
43
+ return;
44
+ }
45
+
46
+ if (!ctx.model) {
47
+ ctx.ui.notify('No model selected', 'error');
48
+ return;
49
+ }
48
50
 
49
- if (history.length === 0) {
51
+ const goal = args.trim();
52
+ if (!goal) {
50
53
  ctx.ui.notify(
51
- 'No conversation history to hand off',
52
- 'warning',
54
+ 'Usage: /handoff <goal for new thread>',
55
+ 'error',
53
56
  );
54
57
  return;
55
58
  }
56
59
 
57
- const context = history
58
- .map((h) => `[${h.role}] ${h.summary}`)
59
- .join('\n\n');
60
+ const branch = ctx.sessionManager.getBranch();
61
+ const messages = branch
62
+ .filter(
63
+ (entry): entry is SessionEntry & { type: 'message' } =>
64
+ entry.type === 'message',
65
+ )
66
+ .map((entry) => entry.message);
60
67
 
61
- const handoff = `## Handoff from Previous Session
62
-
63
- ### Context
64
- The previous session covered the following:
68
+ if (messages.length === 0) {
69
+ ctx.ui.notify('No conversation to hand off', 'error');
70
+ return;
71
+ }
65
72
 
66
- ${context}
73
+ const llm_messages = convertToLlm(messages);
74
+ const conversation_text = serializeConversation(llm_messages);
75
+ const current_session_file =
76
+ ctx.sessionManager.getSessionFile();
77
+ const model = ctx.model;
78
+
79
+ const result = await ctx.ui.custom<string | null>(
80
+ (tui, theme, _kb, done) => {
81
+ const loader = new BorderedLoader(
82
+ tui,
83
+ theme,
84
+ 'Generating handoff prompt...',
85
+ );
86
+ loader.onAbort = () => done(null);
87
+
88
+ const generate = async () => {
89
+ const auth =
90
+ await ctx.modelRegistry.getApiKeyAndHeaders(model);
91
+ if (!auth.ok || !auth.apiKey) {
92
+ throw new Error(
93
+ auth.ok
94
+ ? `No API key for ${model.provider}`
95
+ : auth.error,
96
+ );
97
+ }
98
+
99
+ const user_message: Message = {
100
+ role: 'user',
101
+ content: [
102
+ {
103
+ type: 'text',
104
+ text: `## Conversation History\n\n${conversation_text}\n\n## User's Goal for New Thread\n\n${goal}`,
105
+ },
106
+ ],
107
+ timestamp: Date.now(),
108
+ };
109
+
110
+ const response = await complete(
111
+ model,
112
+ {
113
+ systemPrompt: SYSTEM_PROMPT,
114
+ messages: [user_message],
115
+ },
116
+ {
117
+ apiKey: auth.apiKey,
118
+ headers: auth.headers,
119
+ signal: loader.signal,
120
+ },
121
+ );
122
+
123
+ if (response.stopReason === 'aborted') {
124
+ return null;
125
+ }
126
+
127
+ return response.content
128
+ .filter(
129
+ (c): c is { type: 'text'; text: string } =>
130
+ c.type === 'text',
131
+ )
132
+ .map((c) => c.text)
133
+ .join('\n');
134
+ };
135
+
136
+ generate()
137
+ .then(done)
138
+ .catch((err) => {
139
+ console.error('Handoff generation failed:', err);
140
+ done(null);
141
+ });
142
+
143
+ return loader;
144
+ },
145
+ );
67
146
 
68
- ### Task
69
- ${task || 'Continue from where the previous session left off.'}
147
+ if (result === null) {
148
+ ctx.ui.notify('Cancelled', 'info');
149
+ return;
150
+ }
70
151
 
71
- ### Instructions
72
- - Review the context above to understand what was done
73
- - Do NOT repeat work that was already completed
74
- - Focus on the task described above
75
- `;
152
+ const edited_prompt = await ctx.ui.editor(
153
+ 'Edit handoff prompt',
154
+ result,
155
+ );
156
+ if (edited_prompt === undefined) {
157
+ ctx.ui.notify('Cancelled', 'info');
158
+ return;
159
+ }
76
160
 
77
- // Write to file
78
- const filename = `handoff-${Date.now()}.md`;
79
- const filepath = join(ctx.cwd, filename);
80
- writeFileSync(filepath, handoff, 'utf-8');
161
+ const new_session_result = await ctx.newSession({
162
+ parentSession: current_session_file,
163
+ });
164
+ if (new_session_result.cancelled) {
165
+ ctx.ui.notify('New session cancelled', 'info');
166
+ return;
167
+ }
81
168
 
82
- ctx.ui.notify(
83
- `Handoff written to ${filename}\n\nUse: my-pi < ${filename}`,
84
- );
169
+ ctx.ui.setEditorText(edited_prompt);
170
+ ctx.ui.notify('Handoff ready. Submit when ready.', 'info');
85
171
  },
86
172
  });
87
173
  }