Skip to content

Commit 791374b

Browse files
committed
🤖 fix: stream bash output in output pane
Change-Id: Iaa4abaa85fe259214f09039381233c37726df175 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent f559ea5 commit 791374b

File tree

1 file changed

+20
-54
lines changed

1 file changed

+20
-54
lines changed

src/browser/components/tools/BashToolCall.tsx

Lines changed: 20 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
DetailSection,
1212
DetailLabel,
1313
DetailContent,
14-
LoadingDots,
1514
ToolIcon,
1615
ErrorBox,
1716
ExitCodeBadge,
@@ -60,31 +59,24 @@ export const BashToolCall: React.FC<BashToolCallProps> = ({
6059

6160
const liveOutput = useBashToolLiveOutput(workspaceId, toolCallId);
6261

63-
const stdoutRef = useRef<HTMLPreElement>(null);
64-
const stderrRef = useRef<HTMLPreElement>(null);
65-
const stdoutPinnedRef = useRef(true);
66-
const stderrPinnedRef = useRef(true);
62+
const outputRef = useRef<HTMLPreElement>(null);
63+
const outputPinnedRef = useRef(true);
6764

68-
const updatePinned = (el: HTMLPreElement, pinnedRef: React.MutableRefObject<boolean>) => {
65+
const updatePinned = (el: HTMLPreElement) => {
6966
const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
70-
pinnedRef.current = distanceToBottom < 40;
67+
outputPinnedRef.current = distanceToBottom < 40;
7168
};
7269

73-
useEffect(() => {
74-
const el = stdoutRef.current;
75-
if (!el) return;
76-
if (stdoutPinnedRef.current) {
77-
el.scrollTop = el.scrollHeight;
78-
}
79-
}, [liveOutput?.stdout]);
70+
const liveOutputView = liveOutput ?? EMPTY_LIVE_OUTPUT;
71+
const combinedLiveOutput = liveOutputView.stdout + liveOutputView.stderr;
8072

8173
useEffect(() => {
82-
const el = stderrRef.current;
74+
const el = outputRef.current;
8375
if (!el) return;
84-
if (stderrPinnedRef.current) {
76+
if (outputPinnedRef.current) {
8577
el.scrollTop = el.scrollHeight;
8678
}
87-
}, [liveOutput?.stderr]);
79+
}, [combinedLiveOutput]);
8880
const startTimeRef = useRef<number>(startedAt ?? Date.now());
8981

9082
// Track elapsed time for pending/executing status
@@ -115,13 +107,9 @@ export const BashToolCall: React.FC<BashToolCallProps> = ({
115107

116108
const resultHasOutput = typeof (result as { output?: unknown } | undefined)?.output === "string";
117109

118-
const hasLiveOutputSource = Boolean(workspaceId && toolCallId);
119110
const showLiveOutput =
120-
!isBackground &&
121-
hasLiveOutputSource &&
122-
(status === "executing" || (Boolean(liveOutput) && !resultHasOutput));
111+
!isBackground && (status === "executing" || (Boolean(liveOutput) && !resultHasOutput));
123112

124-
const liveOutputView = liveOutput ?? EMPTY_LIVE_OUTPUT;
125113
const liveLabelSuffix = status === "executing" ? " (live)" : " (tail)";
126114

127115
return (
@@ -187,6 +175,11 @@ export const BashToolCall: React.FC<BashToolCallProps> = ({
187175

188176
{expanded && (
189177
<ToolDetails>
178+
<DetailSection>
179+
<DetailLabel>Script</DetailLabel>
180+
<DetailContent className="px-2 py-1.5">{args.script}</DetailContent>
181+
</DetailSection>
182+
190183
{showLiveOutput && (
191184
<>
192185
{liveOutputView.truncated && (
@@ -196,38 +189,20 @@ export const BashToolCall: React.FC<BashToolCallProps> = ({
196189
)}
197190

198191
<DetailSection>
199-
<DetailLabel>{`Stdout${liveLabelSuffix}`}</DetailLabel>
200-
<DetailContent
201-
ref={stdoutRef}
202-
onScroll={(e) => updatePinned(e.currentTarget, stdoutPinnedRef)}
203-
className={cn(
204-
"px-2 py-1.5",
205-
liveOutputView.stdout.length === 0 && "text-muted italic"
206-
)}
207-
>
208-
{liveOutputView.stdout.length > 0 ? liveOutputView.stdout : "No output yet"}
209-
</DetailContent>
210-
</DetailSection>
211-
212-
<DetailSection>
213-
<DetailLabel>{`Stderr${liveLabelSuffix}`}</DetailLabel>
192+
<DetailLabel>{`Output${liveLabelSuffix}`}</DetailLabel>
214193
<DetailContent
215-
ref={stderrRef}
216-
onScroll={(e) => updatePinned(e.currentTarget, stderrPinnedRef)}
194+
ref={outputRef}
195+
onScroll={(e) => updatePinned(e.currentTarget)}
217196
className={cn(
218197
"px-2 py-1.5",
219-
liveOutputView.stderr.length === 0 && "text-muted italic"
198+
combinedLiveOutput.length === 0 && "text-muted italic"
220199
)}
221200
>
222-
{liveOutputView.stderr.length > 0 ? liveOutputView.stderr : "No output yet"}
201+
{combinedLiveOutput.length > 0 ? combinedLiveOutput : "No output yet"}
223202
</DetailContent>
224203
</DetailSection>
225204
</>
226205
)}
227-
<DetailSection>
228-
<DetailLabel>Script</DetailLabel>
229-
<DetailContent className="px-2 py-1.5">{args.script}</DetailContent>
230-
</DetailSection>
231206

232207
{result && (
233208
<>
@@ -258,15 +233,6 @@ export const BashToolCall: React.FC<BashToolCallProps> = ({
258233
)}
259234
</>
260235
)}
261-
262-
{status === "executing" && !result && !showLiveOutput && (
263-
<DetailSection>
264-
<DetailContent className="px-2 py-1.5">
265-
Waiting for result
266-
<LoadingDots />
267-
</DetailContent>
268-
</DetailSection>
269-
)}
270236
</ToolDetails>
271237
)}
272238
</ToolContainer>

0 commit comments

Comments
 (0)