import React, { useState, useEffect, useCallback } from "react"; import { MultiFileDiff } from "@pierre/diffs/react"; import type { FileContents, SupportedLanguages, ThemeTypes, ThemesType } from "@pierre/diffs"; import { LLMContent } from "../types"; import { isDarkModeActive } from "../services/theme"; // LocalStorage key for side-by-side preference const STORAGE_KEY_SIDE_BY_SIDE = "shelley-diff-side-by-side"; // Get saved side-by-side preference (default: true for desktop) function getSideBySidePreference(): boolean { try { const stored = localStorage.getItem(STORAGE_KEY_SIDE_BY_SIDE); if (stored !== null) { return stored === "true"; } // Default to side-by-side on desktop, inline on mobile return window.innerWidth >= 768; } catch { return window.innerWidth >= 768; } } function setSideBySidePreference(value: boolean): void { try { localStorage.setItem(STORAGE_KEY_SIDE_BY_SIDE, value ? "true" : "false"); } catch { // Ignore storage errors } } // Display data structure from the patch tool interface PatchDisplayData { path: string; oldContent: string; newContent: string; diff: string; } interface PatchToolProps { // For tool_use (pending state) toolInput?: unknown; isRunning?: boolean; // For tool_result (completed state) toolResult?: LLMContent[]; hasError?: boolean; executionTime?: string; display?: unknown; // Display data from the tool_result Content (contains the diff or structured data) onCommentTextChange?: (text: string) => void; } // Map file extension to language for syntax highlighting function getLanguageFromPath(path: string): SupportedLanguages { const ext = path.split(".").pop()?.toLowerCase() || ""; const langMap: Record = { ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx", py: "python", rb: "ruby", go: "go", rs: "rust", java: "java", c: "c", cpp: "cpp", h: "c", hpp: "cpp", cs: "csharp", php: "php", swift: "swift", kt: "kotlin", scala: "scala", sh: "bash", bash: "bash", zsh: "bash", fish: "fish", ps1: "powershell", sql: "sql", html: "html", htm: "html", css: "css", scss: "scss", sass: "sass", less: "less", json: "json", xml: "xml", yaml: "yaml", yml: "yaml", toml: "toml", ini: "ini", md: "markdown", markdown: "markdown", txt: "text", dockerfile: "dockerfile", makefile: "makefile", cmake: "cmake", lua: "lua", perl: "perl", r: "r", vue: "vue", svelte: "svelte", astro: "astro", }; return langMap[ext] || "text"; } // Diff view component using @pierre/diffs function DiffView({ displayData, sideBySide, }: { displayData: PatchDisplayData; sideBySide: boolean; }) { const [themeType, setThemeType] = useState(isDarkModeActive() ? "dark" : "light"); // Listen for theme changes useEffect(() => { const updateTheme = () => { setThemeType(isDarkModeActive() ? "dark" : "light"); }; const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.attributeName === "class") { updateTheme(); } } }); observer.observe(document.documentElement, { attributes: true }); return () => observer.disconnect(); }, []); const lang = getLanguageFromPath(displayData.path); const oldFile: FileContents = { name: displayData.path, contents: displayData.oldContent, lang, }; const newFile: FileContents = { name: displayData.path, contents: displayData.newContent, lang, }; const theme: ThemesType = { dark: "github-dark", light: "github-light", }; return (
); } // Side-by-side toggle icon component function DiffModeToggle({ sideBySide, onToggle }: { sideBySide: boolean; onToggle: () => void }) { return ( ); } function PatchTool({ toolInput, isRunning, toolResult, hasError, display }: PatchToolProps) { // Default to collapsed for errors (since agents typically recover), expanded otherwise const [isExpanded, setIsExpanded] = useState(!hasError); const [isMobile, setIsMobile] = useState(window.innerWidth < 768); const [sideBySide, setSideBySide] = useState(() => !isMobile && getSideBySidePreference()); // Track viewport size useEffect(() => { const handleResize = () => { const mobile = window.innerWidth < 768; setIsMobile(mobile); // Force unified view on mobile if (mobile) { setSideBySide(false); } }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); // Toggle side-by-side mode const toggleSideBySide = useCallback(() => { const newValue = !sideBySide; setSideBySide(newValue); setSideBySidePreference(newValue); }, [sideBySide]); // Extract path from toolInput const path = typeof toolInput === "object" && toolInput !== null && "path" in toolInput && typeof toolInput.path === "string" ? toolInput.path : typeof toolInput === "string" ? toolInput : ""; // Parse display data (structured format from backend) const displayData: PatchDisplayData | null = display && typeof display === "object" && "path" in display && "oldContent" in display && "newContent" in display ? (display as PatchDisplayData) : null; // Extract error message from toolResult if present const errorMessage = toolResult && toolResult.length > 0 && toolResult[0].Text ? toolResult[0].Text : ""; const isComplete = !isRunning && toolResult !== undefined; // Extract filename from path or diff headers const filename = displayData?.path || path || "patch"; // Show toggle only on desktop when expanded and complete with diff data const showDiffToggle = !isMobile && isExpanded && isComplete && !hasError && displayData; return (
setIsExpanded(!isExpanded)}>
🖋️ {filename} {isComplete && hasError && } {isComplete && !hasError && }
{showDiffToggle && }
{isExpanded && (
{isComplete && !hasError && displayData && (
)} {isComplete && hasError && (
{errorMessage || "Patch failed"}
)} {isRunning && (
Applying patch...
)}
)}
); } export default PatchTool;