Skip to content

Commit e954200

Browse files
🤖 feat: add UI styling for background bash processes (#923)
### Stack: 1. #923 <- This PR 1. #920 (base) --- Adds custom UI components for background bash tools: - **BashToolCall**: Shows `⚡ background • display_name` for background spawns instead of timeout/duration; shows output file paths in expanded view - **BashBackgroundListToolCall**: New component showing process list with status badges, exit codes, uptimes, scripts, and output file paths - **BashBackgroundTerminateToolCall**: New component showing terminated process with display_name from result <img width="984" height="946" alt="image" src="https://github.com/user-attachments/assets/3de5a0bb-6c00-4641-96cb-65c67e3d0085" /> **Testing:** Manually tested on Linux, Mac, and Windows. Closes #493 _Generated with mux_
1 parent acb9052 commit e954200

File tree

11 files changed

+350
-79
lines changed

11 files changed

+350
-79
lines changed

src/browser/components/Messages/ToolMessage.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@ import { ProposePlanToolCall } from "../tools/ProposePlanToolCall";
99
import { TodoToolCall } from "../tools/TodoToolCall";
1010
import { StatusSetToolCall } from "../tools/StatusSetToolCall";
1111
import { WebFetchToolCall } from "../tools/WebFetchToolCall";
12+
import { BashBackgroundListToolCall } from "../tools/BashBackgroundListToolCall";
13+
import { BashBackgroundTerminateToolCall } from "../tools/BashBackgroundTerminateToolCall";
1214
import type {
1315
BashToolArgs,
1416
BashToolResult,
17+
BashBackgroundListArgs,
18+
BashBackgroundListResult,
19+
BashBackgroundTerminateArgs,
20+
BashBackgroundTerminateResult,
1521
FileReadToolArgs,
1622
FileReadToolResult,
1723
FileEditReplaceStringToolArgs,
@@ -89,6 +95,19 @@ function isWebFetchTool(toolName: string, args: unknown): args is WebFetchToolAr
8995
return TOOL_DEFINITIONS.web_fetch.schema.safeParse(args).success;
9096
}
9197

98+
function isBashBackgroundListTool(toolName: string, args: unknown): args is BashBackgroundListArgs {
99+
if (toolName !== "bash_background_list") return false;
100+
return TOOL_DEFINITIONS.bash_background_list.schema.safeParse(args).success;
101+
}
102+
103+
function isBashBackgroundTerminateTool(
104+
toolName: string,
105+
args: unknown
106+
): args is BashBackgroundTerminateArgs {
107+
if (toolName !== "bash_background_terminate") return false;
108+
return TOOL_DEFINITIONS.bash_background_terminate.schema.safeParse(args).success;
109+
}
110+
92111
export const ToolMessage: React.FC<ToolMessageProps> = ({ message, className, workspaceId }) => {
93112
// Route to specialized components based on tool name
94113
if (isBashTool(message.toolName, message.args)) {
@@ -204,6 +223,30 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({ message, className, wo
204223
);
205224
}
206225

226+
if (isBashBackgroundListTool(message.toolName, message.args)) {
227+
return (
228+
<div className={className}>
229+
<BashBackgroundListToolCall
230+
args={message.args}
231+
result={message.result as BashBackgroundListResult | undefined}
232+
status={message.status}
233+
/>
234+
</div>
235+
);
236+
}
237+
238+
if (isBashBackgroundTerminateTool(message.toolName, message.args)) {
239+
return (
240+
<div className={className}>
241+
<BashBackgroundTerminateToolCall
242+
args={message.args}
243+
result={message.result as BashBackgroundTerminateResult | undefined}
244+
status={message.status}
245+
/>
246+
</div>
247+
);
248+
}
249+
207250
// Fallback to generic tool call
208251
return (
209252
<div className={className}>
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React from "react";
2+
import type {
3+
BashBackgroundListArgs,
4+
BashBackgroundListResult,
5+
BashBackgroundListProcess,
6+
} from "@/common/types/tools";
7+
import {
8+
ToolContainer,
9+
ToolHeader,
10+
ExpandIcon,
11+
StatusIndicator,
12+
ToolDetails,
13+
DetailSection,
14+
LoadingDots,
15+
ToolIcon,
16+
ErrorBox,
17+
OutputPaths,
18+
} from "./shared/ToolPrimitives";
19+
import {
20+
useToolExpansion,
21+
getStatusDisplay,
22+
formatDuration,
23+
type ToolStatus,
24+
} from "./shared/toolUtils";
25+
import { cn } from "@/common/lib/utils";
26+
27+
interface BashBackgroundListToolCallProps {
28+
args: BashBackgroundListArgs;
29+
result?: BashBackgroundListResult;
30+
status?: ToolStatus;
31+
}
32+
33+
function getProcessStatusStyle(status: BashBackgroundListProcess["status"]) {
34+
switch (status) {
35+
case "running":
36+
return "bg-success text-on-success";
37+
case "exited":
38+
return "bg-[hsl(0,0%,40%)] text-white";
39+
case "killed":
40+
case "failed":
41+
return "bg-danger text-on-danger";
42+
}
43+
}
44+
45+
export const BashBackgroundListToolCall: React.FC<BashBackgroundListToolCallProps> = ({
46+
args: _args,
47+
result,
48+
status = "pending",
49+
}) => {
50+
const { expanded, toggleExpanded } = useToolExpansion(false);
51+
52+
const processes = result?.success ? result.processes : [];
53+
const runningCount = processes.filter((p) => p.status === "running").length;
54+
55+
return (
56+
<ToolContainer expanded={expanded}>
57+
<ToolHeader onClick={toggleExpanded}>
58+
<ExpandIcon expanded={expanded}></ExpandIcon>
59+
<ToolIcon emoji="📋" toolName="bash_background_list" />
60+
<span className="text-text-secondary">
61+
{result?.success
62+
? runningCount === 0
63+
? "No background processes"
64+
: `${runningCount} background process${runningCount !== 1 ? "es" : ""}`
65+
: "Listing background processes"}
66+
</span>
67+
<StatusIndicator status={status}>{getStatusDisplay(status)}</StatusIndicator>
68+
</ToolHeader>
69+
70+
{expanded && (
71+
<ToolDetails>
72+
{result?.success === false && (
73+
<DetailSection>
74+
<ErrorBox>{result.error}</ErrorBox>
75+
</DetailSection>
76+
)}
77+
78+
{result?.success && processes.length > 0 && (
79+
<DetailSection>
80+
<div className="space-y-2">
81+
{processes.map((proc) => (
82+
<div key={proc.process_id} className="bg-code-bg rounded px-2 py-1.5 text-[11px]">
83+
<div className="mb-1 flex items-center gap-2">
84+
<span className="text-text font-mono">
85+
{proc.display_name ?? proc.process_id}
86+
</span>
87+
<span
88+
className={cn(
89+
"inline-block rounded px-1.5 py-0.5 text-[9px] font-medium uppercase",
90+
getProcessStatusStyle(proc.status)
91+
)}
92+
>
93+
{proc.status}
94+
{proc.exitCode !== undefined && ` (${proc.exitCode})`}
95+
</span>
96+
<span className="text-text-secondary ml-auto">
97+
{formatDuration(proc.uptime_ms)}
98+
</span>
99+
</div>
100+
<div className="text-text-secondary truncate font-mono" title={proc.script}>
101+
{proc.script}
102+
</div>
103+
<OutputPaths stdout={proc.stdout_path} stderr={proc.stderr_path} compact />
104+
</div>
105+
))}
106+
</div>
107+
</DetailSection>
108+
)}
109+
110+
{status === "executing" && !result && (
111+
<DetailSection>
112+
<div className="text-[11px]">
113+
Listing processes
114+
<LoadingDots />
115+
</div>
116+
</DetailSection>
117+
)}
118+
</ToolDetails>
119+
)}
120+
</ToolContainer>
121+
);
122+
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from "react";
2+
import type {
3+
BashBackgroundTerminateArgs,
4+
BashBackgroundTerminateResult,
5+
} from "@/common/types/tools";
6+
import { ToolContainer, ToolHeader, StatusIndicator, ToolIcon } from "./shared/ToolPrimitives";
7+
import { getStatusDisplay, type ToolStatus } from "./shared/toolUtils";
8+
9+
interface BashBackgroundTerminateToolCallProps {
10+
args: BashBackgroundTerminateArgs;
11+
result?: BashBackgroundTerminateResult;
12+
status?: ToolStatus;
13+
}
14+
15+
export const BashBackgroundTerminateToolCall: React.FC<BashBackgroundTerminateToolCallProps> = ({
16+
args,
17+
result,
18+
status = "pending",
19+
}) => {
20+
const statusDisplay = getStatusDisplay(status);
21+
22+
return (
23+
<ToolContainer expanded={false}>
24+
<ToolHeader>
25+
<ToolIcon emoji="⏹️" toolName="bash_background_terminate" />
26+
<span className="text-text font-mono">
27+
{result?.success === true ? (result.display_name ?? args.process_id) : args.process_id}
28+
</span>
29+
{result?.success === true && (
30+
<span className="text-text-secondary text-[10px]">terminated</span>
31+
)}
32+
{result?.success === false && (
33+
<span className="text-danger text-[10px]">{result.error}</span>
34+
)}
35+
<StatusIndicator status={status}>{statusDisplay}</StatusIndicator>
36+
</ToolHeader>
37+
</ToolContainer>
38+
);
39+
};

src/browser/components/tools/BashToolCall.tsx

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,17 @@ import {
1111
DetailLabel,
1212
DetailContent,
1313
LoadingDots,
14+
ToolIcon,
15+
ErrorBox,
16+
OutputPaths,
1417
} from "./shared/ToolPrimitives";
15-
import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils";
18+
import {
19+
useToolExpansion,
20+
getStatusDisplay,
21+
formatDuration,
22+
type ToolStatus,
23+
} from "./shared/toolUtils";
1624
import { cn } from "@/common/lib/utils";
17-
import { TooltipWrapper, Tooltip } from "../Tooltip";
1825

1926
interface BashToolCallProps {
2027
args: BashToolArgs;
@@ -23,13 +30,6 @@ interface BashToolCallProps {
2330
startedAt?: number;
2431
}
2532

26-
function formatDuration(ms: number): string {
27-
if (ms < 1000) {
28-
return `${Math.round(ms)}ms`;
29-
}
30-
return `${Math.round(ms / 1000)}s`;
31-
}
32-
3333
export const BashToolCall: React.FC<BashToolCallProps> = ({
3434
args,
3535
result,
@@ -59,35 +59,43 @@ export const BashToolCall: React.FC<BashToolCallProps> = ({
5959
}, [status, startedAt]);
6060

6161
const isPending = status === "executing" || status === "pending";
62+
const isBackground = args.run_in_background ?? (result && "backgroundProcessId" in result);
6263

6364
return (
6465
<ToolContainer expanded={expanded}>
6566
<ToolHeader onClick={toggleExpanded}>
6667
<ExpandIcon expanded={expanded}></ExpandIcon>
67-
<TooltipWrapper inline>
68-
<span>🔧</span>
69-
<Tooltip>bash</Tooltip>
70-
</TooltipWrapper>
68+
<ToolIcon emoji="🔧" toolName="bash" />
7169
<span className="text-text font-monospace max-w-96 truncate">{args.script}</span>
72-
<span
73-
className={cn(
74-
"ml-2 text-[10px] whitespace-nowrap [@container(max-width:500px)]:hidden",
75-
isPending ? "text-pending" : "text-text-secondary"
76-
)}
77-
>
78-
timeout: {args.timeout_secs ?? BASH_DEFAULT_TIMEOUT_SECS}s
79-
{result && ` • took ${formatDuration(result.wall_duration_ms)}`}
80-
{!result && isPending && elapsedTime > 0 && ` • ${formatDuration(elapsedTime)}`}
81-
</span>
82-
{result && (
83-
<span
84-
className={cn(
85-
"ml-2 inline-block shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium whitespace-nowrap",
86-
result.exitCode === 0 ? "bg-success text-on-success" : "bg-danger text-on-danger"
87-
)}
88-
>
89-
{result.exitCode}
70+
{isBackground ? (
71+
// Background mode: show background badge and optional display name
72+
<span className="text-text-secondary ml-2 text-[10px] whitespace-nowrap">
73+
⚡ background{args.display_name && ` • ${args.display_name}`}
9074
</span>
75+
) : (
76+
// Normal mode: show timeout and duration
77+
<>
78+
<span
79+
className={cn(
80+
"ml-2 text-[10px] whitespace-nowrap [@container(max-width:500px)]:hidden",
81+
isPending ? "text-pending" : "text-text-secondary"
82+
)}
83+
>
84+
timeout: {args.timeout_secs ?? BASH_DEFAULT_TIMEOUT_SECS}s
85+
{result && ` • took ${formatDuration(result.wall_duration_ms)}`}
86+
{!result && isPending && elapsedTime > 0 && ` • ${formatDuration(elapsedTime)}`}
87+
</span>
88+
{result && (
89+
<span
90+
className={cn(
91+
"ml-2 inline-block shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium whitespace-nowrap",
92+
result.exitCode === 0 ? "bg-success text-on-success" : "bg-danger text-on-danger"
93+
)}
94+
>
95+
{result.exitCode}
96+
</span>
97+
)}
98+
</>
9199
)}
92100
<StatusIndicator status={status}>{getStatusDisplay(status)}</StatusIndicator>
93101
</ToolHeader>
@@ -104,19 +112,26 @@ export const BashToolCall: React.FC<BashToolCallProps> = ({
104112
{result.success === false && result.error && (
105113
<DetailSection>
106114
<DetailLabel>Error</DetailLabel>
107-
<div className="text-danger bg-danger-overlay border-danger rounded border-l-2 px-2 py-1.5 text-[11px]">
108-
{result.error}
109-
</div>
115+
<ErrorBox>{result.error}</ErrorBox>
110116
</DetailSection>
111117
)}
112118

113-
{result.output && (
119+
{"backgroundProcessId" in result ? (
120+
// Background process: show file paths
114121
<DetailSection>
115-
<DetailLabel>Output</DetailLabel>
116-
<pre className="bg-code-bg border-success m-0 max-h-[200px] overflow-y-auto rounded border-l-2 px-2 py-1.5 text-[11px] leading-[1.4] break-words whitespace-pre-wrap">
117-
{result.output}
118-
</pre>
122+
<DetailLabel>Output Files</DetailLabel>
123+
<OutputPaths stdout={result.stdout_path} stderr={result.stderr_path} />
119124
</DetailSection>
125+
) : (
126+
// Normal process: show output
127+
result.output && (
128+
<DetailSection>
129+
<DetailLabel>Output</DetailLabel>
130+
<pre className="bg-code-bg border-success m-0 max-h-[200px] overflow-y-auto rounded border-l-2 px-2 py-1.5 text-[11px] leading-[1.4] break-words whitespace-pre-wrap">
131+
{result.output}
132+
</pre>
133+
</DetailSection>
134+
)
120135
)}
121136
</>
122137
)}

0 commit comments

Comments
 (0)