mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 21:03:19 +00:00
Add handoff example hook
Transfers context to a new focused session instead of compacting. User provides a goal, hook generates a prompt with relevant context, creates a new session with parent tracking, and loads the prompt as a draft in the editor for review. Inspired by Amp's handoff feature.
This commit is contained in:
parent
5c37fe27c6
commit
ace0063a00
1 changed files with 159 additions and 0 deletions
159
packages/coding-agent/examples/hooks/handoff.ts
Normal file
159
packages/coding-agent/examples/hooks/handoff.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* Handoff hook - 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.
|
||||
*/
|
||||
|
||||
import { complete, type Message } from "@mariozechner/pi-ai";
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { BorderedLoader } from "@mariozechner/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]`;
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.registerCommand("handoff", {
|
||||
description: "Transfer context to a new focused session",
|
||||
handler: async (args, ctx) => {
|
||||
if (!ctx.hasUI) {
|
||||
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 <goal for new thread>", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Gather conversation context from current branch
|
||||
const branch = ctx.sessionManager.getBranch();
|
||||
const conversationParts: string[] = [];
|
||||
|
||||
for (const entry of branch) {
|
||||
if (entry.type === "message") {
|
||||
const msg = entry.message;
|
||||
if ("role" in msg && (msg.role === "user" || msg.role === "assistant")) {
|
||||
let text: string;
|
||||
if (typeof msg.content === "string") {
|
||||
text = msg.content;
|
||||
} else {
|
||||
text = msg.content
|
||||
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join("\n");
|
||||
}
|
||||
if (text) {
|
||||
const role = msg.role === "user" ? "User" : "Assistant";
|
||||
conversationParts.push(`${role}: ${text}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (conversationParts.length === 0) {
|
||||
ctx.ui.notify("No conversation to hand off", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const conversationText = conversationParts.join("\n\n");
|
||||
const currentSessionFile = ctx.sessionManager.getSessionFile();
|
||||
|
||||
// Generate the handoff prompt with loader UI
|
||||
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
|
||||
const loader = new BorderedLoader(tui, theme, `Generating handoff prompt...`);
|
||||
loader.onAbort = () => done(null);
|
||||
|
||||
const doGenerate = async () => {
|
||||
const apiKey = await ctx.modelRegistry.getApiKey(ctx.model!);
|
||||
|
||||
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 complete(
|
||||
ctx.model!,
|
||||
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
|
||||
{ apiKey, signal: loader.signal },
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Create new session with parent tracking
|
||||
const newSessionResult = await ctx.newSession({
|
||||
parentSession: currentSessionFile,
|
||||
});
|
||||
|
||||
if (newSessionResult.cancelled) {
|
||||
ctx.ui.notify("New session cancelled", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the generated prompt as a draft in the editor
|
||||
ctx.ui.setEditorText(result);
|
||||
ctx.ui.notify("Handoff ready. Review the prompt and submit when ready.", "info");
|
||||
},
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue