BIG pi update with claude chat
This commit is contained in:
275
pi/.pi/agent/extensions/ask-claude.ts
Normal file
275
pi/.pi/agent/extensions/ask-claude.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* ask-claude — Stream Claude agent reviews into pi.
|
||||
*
|
||||
* For AGENTS to use. Delegates to specialized Claude agents or raw models
|
||||
* for review, analysis, debugging, and second opinions.
|
||||
*
|
||||
* Tool (callable by the LLM):
|
||||
* ask_claude(prompt, agent?, model?, question?, session_id?)
|
||||
* agent — any agent name from ~/.claude/agents/ (e.g. "plan_review", "code_review", "oracle", "debug")
|
||||
* model — model override: "opus", "sonnet", or full model ID
|
||||
* question — specific focus prepended as a review header
|
||||
* session_id — resume a prior conversation (returned in every response)
|
||||
*
|
||||
* Commands:
|
||||
* /review-plan — editor → Claude Opus plan_review → inject result
|
||||
* /review-code — editor → Claude Sonnet code_review → inject result
|
||||
*/
|
||||
|
||||
import { defineTool, getMarkdownTheme } from "@mariozechner/pi-coding-agent";
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { Type } from "@mariozechner/pi-ai";
|
||||
import { Box, Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import {
|
||||
type ClaudeDetails,
|
||||
type Theme,
|
||||
renderToolCallLine,
|
||||
renderToolResultBox,
|
||||
renderToolBlock,
|
||||
formatUsage,
|
||||
buildEditDiff,
|
||||
formatAnthropicError,
|
||||
runClaude,
|
||||
type RunClaudeResult,
|
||||
} from "../shared/claude-stream.js";
|
||||
|
||||
// =============================================================================
|
||||
// Rendering (ask-claude specific — uses shared tool renderers)
|
||||
// =============================================================================
|
||||
|
||||
function buildLabel(agent?: string, model?: string): string {
|
||||
if (agent) return `Claude [${agent}]`;
|
||||
if (model) return `Claude [${model}]`;
|
||||
return "Claude Sonnet";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tool definition
|
||||
// =============================================================================
|
||||
|
||||
const AskClaudeParams = Type.Object({
|
||||
prompt: Type.String({
|
||||
description: "Full content to review/analyze. Include all relevant context: CLAUDE.md conventions, files explored, code or plan to review.",
|
||||
}),
|
||||
agent: Type.Optional(Type.String({
|
||||
description: "Agent name from ~/.claude/agents/ (e.g. 'plan_review', 'code_review', 'oracle', 'debug'). Omit to use model= directly.",
|
||||
})),
|
||||
model: Type.Optional(Type.String({
|
||||
description: "Model override: 'opus', 'sonnet', 'haiku', or a full model ID. When agent is set this overrides the agent's default. When agent is omitted this selects the model directly.",
|
||||
})),
|
||||
question: Type.Optional(Type.String({
|
||||
description: "Specific question or focus area prepended to the prompt (e.g. 'Focus on security', 'Are there race conditions?').",
|
||||
})),
|
||||
session_id: Type.Optional(Type.String({
|
||||
description: "Resume a prior conversation. Pass the session_id returned from a previous ask_claude call.",
|
||||
})),
|
||||
});
|
||||
|
||||
const askClaudeTool = defineTool({
|
||||
name: "ask_claude",
|
||||
label: "Ask Claude",
|
||||
description: [
|
||||
"Ask a Claude agent or model for review, analysis, or a second opinion.",
|
||||
"agent=<name> runs a named agent from ~/.claude/agents/ (e.g. 'plan_review', 'code_review', 'oracle', 'debug').",
|
||||
"Use model= alone for free-form requests without an agent. Use question= to specify a focus.",
|
||||
"Pass session_id from a prior response to continue the same conversation across turns.",
|
||||
"CLAUDE.md and .claude/skills are loaded automatically from the project root.",
|
||||
].join(" "),
|
||||
promptSnippet: "Ask a Claude agent or model for review, analysis, or a second opinion",
|
||||
promptGuidelines: [
|
||||
"Use ask_claude(agent=<name>) to invoke a specialized agent — include all relevant context in the prompt.",
|
||||
"Use ask_claude(model='opus', question='...') for free-form deep analysis.",
|
||||
"Always include the artifact to review (plan, code, problem description) in the prompt.",
|
||||
"Pass session_id back from the previous response to continue the conversation.",
|
||||
],
|
||||
parameters: AskClaudeParams,
|
||||
|
||||
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
||||
const fullPrompt = params.question
|
||||
? `## Review Focus\n\n${params.question}\n\n## Content\n\n${params.prompt}`
|
||||
: params.prompt;
|
||||
|
||||
const label = buildLabel(params.agent, params.model);
|
||||
const details: ClaudeDetails = { label, done: false, blocks: [], finalText: "", isResume: !!params.session_id };
|
||||
|
||||
try {
|
||||
const result: RunClaudeResult = await runClaude(fullPrompt, {
|
||||
agent: params.agent,
|
||||
model: params.model,
|
||||
sessionId: params.session_id,
|
||||
enrichEditDiffs: true, // ask-claude wants diff enrichment
|
||||
cwd: ctx.cwd,
|
||||
signal,
|
||||
onUpdate: (partial) => {
|
||||
Object.assign(details, partial);
|
||||
onUpdate?.({
|
||||
content: [{ type: "text", text: details.finalText || "(thinking…)" }],
|
||||
details: { ...details },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
details.done = true;
|
||||
details.finalText = result.finalText;
|
||||
details.blocks = result.blocks;
|
||||
details.sessionId = result.sessionId;
|
||||
details.costUsd = result.costUsd;
|
||||
details.inputTokens = result.inputTokens;
|
||||
details.outputTokens = result.outputTokens;
|
||||
details.cacheReadTokens = result.cacheReadTokens;
|
||||
details.cacheWriteTokens = result.cacheWriteTokens;
|
||||
|
||||
const sessionNote = result.sessionId
|
||||
? `\n\n---\n*session_id: \`${result.sessionId}\`*`
|
||||
: "";
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: (result.finalText || "(no output)") + sessionNote }],
|
||||
details: { ...details },
|
||||
};
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
details.done = true;
|
||||
details.finalText = `Error: ${errMsg}`;
|
||||
return {
|
||||
content: [{ type: "text", text: `**Claude error:** ${errMsg}` }],
|
||||
details: { ...details },
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
renderCall(args, theme, _ctx) {
|
||||
const label = buildLabel(args.agent, args.model);
|
||||
let text = theme.fg("toolTitle", theme.bold("ask_claude ")) + theme.fg("accent", `[${label}]`);
|
||||
if (args.question) {
|
||||
text += "\n " + theme.fg("dim", theme.italic(args.question));
|
||||
} else {
|
||||
const lines = args.prompt.split("\n").filter((l) => l.trim()).slice(0, 3);
|
||||
text += "\n " + theme.fg("dim", lines.join("\n "));
|
||||
}
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { isPartial }, theme, _ctx) {
|
||||
const d = result.details as ClaudeDetails | undefined;
|
||||
if (!d) return new Text(theme.fg("muted", "…"), 0, 0);
|
||||
|
||||
const isDone = d.done && !isPartial;
|
||||
const statusIcon = isDone ? theme.fg("success", "✓ ") : theme.fg("warning", "⏳ ");
|
||||
const c = new Container();
|
||||
|
||||
const resume = d.isResume ? theme.fg("dim", " ↩") : "";
|
||||
c.addChild(new Text(statusIcon + theme.fg("toolTitle", theme.bold(d.label)) + resume, 0, 0));
|
||||
|
||||
for (const block of d.blocks ?? []) {
|
||||
if (block.type === "thinking" && block.text.trim()) {
|
||||
c.addChild(new Text(theme.fg("dim", theme.italic(block.text.trimEnd())), 0, 0));
|
||||
} else if (block.type === "tool") {
|
||||
c.addChild(renderToolBlock(block, theme as any));
|
||||
} else if (block.type === "text" && block.text.trim()) {
|
||||
c.addChild(new Spacer(1));
|
||||
if (isDone) {
|
||||
c.addChild(new Markdown(block.text.trim(), 0, 0, getMarkdownTheme()));
|
||||
} else {
|
||||
c.addChild(new Text(theme.fg("dim", block.text.trimEnd()), 0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDone) {
|
||||
const usageLine = formatUsage(d);
|
||||
const parts: string[] = [];
|
||||
if (usageLine) parts.push(usageLine);
|
||||
if (d.sessionId) parts.push(`session:${d.sessionId.slice(0, 8)}`);
|
||||
if (parts.length > 0) {
|
||||
c.addChild(new Spacer(1));
|
||||
c.addChild(new Text(theme.fg("dim", parts.join(" ")), 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
return c;
|
||||
},
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Extension entry point
|
||||
// =============================================================================
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerTool(askClaudeTool);
|
||||
|
||||
// ── /review-plan ───────────────────────────────────────────────────────
|
||||
|
||||
pi.registerCommand("review-plan", {
|
||||
description: "Editor → Claude Opus plan_review → inject review into conversation",
|
||||
handler: async (_args, ctx) => {
|
||||
const input = await ctx.ui.editor(
|
||||
"Plan Review · Claude Opus",
|
||||
"Paste your plan or strategy. Claude will review for correctness, completeness, and risk.",
|
||||
);
|
||||
if (!input?.trim()) { ctx.ui.notify("Cancelled.", "info"); return; }
|
||||
ctx.ui.setStatus("ask-claude", "Asking Claude Opus (plan_review)…");
|
||||
try {
|
||||
const r = await runClaude(input, { agent: "plan_review", cwd: ctx.cwd, onUpdate: () => {} });
|
||||
if (!r.finalText.trim()) { ctx.ui.notify("No output from Claude.", "warning"); return; }
|
||||
pi.sendMessage(
|
||||
{ customType: "ask-claude-review", content: r.finalText.trim(), display: true,
|
||||
details: { label: "Claude Opus · plan_review", output: r.finalText } },
|
||||
{ triggerTurn: true },
|
||||
);
|
||||
} catch (err) {
|
||||
ctx.ui.notify(`Claude error: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||
} finally { ctx.ui.setStatus("ask-claude", undefined); }
|
||||
},
|
||||
});
|
||||
|
||||
// ── /review-code ───────────────────────────────────────────────────────
|
||||
|
||||
pi.registerCommand("review-code", {
|
||||
description: "Editor → Claude Sonnet code_review → inject review into conversation",
|
||||
handler: async (_args, ctx) => {
|
||||
const input = await ctx.ui.editor(
|
||||
"Code Review · Claude Sonnet",
|
||||
"Paste code to review. Include the plan it implements and any specific concerns.",
|
||||
);
|
||||
if (!input?.trim()) { ctx.ui.notify("Cancelled.", "info"); return; }
|
||||
ctx.ui.setStatus("ask-claude", "Asking Claude Sonnet (code_review)…");
|
||||
try {
|
||||
const r = await runClaude(input, { agent: "code_review", cwd: ctx.cwd, onUpdate: () => {} });
|
||||
if (!r.finalText.trim()) { ctx.ui.notify("No output from Claude.", "warning"); return; }
|
||||
pi.sendMessage(
|
||||
{ customType: "ask-claude-review", content: r.finalText.trim(), display: true,
|
||||
details: { label: "Claude Sonnet · code_review", output: r.finalText } },
|
||||
{ triggerTurn: true },
|
||||
);
|
||||
} catch (err) {
|
||||
ctx.ui.notify(`Claude error: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||
} finally { ctx.ui.setStatus("ask-claude", undefined); }
|
||||
},
|
||||
});
|
||||
|
||||
// ── Message renderer for injected reviews ──────────────────────────────
|
||||
|
||||
pi.registerMessageRenderer("ask-claude-review", (message, { expanded }, theme) => {
|
||||
const d = message.details as { label?: string; output?: string } | undefined;
|
||||
const label = d?.label ?? "Claude";
|
||||
const output = (d?.output ?? "").trim();
|
||||
|
||||
if (expanded) {
|
||||
const c = new Container();
|
||||
c.addChild(new Text(theme.fg("accent", "◆ ") + theme.fg("toolTitle", theme.bold(label)), 0, 0));
|
||||
c.addChild(new Spacer(1));
|
||||
c.addChild(new Markdown(output, 0, 0, getMarkdownTheme()));
|
||||
return c;
|
||||
}
|
||||
|
||||
let text = theme.fg("accent", "◆ ") + theme.fg("toolTitle", theme.bold(label));
|
||||
const lines = output.split("\n").filter((l) => l.trim());
|
||||
const preview = lines.slice(0, 4).join("\n");
|
||||
if (preview) {
|
||||
text += "\n" + theme.fg("dim", preview);
|
||||
if (lines.length > 4) text += "\n" + theme.fg("muted", `… ${lines.length - 4} more (Ctrl+O)`);
|
||||
}
|
||||
return new Text(text, 0, 0);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user