|
/** |
|
* tuicr Extension |
|
* |
|
* Registers a `tuicr` tool that the LLM can call to launch the tuicr TUI |
|
* for interactive code review. Suspends the pi TUI and runs tuicr full-screen |
|
* with terminal access. Captures exported instructions via --stdout. |
|
*/ |
|
|
|
import { spawnSync, execSync } from "node:child_process"; |
|
import { mkdtempSync, readFileSync, unlinkSync, existsSync } from "node:fs"; |
|
import { join } from "node:path"; |
|
import { tmpdir } from "node:os"; |
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; |
|
import { Type } from "@sinclair/typebox"; |
|
|
|
export default function (pi: ExtensionAPI) { |
|
pi.registerTool({ |
|
name: "tuicr", |
|
label: "tuicr", |
|
description: |
|
"Launch tuicr TUI for interactive code review. The user reviews diffs, leaves comments, and exports structured feedback. Use when the user wants to interactively review code changes or AI-generated edits.", |
|
parameters: Type.Object({ |
|
directory: Type.Optional(Type.String({ description: "Git repository directory to review (default: cwd)" })), |
|
revisions: Type.Optional(Type.String({ description: "Commit range/revset to review (passed as -r flag)" })), |
|
}), |
|
|
|
async execute(toolCallId, params, signal, onUpdate, ctx) { |
|
// Check tuicr is installed |
|
try { |
|
execSync("command -v tuicr", { stdio: "ignore" }); |
|
} catch { |
|
return { |
|
content: [ |
|
{ |
|
type: "text", |
|
text: "tuicr is not installed. Install via: nix run github:agavra/tuicr, cargo install tuicr, or brew install agavra/tap/tuicr", |
|
}, |
|
], |
|
}; |
|
} |
|
|
|
const targetDir = params.directory || ctx.cwd; |
|
|
|
// Check it's a git repo |
|
try { |
|
execSync("git rev-parse --git-dir", { cwd: targetDir, stdio: "ignore" }); |
|
} catch { |
|
return { |
|
content: [{ type: "text", text: `Not a git repository: ${targetDir}` }], |
|
}; |
|
} |
|
|
|
if (!ctx.hasUI) { |
|
return { |
|
content: [{ type: "text", text: "(tuicr requires interactive TUI mode)" }], |
|
}; |
|
} |
|
|
|
// Build tuicr args |
|
const tuicrArgs: string[] = []; |
|
if (params.revisions) { |
|
tuicrArgs.push("-r", params.revisions); |
|
} |
|
|
|
// Check if --stdout is supported for output capture |
|
let hasStdout = false; |
|
try { |
|
const help = execSync("tuicr --help 2>&1", { encoding: "utf-8" }); |
|
hasStdout = help.includes("--stdout"); |
|
} catch {} |
|
|
|
// With --stdout, tuicr writes export to stdout instead of clipboard. |
|
// We redirect stdout to a file and give tuicr /dev/tty for its TUI via fd 3. |
|
const outputFile = hasStdout ? join(mkdtempSync(join(tmpdir(), "tuicr-")), "output.md") : null; |
|
if (hasStdout) { |
|
tuicrArgs.push("--stdout"); |
|
} |
|
|
|
// Run tuicr with full terminal access |
|
const exitCode = await ctx.ui.custom<number | null>((tui, _theme, _kb, done) => { |
|
tui.stop(); |
|
process.stdout.write("\x1b[2J\x1b[H"); |
|
|
|
let result; |
|
if (hasStdout && outputFile) { |
|
const { openSync, closeSync } = require("node:fs"); |
|
const fd = openSync(outputFile, "w"); |
|
result = spawnSync("tuicr", tuicrArgs, { |
|
// stdin: terminal, stdout: file (export), stderr: terminal |
|
stdio: ["inherit", fd, "inherit"], |
|
env: process.env, |
|
cwd: targetDir, |
|
}); |
|
closeSync(fd); |
|
} else { |
|
result = spawnSync("tuicr", tuicrArgs, { |
|
stdio: "inherit", |
|
env: process.env, |
|
cwd: targetDir, |
|
}); |
|
} |
|
|
|
tui.start(); |
|
tui.requestRender(true); |
|
done(result.status); |
|
|
|
return { render: () => [], invalidate: () => {} }; |
|
}); |
|
|
|
// Read captured instructions |
|
let instructions = ""; |
|
if (outputFile && existsSync(outputFile)) { |
|
try { |
|
const content = readFileSync(outputFile, "utf-8").trim(); |
|
// Filter out terminal escape sequences (TUI noise) |
|
const clean = content.replace(/\x1b\[[^a-zA-Z]*[a-zA-Z]/g, "").replace(/\x1b[^\x1b]{0,8}/g, "").trim(); |
|
if (clean) { |
|
instructions = clean; |
|
} |
|
unlinkSync(outputFile); |
|
} catch {} |
|
} |
|
|
|
if (instructions) { |
|
return { |
|
content: [ |
|
{ |
|
type: "text", |
|
text: `tuicr review completed. The user exported the following instructions:\n\n${instructions}\n\nPlease address the comments above.`, |
|
}, |
|
], |
|
}; |
|
} |
|
|
|
return { |
|
content: [ |
|
{ |
|
type: "text", |
|
text: "tuicr review completed. No instructions were exported. If the user exported to clipboard, they can paste the instructions here.", |
|
}, |
|
], |
|
}; |
|
}, |
|
}); |
|
} |