"use client"; import { JSX, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { Terminal, FileText, ChevronDown, ChevronUp, MessageSquare, Search, CheckCircle, XCircle, Loader2, Globe, Zap, FileCode, CloudDownload, } from "lucide-react"; import { BasicMarkdownText } from "../thread/markdown-text"; import { createApplyPatchToolFields, createShellToolFields, createInstallDependenciesToolFields, createScratchpadFields, createGetURLContentToolFields, createGrepToolFields, createSearchDocumentForToolFields, createTextEditorToolFields, } from "@open-swe/shared/open-swe/tools"; import { z } from "zod"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "../ui/tooltip"; import { cn } from "@/lib/utils"; import { ToolIconWithTooltip } from "./tool-icon-tooltip"; // 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 >; const scratchpadTool = createScratchpadFields(""); type ScratchpadToolArgs = z.infer; const getURLContentTool = createGetURLContentToolFields(); type GetURLContentToolArgs = z.infer; const grepTool = createGrepToolFields(dummyRepo); type GrepToolArgs = z.infer; const searchDocumentForTool = createSearchDocumentForToolFields(); type SearchDocumentForToolArgs = z.infer; const textEditorTool = createTextEditorToolFields(dummyRepo, {}); type TextEditorToolArgs = z.infer; // 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 InstallDependenciesActionProps = BaseActionProps & Partial & { actionType: "install_dependencies"; output?: string; errorCode?: number; }; type ScratchpadActionProps = BaseActionProps & Partial & { actionType: "scratchpad"; }; type GetURLContentActionProps = BaseActionProps & Partial & { actionType: "get_url_content"; output?: string; }; type SearchActionProps = BaseActionProps & Partial & { actionType: "grep"; output?: string; errorCode?: number; }; type SearchDocumentForActionProps = BaseActionProps & Partial & { actionType: "search_document_for"; output?: string; }; type TextEditorActionProps = BaseActionProps & Partial & { actionType: "text_editor"; output?: string; }; type McpActionProps = BaseActionProps & { actionType: "mcp"; toolName: string; args: Record; output?: string; }; export type ActionItemProps = | (BaseActionProps & { status: "loading" }) | ShellActionProps | PatchActionProps | InstallDependenciesActionProps | ScratchpadActionProps | GetURLContentActionProps | McpActionProps | SearchActionProps | SearchDocumentForActionProps | TextEditorActionProps; export type ActionStepProps = { actions: ActionItemProps[]; reasoningText?: string; summaryText?: string; }; const ACTION_GENERATING_TEXT_MAP = { [shellTool.name]: "Executing...", [applyPatchTool.name]: "Applying patch...", [installDependenciesTool.name]: "Installing dependencies...", [scratchpadTool.name]: "Saving notes...", [getURLContentTool.name]: "Fetching URL content...", [searchDocumentForTool.name]: "Searching document...", [grepTool.name]: "Searching...", [textEditorTool.name]: "Editing file...", }; function MatchCaseIcon({ matchCase }: { matchCase: boolean }) { return (

Aa

Match case {matchCase ? "on" : "off"}
); } const coerceStringToArray = (str: string | string[]) => { if (Array.isArray(str)) { return str; } try { return JSON.parse(str); } catch { return [str]; } }; 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 = (): string | JSX.Element => { if (props.status === "loading") { return "Preparing action..."; } if (props.status === "generating") { return ACTION_GENERATING_TEXT_MAP[props.actionType]; } 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 === "install_dependencies") { return props.success ? "Dependencies installed" : "Installation failed"; } else if (props.actionType === "scratchpad") { return props.success ? "Scratchpad updated" : "Failed to update scratchpad"; } else if (props.actionType === "get_url_content") { return props.success ? "URL content fetched" : "Failed to fetch URL content"; } else if (props.actionType === "search_document_for") { return props.success ? "Document search completed" : "Document search failed"; } else if (props.actionType === "grep") { return props.success ? "Search completed" : "Search failed"; } else if (props.actionType === "text_editor") { const command = props.command || "unknown"; return props.success ? (

{command}

{" "} command completed
) : (

{command}

{" "} command failed
); } else if (props.actionType === "mcp") { return props.success ? `${props.toolName} completed` : `${props.toolName} failed`; } } return ""; }; const shouldShowToggle = () => { if (props.status !== "done") return false; if ( props.actionType === "shell" || props.actionType === "install_dependencies" || props.actionType === "get_url_content" || props.actionType === "search_document_for" || props.actionType === "grep" || props.actionType === "text_editor" ) { return !!props.output; } else if (props.actionType === "apply-patch") { return !!props.diff; } else if (props.actionType === "scratchpad") { return !!(props.scratchpad && props.scratchpad.length > 0); } else if (props.actionType === "mcp") { const hasArgs = props.args && Object.keys(props.args).length > 0; const hasOutput = !!props.output; return hasArgs || hasOutput; } return false; }; // Render the header icon based on action type const renderHeaderIcon = () => { const defaultIconStyling = "text-muted-foreground mr-2 size-3.5"; 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 === "scratchpad") { return ( } /> ); } else if (props.actionType === "install_dependencies") { return ( } /> ); } else if (props.actionType === "apply-patch") { return ( } /> ); } else if (props.actionType === "get_url_content") { return ( } /> ); } else if (props.actionType === "search_document_for") { return ( } /> ); } else if (props.actionType === "mcp") { return ( } /> ); } else if (props.actionType === "grep") { return ( } /> ); } else if (props.actionType === "text_editor") { 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 === "scratchpad") { return (
Scratchpad
); } if (props.actionType === "get_url_content") { return (
{props.url}
); } if (props.actionType === "grep") { const castProps = props as SearchActionProps; return (
{castProps.query}
{!castProps.match_string && ( regex )}
{castProps.include_files && ( Include: {castProps.include_files} )} {castProps.exclude_files && ( Exclude: {castProps.exclude_files} )} {castProps.context_lines !== undefined && castProps.context_lines > 0 && ( Context: {castProps.context_lines} lines )} {castProps.max_results !== undefined && castProps.max_results > 0 && ( Max results: {castProps.max_results} )} {castProps.file_types && castProps.file_types.length > 0 && ( File types:{" "} {coerceStringToArray(castProps.file_types).join(", ")} )} {castProps.follow_symlinks && Follow symlinks}
); } if ( props.actionType === "shell" || props.actionType === "install_dependencies" ) { const shellProps = props as | ShellActionProps | InstallDependenciesActionProps; let commandStr = ""; if (shellProps.command) { if (Array.isArray(shellProps.command)) { commandStr = shellProps.command.join(" "); } else if ( typeof shellProps.command === "string" && (shellProps.command as string).length > 0 ) { try { commandStr = JSON.parse(shellProps.command); } catch { commandStr = shellProps.command; } } } return (
{shellProps.workdir && (
{shellProps.workdir}
)} {commandStr}
); } else if (props.actionType === "search_document_for") { return (
{props.query}
{props.url}
); } else if (props.actionType === "text_editor") { const command = props.command || "unknown"; const path = props.path || ""; return (
{command} {path}
{command === "view" && props.view_range && (
Lines {props.view_range[0]}- {props.view_range[1] === -1 ? "end" : props.view_range[1]}
)} {command === "str_replace" && props.old_str && (
Replace: {props.old_str.substring(0, 50)} {props.old_str.length > 50 ? "..." : ""}
)} {command === "insert" && props.insert_line !== undefined && (
Insert at line {props.insert_line}
)}
); } else if (props.actionType === "mcp") { return (
{props.toolName}
); } else { return ( {props.actionType === "apply-patch" && "file_path" in props ? props.file_path : ""} ); } }; // Render the content based on action type const renderContent = () => { if (!("actionType" in props)) return null; if (props.status !== "done") return null; if (!expanded) return null; if ( (props.actionType === "shell" || props.actionType === "grep" || props.actionType === "search_document_for" || props.actionType === "install_dependencies" || props.actionType === "text_editor") && props.output ) { return (
            {props.output}
          
