feat: UI polish — chat redesign, spiral loader, smooth upward animation - #21
Conversation
User messages remain as right-aligned bubbles with the warm gradient. AI messages now render as full-width plain text with no bubble chrome, matching the ChatGPT/Claude convention. Copy button is always visible for both message types (previously hover-only). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Replaces the old iMessage-style bouncing dots with a 3×3 grid. The brand color sweeps the spiral path (top-right → outer ring → center) at 110ms/step. The center dot holds for 200ms, all dots dim, then a 500ms pause precedes the next cycle. No shadows or glows — clean color transitions only. Positioned as plain text (no bubble) to match the redesigned AI message layout. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Root cause: Streamdown opens empty block elements (<p>, list items) before their content arrives, causing sudden ~20 px height jumps. The ResizeObserver + requestAnimationFrame path adds 1-2 frames of lag before set_window_frame repositions the window, leaving new content briefly above the window boundary — visible as a jump. Downward growth is immune because only height changes (origin fixed). Fix: pre-expand the upward-anchor window by STREAM_BUFFER_PX (32 px) while isGenerating is true. New reserved lines always land inside the already-repositioned window, eliminating the clip-then-reveal cycle. useLayoutEffect syncs the ref before paint so the buffer is applied from the very first streaming frame. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…jump When an upward-anchor session begins generating (isGenerating → true), a new useEffect immediately calls set_window_frame with the full max height before any streaming tokens arrive. This gives the streaming text a fixed canvas, so Streamdown's paragraph-level height reservations never cause incremental upward repositioning — the window is already at its final size. Removes the failed STREAM_BUFFER_PX approach (32px additive buffer) which still produced visible jumps because ResizeObserver callbacks were racing with each Streamdown block-element reservation. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
When isChatMode becomes true on submit, ConversationView mounts with flex-1 causing the morphing container to grow to 600px. The ResizeObserver then calls set_window_frame to expand the native window upward. Between that call and WKWebView updating its viewport size, h-screen still reports the old small height — with justify-end this temporarily places the content near the top of the repositioned window, visible as a jarring flash. Fix: before every set_window_frame call in the anchor path, explicitly set minHeight on the outer container div. This forces the CSS layout to use the target height immediately, so justify-end positions the content correctly during the 1-2 frame WKWebView viewport update lag. minHeight is cleared when a new session starts or the overlay hides. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Code reviewFound 2 issues:
The inner thuki/src/components/TypingIndicator.tsx Lines 90 to 97 in 1f9ad57 Missing test coverage in: thuki/src/components/__tests__/TypingIndicator.test.tsx Lines 75 to 84 in 1f9ad57
The thuki/src/components/ChatBubble.tsx Lines 68 to 70 in 1f9ad57 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
- Add restart-cycle test for TypingIndicator: advances past PAUSE_MS to exercise the inner schedule callback (currentStep reset, setDimmed(false)) that was previously an untested code path - Update ChatBubble test to query max-w-[80%] directly instead of via the now-removed .group class - Remove dead `group` class from user bubble wrapper in ChatBubble — the only consumer was group-hover on CopyButton which was removed when the button became always-visible - Add v8 ignore start/stop around defensive null guards (outerContainerRef, timerRef, cancelled flag) that are unreachable in the jsdom/happy-dom test environment Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Mirrors the exact cargo llvm-cov invocation from pr-backend-tests.yml so developers can run backend coverage locally with the same 100% line enforcement as CI, instead of having to copy the raw cargo command from the workflow file. Also updates CLAUDE.md to reference the new script instead of the raw cargo command. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…on (#21) * feat: redesign chat view to industry-standard assistant layout User messages remain as right-aligned bubbles with the warm gradient. AI messages now render as full-width plain text with no bubble chrome, matching the ChatGPT/Claude convention. Copy button is always visible for both message types (previously hover-only). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * feat: replace 3-dot bounce with 9-dot spiral loading indicator Replaces the old iMessage-style bouncing dots with a 3×3 grid. The brand color sweeps the spiral path (top-right → outer ring → center) at 110ms/step. The center dot holds for 200ms, all dots dim, then a 500ms pause precedes the next cycle. No shadows or glows — clean color transitions only. Positioned as plain text (no bubble) to match the redesigned AI message layout. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * style: shrink spiral loading dots to 3px with 3px gaps Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix: smooth upward-anchor window growth during LLM streaming Root cause: Streamdown opens empty block elements (<p>, list items) before their content arrives, causing sudden ~20 px height jumps. The ResizeObserver + requestAnimationFrame path adds 1-2 frames of lag before set_window_frame repositions the window, leaving new content briefly above the window boundary — visible as a jump. Downward growth is immune because only height changes (origin fixed). Fix: pre-expand the upward-anchor window by STREAM_BUFFER_PX (32 px) while isGenerating is true. New reserved lines always land inside the already-repositioned window, eliminating the clip-then-reveal cycle. useLayoutEffect syncs the ref before paint so the buffer is applied from the very first streaming frame. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix: pre-expand to max height on streaming start to eliminate upward jump When an upward-anchor session begins generating (isGenerating → true), a new useEffect immediately calls set_window_frame with the full max height before any streaming tokens arrive. This gives the streaming text a fixed canvas, so Streamdown's paragraph-level height reservations never cause incremental upward repositioning — the window is already at its final size. Removes the failed STREAM_BUFFER_PX approach (32px additive buffer) which still produced visible jumps because ResizeObserver callbacks were racing with each Streamdown block-element reservation. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix: eliminate ask-bar jump on submit in upward-anchor mode When isChatMode becomes true on submit, ConversationView mounts with flex-1 causing the morphing container to grow to 600px. The ResizeObserver then calls set_window_frame to expand the native window upward. Between that call and WKWebView updating its viewport size, h-screen still reports the old small height — with justify-end this temporarily places the content near the top of the repositioned window, visible as a jarring flash. Fix: before every set_window_frame call in the anchor path, explicitly set minHeight on the outer container div. This forces the CSS layout to use the target height immediately, so justify-end positions the content correctly during the 1-2 frame WKWebView viewport update lag. minHeight is cleared when a new session starts or the overlay hides. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix: restore 100% coverage and remove dead group class - Add restart-cycle test for TypingIndicator: advances past PAUSE_MS to exercise the inner schedule callback (currentStep reset, setDimmed(false)) that was previously an untested code path - Update ChatBubble test to query max-w-[80%] directly instead of via the now-removed .group class - Remove dead `group` class from user bubble wrapper in ChatBubble — the only consumer was group-hover on CopyButton which was removed when the button became always-visible - Add v8 ignore start/stop around defensive null guards (outerContainerRef, timerRef, cancelled flag) that are unreachable in the jsdom/happy-dom test environment Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * chore: add bun run test:backend:coverage script Mirrors the exact cargo llvm-cov invocation from pr-backend-tests.yml so developers can run backend coverage locally with the same 100% line enforcement as CI, instead of having to copy the raw cargo command from the workflow file. Also updates CLAUDE.md to reference the new script instead of the raw cargo command. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> --------- Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…on (#21) * feat: redesign chat view to industry-standard assistant layout User messages remain as right-aligned bubbles with the warm gradient. AI messages now render as full-width plain text with no bubble chrome, matching the ChatGPT/Claude convention. Copy button is always visible for both message types (previously hover-only). Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * feat: replace 3-dot bounce with 9-dot spiral loading indicator Replaces the old iMessage-style bouncing dots with a 3×3 grid. The brand color sweeps the spiral path (top-right → outer ring → center) at 110ms/step. The center dot holds for 200ms, all dots dim, then a 500ms pause precedes the next cycle. No shadows or glows — clean color transitions only. Positioned as plain text (no bubble) to match the redesigned AI message layout. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * style: shrink spiral loading dots to 3px with 3px gaps Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix: smooth upward-anchor window growth during LLM streaming Root cause: Streamdown opens empty block elements (<p>, list items) before their content arrives, causing sudden ~20 px height jumps. The ResizeObserver + requestAnimationFrame path adds 1-2 frames of lag before set_window_frame repositions the window, leaving new content briefly above the window boundary — visible as a jump. Downward growth is immune because only height changes (origin fixed). Fix: pre-expand the upward-anchor window by STREAM_BUFFER_PX (32 px) while isGenerating is true. New reserved lines always land inside the already-repositioned window, eliminating the clip-then-reveal cycle. useLayoutEffect syncs the ref before paint so the buffer is applied from the very first streaming frame. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix: pre-expand to max height on streaming start to eliminate upward jump When an upward-anchor session begins generating (isGenerating → true), a new useEffect immediately calls set_window_frame with the full max height before any streaming tokens arrive. This gives the streaming text a fixed canvas, so Streamdown's paragraph-level height reservations never cause incremental upward repositioning — the window is already at its final size. Removes the failed STREAM_BUFFER_PX approach (32px additive buffer) which still produced visible jumps because ResizeObserver callbacks were racing with each Streamdown block-element reservation. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix: eliminate ask-bar jump on submit in upward-anchor mode When isChatMode becomes true on submit, ConversationView mounts with flex-1 causing the morphing container to grow to 600px. The ResizeObserver then calls set_window_frame to expand the native window upward. Between that call and WKWebView updating its viewport size, h-screen still reports the old small height — with justify-end this temporarily places the content near the top of the repositioned window, visible as a jarring flash. Fix: before every set_window_frame call in the anchor path, explicitly set minHeight on the outer container div. This forces the CSS layout to use the target height immediately, so justify-end positions the content correctly during the 1-2 frame WKWebView viewport update lag. minHeight is cleared when a new session starts or the overlay hides. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix: restore 100% coverage and remove dead group class - Add restart-cycle test for TypingIndicator: advances past PAUSE_MS to exercise the inner schedule callback (currentStep reset, setDimmed(false)) that was previously an untested code path - Update ChatBubble test to query max-w-[80%] directly instead of via the now-removed .group class - Remove dead `group` class from user bubble wrapper in ChatBubble — the only consumer was group-hover on CopyButton which was removed when the button became always-visible - Add v8 ignore start/stop around defensive null guards (outerContainerRef, timerRef, cancelled flag) that are unreachable in the jsdom/happy-dom test environment Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * chore: add bun run test:backend:coverage script Mirrors the exact cargo llvm-cov invocation from pr-backend-tests.yml so developers can run backend coverage locally with the same 100% line enforcement as CI, instead of having to copy the raw cargo command from the workflow file. Also updates CLAUDE.md to reference the new script instead of the raw cargo command. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> --------- Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…on (#21) * feat: redesign chat view to industry-standard assistant layout User messages remain as right-aligned bubbles with the warm gradient. AI messages now render as full-width plain text with no bubble chrome, matching the ChatGPT/Claude convention. Copy button is always visible for both message types (previously hover-only). Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * feat: replace 3-dot bounce with 9-dot spiral loading indicator Replaces the old iMessage-style bouncing dots with a 3×3 grid. The brand color sweeps the spiral path (top-right → outer ring → center) at 110ms/step. The center dot holds for 200ms, all dots dim, then a 500ms pause precedes the next cycle. No shadows or glows — clean color transitions only. Positioned as plain text (no bubble) to match the redesigned AI message layout. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * style: shrink spiral loading dots to 3px with 3px gaps Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix: smooth upward-anchor window growth during LLM streaming Root cause: Streamdown opens empty block elements (<p>, list items) before their content arrives, causing sudden ~20 px height jumps. The ResizeObserver + requestAnimationFrame path adds 1-2 frames of lag before set_window_frame repositions the window, leaving new content briefly above the window boundary — visible as a jump. Downward growth is immune because only height changes (origin fixed). Fix: pre-expand the upward-anchor window by STREAM_BUFFER_PX (32 px) while isGenerating is true. New reserved lines always land inside the already-repositioned window, eliminating the clip-then-reveal cycle. useLayoutEffect syncs the ref before paint so the buffer is applied from the very first streaming frame. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix: pre-expand to max height on streaming start to eliminate upward jump When an upward-anchor session begins generating (isGenerating → true), a new useEffect immediately calls set_window_frame with the full max height before any streaming tokens arrive. This gives the streaming text a fixed canvas, so Streamdown's paragraph-level height reservations never cause incremental upward repositioning — the window is already at its final size. Removes the failed STREAM_BUFFER_PX approach (32px additive buffer) which still produced visible jumps because ResizeObserver callbacks were racing with each Streamdown block-element reservation. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix: eliminate ask-bar jump on submit in upward-anchor mode When isChatMode becomes true on submit, ConversationView mounts with flex-1 causing the morphing container to grow to 600px. The ResizeObserver then calls set_window_frame to expand the native window upward. Between that call and WKWebView updating its viewport size, h-screen still reports the old small height — with justify-end this temporarily places the content near the top of the repositioned window, visible as a jarring flash. Fix: before every set_window_frame call in the anchor path, explicitly set minHeight on the outer container div. This forces the CSS layout to use the target height immediately, so justify-end positions the content correctly during the 1-2 frame WKWebView viewport update lag. minHeight is cleared when a new session starts or the overlay hides. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * fix: restore 100% coverage and remove dead group class - Add restart-cycle test for TypingIndicator: advances past PAUSE_MS to exercise the inner schedule callback (currentStep reset, setDimmed(false)) that was previously an untested code path - Update ChatBubble test to query max-w-[80%] directly instead of via the now-removed .group class - Remove dead `group` class from user bubble wrapper in ChatBubble — the only consumer was group-hover on CopyButton which was removed when the button became always-visible - Add v8 ignore start/stop around defensive null guards (outerContainerRef, timerRef, cancelled flag) that are unreachable in the jsdom/happy-dom test environment Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> * chore: add bun run test:backend:coverage script Mirrors the exact cargo llvm-cov invocation from pr-backend-tests.yml so developers can run backend coverage locally with the same 100% line enforcement as CI, instead of having to copy the raw cargo command from the workflow file. Also updates CLAUDE.md to reference the new script instead of the raw cargo command. Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com> --------- Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Summary
minHeightis set on the outer container before everyset_window_framecall to prevent a WKWebView viewport-lag flash that caused the ask bar to jump to the top of the window on submitTest plan
bun run test)bun run validate-buildpasses with zero warnings/errors🤖 Generated with Claude Code