3564 Commits
Author SHA1 Message Date
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
c_w_xiaohei ceee7f350e fix(ui): preserve upstream sidebar and selection behavior 2026-08-26 02:36:07 +08:00
c_w_xiaohei f329a22cf3 test(ui): align selection with context shell 2026-08-26 02:31:35 +08:00
c_w_xiaohei 1d694932aa fix(ui): complete session rebase integration 2026-08-26 02:31:19 +08:00
c_w_xiaohei 45693e84bd perf(ui): widen markdown session cache
Retain detached Markdown DOM for eight recent sessions so medium-frequency session switching can restore more first-screen content without reparsing it.

Limit each session to four cached parts, reducing the worst-case retained fragment count from 36 to 32 while preventing long sessions from dominating the cache.
2026-08-26 00:44:14 +08:00
c_w_xiaohei fa1503a038 perf(ui): narrow session switch fanout
Keep sidebar rows independent from the active sync directory by moving export history loading behind an explicit-directory command.

Make active-project selection preserve project topology and remove activeProjectId from mounted group render props. Reuse a per-session-array ID index so subscription snapshots do not repeatedly scan session lists.
2026-08-26 00:44:14 +08:00
c_w_xiaohei 701173e399 perf(ui): index and batch session updates
Batch global session, status, ordering, and activity timing mutations at the existing directory event boundary so large subagent bursts publish each owner once.

Maintain active session roots, children, and directory buckets in the global store, reuse them in Sidebar projections, and avoid rebuilding live aggregates and structural data for unrelated renders while preserving authoritative ordering reconciliation.
2026-08-26 00:42:37 +08:00
c_w_xiaohei df924aa532 fix(ui): bound settings mutation history
Skip mutation history when no settings operation is in flight and merge updates that share the same operation visibility window. This keeps stale-response reconciliation unchanged while preventing history from growing with repeated debounced updates.

Add a repeated-update regression covering an older pending load and latest-value reconciliation.

Tests: bun test src/lib/persistence.test.ts
2026-08-26 00:42:37 +08:00
c_w_xiaohei 4454bf7e14 refactor(ui): encapsulate settings mutation tracking
Keep settings mutation revisions and in-flight operations behind a private tracker so stale GET and PUT responses cannot overwrite newer local settings.

Remove redundant runtime identity and numeric operation IDs. Use operation object identity while preserving runtime reset and mutation retention behavior.

Tests: bun test src/lib/persistence.test.ts
2026-08-26 00:42:36 +08:00
c_w_xiaohei 532bdab5e8 fix(ui): preserve upstream behavior after performance rebase 2026-08-26 00:42:36 +08:00
c_w_xiaohei 9b9d7069c7 perf(ui): isolate scroll shadow effects 2026-08-26 00:42:36 +08:00
c_w_xiaohei dd6a632bc5 perf(ui): streamline overlay scrollbar updates 2026-08-26 00:42:36 +08:00
c_w_xiaohei cd179bd118 perf(ui): avoid parsing markdown control icons 2026-08-26 00:42:36 +08:00
c_w_xiaohei ed37d8e249 perf(ui): isolate sidebar session selection 2026-08-26 00:42:36 +08:00
c_w_xiaohei 77da97953f perf(ui): reuse detached markdown DOM 2026-08-26 00:42:36 +08:00
c_w_xiaohei 1dfa3aee2f perf(ui): stage history overscan restoration 2026-08-26 00:42:36 +08:00
c_w_xiaohei 1c3c26f2bf perf(ui): batch Mermaid viewer initialization 2026-08-26 00:42:36 +08:00
c_w_xiaohei 25be29731f perf(ui): reuse warm markdown blocks 2026-08-26 00:42:36 +08:00
c_w_xiaohei 26dbc2f309 perf(ui): streamline session sidebar state 2026-08-26 00:42:36 +08:00
jaygupta17 d00a48b8b1 test(ui): drop stale chat surface assertions
Carries openchamber/openchamber#3124 until it lands: upstream commit
c82f188fc removed setActiveSurface from production code but left the
addSelectionToChat test asserting it, so the ui suite fails on any
branch including main. Byte-identical to that PR's fix; drop this
commit when rebasing onto main after #3124 merges.
2026-08-25 21:30:45 +05:30
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
jaygupta17 72c1d28fc6 fix(ui): drop unused nestedRepoSelection binding and stale review-flow dependency 2026-08-25 21:09:39 +05:30
jaygupta17 11a48958e8 fix(git): resolve blank nested-repo surfaces; add repository pickers
The resolution gate on the pull-request, walkthrough, and mobile changes
surfaces stayed on forever (rootIsGitRepo stays false on a non-repo
root) while NestedRepoResolutionStates exits once the selected
repository probes as a repository, so those surfaces rendered nothing.
The gate now shows resolution states only while the operating directory
has not proven to be a repository, matching GitView.

