@ssweens/pi-handoff 1.0.0 → 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/extensions/handoff.ts +93 -37
- package/package.json +5 -1
package/extensions/handoff.ts
CHANGED
|
@@ -20,7 +20,12 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { complete, type Message } from "@mariozechner/pi-ai";
|
|
23
|
-
import type {
|
|
23
|
+
import type {
|
|
24
|
+
ExtensionAPI,
|
|
25
|
+
ExtensionCommandContext,
|
|
26
|
+
ExtensionContext,
|
|
27
|
+
SessionEntry,
|
|
28
|
+
} from "@mariozechner/pi-coding-agent";
|
|
24
29
|
import { BorderedLoader, convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
|
|
25
30
|
import { Type } from "@sinclair/typebox";
|
|
26
31
|
|
|
@@ -28,6 +33,11 @@ import { Type } from "@sinclair/typebox";
|
|
|
28
33
|
// Key: parent session file path, Value: handoff text to set in editor
|
|
29
34
|
const pendingHandoffText = new Map<string, string>();
|
|
30
35
|
|
|
36
|
+
/** @internal Test-only: clear all pending handoff state between tests. */
|
|
37
|
+
export function __clearPendingHandoffText(): void {
|
|
38
|
+
pendingHandoffText.clear();
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
// Handoff generation system prompt.
|
|
32
42
|
//
|
|
33
43
|
// Combines Pi's structured compaction format (Goal, Progress, Decisions,
|
|
@@ -81,7 +91,7 @@ Rules:
|
|
|
81
91
|
|
|
82
92
|
// System prompt fragment injected via before_agent_start.
|
|
83
93
|
// Teaches the model about handoffs so it can suggest them proactively.
|
|
84
|
-
const HANDOFF_SYSTEM_HINT = `
|
|
94
|
+
export const HANDOFF_SYSTEM_HINT = `
|
|
85
95
|
## Handoff
|
|
86
96
|
|
|
87
97
|
Use \`/handoff <goal>\` to transfer context to a new focused session.
|
|
@@ -89,9 +99,10 @@ Handoffs are especially effective after planning — clear the context and start
|
|
|
89
99
|
At high context usage, suggest a handoff rather than losing important context.`;
|
|
90
100
|
|
|
91
101
|
/**
|
|
92
|
-
* Generate a session name from the goal (slug format)
|
|
102
|
+
* Generate a session name from the goal (slug format).
|
|
103
|
+
* Exported for testing.
|
|
93
104
|
*/
|
|
94
|
-
function goalToSessionName(goal: string): string {
|
|
105
|
+
export function goalToSessionName(goal: string): string {
|
|
95
106
|
return goal
|
|
96
107
|
.toLowerCase()
|
|
97
108
|
.replace(/[^a-z0-9\s-]/g, "")
|
|
@@ -100,13 +111,38 @@ function goalToSessionName(goal: string): string {
|
|
|
100
111
|
.slice(0, 50);
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Build the full handoff prompt from goal, session file, and generated summary.
|
|
116
|
+
* Includes parent session reference and skill prefix when applicable.
|
|
117
|
+
* Exported for testing.
|
|
118
|
+
*/
|
|
119
|
+
export function buildFullPrompt(
|
|
120
|
+
goal: string,
|
|
121
|
+
currentSessionFile: string | null,
|
|
122
|
+
summary: string,
|
|
123
|
+
): string {
|
|
124
|
+
let fullPrompt = `# ${goal}\n\n`;
|
|
125
|
+
|
|
126
|
+
if (currentSessionFile) {
|
|
127
|
+
fullPrompt += `**Parent session:** \`${currentSessionFile}\`\n\n`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fullPrompt += summary;
|
|
131
|
+
|
|
132
|
+
// Prepend session-query skill if parent session present
|
|
133
|
+
return /\*\*Parent session:\*\*/.test(fullPrompt)
|
|
134
|
+
? `/skill:pi-session-query ${fullPrompt}`
|
|
135
|
+
: fullPrompt;
|
|
136
|
+
}
|
|
137
|
+
|
|
103
138
|
/**
|
|
104
139
|
* Handoff modes:
|
|
105
140
|
* - "command": User-initiated via /handoff
|
|
106
141
|
* - "tool": Agent-initiated via handoff tool
|
|
107
142
|
* - "compactHook": Triggered from session_before_compact
|
|
108
143
|
*
|
|
109
|
-
*
|
|
144
|
+
* Command mode has ExtensionCommandContext (with newSession).
|
|
145
|
+
* Tool and compactHook modes have ExtensionContext (ReadonlySessionManager, no newSession).
|
|
110
146
|
*/
|
|
111
147
|
type HandoffMode = "command" | "tool" | "compactHook";
|
|
112
148
|
|
|
@@ -115,6 +151,13 @@ type HandoffMode = "command" | "tool" | "compactHook";
|
|
|
115
151
|
* and the auto-handoff compaction hook.
|
|
116
152
|
*
|
|
117
153
|
* Returns an error string on failure, or undefined on success.
|
|
154
|
+
*
|
|
155
|
+
* Session creation behavior:
|
|
156
|
+
* - "command" mode: ctx has newSession() — creates new session immediately.
|
|
157
|
+
* - "tool"/"compactHook" mode: ctx is ReadonlySessionManager — cannot create
|
|
158
|
+
* sessions. Instead, pre-fills the editor with the generated prompt and notifies
|
|
159
|
+
* the user. The session_switch handler picks up pendingHandoffText when they
|
|
160
|
+
* manually start a new session.
|
|
118
161
|
*/
|
|
119
162
|
async function performHandoff(
|
|
120
163
|
pi: ExtensionAPI,
|
|
@@ -201,48 +244,46 @@ async function performHandoff(
|
|
|
201
244
|
return "Handoff cancelled.";
|
|
202
245
|
}
|
|
203
246
|
|
|
204
|
-
|
|
205
|
-
let fullPrompt = `# ${goal}\n\n`;
|
|
206
|
-
|
|
207
|
-
if (currentSessionFile) {
|
|
208
|
-
fullPrompt += `**Parent session:** \`${currentSessionFile}\`\n\n`;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
fullPrompt += result;
|
|
247
|
+
const messageToSend = buildFullPrompt(goal, currentSessionFile ?? null, result);
|
|
212
248
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
? `/skill:pi-session-query ${fullPrompt}`
|
|
216
|
-
: fullPrompt;
|
|
217
|
-
|
|
218
|
-
// Store the handoff text for the session_switch event to pick up
|
|
219
|
-
// We use the parent session file as key since that's what we pass to newSession
|
|
249
|
+
// Store the handoff text for the session_switch event to pick up.
|
|
250
|
+
// Key: parent session file (passed to newSession as parentSession).
|
|
220
251
|
if (currentSessionFile) {
|
|
221
252
|
pendingHandoffText.set(currentSessionFile, messageToSend);
|
|
222
253
|
}
|
|
223
254
|
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
255
|
+
// Session creation: only possible with ExtensionCommandContext (command mode).
|
|
256
|
+
// Hook and tool modes have ReadonlySessionManager — newSession() does not exist.
|
|
257
|
+
// In those modes, pre-fill the editor so the user can start a new session manually.
|
|
258
|
+
const hasNewSession =
|
|
259
|
+
"newSession" in ctx && typeof (ctx as ExtensionCommandContext).newSession === "function";
|
|
260
|
+
|
|
261
|
+
if (hasNewSession) {
|
|
262
|
+
const cmdCtx = ctx as ExtensionCommandContext;
|
|
263
|
+
const newSessionResult = await cmdCtx.newSession({
|
|
264
|
+
parentSession: currentSessionFile ?? undefined,
|
|
229
265
|
});
|
|
230
266
|
|
|
231
267
|
if (newSessionResult.cancelled) {
|
|
232
|
-
// Clean up pending text if cancelled
|
|
268
|
+
// Clean up pending text if session creation was cancelled
|
|
233
269
|
if (currentSessionFile) {
|
|
234
270
|
pendingHandoffText.delete(currentSessionFile);
|
|
235
271
|
}
|
|
236
272
|
return "New session cancelled.";
|
|
237
273
|
}
|
|
274
|
+
|
|
275
|
+
pi.setSessionName(goalToSessionName(goal));
|
|
238
276
|
} else {
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
277
|
+
// Hook / tool mode: set editor text so the user can see the generated prompt.
|
|
278
|
+
// The session_switch handler will auto-set it in the new session when they
|
|
279
|
+
// start one (Ctrl+N or equivalent).
|
|
280
|
+
ctx.ui.setEditorText(messageToSend);
|
|
281
|
+
ctx.ui.notify(
|
|
282
|
+
"Handoff ready! Start a new session to automatically send the generated prompt.",
|
|
283
|
+
"info",
|
|
284
|
+
);
|
|
242
285
|
}
|
|
243
286
|
|
|
244
|
-
pi.setSessionName(goalToSessionName(goal));
|
|
245
|
-
|
|
246
287
|
return undefined;
|
|
247
288
|
}
|
|
248
289
|
|
|
@@ -291,7 +332,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
291
332
|
}
|
|
292
333
|
|
|
293
334
|
const usage = ctx.getContextUsage();
|
|
294
|
-
const pctStr = usage ? `${Math.round(usage.percent)}%` : "high";
|
|
335
|
+
const pctStr = usage?.percent != null ? `${Math.round(usage.percent)}%` : "high";
|
|
295
336
|
|
|
296
337
|
const choice = await ctx.ui.select(
|
|
297
338
|
`Context is ${pctStr} full. What would you like to do?`,
|
|
@@ -313,9 +354,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
313
354
|
}
|
|
314
355
|
contextForHandoff += `## Recent Conversation\n\n${conversationText}`;
|
|
315
356
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
357
|
+
try {
|
|
358
|
+
const error = await performHandoff(
|
|
359
|
+
pi,
|
|
360
|
+
ctx,
|
|
361
|
+
"Continue current work",
|
|
362
|
+
"compactHook",
|
|
363
|
+
contextForHandoff,
|
|
364
|
+
);
|
|
365
|
+
if (error) {
|
|
366
|
+
ctx.ui.notify(`Handoff failed: ${error}. Compacting instead.`, "warning");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
} catch (err) {
|
|
370
|
+
ctx.ui.notify(
|
|
371
|
+
`Handoff error: ${err instanceof Error ? err.message : String(err)}. Compacting instead.`,
|
|
372
|
+
"warning",
|
|
373
|
+
);
|
|
319
374
|
return;
|
|
320
375
|
}
|
|
321
376
|
|
|
@@ -355,11 +410,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
355
410
|
content: [
|
|
356
411
|
{
|
|
357
412
|
type: "text" as const,
|
|
358
|
-
text:
|
|
413
|
+
text:
|
|
414
|
+
error ??
|
|
415
|
+
"Handoff queued. The generated prompt has been placed in the editor — start a new session to send it.",
|
|
359
416
|
},
|
|
360
417
|
],
|
|
361
418
|
};
|
|
362
419
|
},
|
|
363
420
|
});
|
|
364
|
-
|
|
365
421
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssweens/pi-handoff",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"test": "bun test tests/",
|
|
6
|
+
"test:watch": "bun test --watch tests/"
|
|
7
|
+
},
|
|
4
8
|
"description": "Enhanced handoff extension for pi - context management for agentic coding workflows",
|
|
5
9
|
"keywords": [
|
|
6
10
|
"pi-package"
|