import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, type ToolDefinition, } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import type { TSchema } from "typebox"; import { getCursorSessionCwd } from "./session-scope.js"; import { isCursorReplayToolName, type BuiltinNativeCursorToolName, type NativeCursorToolName, } from "./tool-presentation-registry.js"; import { createCursorReplayOnlyToolDefinition, isCursorReplayNativeEditDetails, isCursorReplayNativeWriteDetails, parseCursorReplayToolDetails, renderCursorReplayResult, renderNativeLookingCursorFileMutationCall, renderNativeLookingCursorReadReplayResult, } from "./native-tool-display-replay.js"; import { consumeCursorNativeToolDisplay, isCursorReplayToolCallId, } from "./native-tool-display-state.js"; type AnyToolDefinition = ToolDefinition; type RenderCall = NonNullable; type RenderResult = NonNullable; type NativeReplayStrategy = { createDefinition: (cwd: string) => AnyToolDefinition; missingReplayPolicy?: "block-file-mutation"; renderReplayCall?: ( args: Parameters[0], theme: Parameters[1], context: Parameters[2], renderBase: () => ReturnType, ) => ReturnType; renderReplayResult?: ( result: Parameters[0], options: Parameters[1], theme: Parameters[2], context: Parameters[3], renderBase: () => ReturnType, ) => ReturnType; }; function emptyText(): Text { return new Text("", 0, 0); } function renderReadReplayCall( args: Parameters[0], theme: Parameters[1], context: Parameters[2], renderBase: () => ReturnType, ): ReturnType { const rendered = renderBase(); if ((args as Record).localReadPreview !== true || context.expanded) return rendered; const baseText = rendered.render(120).join("\n").trimEnd(); const labeled = `${baseText}${theme.fg("muted", " ยท local file preview")}`; if (rendered instanceof Text) { rendered.setText(labeled); return rendered; } return new Text(labeled, 0, 0); } function renderReadReplayResult( result: Parameters[0], options: Parameters[1], theme: Parameters[2], context: Parameters[3], renderBase: () => ReturnType, ): ReturnType { return renderNativeLookingCursorReadReplayResult( result, options, theme, context as Parameters[3], renderBase, ); } function renderEditReplayResult( result: Parameters[0], options: Parameters[1], theme: Parameters[2], context: Parameters[3], renderBase: () => ReturnType, ): ReturnType { const details = parseCursorReplayToolDetails(result.details); return details && isCursorReplayNativeEditDetails(details) ? renderCursorReplayResult(result, options, theme, context as Parameters[3], context.isError) : renderBase(); } function renderWriteReplayResult( result: Parameters[0], options: Parameters[1], theme: Parameters[2], context: Parameters[3], renderBase: () => ReturnType, ): ReturnType { const details = parseCursorReplayToolDetails(result.details); return details && isCursorReplayNativeWriteDetails(details) ? renderCursorReplayResult(result, options, theme, context as Parameters[3], context.isError) : renderBase(); } const NATIVE_CURSOR_TOOL_STRATEGIES: Record = { read: { createDefinition: (cwd) => createReadToolDefinition(cwd) as AnyToolDefinition, renderReplayCall: renderReadReplayCall, renderReplayResult: renderReadReplayResult, }, bash: { createDefinition: (cwd) => createBashToolDefinition(cwd) as AnyToolDefinition }, edit: { createDefinition: (cwd) => createEditToolDefinition(cwd) as AnyToolDefinition, missingReplayPolicy: "block-file-mutation", renderReplayCall: (args, theme, context) => renderNativeLookingCursorFileMutationCall("edit", args as Record, theme, context.isPartial), renderReplayResult: renderEditReplayResult, }, write: { createDefinition: (cwd) => createWriteToolDefinition(cwd) as AnyToolDefinition, missingReplayPolicy: "block-file-mutation", renderReplayCall: (args, theme, context) => renderNativeLookingCursorFileMutationCall("write", args as Record, theme, context.isPartial), renderReplayResult: renderWriteReplayResult, }, grep: { createDefinition: (cwd) => createGrepToolDefinition(cwd) as AnyToolDefinition }, find: { createDefinition: (cwd) => createFindToolDefinition(cwd) as AnyToolDefinition }, ls: { createDefinition: (cwd) => createLsToolDefinition(cwd) as AnyToolDefinition }, }; function getNativeReplayStrategy(toolName: string): NativeReplayStrategy | undefined { return Object.hasOwn(NATIVE_CURSOR_TOOL_STRATEGIES, toolName) ? NATIVE_CURSOR_TOOL_STRATEGIES[toolName as BuiltinNativeCursorToolName] : undefined; } export function wrapNativeCursorTool( definition: ToolDefinition, getCurrentDefinition: () => ToolDefinition, ): ToolDefinition { const strategy = getNativeReplayStrategy(definition.name); return { ...definition, async execute(toolCallId, params, signal, onUpdate, ctx) { const cursorDisplay = consumeCursorNativeToolDisplay(toolCallId); if (cursorDisplay) { if (cursorDisplay.isError) { const text = cursorDisplay.result.content .map((entry) => (entry.type === "text" ? entry.text : undefined)) .filter((entry): entry is string => Boolean(entry)) .join("\n"); throw new Error(text || "Cursor tool replay failed"); } return { content: cursorDisplay.result.content, details: cursorDisplay.result.details as TDetails, terminate: cursorDisplay.terminate ?? true, }; } if (strategy?.missingReplayPolicy === "block-file-mutation" && isCursorReplayToolCallId(toolCallId)) { throw new Error(`No recorded Cursor ${definition.name} result was available. This replay-only call does not execute file mutations.`); } return getCurrentDefinition().execute(toolCallId, params, signal, onUpdate, ctx); }, renderCall(args, theme, context) { const currentRenderCall = getCurrentDefinition().renderCall; const renderBase = () => currentRenderCall?.(args, theme, context) ?? emptyText(); const isReplayCall = typeof context.toolCallId === "string" && isCursorReplayToolCallId(context.toolCallId); if (isReplayCall && strategy?.renderReplayCall) { return strategy.renderReplayCall(args, theme, context, renderBase) as ReturnType["renderCall"]>>; } return renderBase(); }, renderResult(result, options, theme, context) { const currentRenderResult = getCurrentDefinition().renderResult; const renderBase = () => currentRenderResult?.(result, options, theme, context) ?? emptyText(); const isReplayCall = typeof context.toolCallId === "string" && isCursorReplayToolCallId(context.toolCallId); if (isReplayCall && strategy?.renderReplayResult) { return strategy.renderReplayResult(result, options, theme, context, renderBase) as ReturnType["renderResult"]>>; } return renderBase(); }, }; } export function createNativeCursorToolDefinition(toolName: NativeCursorToolName, cwd: string): ToolDefinition { const strategy = getNativeReplayStrategy(toolName); if (strategy) return strategy.createDefinition(cwd); if (isCursorReplayToolName(toolName)) return createCursorReplayOnlyToolDefinition(toolName) as ToolDefinition; throw new Error(`Unsupported Cursor native replay tool: ${toolName}`); } export function registerNativeCursorTool( pi: Pick, toolName: NativeCursorToolName, ): void { const definition = createNativeCursorToolDefinition(toolName, getCursorSessionCwd()); pi.registerTool(wrapNativeCursorTool(definition, () => createNativeCursorToolDefinition(toolName, getCursorSessionCwd()))); }