- Cache turnWindowModel per sessionId to skip rebuild on re-visit
- Compensate scroll position after virtualizer measurement settles
(RAF-based) so large uncached sessions open at bottom, not top
- Add MessageListHandle.scrollToBottom via virtualizer API
Replace @remixicon/react with a shared Icon component that renders
via <use href> references to a single hidden SVG sprite. This reduces
DOM node count by replacing inline SVGs with lightweight references.
- Create Icon component with sprite injection (packages/ui/src/components/icon/)
- Migrate all 164 files from @remixicon/react to Icon component
- Auto-generate sprite data from remixicon bundle (scripts/generate-icon-sprite.mjs)
- Add bun run icons:generate to package.json
- Move @remixicon/react to devDependencies
- Add icon usage instructions to theme-system skill
Virtualize message timeline at 5+ turns. Stabilize virtualizer keys,
add prepend-aware scroll compensation for "Load older", and defer
expanded tool bodies by one animation frame. Tighten initial turn
window to 7 turns for faster session open.
Removed duplicate Mini Chat launch action from the closed-sidebar header
Enabled opening Mini Chat from draft sessions via the header PiP action
Added read-only context usage info to existing Mini Chat sessions
* fix(files): ignore stale text loads
* fix(files): clear loading on directory read error
---------
Co-authored-by: Isaac Sanchez <isanchez-hawkins@arize.com>
* fix(ui): expose sidebar group collapse state
* fix(ui): keep sidebar group panel mounted
---------
Co-authored-by: Isaac Sanchez <isanchez-hawkins@arize.com>
Use existing local PR branches instead of blocking worktree creation
Only block PR branches already checked out in another worktree
Clarify local branch validation messages across locales
The previous system layered three hooks (useScrollEngine, useChatScrollManager,
useChatTimelineController) with overlapping responsibilities, four parallel
ResizeObservers/MutationObservers, and six entry points to "scroll to bottom"
(force-flag combinations, persistent follow loops, materialization recovery).
This produced bugs where users could not break free of auto-follow during
streaming: scrollbar drag, keyboard scrolling and find-in-page were not
detected as user intent, and observers kept restarting the follow loop on
every DOM mutation.
The new architecture replaces the two low-level hooks with a single
useChatAutoFollow that owns scroll behaviour end to end:
- One state: 'following' or 'released'. No follow modes, no pin flags,
no marker pixels.
- One scroll writer: a lerp loop that runs only while the session is
streaming and state is 'following'. Idle sessions never write scrollTop
programmatically.
- One user-intent detector: wheel up, touch drag down, keyboard
(PageUp / Home / ArrowUp), pointerdown on the OverlayScrollbar thumb,
and explicit releaseAutoFollow() calls all flip the state to 'released'.
- A 1.2s grace period after explicit release: re-pin will not auto-engage
inside this window, so a small wheel up cannot snap the user back even
while they remain near the bottom spacer.
- Re-pin and the scroll-to-bottom button share the same threshold: the
height of the empty bottom spacer (10vh on desktop, 40px on mobile).
Released users see the button only after they have scrolled past the
spacer that already exists at the end of the chat.
- Save/restore of scroll position uses ratio mapping, debounced at 150ms
on user-driven scroll events; programmatic writes are masked via a short
window so they never persist as user positions.
- Container reattachment is detected via a useLayoutEffect probe over
scrollRef.current. Listeners and observers re-bind when ChatViewport
mounts after hydration or after the first message promotes a draft
session into a real chat.
- A pending-restore queue replays restoreSnapshot once the scroll
container appears, fixing the case where a hydrating session landed at
the top instead of the bottom.
Removed: useScrollEngine.ts, useChatScrollManager.ts, the persistent
follow loop with its own ResizeObserver+MutationObserver pair, the
materialization-recovery .finally resume that yanked idle users to the
bottom on transient sync gaps, and the openchamber:session-reselected
event (re-select still works through the existing onSessionSelected
callback). The openchamber:chat-force-scroll-bottom event remains for
synthetic-message paths like git-message generation.
Net change: ~1300 lines removed, two hooks replaced with one, one
observer pair instead of four.
Add dedicated Electron Mini Chat windows for focused chat sessions without the full desktop shell. Mini Chat can open existing sessions or draft sessions, supports pinning above other windows, transfers sessions or drafts back to the main window, and deduplicates existing-session windows.
Expose Mini Chat entry points from the main header, session sidebar, command palette, and `mod+alt+n`. Add a dedicated Vite entry and React runtime so the compact surface can stay isolated from full-app chrome while still sharing chat, sync, theme, locale, model, agent, and worktree behavior.
Keep Mini Chat behavior scoped to the compact surface:
- limit assistant/user message actions to the appropriate Mini Chat set
- hide workspace changed-files UI in Mini Chat
- keep draft worktree selection and streaming directory state in sync
- mark sessions viewed while they are open in Mini Chat
- support Mini Chat-specific keyboard shortcuts for input focus, model selection, thinking variant cycling, favorite model cycling, and opening new Mini Chat drafts
Harden Electron integration by gating Mini Chat controls on desktop IPC availability, restricting pin/unpin IPC to Mini Chat windows, and only closing Mini Chat after the main window handoff succeeds.
* fix: handle terminal keyboard input on Android with desktop UA
- Detect Android via navigator.userAgentData.platform when UA is
spoofed by Chrome's "Desktop site" mode, so the <input> overlay
(useTextInput) is used instead of <textarea>.
- Handle Ctrl+letter and Alt+letter combos in the hidden input
overlay's keydown handler, so terminal control sequences (^C, ^D,
^S, M-x, etc.) work through the touch input path.
- Skip global keyboard shortcuts (sidebar toggle, etc.) when focus
is inside the terminal viewport, preventing shortcut steal.
* fix: preserve terminal keyboard shortcuts
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
Conflict resolution prompts now ask for an analysis-first strategy
Response style instructions are wrapped as system reminders
Prompt templates stay separate from reminder formatting
Canonicalize session message/part materialization across load, prefetch, reconnect, and recovery paths so OpenChamber restores session snapshots through one consistent merge flow.
Preserve live assistant streaming text when stale or delayed snapshots arrive, while still replacing optimistic user parts with confirmed server snapshots to avoid duplicated user messages.
Narrow recovery triggers to explicit incomplete snapshot signals instead of broad session-event fallbacks, reducing unnecessary session refetches during active streaming.
Keep turn windowing aligned with parented assistant replies and add regression coverage for materialization gaps, stale snapshot protection, optimistic user replacement, reconnect recovery, and turn grouping.
On Android tablets in Desktop site mode, the handleDocumentFocusIn handler
intercepted Ghostty's internal element focus and blurred them, but never
redirected focus to the hidden input overlay. This left the terminal with
no focused element - the IME keyboard would appear but keystrokes went
nowhere. Now focusHiddenInput() is called after the blur to ensure the
terminal always has a focused input surface.
Deduplicates concurrent app initialization in the config store
Lazy-loads VS Code-only app surfaces
Avoids broad view barrel imports for better chunk isolation
Replace placeholder conflict text with localized copy
Split in-progress operation details and actions into clearer rows
Keep merge and rebase status messages consistent