diff --git a/NOTICE b/NOTICE index b382b53..9021939 100644 --- a/NOTICE +++ b/NOTICE @@ -16,10 +16,6 @@ extensions/goal.ts Modified: session-log-backed state instead of an external database; optional token budget; stricter completion and blocked audits. -extensions/handoff.ts - Copied from earendil-works/pi (MIT, Copyright (c) earendil-works): - https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/handoff.ts - themes/catppuccin-frappe.json Derived from madeleineostoja/pi-catppuccin (MIT, Copyright (c) 2026 Madeleine Ostoja): https://github.com/madeleineostoja/pi-catppuccin/blob/main/themes/catppuccin-frappe.json diff --git a/README.md b/README.md index 37b065c..65d2664 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ Personal [Pi](https://pi.dev) package — extensions, skills, prompts, and theme ## Extensions - [goal.ts](./extensions/goal.ts) - long-running objective mode with automatic continuation. -- [handoff.ts](./extensions/handoff.ts) - transfers context to a new focused session. - [notify.ts](./extensions/notify.ts) - sends a native terminal notification (OSC 777) when Pi finishes and is waiting for input. ## Prompts diff --git a/extensions/handoff.ts b/extensions/handoff.ts deleted file mode 100644 index 82276fc..0000000 --- a/extensions/handoff.ts +++ /dev/null @@ -1,194 +0,0 @@ -/** - * Handoff extension - transfer context to a new focused session - * - * Instead of compacting (which is lossy), handoff extracts what matters - * for your next task and creates a new session with a generated prompt. - * - * Usage: - * /handoff now implement this for teams as well - * /handoff execute phase one of the plan - * /handoff check other places that need this fix - * - * The generated prompt appears as a draft in the editor for review/editing. - * - * Copied from [earendil-works/pi - * `packages/coding-agent/examples/extensions/handoff.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/handoff.ts) - * (MIT, Copyright (c) earendil-works). - */ - -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { type Message, uuidv7 } from "@earendil-works/pi-ai"; -import type { ExtensionAPI, SessionEntry } from "@earendil-works/pi-coding-agent"; -import { BorderedLoader, convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent"; - -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: - -1. Summarizes relevant context from the conversation (decisions made, approaches taken, key findings) -2. Lists any relevant files that were discussed or modified -3. Clearly states the next task based on the user's goal -4. Is self-contained - the new thread should be able to proceed without the old conversation - -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. - -Example output format: -## Context -We've been working on X. Key decisions: -- Decision 1 -- Decision 2 - -Files involved: -- path/to/file1.ts -- path/to/file2.ts - -## Task -[Clear description of what to do next based on user's goal]`; - -function entryToMessage(entry: SessionEntry): AgentMessage | undefined { - if (entry.type === "message") { - return entry.message; - } - if (entry.type === "compaction") { - return { - role: "compactionSummary", - summary: entry.summary, - tokensBefore: entry.tokensBefore, - timestamp: new Date(entry.timestamp).getTime(), - }; - } - return undefined; -} - -function getHandoffMessages(branch: SessionEntry[]): AgentMessage[] { - let compactionIndex = -1; - for (let i = branch.length - 1; i >= 0; i--) { - if (branch[i].type === "compaction") { - compactionIndex = i; - break; - } - } - if (compactionIndex < 0) { - return branch.map(entryToMessage).filter((message) => message !== undefined); - } - - const compaction = branch[compactionIndex]; - const firstKeptIndex = - compaction.type === "compaction" ? branch.findIndex((entry) => entry.id === compaction.firstKeptEntryId) : -1; - const compactedBranch = [ - compaction, - ...(firstKeptIndex >= 0 ? branch.slice(firstKeptIndex, compactionIndex) : []), - ...branch.slice(compactionIndex + 1), - ]; - return compactedBranch.map(entryToMessage).filter((message) => message !== undefined); -} - -export default function (pi: ExtensionAPI) { - pi.registerCommand("handoff", { - description: "Transfer context to a new focused session", - handler: async (args, ctx) => { - if (ctx.mode !== "tui") { - ctx.ui.notify("handoff requires interactive mode", "error"); - return; - } - - if (!ctx.model) { - ctx.ui.notify("No model selected", "error"); - return; - } - - const goal = args.trim(); - if (!goal) { - ctx.ui.notify("Usage: /handoff ", "error"); - return; - } - - // Gather conversation context from current branch. If the branch was compacted, - // include the compaction summary plus entries from firstKeptEntryId onward. - const messages = getHandoffMessages(ctx.sessionManager.getBranch()); - - if (messages.length === 0) { - ctx.ui.notify("No conversation to hand off", "error"); - return; - } - - // Convert to LLM format and serialize - const llmMessages = convertToLlm(messages); - const conversationText = serializeConversation(llmMessages); - const currentSessionFile = ctx.sessionManager.getSessionFile(); - - // Generate the handoff prompt with loader UI - const result = await ctx.ui.custom((tui, theme, _kb, done) => { - const loader = new BorderedLoader(tui, theme, `Generating handoff prompt...`); - loader.onAbort = () => done(null); - - const doGenerate = async () => { - const userMessage: Message = { - role: "user", - content: [ - { - type: "text", - text: `## Conversation History\n\n${conversationText}\n\n## User's Goal for New Thread\n\n${goal}`, - }, - ], - timestamp: Date.now(), - }; - - const response = await ctx.modelRegistry.complete( - ctx.model!, - { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] }, - { - signal: loader.signal, - cacheRetention: "none", - sessionId: uuidv7(), - }, - ); - - if (response.stopReason === "aborted") { - return null; - } - - return response.content - .filter((c): c is { type: "text"; text: string } => c.type === "text") - .map((c) => c.text) - .join("\n"); - }; - - doGenerate() - .then(done) - .catch((err) => { - console.error("Handoff generation failed:", err); - done(null); - }); - - return loader; - }); - - if (result === null) { - ctx.ui.notify("Cancelled", "info"); - return; - } - - // Let user edit the generated prompt - const editedPrompt = await ctx.ui.editor("Edit handoff prompt", result); - - if (editedPrompt === undefined) { - ctx.ui.notify("Cancelled", "info"); - return; - } - - // Create new session with parent tracking. Use the replacement-session - // context for post-switch UI work; the original ctx is stale after a - // successful session replacement. - const newSessionResult = await ctx.newSession({ - parentSession: currentSessionFile, - withSession: async (replacementCtx) => { - replacementCtx.ui.setEditorText(editedPrompt); - replacementCtx.ui.notify("Handoff ready. Submit when ready.", "info"); - }, - }); - - if (newSessionResult.cancelled) { - ctx.ui.notify("New session cancelled", "info"); - } - }, - }); -}