Commit Graph
100 Commits
Author SHA1 Message Date
Bohdan Triapitsyn ea405eafdc fix(mobile): let the keyboard shrink the page on Android browsers
The pre-dvh -webkit-fill-available viewport fix freezes Android Chrome's
root at the pre-keyboard height; when interactive-widget=resizes-content
shrinks the viewport, the document stays taller than the screen and the
clipped composer hides behind the keyboard with no way to scroll to it.
dvh-capable browsers now take a dynamic 100dvh instead, and the legacy
fallback keeps serving browsers without dvh.
2026-08-26 20:24:27 +03:00
Bohdan Triapitsyn defd0719b7 docs: clarify changelog highlight ordering guidance 2026-08-26 19:58:32 +03:00
Bohdan Triapitsyn 717854d06b refactor: remove aborted status banner from chat UI
Removes the transient aborted banner from the composer status area
Simplifies status row rendering to focus on working state and pending changes
Cleans up unused abort-status localization strings
2026-08-26 19:50:07 +03:00
Bohdan Triapitsyn 340738a33f chore: update changelog entries for chat shortcut and comment text 2026-08-26 19:49:17 +03:00
Bohdan Triapitsyn cb18f8c9af chore: bump @opencode-ai/sdk to 1.18.23 2026-08-26 19:46:23 +03:00
Bohdan Triapitsyn c4df01f707 chore(quota): drop the Command Code usage provider
Command Code's official API has no usage endpoints; the old usage source
was the unofficial studio API reached through a now-archived plugin, so
the tile could only ever fail for officially configured users. Removed
across server, shared UI, and the VS Code extension; the provider logo
fallback stays — it serves the model picker, not usage.
2026-08-26 19:29:04 +03:00
Bohdan Triapitsyn 067c6caf0c refactor(chat): slim the selection menu down to comment and notes
Add to input leaves the desktop menu (mod+L owns it; mobile keeps the
button) and the New session action is gone from both variants along with
its handler and dead locale keys.
2026-08-26 19:17:55 +03:00
Bohdan Triapitsyn 4020226984 docs: tighten changelog bullets and normalize contributor credits 2026-08-26 19:11:33 +03:00
Bohdan Triapitsyn fc21ad5e9c docs: headline session tabs instead of the auth-expiry fix 2026-08-26 18:59:52 +03:00
Bohdan Triapitsyn 3f38624850 docs: reorder and tighten the unreleased changelog 2026-08-26 18:58:20 +03:00
Bohdan Triapitsyn b11bace75b docs: add changelog entries for the rail configurator and chat fixes 2026-08-26 18:48:10 +03:00
Bohdan Triapitsyn edec60faff fix(github): stop fork remotes from claiming the local branch's PR badge
PR-status source candidates were every configured remote, so a checkout
carrying contributor forks matched a fork's closed PR whose head merely
shared the branch name — a fork's 'main' surfaced on the local main in the
git and work-status panels. Only the ranked-first remote (the one the
branch pushes to) and its fork network are PR sources now; other remotes
remain search targets but their owner:branch heads no longer count.
2026-08-26 18:40:34 +03:00
Bohdan Triapitsyn 098034dc33 fix(desktop): stop windows from adopting each other's active project
Every window shares one server settings document, and every PUT returns the
merged whole, so one window's activeProjectId write was adopted by the
other on its next unrelated settings save — its sidebar then auto-selected
a session in that project and wrote the pointer back, converging both
windows onto one session. settings-synced now carries an adoptWorkspace
flag: only bootstrap-grade syncs (startup, runtime switch) may adopt the
shared workspace pointers; reconcile responses keep the window's own active
project while it exists. Notification clicks and session deep links also
stopped broadcasting the session switch to every window.
2026-08-26 18:36:50 +03:00
Bohdan Triapitsyn f7a006dc6a feat(auth): detect session expiry live and offer re-login in place
Every response already funnels through runtimeFetch, so a classifier there
spots 401s, confirms them against /auth/session (a proxied provider 401
must not read as a logout), and flips a small auth-session store. The web
and hosted surfaces show a frosted banner under the header whose Log in
button hands off to the session gate's existing unlock flow; sends are
paused while expired, the session-load error screen explains the auth case
and retries itself after login, and returning to a long-idle window
revalidates once via visibility/focus. Native mobile feeds the same signal
into its connection re-probe instead of showing the banner; VS Code is
exempt.
2026-08-26 18:22:04 +03:00
Bohdan Triapitsyn 27377efaf3 fix(chat): return the typed prompt to the input on any send failure
The failure handler restored the text only for a new-session draft; a
regular session kept its attachments but lost the prompt to a toast. The
restore now runs before any cause-specific branching: an unchanged composer
gets the text (and the session draft) back, new typing gets the failed
prompt appended instead of clobbered, and a mid-send session switch writes
it into the originating session's persisted draft.
2026-08-26 17:10:05 +03:00
Bohdan Triapitsyn ba3a604e9e fix(chat): offer the scroll pill when growth leaves the reader behind
With streaming auto-follow off nothing moves the viewport, so a growing
reply slides below the composer without a single scroll event and the
at-end transition that shows the pill never fires. Content growth now
doubles as the signal: once the measured last row extends past the visible
area by the follow re-arm threshold, the end state clears and the pill is
scheduled.
2026-08-26 16:49:21 +03:00
Bohdan Triapitsyn 087c148b2e fix(chat): rescue stranded viewports and settle navigation jumps
Opening a session (or any relayout that shrinks off-screen size estimates)
could leave the viewport in a phantom tail below the measured content, with
every row out of reach above; a totalSize-change check now detects the
fully blank viewport and returns to the real end, and settling a width
resize re-asserts the end for a reader who was on it. Prompt-rail and
message jumps land on estimated offsets that shift as the target mounts and
measures; a short settle loop now re-aligns the target until layout rests,
backing off on the first user gesture.
2026-08-26 16:42:42 +03:00
Bohdan Triapitsyn 2e49e44205 feat(ui): let users hide context rail surfaces
A trailing configure button on the rail — outside the sortable list and the
digit shortcuts — opens a dialog that toggles each surface. The choice is
stored as the hidden set so newly added surfaces appear for everyone, and
the rail and the mod+alt+digit switcher share the same visibility filter, so
badges and shortcuts always agree. Hidden surfaces keep their data and stay
reachable from the command palette.
2026-08-26 16:18:15 +03:00
Bohdan Triapitsyn f2ec9b1003 feat(ui): add session history navigation, permission keys, and review shortcuts
mod+alt+arrows step through this window's session-open history (or between
neighbouring tabs when session tabs are on), mod+k r renames the current
session inline, and mod+k a toggles permission auto-accept. Pending
permission cards respond to alt+enter / alt+shift+enter / alt+backspace with
the keys printed on the buttons. The commit message box commits on
mod+enter, alt+arrows step the diff review between changed files, and the
command palette gains search-only commands for rare actions so the initial
list stays short.
2026-08-26 15:46:44 +03:00
Bohdan Triapitsyn 7977842e1e feat(ui): redesign the default shortcut layout around a mod+k leader
Single chords stay for everyday actions; open/go actions move to two-step
mod+k sequences; held mod+digit switches header session tabs and held
mod+alt+digit switches context panel surfaces. Rare actions leave the
shortcut schema for the command palette, every remaining action ships with
a default binding, and stored overrides from the old layout reset once.
Key matching now follows the physical key on non-Latin layouts and for
Option-modified digits on macOS, including in the recording dialog.
2026-08-26 14:52:31 +03:00
Bohdan Triapitsyn 76f977e580 docs: add changelog entries for the centralized shortcut registry 2026-08-26 12:30:48 +03:00
Bohdan Triapitsyn 3a3e719829 Merge pull request #2532 from ChangeHow/feature/shortcut-registry-prefix
feat(ui): centralize shortcuts and add prefix sequences
2026-08-26 12:30:03 +03:00
Bohdan Triapitsyn c09d42760c Merge the centralized shortcut registry onto current main
Lands the shortcut core from this branch on top of current main with review
fixes (close_session_tab support, prefix typing guard, broadened risky-binding
checks) and re-applies every surface migration against the code that moved
since the branch point.
2026-08-26 12:29:15 +03:00
Bohdan Triapitsyn e904abb794 fix(ui): drop the dead switch_tab actions that collided with surface switching 2026-08-26 11:17:33 +03:00
Bohdan Triapitsyn 94f6b5fd38 feat(ui): migrate all shortcut surfaces to the centralized registry 2026-08-26 10:59:04 +03:00
Bohdan Triapitsyn f1c3870909 feat(ui): land the centralized shortcuts core from #2532 with review fixes
The schema/config/bindings/registry/dispatcher module, useKeybind hooks,
recording dialog, reworked shortcuts settings page, help dialog, and the
localized action labels — re-based onto current main rather than merged
(the branch predates 440+ commits including the session-tabs shortcuts).

