Address review: pin STT transcript, clear stale scroll state
Build App / compute-version (pull_request) Successful in 3s
Build App / build-macos (pull_request) Successful in 2m15s
Build App / build-windows (pull_request) Successful in 3m49s
Build App / build-linux (pull_request) Successful in 4m48s
Build App / create-tag (pull_request) Has been skipped
Build App / sync-to-github (pull_request) Has been skipped

Follow-up to PR review on terminal-layout-statusbar:

- [Major] Pin STT transcripts to the originating terminal. The single
  useSTT instance is bound to the live active session, which can change
  mid-recording. Capture the session id at recording start in a ref and
  inject the transcript there instead of the live sessionId, so text
  always lands in the terminal where recording began.
- [Minor] Clear the status-bar scroll state when the active terminal
  unmounts, and null out termRef on dispose, so scrollActiveToBottom
  can't point at a disposed terminal. Tab switches don't unmount, so
  this only fires when the active session is actually closed.
- [Nit] Fix the terminal padding comment to match the symmetric value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 19:33:27 -07:00
parent 3d2d979197
commit da7b7b9bd5
2 changed files with 27 additions and 5 deletions
+18 -2
View File
@@ -316,6 +316,7 @@ export default function TerminalView({ sessionId, active }: Props) {
try { webglRef.current?.dispose(); } catch { /* may already be disposed */ } try { webglRef.current?.dispose(); } catch { /* may already be disposed */ }
webglRef.current = null; webglRef.current = null;
term.dispose(); term.dispose();
termRef.current = null;
}; };
}, [sessionId]); // eslint-disable-line react-hooks/exhaustive-deps }, [sessionId]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -398,6 +399,21 @@ export default function TerminalView({ sessionId, active }: Props) {
setScrollActiveToBottom(handleScrollToBottom); setScrollActiveToBottom(handleScrollToBottom);
}, [active, isAtBottom, handleScrollToBottom, setTerminalAtBottom, setScrollActiveToBottom]); }, [active, isAtBottom, handleScrollToBottom, setTerminalAtBottom, setScrollActiveToBottom]);
// On unmount, if this was the active terminal, clear the status-bar scroll
// state so it doesn't point at a disposed terminal. (Tab switches don't
// unmount — the deactivating terminal stays mounted but hidden — so this
// only fires when the active session is actually closed.)
const activeRef = useRef(active);
activeRef.current = active;
useEffect(() => {
return () => {
if (activeRef.current) {
setTerminalAtBottom(true);
setScrollActiveToBottom(() => {});
}
};
}, [setTerminalAtBottom, setScrollActiveToBottom]);
const writeSelection = useCallback((mode: "trimmed" | "raw") => { const writeSelection = useCallback((mode: "trimmed" | "raw") => {
const term = termRef.current; const term = termRef.current;
if (!term) return; if (!term) return;
@@ -466,8 +482,8 @@ export default function TerminalView({ sessionId, active }: Props) {
FitAddon measures the host element it's mounted into; padding there FitAddon measures the host element it's mounted into; padding there
causes the grid to overhang and clip the rightmost column / bottom causes the grid to overhang and clip the rightmost column / bottom
row. The host below fills this wrapper's content box with no padding. row. The host below fills this wrapper's content box with no padding.
Kept tight so the terminal claims as much area as possible; right side Kept to a tight, even gutter so the terminal claims as much area as
leaves a little room beside the scrollbar. */} possible while leaving a little breathing room beside the scrollbar. */}
<div className="w-full h-full" style={{ padding: "4px 8px 4px 8px" }}> <div className="w-full h-full" style={{ padding: "4px 8px 4px 8px" }}>
<div <div
ref={containerRef} ref={containerRef}
+9 -3
View File
@@ -13,6 +13,11 @@ export function useSTT(sessionId: string, sendInput: (sessionId: string, data: s
const streamRef = useRef<MediaStream | null>(null); const streamRef = useRef<MediaStream | null>(null);
const workletRef = useRef<AudioWorkletNode | null>(null); const workletRef = useRef<AudioWorkletNode | null>(null);
const chunksRef = useRef<Int16Array[]>([]); const chunksRef = useRef<Int16Array[]>([]);
// Pin the transcript to the terminal that was active when recording STARTED.
// The hook is bound to the live active session, which can change mid-recording
// (the user switches tabs); without this the transcript would land in whatever
// tab is active at stop time.
const recordingSessionIdRef = useRef(sessionId);
const appSettings = useAppState((s) => s.appSettings); const appSettings = useAppState((s) => s.appSettings);
const deviceId = appSettings?.default_microphone; const deviceId = appSettings?.default_microphone;
@@ -22,6 +27,7 @@ export function useSTT(sessionId: string, sendInput: (sessionId: string, data: s
setState("recording"); setState("recording");
setError(null); setError(null);
chunksRef.current = []; chunksRef.current = [];
recordingSessionIdRef.current = sessionId;
try { try {
const audioConstraints: MediaTrackConstraints = { const audioConstraints: MediaTrackConstraints = {
@@ -57,7 +63,7 @@ export function useSTT(sessionId: string, sendInput: (sessionId: string, data: s
setError(msg); setError(msg);
setState("error"); setState("error");
} }
}, [state, deviceId]); }, [state, deviceId, sessionId]);
const stopRecording = useCallback(async () => { const stopRecording = useCallback(async () => {
if (state !== "recording") return; if (state !== "recording") return;
@@ -102,7 +108,7 @@ export function useSTT(sessionId: string, sendInput: (sessionId: string, data: s
const text = await commands.transcribeAudio(audioData); const text = await commands.transcribeAudio(audioData);
if (text) { if (text) {
await sendInput(sessionId, text); await sendInput(recordingSessionIdRef.current, text);
} }
setState("idle"); setState("idle");
} catch (e) { } catch (e) {
@@ -112,7 +118,7 @@ export function useSTT(sessionId: string, sendInput: (sessionId: string, data: s
// Reset to idle after a brief delay so the UI shows the error // Reset to idle after a brief delay so the UI shows the error
setTimeout(() => setState("idle"), 3000); setTimeout(() => setState("idle"), 3000);
} }
}, [state, sessionId, sendInput]); }, [state, sendInput]);
const cancelRecording = useCallback(async () => { const cancelRecording = useCallback(async () => {
workletRef.current?.disconnect(); workletRef.current?.disconnect();