Extract GitHeader's repository switcher into git/NestedRepoPicker and
mount it in the diff toolbar, a new slim header in the pull-request
view, the walkthrough header, and the mobile changes header. The pick
is shared per root, so every surface follows.

The walkthrough tab mounts keep-alive and hidden; it now receives a
visible prop so discovery waits until the tab is actually opened. Add
component tests for the shared resolution states.
2026-08-25 20:47:02 +05:30
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
Roberto Berto 14c40c9615 feat(chat): add quick-open file icon in tool card header
Add a small external-link icon next to the tool display name in the
collapsed tool card header (Write/Edit/MultiEdit/ApplyPatch), so the
target file can be opened in the side panel (web/desktop) or editor
(VS Code) without expanding the card.

On web/desktop (no runtime.editor), the icon falls back to
useUIStore.openContextFile{AtLine} + mobileActions.openFiles(), matching
the existing openEntryFile pattern. The existing handleMainClick only
opens the file when runtime.editor is available, so this icon is the
first way to open a file from the tool header in the browser.

Path resolution reuses getPrimaryToolPath + toAbsoluteFilePath; the diff
tools also resolve the first changed line and primary diff via the
existing getFirstChangedLineFromMetadata / getPrimaryDiffFromMetadata
helpers. The icon stops click propagation so the card-toggle-on-click
behavior is preserved.

Adds the chat.toolPart.openFile i18n key across all 11 locales.
2026-08-25 14:32:33 +00: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
jaygupta17 e26b55e067 fix(git): exit nested-repo states once resolution succeeds
NestedRepoResolutionStates had no success exit: on a non-repo root
rootIsGitRepo stays false forever, so once repositories were found the
pull-request and walkthrough tabs kept showing 'Checking repository'
even after the selected repository probed as a repository. GitView never
hit this because its call site sits inside its own isGitRepo === false
branch. The component now takes the operating directory's probe and
returns null when it resolves true.

DiffView also still keyed its not-a-repository gate and every diff
fetch off the raw project root, so opening a change from a nested
repository showed 'This directory is not a Git repository'. It now
resolves the nested repository for git data and diff operations while
session-scoped lookups (session messages, review-flow directory) stay
on the root.
2026-08-25 19:55:00 +05:30
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
jaygupta17 cda273d69f feat(git): resolve nested repositories in the other git surfaces
Extract GitView's resolution flow into hooks/useNestedGitDirectory (root
probe, discovery, auto-select, stale-selection recovery) so the flow no
longer depends on SessionSidebar probing the root first, and reuse it in
the pull-request view, walkthrough view, and mobile changes surface —
all three now operate on the selected nested repository instead of
dead-ending on a non-repo root. Shared pending/failed/unsupported/empty
states live in git/NestedRepoResolutionStates; desktop changes inherits
the behavior through GitView. Selection stays shared per root, so the
picker's pick carries across surfaces.
2026-08-25 19:15:53 +05:30
jaygupta17 5e3f9d1ba2 fix(git): harden nested repository discovery
- discard an in-flight discovery when the runtime switches, mirroring the
  isRequestCurrent pattern, so a late completion cannot repopulate the
  cleared map and suppress a fresh scan
- treat a 501 from /api/fs/git-dirs as an explicit 'unsupported' marker
  instead of a generic failure; the VS Code webview now answers the route
  with unsupportedWebRouteResponse so non-repo roots show the honest
  not-a-repository state without a futile Retry
2026-08-25 19:15:35 +05:30
Matt Peter 56b5e9971f fix(small-model): don't double up /v1 in Anthropic baseURL override
Match @ai-sdk/anthropic exactly: baseURL is the full API prefix, so
/messages is appended as-is instead of unconditionally inserting /v1,
which broke configs where the baseURL already ends in /v1 (the common
form OpenCode itself passes to @ai-sdk/anthropic).

Addresses review feedback from @btriapitsyn on PR #3000.
2026-08-25 09:42:28 -04:00
Matt Peter f459206e4a fix(small-model): route anthropic provider through configured baseURL
The Anthropic small-model dispatch (session-assist recap/suggestion,
walkthrough, etc.) hardcoded https://api.anthropic.com/v1/messages,
ignoring provider.anthropic.options.baseURL — the same override every
other provider branch already respects. Any setup routing the anthropic
provider through a local proxy (e.g. Meridian for Claude Max / Vertex)
had the small-model call bypass the proxy and hit the real Anthropic API
with whatever placeholder credential the proxy config uses, failing with
401 invalid x-api-key on every call.
2026-08-25 09:42:28 -04:00
jaygupta17 f56b934dc9 Merge remote-tracking branch 'origin/main' into feat/nested-git-repos
# Conflicts:
#	packages/web/server/lib/fs/routes.test.js
2026-08-25 18:39:25 +05:30
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