Agent-JAE/packages/web-ui/src/tools/diff-viewer.ts

98 lines
3.9 KiB
TypeScript

import type { AgentTool } from "@jaeswift/jae-agent-core";
import { Type } from "@sinclair/typebox";
import type { ToolResultMessage } from "@jaeswift/jae-ai";
import { html } from "lit";
import { GitCompare } from "lucide";
import { registerToolRenderer, renderHeader } from "./renderer-registry.js";
import type { ToolRenderer, ToolRenderResult } from "./types.js";
export interface DiffDetails {
original: string;
modified: string;
filename?: string;
}
interface DiffParams {
original: string;
modified: string;
filename?: string;
}
const diffSchema = Type.Object({
original: Type.String({ description: "Original file content" }),
modified: Type.String({ description: "Modified file content" }),
filename: Type.Optional(Type.String({ description: "Filename for display" })),
});
function computeLineDiff(original: string, modified: string): Array<{ type: "add" | "remove" | "same"; line: string }> {
const oldLines = original.split("\n");
const newLines = modified.split("\n");
const result: Array<{ type: "add" | "remove" | "same"; line: string }> = [];
const maxLen = Math.max(oldLines.length, newLines.length);
for (let i = 0; i < maxLen; i++) {
if (i >= oldLines.length) { result.push({ type: "add", line: newLines[i] }); }
else if (i >= newLines.length) { result.push({ type: "remove", line: oldLines[i] }); }
else if (oldLines[i] === newLines[i]) { result.push({ type: "same", line: oldLines[i] }); }
else {
result.push({ type: "remove", line: oldLines[i] });
result.push({ type: "add", line: newLines[i] });
}
}
return result;
}
export const diffTool: AgentTool<typeof diffSchema, DiffDetails> = {
name: "show_diff",
label: "Show Diff",
description: "Show a diff between two versions of code or text",
parameters: diffSchema,
async execute(toolCallId, params, signal) {
return {
content: [{ type: "text", text: `Diff shown for: ${params.filename || "file"}` }],
details: { original: params.original, modified: params.modified, filename: params.filename },
};
},
};
class DiffRenderer implements ToolRenderer<DiffParams, DiffDetails> {
render(params: DiffParams | undefined, result: ToolResultMessage<DiffDetails> | undefined): ToolRenderResult {
const state = result ? (result.isError ? "error" : "complete") : "inprogress";
if (!result?.details) {
return { content: renderHeader(state, GitCompare, `Diff: ${params?.filename || "file"}`), isCustom: false };
}
const { original, modified, filename } = result.details;
const diffLines = computeLineDiff(original, modified);
const adds = diffLines.filter(l => l.type === "add").length;
const removes = diffLines.filter(l => l.type === "remove").length;
return {
content: html`
<div class="flex flex-col gap-3">
${renderHeader(state, GitCompare, html`Diff: ${filename || "file"} <span class="text-green-500 ml-2">+${adds}</span><span class="text-red-500 ml-1">-${removes}</span>`)}
<div class="rounded border border-border overflow-auto max-h-96 text-xs font-mono">
${diffLines.map((l, i) => html`
<div class="flex gap-0 ${
l.type === "add" ? "bg-green-500/10 text-green-700 dark:text-green-400" :
l.type === "remove" ? "bg-red-500/10 text-red-700 dark:text-red-400" :
"text-muted-foreground"
}">
<span class="w-6 text-center shrink-0 select-none border-r border-border px-1">${i + 1}</span>
<span class="px-2 whitespace-pre">${
l.type === "add" ? "+ " : l.type === "remove" ? "- " : " "
}${l.line}</span>
</div>
`)}
</div>
</div>
`,
isCustom: false,
};
}
}
registerToolRenderer("show_diff", new DiffRenderer());
export function createDiffTool(): AgentTool<typeof diffSchema, DiffDetails> {
return diffTool;
}