Nothing covered ModelControls, which is where the effort restore decides
between "the user chose Default" and "nothing was found". Three cases: a
concrete effort in the session history is restored, history without an effort
records no choice, and a preserved manual override keeps a recorded Default.
The middle one fails on the previous commit's parent — it recorded `null` and
then `undefined` for the same restore.
Also drops the `fetchAll` binding GitView stopped using when the post-bootstrap
refresh moved to a forced `fetchStatus`; it was failing `bun run lint:ui`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZuVVgziiLjD81W5vaxdH2
`commitVariantSelectionForModel` turned every `undefined` into an explicit
`Default` (`null`), but it serves two kinds of caller. The picker means "the
user chose Default"; the history and manual-override restores mean "nothing
was found". Restoring a session therefore recorded a choice nobody made, and
`resolveModelVariantSelection` collapsed that `null` back to `undefined`, so
the next restore recorded it again. Because an explicit `Default` outranks the
agent and settings defaults by design, the session latched onto `Default` and
the concrete effort its own history carried could not come back.
Move the decision to the callers: the four picker paths pass `variant ?? null`,
the restore paths pass their result through, and the resolver returns the
selection store's three states instead of two. The follow-up write in the
history restore goes with it — the apply above it already recorded the same
agent and model, and a second write could only disagree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZuVVgziiLjD81W5vaxdH2
Refresh hints fired while setup commands run can cache a mid-creation
dirty snapshot; lifting the gate the moment bootstrap settles flashed
that stale snapshot until the forced fetch resolved. The gate now stays
down until the post-bootstrap status fetch completes.
Message metadata records the effective effort, inherited defaults
included. The history-restore effect re-applied it as an explicit
override and re-pinned it per session one render after every send, so
the picker still jumped from Default to the settings default despite the
send-time fix. Only an effort deviating from what the model would
inherit is restored as a choice; the preserve-manual-override branch
follows the same rule.
A freshly created worktree transiently looks dirty until its setup
commands and initial git reset finish. The work status panel showed
those files as changes on the branch and the draft's worktree dropdown
flashed its dirty warning, and both then froze on that state because
nothing refetched after bootstrap. Make the bootstrap state (the
existing authority on unfinished creation) subscribable, suppress the
dirty probe and the changed-files row while it is pending, and force one
status fetch when it settles so the lifted gate shows the reset tree.
The explicit-Default behavior from 77d756aeb (Default = send no effort)
was based on a misdiagnosis: the tester did not know a default effort was
configured in settings, so the settings default applying under Default is
the intended behavior, not a bug. Restores the pre-fix effort resolution,
tests, and documentation.
Linear and GitHub logins, quotas, MCP status, skills and agent memory are
served by whichever instance is connected, but each was cached globally or
by directory alone — which two instances can share. Switching instances left
the previous instance's answers on screen and its Linear login usable against
a runtime that has no Linear.
Reset them all through runtimeEndpointReset, each store guarding its in-flight
requests with a generation so a response for the previous instance cannot land
in the new one. The Linear team filter is now persisted per instance: a team
belongs to one workspace, so carrying it across filtered the new instance's
issue list down to nothing.
Usage also waits for the instance to report itself initialised before loading.
Providers report themselves as configured only once the instance can read their
credentials, so a fetch fired at mount answered "nothing configured" for every
provider and cached it — which is why Usage stayed missing from the work-status
panel until Settings -> Usage forced a fresh fetch.
* feat(ui): block branch switches on dirty trees
* feat(ui): show unpushed commits in git branch selector
* feat(ui): show recent branches in git selector
* fix(ui): persist recent branch status
* feat(ui): add mobile branch picker
* fix(ui): guard mobile branch checkout
* fix(i18n): restore Turkish git empty state labels
* feat(ui): flag dirty draft directories on the branch selector
Replaces the draft dirty-directory banner with an indicator on the branch
selector: a warning icon plus a hover tooltip that opens by itself for five
seconds when the dirty state first appears, then stays hover-only. The copy
states the situation and the options (commit or worktree) without prescribing
either.
* feat(ui): optional push in the dirty branch switch dialog
Commit-and-switch gains an opt-in "Push after commit" checkbox. When the
push fails the commit stands but the switch is cancelled with an explicit
toast, so the user is never moved off a branch without knowing its push did
not happen. Without the checkbox the toast states the commit is local only.
* fix(i18n): align dirty-directory copy across locales
* fix(a11y): name the unpushed-commit badge in the branch picker
The badge showed a bare arrow and number with no accessible name or tooltip.
Both the desktop recents list and the mobile picker now carry a localized
"N commits not pushed" title and aria-label.
* fix(mobile): push before switching dirty branches
Honor the dirty-switch dialog's push option on the mobile Changes surface.
A failed push leaves the new commit on its source branch, refreshes state, and
cancels checkout. Mobile branch selection now also shows the existing dirty
switch notice.
Keeps idle readers from snapping back to the end while rows re-wrap
Releases the pin instead of scrolling if a resize moves an idle view off the end
Re-asserts the live edge after resize settle for active streaming sessions
Switching sessions moved the conversation on screen before the swap: the
composer and the status chip followed the live selection and re-shaped a
commit ahead of the timeline, so the pinned outgoing chat jumped; and the
reveal effect re-ran for the outgoing session when its waited flag flipped,
hiding it a few frames before the next one mounted. The chat column now
reads one deferred session, and the reveal runs once per opened session.
Opening a file from the Git panel while a nested repository was selected
created the diff tab under the repository path, a key the context panel
never displays. Tabs are keyed by the project root; the diff surface
resolves the selected nested repository itself.
The work-status Project section now reads branch, changes, and PR from the
same resolved repository as the Git tab and names the nested folder under
the branch so the reader knows which repository the readouts describe.
* feat(linear): start sessions from Linear issues
Authorize a Linear workspace on this OpenChamber server, map teams to
projects, attach an issue from chat, start a session or worktree from an
issue, and post started/completed/failed comments that open the session.
Hidden in VS Code.
* feat(linear): connect more than one Linear workspace
Store each OAuth grant on this OpenChamber server and keep one current, so Settings can add and switch workspaces without dropping the others. Project mapping is per workspace. Remove the Linear button next to New Chat; start-from-issue stays on New Worktree.
* feat(linear): add a right-hand issues panel
Browse and filter issues in the rail, open a card to change status or start a session, and collapse search plus most filters to icons on a narrow panel.
* feat(linear): open issues in the rail and filter by Linear status
The rail icon only shows after Linear is connected. Clicking a Linear row on work status opens the panel. Status options match the card, including Done, Canceled, and Duplicate. The Integrations experimental warning sits under Third-party integrations.
* fix(linear): use stable OAuth callback broker
* fix(chat): preview Linear issue attachments
The context switch missed linear-issue, so tsc treated the preview helpers as incomplete.
* fix(ui): restore Linear i18n parity and the #2903 sync harness
Turkish was missing the Linear dictionaries, and the subagent test still wrapped only SyncContext after reads moved to SyncRuntimeContext.
* fix(linear): drop changelog hunks and close review races
Keep changelogs out of this PR, restore CodeMirror ranges, ignore stale Linear list pages, and leave a persisted Linear tab open until auth has actually resolved.
* fix(linear): tint active issue filters and clear them in one click
* fix(markdown): read escaped brackets as text, not display math
`\[...\]` is display math in LaTeX and an escaped bracket pair in
CommonMark. The block tokenizer claimed every `\[`, so prose like
`[title \[Bug\] more](url)` was handed to KaTeX: "Bug" rendered as a
centered formula and the block token split the paragraph, tearing the
link into three pieces. Linear, GitHub and any other source that escapes
brackets the way CommonMark requires hit this.
Display math now has to own its line — `\[` starts one and `\]` ends
one. A formula on its own line still renders; `\[` mid-sentence stays an
escape, which is what CommonMark says it is and what prose almost always
means. Inline `\(...\)` keeps the same ambiguity, but inline math is
legitimately mid-sentence, so there is no position to judge it by.
Covered by regression tests, including the verbatim comment body that
surfaced this.
* feat(linear): make session status comments opt-in and public-only
A status comment lands in a Linear workspace the whole team reads, and
the link it carried pointed at whatever origin started the session —
usually loopback or a LAN address. Everyone but its author got a dead
link, and nobody had agreed to the comments in the first place.
Comments are now off until the user turns them on in Settings ->
Integrations -> Linear, and the check lives on the server: the event hub
posts completed and failure without going through the interface, so a
client-side gate would not hold. When the resolved origin is not
publicly reachable the server posts nothing at all rather than a link
only its author can open; `isPublicSessionOrigin` rejects loopback,
private LAN, carrier-grade NAT, link-local and single-label hosts. The
desktop deep-link origin is gone with it, since no one else can follow
one either.
The comment body also dropped the session title. It repeated the issue
the comment already sits on, and issue titles routinely carry brackets
("[Bug] ...") that broke the markdown link. The body is now one short
link, and `sessionTitle` is gone from the route, client and types.
Also caps the dedupe file at the newest 500 sessions; it grew forever.
* fix(linear): match the pull request panel and clear review findings
Comments in the Linear panel now render as the same avatar timeline the
pull request panel uses, with the shared time-format preference instead
of a raw locale string. Comment authors carry `avatarUrl`, which the
GraphQL selection was not requesting.
Review findings from the same pass:
- `status-runtime.js` hand-rolled `typeof` narrowing and failed the
vendored anti-slop lint; it now parses through `parse.js` like every
other file in the module.
- `useLinearAuthStore` turned any failed request into `connected: false`
with `hasChecked: true`. Since the rail icon, the composer entry and
the worktree option all gate on `connected === true`, one network blip
hid Linear for the rest of the session, and Settings only re-checked
when it had never checked. It now keeps the last known status and
leaves `hasChecked` false so the next caller retries.
- `LinearIssuesView` (1096 lines) was a static import in `ContextPanel`,
shipping in the main bundle although its rail icon stays hidden until
a workspace is connected. It is lazy now, like `GitView`.
- Dropped dead code: the unused port helpers left over from the loopback
callback, two re-exported default values nothing read, and a redundant
export in `linkedIssues`.
- Integrations is no longer badged beta.
* test(ui): provide sync runtime context in issue-2903 harness
edfc9779c (perf(chat): make session switching feel instant) rewired
useDirectoryStore and friends from the system context to the runtime
context, but this harness only rendered SyncContext.Provider, so the
render phase threw 'useSyncRuntime must be used within <SyncProvider>'
and every PR run since failed this file.
Mirror SyncProvider's own nesting: render the runtime context (read
from its globalThis registry key) inside the system one, with a
currentDirectory source matching the new CurrentDirectorySource
contract. Also drop the chained globalThis type assertions in favor of
one documented cast.
Test-only change; no runtime behavior affected.
* fix(desktop): pair close-button hover with solid error red and its foreground
The classic window-control close button hovered with the
--status-error-background banner wash but colored the glyph with
--status-error-foreground, which each theme authors as the contrast
color for the solid error red (the --destructive pairing). On the wash
the glyph loses contrast in both modes - near-black on muted dark red
in dark themes, white on pale red in light themes - and the dark-mode
wash reads as a muddy saturated red.
Hover now uses the solid --status-error with its authored foreground,
matching the destructive button pairing and the Windows caption-button
convention.
* chore: re-run PR review bot (evidence added at HEAD)
* fix(header): remove right-edge gap before close button with custom window controls
The header root already applied pr-0 for frameless chrome with
right-side controls, but webWindowControlsOverlayStyle set an inline
padding-right on the same element, which overrides the class. In
Electron (frame: false, no titleBarOverlay) the WCO right inset is
always 0, so the close button sat 12px from the window edge and the
top-right corner did not trigger close.
Skip the inline style for the frameless + right case so the class
governs; the browser window-controls-overlay path keeps its padding
and inset reservation.
* fix(desktop): inset right-side traffic lights from the window edge
The header flush-edge fix (pr-0 for frameless + right controls) also
pulled the traffic-lights cluster against the window edge, but the
inset is a Windows-caption convention that only the classic style
follows. macOS-style circles keep their spacing: an explicit 12px
right margin on the right-side cluster, owned by the component so the
mini-chat window matches.
* chore: re-run PR review bot (body now documents the traffic-lights inset)
A turn that OpenCode stopped could end with nothing on screen: the
session.error event was only turned into a sidebar badge, its message was
dropped (the notification expected a different shape than OpenCode sends),
and a send that was accepted but never answered looked the same as success.
- The chat shows what OpenCode reported under the last message while that
turn is the latest one, and names a user message an idle session has left
unanswered for five seconds.
- The last 20 session errors are kept in memory and listed in the status
report (Ctrl/Cmd+Shift+L, also `__opencodeDebug.statusReport()`), next to
rejected sends, the managed OpenCode process's last error and stderr
tail, and the OpenCode and desktop log file locations.
- The OpenCode health probe hits /global/health instead of a route that
does not exist, and probe URLs resolve against the page for web runtimes.
A session opened from the sidebar could land above the bottom, or show a
frame sitting lower and then snap up. The viewport was pinned before the
content was final: the recap note renders once the session record arrives
and grew the footer under the pinned viewport, and on large sessions
subagent task cards grow when their child sessions load, moving everything
above the viewport.
- The recap note holds the timeline reveal until the session record is in
memory, so it is part of the first finished picture.
- The scroll hook holds the reveal until the viewport is pinned; the reveal
itself runs once the content height has held still for two frames, with
one exact pin against the final height (bounded at 300ms).
- Sitting on the end of a session that is not producing output is an
invariant: content growth re-pins from a MutationObserver in the same
frame the list writes its layout, so no frame paints with the end out of
view. Output growth keeps gliding through followEnd, which now glides only
while the session is working.
- ChatContainer swaps the timeline on a deferred copy of the selection so the
active row, URL, and tab commit first. (This part had been overwritten by a
concurrent edit before the previous commit and is restored here.)
- A session whose messages are not in memory keeps the previous conversation
on screen for up to 400ms while they load, instead of flashing a skeleton
between two conversations.
- The reveal fade runs only for a session the user waited for and is 100ms;
a session that was ready at the click appears in the same frame.
- The sidebar prefetches the two rows on either side of the open session
150ms after it settles, two at a time, so neighbouring switches are warm.
Commands and skills for the composer were read from the ambient store, which
follows the project selected in the sidebar; opening a managed chat or a chat
draft never changes that selection, so the previous project's commands,
skills, and (for drafts) files leaked into chats. The autocompletes now load
and read commands and skills for the effective directory, and a chat draft
resolves its effective directory to the prepared chat directory or the Chats
root instead of the last project.
Claude-Session: https://claude.ai/code/session_017TK5JAYDfT3Fotc23UEg98
Switching sessions ran as one synchronous commit: sidebar highlight, URL,
a full timeline remount with markdown re-parse, and around nine requests,
so nothing changed on screen for 150-250ms after the click.
- ChatContainer swaps the timeline on a deferred copy of the selection, so
the active row, URL, and tab commit first and the timeline renders behind
them; selection policy keeps reading the live store value.
- The message fetch starts before the selection is published.
- Sidebar rows stop re-rendering on a project switch: directory-scoped sync
hooks read the runtime context and a subscribable current-directory source
instead of the directory-bearing context; the grouping builder reads git
branches through a ref and section caches key the branches they use;
descendant ids are keyed by content. Rows per switch went from 73 to 8.
- Markdown skips the async re-render when the settled cached blocks are
already painted, and mounts synchronously once its lazy module is loaded;
the module is preloaded at boot.
- A timeline reveal gate holds a freshly opened session at opacity 0 while
any provisional markdown paint catches up (250ms cap), then fades the whole
timeline in once, so text, tools, and recap appear together.
- Switch fan-out trimmed: knowledge summary deduped, MCP status refreshed only
when stale, non-repo directories cached by the git repo check, OpenChamber
defaults cached briefly, agent memory reused for the same project, goal
text cached, PWA manifest rebuilt after the switch settles.
- Header tabs snap into the active state and keep the title at the same
height in both states.
- Prefetch on row press; composer focus moved off the commit.
`bun run profile:switch` records ack/content latency, longest task, and
requests per switch, cold and warm, and compares runs against a baseline.
Measured warm switch: ack 228ms to about 40-60ms, content 228ms to about
100-120ms.
Context parts are marked synthetic, so a message made only of quoted
fragments was skipped as an injected message and could not be navigated
to. Such turns now appear with the same caption the bubble shows.
Opening or closing the context panel re-wrapped the timeline; the settle
handler then scrolled to LegendList's total content length, which still
carried pre-wrap sizes for unmounted rows, leaving a viewport-sized blank
tail under the last message.
Post-merge follow-ups for #2740#2735#2734#2690#2676#2738#2684#2689#2733#2739#2462#2687#2736#2618#2697, plus three regressions
found while reviewing them:
- ctrl/cmd+digit while typing no longer switches session tabs (#2503 was
still open in practice: the guard only covered the mod+alt surface binding)
- Shiki template-call sanitizer now covers every bundled grammar, including
the js/ts aliases and embedding grammars; timed-out highlight requests are
memoized and no longer cancel unrelated in-flight requests
- settings flush on suspend uses keepalive and also fires on Capacitor
appStateChange; keeps the selected model persisted across mode switches
- remote-only branches fetch before checkout; range helpers fail clearly
- git status invalidation now fires for runtime adapters too
- settings number inputs and select triggers size in ch so they scale with
the interface font
- recent-activity timestamps tick from one list-level ticker
- Markdown preview find goes through the shared find_in_file keybind with
containment, no longer counts its own bar, and debounces observer runs
- #2676 reverted; #2524 fixed by fading the sticky header's own background
instead of overlaying the content below it
- sticky group headers in the model picker and sidebar render again
(oc-sticky-fade-scroller class restored after 9b9d7069c)
- project switcher names are left-aligned again (wrapper lost in 26dbc2f30)
- tool card quick-open icon is always visible and opens the same line as the
expanded card's button
- tautological tests replaced or removed; new oxlint findings fixed
Adds the Turkish dictionary entries main introduced after this branch was
opened, keeps the composer's four-space style in the new paste modules,
and drops the unused offer-action type plus the widening return
annotation flagged by the anti-slop lint.
Shiki's bundled JS/TS/JSX/TSX grammars carry a `template-call` rule whose
triple-nested lookahead sends the Oniguruma WASM engine into exponential
backtracking on ordinary backtick templates. The WASM heap grows until the
renderer runs out of memory and the window goes black.
Strip `template-call` from those four grammars as they are loaded in the
Shiki worker. Plain backticks and simple tagged templates still highlight;
only the rare `ident<TypeArgs>` tagged-template form loses its specialized
type-argument coloring.
Add a 5s per-request budget on the worker client as a safety net for any
other pathological pattern. Matching is synchronous inside the worker, so
the only way to reclaim its heap is to terminate it from the main thread.
A timed-out request resolves `null` like any other failure, so the caller
keeps plain text and nothing is written to the result cache.
Closes#2587
Reasoning streaming state now derives only from the live stream phase
(streaming/cooldown), never from missing persisted timing data. A cached
part without time.end is no longer treated as live just because the
timing field is absent, so completed reasoning renders in full on load
instead of replaying a fake stream.
While resolving the merge onto main, also fixed a regression the merge
uncovered: main had added block-level streaming reveal (commitStreamedText)
to ReasoningPart since this fix was authored, which caused the busy
"Thinking…" header to stay hidden for the first moments of a short,
single-paragraph streaming response (no committed line yet). The busy
header now mounts as soon as streaming starts, independent of whether
any text has been committed for display.
Closes#2020
Selecting text that crosses the top scroll edge could push the
floating selection menu above the viewport because only the X
position was clamped, not Y. Add getDesktopClampedY alongside the
existing X clamp in selectionMenuPosition.ts, measure the menu's
height the same way its width is measured, and re-clamp Y in
showMenu, the layout effect, and the resize handler.
Ported by hand: TextSelectionMenu.tsx changed significantly on main
(comment mode, glass surfaces, header drag-zone push-down) since this
fix was written, so main's file was kept and the fix re-applied on
top of it.
Closes#2257
Merge main and reshape the follow opt-out to the gestures the timeline was
missing: a middle-button press starts the platform autoscroll pan (the only
scroll gesture on wheel-less mice and tablets with a pointer), Shift+Space
scrolls up from the keyboard, and an upward wheel over a nested scroller that
still has room above stays with that scroller instead of releasing the chat.
The grace re-pin timer and scroll-direction tracking are dropped: returning to
within the end band already re-arms follow, and the mode machine is built
without timers on purpose. Pause/Break never move the viewport and are not
gestures.
Closes#1640
CodeMirror collapses a CRLF pair into one line break, so the document is
shorter than the string it was given. The composer derived the caret from
the JS string length, which put it past the end of the document and made
dispatch throw `RangeError: Selection points outside of document`.
Because the exception fires before the transaction applies, the document
never updates, the un-normalized text stays in React state, and the draft
persists as-is: every later visit to the session restores it and crashes
again, with no way out from the UI.
Derive the caret from the change set instead, in the controlled writeback
and in the imperative insert/replace handles.
fixes#3013
# Conflicts:
# CHANGELOG.md
# packages/vscode/CHANGELOG.md
Follow-ups promised on merge, plus review findings on the batch itself:
- chat: task-tool output now respects the 512KiB render cap; quick-open
icon is visible at rest on coarse pointers and reachable by keyboard
(row keydown no longer swallows inner-button Enter/Space); composer
inline-code decoration drops the metric-shifting padding; a btw fork
send carries only the boundary instruction, never the promotion notice
- sync: cascade revert/unrevert aborts busy descendants, busy state is
read from every child store at the moment of use; rule 9 documents
redo clearing all descendant revert markers
- electron: renderer recovery keeps memory-eviction (a valid
render-process-gone reason) and both windows share one
attachRendererRecovery helper
- vscode: process registry is a thin re-export of the web module
(provider-env-aliases precedent) with ordered register/unregister
writes and an awaited close
- server/cli: managed-process registry takes injectable deps (fixes the
unreaped-orphans ReferenceError), corrupt settings errors name the
file, getWorktrees test restores console.warn
- tests: module-mock harnesses removed (AgentsSidebar, SettingsView
mobile focus — behaviors stay live but uncovered, accepted trade),
QuestionMarkdown asserts rendered DOM
- i18n: German gains the debug-panel request keys, Japanese/German drop
removed worktree keys, Ukrainian unit spacing fixed
- changelog: Copilot AI Credits entries (main + VS Code)