Commit Graph
10 Commits
Author SHA1 Message Date
Bohdan Triapitsyn b384deb812 fix(chat): stop scroll jiggle and double-scroll on send while pinned
Two scroll owners were writing the chat container's scrollTop concurrently
during pinned content growth and on send, fighting frame-to-frame and
producing the reported flicker/jiggle (after a pause, from the queue, on user
interruptions) plus a visible double scroll on a normal user send.

Enforce a single-writer invariant in useChatAutoFollow:
- The easing follow loop and the instant settle burst now mutually exclude:
  starting one stops the other, so they can never write scrollTop in the same
  frame. The isFollowingProgrammatically flag (which suppresses the overlay
  scrollbar) is owned by whichever loop is active and cleared only when both
  are idle, including the settle burst's natural 280ms end.

Stop the redundant re-pin storm in useChatTimelineController:
- While pinned, route goToBottom('instant') only for a prepend (history loaded
  above), not on every bottom append / streaming part. Normal growth is owned
  by the follow loop (kicked by the content ResizeObserver and chunk handlers).

Remove the double movement on send:
- Add scrollToBottomOnSend: when already following, just (re)kick the follow
  loop for a single smooth movement instead of also firing an instant
  goToBottom that raced the ResizeObserver-driven loop. When released (scrolled
  up), keep the instant jump to the just-sent message.
2026-06-27 00:06:19 +03:00
00821700de chore: remove dead code (59 unused files + ~125 unused exports) (#1835)
* chore: remove dead/unreferenced files across ui, vscode

Remove 59 unused source files (components, hooks, lib utils, stores,
barrels, and orphaned vscode github modules) that are not imported by
any entry-reachable code. Also drop a stale test mock for the removed
execCommands module.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove unused exported symbols (types, functions, consts, hooks)

Remove exported symbols whose identifier is referenced nowhere in the
repository (verified via repo-wide search), across ui types/contracts,
lib utilities, sync layer, stores, and components. Also drop the few
imports/private helpers orphaned by these removals.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove more unused exports (desktop, shortcuts, worktree, vscode)

Continue removing repo-wide unreferenced exported functions, consts and
types across lib/desktop, shortcuts, worktreeSessionCreator, sync, and
vscode gitService, with cascading orphaned helpers/imports cleaned up.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore: add dead-code cleanup tooling

* refactor: checkpoint dead-code cleanup

* refactor: remove dead-code suppressions

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-26 19:27:53 +03:00
Bohdan Triapitsyn f2d4a7833d fix(chat): keep session switch pinned to bottom without backward jump
Release auto-follow based on position (the user has left the near-bottom zone)
instead of scroll-delta direction. The old `currentTop < previousTop` check
treated the tiny scrollTop clamp the browser applies when the composer grows —
which keeps you at the bottom — as a user scroll-up and released follow, so
content finishing loading then drifted the view backward.

Also always return to the bottom on session switch, dropping the saved-ratio
restore: it had a low success rate and, by landing 'released' partway up,
produced the same visible backward jump as content finished loading.

overflow-anchor is already disabled on the chat scroll container, so no
delta-threshold workaround is needed; this is a net simplification.
2026-06-24 18:04:59 +03:00
Hernan Javier Ardila Sanchezandherjarsa 1abb0dc103 fix(chat): set scroll position before paint on session-switch replay (#1730)
Replace useEffect with useLayoutEffect in the pendingInitialRestoreRef
replay so restoreSnapshot runs synchronously after DOM commit, before
the browser paints. Prevents visible flash of content at the wrong
scroll position when the scroll container mounts after session
hydration.

Adapted from openchamber/openchamber#1553 (Fix 2). The virtualVersion
counter (Fix 1) is not applicable: virtua (post #1651) does not use
useVirtualizer's useState-based instance pattern that motivated it.

Validation:
- bun --cwd packages/ui type-check - no new errors in useChatAutoFollow.ts
- bun --cwd packages/ui lint - passed

Co-authored-by: herjarsa <herjarsa@users.noreply.github.com>
2026-06-24 17:28:46 +03:00
Bohdan Triapitsyn 928b7ff1d6 revert: restore stable chat history scrolling 2026-06-14 23:06:03 +03:00
Tom db3558f906 fix: prevent blank chat viewport when switching sessions (#1553)
When switching to a session with long context (especially in Electron
desktop when changing servers), the chat viewport could render blank
until the user scrolled. Two interacting issues caused this:

1. historyVirtualRows memo never recomputed after the first render
   because historyVirtualizer (from useVirtualizer's useState) is a
   stable reference. Frozen range meant items rendered at the top
   while paddingBottom filled the visible viewport after scrolling.

2. pendingInitialRestoreRef replay ran in useEffect (after paint),
   showing a frame at scrollTop:0 with the stale virtualizer range.

Fixed by: adding a virtualVersion counter driven by useVirtualizer's
onChange to bust the memo; switching the replay to useLayoutEffect
so scroll position is set before the browser paints.
2026-06-12 20:31:32 +03:00
Bohdan Triapitsyn 2031e3b4a8 Decouple bundled UI from runtime API and add remote instance tooling (#1228)
Add a packaged-client runtime boundary so the shared UI can talk to local,
desktop, remote, and VS Code runtimes through the right transport instead of
assuming one same-origin web server.

Centralize OpenChamber-owned API access behind RuntimeAPIs, runtimeFetch, and
runtime URL helpers, while keeping official OpenCode traffic on the SDK path.
Support runtime switching, remote host selection, desktop client credentials,
and headless connection links for pairing packaged clients with remote
OpenChamber servers.

Harden the new auth model by moving long-lived client tokens out of browser
URLs, introducing short-lived scoped URL tokens for browser-owned transports,
restricting URL-token access to explicit readable/realtime routes, and making
client-token management session-scoped or self-scoped as appropriate.

Update browser-owned assets and preview proxy flows to work with the split
runtime model, including authenticated project icons, preview token propagation,
CSP-safe preview bridge injection, and preview proxy auth that survives
short-lived URL-token expiry.

Tighten Electron security boundaries for packaged clients by gating privileged
preload state to trusted origins and requiring explicit confirmation before
connect deep-links import or switch remote runtimes.

Also refresh agent guidance and project skills so future runtime/API, auth,
preview, UI, CLI, settings, locale, and drag-to-reorder work follows the new
architecture.
2026-06-02 00:43:05 +03:00
Bohdan Triapitsyn 8f45596c99 fix: keep pinned chat following final summaries 2026-05-25 02:13:00 +03:00
Bohdan Triapitsyn d659929685 fix(chat): tighten auto-follow release thresholds
Bottom-zone threshold now scales with the scroll container's clientHeight
rather than window.innerHeight. The threshold is meant to mirror the empty
spacer at the end of the chat, but what users perceive as "the empty tail"
is relative to the visible chat area, not the whole viewport — on desktop
the chat is shorter than the window because of the input row below it.
This keeps the scroll-to-bottom button hidden until the user has actually
scrolled past the spacer they see, regardless of viewport size.

Wheel and touch release detection no longer treats every nested
[data-scrollable] as a blocker. The old check returned true for any
nested scrollable ancestor, so wheel-up over a code block already at its
own scrollTop=0 produced no effect — the inner element could not consume
the gesture and the outer chat refused to release auto-follow. The new
check returns true only when the nested element can actually scroll up
(scrollTop > 0); otherwise the gesture bubbles to the chat and releases
auto-follow as expected.
2026-05-08 14:31:16 +03:00
Bohdan Triapitsyn 0ea573f766 refactor: redesign chat scroll system
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.
2026-05-08 14:20:16 +03:00