import type { AgentToolResult, ExtensionContext, Theme, ToolRenderResultOptions, } from "@earendil-works/pi-coding-agent"; import { getMarkdownTheme } from "@earendil-works/pi-coding-agent"; import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui"; import { acquireSubagentSlot } from "../../shared/subagent/slot-semaphore.js"; import { thinkingLevelForModel } from "../../shared/subagent/thinking-level.js"; import { compactText, formatModelWithThinking, previewLines, resultText, } from "../../shared/tui/tool-format.js"; import { toolCall, toolHeader, toolResult, } from "../../shared/tui/tool-render.js"; import { loadConfig } from "../core/config.js"; import { DEFAULT_GIT_WORKFLOW_POLICY } from "../git/policy.js"; import { collectTapdReviewContext } from "./context.js"; import { resolveReviewModel, resolveReviewThinkingLevel } from "./model-config.js"; import { buildReviewTask } from "./prompt.js"; import { runReviewSubagent } from "./subagent.js"; import type { TapdReviewMetadata, TapdReviewScope, TapdReviewToolDetails, } from "./types.js"; export interface ReviewToolParams { scope?: TapdReviewScope; baseRef?: string; instructions?: string; } function previewToolCall(name: string, args: Record): string { const path = String(args.path ?? args.file_path ?? "."); if (name === "grep") return `grep /${String(args.pattern ?? "")}/ in ${path}`; if (name === "find") return `find ${String(args.pattern ?? "*")} in ${path}`; return `${name} ${path}`; } function reportRisk(report: string): string { return ( report.match(/总体风险[::]\s*(LOW|MEDIUM|HIGH|BLOCKED)/)?.[1] ?? "UNKNOWN" ); } function reviewHandle(details: TapdReviewToolDetails): string { return details.reusable && details.subagentId ? ` · #${details.subagentId.slice(0, 8)} · turn ${details.turn ?? 0}` : ""; } export async function executeTapdReview( _toolCallId: string, params: ReviewToolParams, signal: AbortSignal | undefined, onUpdate: | ((partial: { content: Array<{ type: "text"; text: string }>; details: TapdReviewToolDetails; }) => void) | undefined, ctx: ExtensionContext, ) { const config = loadConfig(); if (!config) throw new Error("请先配置 ~/.pi/agent/tapd.json"); const model = resolveReviewModel(config, ctx.model); const thinkingLevel = thinkingLevelForModel( model, resolveReviewThinkingLevel(config, ctx.thinkingLevel), ctx.modelRegistry, ); const scope = params.scope ?? "branch"; const baseRef = params.baseRef?.trim() || DEFAULT_GIT_WORKFLOW_POLICY.baseRef; const toolCalls: Array<{ name: string; arguments: Record; }> = []; const emit = (phase: string) => { const recent = toolCalls .slice(-6) .map((call) => `→ ${previewToolCall(call.name, call.arguments)}`); onUpdate?.({ content: [{ type: "text", text: [phase, ...recent].join("\n") }], details: { running: true, phase, model, thinkingLevel, toolCalls: [...toolCalls], }, }); }; let reviewContext: | Awaited> | undefined; try { reviewContext = await collectTapdReviewContext( ctx, scope, baseRef, (_stage, _state, message) => emit(message), ); if (signal?.aborted) throw new Error("TAPD Review 已取消"); emit(`Review 子代理运行中:${model}`); const releaseSlot = await acquireSubagentSlot( signal ?? new AbortController().signal, ); let result: Awaited>; try { result = await runReviewSubagent({ cwd: reviewContext.repositoryRoot, model, thinkingLevel, task: buildReviewTask(reviewContext, params.instructions), projectTrusted: ctx.isProjectTrusted(), parentSessionId: ctx.sessionManager.getSessionId(), artifactFiles: [reviewContext.contextFile], signal, onToolCall: (name, args) => { toolCalls.push({ name, arguments: args }); emit("Review 子代理正在检查代码"); }, }); } finally { releaseSlot(); } const metadata: TapdReviewMetadata = { storyId: reviewContext.storyId, scope: reviewContext.scope, baseRef: reviewContext.baseRef, mergeBase: reviewContext.mergeBase, comparisonRef: reviewContext.comparisonRef, branch: reviewContext.branch, model: result.model, changedFiles: reviewContext.changedFiles, generatedAt: new Date().toISOString(), }; const details: TapdReviewToolDetails = { running: false, phase: "审核完成", model: result.model, thinkingLevel, toolCalls: result.toolCalls, subagentId: result.subagentId, reusable: result.reusable, turn: result.turn, report: result.report, metadata, }; const handle = result.reusable && result.subagentId ? `\n\nReusable subagentId: ${result.subagentId} (turn ${result.turn}).` : ""; return { content: [ { type: "text" as const, text: `${result.report}${handle}` }, ], details, }; } finally { await reviewContext?.cleanup(); } } export function renderTapdReviewCall(args: ReviewToolParams, theme: Theme) { const range = args.scope === "uncommitted" ? "未提交修改" : args.baseRef || DEFAULT_GIT_WORKFLOW_POLICY.baseRef; return toolCall(theme, "tapd_review", range); } export function renderTapdReviewResult( result: AgentToolResult, { expanded }: ToolRenderResultOptions, theme: Theme, context: { isError: boolean }, ) { const details = result.details as TapdReviewToolDetails | undefined; if (context.isError || !details) { const error = resultText(result.content, "Review failed"); return toolResult(theme, { status: "error", title: "TAPD review", summary: compactText(error, 100), body: expanded ? error : undefined, hint: error.length > 100 ? "Ctrl+O to expand error" : undefined, }); } if (details.running) { const visibleCalls = expanded ? details.toolCalls : details.toolCalls.slice(-6); const calls = visibleCalls.map( (call) => `→ ${previewToolCall(call.name, call.arguments)}`, ); return toolResult(theme, { status: "active", title: "reviewing", summary: formatModelWithThinking( details.model, details.thinkingLevel, ), details: [details.phase], body: calls.length > 0 ? calls.join("\n") : undefined, }); } const report = details.report ?? "(no report)"; const view = { status: "success" as const, title: "TAPD review", summary: `risk:${reportRisk(report)} · ${formatModelWithThinking(details.model, details.thinkingLevel)}${reviewHandle(details)}`, }; if (!expanded) { const preview = previewLines(report, 14); return toolResult(theme, { ...view, body: preview.text, hint: preview.truncated ? "Ctrl+O to expand full report" : undefined, }); } const container = new Container(); container.addChild(new Text(toolHeader(theme, view), 0, 0)); container.addChild(new Spacer(1)); container.addChild(new Markdown(report, 0, 0, getMarkdownTheme())); return container; }