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 */ }
webglRef.current = null;
term.dispose();
termRef.current = null;
};
}, [sessionId]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -398,6 +399,21 @@ export default function TerminalView({ sessionId, active }: Props) {
setScrollActiveToBottom(handleScrollToBottom);
}, [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 term = termRef.current;
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
causes the grid to overhang and clip the rightmost column / bottom
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
leaves a little room beside the scrollbar. */}
Kept to a tight, even gutter so the terminal claims as much area as
possible while leaving a little breathing room beside the scrollbar. */}
<div className="w-full h-full" style={{ padding: "4px 8px 4px 8px" }}>
<div
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 workletRef = useRef<AudioWorkletNode | null>(null);
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 deviceId = appSettings?.default_microphone;
@@ -22,6 +27,7 @@ export function useSTT(sessionId: string, sendInput: (sessionId: string, data: s
setState("recording");
setError(null);
chunksRef.current = [];
recordingSessionIdRef.current = sessionId;
try {
const audioConstraints: MediaTrackConstraints = {
@@ -57,7 +63,7 @@ export function useSTT(sessionId: string, sendInput: (sessionId: string, data: s
setError(msg);
setState("error");
}
}, [state, deviceId]);
}, [state, deviceId, sessionId]);
const stopRecording = useCallback(async () => {
if (state !== "recording") return;
@@ -102,7 +108,7 @@ export function useSTT(sessionId: string, sendInput: (sessionId: string, data: s
const text = await commands.transcribeAudio(audioData);
if (text) {
await sendInput(sessionId, text);
await sendInput(recordingSessionIdRef.current, text);
}
setState("idle");
} 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
setTimeout(() => setState("idle"), 3000);
}
}, [state, sessionId, sendInput]);
}, [state, sendInput]);
const cancelRecording = useCallback(async () => {
workletRef.current?.disconnect();