From 7d02f4e53830ee24ad90eda8097079efb3d367c7 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Thu, 9 Jul 2026 22:01:41 -0400 Subject: [PATCH] fix(voice): apply waveform review findings Three review agents (no regressions found). Applied: - Keyboard arrow-seek now reads the live media currentTime, not the throttled ~500ms state, so rapid presses accumulate instead of dropping steps. - Scrubbing the waveform (or the fallback seek bar) BEFORE first play now loads the media and plays from the clicked position (was a silent no-op). - Unplayed bars use a dimmed accent (color-mix 32%) instead of a faint surface token, for consistent contrast across TDS-dark/light + normal themes. - Fixed first-bar always-lit off-by-one ((i+1)/len), and added overflow:hidden so the strip clips rather than overflows in a very narrow drawer. Co-Authored-By: Claude Opus 4.8 --- .../message/content/AudioContent.tsx | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/app/components/message/content/AudioContent.tsx b/src/app/components/message/content/AudioContent.tsx index fd9a87721..56b922e4e 100644 --- a/src/app/components/message/content/AudioContent.tsx +++ b/src/app/components/message/content/AudioContent.tsx @@ -62,17 +62,22 @@ function WaveformSeek({ currentTime, duration, onSeek, + getCurrentTime, }: { waveform: number[]; currentTime: number; duration: number; onSeek: (time: number) => void; + /** Reads the live media time (the `currentTime` prop is throttled ~500ms). */ + getCurrentTime: () => number; }) { const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal'); const containerRef = useRef(null); const bars = useMemo(() => downsampleWaveform(waveform, WAVEFORM_DISPLAY_BARS), [waveform]); const barMax = useMemo(() => Math.max(...bars, 1), [bars]); const progress = duration > 0 ? Math.min(1, Math.max(0, currentTime / duration)) : 0; + const accent = lotusTerminal ? 'var(--lt-accent-green)' : color.Primary.Main; + const unplayedColor = `color-mix(in srgb, ${accent} 32%, transparent)`; const seekFromClientX = useCallback( (clientX: number) => { @@ -94,12 +99,14 @@ function WaveformSeek({ }; const handleKeyDown = (evt: React.KeyboardEvent) => { if (duration <= 0) return; + // Base off the LIVE time so rapid presses accumulate (the prop is throttled). + const base = getCurrentTime(); if (evt.key === 'ArrowRight' || evt.key === 'ArrowUp') { evt.preventDefault(); - onSeek(Math.min(duration, currentTime + 5)); + onSeek(Math.min(duration, base + 5)); } else if (evt.key === 'ArrowLeft' || evt.key === 'ArrowDown') { evt.preventDefault(); - onSeek(Math.max(0, currentTime - 5)); + onSeek(Math.max(0, base - 5)); } else if (evt.key === 'Home') { evt.preventDefault(); onSeek(0); @@ -132,10 +139,11 @@ function WaveformSeek({ height: toRem(24), cursor: 'pointer', touchAction: 'none', + overflow: 'hidden', }} > {bars.map((v, i) => { - const played = bars.length > 0 && i / bars.length <= progress; + const played = bars.length > 0 && (i + 1) / bars.length <= progress; return (
@@ -196,6 +200,8 @@ export function AudioContent({ ); const audioRef = useRef(null); + // A seek requested before the media has loaded; applied once metadata arrives. + const pendingSeekRef = useRef(null); useEffect( () => () => { @@ -236,6 +242,11 @@ export function AudioContent({ if (!audio) return undefined; const applyRate = () => { audio.playbackRate = playbackSpeed; + // Apply a seek that was requested before the source loaded. + if (pendingSeekRef.current != null && audio.readyState >= 1) { + audio.currentTime = pendingSeekRef.current; + pendingSeekRef.current = null; + } }; // Apply immediately, and re-apply whenever the media element (re)loads a new // source — e.g. after async decrypt swaps in the blob URL — since the browser @@ -265,6 +276,20 @@ export function AudioContent({ } }; + // Seeking before the media has loaded (e.g. clicking the waveform first) loads it + // and applies the position once metadata arrives (the