The IME fix covered the diff/file comment input and the browser annotation
overlay, but two other places where a user writes a comment still acted on
the Enter that confirms an IME candidate: the chat quote comment in the text
selection menu, and the in-place comment editor on a composer context chip.
Confirming a candidate there attached or committed the half-typed reading and
closed the input. Escape, which abandons a candidate, had the same problem.
Both handlers now return early on a composing keystroke, and a single test
asserts the guard across every comment input so the next one added does not
quietly skip it.
* feat(chat): render code comments as cards instead of fenced text
* fix(vscode): route Add Comment to the active session editor panel
* fix(chat): persist queued inline comments and tighten file-chip path matching
* feat(vscode): comment on code from the editor
* fix(chat): keep attached context in the message and broadcast comment removal
* fix(vscode): hold every pending comment and gate both entry points on the workspace
* fix(vscode): let only the owning surface decide its comment threads
* fix(vscode): drop a comment removed while its delivery was still in flight
* test(vscode): cover the in-flight comment removal guard
* test(vscode): cover comment removal reaching every chat surface
* fix(vscode): give up on a comment the chat never confirmed holding
* fix(vscode): retract a comment everywhere before reporting it discarded
* fix(chat): preserve queued comment cards
* fix: preserve inline comment context across send paths
* fix(chat): preserve command routing with context
* fix(chat): keep unavailable actions on normal send path
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
Thanks for extending the existing IME handling to comment inputs and documenting the browser-event checks. We will also protect the annotation Escape handlers from cancelling composition.
Thanks for fixing the cramped table columns and covering the streaming-to-settled transition. The content-sized layout and horizontal scrolling look ready to merge.
Thanks for refining the action placement. This keeps New chat available while idle and preserves Abort while a response is running. We will finish the repository-specific cleanup in the same batch.
Settings → Integrations offered install cards for the Claude Code and
Cursor provider plugins. They are gone: the section, its plugin catalog,
its own i18n module and tests, the settings search entries, the page
keywords, and the two sprite icons only it used. The page now holds the
built-in GitHub and Linear cards, so it is hidden in VS Code where neither
applies; its title and description live in the settings dictionaries.
Docs follow: the Integrations page in every locale now documents GitHub
(pointing at its own page) and Linear in full, the GitHub page names
Settings → Integrations as the place to connect and covers linking an
issue or PR to a message, and the Providers pages no longer promise Claude
or Cursor subscriptions.
Claude-Session: https://claude.ai/code/session_01HB9wdLQoZX2vfyDjwv6Rso
Queueing captured only the text and files. Context chips (inline comments,
terminal selections, browser annotations, PR comments and checks, quotes,
linked issue/PR/Linear references, pending synthetic parts) stayed in the
composer and only left with the next manual send, so a queued message the
server delivered went out without them and the chips rode an unrelated
message later.
A queued message now carries what the composer would have sent: the text
with its agent mention stripped and file mentions resolved into
attachments, the attached context as structured parts, and the skill
instruction derived from the text. The server delivers those parts in the
composer's order, the VS Code auto-send does the same, and editing a queued
message puts the chips and linked references back. A failed queue restores
the composer completely. Snapshots and broadcasts omit the captured
context like attachment payloads; a take returns it.
Claude-Session: https://claude.ai/code/session_01HB9wdLQoZX2vfyDjwv6Rso
The send write was fixed, but the message that send echoes back arrives with
model metadata and no effort, and its model matches the one the send just
saved. That is exactly when `shouldPreserveManualModelOverride` declines to
protect the selection, so the history branch ran with no variant and recorded
"no choice" over the user's explicit `Default`. Picking Default, sending, then
switching agent and back still put the settings default in the picker.
A message carrying no effort is not evidence that the session has none. The
history branch now keeps whatever the session already recorded when the message
carries no variant, and a concrete historical effort still replaces it.
The behavior test ran the real guard through a mock that returned a fixed
answer, so the failing branch had no coverage. It now calls the real function
unless a test opts out, and the new case fails without this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZuVVgziiLjD81W5vaxdH2
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
Messages queued while a session is busy used to live in the browser tab and
were sent by that tab once the session went idle, so closing the tab (or
losing the connection) stranded them. The web server now owns the queue:
it persists to <data-dir>/message-queue.json, watches session.status on the
global event hub, re-verifies idleness against OpenCode before sending, and
delivers the head of the queue via prompt_async (or /command for slash
commands) with the model, agent, variant, attachments, and agent mention
captured at queue time. Failed sends stay queued and retry with backoff; a
user abort holds delivery briefly; every change is broadcast so all clients
see one queue.
The shared UI store becomes a projection of the server queue outside VS
Code (hydrate on connect, apply broadcasts, optimistic mutations settled on
the server's copy, one-time upload of locally queued messages from older
builds). Edit / send-now take the full message back from the server. A
UI-driven auto-review run asks the server to hold that session's queue.
VS Code keeps its local queue and foreground auto-send.
Claude-Session: https://claude.ai/code/session_01HB9wdLQoZX2vfyDjwv6Rso
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 reset-on-open effect also depended on generateUniqueSlug, which is
derived from the available-worktree list, so a worktree appearing or
disappearing while the dialog was open wiped the form (branch name,
linked issue). Initialize once per open via a ref guard, pinned by a
behavior test. Also tightens CreateWorktreeArgs/GitHub selection typing
in place of type assertions.
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.
* refactor(worktrees): fetch source once during creation
* fix(worktrees): remove worktrees in background
* fix(worktrees): show background removal progress
* fix(worktrees): name the worktree in removal toasts
* feat(worktrees): fetch remote source branch before worktree creation
New worktrees based on a local branch that is behind its upstream now
fetch first and branch from the remote-tracking ref, so they are not
born stale. A global setting (on by default) in Settings > Behavior
controls this, and fetch failures toast a warning and fall back to
local state instead of blocking creation.
* fix(worktrees): wire fetch-source toggle to store and honor failed runtime fetches
The Behavior toggle only persisted the setting; the consumer reads the
config store at creation time, so a just-toggled-off setting kept
fetching until the next hydration. Update the store optimistically on
toggle and on page load, and roll it back when the save fails.
The VS Code runtime bridge resolves git fetches with { success: false }
instead of throwing, which the consumer read as success and silently
based the worktree on the stale remote ref. Treat any non-success
result as a failed fetch: warn and fall back to local state, matching
the web/desktop/mobile path.
* fix(worktrees): stop new remote-based worktrees from tracking the base branch
Creating a worktree with a remote start ref made git auto-track the
base branch (branch.autoSetupMerge), so with the new remote fetch every
behind-root worktree was born with upstream origin/<base> and plain
git push refused under push.default=simple.
The new branch's own upstream does not exist until its first push, and
the bootstrap deliberately refuses to write tracking config for refs
that were never fetched, so --set-upstream-to cannot re-point it.
Suppress the auto-track with --no-track on new-mode creation from a
remote ref: the branch ships with no upstream, matching the behavior
before the remote fetch until the first push sets it. Explicit
upstream keys now also win over the remote start ref inference,
aligning the create path with the validate path and the VS Code
runtime.
* fix(worktrees): keep the pre-create remote ref refresh soft
The client fetch and the server's pre-create fetchRemoteBranchRef both
refresh the same branch, and the second fetch throws on failure — so a
connection dropped between the two turned the promised soft fallback
into a rejected creation even though the remote-tracking ref was
already available locally.
The refresh is now best-effort when the ref exists locally (creation
proceeds from it) and still mandatory when the ref was never fetched,
preserving the materialization behavior for remote-only branches.
Applied to both the web server and the VS Code runtime.
* chore: ignore the .openchamber app runtime state directory
The switcher held reachability in component state and replaced the whole map at
the end of a probe run. It ran once per open before the config had loaded — with
Local as the only host — so that pass wiped every other instance's status and
each open started on "Checking", including for the instance the app was
connected to and actively talking to.
Statuses move to their own module: startup warms them so the switcher opens on
real values, a re-probe replaces each value in place as it lands rather than
blanking them first, and stale entries are dropped against the loaded config
instead of a partial host list. The connected instance never reads "Checking" —
the live connection already answers what the probe would ask.
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.
Prevents the project label area from shifting when actions appear
Matches spacing behavior with the collapse-toggle branch
Keeps hover and focus states aligned
Every updateDesktopSettings PUT replays the server's full settings
document as an openchamber:settings-synced event, and the theme listener
adopted it unconditionally - so switching sessions across directories
(any lastDirectory/activeProjectId write) could flip the theme to
whatever the server document held at that moment. A bootstrap GET with
missing theme fields made it worse: materializeAuthoritativeUiSettings
invented useSystemTheme + openchamber defaults and the persist effect
wrote them back to the server and the scoped localStorage entry.
Theme is now adopted only from bootstrap-grade syncs (the renamed
bootstrap flag, formerly adoptWorkspace), missing fields mean 'not set'
and keep the current preference, and the materializer no longer invents
theme defaults. Cross-window same-instance theme sync still rides the
scoped-key storage event; runtime endpoint switches still adopt the new
instance's server theme.
Closes the 'theme flashes to OpenChamber when switching sessions' report;
same family as the pre-#2897 'color mode forced to light' symptom.
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
Adds a shared sidebar tooltip provider with delayed open and instant close behavior
Removes per-button tooltip delays so sidebar actions feel more consistent
Aligns session sidebar hover interactions with the opencode-style tooltip experience
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.
The pull-request rail icon now appears only while GitHub is connected
(OAuth or gh CLI), like Linear; Linear sits after the walkthrough in the
default rail order. The GitHub account avatar and switcher leave the
header for the pull-request panel, where the walkthrough, refresh, and
account controls share one row and one height, and the account stays
visible on the panel's empty state. A manual refresh keeps its spinner on
screen long enough to read as work done.
GitHub sign-in moves from Settings → Git to Settings → Integrations →
Built-in integrations as a card before Linear; search and the connect
buttons follow it.
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.
Text-to-speech picked one voice regardless of what language a reply was in.
A dependency-free language detector (script, marker letters, function words)
now decides the language of the whole message once; with the new
"Match the voice to the language of the text" setting the local provider
switches to a catalog model for that language (Kokoro zh/en and Piper models
for 12 languages, downloaded on first use like the existing model) and macOS
say switches to an installed voice whose locale matches. The local voice
picker lists voices of every installed model, and the settings show which
language models are on disk.
The Ukrainian Piper medium build is a character-level model that sherpa-onnx
turns into noise, so the espeak-based Lada build is used instead.
Claude-Session: https://claude.ai/code/session_017TK5JAYDfT3Fotc23UEg98