Skip to content

Commit 0a76e44

Browse files
committed
🤖 feat: add UI styling for background bash processes
- BashToolCall: show '⚡ background • display_name' for background spawns instead of timeout/duration; show 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 - Wire up new components in ToolMessage.tsx _Generated with mux_
1 parent c20e98e commit 0a76e44

File tree

4 files changed

+283
-23
lines changed

4 files changed

+283
-23
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: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
} from "./shared/ToolPrimitives";
16+
import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils";
17+
import { cn } from "@/common/lib/utils";
18+
import { TooltipWrapper, Tooltip } from "../Tooltip";
19+
20+
interface BashBackgroundListToolCallProps {
21+
args: BashBackgroundListArgs;
22+
result?: BashBackgroundListResult;
23+
status?: ToolStatus;
24+
}
25+
26+
function formatUptime(ms: number): string {
27+
if (ms < 1000) return `${Math.round(ms)}ms`;
28+
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
29+
if (ms < 3600000) return `${Math.round(ms / 60000)}m`;
30+
return `${Math.round(ms / 3600000)}h`;
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 processCount = processes.length;
54+
55+
return (
56+
<ToolContainer expanded={expanded}>
57+
<ToolHeader onClick={toggleExpanded}>
58+
<ExpandIcon expanded={expanded}></ExpandIcon>
59+
<TooltipWrapper inline>
60+
<span>📋</span>
61+
<Tooltip>bash_background_list</Tooltip>
62+
</TooltipWrapper>
63+
<span className="text-text-secondary">
64+
{result?.success
65+
? processCount === 0
66+
? "No background processes"
67+
: `${processCount} background process${processCount !== 1 ? "es" : ""}`
68+
: "Listing background processes"}
69+
</span>
70+
<StatusIndicator status={status}>{getStatusDisplay(status)}</StatusIndicator>
71+
</ToolHeader>
72+
73+
{expanded && (
74+
<ToolDetails>
75+
{result?.success === false && (
76+
<DetailSection>
77+
<div className="text-danger bg-danger-overlay border-danger rounded border-l-2 px-2 py-1.5 text-[11px]">
78+
{result.error}
79+
</div>
80+
</DetailSection>
81+
)}
82+
83+
{result?.success && processes.length > 0 && (
84+
<DetailSection>
85+
<div className="space-y-2">
86+
{processes.map((proc) => (
87+
<div
88+
key={proc.display_name ?? proc.process_id}
89+
className="bg-code-bg rounded px-2 py-1.5 text-[11px]"
90+
>
91+
<div className="mb-1 flex items-center gap-2">
92+
<span className="text-text font-mono">
93+
{proc.display_name ?? proc.process_id}
94+
</span>
95+
<span
96+
className={cn(
97+
"inline-block rounded px-1.5 py-0.5 text-[9px] font-medium uppercase",
98+
getProcessStatusStyle(proc.status)
99+
)}
100+
>
101+
{proc.status}
102+
{proc.exitCode !== undefined && ` (${proc.exitCode})`}
103+
</span>
104+
<span className="text-text-secondary ml-auto">
105+
{formatUptime(proc.uptime_ms)}
106+
</span>
107+
</div>
108+
<div className="text-text-secondary truncate font-mono" title={proc.script}>
109+
{proc.script}
110+
</div>
111+
<div className="text-text-secondary mt-1 space-y-0.5 text-[10px]">
112+
<div>
113+
<span className="opacity-60">stdout:</span> {proc.stdout_path}
114+
</div>
115+
<div>
116+
<span className="opacity-60">stderr:</span> {proc.stderr_path}
117+
</div>
118+
</div>
119+
</div>
120+
))}
121+
</div>
122+
</DetailSection>
123+
)}
124+
125+
{result?.success && processes.length === 0 && (
126+
<DetailSection>
127+
<div className="text-text-secondary text-[11px] italic">
128+
No background processes running
129+
</div>
130+
</DetailSection>
131+
)}
132+
133+
{status === "executing" && !result && (
134+
<DetailSection>
135+
<div className="text-[11px]">
136+
Listing processes
137+
<LoadingDots />
138+
</div>
139+
</DetailSection>
140+
)}
141+
</ToolDetails>
142+
)}
143+
</ToolContainer>
144+
);
145+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from "react";
2+
import type {
3+
BashBackgroundTerminateArgs,
4+
BashBackgroundTerminateResult,
5+
} from "@/common/types/tools";
6+
import { ToolContainer, ToolHeader, StatusIndicator } from "./shared/ToolPrimitives";
7+
import { getStatusDisplay, type ToolStatus } from "./shared/toolUtils";
8+
import { TooltipWrapper, Tooltip } from "../Tooltip";
9+
10+
interface BashBackgroundTerminateToolCallProps {
11+
args: BashBackgroundTerminateArgs;
12+
result?: BashBackgroundTerminateResult;
13+
status?: ToolStatus;
14+
}
15+
16+
export const BashBackgroundTerminateToolCall: React.FC<BashBackgroundTerminateToolCallProps> = ({
17+
args,
18+
result,
19+
status = "pending",
20+
}) => {
21+
const statusDisplay = getStatusDisplay(status);
22+
23+
return (
24+
<ToolContainer expanded={false}>
25+
<ToolHeader>
26+
<TooltipWrapper inline>
27+
<span>⏹️</span>
28+
<Tooltip>bash_background_terminate</Tooltip>
29+
</TooltipWrapper>
30+
<span className="text-text font-mono">
31+
{result?.success === true ? (result.display_name ?? args.process_id) : args.process_id}
32+
</span>
33+
{result?.success === true && (
34+
<span className="text-text-secondary text-[10px]">terminated</span>
35+
)}
36+
{result?.success === false && (
37+
<span className="text-danger text-[10px]">{result.error}</span>
38+
)}
39+
<StatusIndicator status={status}>{statusDisplay}</StatusIndicator>
40+
</ToolHeader>
41+
</ToolContainer>
42+
);
43+
};

src/browser/components/tools/BashToolCall.tsx

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ 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}>
@@ -69,25 +70,35 @@ export const BashToolCall: React.FC<BashToolCallProps> = ({
6970
<Tooltip>bash</Tooltip>
7071
</TooltipWrapper>
7172
<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}
73+
{isBackground ? (
74+
// Background mode: show background badge and optional display name
75+
<span className="text-text-secondary ml-2 text-[10px] whitespace-nowrap">
76+
⚡ background{args.display_name && ` • ${args.display_name}`}
9077
</span>
78+
) : (
79+
// Normal mode: show timeout and duration
80+
<>
81+
<span
82+
className={cn(
83+
"ml-2 text-[10px] whitespace-nowrap [@container(max-width:500px)]:hidden",
84+
isPending ? "text-pending" : "text-text-secondary"
85+
)}
86+
>
87+
timeout: {args.timeout_secs ?? BASH_DEFAULT_TIMEOUT_SECS}s
88+
{result && ` • took ${formatDuration(result.wall_duration_ms)}`}
89+
{!result && isPending && elapsedTime > 0 && ` • ${formatDuration(elapsedTime)}`}
90+
</span>
91+
{result && (
92+
<span
93+
className={cn(
94+
"ml-2 inline-block shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium whitespace-nowrap",
95+
result.exitCode === 0 ? "bg-success text-on-success" : "bg-danger text-on-danger"
96+
)}
97+
>
98+
{result.exitCode}
99+
</span>
100+
)}
101+
</>
91102
)}
92103
<StatusIndicator status={status}>{getStatusDisplay(status)}</StatusIndicator>
93104
</ToolHeader>
@@ -110,13 +121,31 @@ export const BashToolCall: React.FC<BashToolCallProps> = ({
110121
</DetailSection>
111122
)}
112123

113-
{result.output && (
124+
{"backgroundProcessId" in result ? (
125+
// Background process: show file paths
114126
<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>
127+
<DetailLabel>Output Files</DetailLabel>
128+
<div className="bg-code-bg space-y-1 rounded px-2 py-1.5 font-mono text-[11px]">
129+
<div>
130+
<span className="text-text-secondary">stdout:</span>{" "}
131+
<span className="text-text">{result.stdout_path}</span>
132+
</div>
133+
<div>
134+
<span className="text-text-secondary">stderr:</span>{" "}
135+
<span className="text-text">{result.stderr_path}</span>
136+
</div>
137+
</div>
119138
</DetailSection>
139+
) : (
140+
// Normal process: show output
141+
result.output && (
142+
<DetailSection>
143+
<DetailLabel>Output</DetailLabel>
144+
<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">
145+
{result.output}
146+
</pre>
147+
</DetailSection>
148+
)
120149
)}
121150
</>
122151
)}

0 commit comments

Comments
 (0)