Skip to content

Commit 34895ca

Browse files
🤖 feat: add visual feedback when terminating background processes (#1062)
When clicking the terminate button on a background process, the X icon immediately swaps to a spinner and the row dims, giving instant visual feedback. ## Changes - Track `terminatingIds` in `useBackgroundBashHandlers` hook - Pass to `BackgroundProcessesBanner` for visual styling - Show spinning `Loader2` icon instead of X while terminating - Dim row with `opacity-50` + `pointer-events-none` - Clear stale IDs when processes change (via subscription) to handle process restarts ## Why only clear on failure? Clearing on success causes a flash: the promise resolves slightly before the subscription arrives, briefly showing the X again. By only clearing on failure, the spinner stays until the subscription removes the row. _Generated with mux_
1 parent 32a26d9 commit 34895ca

File tree

3 files changed

+75
-32
lines changed

3 files changed

+75
-32
lines changed

‎src/browser/components/AIView.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
126126

127127
const {
128128
processes: backgroundBashes,
129+
terminatingIds: backgroundBashTerminatingIds,
129130
handleTerminate: handleTerminateBackgroundBash,
130131
foregroundToolCallIds,
131132
handleSendToBackground: handleSendBashToBackground,
@@ -682,6 +683,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
682683
)}
683684
<BackgroundProcessesBanner
684685
processes={backgroundBashes}
686+
terminatingIds={backgroundBashTerminatingIds}
685687
onTerminate={handleTerminateBackgroundBash}
686688
/>
687689
<ReviewsBanner workspaceId={workspaceId} />

