mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 02:04:05 +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