Accept browser https origins when TLS terminates before an HTTP proxy hop
Honor forwarded external host while rejecting mismatched origins
Add tests for proxy and host matching behavior
Every project-config write re-serialized normalized tasks, so a server
that shares the config file but predates a field (goal, auto-accept)
stripped it the first time any task ran. Untouched tasks now go back to
disk verbatim, a state update swaps only `state`, and only a deliberately
replaced task is serialized from the normalized shape.
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
* 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.
Reverts #2687. In real use the injected small_model behaves poorly with
OpenCode: its internal small-model consumers and OpenChamber's own small
model are different things and must stay configured separately.
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
Drop the canonical-containment 403 guard and the extra realpath(base) the
read routes (stat/read/raw/serve) had gained. Every workspace resolution
returns insideWorkspace: true and outside-file grants use
base = dirname(canonicalPath), so the guard could never fire; the flag had
no remaining reader and is gone with it. The read routes are back to the
single realpath(resolved.resolved) they had before.
Move the lexical-base fallback out of the inline header parsing in
routes.js. x-opencode-directory decoding belongs to
project-directory-runtime, so resolveProjectDirectory now also returns
requestedDirectory, the pre-realpath candidate that validated.
resolveWorkspacePathFromContext retries against it when the canonical base
rejects a path, which keeps files under a symlinked project root
addressable without a second copy of the header/query parsing.
readProviderConfig read only options.baseURL and options.apiKey, and the
OpenAI-compatible dispatch hardcoded a bearer token, so provider
options.headers never reached the request. OpenCode sends those headers on
every chat turn, which left the small model authenticating differently from
the request path against the same URL.
Providers behind a gateway that authenticates on its own header, such as the
Ocp-Apim-Subscription-Key default of Azure API Management, answered 401 for
walkthroughs, session goal audits, titles and commit summaries while the same
model worked in chat.
Read options.headers alongside the API key, resolve {env:...} and {file:...}
in the values with the existing resolveConfigApiKey, and merge them into the
request after the bearer default so a gateway whose header is the credential
can override it.
Closes#3213
The Small Model override chosen in Settings never reached the managed
OpenCode process config, so OpenCode's own title/summary generation
kept using its fallback chain instead of the user's explicit choice
and sessions stayed untitled.
Merge main into this branch to pick up the includeWeb/includeMemory
flags added to prepareManagedOpenCodeEnv, and re-apply the Small
Model injection on top of that current env shape in
getManagedOpenCodeEnv (server/index.js).
Closes#2497
Merge main and reduce the change to the defect that reproduces: the server's
synchronous login-shell probes (env snapshot and command -v for opencode,
node, bun) ran with no timeout, so a slow or interactive rc file held startup
until it returned — on macOS that is what made a brew-installed opencode look
undetected from a Dock launch. Every probe now carries the same 5s bound the
Electron shell probe already uses and falls through on overrun; the known
install locations already include both Homebrew prefixes.
The non-login command -v fast path and the reproduction script are dropped:
a plain sh inherits the same PATH the resolver has already walked.
Closes#1720
Since opencode 1.18.x, `POST /global/upgrade` requires a `target` semver in
the body. OpenChamber sent an empty object, so every "Update OpenCode" click
came back 400. The rejection arrives as `{name, data:{message}}`, which has
no `error` field, so the user was left with the bare status text: "Bad
Request".
Resolve the target from the latest release — the same lookup the upgrade
prompt already uses to decide there is anything to offer — and fail with an
explicit code when it cannot be resolved, rather than sending a body opencode
is guaranteed to reject. Read the upstream rejection message so a refused
upgrade explains itself.
The VS Code extension carries its own copy of this flow and had the same two
defects; both are fixed there.
fixes#3121
Main already returns entry paths under the requested (lexical) directory,
fixed separately. Re-applying the original LIST hunk introduced two
regressions: shadowing of outer 'let requestedPath' inside the try block,
and the gitignore filter comparing lexical entry paths against
'ignoredPaths' built from the canonical realpath.
This commit drops the LIST hunk and the two list tests that accompanied
it. The read-family fixes (stat/read/raw/serve) stay — those were the
actual symlink resolve-before-containment fix and are not affected by
the LIST regressions.
Refs btriapitsyn on #2872 (2026-08-27).
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)
Command Code's official API has no usage endpoints; the old usage source
was the unofficial studio API reached through a now-archived plugin, so
the tile could only ever fail for officially configured users. Removed
across server, shared UI, and the VS Code extension; the provider logo
fallback stays — it serves the model picker, not usage.
PR-status source candidates were every configured remote, so a checkout
carrying contributor forks matched a fork's closed PR whose head merely
shared the branch name — a fork's 'main' surfaced on the local main in the
git and work-status panels. Only the ranked-first remote (the one the
branch pushes to) and its fork network are PR sources now; other remotes
remain search targets but their owner:branch heads no longer count.
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.
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.
The tab list lived only in per-tab sessionStorage, so a new browser
tab, another device, or cleared storage showed an empty terminal
sidebar while PTYs kept running server-side, and orphans leaked until
the idle sweep. Add GET /api/terminal/sessions and adopt unknown
server sessions into the local tab projection (additive only; a failed
listing changes nothing).
The idle sweep also reaped terminals in background tabs because only
the active tab holds a WebSocket attachment. Add POST
/api/terminal/touch and have open clients periodically refresh
activity for every session their tabs reference.
Two mitigations for local multi-instance contention over the shared relay
identity:
- A standby instance now waits a 2-minute grace period after the host claim
frees before taking over, so a cleanly restarting host (app update or
relaunch) — which reclaims at boot with no wait — always wins the restart
window instead of stranding paired devices on another process.
- Dev instances never host the relay passively: dev scripts set
OPENCHAMBER_RELAY_HOST=off and the Electron dev shell is detected via
OPENCHAMBER_ELECTRON_DEV. Explicit enable/pairing on such an instance still
force-claims; OPENCHAMBER_RELAY_HOST=on overrides.
A dedupe-keyed client re-mint (QR rescan, password/passkey re-login) replaces
the stored record, which also reset the operator-visible name to the app's
hardcoded default ('OpenChamber Mobile'). The app-reported name is now only a
fallback: an explicit pairing label wins, otherwise the replaced record's
label is kept, and the default applies only to a first-ever pairing.