2025-06-13 10:28:21 -07:00
|
|
|
"use client";
|
|
|
|
|
|
2025-07-14 16:39:19 -07:00
|
|
|
import { useState } from "react";
|
2025-06-13 10:28:21 -07:00
|
|
|
import {
|
|
|
|
|
Terminal,
|
2025-07-13 13:26:56 -07:00
|
|
|
FileText,
|
2025-06-13 10:28:21 -07:00
|
|
|
ChevronDown,
|
|
|
|
|
ChevronUp,
|
|
|
|
|
MessageSquare,
|
2025-06-22 15:47:29 -07:00
|
|
|
Search,
|
2025-07-13 13:26:56 -07:00
|
|
|
CheckCircle,
|
|
|
|
|
XCircle,
|
|
|
|
|
Loader2,
|
2025-07-08 11:14:55 -04:00
|
|
|
Globe,
|
2025-07-13 13:26:56 -07:00
|
|
|
FileCode,
|
|
|
|
|
CloudDownload,
|
2025-06-13 10:28:21 -07:00
|
|
|
} from "lucide-react";
|
2025-07-14 16:39:19 -07:00
|
|
|
import { BasicMarkdownText } from "../thread/markdown-text";
|
2025-06-19 12:57:27 -07:00
|
|
|
import {
|
|
|
|
|
createApplyPatchToolFields,
|
|
|
|
|
createShellToolFields,
|
2025-06-22 15:47:29 -07:00
|
|
|
createInstallDependenciesToolFields,
|
2025-06-23 17:48:08 -07:00
|
|
|
createTakePlannerNotesFields,
|
2025-07-08 11:14:55 -04:00
|
|
|
createGetURLContentToolFields,
|
2025-07-14 16:39:19 -07:00
|
|
|
createSearchToolFields,
|
2025-06-19 12:57:27 -07:00
|
|
|
} from "@open-swe/shared/open-swe/tools";
|
|
|
|
|
import { z } from "zod";
|
2025-07-09 02:25:38 +00:00
|
|
|
import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
TooltipContent,
|
|
|
|
|
TooltipProvider,
|
|
|
|
|
TooltipTrigger,
|
|
|
|
|
} from "../ui/tooltip";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
2025-07-13 13:26:56 -07:00
|
|
|
import { ToolIconWithTooltip } from "./tool-icon-tooltip";
|
2025-06-19 12:57:27 -07:00
|
|
|
|
|
|
|
|
// Used only for Zod type inference.
|
|
|
|
|
const dummyRepo = { owner: "dummy", repo: "dummy" };
|
|
|
|
|
const shellTool = createShellToolFields(dummyRepo);
|
|
|
|
|
type ShellToolArgs = z.infer<typeof shellTool.schema>;
|
|
|
|
|
const applyPatchTool = createApplyPatchToolFields(dummyRepo);
|
|
|
|
|
type ApplyPatchToolArgs = z.infer<typeof applyPatchTool.schema>;
|
2025-06-22 15:47:29 -07:00
|
|
|
const installDependenciesTool = createInstallDependenciesToolFields(dummyRepo);
|
|
|
|
|
type InstallDependenciesToolArgs = z.infer<
|
|
|
|
|
typeof installDependenciesTool.schema
|
|
|
|
|
>;
|
2025-06-23 17:48:08 -07:00
|
|
|
const plannerNotesTool = createTakePlannerNotesFields();
|
|
|
|
|
type PlannerNotesToolArgs = z.infer<typeof plannerNotesTool.schema>;
|
2025-07-08 11:14:55 -04:00
|
|
|
const getURLContentTool = createGetURLContentToolFields();
|
|
|
|
|
type GetURLContentToolArgs = z.infer<typeof getURLContentTool.schema>;
|
2025-07-14 16:39:19 -07:00
|
|
|
const searchTool = createSearchToolFields(dummyRepo);
|
|
|
|
|
type SearchToolArgs = z.infer<typeof searchTool.schema>;
|
2025-06-13 10:28:21 -07:00
|
|
|
|
|
|
|
|
// Common props for all action types
|
|
|
|
|
type BaseActionProps = {
|
|
|
|
|
status: "loading" | "generating" | "done";
|
|
|
|
|
success?: boolean;
|
|
|
|
|
reasoningText?: string;
|
|
|
|
|
summaryText?: string;
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-19 12:57:27 -07:00
|
|
|
// 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<ShellToolArgs> & {
|
|
|
|
|
actionType: "shell";
|
|
|
|
|
output?: string;
|
|
|
|
|
errorCode?: number;
|
|
|
|
|
};
|
2025-06-13 10:28:21 -07:00
|
|
|
|
2025-06-19 12:57:27 -07:00
|
|
|
type PatchActionProps = BaseActionProps &
|
|
|
|
|
Partial<ApplyPatchToolArgs> & {
|
|
|
|
|
actionType: "apply-patch";
|
|
|
|
|
errorMessage?: string;
|
|
|
|
|
fixedDiff?: string;
|
|
|
|
|
};
|
2025-06-13 10:28:21 -07:00
|
|
|
|
2025-06-22 15:47:29 -07:00
|
|
|
type InstallDependenciesActionProps = BaseActionProps &
|
|
|
|
|
Partial<InstallDependenciesToolArgs> & {
|
|
|
|
|
actionType: "install_dependencies";
|
|
|
|
|
output?: string;
|
|
|
|
|
errorCode?: number;
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-23 17:48:08 -07:00
|
|
|
type PlannerNotesActionProps = BaseActionProps &
|
|
|
|
|
Partial<PlannerNotesToolArgs> & {
|
|
|
|
|
actionType: "planner_notes";
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-08 11:14:55 -04:00
|
|
|
type GetURLContentActionProps = BaseActionProps &
|
|
|
|
|
Partial<GetURLContentToolArgs> & {
|
|
|
|
|
actionType: "get_url_content";
|
|
|
|
|
output?: string;
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-14 16:39:19 -07:00
|
|
|
type SearchActionProps = BaseActionProps &
|
|
|
|
|
Partial<SearchToolArgs> & {
|
|
|
|
|
actionType: "search";
|
2025-07-09 02:25:38 +00:00
|
|
|
output?: string;
|
|
|
|
|
errorCode?: number;
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-22 12:14:56 -07:00
|
|
|
export type ActionItemProps =
|
2025-06-13 10:28:21 -07:00
|
|
|
| (BaseActionProps & { status: "loading" })
|
|
|
|
|
| ShellActionProps
|
2025-06-22 14:04:58 -07:00
|
|
|
| PatchActionProps
|
2025-06-23 17:48:08 -07:00
|
|
|
| InstallDependenciesActionProps
|
2025-07-08 11:14:55 -04:00
|
|
|
| PlannerNotesActionProps
|
2025-07-09 02:25:38 +00:00
|
|
|
| GetURLContentActionProps
|
2025-07-14 16:39:19 -07:00
|
|
|
| SearchActionProps;
|
2025-06-13 10:28:21 -07:00
|
|
|
|
2025-06-22 12:14:56 -07:00
|
|
|
export type ActionStepProps = {
|
|
|
|
|
actions: ActionItemProps[];
|
|
|
|
|
reasoningText?: string;
|
|
|
|
|
summaryText?: string;
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-27 16:35:47 -06:00
|
|
|
const ACTION_GENERATING_TEXT_MAP = {
|
|
|
|
|
[shellTool.name]: "Executing...",
|
|
|
|
|
[applyPatchTool.name]: "Applying patch...",
|
|
|
|
|
[installDependenciesTool.name]: "Installing dependencies...",
|
|
|
|
|
[plannerNotesTool.name]: "Saving notes...",
|
2025-07-08 11:14:55 -04:00
|
|
|
[getURLContentTool.name]: "Fetching URL content...",
|
2025-07-14 16:39:19 -07:00
|
|
|
[searchTool.name]: "Searching...",
|
2025-06-27 16:35:47 -06:00
|
|
|
};
|
|
|
|
|
|
2025-07-09 02:25:38 +00:00
|
|
|
function MatchCaseIcon({ matchCase }: { matchCase: boolean }) {
|
|
|
|
|
return (
|
|
|
|
|
<TooltipProvider>
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger
|
|
|
|
|
className={cn(
|
|
|
|
|
"rounded-sm border border-gray-300 px-1 py-[2px] text-xs dark:border-gray-600",
|
|
|
|
|
matchCase
|
|
|
|
|
? "border-blue-500 bg-blue-500/80 text-white"
|
|
|
|
|
: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<p className="font-mono">Aa</p>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Match case {matchCase ? "on" : "off"}</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</TooltipProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-17 11:52:25 -07:00
|
|
|
const coerceStringToArray = (str: string | string[]) => {
|
|
|
|
|
if (Array.isArray(str)) {
|
|
|
|
|
return str;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(str);
|
|
|
|
|
} catch {
|
|
|
|
|
return [str];
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-07-09 02:25:38 +00:00
|
|
|
|
2025-06-22 12:14:56 -07:00
|
|
|
function ActionItem(props: ActionItemProps) {
|
2025-06-18 00:59:03 +00:00
|
|
|
const [expanded, setExpanded] = useState(false);
|
2025-06-13 10:28:21 -07:00
|
|
|
|
|
|
|
|
const getStatusIcon = () => {
|
|
|
|
|
switch (props.status) {
|
|
|
|
|
case "loading":
|
2025-06-22 12:14:56 -07:00
|
|
|
return <div className="border-border size-3.5 rounded-full border" />;
|
2025-06-13 10:28:21 -07:00
|
|
|
case "generating":
|
2025-06-18 16:44:41 -07:00
|
|
|
return (
|
2025-06-22 12:14:56 -07:00
|
|
|
<Loader2 className="text-muted-foreground size-3.5 animate-spin" />
|
2025-06-18 16:44:41 -07:00
|
|
|
);
|
2025-06-13 10:28:21 -07:00
|
|
|
case "done":
|
|
|
|
|
return props.success ? (
|
2025-06-22 12:14:56 -07:00
|
|
|
<CheckCircle className="size-3.5 text-green-500" />
|
2025-06-13 10:28:21 -07:00
|
|
|
) : (
|
2025-06-22 12:14:56 -07:00
|
|
|
<XCircle className="size-3.5 text-red-500" />
|
2025-06-13 10:28:21 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getStatusText = () => {
|
|
|
|
|
if (props.status === "loading") {
|
|
|
|
|
return "Preparing action...";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (props.status === "generating") {
|
2025-06-27 16:35:47 -06:00
|
|
|
return ACTION_GENERATING_TEXT_MAP[props.actionType];
|
2025-06-13 10:28:21 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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";
|
2025-06-22 15:47:29 -07:00
|
|
|
} else if (props.actionType === "install_dependencies") {
|
|
|
|
|
return props.success ? "Dependencies installed" : "Installation failed";
|
2025-06-23 17:48:08 -07:00
|
|
|
} else if (props.actionType === "planner_notes") {
|
|
|
|
|
return props.success ? "Notes saved" : "Failed to save notes";
|
2025-07-08 11:14:55 -04:00
|
|
|
} else if (props.actionType === "get_url_content") {
|
|
|
|
|
return props.success
|
|
|
|
|
? "URL content fetched"
|
|
|
|
|
: "Failed to fetch URL content";
|
2025-07-14 16:39:19 -07:00
|
|
|
} else if (props.actionType === "search") {
|
2025-07-09 02:25:38 +00:00
|
|
|
return props.success ? "Search completed" : "Search failed";
|
2025-06-13 10:28:21 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Determine if we should show the content toggle button
|
|
|
|
|
const shouldShowToggle = () => {
|
|
|
|
|
if (props.status !== "done") return false;
|
|
|
|
|
|
2025-06-22 15:47:29 -07:00
|
|
|
if (
|
|
|
|
|
props.actionType === "shell" ||
|
2025-07-08 11:14:55 -04:00
|
|
|
props.actionType === "install_dependencies" ||
|
2025-07-09 02:25:38 +00:00
|
|
|
props.actionType === "get_url_content" ||
|
2025-07-14 16:39:19 -07:00
|
|
|
props.actionType === "search"
|
2025-06-22 15:47:29 -07:00
|
|
|
) {
|
2025-06-13 10:28:21 -07:00
|
|
|
return !!props.output;
|
|
|
|
|
} else if (props.actionType === "apply-patch") {
|
|
|
|
|
return !!props.diff;
|
2025-06-23 17:48:08 -07:00
|
|
|
} else if (props.actionType === "planner_notes") {
|
|
|
|
|
return !!(props.notes && props.notes.length > 0);
|
2025-06-13 10:28:21 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Render the header icon based on action type
|
|
|
|
|
const renderHeaderIcon = () => {
|
2025-07-09 02:25:38 +00:00
|
|
|
const defaultIconStyling = "text-muted-foreground mr-2 size-3.5";
|
2025-06-13 10:28:21 -07:00
|
|
|
if (props.status === "loading" || !("actionType" in props)) {
|
|
|
|
|
// In loading state, we don't know the type yet, use a generic icon
|
2025-07-09 02:25:38 +00:00
|
|
|
return (
|
|
|
|
|
<ToolIconWithTooltip
|
|
|
|
|
toolNamePretty="Loading"
|
|
|
|
|
icon={<Loader2 className={cn(defaultIconStyling)} />}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2025-06-13 10:28:21 -07:00
|
|
|
}
|
|
|
|
|
|
2025-06-23 17:48:08 -07:00
|
|
|
if (props.actionType === "planner_notes") {
|
2025-07-09 02:25:38 +00:00
|
|
|
return (
|
|
|
|
|
<ToolIconWithTooltip
|
|
|
|
|
toolNamePretty="Planner Notes"
|
|
|
|
|
icon={<FileText className={cn(defaultIconStyling)} />}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2025-06-23 17:48:08 -07:00
|
|
|
} else if (props.actionType === "install_dependencies") {
|
2025-07-09 02:25:38 +00:00
|
|
|
return (
|
|
|
|
|
<ToolIconWithTooltip
|
|
|
|
|
toolNamePretty="Install Dependencies"
|
|
|
|
|
icon={<CloudDownload className={cn(defaultIconStyling)} />}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2025-06-22 15:47:29 -07:00
|
|
|
} else if (props.actionType === "apply-patch") {
|
2025-07-09 02:25:38 +00:00
|
|
|
return (
|
|
|
|
|
<ToolIconWithTooltip
|
|
|
|
|
toolNamePretty="Apply Patch"
|
|
|
|
|
icon={<FileCode className={cn(defaultIconStyling)} />}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2025-07-08 11:14:55 -04:00
|
|
|
} else if (props.actionType === "get_url_content") {
|
2025-07-09 02:25:38 +00:00
|
|
|
return (
|
|
|
|
|
<ToolIconWithTooltip
|
|
|
|
|
toolNamePretty="Get URL Contents"
|
|
|
|
|
icon={<Globe className={cn(defaultIconStyling)} />}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2025-07-14 16:39:19 -07:00
|
|
|
} else if (props.actionType === "search") {
|
2025-07-09 02:25:38 +00:00
|
|
|
return (
|
|
|
|
|
<ToolIconWithTooltip
|
2025-07-14 16:39:19 -07:00
|
|
|
toolNamePretty="Search"
|
|
|
|
|
icon={<Search className={cn(defaultIconStyling)} />}
|
2025-07-09 02:25:38 +00:00
|
|
|
/>
|
|
|
|
|
);
|
2025-06-22 15:47:29 -07:00
|
|
|
} else {
|
2025-07-09 02:25:38 +00:00
|
|
|
return (
|
|
|
|
|
<ToolIconWithTooltip
|
|
|
|
|
toolNamePretty="Tool Call"
|
|
|
|
|
icon={<Terminal className={cn(defaultIconStyling)} />}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2025-06-22 15:47:29 -07:00
|
|
|
}
|
2025-06-13 10:28:21 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Render the header content based on action type
|
|
|
|
|
const renderHeaderContent = () => {
|
|
|
|
|
if (props.status === "loading" || !("actionType" in props)) {
|
|
|
|
|
return (
|
2025-06-18 16:44:41 -07:00
|
|
|
<span className="text-foreground/80 text-xs font-normal">
|
2025-06-13 10:28:21 -07:00
|
|
|
Preparing action...
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-23 17:48:08 -07:00
|
|
|
if (props.actionType === "planner_notes") {
|
|
|
|
|
return (
|
2025-07-14 16:39:19 -07:00
|
|
|
<div className="flex items-center">
|
2025-06-23 17:48:08 -07:00
|
|
|
<span className="text-foreground/80 text-xs font-normal">
|
|
|
|
|
Planner Notes
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-08 11:14:55 -04:00
|
|
|
if (props.actionType === "get_url_content") {
|
|
|
|
|
return (
|
2025-07-14 16:39:19 -07:00
|
|
|
<div className="flex items-center">
|
2025-07-08 11:14:55 -04:00
|
|
|
<code className="text-foreground/80 text-xs font-normal">
|
|
|
|
|
{props.url}
|
|
|
|
|
</code>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-14 16:39:19 -07:00
|
|
|
if (props.actionType === "search") {
|
|
|
|
|
const castProps = props as SearchActionProps;
|
2025-07-09 02:25:38 +00:00
|
|
|
return (
|
2025-07-14 16:39:19 -07:00
|
|
|
<div className="flex flex-col">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<code className="text-foreground/80 text-xs font-normal">
|
2025-07-14 20:01:27 -07:00
|
|
|
{castProps.query}
|
2025-07-14 16:39:19 -07:00
|
|
|
</code>
|
|
|
|
|
<div className="bg-border h-4 w-[1px] dark:bg-white"></div>
|
|
|
|
|
<MatchCaseIcon matchCase={!!castProps.case_sensitive} />
|
2025-07-14 20:01:27 -07:00
|
|
|
{!castProps.match_string && (
|
2025-07-14 16:39:19 -07:00
|
|
|
<span className="text-muted-foreground bg-muted/50 rounded px-1 text-xs font-normal">
|
|
|
|
|
regex
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-07-09 02:25:38 +00:00
|
|
|
</div>
|
2025-07-14 16:39:19 -07:00
|
|
|
<div className="text-muted-foreground mt-1 flex flex-wrap gap-x-4 gap-y-1 text-xs font-normal">
|
|
|
|
|
{castProps.include_files && (
|
|
|
|
|
<span>Include: {castProps.include_files}</span>
|
|
|
|
|
)}
|
|
|
|
|
{castProps.exclude_files && (
|
|
|
|
|
<span>Exclude: {castProps.exclude_files}</span>
|
|
|
|
|
)}
|
|
|
|
|
{castProps.context_lines !== undefined &&
|
|
|
|
|
castProps.context_lines > 0 && (
|
|
|
|
|
<span>Context: {castProps.context_lines} lines</span>
|
2025-07-09 02:25:38 +00:00
|
|
|
)}
|
2025-07-14 16:39:19 -07:00
|
|
|
{castProps.max_results !== undefined &&
|
|
|
|
|
castProps.max_results > 0 && (
|
|
|
|
|
<span>Max results: {castProps.max_results}</span>
|
2025-07-09 02:25:38 +00:00
|
|
|
)}
|
2025-07-14 16:39:19 -07:00
|
|
|
{castProps.file_types && castProps.file_types.length > 0 && (
|
2025-07-17 11:52:25 -07:00
|
|
|
<span>
|
|
|
|
|
File types:{" "}
|
|
|
|
|
{coerceStringToArray(castProps.file_types).join(", ")}
|
|
|
|
|
</span>
|
2025-07-14 16:39:19 -07:00
|
|
|
)}
|
|
|
|
|
{castProps.follow_symlinks && <span>Follow symlinks</span>}
|
|
|
|
|
</div>
|
2025-07-09 02:25:38 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-22 15:47:29 -07:00
|
|
|
if (
|
|
|
|
|
props.actionType === "shell" ||
|
|
|
|
|
props.actionType === "install_dependencies"
|
|
|
|
|
) {
|
2025-06-19 16:08:06 -07:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-13 10:28:21 -07:00
|
|
|
return (
|
2025-07-14 16:39:19 -07:00
|
|
|
<div className="flex items-center">
|
2025-06-13 10:28:21 -07:00
|
|
|
{props.workdir && (
|
2025-06-18 16:44:41 -07:00
|
|
|
<div className="text-muted-foreground mb-0.5 text-xs font-normal">
|
2025-06-13 10:28:21 -07:00
|
|
|
{props.workdir}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-06-18 16:44:41 -07:00
|
|
|
<code className="text-foreground/80 text-xs font-normal">
|
2025-06-19 16:08:06 -07:00
|
|
|
{commandStr}
|
2025-06-13 10:28:21 -07:00
|
|
|
</code>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
return (
|
2025-07-14 16:39:19 -07:00
|
|
|
<code className="text-foreground/80 flex items-center text-xs font-normal">
|
2025-06-19 12:57:27 -07:00
|
|
|
{props.file_path}
|
2025-06-13 10:28:21 -07:00
|
|
|
</code>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Render the content based on action type
|
|
|
|
|
const renderContent = () => {
|
|
|
|
|
if (props.status !== "done" || !("actionType" in props)) return null;
|
|
|
|
|
|
|
|
|
|
if (!expanded) return null;
|
|
|
|
|
|
2025-06-22 14:04:58 -07:00
|
|
|
if (
|
2025-06-22 15:47:29 -07:00
|
|
|
(props.actionType === "shell" ||
|
2025-07-14 16:39:19 -07:00
|
|
|
props.actionType === "search" ||
|
2025-06-22 15:47:29 -07:00
|
|
|
props.actionType === "install_dependencies") &&
|
2025-06-22 14:04:58 -07:00
|
|
|
props.output
|
|
|
|
|
) {
|
2025-06-13 10:28:21 -07:00
|
|
|
return (
|
2025-06-18 16:44:41 -07:00
|
|
|
<div className="bg-muted text-foreground/90 overflow-x-auto p-2 dark:bg-gray-900">
|
2025-06-13 10:28:21 -07:00
|
|
|
<pre className="text-xs font-normal whitespace-pre-wrap">
|
|
|
|
|
{props.output}
|
|
|
|
|
</pre>
|
|
|
|
|
{props.errorCode !== undefined && !props.success && (
|
2025-06-18 16:44:41 -07:00
|
|
|
<div className="mt-1 text-xs text-red-500 dark:text-red-400">
|
2025-06-13 10:28:21 -07:00
|
|
|
Exit code: {props.errorCode}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-07-08 11:14:55 -04:00
|
|
|
} else if (props.actionType === "get_url_content" && props.output) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="bg-muted text-foreground/90 overflow-x-auto p-2 dark:bg-gray-900">
|
|
|
|
|
<pre className="text-xs font-normal whitespace-pre-wrap">
|
|
|
|
|
{props.output}
|
|
|
|
|
</pre>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-06-13 10:28:21 -07:00
|
|
|
} else if (props.actionType === "apply-patch" && props.diff) {
|
|
|
|
|
return (
|
2025-06-18 16:44:41 -07:00
|
|
|
<div className="bg-muted overflow-x-auto p-2 dark:bg-gray-900">
|
2025-06-13 10:28:21 -07:00
|
|
|
<pre
|
2025-06-18 16:44:41 -07:00
|
|
|
className="text-foreground/90 text-xs font-normal whitespace-pre-wrap"
|
2025-06-13 10:28:21 -07:00
|
|
|
dangerouslySetInnerHTML={{ __html: formatDiff(props.diff) }}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{!props.success && props.errorMessage && (
|
2025-06-18 16:44:41 -07:00
|
|
|
<div className="mt-2 rounded border border-red-700/30 bg-red-100/30 p-2 text-xs text-red-600 dark:bg-red-900/30 dark:text-red-400">
|
2025-06-13 10:28:21 -07:00
|
|
|
{props.errorMessage}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!props.success && props.fixedDiff && (
|
|
|
|
|
<div className="mt-2">
|
2025-06-18 16:44:41 -07:00
|
|
|
<div className="text-muted-foreground mb-1 text-xs">
|
|
|
|
|
Fixed diff:
|
|
|
|
|
</div>
|
2025-06-13 10:28:21 -07:00
|
|
|
<pre
|
2025-06-18 16:44:41 -07:00
|
|
|
className="text-foreground/90 text-xs font-normal whitespace-pre-wrap"
|
2025-06-13 10:28:21 -07:00
|
|
|
dangerouslySetInnerHTML={{
|
|
|
|
|
__html: formatDiff(props.fixedDiff),
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-06-23 17:48:08 -07:00
|
|
|
} else if (
|
|
|
|
|
props.actionType === "planner_notes" &&
|
|
|
|
|
props.notes &&
|
|
|
|
|
props.notes.length > 0
|
|
|
|
|
) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="bg-muted overflow-x-auto p-2 dark:bg-gray-900">
|
|
|
|
|
<ul className="list-disc pl-5 text-xs font-normal">
|
|
|
|
|
{props.notes.map((note, i) => (
|
|
|
|
|
<li
|
|
|
|
|
key={i}
|
|
|
|
|
className="text-foreground/90 mb-1 whitespace-pre-wrap"
|
|
|
|
|
>
|
|
|
|
|
{note}
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-06-13 10:28:21 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2025-06-22 12:14:56 -07:00
|
|
|
<div className="border-border mb-2 overflow-hidden rounded-md border last:mb-0">
|
2025-07-14 16:39:19 -07:00
|
|
|
<div className="border-border flex w-full items-center border-b bg-gray-50 p-2 dark:bg-gray-800">
|
2025-06-13 10:28:21 -07:00
|
|
|
{renderHeaderIcon()}
|
|
|
|
|
{renderHeaderContent()}
|
2025-07-14 16:39:19 -07:00
|
|
|
<div className="ml-auto flex items-center gap-2">
|
2025-06-18 16:44:41 -07:00
|
|
|
<span className="text-muted-foreground text-xs font-normal">
|
2025-06-13 10:28:21 -07:00
|
|
|
{getStatusText()}
|
|
|
|
|
</span>
|
|
|
|
|
{getStatusIcon()}
|
|
|
|
|
{shouldShowToggle() && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setExpanded(!expanded)}
|
2025-07-14 16:39:19 -07:00
|
|
|
className="text-muted-foreground hover:text-foreground cursor-pointer"
|
2025-06-13 10:28:21 -07:00
|
|
|
>
|
|
|
|
|
{expanded ? (
|
2025-06-22 12:14:56 -07:00
|
|
|
<ChevronUp className="size-3.5" />
|
2025-06-13 10:28:21 -07:00
|
|
|
) : (
|
2025-06-22 12:14:56 -07:00
|
|
|
<ChevronDown className="size-3.5" />
|
2025-06-13 10:28:21 -07:00
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{renderContent()}
|
2025-06-22 12:14:56 -07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ActionStep(props: ActionStepProps) {
|
2025-07-14 16:39:19 -07:00
|
|
|
const [showReasoning, setShowReasoning] = useState(true);
|
|
|
|
|
const [showSummary, setShowSummary] = useState(true);
|
2025-06-22 12:14:56 -07:00
|
|
|
|
|
|
|
|
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 (
|
|
|
|
|
<div className="border-border overflow-hidden rounded-md border">
|
|
|
|
|
<div className="border-b border-blue-300 bg-blue-100/50 p-2 dark:border-blue-800 dark:bg-blue-900/50">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowReasoning(!showReasoning)}
|
|
|
|
|
className="flex cursor-pointer items-center gap-1 text-xs font-normal text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
|
|
|
|
>
|
|
|
|
|
<MessageSquare className="h-3 w-3" />
|
|
|
|
|
{showReasoning ? "Hide reasoning" : "Show reasoning"}
|
|
|
|
|
</button>
|
|
|
|
|
{showReasoning && (
|
|
|
|
|
<p className="mt-1 text-xs font-normal text-blue-700 dark:text-blue-300">
|
|
|
|
|
{reasoningText || "No reasoning provided."}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="p-2">
|
|
|
|
|
{props.actions.map((action: ActionItemProps, index: number) => (
|
|
|
|
|
<ActionItem
|
|
|
|
|
key={index}
|
|
|
|
|
{...action}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2025-06-13 10:28:21 -07:00
|
|
|
|
2025-06-22 12:14:56 -07:00
|
|
|
{summaryText && anyActionDone && (
|
2025-06-18 16:44:41 -07:00
|
|
|
<div className="border-t border-green-300 bg-green-100/50 p-2 dark:border-green-800 dark:bg-green-900/50">
|
2025-06-13 10:28:21 -07:00
|
|
|
<button
|
|
|
|
|
onClick={() => setShowSummary(!showSummary)}
|
2025-06-22 12:14:56 -07:00
|
|
|
className="flex cursor-pointer items-center gap-1 text-xs font-normal text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
|
2025-06-13 10:28:21 -07:00
|
|
|
>
|
|
|
|
|
<FileText className="h-3 w-3" />
|
|
|
|
|
{showSummary ? "Hide summary" : "Show summary"}
|
|
|
|
|
</button>
|
|
|
|
|
{showSummary && (
|
2025-07-14 16:39:19 -07:00
|
|
|
<span className="text-green-700 dark:text-green-300">
|
|
|
|
|
<BasicMarkdownText>{summaryText}</BasicMarkdownText>
|
|
|
|
|
</span>
|
2025-06-13 10:28:21 -07:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDiff(diff: string) {
|
|
|
|
|
return diff
|
|
|
|
|
.split("\n")
|
|
|
|
|
.map((line) => {
|
|
|
|
|
if (line.startsWith("+")) {
|
2025-06-18 16:44:41 -07:00
|
|
|
return `<span class="text-green-600 dark:text-green-400">${line}</span>`;
|
2025-06-13 10:28:21 -07:00
|
|
|
} else if (line.startsWith("-")) {
|
2025-06-18 16:44:41 -07:00
|
|
|
return `<span class="text-red-600 dark:text-red-400">${line}</span>`;
|
2025-06-13 10:28:21 -07:00
|
|
|
} else if (line.startsWith("@")) {
|
2025-06-18 16:44:41 -07:00
|
|
|
return `<span class="text-blue-600 dark:text-blue-400">${line}</span>`;
|
2025-06-13 10:28:21 -07:00
|
|
|
}
|
|
|
|
|
return line;
|
|
|
|
|
})
|
|
|
|
|
.join("\n");
|
|
|
|
|
}
|