* fix(desktop): prevent IME Enter from splitting messages and viewport resize from disarming scroll anchor
Two fixes for the Hermes Desktop composer:
1. IME composition Enter was treated as message submission. When a Korean/
Japanese/Chinese IME is composing text and the user presses Enter to
finalise the preedit, handleEditorKeyDown fired submitDraft() because it
did not check event.nativeEvent.isComposing. The assistant-ui hidden
textarea already guards this correctly; the custom contentEditable
handler was missing it. Added an early return when isComposing is true.
2. Viewport resize (composer expand/collapse, window resize) was disarming
the scroll sticky-bottom anchor. When the composer grows, the thread
viewport shrinks, the browser adjusts scrollTop down to keep content
visible, and the onScroll handler misread this as a user scroll-up.
Added lastClientHeightRef tracking so the disarm condition now requires
BOTH stable scrollHeight AND stable clientHeight before treating a
scrollTop decrease as user intent.
Fixes: random mid-message sends during IME typing; scroll jumps when the
composer resizes or the window changes size.
* fix(desktop): prevent virtualizer measurement adjustments from fighting scroll anchoring
The virtualizer's measureElement callbacks trigger scroll adjustments when
item sizes differ from estimates. These fight our ResizeObserver +
pinToBottom loop, creating visible rubber-banding (view snaps to composer
then jumps back up), even during idle.
Three changes:
1. React.memo on VirtualizedThread to stop parent re-renders cascading
2. Shared stickyBottomRef so scrollToFn can check bottom state
3. scrollToFn override: skip adjustments when user is at bottom
* fix(desktop): use stable useCallback ref instead of inline arrow for onBranchInNewChat
The inline arrow `messageId => void branchInNewChat(messageId)` created a
new function reference on every render. This cascaded through:
desktop-controller → ChatView → Thread → useMemo([...onBranchInNewChat])
→ new messageComponents object → VirtualizedThread receives new prop
→ React.memo overridden → virtualizer recalculates → measurement
adjustments trigger scroll jumps at the 15-second useStatusSnapshot
interval.
Pass the already-useCallback'd branchInNewChat directly.
* fix(desktop): use ctrlEnter submitMode on hidden textarea + gate ResizeObserver on isRunning
Two root-cause fixes:
1. IME message splitting: The hidden ComposerPrimitive.Input textarea had
submitMode='enter' (default), so any Enter keydown it received — even
during IME composition — triggered form.requestSubmit(). Changed to
submitMode='ctrlEnter' so only the contentEditable div (which correctly
checks isComposing) handles plain-Enter submission.
2. Scroll jumps during idle: The ResizeObserver auto-follow loop was
active even when the thread wasn't running, causing spurious
pinToBottom calls whenever any layout shift occurred (browser reflow,
font load, GPU cache eviction). Gated the ResizeObserver on
thread.isRunning so auto-scroll only follows during active streaming.
User messages still pin via useLayoutEffect, and thread.runStart still
calls jumpToBottom.
* fix(desktop): keep chat bottom anchor stable through idle layout shifts
* fix(desktop): prevent code block shrink scroll bounce
* fix(desktop): release bottom height lock on run completion
* fix(desktop): keep streaming code blocks rendered
* fix(desktop): keep bottom anchored through final render
* fix(desktop): render streaming reasoning code blocks
* feat(desktop): add subtle streaming block animations