Review fixes applied on top of the original:
- close_session_tab (alt+w) joins the schema with labels in every locale;
  it shipped on main after the PR's base and would otherwise silently die.
- switch_context_surface's special-case in conflict resolution is now a
  declared prefixStyle config property instead of a magic id string.
- Duplicate handler registration warns in dev builds.
- The risky-browser-shortcut warning inspects every chord and covers
  mod+q/d/h/j/o/u plus mod+shift+w/q.
- The dispatcher remembers which target armed a two-chord prefix so the
  window-level completion handler can distinguish a deliberate sequence
  from typing in an editable field (guard lands with the dispatch hook).
- Schema tests: unique normalized default bindings enforced, and the
  flat-file-era override format proven to keep resolving.
2026-08-26 10:47:44 +03:00
Bohdan Triapitsyn 428854fd78 refactor(sync): replace the session-status setState monkeypatch with an owned API
The global session-status store patched its own setState to derive
active membership for callers replacing statusById — a boundary that
silently trusted any caller passing both fields to keep them consistent.
replaceGlobalSessionStatusById is now the one sanctioned way to swap the
map from outside the event reducers; the runtime-switch reset and the
tests that replaced the map directly go through it, and the patch is
gone. Follow-up to #3126 review.
2026-08-26 03:13:21 +03:00
Bohdan Triapitsyn 8d8d389294 docs(vscode): changelog for the resilient webview connection status (#2997) 2026-08-26 02:43:35 +03:00
Bohdan Triapitsyn 1d16a5e2f4 docs: changelog for the resilient VS Code webview connection status (#2997) 2026-08-26 02:43:17 +03:00
Bohdan Triapitsyn a29d3ac5c2 Merge pull request #2997 from VinciYan/fix/connection-status-resend
fix: resend connectionStatus at staggered delays to prevent stuck loading screen on slow networks
2026-08-26 02:43:00 +03:00
Bohdan Triapitsyn cc0abad9af refactor(vscode): share the cached-state retry schedule across providers
The staggered re-send lived as three near-identical private methods in
the chat view, agent manager, and session editor providers. One helper
now owns the delays, the connected gate, and the replaced-target guard;
the providers keep thin wrappers over their own panel/view identity.
Also restores the package.json trailing newline the branch dropped.
2026-08-26 02:42:47 +03:00
Bohdan Triapitsyn 8638b8abcd Merge branch 'main' into pr2997-integration 2026-08-26 02:40:48 +03:00
Bohdan Triapitsyn efb985f3a7 docs: changelog for virtualized large file previews (#3061) 2026-08-26 02:26:50 +03:00
Bohdan Triapitsyn 0b1ebbdd78 Merge pull request #3061 from gaojunran/pr1-large-file-preview
feat(ui): virtualize large file preview in FilesView
2026-08-26 02:26:34 +03:00
Bohdan Triapitsyn f0b1c889f0 Merge main into pr1-large-file-preview
The floating editor toolbar this branch was based on was removed on main
(the toolbar is always docked now); the file-view region resolves to
main's structure with this branch's virtualizer-bound ScrollableOverlay.
2026-08-26 02:26:20 +03:00
Bohdan Triapitsyn 9cb9b0f91b docs: changelog for the bounded OpenCode read timeout (#2969) 2026-08-26 02:23:57 +03:00
Bohdan Triapitsyn b4c22815b4 Merge pull request #2969 from herjarsa/fix/opencode-read-timeout-2470
fix(ui): bound OpenCode read requests so half-open sockets cannot freeze bootstrap (#2470)
2026-08-26 02:23:41 +03:00
Bohdan Triapitsyn 79b726451d Merge main and keep the read timeout armed without AbortSignal.any
When the caller passes its own AbortSignal and AbortSignal.any is
unavailable, the timeout was silently dropped — disabling the fix on
exactly the bootstrap reads it targets, since those carry a cancellation
signal. Compose the two signals manually through an AbortController in
that case; listeners detach when the request settles.
2026-08-26 02:23:21 +03:00
Bohdan Triapitsyn 90b6776e5b Merge branch 'main' into pr2969-integration 2026-08-26 02:22:29 +03:00
Bohdan Triapitsyn 7d16290827 docs: sort and consolidate the unreleased changelog
Highlights trimmed to the four genuinely major entries and ordered by
user impact; the markdown-preview comment surface folded into the chat
comment bullet, Cmd+P path matching folded into the unified search
matcher bullet, the instant session-open and uncached fade-in clauses
folded into their owning entries; fixes grouped after features, visual
polish last.
2026-08-26 01:33:05 +03:00
Bohdan Triapitsyn 3dbbf63fb9 docs: changelog for the session switching performance work (#3126) 2026-08-26 01:27:23 +03:00
Bohdan Triapitsyn e9b6fd1fd7 Merge pull request #3126 from c-w-xiaohei/perf/switch-and-scroll
perf(ui): speed up session switching and scrolling
2026-08-26 01:26:57 +03:00
Bohdan Triapitsyn 448ecc12b8 fix(ui): close review findings from the switch-and-scroll integration
- Markdown DOM cache: key includes a content-length fingerprint so an
  edited or reverted part re-materializing under the same id cannot
  restore stale DOM, memoization uses scalar identities instead of the
  part object (store reducers recreate part objects on unrelated updates,
  which re-ran the async render pipeline for identical content), and a
  probe with a mismatched locale/directory no longer destroys the entry
  it failed to claim.
- Sidebar bootstrap: the layout-level sync owner only knows known
  directories, so expanded projects bootstrapped serialized at background
  priority. The visible collection now publishes a second, expansion-
  aware demand owner, restoring concurrent hydration for expanded
  projects and worktree groups.
- Settings: local changes still sitting in the debounce buffer are not
  yet tracked as mutations, so a settings GET racing the debounce window
  briefly reverted them; reconciled results now reapply the pending
  buffer.
2026-08-26 01:03:47 +03:00
Bohdan Triapitsyn ac880e8e62 Merge main (anchored-turn chat scrolling) into perf/switch-and-scroll
Main replaced the chat timeline scroll engine while this branch was in
flight, which obsoletes two of its subareas and reshapes a third:

- Chat timeline: main's LegendList-based MessageList/ChatContainer win;
  the activation-overscan staircase targeted the removed tanstack path
  (LegendList provides adaptive rendering natively) and is dropped along
  with its test.
- Scroll shadows: main's hook-based masks stay (the virtualized list owns
  its scroll element — there is no wrapper to hand the styling to); the
  viewport-wrapper ScrollShadow rewrite, its index.css replacement, its
  test, and the call-site viewportClassName adaptations are reverted to
  main. The chat OverlayScrollbar keeps this branch's disableHorizontal.
- OverlayScrollbar: the direct-DOM rewrite lands, but binding now follows
  the live container node instead of binding once per ref object — the
  chat scroller remounts on every session switch, and a bind-once
  contract left the scrollbar attached to a dead element.
- Markdown renderer: the detached-DOM cache and warm-block fast path
  merge with main's block-commit reveal (enter cascade), streaming code
  highlighting, and gutter reservation; the per-block reconcile keeps
  both the decoration-refresh path and the reveal cascade.
2026-08-26 00:59:45 +03:00
Bohdan Triapitsyn 88c76557a5 docs: changelog for the anchored-turn chat scrolling rework
Also point a stale mobile.css comment at useChatTimelineScroll — the
engine it referenced no longer exists.
2026-08-26 00:40:13 +03:00
Bohdan Triapitsyn 5f0f8e64ed refactor(chat): split the composer status bar from the assistant status chip
The composer rendered a second StatusRow instance carrying the
pending-changes bar and the todos dropdown, so every restyle of the
floating assistant-status chip (glass, placement, sizing) silently
restyled the composer bar and its dropdown too — for the fourth time.

StatusRow is now only the floating chip above the composer; the
composer's own bar is a new ComposerStatusBar with the pre-glass layout,
its own container name, and its own container-query classes (the
anti-overlap rules that hide the long active todo under 38rem and the
changed-files label under 30rem moved with it — losing them during the
split let the two dropdown triggers overlap on mobile). The two
components share no markup and no CSS hooks anymore.
2026-08-26 00:35:25 +03:00
Bohdan Triapitsyn 043e71d725 fix(ui): give the scroll shadow size variable a real default
The base rule declared --scroll-shadow-size as var(--scroll-shadow-size,
48px) — a self-referential cycle, which computes to invalid and silently
resolves every mask below to none. The old ScrollShadow component masked
the bug by setting the variable inline; the hook-based path (the chat
scroller) sets only the data attributes, so the chat lost its top and
bottom scroll fades. A concrete 48px default restores them; inline
overrides from the component still win.
2026-08-26 00:09:34 +03:00
Bohdan Triapitsyn 2d8571c6f2 fix(chat): animate end following only during a live stream
The animated maintainScrollAtEnd also ran outside streaming, so opening
a historical session glided visibly through the whole conversation as
late row measurements corrected the end position, and an in-flight glide
could supersede explicit navigation. Corrections are instant unless the
session is actively working.

Returning to the bottom during a stream also lands on the end as of that
moment, and the list's own follow may not have re-armed after the user's
earlier gestures — the queued-send jump then fell behind the growing
reply. goToBottom now re-asserts the edge a few times (150/400/800ms)
until it holds; a new user gesture cancels the window.
2026-08-25 23:54:00 +03:00
Bohdan Triapitsyn ab370c018a refactor(chat): drop the blur from the block reveal
During a long stream the follow scroll masks the entrance entirely, so
the blur was invisible where it streamed and only a cost risk on weak
GPUs. Keep the compositor-only fade and rise (and the same-tick cascade)
— those are effectively free and still show on short replies where no
scroll motion is involved.
2026-08-25 23:16:15 +03:00
Bohdan Triapitsyn 5e37a436c1 feat(chat): soften the committed-block reveal into a materialize cascade
The 140ms fade read as a pop next to the scroll glide. The reveal now
runs 320ms on an ease-out-quint curve with a 10px rise and a touch of
blur resolving — paint-only properties throughout, so row measurement is
untouched — and blocks committed in the same tick cascade with a 55ms
stagger instead of appearing together. Reduced motion still disables the
whole thing.
2026-08-25 23:08:36 +03:00
Bohdan Triapitsyn d71bcb5025 feat(chat): colored streaming code and a reveal for committed blocks
With block-level commit the open code fence grows by whole lines at the
throttle cadence, so the old reason to skip highlighting it (re-tokenizing
a growing block ~40x/sec) no longer applies: a partial fence now
highlights too, and streamed code arrives colored instead of colorizing
only when the fence closes. Fences beyond 300 lines fall back to plain
text until closed, keeping the repeated worker re-tokenization bounded.

A freshly committed block also enters with a short fade-and-rise. The
class goes on the block's children (the wrapper is display:contents and
cannot animate) and the transform is paint-only, so virtualized row
measurement is unaffected; reduced motion disables it.

Verified over CDP on a production build: streamed code inside a still-
open fence renders highlighted, and the code text's x-position is
identical during and after the stream.
2026-08-25 18:40:42 +03:00
Bohdan Triapitsyn 3b8ed8fc36 feat(chat): block-level streaming reveal with a gliding follow
Token-by-token streaming mutates the trailing paragraph in place on
every tick: words rewrap, the last line jitters, and the whole reply
reads as flicker. Streamed text now commits only up to the last complete
line — prose arrives a paragraph at a time (a markdown paragraph is one
logical line), code fences reveal line by line, tables row by row — and
a shown block never changes again. A paragraph that runs long without a
newline releases at the last sentence (then word) boundary so the stream
never stalls. Applies to assistant text and reasoning; tool output keeps
its raw tail.

With growth arriving in block steps, the end follow switches to the
list's animated mode so each step is a glide — reveal and scroll read as
one continuous motion. The gesture opt-out now measures at-end from the
live list state instead of the cached flag, which the animated glide
deliberately leaves stale while trailing the edge; without that, a drag
during a glide could leave the scroll-to-bottom pill unshown.

Measured: end-following holds at distance 0 for the whole stream, and
the mobile drag opt-out shows the pill in three of three runs. The
continuous glide costs ~15% more main-thread time per streamed character
than the instant follow — the price of the motion.
2026-08-25 18:16:49 +03:00
Bohdan Triapitsyn b946fc7083 fix(chat): stable code-block geometry, hydration reveal, deduped file stats
Three first-render polish issues:

Code blocks jumped at end-of-stream: streaming defers the per-line
line-number markup, so the finished decorate pass inserted the gutter
column and shifted every code line right. The gutter's horizontal
footprint is now reserved with CSS while the markup is deferred, so the
final pass only fills in numbers and colors.

File references were verified twice per render and re-verified with the
wrong directory: the annotation pass's own DOM writes re-triggered its
MutationObserver, and the pass also ran before the effective directory
resolved — issuing stat probes under an empty directory and a second
time under the real one. The observer now ignores the pass's own
mutations and annotation waits for a resolved directory.

Content replacing the hydration skeleton popped in: it now plays a
one-shot 180ms fade (reduced-motion aware); cached session switches
never carry the class and stay instant. Also removed the dead
disableStaging prop and its unused pendingRevealWork threading.

Verified on a production build over CDP: a streamed code block's text
keeps its exact x-position across end-of-stream, and stat probes for a
message with file mentions are unique per path with the directory header
always present. The hydration fade path could not be exercised in the
harness (the live event stream pre-populates parts in a single-project
environment) and needs an eyeball check on a cold multi-project open.
2026-08-25 17:51:43 +03:00
Bohdan Triapitsyn 8c102cddd0 fix(chat): teleport the send anchor when sent away from the live edge
Sending from mid-history positioned the new turn with an animated
scrollToIndex; over a long distance the smooth scroll gets cancelled by
rows mounting and measuring along the way — a weak scroll attempt that
dies partway, leaving the reader where they were. Park instantly when
the viewport was not at the end at send time; the short smooth park
remains for sends from the live edge.
2026-08-25 17:25:06 +03:00
Bohdan Triapitsyn f85e70b359 fix(chat): return to the live edge on queued sends and teleport rail jumps
Sending while the agent is working queues the message without creating a
user row, so the anchor-arming send path had nothing to claim and the
viewport stayed parked mid-history. Queued sends now jump straight to
the live edge (instant, so end following re-latches immediately).

Rail selection also switches from smooth to instant scrolling: a long
smooth scroll through the virtualized timeline gets cancelled by row
remounts and lands mid-way or on the wrong message, while a teleport
always arrives.
2026-08-25 17:09:55 +03:00
Bohdan Triapitsyn 04f338cc49 feat(chat): upgrade @legendapp/list to 3.3.8 and let it own end following
3.3.x makes maintainScrollAtEnd follow content growth on its own — a
tail row growing in place included — which is exactly what the manual
totalSize correction existed for. Delete that correction (the totalSize
listener now only drives the anchored-turn glide) and pick up 3.3.x's
measurement batching, prepend-flash fixes, and web programmatic-scroll
fixes. Opt the explicit maintainScrollAtEnd config into footerLayout per
the 3.1.1 guidance.

The library's own released-on-user-scroll heuristic proved unreliable
one run in three against synthetic touch, so the gesture state machine
stays authoritative: while a real gesture owns the scroll, the list's
end pinning is switched off through a threaded endPinningReleased prop
and re-engages when the user returns to the end.

Validated with the CDP battery on a production build: stream follow
stays at distance 0, mobile drag releases with the pill shown in three
of three runs, resize oscillation stays at the reduced level, the rail
reaches the last turn, and profiled streaming cost per rendered
character matches the tuned 3.2.0 numbers.
2026-08-25 16:49:22 +03:00
Bohdan Triapitsyn e801afec43 fix(chat): do not snap back to the end after a width resize
A slow edge drag settles the resize detector repeatedly, and every settle
replayed the instant return to the live edge — reading as exactly the
jumping the resize suspension removes. Stay where the reader is; geometry
changed, and the next explicit action or stream correction re-engages the
edge as usual.
2026-08-25 16:01:22 +03:00
Bohdan Triapitsyn 5c39db0e36 fix(chat): release end pinning while the list width resizes
A pinned viewport shook during window resizes while a scrolled-up one
stayed calm: both pinning mechanisms — the growth corrections and the
list's maintain-scroll-at-end — re-assert the end against rows that are
still re-measuring, fighting the size compensation that keeps the free
case stable. While the width is actively changing, stand both down and
hold the reading position the same way the free path does, then re-assert
the live edge once with a single instant write after the resize settles
(only when the viewport was still following).
2026-08-25 15:54:39 +03:00
Bohdan Triapitsyn 92185f3a7b fix(chat): let the timeline rail reach the last turn while it is unmounted
Both scrollToTurnId and scrollToMessageId hard-returned false when the
target was the trailing (last) turn and its element was not mounted, so
clicking the rail's last item in a long scrolled-up session did nothing.
The trailing entry is a regular list row at the end of the data — scroll
to its index the same way history targets are reached.
2026-08-25 15:42:02 +03:00
Bohdan Triapitsyn 44de220442 fix(chat): compensate row size changes while the list width resizes
A width change re-wraps every row at once, and with size restoration off
the accumulated height delta above the viewport threw the visible content
hundreds of pixels up and down while dragging the window edge. Size
restoration must stay off in steady state — a tool result expanding in
place has to grow downward — so a ResizeObserver on the scroll node
enables maintainVisibleContentPosition size compensation only while the
width is actively changing and releases it 300ms after it settles.

Measured on a continuous 1400-800-1400 drag over a long session pinned to
the live edge: the largest per-step displacement of a visible paragraph
drops from ~680px (median of three runs) to ~200px, and the viewport
never detaches from the end.
2026-08-25 15:33:03 +03:00
Bohdan Triapitsyn 4d3542fdb4 fix(chat): register touch opt-out by finger direction
On touch surfaces the gesture opt-out waited for the viewport to have
already left the end, but streaming corrections re-pin the viewport every
chunk, so the drag never registered: the user could not scroll away
mid-stream, the scroll-to-bottom pill (which waits for a gesture) never
appeared, and live-follow stayed armed — which also made the mobile
load-older button throw the viewport to the bottom, since the prepend's
content growth ran the end correction.

Track the finger's Y between touch events and treat a downward drag (the
touch mirror of wheel-up) as an immediate opt-out. Loading older history
also releases live follow explicitly before the prepend, covering the
case where the button is reachable without any prior scroll.
2026-08-25 15:06:31 +03:00
Bohdan Triapitsyn 34ae8b059e refactor(chat): drop the inert content-change and animation-handler contract
The old scroll engine needed message parts to report content growth
(onContentChange) and per-message animation lifecycle callbacks
(AnimationHandlers) so it could re-pin the viewport. The timeline list
measures growth itself now, and the replacement hook had already stubbed
the whole contract with no-ops kept only for source compatibility.

Remove it end to end: the hook exports, the container and list threading,
the ChatMessage/MessageBody signal-only effects, and every part-level
prop and call site. Expand/collapse behavior and reveal animations are
untouched — only the reporting channel goes.
2026-08-25 15:01:55 +03:00
Bohdan Triapitsyn dac31ee026 feat(settings): add streaming auto-follow toggle
New Streaming section on the Chat settings page with a checkbox that
controls whether the viewport follows new content while a response
streams. Default stays on. With it off, the anchored user message still
parks at the top on send, but no glide or end-follow correction runs and
the list's maintain-scroll-at-end stays disabled; the scroll-to-bottom
pill and session open keep scrolling explicitly.

Persisted through desktop settings like the other chat toggles (auto-save
diff, authoritative apply, sanitize), registered in settings search, and
localized in every locale.
2026-08-25 14:53:43 +03:00
Bohdan Triapitsyn 87c1ae809d perf(chat): follow the live edge with a direct scroll write
Streaming follow scheduled a two-frame animation-frame chain per content
growth and then went through the list's programmatic scrollToEnd, whose
scroll bookkeeping schedules further frames. Together they roughly
doubled frame production for the whole stream (16 -> 35 frames/s in a
profiled streaming session), with the matching style recalc and layout
cost per frame.

In following-end mode, correct synchronously in the totalSize listener by
writing scrollTop directly — the same write path a user gesture takes —
and keep the animation-frame chain only for the anchored-turn glide.
Profiled streaming frame production returns to the previous engine's
range and peak heap drops from 164 MB to 90 MB.
2026-08-25 14:27:37 +03:00
Bohdan Triapitsyn 3703ba730e fix(chat): overlay live parts on every streaming tail message
The streaming tail only overlaid the actively streaming message with live
parts from the sync store. When a turn moved to its next step message, the
previous message fell back to its lagging base record, briefly dropping its
completed tool parts — remounting them and replaying their reveal
animation once the record caught up.

Overlay live parts for every message of the streaming tail via a new
useSessionPartsForMessages hook, guarded so an empty live array never
erases parts the record does have. Also key generate-effect glyphs by
index so appended text does not replay earlier characters, and keep tool
row reveal wrappers mounted unconditionally.
2026-08-25 14:07:33 +03:00
Bohdan Triapitsyn 6a944db746 feat(chat): status row and scroll pill share one anchored slot
The streaming status and the scroll-to-bottom pill now hand off to each
other in one place above the composer: same anchor, same input-aligned
column, both in the popover glass (ring and soft shadow stay pill-only
as the interactivity cue, with the shadow on a wrapper so the glass
backdrop-filter cannot drop it after hide/show cycles). The pill is a
single full-surface button, moves to the left edge, and while the
session streams it carries a compact one-line status label instead of
the status row's animation machinery, which did not survive a 32px
chip.

Behavioral fixes along the way: a gesture shows the pill immediately
(no debounce) and hides the status in the same frame; gestures register
whenever the viewport can actually move up — an anchored short turn
kept live-follow armed and suppressed the pill entirely; the status
row sheds its in-list morph offsets (mb-6, message column) and its
inline-size container gets the glass on an inner row, since a query
container cannot shrink-wrap; its working text uses full
muted-foreground to match the pill.
2026-08-25 12:35:12 +03:00
Bohdan Triapitsyn 5685e9a6a3 fix(chat): mode-driven scroll pill — no crossfade mush with the status row
The animated glide trails its target between corrections, so the
distance-based at-end signal flapped during streaming and the pill and
the status row crossfaded into overlapping half-transparent mush. The
pill now shows only in free-scrolling (a real gesture), which also
keeps the status row's visibility stable; a gesture taken after our own
movement already left the end offers the pill immediately.
2026-08-25 11:49:52 +03:00
Bohdan Triapitsyn d48de26070 feat(chat): glass scroll-to-bottom pill on the left carries the streaming signal
Away from the live edge the floating status row fades out (it was
transparent text over scrolled history) and the scroll-to-bottom pill
takes over the activity signal with a pulsing dot. The pill moves to
the left edge — matching the status row it stands in for — and gets the
same glass treatment as the dropdowns.
2026-08-25 11:45:17 +03:00
Bohdan Triapitsyn aa96a0fb5c fix(chat): status row floats inside the existing bottom spacer, no extra inset
The rest gap after completion doubled up: the status overlay reserved
its own end inset on top of the footer's 10vh spacer. The overlay now
floats within the spacer zone — its height feeds only the glide target
(the live line still rests above it), the list gets no extra inset, and
completion cannot shift layout because there is nothing to collapse.
The finished message's metadata lands in the slack the glide left above
the status row, restoring the old morph-in-place feel. The frozen
height hack and the opaque background are gone — the overlay only ever
covers blank spacer, not text.
2026-08-25 11:33:42 +03:00
Bohdan Triapitsyn 3ff3354168 fix(chat): completion is a pure visibility change — status inset never collapses mid-session
The barely-visible shift at end-of-stream was the reserved status-row
inset being released when the row unmounted. The measured overlay
height now only grows during a session and resets on session switch, so
the finished message's metadata footer appears exactly where the status
row was, with zero layout movement.
2026-08-25 11:20:52 +03:00
Bohdan Triapitsyn d1a6955273 fix(chat): status overlay is opaque and occludes the scroll math
The floating status/working row let streaming text show through it and,
because the scroll math did not know the overlay existed, the reveal
correction targeted the very bottom edge — the live line ended up
hidden beneath the status row ('catches up to almost-the-end'). The
overlay now has a solid background, its height is measured with a
ResizeObserver and fed to the timeline as the occluding overlay height,
so the live streaming line glides to rest just above the status row and
the list reserves matching end inset at rest.
2026-08-25 11:02:01 +03:00
Bohdan Triapitsyn 1b4b8509ba fix(chat): glide the anchored end-follow instead of per-line hops
The phase-2 reveal correction now scrolls animated; successive
corrections restart the smooth scroll from the current position, so
streaming follows as one continuous motion. User gestures interrupt the
native smooth scroll, so free-scrolling is unaffected.
2026-08-25 01:59:53 +03:00
Bohdan Triapitsyn 6a00d6a830 fix(chat): drive anchored end-follow from the list's total size
Phase 2 of the anchored turn (following the live edge once the reply
outgrows the viewport) never ran: the reveal correction was triggered
only by entries-array changes, and the streaming tail grows inside a
single row without touching the array. The hook now also subscribes to
LegendList's totalSize and re-runs the same guarded correction on every
content growth.
2026-08-25 01:54:52 +03:00
Bohdan Triapitsyn 955b2cc812 fix(chat): anchored-scroll behavior parity with the reference model
Six reported defects, one root theme — our port diverged from the
reference semantics:

- a user gesture no longer collapses the reserved anchored end space
  (that snap-to-bottom was the 'anchoring vanishes on a micro-scroll');
  it only disarms the follow/anchor machinery, and anchor remeasures
  after the user takes over can no longer re-position the viewport
- the anchor arms synchronously BEFORE the send reaches the store, and
  the claim compares against the arm-time baseline — arming a frame
  after the optimistic row committed is why anchoring 'rarely worked'
  and sending from mid-history did nothing
- gestures are selective (wheel up, touch/pointer away from the end,
  PageUp/Home/ArrowUp), so scrolling toward the end or clicking a row
  at the live edge no longer kills follow
- 'at end' is a tight 40px band against the full content length instead
  of the list's half-viewport isNearEnd, so the scroll-to-bottom pill
  appears when the viewport actually leaves the end
- reaching the end re-arms follow without knocking an anchored turn out
  of its mode, and the overlay scrollbar suppression follows scroll
  ownership rather than the anchor's mere existence
- the status/working row moved out of the list footer to a fixed spot
  above the composer: inside the list it walked down with every
  streamed line, and its unmount was part of the end-of-stream jerk
2026-08-25 01:35:45 +03:00
Bohdan Triapitsyn 42a8eafafc fix(electron): use Bun to launch dev app 2026-08-25 01:13:46 +03:00
Bohdan Triapitsyn d2d8669564 refactor(chat): replace timeline scroll engine with anchored-turn LegendList
Sending a message now parks that message near the top of the viewport
and streams the reply into reserved end space below it, instead of
jumping to the bottom and chasing it.

- swap @tanstack/react-virtual for @legendapp/list in the chat timeline; the
  streaming tail becomes a normal list row rather than a separately rendered
  block, so one component owns the scroll position
- add timelineScrollAnchoring: pure anchored-turn geometry plus the three
  scroll modes (following-end / anchoring-new-turn / free-scrolling)
- replace useChatAutoFollow with useChatTimelineScroll, which opts out of
  automatic movement on real gestures via a generation counter instead of the
  timer windows the old implementation needed to recognise its own writes
- move the load-older button, question/permission cards, recap, status row and
  bottom spacer into the list header/footer, since the list owns its container
- extract useScrollShadow so the shadows can attach to that container

maintainScrollAtEnd and maintainVisibleContentPosition replace the manual
prepend anchor-hold and the mobile quiet-window prepend deferral.

Validated: workspace type-check, lint, web build, ui tests per file.
Scroll behaviour itself is unverified and needs manual testing on web, desktop
and iOS.
2026-08-25 01:10:00 +03:00
Bohdan Triapitsyn 3b7d5e1c34 refactor(header): drop session tab tooltips
The hover info card (project/branch/PR/time) added noise without pull —
the tab title plus the sidebar already cover it. The tooltip body
component, its per-hover subscriptions, and the native title attribute
are gone; right-click/menu and the status dot are untouched.
2026-08-25 00:52:01 +03:00
Bohdan Triapitsyn 8458d2a446 feat(header): session tabs are opt-in
Default off; the changelog entry points at Settings → General →
Navigation → Session tabs and mentions the Alt+W close shortcut.
2026-08-25 00:50:19 +03:00
Bohdan Triapitsyn b6e14cbf21 refactor(settings): always-docked editor toolbar; reorder Navigation
The 'Always show editor toolbar' preference is gone — the docked
toolbar under the file tabs is now the only mode, and the floating
hover toolbar branch in the files editor (with its open-state and
outside-click machinery) is deleted. The stored preference is dropped
by a store migration and removed from desktop settings persistence,
settings search and every locale.

Navigation section order now reads: file editor keymap, auto-save,
terminal shell + login shell, Terminal Quick Keys, and the Session
tabs group last.
2026-08-25 00:32:09 +03:00
Bohdan Triapitsyn f40a247be9 fix(settings): include the Session tabs toggle in the General page's visible list
The General section renders OpenChamberVisualSettings with an explicit
visibleSettings allowlist, so the new control was searchable but never
rendered.
2026-08-25 00:20:37 +03:00
Bohdan Triapitsyn ce90e7e24f feat(settings): Session tabs toggle in General → Navigation
A new Session tabs group (web/desktop only) turns the header session
tabs off; disabled, the header renders the exact pre-tabs view — plain
session title with meta row and the always-visible session menu (the
same block VS Code uses). The Alt+W close-tab shortcut no-ops while
tabs are off. Registered in settings search with a matching anchor;
labels translated in all locales.
2026-08-25 00:05:45 +03:00
Bohdan Triapitsyn 2c1d8d592e feat(header): Alt+W closes the active session tab, customizable in Settings
The close-with-neighbour-activation logic moved into a shared helper
(the strip and the shortcut use the same path). Alt+W is the default
because the browser owns Cmd/Ctrl+W on web; desktop users can rebind it
— the action is registered as customizable, so it appears in the
Settings shortcuts section and the shortcuts help automatically. VS
Code is excluded (it has no session tabs).
2026-08-24 20:57:01 +03:00
Bohdan Triapitsyn 71bfb3c8e0 fix(header): cap session tabs at 10 2026-08-24 20:50:22 +03:00
Bohdan Triapitsyn dcacf4b3fc perf(header): cap session tabs at 20; tooltip data loads on hover only
Auto-add means the strip only grows, so past 20 tabs the oldest one
leaves the working set (the newly opened tab is always the survivor).
The branch/worktree/PR/project subscriptions that feed the hover
tooltip moved into the tooltip body component, which mounts only while
the tooltip is open — a resting tab now subscribes only to its status
dot's session status and unread count.
2026-08-24 20:47:51 +03:00
Bohdan Triapitsyn 275d381bd0 docs: changelog entry for header session tabs 2026-08-24 20:41:42 +03:00
Bohdan Triapitsyn a53c78b54a fix: adjust light theme selection color 2026-08-24 20:37:25 +03:00
Bohdan Triapitsyn 4b2ad01b4a fix(header): inactive-tab rename actually starts; snappier tab state change
Rename from another tab's menu stored only a boolean that the
session-switch reset effect wiped before the menu finished closing, so
the tab activated but rename never began. The pending rename now
carries the target session id and begins exactly when that session
becomes active (whether the menu closes before or after the switch),
without the reset effect cancelling it — and without an in-flight
rename being cancelled by unrelated re-renders. Tab background/text
state changes animate at 75ms so activation reads immediate.
2026-08-24 20:18:45 +03:00
Bohdan Triapitsyn b1023b87e7 fix(header): tab title fade is resting-state only; clearer separators
The title fade no longer sits on controls: it lifts while the tab is
hovered or its controls/menu are open (hard clip instead) and is not
applied at all while the active tab renames. The active tab clips its
title instead of showing an ellipsis, matching inactive tabs. Tab
separators drop the extra transparency that made them nearly
invisible.
2026-08-24 20:16:06 +03:00
Bohdan Triapitsyn aad9c25164 fix(vscode): adjust expanded layout threshold for showing sessions sidebar
Keeps enough space for the chat panel when the sessions sidebar stays visible
Bases the expanded layout breakpoint on the sidebar width instead of a fixed value
2026-08-24 20:11:18 +03:00
Bohdan Triapitsyn c47c7190f4 feat(header): session tab tooltips, status dots, cursor context menu
Right-click now opens the session menu under the cursor (the sidebar's
context-menu pattern; the same header-supplied items back both the
"..." dropdown and the context menu via injected menu primitives) and
still never changes the active tab. Hovering a tab shows the sidebar's
session tooltip — title, last-activity time, project, branch and PR
status — after a delay, suppressed while a menu is open or a drag is in
flight. Each tab carries the sidebar's status dot at its end (accent
while the session runs, info-blue for unread), hidden while the hover
controls overlay it. Tab titles fade out instead of ending in "...",
and while the active tab is renaming its hover controls stay hidden so
only the rename controls show.
2026-08-24 20:02:59 +03:00
Bohdan Triapitsyn 338dc73e96 fix(header): one session-tab menu, hidden scrollbar, right-click, control order
The strip now owns a single dropdown per tab, fed by the header with
items bound to that tab's session — rename (activates the tab first),
copy id, share/copy link/unshare, export and move-to-worktree (active
tab only, they need the loaded directory), close other tabs, archive
and delete with the confirm dialog targeting the right session. The
separate inactive-tab menu is gone, and there is no Close item — the
tab's close button covers it, now placed after the menu button.

Right-click opens that menu without activating the tab. The menu's
anchor overlay stays mounted until the close animation finishes, which
removes the popup flashing in the top-left corner on close. The
scroller hides its scrollbar via a dedicated CSS class (the bar was
shifting the header content vertically).
2026-08-24 19:24:17 +03:00
Bohdan Triapitsyn 97ddab73a1 fix(header): session tabs span to the right controls, close buttons, even padding
The old flex spacer split the header in half, boxing the strip into the
middle while tabs shrank to slivers; the spacer now renders only for
the VS Code and surface-title layouts, tabs keep a fixed width (w-44)
and the strip scrolls behind the right-side buttons. Every tab gains a
hover-revealed close button next to the menu; the active tab's menu is
hover-revealed too (it was always visible) — the header passes it into
the strip so both overlays behave identically. The active tab's inner
padding matches inactive tabs.
2026-08-24 19:08:33 +03:00
Bohdan Triapitsyn 447e43eac5 style(header): session tabs follow the titlebar tab design language
Uniform compact tabs (h-7, 6px radius, 13px medium) that share the strip
width and shrink before scrolling, thin separators between inactive
neighbours that disappear around the active or hovered tab, a
hover-revealed menu button anchored to the tab's end, a soft selection
pill for the active tab, and hidden scrollbars with fade edges. The
active tab is single-line: the project/branch meta row stays only in the
VS Code header, which keeps the plain title.
2026-08-24 17:41:34 +03:00
Bohdan Triapitsyn a3813f57e9 feat(header): session tabs — a horizontal working set of open sessions
Web/desktop header replaces the single session title with a strip of
soft pill tabs, one per session the user has opened (sidebar, palette
or deep link — opening anywhere adds a tab once). The active tab is the
familiar title block — rename, meta row and the full session menu —
inside a gently selected pill; a brand-new draft shows as a transient
pill until its session exists. Inactive tabs show the title with a
hover-revealed "..." menu (close tab, close other tabs, copy id) that
nudges the text like sidebar rows, and close by middle-click too.

Tabs drag to reorder, scroll behind the right-side header buttons with
soft fade edges, respect the reserved window-controls inset, and
persist across reloads. Closing the active tab activates its neighbour
(or opens a new draft when it was the last). Tab ids whose session is
not in the loaded list stay stored but hidden, so a partial session
list never destroys the working set. VS Code keeps the plain title;
mobile is untouched.
2026-08-24 17:07:09 +03:00
Bohdan Triapitsyn 7bd27d44d6 fix(vscode): bundle syntax worker imports 2026-08-24 16:58:45 +03:00
Bohdan Triapitsyn c82f188fc8 refactor(surface): remove the main-area surface concept entirely
activeSurface was permanently 'chat' after the legacy mobile layout
removal, so the whole concept is gone: the store field, surfaceGuard,
setActiveSurface/setSurfaceGuard, the per-runtime surface memory in
prepare/restoreForRuntimeSwitch, and WorkspaceSurface itself. All ~30
setActiveSurface('chat') call sites were no-ops and are deleted;
always-true 'is the chat active' checks in keyboard shortcuts, Header
and ChatContainer are unconditional now. FilesView's dirty-file guard
kept its file-switch and close protection but drops the surface-switch
branch nothing could trigger. TerminalView visibility comes only from
its callers. The router keeps parsing legacy ?tab= links (they open the
matching context-panel surface) via its own RouteTab type and no longer
serializes a tab or diff file into URLs — desktop URLs never carried
them anyway.
2026-08-24 16:36:41 +03:00
Bohdan Triapitsyn c6e39f15fe docs: changelog entry for the app-shell viewport switch 2026-08-24 16:21:13 +03:00
Bohdan Triapitsyn 4da7604d7d refactor(surface): drop the deprecated main-tab aliases and dead diagram surface
MainTab/activeMainTab/setActiveMainTab/setMainTabGuard were deprecated
mirrors of the surface names — every call site now uses
activeSurface/setActiveSurface/setSurfaceGuard directly and the aliases
are gone, including the persisted mirror field.

The 'diagram' surface had no way to open it (navigateToDiagram had no
callers except a .drawio attachment click that navigated to a surface
nothing rendered); the surface, DiagramView, and its store plumbing are
removed, and a .drawio attachment now opens in the file panel.

?tab= deep links map to the matching context-panel surface instead of
setting a main-area surface nothing renders, and a persisted non-chat
surface can no longer rehydrate into a blank main area.
2026-08-24 16:21:05 +03:00
Bohdan Triapitsyn 8b7d7803de refactor(layout): remove the legacy mobile layout from MainLayout and Header
Phone viewports run the separate MobileApp shell (and a viewport crossing
now reloads into it), so the desktop layout's mobile branch was
unreachable: the drawer machinery, the full-screen secondaryView surface
switch (including the terminal/diagram desktop carve-out nothing could
trigger), the mobile header with its tab bar, the Cmd+number tab
shortcuts, the mobile quota panel, and the surface guard that reset
non-chat tabs. DrawerContext had no consumers left and is deleted.
Header drops from 2630 to 1853 lines; the desktop render is unchanged.
2026-08-24 16:15:16 +03:00
Bohdan Triapitsyn 841eca5720 feat(surface): switch app shells when the viewport crosses the phone threshold
The mobile-vs-desktop surface is stamped once at boot, so a browser
window narrowed past the phone threshold kept the desktop shell (and
its legacy squeezed layout) until a manual reload. A viewport watcher
now reloads into the other shell once the resize settles — the same
mechanism the old Settings toggle used. Fixed shells (Capacitor,
desktop, VS Code) and ?surface= overrides never switch.

With the new mobile app reachable this way, the old/new mobile layout
preference is gone: phones always get the mobile app.
2026-08-24 16:08:04 +03:00