diff --git a/src/browser/components/Messages/ToolMessage.tsx b/src/browser/components/Messages/ToolMessage.tsx index a7a4102729..77b0f61554 100644 --- a/src/browser/components/Messages/ToolMessage.tsx +++ b/src/browser/components/Messages/ToolMessage.tsx @@ -9,9 +9,15 @@ import { ProposePlanToolCall } from "../tools/ProposePlanToolCall"; import { TodoToolCall } from "../tools/TodoToolCall"; import { StatusSetToolCall } from "../tools/StatusSetToolCall"; import { WebFetchToolCall } from "../tools/WebFetchToolCall"; +import { BashBackgroundListToolCall } from "../tools/BashBackgroundListToolCall"; +import { BashBackgroundTerminateToolCall } from "../tools/BashBackgroundTerminateToolCall"; import type { BashToolArgs, BashToolResult, + BashBackgroundListArgs, + BashBackgroundListResult, + BashBackgroundTerminateArgs, + BashBackgroundTerminateResult, FileReadToolArgs, FileReadToolResult, FileEditReplaceStringToolArgs, @@ -89,6 +95,19 @@ function isWebFetchTool(toolName: string, args: unknown): args is WebFetchToolAr return TOOL_DEFINITIONS.web_fetch.schema.safeParse(args).success; } +function isBashBackgroundListTool(toolName: string, args: unknown): args is BashBackgroundListArgs { + if (toolName !== "bash_background_list") return false; + return TOOL_DEFINITIONS.bash_background_list.schema.safeParse(args).success; +} + +function isBashBackgroundTerminateTool( + toolName: string, + args: unknown +): args is BashBackgroundTerminateArgs { + if (toolName !== "bash_background_terminate") return false; + return TOOL_DEFINITIONS.bash_background_terminate.schema.safeParse(args).success; +} + export const ToolMessage: React.FC = ({ message, className, workspaceId }) => { // Route to specialized components based on tool name if (isBashTool(message.toolName, message.args)) { @@ -204,6 +223,30 @@ export const ToolMessage: React.FC = ({ message, className, wo ); } + if (isBashBackgroundListTool(message.toolName, message.args)) { + return ( +
+ +
+ ); + } + + if (isBashBackgroundTerminateTool(message.toolName, message.args)) { + return ( +
+ +
+ ); + } + // Fallback to generic tool call return (
diff --git a/src/browser/components/tools/BashBackgroundListToolCall.tsx b/src/browser/components/tools/BashBackgroundListToolCall.tsx new file mode 100644 index 0000000000..7e9faefafb --- /dev/null +++ b/src/browser/components/tools/BashBackgroundListToolCall.tsx @@ -0,0 +1,122 @@ +import React from "react"; +import type { + BashBackgroundListArgs, + BashBackgroundListResult, + BashBackgroundListProcess, +} from "@/common/types/tools"; +import { + ToolContainer, + ToolHeader, + ExpandIcon, + StatusIndicator, + ToolDetails, + DetailSection, + LoadingDots, + ToolIcon, + ErrorBox, + OutputPaths, +} from "./shared/ToolPrimitives"; +import { + useToolExpansion, + getStatusDisplay, + formatDuration, + type ToolStatus, +} from "./shared/toolUtils"; +import { cn } from "@/common/lib/utils"; + +interface BashBackgroundListToolCallProps { + args: BashBackgroundListArgs; + result?: BashBackgroundListResult; + status?: ToolStatus; +} + +function getProcessStatusStyle(status: BashBackgroundListProcess["status"]) { + switch (status) { + case "running": + return "bg-success text-on-success"; + case "exited": + return "bg-[hsl(0,0%,40%)] text-white"; + case "killed": + case "failed": + return "bg-danger text-on-danger"; + } +} + +export const BashBackgroundListToolCall: React.FC = ({ + args: _args, + result, + status = "pending", +}) => { + const { expanded, toggleExpanded } = useToolExpansion(false); + + const processes = result?.success ? result.processes : []; + const runningCount = processes.filter((p) => p.status === "running").length; + + return ( + + + â–ļ + + + {result?.success + ? runningCount === 0 + ? "No background processes" + : `${runningCount} background process${runningCount !== 1 ? "es" : ""}` + : "Listing background processes"} + + {getStatusDisplay(status)} + + + {expanded && ( + + {result?.success === false && ( + + {result.error} + + )} + + {result?.success && processes.length > 0 && ( + +
+ {processes.map((proc) => ( +
+
+ + {proc.display_name ?? proc.process_id} + + + {proc.status} + {proc.exitCode !== undefined && ` (${proc.exitCode})`} + + + {formatDuration(proc.uptime_ms)} + +
+
+ {proc.script} +
+ +
+ ))} +
+
+ )} + + {status === "executing" && !result && ( + +
+ Listing processes + +
+
+ )} +
+ )} +
+ ); +}; diff --git a/src/browser/components/tools/BashBackgroundTerminateToolCall.tsx b/src/browser/components/tools/BashBackgroundTerminateToolCall.tsx new file mode 100644 index 0000000000..86358cf6a5 --- /dev/null +++ b/src/browser/components/tools/BashBackgroundTerminateToolCall.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import type { + BashBackgroundTerminateArgs, + BashBackgroundTerminateResult, +} from "@/common/types/tools"; +import { ToolContainer, ToolHeader, StatusIndicator, ToolIcon } from "./shared/ToolPrimitives"; +import { getStatusDisplay, type ToolStatus } from "./shared/toolUtils"; + +interface BashBackgroundTerminateToolCallProps { + args: BashBackgroundTerminateArgs; + result?: BashBackgroundTerminateResult; + status?: ToolStatus; +} + +export const BashBackgroundTerminateToolCall: React.FC = ({ + args, + result, + status = "pending", +}) => { + const statusDisplay = getStatusDisplay(status); + + return ( + + + + + {result?.success === true ? (result.display_name ?? args.process_id) : args.process_id} + + {result?.success === true && ( + terminated + )} + {result?.success === false && ( + {result.error} + )} + {statusDisplay} + + + ); +}; diff --git a/src/browser/components/tools/BashToolCall.tsx b/src/browser/components/tools/BashToolCall.tsx index 36e4fcfa88..aad33a363e 100644 --- a/src/browser/components/tools/BashToolCall.tsx +++ b/src/browser/components/tools/BashToolCall.tsx @@ -11,10 +11,17 @@ import { DetailLabel, DetailContent, LoadingDots, + ToolIcon, + ErrorBox, + OutputPaths, } from "./shared/ToolPrimitives"; -import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils"; +import { + useToolExpansion, + getStatusDisplay, + formatDuration, + type ToolStatus, +} from "./shared/toolUtils"; import { cn } from "@/common/lib/utils"; -import { TooltipWrapper, Tooltip } from "../Tooltip"; interface BashToolCallProps { args: BashToolArgs; @@ -23,13 +30,6 @@ interface BashToolCallProps { startedAt?: number; } -function formatDuration(ms: number): string { - if (ms < 1000) { - return `${Math.round(ms)}ms`; - } - return `${Math.round(ms / 1000)}s`; -} - export const BashToolCall: React.FC = ({ args, result, @@ -59,35 +59,43 @@ export const BashToolCall: React.FC = ({ }, [status, startedAt]); const isPending = status === "executing" || status === "pending"; + const isBackground = args.run_in_background ?? (result && "backgroundProcessId" in result); return ( â–ļ - - 🔧 - bash - + {args.script} - - timeout: {args.timeout_secs ?? BASH_DEFAULT_TIMEOUT_SECS}s - {result && ` â€ĸ took ${formatDuration(result.wall_duration_ms)}`} - {!result && isPending && elapsedTime > 0 && ` â€ĸ ${formatDuration(elapsedTime)}`} - - {result && ( - - {result.exitCode} + {isBackground ? ( + // Background mode: show background badge and optional display name + + ⚡ background{args.display_name && ` â€ĸ ${args.display_name}`} + ) : ( + // Normal mode: show timeout and duration + <> + + timeout: {args.timeout_secs ?? BASH_DEFAULT_TIMEOUT_SECS}s + {result && ` â€ĸ took ${formatDuration(result.wall_duration_ms)}`} + {!result && isPending && elapsedTime > 0 && ` â€ĸ ${formatDuration(elapsedTime)}`} + + {result && ( + + {result.exitCode} + + )} + )} {getStatusDisplay(status)} @@ -104,19 +112,26 @@ export const BashToolCall: React.FC = ({ {result.success === false && result.error && ( Error -
- {result.error} -
+ {result.error}
)} - {result.output && ( + {"backgroundProcessId" in result ? ( + // Background process: show file paths - Output -
-                    {result.output}
-                  
+ Output Files +
+ ) : ( + // Normal process: show output + result.output && ( + + Output +
+                      {result.output}
+                    
+
+ ) )} )} diff --git a/src/browser/components/tools/FileEditToolCall.tsx b/src/browser/components/tools/FileEditToolCall.tsx index e5ee77edc2..ff2c52d518 100644 --- a/src/browser/components/tools/FileEditToolCall.tsx +++ b/src/browser/components/tools/FileEditToolCall.tsx @@ -18,10 +18,11 @@ import { DetailSection, DetailLabel, LoadingDots, + ToolIcon, + ErrorBox, } from "./shared/ToolPrimitives"; import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils"; import { useCopyToClipboard } from "@/browser/hooks/useCopyToClipboard"; -import { TooltipWrapper, Tooltip } from "../Tooltip"; import { DiffContainer, DiffRenderer, SelectableDiffRenderer } from "../shared/DiffRenderer"; import { KebabMenu, type KebabMenuItem } from "../KebabMenu"; @@ -84,11 +85,7 @@ function renderDiff( )); } catch (error) { - return ( -
- Failed to parse diff: {String(error)} -
- ); + return Failed to parse diff: {String(error)}; } } @@ -135,10 +132,7 @@ export const FileEditToolCall: React.FC = ({ className="hover:text-text flex flex-1 cursor-pointer items-center gap-2" > â–ļ - - âœī¸ - {toolName} - +
{filePath} @@ -161,9 +155,7 @@ export const FileEditToolCall: React.FC = ({ {result.success === false && result.error && ( Error -
- {result.error} -
+ {result.error}
)} diff --git a/src/browser/components/tools/FileReadToolCall.tsx b/src/browser/components/tools/FileReadToolCall.tsx index 32886db515..074cb7457e 100644 --- a/src/browser/components/tools/FileReadToolCall.tsx +++ b/src/browser/components/tools/FileReadToolCall.tsx @@ -11,9 +11,10 @@ import { DetailLabel, DetailContent, LoadingDots, + ToolIcon, + ErrorBox, } from "./shared/ToolPrimitives"; import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils"; -import { TooltipWrapper, Tooltip } from "../Tooltip"; interface FileReadToolCallProps { args: FileReadToolArgs; @@ -79,10 +80,7 @@ export const FileReadToolCall: React.FC = ({ â–ļ - - 📖 - file_read - +
{filePath} @@ -125,9 +123,7 @@ export const FileReadToolCall: React.FC = ({ {result.success === false && result.error && ( Error -
- {result.error} -
+ {result.error}
)} diff --git a/src/browser/components/tools/StatusSetToolCall.tsx b/src/browser/components/tools/StatusSetToolCall.tsx index 46717b7997..4643b64ea2 100644 --- a/src/browser/components/tools/StatusSetToolCall.tsx +++ b/src/browser/components/tools/StatusSetToolCall.tsx @@ -1,8 +1,7 @@ import React from "react"; import type { StatusSetToolArgs, StatusSetToolResult } from "@/common/types/tools"; -import { ToolContainer, ToolHeader, StatusIndicator } from "./shared/ToolPrimitives"; +import { ToolContainer, ToolHeader, StatusIndicator, ToolIcon } from "./shared/ToolPrimitives"; import { getStatusDisplay, type ToolStatus } from "./shared/toolUtils"; -import { TooltipWrapper, Tooltip } from "../Tooltip"; interface StatusSetToolCallProps { args: StatusSetToolArgs; @@ -26,10 +25,7 @@ export const StatusSetToolCall: React.FC = ({ return ( - - {args.emoji} - status_set - + {args.message} {errorMessage && ({errorMessage})} {statusDisplay} diff --git a/src/browser/components/tools/TodoToolCall.tsx b/src/browser/components/tools/TodoToolCall.tsx index b8238b5d8e..a1664eb6d0 100644 --- a/src/browser/components/tools/TodoToolCall.tsx +++ b/src/browser/components/tools/TodoToolCall.tsx @@ -6,9 +6,9 @@ import { ExpandIcon, StatusIndicator, ToolDetails, + ToolIcon, } from "./shared/ToolPrimitives"; import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils"; -import { TooltipWrapper, Tooltip } from "../Tooltip"; import { TodoList } from "../TodoList"; interface TodoToolCallProps { @@ -29,10 +29,7 @@ export const TodoToolCall: React.FC = ({ â–ļ - - 📋 - todo_write - + {statusDisplay} diff --git a/src/browser/components/tools/WebFetchToolCall.tsx b/src/browser/components/tools/WebFetchToolCall.tsx index 2d4f17eda7..b81924e857 100644 --- a/src/browser/components/tools/WebFetchToolCall.tsx +++ b/src/browser/components/tools/WebFetchToolCall.tsx @@ -9,9 +9,10 @@ import { DetailSection, DetailLabel, LoadingDots, + ToolIcon, + ErrorBox, } from "./shared/ToolPrimitives"; import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils"; -import { TooltipWrapper, Tooltip } from "../Tooltip"; import { MarkdownRenderer } from "../Messages/MarkdownRenderer"; interface WebFetchToolCallProps { @@ -51,10 +52,7 @@ export const WebFetchToolCall: React.FC = ({ â–ļ - - 🌐 - web_fetch - +
{domain}
@@ -96,9 +94,7 @@ export const WebFetchToolCall: React.FC = ({ {result.success === false && result.error && ( Error -
- {result.error} -
+ {result.error}
)} diff --git a/src/browser/components/tools/shared/ToolPrimitives.tsx b/src/browser/components/tools/shared/ToolPrimitives.tsx index 7fccb39520..22368bd00e 100644 --- a/src/browser/components/tools/shared/ToolPrimitives.tsx +++ b/src/browser/components/tools/shared/ToolPrimitives.tsx @@ -1,5 +1,6 @@ import React from "react"; import { cn } from "@/common/lib/utils"; +import { TooltipWrapper, Tooltip } from "../../Tooltip"; /** * Shared styled components for tool UI @@ -157,3 +158,67 @@ export const HeaderButton: React.FC = ({ active, className, . {...props} /> ); + +/** + * Tool icon with tooltip showing tool name + */ +interface ToolIconProps { + emoji: string; + toolName: string; +} + +export const ToolIcon: React.FC = ({ emoji, toolName }) => ( + + {emoji} + {toolName} + +); + +/** + * Error display box with danger styling + */ +export const ErrorBox: React.FC> = ({ + className, + ...props +}) => ( +
+); + +/** + * Output file paths display (stdout/stderr) + * @param compact - Use smaller text without background (for inline use in cards) + */ +interface OutputPathsProps { + stdout: string; + stderr: string; + compact?: boolean; +} + +export const OutputPaths: React.FC = ({ stdout, stderr, compact }) => + compact ? ( +
+
+ stdout: {stdout} +
+
+ stderr: {stderr} +
+
+ ) : ( +
+
+ stdout:{" "} + {stdout} +
+
+ stderr:{" "} + {stderr} +
+
+ ); diff --git a/src/browser/components/tools/shared/toolUtils.tsx b/src/browser/components/tools/shared/toolUtils.tsx index 966b0f17cf..1bcf262ea2 100644 --- a/src/browser/components/tools/shared/toolUtils.tsx +++ b/src/browser/components/tools/shared/toolUtils.tsx @@ -64,3 +64,13 @@ export function formatValue(value: unknown): string { return "[Complex Object - Cannot Stringify]"; } } + +/** + * Format duration in human-readable form (ms, s, m, h) + */ +export function formatDuration(ms: number): string { + if (ms < 1000) return `${Math.round(ms)}ms`; + if (ms < 60000) return `${Math.round(ms / 1000)}s`; + if (ms < 3600000) return `${Math.round(ms / 60000)}m`; + return `${Math.round(ms / 3600000)}h`; +}