‎src/browser/components/BackgroundProcessesBanner.tsx‎

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useCallback, useEffect } from "react";
2-
import { Terminal, X, ChevronDown, ChevronRight } from "lucide-react";
2+
import { Terminal, X, ChevronDown, ChevronRight, Loader2 } from "lucide-react";
33
import { Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip";
44
import type { BackgroundProcessInfo } from "@/common/orpc/schemas/api";
55
import { cn } from "@/common/lib/utils";
@@ -19,6 +19,7 @@ function truncateScript(script: string, maxLength = 60): string {
1919

2020
interface BackgroundProcessesBannerProps {
2121
processes: BackgroundProcessInfo[];
22+
terminatingIds: Set<string>;
2223
onTerminate: (processId: string) => void;
2324
}
2425

@@ -85,39 +86,51 @@ export const BackgroundProcessesBanner: React.FC<BackgroundProcessesBannerProps>
8586
{/* Expanded view - content aligned with chat */}
8687
{isExpanded && (
8788
<div className="border-border mx-auto max-h-48 max-w-4xl space-y-1.5 overflow-y-auto border-t py-2">
88-
{runningProcesses.map((proc) => (
89-
<div
90-
key={proc.id}
91-
className={cn(
92-
"hover:bg-hover flex items-center justify-between gap-3 rounded px-2 py-1.5",
93-
"transition-colors"
94-
)}
95-
>
96-
<div className="min-w-0 flex-1">
97-
<div className="text-foreground truncate font-mono text-xs" title={proc.script}>
98-
{proc.displayName ?? truncateScript(proc.script)}
89+
{runningProcesses.map((proc) => {
90+
const isTerminating = props.terminatingIds.has(proc.id);
91+
return (
92+
<div
93+
key={proc.id}
94+
className={cn(
95+
"hover:bg-hover flex items-center justify-between gap-3 rounded px-2 py-1.5",
96+
"transition-colors",
97+
isTerminating && "pointer-events-none opacity-50"
98+
)}
99+
>
100+
<div className="min-w-0 flex-1">
101+
<div className="text-foreground truncate font-mono text-xs" title={proc.script}>
102+
{proc.displayName ?? truncateScript(proc.script)}
103+
</div>
104+
<div className="text-muted font-mono text-[10px]">pid {proc.pid}</div>
105+
</div>
106+
<div className="flex shrink-0 items-center gap-2">
107+
<span className="text-muted text-[10px]">
108+
{formatDuration(Date.now() - proc.startTime)}
109+
</span>
110+
<Tooltip>
111+
<TooltipTrigger asChild>
112+
<button
113+
type="button"
114+
disabled={isTerminating}
115+
onClick={(e) => handleTerminate(proc.id, e)}
116+
className={cn(
117+
"text-muted hover:text-error rounded p-1 transition-colors",
118+
isTerminating && "cursor-not-allowed"
119+
)}
120+
>
121+
{isTerminating ? (
122+
<Loader2 size={14} className="animate-spin" />
123+
) : (
124+
<X size={14} />
125+
)}
126+
</button>
127+
</TooltipTrigger>
128+
<TooltipContent>Terminate process</TooltipContent>
129+
</Tooltip>
99130
</div>
100-
<div className="text-muted font-mono text-[10px]">pid {proc.pid}</div>
101-
</div>
102-
<div className="flex shrink-0 items-center gap-2">
103-
<span className="text-muted text-[10px]">
104-
{formatDuration(Date.now() - proc.startTime)}
105-
</span>
106-
<Tooltip>
107-
<TooltipTrigger asChild>
108-
<button
109-
type="button"
110-
onClick={(e) => handleTerminate(proc.id, e)}
111-
className="text-muted hover:text-error rounded p-1 transition-colors"
112-
>
113-
<X size={14} />
114-
</button>
115-
</TooltipTrigger>
116-
<TooltipContent>Terminate process</TooltipContent>
117-
</Tooltip>
118131
</div>
119-
</div>
120-
))}
132+
);
133+
})}
121134
</div>
122135
)}
123136
</div>

‎src/browser/hooks/useBackgroundBashHandlers.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export function useBackgroundBashHandlers(
2323
): {
2424
/** List of background processes */
2525
processes: BackgroundProcessInfo[];
26+
/** Set of process IDs currently being terminated */
27+
terminatingIds: Set<string>;
2628
/** Terminate a background process */
2729
handleTerminate: (processId: string) => void;
2830
/** Set of tool call IDs of foreground bashes */
@@ -36,6 +38,8 @@ export function useBackgroundBashHandlers(
3638
} {
3739
const [processes, setProcesses] = useState<BackgroundProcessInfo[]>(EMPTY_PROCESSES);
3840
const [foregroundToolCallIds, setForegroundToolCallIds] = useState<Set<string>>(EMPTY_SET);
41+
// Process IDs currently being terminated (for visual feedback)
42+
const [terminatingIds, setTerminatingIds] = useState<Set<string>>(EMPTY_SET);
3943
// Keep a ref for handleMessageSentBackground to avoid recreating on every change
4044
const foregroundIdsRef = useRef<Set<string>>(EMPTY_SET);
4145
const error = usePopoverError();
@@ -86,6 +90,7 @@ export function useBackgroundBashHandlers(
8690
if (!api || !workspaceId) {
8791
setProcesses(EMPTY_PROCESSES);
8892
setForegroundToolCallIds(EMPTY_SET);
93+
setTerminatingIds(EMPTY_SET);
8994
return;
9095
}
9196

@@ -104,6 +109,17 @@ export function useBackgroundBashHandlers(
104109

105110
setProcesses(state.processes);
106111
setForegroundToolCallIds(new Set(state.foregroundToolCallIds));
112+
113+
// Clear terminating IDs for processes that are no longer running
114+
// (killed/exited/failed should clear so new processes with same name aren't affected)
115+
const runningIds = new Set(
116+
state.processes.filter((p) => p.status === "running").map((p) => p.id)
117+
);
118+
setTerminatingIds((prev) => {
119+
if (prev.size === 0) return prev;
120+
const stillRunning = new Set([...prev].filter((id) => runningIds.has(id)));
121+
return stillRunning.size === prev.size ? prev : stillRunning;
122+
});
107123
}
108124
} catch (err) {
109125
if (!signal.aborted) {
@@ -122,7 +138,17 @@ export function useBackgroundBashHandlers(
122138
const { showError } = error;
123139
const handleTerminate = useCallback(
124140
(processId: string) => {
141+
// Mark as terminating immediately for visual feedback
142+
setTerminatingIds((prev) => new Set(prev).add(processId));
143+
125144
terminate(processId).catch((err: Error) => {
145+
// Only clear on FAILURE - restore to normal so user can retry
146+
// On success: don't clear - subscription removes the process while still dimmed
147+
setTerminatingIds((prev) => {
148+
const next = new Set(prev);
149+
next.delete(processId);
150+
return next;
151+
});
126152
showError(processId, err.message);
127153
});
128154
},
@@ -150,6 +176,7 @@ export function useBackgroundBashHandlers(
150176
return useMemo(
151177
() => ({
152178
processes,
179+
terminatingIds,
153180
handleTerminate,
154181
foregroundToolCallIds,
155182
handleSendToBackground,
@@ -158,6 +185,7 @@ export function useBackgroundBashHandlers(
158185
}),
159186
[
160187
processes,
188+
terminatingIds,
161189
handleTerminate,
162190
foregroundToolCallIds,
163191
handleSendToBackground,

0 commit comments

Comments
 (0)