Fix xterm clipping; move mic + Jump to Current into status bar #7

Merged
jknapp merged 2 commits from terminal-layout-statusbar into main 2026-06-29 15:23:35 +00:00
2 changed files with 27 additions and 5 deletions
Showing only changes of commit da7b7b9bd5 - Show all commits
+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();