OpenCode's authorize response reports how the client must finish: `code`
expects a pasted code, while `auto` requires the client to call
oauth/callback immediately and hold it open — upstream blocks in there
polling for the device code or waiting on its loopback redirect, and only
that call persists the credential. Every auth plugin OpenCode ships uses
`auto`; none use `code`.
The page implemented only `code`. It opened the browser, showed a paste
field no provider can fill, and never called back, so a successful sign-in
stored nothing and the app sat unchanged. Authorization now drives the UI:
`auto` chains straight into the callback behind a waiting state with a
cancel, and the paste field appears only when a provider actually asks
for a code.
Two smaller failures shared that surface. Prompts were never collected,
which put GitHub Copilot Enterprise out of reach entirely, so a method
that declares them now asks first and passes the answers to authorize.
Device codes are also recovered from the instructions text, where they
actually live — the old code read fields the API does not return, so the
copy button never appeared.
The callback is exempt from the ordinary proxy deadline and gets a
15-minute budget, bounded by the shortest upstream expiry we know of.
A human sign-in with 2FA does not fit in four minutes, and expiring it
turned a completed login into a 504.
A server without these routes does not answer 404 with JSON. The unmatched
/api path reaches the OpenCode proxy, and OpenCode serves its embedded web UI
for anything it does not recognise — HTML, status 200 — so a client newer than
its server parsed a web page as JSON and put "Unexpected token '<', "<!doctype"
in the panel, naming neither the cause nor the remedy.
The client now checks the content type before parsing. A non-JSON answer on 2xx
or 404 blocks with "this server is older than the app, update it and refresh".
A non-JSON 5xx keeps its own failure: a server that answered badly is not a
server missing the feature, and sending that user to upgrade chases the wrong
thing.
Do not present a provider without a login as the selected walkthrough
model, and grey out Generate when readiness is false instead of showing
a login-error blocker or raw auth banner.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
When the walkthrough small model resolves to a provider with no usable
login, readiness was still ready and generate returned a raw 500 message.
Refuse up front with no-provider-login and surface a blocker instead.
Closesopenchamber/openchamber#2607
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
`prompt_async` answers 204 as soon as OpenCode forks the run and reports
every later failure only on the session event stream, so an unusable
model, agent, or variant produced a session with no message while the
result still claimed `promptDispatched: true`.
Validate an explicitly requested model, agent, and variant against the
directory's own agent and provider lists before any session, worktree, or
goal is created, and confirm a new user message actually reached the
session before reporting the dispatch. A failed or empty lookup never
turns a valid selection into a rejection.
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.
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.
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.
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 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>
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>
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>
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>
Rename skills by moving the skill directory and updating frontmatter
name instead of recreate-with-stub-description, which wiped the body
and supporting files.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
A guided explanation is only useful in a language the reader reads, so the
panel header gets a language picker alongside the model one, defaulting to
the interface language. Like the model, it is request state rather than a
setting: the language travels with the read and the generation, and the one
a walkthrough was written in is stored with it, so reopening a review
describes what is there instead of what a fresh one would be.
Only prose is translated. Hunk aliases resolve back to hunk ids and
icon/importance are validated against fixed English values, so a translated
one would be dropped by the normalizer — silently losing an anchor or a
style. Identifiers and paths stay as they appear in the code.
The language is part of the cache key, and a read now asks the cache for the
exact request it was given before falling back to the pointer. Without that
the panel answered a request to switch languages with the text it already
had, leaving the other language unused in the cache.
Alongside it:
- The answer budget is derived from the resolved model instead of a flat 24k.
That number was the same for a 64k-context model and for one that admits to
384k output tokens, and on the latter it was the only reason generation
failed: the model spent the whole allowance reasoning and returned nothing.
It is now min(96k, max(24k, a quarter of the context)) capped by the
catalog's output limit, decided once so the input reserve and the request
cannot drift apart.
- A read no longer offers Cancel. It is a few hundred milliseconds of git with
nothing to cancel, and the button flickered on every model or language
change. When the panel is showing a fallback, a banner names what is on
screen versus what was asked for — only once the read has settled.
- The header keeps one 32px control height and drops its labels below 680px
instead of squeezing them to two letters and an ellipsis.
Docs and module documentation updated in every locale.
A diff is ordered by file path, which is almost never the order in which a
change makes sense. This adds a Walkthrough surface that reorders it: the model
groups related hunks into stops, explains what each group changes about
behavior, and orders the stops so each builds on the last. It explains and
orders; judging code stays with the existing Review action.
Reviews uncommitted work (all, staged, unstaged), a branch against its base, or
a pull request. Generation is always user-initiated — nothing runs on a timer,
on a file change, or as a side effect of opening a panel.
Invariants worth preserving:
- Hunk identity is derived on the server and only there. Ids are content
hashes, so an anchor that no longer resolves is proof the code it described
changed, and staleness needs no heuristics. The client matches ids to ids and
never recomputes them; two implementations would have to agree forever.
- The digest is never truncated. A diff that does not fit the model's context
is refused with an actionable reason, because a walkthrough written against
half a diff reads as confident and is wrong.
- Nothing disappears. Lockfiles and other generated output are excluded from
the model's input by name — never by size — and everything no stop covers is
listed at the end, so "have I seen all of it" stays answerable.
- Cost is explicit. Results are content-addressed, so returning the working
tree to an earlier state costs nothing; generation outlives its request, so a
refresh detaches the client rather than discarding paid-for work, and only an
explicit cancel stops it.
Supporting changes to shared modules:
- git: expose the existing getRangeDiff as GET /api/git
listUntrackedPaths and getUntrackedDiffs. The latter resolve the repository
once for a batch instead of per file, taking a panel
~340ms on an 80-file working tree.
- small-model: structured output across four wire forma
and abort signal, and an onOverflow policy so an oversized prompt fails
loudly instead of being silently clipped. A provider
remembered so the prompt-side fallback goes first next time.
- models.dev metadata: surface structured_output as tri
false blocks a model, a missing field does not, because the catalog omits it
for roughly half of all models.
Desktop and tablet only: VS Code serves Git through its
these routes, and the mobile shell does not consume the surface registry.
Docs: packages/docs walkthrough page in English and all eight locales.
Skills listing ignored the active project when the OpenCode client
directory was unset, so project `.agents/skills` files were created but
never shown. Prefer the active project path (matching Commands/Agents)
and soft-fall back to it on skill API routes when directory is omitted.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Require an API key or {env:VAR} on client and server, add edit/prefill for
existing custom providers, save auth before config, and surface incomplete
auth plus disconnect after partial failures. Add VS Code parity tests and
drop the unused allProvidersConnected locale key.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Custom provider upsert uses PUT /api/provider, which was skipped by the
selective express.json allowlist and always saw an empty body.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Allow Settings → Providers to define custom providers (id, name, base URL,
API key, models, headers) without code changes. Persist config via OpenCode
layers, store keys through auth.set, and keep web/VS Code parity.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Navigation model rebuilt around two full-width drawers and a minimal
header (sessions / title-switcher / usage ring / workspace):
- Left sessions drawer: cross-project tree with live status indicators,
swipe actions on sessions (rename/archive/delete) and on group headers
(project edit / two-step close, worktree delete), reorder-only edit
mode with collapsible project cards and draggable worktrees, app-level
footer (connected instance, settings, pending web update).
- Right workspace drawer: Changes / Files / Terminal / Notes / MCP as
pill tabs (inactive tabs icon-only); panes stay mounted once visited.
The full desktop file editor serves the Files tab; read/skill tool taps
in chat open the file there at the requested line.
- Header session switcher on title tap: 10 cross-project recents with
live busy/attention indicators and project · branch metadata; the
usage ring opens a metadata overlay with an explicit loading state.
- The overflow menu is gone on phones (its destinations moved into the
drawers); iPad keeps it until its dedicated layout pass.
Correctness and continuity:
- /auth/session answers bearer-first, so a stale WebView cookie can no
longer mask a revoked device token; cold launches classify failures
fast and land on an explicit connect screen.
- Authoritative session snapshots raise frozen ordering baselines and
stale live ranks — recents stay truthful after the app slept.
- Cold launches reopen the last active session per instance (persisted
pointer, confirmed against a sessions snapshot; a user-opened draft
clears it), with a logo hold instead of a draft flash.
Also: collapsed pill composer gains the stop control; chat tool rows
share one 36px rhythm; Task subtool rows truncate; larger bottom safe
area so the composer clears big-screen corner radii; Capacitor build
hides About/Update (store updates apply there); widgets link to the
sessions drawer with a list icon; MobileApp split into focused modules;
five mobile-surface detectors unified; translucent borders normalized to
70%; all new strings translated across the 10 locales.
iPad and foldable layouts are intentionally untouched - separate next version PR.
Untracked symlinks now show as link entries in diff output.
File diffs display symlink targets instead of following them.
Added tests for patch and split diff behavior.
simple-git without baseDir inherits process.cwd(), so launching
OpenChamber from a neutral directory (e.g. $HOME) and opening a git
project elsewhere produced repeated "not a git repository" status
errors and could abort project/session enumeration. Always require an
explicit baseDir, soft-handle non-repo GitErrors on status/check
routes, and cover non-git, foreign-cwd, and nested-repo cases.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* perf: optimize session loading and startup
* fix(chat): stabilize history prepend virtualization
* perf: unblock first session open from startup network contention
Opening the first session after app start waited seconds for its message
fetch. Three independent contributors, each measured via CDP network
capture and Chromium net-log against the packaged desktop app:
- The active-session watchdog fired an uncapped per-directory status poll
and child-session discovery burst at startup, and other subsystems
(git checks, global session pages, command/skill discovery) fanned out
alongside it, saturating the browser's ~6 HTTP/1.1 sockets per origin.
Add a shared background-network gate (concurrency 3) and route the
watchdog, poll-shaped git reads (also priority: low), global session
pages, command/skill loads, and the background update check through it.
- The packaged renderer is cross-origin to the loopback backend, so every
API call needs a CORS preflight; a few slow OpenCode-proxied requests
held the whole pool while preflights and interactive traffic queued
behind them. Lift Chromium's per-host connection cap for loopback via
ignore-connections-limit in the Electron shell.
- OpenCode initializes each directory lazily on its first request, so the
first click paid that cost interactively. Warm the last-used directory
and the three most recently opened projects right after OpenCode
readiness, sequentially and best-effort, overlapping UI startup.
Validation: new background-network tests, lifecycle warmup test, focused
store/sync tests, UI type-check and lint, dead-code report, node --check
plus electron type-check/lint, and CDP first-open measurements on the
packaged app (message fetch socket queue 5.4s -> 0.03s).
* fix(ui): keep interactive git reads out of background queue
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
* feat: add Windows ARM64 support with x64-baseline CLI workaround
Windows ARM64 native opencode.exe fails with a Bun FFI/TinyCC dlopen
error (anomalyco/opencode#19130). As a temporary workaround:
- Bundle x64-baseline OpenCode CLI on ARM64 instead of native ARM64
(prepare-opencode-cli.mjs, env-runtime.js)
- Disable OpenCode self-upgrade on ARM64 in server, VS Code, and UI
(upgrade-capability.js, opencode-upgrade-runtime.ts, useUIStore.ts,
OpenCodeCliSettings.tsx, search.ts, SettingsView.tsx, platform.ts)
- Add ARM64 Windows cross-compile builds to release and smoke workflows
(release.yml, release-desktop-smoke.yml)
- Refactor Windows latest.yml to use combine-electron-manifests pattern
matching macOS, since two arches now produce per-arch manifests
The CI ARM64 build itself is permanent; only the x64-baseline CLI
bundling and upgrade disablement are temporary and should be reverted
when the upstream issue is resolved.
* fix(desktop): select Windows updater by architecture
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>