Terminal layout fixes for the xterm pane, plus relocating two floating overlay controls into the status bar.
Changes
Fix right/bottom clipping of the terminal grid. The padding was applied directly to the element term.open() mounts into, which xterm's FitAddon also measures — so the computed grid overhung the padding and the rightmost column / bottom row got clipped (visible as text sliced mid-character at the window edge). Padding now lives on a wrapper <div>; the xterm host fills that wrapper's content box with no padding of its own.
Move the STT mic into the status bar (far right). Previously one useSTT instance per TerminalView rendered a floating bottom-left button. Now a single useSTT bound to the active session lives in App, and the mic renders inline in StatusBar. Ctrl+Shift+M is routed through the store (sttToggle) so the shortcut still toggles the active session's mic.
Move "Jump to Current" into the status bar. The active TerminalView surfaces its scroll state (terminalAtBottom) and scroll action (scrollActiveToBottom) via the store; the status bar shows the control only when the active terminal is scrolled up.
Tighten terminal padding (8px 12px 48px 16px → 4px 8px 4px 8px) now that nothing floats over the terminal, so it claims as much area as possible.
Testing
npm run build (tsc + vite) — clean
npx vitest run — 33 passed (the 7 reported errors are pre-existing, from ProjectCard.test.tsx's listen mock, unrelated to this change)
npx tauri build — compiled and produced the .deb/.rpm bundles successfully
## Summary
Terminal layout fixes for the xterm pane, plus relocating two floating overlay controls into the status bar.
### Changes
- **Fix right/bottom clipping of the terminal grid.** The padding was applied directly to the element `term.open()` mounts into, which xterm's `FitAddon` also measures — so the computed grid overhung the padding and the rightmost column / bottom row got clipped (visible as text sliced mid-character at the window edge). Padding now lives on a wrapper `<div>`; the xterm host fills that wrapper's content box with no padding of its own.
- **Move the STT mic into the status bar** (far right). Previously one `useSTT` instance per `TerminalView` rendered a floating bottom-left button. Now a single `useSTT` bound to the **active session** lives in `App`, and the mic renders inline in `StatusBar`. `Ctrl+Shift+M` is routed through the store (`sttToggle`) so the shortcut still toggles the active session's mic.
- **Move "Jump to Current" into the status bar.** The active `TerminalView` surfaces its scroll state (`terminalAtBottom`) and scroll action (`scrollActiveToBottom`) via the store; the status bar shows the control only when the active terminal is scrolled up.
- **Tighten terminal padding** (`8px 12px 48px 16px` → `4px 8px 4px 8px`) now that nothing floats over the terminal, so it claims as much area as possible.
### Testing
- `npm run build` (tsc + vite) — clean
- `npx vitest run` — 33 passed (the 7 reported errors are pre-existing, from `ProjectCard.test.tsx`'s `listen` mock, unrelated to this change)
- `npx tauri build` — compiled and produced the `.deb`/`.rpm` bundles successfully
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Terminal layout fixes for the xterm pane:
- Stop the terminal grid from clipping its rightmost column / bottom
row. The padding was on the element xterm mounts into, which the
FitAddon measures; the grid overhang got clipped. Padding now lives on
a wrapper and the xterm host fills it with no padding.
- Move the STT mic from a floating bottom-left overlay into the status
bar (far right). A single useSTT instance bound to the active session
now lives in App; Ctrl+Shift+M routes through the store.
- Move "Jump to Current" from a floating terminal overlay into the
status bar. The active TerminalView surfaces its scroll state and
scroll action via the store.
- Tighten terminal padding (was 8/12/48/16) now that nothing floats over
it, so the terminal claims as much area as possible.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verdict: Largely correct. The zustand-function-storage pattern is used correctly, hook deps are sound, and background terminals cannot clobber the active one. One genuine behavioral regression around mid-recording tab switches, plus minor items.
Findings
1. [Major] STT transcript goes to whatever tab is active at stop time, not where recording started — App.tsx + hooks/useSTT.ts
A single useSTT(activeSessionId ?? "", sendInput) lives in App. The recording lifecycle does not reset when activeSessionId changes. Start recording in session A, switch to B, stop → stopRecording closes over the current sessionId (B) and injects the transcript into terminal B. Regression vs. the old per-terminal instance. Fix: capture the target session at recording start (a recordingSessionIdRef set in startRecording, used by stopRecording instead of the live sessionId). Keeps a single mic while pinning the transcript to the originating terminal.
2. [Minor] Stale scrollActiveToBottom/terminalAtBottom after the active terminal unmounts — TerminalView.tsx, store defaults
The registration effect only writes when active (good), but nothing clears the store on close. After closing the last session, scrollActiveToBottom still points at the disposed terminal's handler (termRef.current is never reset to null in cleanup). Masked in practice because StatusBar gates both controls behind activeSessionId. A reset in the unmount path would be more robust.
3. [Nit] Padding comment doesn't match the value — TerminalView.tsx
Comment says "right side leaves a little room beside the scrollbar," but padding is symmetric 4px 8px 4px 8px.
Confirmed correct
Zustand function storage:set({ sttToggle: fn }) passes an object → shallow-merged and stored verbatim, not misinterpreted as a state updater. Correct.
setSttToggle effect deps[stt.toggle, setSttToggle] are right; stt.toggle identity changes with STT state so the store always holds the latest. Read via getState() at call time — no stale closure, no render loop.
Multi-terminal isolation: inactive terminals early-return, so background output never writes scroll state. On tab switch the activating effect wins regardless of order. Correct.
Dangling references:useSTT/SttButton fully removed from TerminalView; no leftover imports.
xterm padding fix: host containerRef now has no padding; padding moved to wrapper. FitAddon + ResizeObserver measure the host, so the grid no longer overhangs. Correct.
No-active-session guards:StatusBar hides both controls when !activeSessionId; store defaults are safe no-ops.
## Automated code review (Claude agent)
**Verdict:** Largely correct. The zustand-function-storage pattern is used correctly, hook deps are sound, and background terminals cannot clobber the active one. One genuine behavioral regression around mid-recording tab switches, plus minor items.
### Findings
**1. [Major] STT transcript goes to whatever tab is active at *stop* time, not where recording started** — `App.tsx` + `hooks/useSTT.ts`
A single `useSTT(activeSessionId ?? "", sendInput)` lives in `App`. The recording lifecycle does not reset when `activeSessionId` changes. Start recording in session A, switch to B, stop → `stopRecording` closes over the current `sessionId` (B) and injects the transcript into terminal B. Regression vs. the old per-terminal instance.
_Fix:_ capture the target session at recording *start* (a `recordingSessionIdRef` set in `startRecording`, used by `stopRecording` instead of the live `sessionId`). Keeps a single mic while pinning the transcript to the originating terminal.
**2. [Minor] Stale `scrollActiveToBottom`/`terminalAtBottom` after the active terminal unmounts** — `TerminalView.tsx`, store defaults
The registration effect only writes when `active` (good), but nothing clears the store on close. After closing the last session, `scrollActiveToBottom` still points at the disposed terminal's handler (`termRef.current` is never reset to null in cleanup). Masked in practice because `StatusBar` gates both controls behind `activeSessionId`. A reset in the unmount path would be more robust.
**3. [Nit] Padding comment doesn't match the value** — `TerminalView.tsx`
Comment says "right side leaves a little room beside the scrollbar," but padding is symmetric `4px 8px 4px 8px`.
### Confirmed correct
- **Zustand function storage:** `set({ sttToggle: fn })` passes an object → shallow-merged and stored verbatim, not misinterpreted as a state updater. Correct.
- **`setSttToggle` effect deps** `[stt.toggle, setSttToggle]` are right; `stt.toggle` identity changes with STT state so the store always holds the latest. Read via `getState()` at call time — no stale closure, no render loop.
- **Multi-terminal isolation:** inactive terminals early-return, so background output never writes scroll state. On tab switch the activating effect wins regardless of order. Correct.
- **Dangling references:** `useSTT`/`SttButton` fully removed from `TerminalView`; no leftover imports.
- **xterm padding fix:** host `containerRef` now has no padding; padding moved to wrapper. `FitAddon` + `ResizeObserver` measure the host, so the grid no longer overhangs. Correct.
- **No-active-session guards:** `StatusBar` hides both controls when `!activeSessionId`; store defaults are safe no-ops.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
[Major] STT transcript routing — startRecording now captures the active session id into recordingSessionIdRef; stopRecording injects the transcript via that ref instead of the live sessionId. Switching tabs mid-recording no longer misroutes the transcript — it lands in the terminal where recording began. (startRecording deps gained sessionId; stopRecording no longer depends on it.)
[Minor] Stale scroll state — termRef.current is now nulled on dispose, and an unmount effect clears terminalAtBottom/scrollActiveToBottom when the active terminal is closed (guarded by an activeRef, so tab switches — which don't unmount — are unaffected and the newly-active terminal's effect re-writes fresh values after).
[Nit] Padding comment — reworded to match the symmetric 4px 8px gutter.
Verified: tsc clean, vitest 33 passed, vite build clean.
## Review follow-up (commit da7b7b9)
All three findings addressed:
- **[Major] STT transcript routing** — `startRecording` now captures the active session id into `recordingSessionIdRef`; `stopRecording` injects the transcript via that ref instead of the live `sessionId`. Switching tabs mid-recording no longer misroutes the transcript — it lands in the terminal where recording began. (`startRecording` deps gained `sessionId`; `stopRecording` no longer depends on it.)
- **[Minor] Stale scroll state** — `termRef.current` is now nulled on dispose, and an unmount effect clears `terminalAtBottom`/`scrollActiveToBottom` when the *active* terminal is closed (guarded by an `activeRef`, so tab switches — which don't unmount — are unaffected and the newly-active terminal's effect re-writes fresh values after).
- **[Nit] Padding comment** — reworded to match the symmetric `4px 8px` gutter.
Verified: `tsc` clean, `vitest` 33 passed, `vite build` clean.
jknapp
merged commit 1716fb5c82 into main2026-06-29 15:23:35 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Terminal layout fixes for the xterm pane, plus relocating two floating overlay controls into the status bar.
Changes
term.open()mounts into, which xterm'sFitAddonalso measures — so the computed grid overhung the padding and the rightmost column / bottom row got clipped (visible as text sliced mid-character at the window edge). Padding now lives on a wrapper<div>; the xterm host fills that wrapper's content box with no padding of its own.useSTTinstance perTerminalViewrendered a floating bottom-left button. Now a singleuseSTTbound to the active session lives inApp, and the mic renders inline inStatusBar.Ctrl+Shift+Mis routed through the store (sttToggle) so the shortcut still toggles the active session's mic.TerminalViewsurfaces its scroll state (terminalAtBottom) and scroll action (scrollActiveToBottom) via the store; the status bar shows the control only when the active terminal is scrolled up.8px 12px 48px 16px→4px 8px 4px 8px) now that nothing floats over the terminal, so it claims as much area as possible.Testing
npm run build(tsc + vite) — cleannpx vitest run— 33 passed (the 7 reported errors are pre-existing, fromProjectCard.test.tsx'slistenmock, unrelated to this change)npx tauri build— compiled and produced the.deb/.rpmbundles successfully🤖 Generated with Claude Code
Automated code review (Claude agent)
Verdict: Largely correct. The zustand-function-storage pattern is used correctly, hook deps are sound, and background terminals cannot clobber the active one. One genuine behavioral regression around mid-recording tab switches, plus minor items.
Findings
1. [Major] STT transcript goes to whatever tab is active at stop time, not where recording started —
App.tsx+hooks/useSTT.tsA single
useSTT(activeSessionId ?? "", sendInput)lives inApp. The recording lifecycle does not reset whenactiveSessionIdchanges. Start recording in session A, switch to B, stop →stopRecordingcloses over the currentsessionId(B) and injects the transcript into terminal B. Regression vs. the old per-terminal instance.Fix: capture the target session at recording start (a
recordingSessionIdRefset instartRecording, used bystopRecordinginstead of the livesessionId). Keeps a single mic while pinning the transcript to the originating terminal.2. [Minor] Stale
scrollActiveToBottom/terminalAtBottomafter the active terminal unmounts —TerminalView.tsx, store defaultsThe registration effect only writes when
active(good), but nothing clears the store on close. After closing the last session,scrollActiveToBottomstill points at the disposed terminal's handler (termRef.currentis never reset to null in cleanup). Masked in practice becauseStatusBargates both controls behindactiveSessionId. A reset in the unmount path would be more robust.3. [Nit] Padding comment doesn't match the value —
TerminalView.tsxComment says "right side leaves a little room beside the scrollbar," but padding is symmetric
4px 8px 4px 8px.Confirmed correct
set({ sttToggle: fn })passes an object → shallow-merged and stored verbatim, not misinterpreted as a state updater. Correct.setSttToggleeffect deps[stt.toggle, setSttToggle]are right;stt.toggleidentity changes with STT state so the store always holds the latest. Read viagetState()at call time — no stale closure, no render loop.useSTT/SttButtonfully removed fromTerminalView; no leftover imports.containerRefnow has no padding; padding moved to wrapper.FitAddon+ResizeObservermeasure the host, so the grid no longer overhangs. Correct.StatusBarhides both controls when!activeSessionId; store defaults are safe no-ops.🤖 Generated with Claude Code
Review follow-up (commit
da7b7b9)All three findings addressed:
startRecordingnow captures the active session id intorecordingSessionIdRef;stopRecordinginjects the transcript via that ref instead of the livesessionId. Switching tabs mid-recording no longer misroutes the transcript — it lands in the terminal where recording began. (startRecordingdeps gainedsessionId;stopRecordingno longer depends on it.)termRef.currentis now nulled on dispose, and an unmount effect clearsterminalAtBottom/scrollActiveToBottomwhen the active terminal is closed (guarded by anactiveRef, so tab switches — which don't unmount — are unaffected and the newly-active terminal's effect re-writes fresh values after).4px 8pxgutter.Verified:
tscclean,vitest33 passed,vite buildclean.