"use client"; import { useState } from "react"; import { Terminal, FileCode, Loader2, CheckCircle, XCircle, ChevronDown, ChevronUp, MessageSquare, FileText, CloudDownload, Search, } from "lucide-react"; import { createApplyPatchToolFields, createShellToolFields, createInstallDependenciesToolFields, formatRgCommand, RipgrepCommand, } from "@open-swe/shared/open-swe/tools"; import { z } from "zod"; // Used only for Zod type inference. const dummyRepo = { owner: "dummy", repo: "dummy" }; const shellTool = createShellToolFields(dummyRepo); type ShellToolArgs = z.infer; const applyPatchTool = createApplyPatchToolFields(dummyRepo); type ApplyPatchToolArgs = z.infer; const installDependenciesTool = createInstallDependenciesToolFields(dummyRepo); type InstallDependenciesToolArgs = z.infer< typeof installDependenciesTool.schema >; // Common props for all action types type BaseActionProps = { status: "loading" | "generating" | "done"; success?: boolean; reasoningText?: string; summaryText?: string; }; // Shell command specific props. We need to wrap the args in Partial<...> // because even though they're required, they may be undefined at a point in time // due to streaming. type ShellActionProps = BaseActionProps & Partial & { actionType: "shell"; output?: string; errorCode?: number; }; type PatchActionProps = BaseActionProps & Partial & { actionType: "apply-patch"; errorMessage?: string; fixedDiff?: string; }; type RgActionProps = BaseActionProps & Partial & { actionType: "rg"; output?: string; errorCode?: number; }; type InstallDependenciesActionProps = BaseActionProps & Partial & { actionType: "install_dependencies"; output?: string; errorCode?: number; }; export type ActionItemProps = | (BaseActionProps & { status: "loading" }) | ShellActionProps | PatchActionProps | RgActionProps | InstallDependenciesActionProps; export type ActionStepProps = { actions: ActionItemProps[]; reasoningText?: string; summaryText?: string; }; function ActionItem(props: ActionItemProps) { const [expanded, setExpanded] = useState(false); const getStatusIcon = () => { switch (props.status) { case "loading": return
; case "generating": return ( ); case "done": return props.success ? ( ) : ( ); } }; const getStatusText = () => { if (props.status === "loading") { return "Preparing action..."; } if (props.status === "generating") { return props.actionType === "shell" ? "Executing..." : "Applying patch..."; } if (props.status === "done") { if (props.actionType === "shell") { return props.success ? "Command completed" : "Command failed"; } else if (props.actionType === "apply-patch") { return props.success ? "Patch applied" : "Patch failed"; } else if (props.actionType === "rg") { return props.success ? "Search completed" : "Search failed"; } else if (props.actionType === "install_dependencies") { return props.success ? "Dependencies installed" : "Installation failed"; } } return ""; }; // Determine if we should show the content toggle button const shouldShowToggle = () => { if (props.status !== "done") return false; if ( props.actionType === "shell" || props.actionType === "rg" || props.actionType === "install_dependencies" ) { return !!props.output; } else if (props.actionType === "apply-patch") { return !!props.diff; } return false; }; // Render the header icon based on action type const renderHeaderIcon = () => { if (props.status === "loading" || !("actionType" in props)) { // In loading state, we don't know the type yet, use a generic icon return ; } if (props.actionType === "install_dependencies") { return ; } else if (props.actionType === "apply-patch") { return ; } else if (props.actionType === "rg") { return ; } else { return ; } }; // Render the header content based on action type const renderHeaderContent = () => { if (props.status === "loading" || !("actionType" in props)) { return ( Preparing action... ); } if ( props.actionType === "shell" || props.actionType === "install_dependencies" ) { let commandStr = ""; if (props.command) { if (Array.isArray(props.command)) { commandStr = props.command.join(" "); } else if ( typeof props.command === "string" && (props.command as string).length > 0 ) { try { commandStr = JSON.parse(props.command); } catch { commandStr = props.command; } } } return (
{props.workdir && (
{props.workdir}
)} {commandStr}
); } else if (props.actionType === "rg") { let formattedRgCommand = ""; try { formattedRgCommand = formatRgCommand({ pattern: props.pattern, paths: props.paths, flags: props.flags, })?.join(" ") ?? ""; } catch { // no-op } return (
{formattedRgCommand}
); } else { return ( {props.file_path} ); } }; // Render the content based on action type const renderContent = () => { if (props.status !== "done" || !("actionType" in props)) return null; if (!expanded) return null; if ( (props.actionType === "shell" || props.actionType === "rg" || props.actionType === "install_dependencies") && props.output ) { return (
            {props.output}
          
{props.errorCode !== undefined && !props.success && (
Exit code: {props.errorCode}
)}
); } else if (props.actionType === "apply-patch" && props.diff) { return (

          {!props.success && props.errorMessage && (
            
{props.errorMessage}
)} {!props.success && props.fixedDiff && (
Fixed diff:
            
)}
); } return null; }; return (
{renderHeaderIcon()} {renderHeaderContent()}
{getStatusText()} {getStatusIcon()} {shouldShowToggle() && ( )}
{renderContent()}
); } export function ActionStep(props: ActionStepProps) { const [showReasoning, setShowReasoning] = useState(false); const [showSummary, setShowSummary] = useState(false); const reasoningText = "reasoningText" in props ? props.reasoningText : undefined; const summaryText = "summaryText" in props ? props.summaryText : undefined; const anyActionDone = props.actions.some( (action: ActionItemProps) => action.status === "done", ); return (
{showReasoning && (

{reasoningText || "No reasoning provided."}

)}
{props.actions.map((action: ActionItemProps, index: number) => ( ))}
{summaryText && anyActionDone && (
{showSummary && (

{summaryText}

)}
)}
); } function formatDiff(diff: string) { return diff .split("\n") .map((line) => { if (line.startsWith("+")) { return `${line}`; } else if (line.startsWith("-")) { return `${line}`; } else if (line.startsWith("@")) { return `${line}`; } return line; }) .join("\n"); }