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
Detects the OpenChamber CLI from Bun's global install directory
Starts the global instance via the installed CLI path instead of relying on PATH
Fails fast if the global CLI was not installed
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.
Drop bullets that fixed bugs introduced within the same unreleased range,
fold the load-older button into its feature description, and group the
Capacitor-only keyboard/draft/instance polish under the pre-release
mobile-apps bullet like the 1.13.9 entry did.
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.
Transforms on the chat scroller (content or container) forced WebKit to
rebuild its composited scrolling layers — a multi-second stall on long
chats. The scroller now gets no transforms at all: it keeps a constant
client height by extending below its shrunken region (by keyboard minus
the safe inset the shell gives up, so the settle snap is geometry-neutral)
and converting the keyboard strip into its own bottom padding, driven by
--oc-kb-scroll-inset from the very start of the rise. Nothing resizes for
the virtualizer, every reachable row stays mounted, and open/close is a
single cheap scrollTop write: the re-pin happens as the keyboard starts
rising, and the hide clamp lands behind the still-visible keyboard.
The composer and draft title keep sliding with the keyboard, now via
inline transforms set by the choreography — WebKit does not reliably
start transitions when a transform changes through a CSS custom property,
which had parked the composer until the keyboard finished.
Opening a bottom-sheet overlay usually dismisses the keyboard in the same
moment, so the panel's keyboard-inset bottom anchor was transitioning down
underneath the panel's own rise animation — a jerky, offset entrance. The
anchor now snaps to its final position for the duration of the enter
animation and resumes animating afterwards (so an already-open overlay
still lifts smoothly when the keyboard reopens).
Drop the animated pill/composer morph in favor of instant swaps that are
synchronized with the keyboard choreography: a new oc:keyboard-intent
event collapses the composer (flushSync) before the hide compensation is
measured, so keyboard travel and composer height change land as a single
chat motion on both iOS and Android (Android also gains keyboard signals
and deterministic re-pins around its native resize). The WKWebView caret
is hidden during the transition so it no longer flies to its new position.
Draft screen: starter chips hide instantly while the keyboard is up and
the centered title rides the keyboard shift compensation instead of
double-jumping; the composer drag handle also works in dictation mode;
the highlight mirror is disabled on mobile so the caret matches the text.
Fixes: worktree discovery and the GitHub auth probe now wait for the
runtime connection (no more empty branch pickers / stale auth on cold
start), worktree discovery merges per project instead of clobbering the
persisted map, the cross-project session list resets on instance switch
(with an in-flight load guard) so no stale sessions linger, and mobile
overlay content contains its overscroll instead of bouncing the page.
Mobile composer redesign: when the keyboard is closed the input collapses
into a narrow pill (sessions, attach, placeholder, mic) with a round
new-session button that fades away on the draft screen. Model and agent
selectors move into a row above the textarea; the draft project/branch
pickers and the attachment menu become searchable bottom sheets reusing
MobileOverlayPanel; a drag handle (also available while dictating) swipes
the composer into and out of a fullscreen mode.
Keyboard-lifecycle hardening: composer controls (agent cycle, dictation
and its overlay controls) no longer steal focus and dismiss the keyboard;
overlays reopen the keyboard on close via a debounced restore chain that
survives menu-to-picker handoffs and skips the native file picker; open
overlays and dictation keep the composer expanded. Dictation starts
directly from the pill and its overlay content fades in after the shape
settles. The keyboard slide compensates the pill-to-full height change in
one motion, and the mobile highlight mirror is disabled so the caret
always matches the text layout.
Stop animating the app shell height when the keyboard opens: per-frame
reflow plus the scroll-follow chase caused visible micro-jitter on the
composer and pinned chat. The shell layout now snaps exactly once per
open/close at an invisible choreography point, while the composer and
pinned chat content slide via compositor-only transforms in sync with
the keyboard. Dismissal starts from the textarea focusout (no bridge
latency), runs a shorter 0.2s leg, and the WebKit form accessory bar
is disabled. Composer keeps a 12px gap above the open keyboard.
Complete rebuild of voice input on a server-authoritative streaming
architecture, replacing the legacy Web Speech / whole-blob / WASM engines
and the dead voice-agent layer (~4k lines removed).
Speech-to-text (dictation):
- Client streams 16 kHz mono PCM16 chunks over /api/dictation/ws with
seq/ack ordering; buffered audio is retained and replayed on reconnect
- Server transcribes and streams live partial transcripts back;
segments auto-commit every ~15s with silence suppression and adaptive
finalization timeouts
- Local provider (default, zero config): sherpa-onnx models in a forked
worker process — auto-download with progress, staged extraction with
verification, corrupt-model auto-recovery, idle shutdown after 5 min
- Model catalog with settings picker (accuracy/speed ratings, sizes,
download/delete): Parakeet TDT v2 (English) and v3 (25 European
languages, auto-detected), Whisper base and tiny (multilingual, light)
- OpenAI-compatible provider for any Whisper endpoint
- Composer overlay with live transcript, volume meter, timer, and
cancel / insert / insert-and-send actions; failed transcriptions keep
their audio for retry or accepting the partial text as-is
- Configurable keyboard shortcut (default mod+alt+v) toggles dictation;
Enter confirms and Escape cancels while recording
- Overlay is pixel-aligned with the composer (measured footer height,
matching paddings/typography/gaps) — no layout shift when toggling
Text-to-speech:
- Local Kokoro provider (English, 11 voices) synthesized in the same
worker via /api/dictation/tts/speak, managed by the shared model
pipeline; sentence-pipelined playback keeps time-to-first-audio at
~1 sentence regardless of message length, and stop cancels in-flight
synthesis
- Sanitizer keeps inline-code content (strips backticks only), reads
interword slashes aloud, and removes only absolute file paths
Settings:
- Voice page unified: a single read-aloud toggle owns all playback
options (the confusing "Enable Voice Mode" is gone); a new "Enable
voice input" toggle (default on, persisted to settings.json) hides
the composer mic entirely when disabled
Mobile and transport:
- iOS/Android microphone permissions added (dictation was previously
impossible on mobile)
- Fixed Android WebSocket upgrades: the Capacitor WebView origin
(https://localhost) was missing from the packaged-client allowlist,
403-ing every WS connection — root cause of the old mobile SSE lock,
which is now removed for all transports
Security and conventions:
- All HTTP routes sit behind the global /api auth gate; the WS upgrade
explicitly validates the UI session and origin, with oc_url_token
narrowly allowlisted and covered by tests; the dictation socket mints
a fresh URL token before connecting
- Routes register before the generic OpenCode proxy; the client goes
through runtimeFetch/getRuntimeUrlResolver, and runtime switches
reset the dictation socket
- VS Code deliberately reports dictation as unavailable (no server
process in that runtime)
CI: workflow Node bumped 20 -> 22 to match the repo engines and fix
better-sqlite3 installs broken by node-gyp@latest on Node 20.
New dependency: sherpa-onnx-node (prebuilt N-API; macOS/Linux x64+arm64,
Windows x64 — Windows-on-ARM falls back to the OpenAI-compatible provider)
- Migrate sidebar session groups, git changes panel, virtualized code
blocks, and JSON tree viewer from virtua to @tanstack/react-virtual;
virtua remains only inside the Pierre diff viewer integration
- Sidebar: preserve scroll position when virtualization enables
mid-session (enable only once the ancestor scroll element is resolved,
seed initial offset from its live scrollTop, render plain rows for the
single pre-paint frame); disable native scroll anchoring on the
sessions scroller; keep row spacing identical between plain and
virtualized modes; absolute row positioning so variable-height rows
cannot drift past the container
- Chat: expand tool/thinking blocks downward by only adjusting scroll
for rows growing above the viewport; raise the desktop history-load
lead to 1.5 viewports so prepends land above the visible area
- Git changes: compute the prefetch window from the first visible row,
skipping overscan rows above the viewport
- Sidebar rows: make the whole highlighted row area clickable, guarded
against double-firing from interactive children
- Replace virtua with @tanstack/react-virtual for chat history on all
surfaces: bottom anchoring (anchorTo: end), key-stable prepend
preservation, and native iOS touch/momentum deferral live in the core
- Patch virtual-core to clamp the render range to real scroll bounds
during transient adjustments
- Rows render in normal flow inside a translated wrapper so sticky user
headers keep working; measurement snapshots cached per session
- Pre-write container height in scrollToFn so the browser cannot clamp
anchor corrections to the stale height; hold the prepend anchor for up
to 180 frames on mobile while fresh rows settle (cancelled by user
input; desktop relies on core anchoring alone)
- Adaptive row-size estimate from per-session measured averages; disable
reveal fade-in for virtualized history rows
- Mobile loads older history only through an explicit localized top
button: no scroll-position trigger and no post-mount background
prepend, so every insert happens from a resting state; a quiet-window
hold defers any stray prepend commit while a touch gesture is active
- Desktop/VS Code keep the seamless scroll-up trigger and progressive
background prepend
- Mobile: defeat iOS momentum scroll when compensating history prepend
(overflow toggle + short rAF watchdog); disable history virtualization
and post-paint background prepends; preload Markdown renderer and use
plain-text Suspense fallback to avoid first-frame geometry shifts
- Desktop: stop double-compensating prepends on the virtualized list -
virtua shift owns the adjustment; remove sticky-anchor heuristics that
misfired as failed restores
- Sync: skip no-op store writes when messages/parts are unchanged
Avoid unnecessary resync on clean initial stream connect
Skip no-op message snapshot writes during recovery
Only trigger mobile resume sync after real app resume
The release workflow reads the pinned @opencode-ai/sdk version with bash command-substitution syntax before caching the bundled OpenCode CLI artifact. On Windows jobs GitHub Actions defaults run steps to PowerShell, which treated VERSION= as a command and failed before the cache/build steps ran.
Set shell: bash on the release workflow version-discovery steps so macOS and Windows use the same syntax. The smoke workflow already used bash for the Windows version step, which is why the smoke artifact could pass while the production Windows release job failed.