Adds a new info-styled button variant for consistent secondary actions.
Updates mobile sessions sheet actions to use the shared Button component.
Cleans up the mobile footer buttons with the shared sizing and styling system.
Review found the owning documentation still describing the behaviour this
branch replaced, in one case stacked directly above the new docstring saying
the opposite. Holding a session proves containment, not ownership, so every
text that called store membership the authoritative mapping was actively
misleading for the module whose wrong answer misroutes every send.
Corrected in the module docstring, the resolution module's precedence
description, the sync-refs helper it points at, and the sync DOCUMENTATION.md
table and rules.
The debug report built its authoritative value membership-first, so for exactly
the scenario this branch fixes it reported the parent directory and could raise
a source-disagreement alert while routing was in fact correct. It now uses the
same record-first order as the resolver.
The CLI timeout comment claimed the wait and provisioning windows were additive
while the code took the larger of the two. The server provisions the worktree
inside session creation, before it waits for the session to go idle, so they do
run in sequence: the windows are now summed and the tests pin both cases.
Softens the dark surface and syntax background tones
Updates foreground, selection, and cursor contrast
Keeps markdown, chat, and tool text aligned with the new palette
Creating a session with a worktree reported "Request to /api/openchamber/control
timed out after 4000ms" while the worktree was in fact created, leaving the user
with a failure message, a real worktree, and no session id. Reported alongside
worktree creation appearing to take forever.
The client HTTP timeout was extended only when the caller asked to wait for the
session. Provisioning a worktree is slow on its own: it runs git against the
repository and prepares a new directory. Measured on a cold path immediately
after a restart it takes about four seconds, which lands exactly on the four
second default and explains why this failed intermittently rather than always.
A warm run finishes in well under two.
The timeout now follows the work being requested rather than only the wait
flag, and covers whichever of the two windows is longer. The server always
completed the operation, so nothing about the outcome changes: only the client
stops abandoning it.
Verified by creating a worktree on the cold path immediately after a restart,
which previously failed here: 4004 ms and 1376 ms, both reported ok.
A session created in a git worktree while the client was already running did
not render: the message list stayed empty while the prompt and the assistant
reply were both present in the session, visible on any fresh load. Reported as
prompting in a worktree sometimes not working.
Ownership was read from which child store holds the session. That is
containment, not ownership. A project's session list includes the sessions of
its worktrees so the sidebar can group them, so the parent repository holds
worktree sessions too, and whichever store bootstrapped first won. Captured
mid-failure, the two signals disagreed outright:
owningDirectory /repo <- parent, merely holds it
recordDirectory /repo/.worktrees/feature <- the session's own directory
The parent won, so every fetch was addressed to a directory that does not own
the session, the session id resolved to undefined there, and the requests
failed as /api/session/undefined in a retry loop. The session's own record is
now believed; store membership remains the fallback for a record that carries
no directory.
This also explains why the previous commit alone was not enough: settling the
guessed directory adopted this same wrong value and then cleared the guess,
which prevented any later correction.
Verified against the reproduction rather than by reasoning. Before: three of
four runs never rendered. After, on a clean build with the instrumentation
removed: three of three rendered the reply live, each routed to its own
worktree. Tests cover ownership disagreeing with containment, plus both
directions of the guess promotion.
Adds an Electron install check that repairs incomplete or wrong-architecture binaries
Runs the check during root postinstall and before electron dev startup
Adds tests and docs for the new ensure:electron workflow
Selecting a session whose directory this client has not indexed yet routes it
through the active directory. That is a deliberate, documented guess: it keeps
routing usable while the owning store bootstraps, and it is excluded from both
the resolver and persistence.
Nothing settled the guess afterwards. `setSessionDirectory` performs exactly
that promotion, but only confirmed destinations call it — a completed move or a
worktree this client created. A session whose directory the client learned about
later, such as one in a worktree created outside this client, kept the guess
forever: every message fetch was addressed to the parent repository, which does
not own the session.
Captured for such a session before this change, with the session already
indexed and its owning store known:
routedDirectory .../worktree/feature
currentSessionDirectory /repo <- guess, never settled
opencodeClientDirectory /repo
conflict selected -> /repo
and after:
routedDirectory .../worktree/feature
currentSessionDirectory .../worktree/feature
opencodeClientDirectory .../worktree/feature
conflict null
Directory bootstrap completion is the moment the authoritative directory first
becomes readable, so the promotion runs there. It only ever promotes a guess:
a confirmed selection and a selection that has since moved on are both left
alone, and tests cover both directions.
This removes a real routing split-brain. It does not by itself fix the reported
symptom of a session created mid-session never rendering; that remains open.
The SQLite write into OpenCode's database was the only consumer of
better-sqlite3 in the repository. Everything that existed to ship its native
binary went with it:
- the dependency in @openchamber/web and @openchamber/electron
- the afterPack hook staging better_sqlite3.node into app.asar.unpacked
- a dedicated @electron/rebuild pass (onlyModules) and its binary assertion,
so desktop packaging now runs one native rebuild instead of two
- the bundler external entry and the AppImage required-native-module check
Desktop packaging, the AppImage verification tests, and the extension bundle
were re-validated after a clean reinstall, so no stale module could satisfy a
missed import.
Creating a worktree wrote the new directory straight into OpenCode's own
project storage: the web server updated `storage/project/<id>.json` and ran an
`UPDATE project SET sandboxes` against `opencode.db` through better-sqlite3,
and the VS Code extension wrote the same JSON.
Both wrote behind the back of a running OpenCode process. OpenCode registers a
sandbox through `project.addSandbox`, which emits a project-updated event; a
direct row write emits nothing, so a worktree created while OpenCode was
running stayed unknown to it until a restart. The SQLite write also opened a
database file owned by another live process. The VS Code write was inert on top
of that: OpenCode v2 reads sandboxes from the database, not from that JSON.
Registration is not ours to perform. OpenCode records a worktree as a sandbox
itself when an instance boots for that directory, and filters entries whose
directory no longer exists when reading them back, so removal needs no
counterpart either. The only consumer on our side, the project seed in
sync/bootstrap.ts, already falls back to `project.current()` when the seed is
absent; the worktree list itself comes from git, not from sandboxes.
Reported symptom this targets: a worktree created after `openchamber restart`
never answers prompts, and restarting OpenChamber makes it work. Not reproduced
locally, so this is not confirmed as the cause.
A queued message is removed from the queue only after its send resolves,
so between dispatch and resolution it stays visible to every reader — and
a composer submit merges the whole queue into its own send. Over a relay
that window is seconds, long enough to deliver the same message twice.
The queue now tracks which entries are awaiting the server. Dispatchers
skip them, clearQueue retains them so the pending send can still remove
or restore its own entry, and the flag is not persisted because a restart
has no in-flight sends.
A prompt whose response is lost after the request left the client may
already be running server-side. The relay tunnel reported those failures
as plain text errors ("stream aborted by host", "relay keepalive
timeout"), which matched none of the patterns in isAmbiguousSendFailure,
so an accepted prompt was rolled back and the message queue re-sent it —
two independent AI responses for one user message (#2425). Direct
connections never hit the path.
Transports now tag dispatched-but-unconfirmed failures and the classifier
reads the tag before falling back to status/text heuristics. Confirmation
waits for the connection to actually return (bounded) and retries with
backoff instead of two attempts 150ms apart over the just-broken tunnel.
Reveal-in-file-manager was always offered whenever the server exposed
revealPath, including in a plain browser tab where there is no local
file manager to reveal into. Gate it behind a new isBrowserClientRuntime
check (web platform, no Electron shell) and relabel the save action to
"Download" for that case, since it triggers a browser-style file
download rather than an in-place save.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expanding a project with more worktrees and sessions than MAX_DIR_STORES put
the sidebar into an endless request loop (#1472).
Every sidebar row calls ensureChild during render, but the pin that protects
the directory is only taken in an effect after commit. ensureChild marked the
directory and ran eviction synchronously, so directories that were actively
rendering looked unpinned and were disposed. The next render recreated them
with a loading status, which issued another bootstrap request, and the cycle
repeated for as long as the project stayed expanded.
Raising the limit only moves the cliff, so the limit is now a soft target
instead: a directory touched within a grace window is never an overflow
victim. A burst of live directories overflows the cache briefly rather than
thrashing, while idle-time eviction still bounds it. Eviction is also coalesced
into one deferred pass per tick, so a render that mounts many rows no longer
sorts and scans every directory once per row, and a whole commit's pin effects
settle before anything is considered for disposal. Releasing the final consumer
stays synchronous, since that is an explicit lifecycle edge.
The idle profiler gains --expand-projects to reach this state.
Not yet verified end to end: reproducing the loop needs many worktrees under
one project, which this development environment does not have.
`getRuntimeKey` keys caches, stores, and persisted state across the whole UI,
so it runs on store reads, event handling, and render paths. Until the runtime
endpoint is explicitly initialised, every call re-derived the key by trimming
two injected globals and constructing three URL objects.
In a streaming capture this made `readInjectedLocalOrigin` the single most
expensive application function: 315 ms of self time, 12% of all main-thread
busy time. After the change it does not appear in the profile at all, and the
same capture went from two long tasks to none, with the longest task dropping
from 210 ms to 47 ms.
The key depends only on the active API base URL and two injected globals, and
`switchRuntimeEndpoint` writes the injected API base URL at runtime, so the
cache is validated against the raw untrimmed values rather than memoised
outright. That comparison allocates nothing and still recomputes as soon as any
input changes. Tests cover both directions, including an operation-count
assertion that repeated calls construct no URLs.
The streaming profiler also reports output-normalised metrics, because response
length varies between runs and makes per-second totals incomparable.
Use getRequestDirectory and x-opencode-directory like the other skill
mutations, and pin renamable list/store mapping with focused tests.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
German was added to the interface but not to the walkthrough's own language
list, and nothing failed: the picker offered Deutsch because it is built from
the interface locales, the server resolved the tag to English, and a German
reader paid for a walkthrough written in English while the picker still said
Deutsch.
The two lists cannot be one — the server cannot import from packages/ui — so
a test reads i18n/runtime.ts and compares them, in both directions and through
normalizeLanguage. Drift this quiet needs a test rather than vigilance.
Keep the active todo text hidden below 38rem, as before, while hiding only the changed-files secondary label below 30rem. This fixes the mobile collision without reintroducing the documented narrow desktop overlap.
Keep ARGV0/env-u assertions from this branch and the DA startup-reply
expectations from main's terminal PTY-before-viewport fix.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Session directory resolution had no precedence contract: the selection-time
directory short-circuited every lookup, and a persisted runtime value was
consulted before the authoritative record. A worktree session selected before
its directory store bootstrapped kept the active-directory fallback, and that
guess was persisted, so it survived reloads and restarts.
Directory resolution now lives in one module and orders sources by whether the
server confirmed the path, not by whether the value is local or synced:
authoritative (the child store that holds the session) > server-confirmed
selection > worktree attachment/metadata (the requested path, pre-canonical) >
remembered. A guessed selection is no longer persisted, remembered, or ranked.
Chips read the same resolution the composer used, so queue keys cannot diverge.
Queued auto-send could strand an item indefinitely: backoff, missing send
configuration, and the recent-abort window all returned without scheduling a
wake-up, so the queue only retried when an unrelated status or directory change
re-ran the effect. A retry scheduler now wakes it at the earliest known time.
A rejected send rolls the optimistic message back while the composer stays
silent for transport failures, which makes it indistinguishable from nothing
happening. Failures are now recorded to a bounded in-memory log surfaced in the
About diagnostics report, alongside a directory-resolution breakdown, plus
__opencodeDebug.diagnoseSessionDirectory() and getRecentSendFailures().
Prompted by a report of worktree prompting silently failing. That failure was
not reproduced locally, so the diagnostics are what will identify it.
Terminal creation no longer waits for the Ghostty viewport to report its
size: it starts the PTY immediately with a container/font-derived
provisional size (falling back to 80x24), then resizes once the real
viewport dimensions are known, with a dedupe guard while sizing settles.
Starting the shell earlier means it can emit device/theme queries before
a browser terminal is attached to answer them, so the server now answers
primary device attribute queries itself (Fish blocks ~10s on this at
startup) and bun-pty buffers output emitted before a data subscriber
attaches. Also fixes a few WebSocket transport reconnect races surfaced
by session creation now overlapping renderer setup.
Expose authoritative renamable on skill list responses using the same
managed-root policy as renameSkill, drop the divergent UI path heuristic,
and remove an unused rejection-test fixture.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
bun-pty merges the OS environ into PTY children, so deleting ARGV0 from the
JS env object alone left the AppImage path in the shell. Wrap Linux PTY
spawns with env -u ARGV0, clear native ARGV0 under Bun via libc unsetenv,
and always clear process.env even when no login-shell snapshot exists.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Merge of main brought the de locale without the custom-provider keys
added on this branch, which broke the UI build type cast to I18nDictionary.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
AppImage exports ARGV0 into the process environment. zsh treats that as
argv[0] for every external command, which broke Python venv detection in
the integrated terminal and managed OpenCode sessions.
Clear ARGV0 in Electron before login-shell probing, refuse to re-apply it
from shell snapshots, and strip it from terminal PTY and managed OpenCode
launch environments.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Restrict in-place skill rename to managed skill directories, require
frontmatter name to match before moving, roll back/reject with tests,
hide rename in the UI for unmanaged paths, and drop unused toast keys.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
On narrow mobile widths the chat status row rendered the edited-files
summary, diff totals, todo trigger, and chevron in a single flex line.
The left side has several fixed-width parts (file count, +/- counts,
chevron) so it could not shrink past the right side and the right-side
controls visually overlapped the truncated 'changed in workspace' label.
Fold the existing 24rem hide rule for status-row__changed-label into the
same 30rem container query that already hides status-row__active-todo,
and lower the breakpoint from 38rem to 30rem so the secondary label
remains visible on a wider range of mobile widths while the two
non-collapsible sides stop competing for space.
Validation:
- bun run type-check (all workspaces, exit 0)
- bun run lint (all workspaces, exit 0)
- mobile HMR route (HTTP 200)
- visual: before/after screenshots at ~360-400px width
performConfigRefresh passed client-directory-first path into
invalidateSkillsLoadCache, missing the active-project cache key used by
loadSkills after the repository-local skills discovery fix.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Derive the effective OpenCode config layer (custom > project > user) from
provider sources and send it on PUT /api/provider so project/custom edits
update that layer instead of creating a global user override. Resolve
OPENCODE_CONFIG at call time and add UI/web/VS Code coverage for scoped
upserts.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>