{(props.actionType === "shell" || props.actionType === "grep") && "errorCode" in props && props.errorCode !== undefined && !props.success && (
Exit code: {props.errorCode}
)}
); } else if (props.actionType === "mcp") { const hasArgs = props.args && Object.keys(props.args).length > 0; const hasOutput = !!props.output; if (!hasArgs && !hasOutput) { return null; } return (
{hasArgs && (
Arguments
{Object.entries(props.args).map(([key, value]) => (
{key}
{typeof value === "object" ? ( {JSON.stringify(value, null, 2)} ) : ( {String(value)} )}
))}
)} {hasOutput && ( <>
Output
                {props.output}
              
)}
); } else if (props.actionType === "get_url_content" && props.output) { return (
            {props.output}
          
); } else if (props.actionType === "apply-patch" && props.diff) { return (

          {!props.success && props.errorMessage && (
            
{props.errorMessage}
)} {!props.success && props.fixedDiff && (
Fixed diff:
            
)}
); } else if ( props.actionType === "scratchpad" && props.scratchpad && props.scratchpad.length > 0 ) { return (
    {props.scratchpad.map((note, i) => (
  • {note}
  • ))}
); } return null; }; return (
{renderHeaderIcon()} {renderHeaderContent()}
{getStatusText()} {getStatusIcon()} {shouldShowToggle() && ( )}
{expanded && ( {renderContent()} )}
); } export function ActionStep(props: ActionStepProps) { 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", ); const [showReasoning, setShowReasoning] = useState(!!reasoningText); const [showSummary, setShowSummary] = useState(!!summaryText); 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"); }