PR #1673 added sanitizeHeadersForBrowser in the fetch bridge to handle
non-ISO-8859-1 directory paths by encoding the value and attaching a
x-opencode-directory-encoding: uri header. The server-side decoder was
already in place (08b86613). However, the CORS Access-Control-Allow-Headers
list on the Express server was not updated to include this new header.
When a user opens a directory with CJK or other non-Latin-1 characters
(e.g. D:\文件), the browser sends a CORS preflight OPTIONS request with
x-opencode-directory-encoding in the Access-Control-Request-Headers.
The preflight fails because the server does not list it as allowed,
blocking all subsequent API requests with 'Failed to fetch'.
Add X-OpenCode-Directory-Encoding to the Access-Control-Allow-Headers
response header for openchamber-ui://app packaged client origin.
* fix: open mobile model/agent panels on tablet-width Capacitor shells and keep composer taps from dismissing the keyboard
* feat: add iPadOS-style split layout to the Capacitor app
- classify the Capacitor shell as mobile in device detection so shared
surfaces (draft starters, panels) stop falling into tablet branches
- add isIPadApp() and useOrientation() helpers
- iPad: persistent full-height sessions sidebar (mobile sessions surface
inline), Changes/Files in a right sidebar with header shortcut toggles
- animate sidebar open/close like the desktop sidebars and add
finger-sized drag-resize with persisted widths
- anchor the overflow menu and the usage/metadata popover next to their
header buttons regardless of open sidebars
* fix: re-anchor metadata popover on layout shifts and untangle sidebar toggle updates
- recompute the iPad metadata popover anchor via a ResizeObserver on its
wrapper so sidebar toggles/resizes while it is open cannot leave it
misplaced
- move the portrait right-panel close out of the setIpadSidebarOpen
updater into plain sequential state updates
Avoids syncing code line numbers while markdown is still streaming
Keeps code block wrapping and line numbers stable after render
Updates code block layout to support deferred gutter insertion
Normalizes bare ---/+++ headers and paths before rendering
Repairs loosely formatted hunk bodies for patch display
Recounts hunk ranges so diff headers stay accurate
Adds line-number gutters for markdown code blocks
Keeps gutter heights in sync when wrapping or resizing changes
Applies wrap styles directly to pre and code for better overflow handling
Adds a chat code block wrap toggle in markdown code block headers
Persists and restores the setting across desktop/web settings
Adds localized labels and OpenChamber search entry for the new option
Adds OpenChamber Relay — an opt-in way to reach an instance from a phone,
browser, or another desktop from anywhere, with no open inbound ports, no
tunnel, and no shared LAN. The instance dials outbound to a relay; all app
traffic (HTTP, the event stream, terminal, dictation) is multiplexed and
encrypted through a single connection per client, so the relay only ever
forwards opaque ciphertext.
Transport
- End-to-end-encrypted channel over WebCrypto (ECDH P-256 -> HKDF ->
AES-256-GCM) with a capability-negotiated handshake and a small
HTTP/SSE/WebSocket multiplexing protocol. A byte-compatible JS host mirror
is cross-checked by tests.
- Host: outbound connection manager, per-client tunnel dispatcher to the local
server over loopback, reuse of the existing instance identity key, and
management routes. Disabled by default; explicit opt-in.
- Client: plugs into the existing runtime layer (runtime-fetch/-url/-switch/
-auth, event pipeline, terminal, dictation) so features work over the relay
unchanged; direct-URL and Electron realtime-proxy paths are untouched.
Pairing & UX
- Relay section in Settings -> Remote Instances (live status, QR/link pairing,
revocation via the existing client-token list) and the mobile connect flow.
- Frame batching and idle-gated keepalive keep tunnel message volume low
without affecting streaming smoothness.
Security
- The tunnel is transport only; the server authenticates every tunneled
request exactly as for a direct remote client.
fragments only. The relay stores no keys, tokens, or payloads.
Operability
- The endpoint can be pinned to a self-hosted rel
paired clients inherit it from the offer automatically.
- Relay module DOCUMENTATION.md and a relay-trans
invariants that future WebSocket/streaming changes must follow.
The relay transport is complete and tested; the UI for enabling and pairing
is gated behind openchamber_relay_gate and stays
Keep the browser pane loaded URL separate from the in-frame current URL so SPA navigation updates the address bar and history without remounting the iframe or resetting the Electron webview src.
Preserve parsed ?session= route params during initial URL normalization and pass a directory hint when applying deep links, preventing embedded OpenChamber sessions from collapsing back to / while bootstrap catches up.
Adds a Last turn scope to DiffView that renders OpenCode snapshot diffs from the latest user message summary without re-fetching git contents. The view hides Review in that mode and carries the selected diff scope through main and context-panel navigation.
Connects latest-turn changed-file chips in chat to the snapshot diff view on desktop and mobile, while keeping older turn chips static/read-only to avoid misleading affordances and extra subscriptions. Updates localized labels and empty states plus changelog.
Validation: bun run type-check (packages/ui); bun run lint (packages/ui).
Moves the hidden file input out of the attachment controls so it stays mounted.
Prevents file selections from being lost when the composer variant changes.
Restores reliable local attachment uploads after opening the OS picker.
Keeps mobile composer controls from missing taps during keyboard blur/reflow
Applies the deferred blur behavior to mobile browsers and installed PWAs
Leaves Capacitor behavior unchanged
The dictation overlay is absolutely positioned over the composer, so the
transcript could not expand it — long dictations clipped after two lines.
ComposerDictation now measures the transcript text block (not the flex-1
container, which would feed the composer's own height back and creep a few
px per update) and reports it to ChatInput, which feeds it into the
textarea autosize: same line cap as typing, transcript area scrolls past
it and follows the newest words. Idle/unmount releases the height, and an
idle sibling instance (mobile footer + wrapper engine) can no longer zero
the active one's report.
- Add standalone-only safe-area padding for the composer (bottom floor +
fullscreen top inset) and top toast offset; env() reports 0 on iOS 26
standalone so a fixed floor is required
- Pin the mobile shell to 100lvh: WebKit leaves 100dvh stuck at the
keyboard-shrunk value after dismissal
- Clamp visual-viewport pinning to documentElement.clientHeight to guard
against stale visualViewport metrics
- Defer the composer blur flip (120ms) so taps on composer controls
survive the keyboard-resize reflow; transition the bottom padding so
the late flip reads as a slide, not a dip
- Restore the keyboard after mobile overlays close: MobileOverlayPanel
dispatches synchronous open/close events, ChatInput refocuses within
the same gesture, holds focus through iOS's tap-settle dismissal,
guards the pill collapse via DOM focus, and reveals the composer form
above the keyboard (programmatic focus skips iOS's native reveal)
Adds a load older button when earlier history is available
Preserves scroll position while older messages are loaded
Shows date-grouped messages with clearer per-message timestamps
Uses hunk contents to find the first modified line instead of the hunk start
Handles added, removed, and binary-only patches more accurately
Adds tests for patch parsing edge cases
File references in chat messages can now use the 'path:start-end'
form (e.g. 'src/foo.ts:120-145'). The reference becomes clickable in
the renderer and, on click, the file opens at the start line. Range
selection is intentionally not done at this layer — the
'path:start-end' form is parsed only so the link resolves to the
correct path; navigation jumps to the start line, matching the
behavior of the existing 'path:line' form.
- Extract the file-reference parser to a dedicated module so it can
be unit-tested without pulling in the markdown renderer's worker
dependencies.
- Add a range branch to 'parseFileReference' and update the
block-code path regex to recognize the new form.
- Switch the colon-form regex to a non-greedy path match so
'path:line:col' is no longer mis-parsed as 'path:line' with the
first numeric suffix dropped into the path.
- Add a unit test covering the new and existing parser forms.
Keeps the textarea reference available during mobile viewport adjustments
Ensures the composer scrolls back into view after keyboard interactions
Updates text selection menu dependencies to include the current session
Adds a dialog with call booking and survey actions for user feedback
Shows a one-time sidebar toast prompting users to share what’s useful or missing
Adds localized copy for the new feedback entry points
Checks the runtime session before restoring a mobile connection
Disconnects and resets state when the session is no longer valid
Adds tests for reachable, unreachable, and unauthenticated runtimes
Adds a server-side "small model" capability: direct, cheap LLM calls that
reuse the user's existing OpenCode provider logins — the mechanism OpenCode
uses internally for titles and summaries but does not expose through the
SDK or plugins. Zero new dependencies; plain fetch with per-provider wire
formats, credentials never leave the server.
Core (packages/web/server/lib/small-model):
- Resolution mirrors OpenCode's session scoping: explicit settings override
→ small_model from the OpenCode config → family scan within the session's
provider → the session's own model. The global provider scan only serves
callers without a session context, and background callers forbid it
entirely (restrictToPreferredProvider), so conversation content never
reaches a provider the user didn't pick — explicit choices excepted.
- Per-provider auth replicating OpenCode's plugin loaders: GitHub Copilot
(device token as bearer, no exchange), ChatGPT plan via the codex
Responses API (single-flight OAuth refresh written back to auth.json),
Anthropic messages, Google generateContent, generic OpenAI-compatible.
- OpenCode's free models (opencode/big-pickle, *-free) are never called
directly; unauthenticated providers are skipped by design.
- Prompt clamping to the model's catalog context limit; thinking disabled
where a wire switch exists (Z.AI/GLM, MiniMax-M3, Gemini Flash); robust
content parsing with a clear error when a thinking model spends its whol
budget on reasoning.
- Settings → Sessions gains a Small Model group: use-default checkbox plus
an override picker limited to authenticated providers, persisted with
web/desktop/VS Code sanitization parity.
Consumers:
- Session assist: a server-side watcher on the global SSE hub generates a
short recap and one suggested follow-up after a session idles quietly fo
a minute, stored on session metadata (openchamber.assist). Freshness is
keyed to the last assistant message id, so new activity invalidates the
payload everywhere with no extra writes. The chat shows the recap under
the last message after five quiet minutes and the suggestion as a
dismissible chip above the composer (tap fills the input, never sends).
Gated by a new Chat setting (default on) that is a hard generation
switch. Language is anchored to the conversation itself, with a
script-mismatch guard against model/backend language hallucination.
- TTS: a third input mode, summarized — long replies are condensed to
spoken prose before playback on any TTS engine.
- Git: commit-message and PR generation moved off the active chat session
onto the small model fed with real diffs and the commit list (bodies
included), with a session-transport fallback for free-model-only setups.
- Notes: Add to notes distills long selections into 1-3 dense sentences
preserving exact identifiers, with verbatim fallback on failure.
Fixes along the way:
- The global event watcher now starts unconditionally; it was gated behind
the desktop-notify env, leaving the server-side event hub dead in
packaged apps.
- OpenCode re-emits message.updated for old user messages after idle; the
watcher no longer mistakes those for new activity.
- Session metadata merges from a fresh read right before the PATCH, so
writes made during the generation window (suggestion dismissals, review
links) are preserved; the assist runtime stops during graceful shutdown.
The stop button sent the abort with the UI's active directory, but
OpenCode dispatches the request to the per-directory instance — for a
session running under a different project, worktree, or a mapped docker
path the abort hit an instance that didn't own the prompt, cancelled
nothing, and still returned 200. The abort now resolves the session's
own directory, like the revert flow's aborts always did.
Launching: the VS Code extension spawned .cmd shims with shell:true,
which builds an unquoted command line — a path like
"C:\Program Files\nodejs\opencode.cmd" broke with "'C:\Program' is not
recognized". It now spawns cmd.exe directly with the shim path as its
own argv element (the web server's existing cmd-wrapper pattern), and the
web server's final launch-spec fallback routes raw .cmd/.bat through the
same wrapper instead of handing them to spawn.
Configured paths: wrapping quote pairs (Windows "Copy as path" pastes)
are now stripped everywhere a binary path enters — the VS Code setting
and shared settings.json, env vars on both surfaces, the server's
directory-path normalization, and the desktop settings input.
Discovery: added the system-wide npm prefix (Program Files\nodejs) and
scoop's .exe shim to the Windows candidates, Linuxbrew on Linux, and the
where-probe now runs with windowsHide. The macOS desktop-app exclusion
also covers the OpenCode Dev/Beta app bundles.
The bundled CLI used to outrank PATH and known install locations, so
updating the desktop app silently switched people off their own OpenCode.
It is now the last resort: explicit env/settings, PATH, known install
locations, and shell discovery all win; the bundle only serves machines
with no OpenCode install at all. The env-runtime accepts an injectable
homedir so the fallback test stays hermetic on machines with a real
~/.opencode install.
The client-create gate added in 1.13.9 rejects client tokens without the
desktop-local kind, but the kind was only attached when the runtime
origin exactly matched the injected local origin — an empty (same-origin)
api base, loopback aliases, and the embedded server addressed via a LAN
interface (0.0.0.0 binds) all minted untagged tokens, which then hit 403
and surfaced as "Local — Auth required" plus the unreachable-server
screen. The renderer now treats same-origin and loopback targets as
local, the Electron main additionally matches any of the machine's own
interface addresses on the local server's port, and a deduped kind-tagged
mint migrates away legacy same-label tokens that predate client kinds.
The client-create gate itself is unchanged.
Mobile browsers don't shrink the layout for the keyboard, so the
fullscreen composer is now pinned to the visual viewport (fixed at its
offset and height, tracked as the browser pans) instead of overflowing
underneath it; the draft screen's normal composer gets the same pinning
anchored to the visible bottom via a rAF tracker, since Safari's own
focused-field reveal proved unreliable there after leaving fullscreen.
The draft and empty-session roots drop their transform-gpu (a transform
would make them the containing block for the pinned form), the app
header hides while the browser fullscreen composer is up (the form can't
out-stack it from inside the composer wrapper's stacking context), and
leaving fullscreen nudges the still-focused field back into view.
Draft starter chips now hide while the keyboard is open in browsers too,
via an oc-browser-keyboard-open root class driven by composer focus.
The mobile browser flashed the unreachable-server screen during the
ordinary initial connect; it now keeps the logo splash and only shows the
error once the 8s recovery window expires (shared with the native path).
All MobileApp splash logos are 120px to hand off seamlessly from the
static boot splash instead of visibly shrinking.
Command, file/agent, skill, and snippet autocompletes now stop at the top
of the chat area (Capacitor) or the visible viewport edge (mobile
browsers, which pan the page for the keyboard) and may grow that far,
measured live across keyboard settles and viewport changes. On mobile the
keyboard-hint footer and description lines are gone, rows center their
icons, and list overscroll no longer bounces the page behind. Selecting a
command no longer dismisses the keyboard (its rows now block the tap's
focus steal like the composer buttons), and the dead dismissKeyboard
option is removed.
Starter chips route their text into the submit as an explicit override;
the previous flow staged it in the textarea, which doesn't exist while
the mobile composer is collapsed into the pill, so the submit read an
empty snapshot and silently bailed, leaving the command text sitting in
the input.
The pill's expand focused the textarea from a rAF, outside the user
gesture — mobile browsers only show the soft keyboard for synchronous
focus, so the composer expanded silently. The expand now flushes the
render and focuses in the same gesture, and preventScroll applies only
inside the Capacitor shell so browsers keep their native reveal that
lifts the field above the keyboard (same for the post-overlay keyboard
restore). Also drafts the [Unreleased] changelog entries for everything
since v1.13.9.
The native-apps change removed SessionAuthGate from the whole mobile
render path, but that path also serves the plain mobile browser: against
a --ui-password server it never authenticated and dead-ended on the
unreachable-server screen with no way to enter the password. The gate is
back for the browser shell; the Capacitor app keeps its own
instance-connect flow ungated.
The button's visibility mixed the sync meta with the prefetch-cache hint;
a stale prefetch entry (cursor recorded at the initial page) could keep
the affordance alive after the user had already loaded to the top. Sync
now exposes an explicit isComplete (positive confirmation from a fetch,
distinct from !hasMore on unpopulated meta) and it overrides the prefetch
hint, which stays in effect only before the meta knows anything.