import { amaEventToRelayEvent } from "../components/RelayRuntimeProvider"; import type { RelayEvent } from "../hooks/useSessionRelay"; export type CodeChangeKind = "edit" | "write" | "read" | "bash" | "diff" | "test"; export type CodeChangeStatus = "running" | "done"; export interface CodeChangeActivity { id: string; kind: CodeChangeKind; status: CodeChangeStatus; title: string; subject: string; detail: string | null; timestamp: string; } interface ToolState extends CodeChangeActivity { toolCallId: string; } const CODE_BASH_RE = /\b(git\s+(status|diff|show|log|add|commit|push)|gh\s+pr|pnpm\s+.*(test|vitest|typecheck|build)|npm\s+(test|run)|npx\s+vitest|vitest|tsc\b)/i; const TEST_BASH_RE = /\b(test|vitest|typecheck|tsc|build)\b/i; const DIFF_BASH_RE = /\bgit\s+(diff|status|show)\b/i; export function amaRuntimeEventsToRelayEvents(events: Record[]): RelayEvent[] { return events.flatMap((event) => { try { return [amaEventToRelayEvent(event)]; } catch { return []; } }); } export function extractCodeChangeActivity(events: RelayEvent[], limit = 8): CodeChangeActivity[] { const tools = new Map(); const ordered: ToolState[] = []; function upsertTool(block: Record, timestamp: string, fallbackId: string) { const toolCallId = stringValue(block.id); if (!toolCallId) return; const name = normalizeToolName(stringValue(block.name)); const input = objectValue(block.input) ?? {}; const item = codeActivityFromTool(toolCallId, name, input, timestamp, fallbackId); if (!item) return; const existing = tools.get(toolCallId); if (existing) { Object.assign(existing, item, { status: existing.status }); return; } const tool = { ...item, toolCallId }; tools.set(toolCallId, tool); ordered.push(tool); } function finishTool(block: Record) { const toolCallId = stringValue(block.tool_use_id); if (!toolCallId) return; const existing = tools.get(toolCallId); if (!existing) return; existing.status = "done"; existing.detail = summarizeResult(stringValue(block.output)) ?? existing.detail; } for (const event of events) { const timestamp = event.timestamp; const payload = event.event as Record; if (payload.type === "block.start" || payload.type === "block.done") { const block = objectValue(payload.block); if (!block) continue; if (block.type === "tool_use") upsertTool(block, timestamp, event.id); if (block.type === "tool_result") finishTool(block); continue; } if (payload.type === "message") { const blocks = Array.isArray(payload.blocks) ? payload.blocks : []; for (const rawBlock of blocks) { const block = objectValue(rawBlock); if (!block) continue; if (block.type === "tool_use") upsertTool(block, timestamp, event.id); if (block.type === "tool_result") finishTool(block); } } } return ordered.slice(-limit).reverse(); } function codeActivityFromTool( toolCallId: string, name: string, input: Record, timestamp: string, fallbackId: string, ): CodeChangeActivity | null { if (name === "edit" || name === "multiedit") { const filePath = filePathFromInput(input); if (!filePath) return null; const edits = Array.isArray(input.edits) ? input.edits.length : 1; return { id: `${fallbackId}:${toolCallId}`, kind: "edit", status: "running", title: edits > 1 ? `Editing ${edits} hunks` : "Editing file", subject: filePath, detail: null, timestamp, }; } if (name === "write") { const filePath = filePathFromInput(input); if (!filePath) return null; return { id: `${fallbackId}:${toolCallId}`, kind: "write", status: "running", title: "Writing file", subject: filePath, detail: null, timestamp, }; } if (name === "read") { const filePath = filePathFromInput(input); if (!filePath) return null; return { id: `${fallbackId}:${toolCallId}`, kind: "read", status: "running", title: "Reading file", subject: filePath, detail: null, timestamp, }; } if (name === "bash") { const command = stringValue(input.command); if (!command || !CODE_BASH_RE.test(command)) return null; const kind = TEST_BASH_RE.test(command) ? "test" : DIFF_BASH_RE.test(command) ? "diff" : "bash"; return { id: `${fallbackId}:${toolCallId}`, kind, status: "running", title: kind === "test" ? "Running check" : kind === "diff" ? "Inspecting diff" : "Running command", subject: command, detail: null, timestamp, }; } return null; } function filePathFromInput(input: Record): string | null { return stringValue(input.filePath) ?? stringValue(input.path) ?? stringValue(input.notebookPath); } function normalizeToolName(name: string | null): string { return (name ?? "").replace(/[^a-z]/gi, "").toLowerCase(); } function summarizeResult(output: string | null): string | null { if (!output) return null; const lines = output .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); if (lines.length === 0) return null; return lines.slice(0, 2).join(" / ").slice(0, 180); } function objectValue(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : null; } function stringValue(value: unknown): string | null { if (typeof value === "string" && value.length > 0) return value; if (typeof value === "number") return String(value); return null; }