Omits the craft-goal command from draft starters in VS Code
Prevents the starter from being resolved or pinnable in that runtime
Keeps non-VS Code behavior unchanged
Adds section headers and tooltip help for the goal settings area
Reorganizes chat settings into Session Assistance, Reasoning, Message Appearance, Tools & Files, and Composer groups
Adds localized labels and search entries for the new section headings
Relay connect used to serialize a dead LAN probe (up to 8s per stale address
on mobile, 2-4s on desktop) in front of the relay attempt, then paid a second
WebSocket connect + E2EE handshake because the probe tunnel was thrown away.
- mobile probeConnectionCandidates: race the relay probe against the direct
chain with a 1.5s direct headstart; a live LAN still wins, a dead one no
longer delays startup
- relay probes adopt their tunnel as the runtime tunnel (adoptRelayTunnel)
instead of dialing a fresh one — applies to auto-connect, pairing redeem,
password login, and the desktop host switcher's relay fallback
- relay probe drops the /health round-trip: the E2EE handshake already proves
the server identity, /auth/session alone proves liveness and auth
- desktop restoreDesktopRelayRuntime: same headstart race; a late direct
success hot-switches back (stable runtimeKey); startup probe now passes
expectedServerId so a re-leased LAN address never sees the token
- launch splash shows 'Connecting to device: <label>' with animated dots
under the (still centered) logo, translated in all locales
- editing a saved instance no longer rebuilds it from the URL field alone:
the id is passed through, relay/https candidates are preserved, and a
token-key change migrates the Keychain token instead of orphaning it
All local instances share the data dir and therefore the relay identity
(serverId), so concurrent relay hosts evicted each other at the relay worker
(4001: Control replaced) and paired devices landed on whichever local process
won last — often a stale dev server, surfacing as 'Unable to reach server'
and devices stuck on relay with 503s on newer endpoints.
- relay/host-lock.js: per-machine claim file (relay-host.lock, {pid}); stale
claims from dead pids are ignored; unwritable data dir falls back to
pre-lock behavior
- relay/service.js: start only when the claim is free or ours, otherwise
'standby' with the holder pid in lastError; 30s watcher takes over when the
claimant dies and stands down when another process claims; pairing-link
creation and explicit /relay/enable force-claim (user intent wins)
- mobileConnections.ts: log candidate-refresh skip reasons and the refresh
result instead of failing silently
Candidates refresh (server + mobile + desktop clients):
- GET /api/client-auth/connection/candidates returns the server's current
LAN URLs, relay candidate, and serverId for already-paired devices
- /health and /api/version expose serverId so clients can verify a learned
address belongs to the expected server before sending their bearer token
- mobile: refresh saved candidates over the live transport after every
connect/wake, hot-switch relay->LAN when a fresh address is reachable;
serverId gate on direct probes; token no longer sent to /health
- desktop: refresh stored host apiUrl after a relay connect and hot-switch
back to direct; electron probe verifies serverId before authenticated fetch
Fixes found while debugging a dead pairing:
- settings: strict reader that throws on corrupt/unreadable file instead of
returning {}; relay signing/encryption key generation is now gated on it,
so a swallowed read failure can no longer mint a new server identity and
orphan every paired device (loud log when a keypair IS generated)
- SessionAuthGate: bounded auto-retry for transient session-check failures
(initial request racing the relay tunnel's first WS attempt, startup 5xx)
- the goal dialog renders as the shared MobileOverlayPanel bottom sheet
on mobile instead of a centered dialog
- tapping the target button to ARM keeps the soft keyboard open (the next
message is the objective; same guard as the attachment/mic buttons),
while opening the manage sheet lets the keyboard close as usual
- capacitor: bottom-sheet overlays cap their height by the keyboard inset
AND the top safe area — with the keyboard raised while typing inside a
sheet, 100dvh does not shrink (native resize is off) and the panel could
slide under the notch/status bar; applies to every MobileOverlayPanel
The distillation moved from PlanView into setSessionGoal — the shared
choke point for every goal source (composer, fork dialog, plans) — and
into the scheduled-tasks goal creation server-side. Any objective over
5000 chars is distilled into completion criteria by the small model; on a
transient failure a head+tail excerpt keeps the intent (top) and the
acceptance criteria (bottom) with a trim marker, plus a toast. The
working agent always received the full prompt in chat — only the auditor
is bound by the limit.
Plan-goal objectives are capped at 5000 chars for the auditor while the
working agent reads the full plan from its file. Plans over the limit are
now distilled by the small model into completion criteria (end goals +
per-phase verification, no implementation steps), prefixed with a header
pointing back at the plan file so every continuation re-anchors on the
live source of truth. If distillation fails (transient small-model
hiccup), a head+tail excerpt keeps the plan's intent (top) and acceptance
criteria (bottom) with a trim marker between — sacrificing the
implementation middle the agent reads from the file anyway — and a toast
tells the user the objective is degraded.
Removes recent-project grouping from the sidebar project list
Keeps project ordering based on manual and default sort rules only
Reduces memoized session-derived state in the sidebar
Move per-session permission auto-accept policy ownership from the UI to the
OpenChamber server so enabled sessions continue running when clients disconnect
or the server restarts.
- persist explicit per-session policies in OpenChamber settings
- inherit the nearest explicit policy across subagent session hierarchies
- allow child sessions to opt out of an inherited parent policy
- immediately accept matching global and directory-scoped pending requests
- process future requests without requiring a connected UI client
- reconcile pending permissions after startup and event-stream reconnects
- deduplicate concurrent requests and retry transient reply failures
- synchronize policy updates across connected clients
- migrate existing browser-persisted policies to server storage
- suppress auto-accepted permission cards before they enter UI state
- show deduplicated permission toasts for inactive sessions
- preserve foreground-only permission handling in VS Code
- integrate directory-aware notification routing from main
- add coverage for persistence, inheritance, retries, reconciliation, pending
requests, client hydration, and inactive-session toasts
Use authoritative session.idle and session.error events for notifications
while retaining legacy message.updated handling for compatibility.
Classify sessions through targeted, directory-aware session lookups instead
of fetching the full session list. Cache confirmed root and parent session
relationships without treating failed lookups as root sessions.
Honor subagent completion settings and templates across the server-driven
web, desktop, and mobile paths, and bring the VS Code webview notification
policy to feature parity.
Use freshly synchronized VS Code settings, retry failed settings syncs,
extract session error messages, and deduplicate authoritative and legacy
completion and error events.
Adds /craft-goal autocomplete and chat handling for starting a Goal crafting session.
Introduces new Magic Prompts content and localized labels/descriptions for Goal crafting.
Migrates desktop draft starters to include Craft a Goal once and persists the migration marker.
Compaction fixes (observed in a real long run):
- the summary message's zeroed tokens froze the goal counter at its
pre-compaction value; segments now close with the previously displayed
total as a continuity floor
- audits and continuations after a summary tail now take execution params
(provider/model/agent/variant) from the newest non-summary assistant
turn instead of inheriting agent 'compaction' and the summarize model
File-backed objectives:
- the objective text lives in <data-dir>/goals/<sessionId>.md, keyed by
session id (one goal per session, a new goal overwrites the file);
metadata carries only an objectiveFile flag so session.updated fanout
stays light, and never a path — ids are pattern-validated before any
filesystem access
- limit raised to 5000 chars, no snapshot field: the UI fetches content
via PUT/GET/DELETE /api/goals/objective/:sessionId (behind the blanket
/api auth gate), writes the file before stamping metadata, and falls
back to an inline objective when the write fails
- the loop reads the file fresh on every tick, so objectives are
live-editable mid-goal; a missing file falls back to the inline text
- scheduled goal tasks write the objective file server-side; VS Code
degrades to the audit note (route unavailable there by design)
Arm the target button in the composer and the next prompt becomes a goal:
the server keeps the session working toward it (idle tick -> small-model
audit -> continuation) until the objective is verifiably complete, blocked,
or out of budget — even with the UI closed.
Server (packages/web/server/lib/session-goal):
- event-driven loop on the global SSE hub; goal state lives in
session.metadata.openchamber.goal (merge-safe patches, stale-write guard
by goal id), so it survives restarts and syncs to every client for free
- the small-model audit (objective + last assistant turn only, language
pinned to the objective) is the sole termination authority; blocked needs
3 consecutive verdicts, audit outages tolerate one unaudited continuation
then stop the goal as resumable-blocked
- hard stops: optional token budget, auto-continuation cap (Resume grants a
fresh allowance), turn errors; user abort pauses the goal instead of
blocking it, and resuming over an aborted tail nudges immediately
- token accounting as a snapshot of the latest turn (input + cache.read +
output), goal-relative via a creation baseline and segmented across
compactions; a compaction summary skips the audit and continues
- continuations reuse the session's own provider/model/agent/variant
UI:
- three-mode target button (arm / disarm / manage dialog), informational
goal strip with inline pause/resume and an Evaluating indicator, sidebar
state glyph, objective length counter (2000-char server clamp),
read-only completed goals
- goal entry points: composer (sessions and drafts), start-new-session-
from-answer dialog, plan implement dialog (plan content becomes the
objective), scheduled tasks (Run as goal + budget)
- Settings -> Chat -> Goal: feature toggle + default token budget with
three-layer parity (web server, client persistence, VS Code bridge);
VS Code renders goal state but hides the entry points (the loop runs in
the web server only)
Notifications: per-turn "ready" notifications are suppressed while a goal
is active; settling sends one final notification (desktop, web-push, APNs
generic titles with the session name as body) honoring the completion
toggle. Error/question/permission notifications are untouched.
Docs: user guide (session-goals) in all 9 locales + sidebar entry,
scheduled-tasks cross-reference, server module DOCUMENTATION.md.
Adds a surface=desktop URL param to embedded session chat links
Uses the surface override to classify embedded chat as desktop
Covers the new URL parameter in tests
Prevents narrow embedded session chat panels from being misdetected as mobile
Uses the ocPanel=session-chat query parameter to पहचान desktop-like layout
Keeps device detection aligned with other desktop shell runtimes
Android 15 enforces edge-to-edge and ignores the StatusBar overlay:false
inset the app relied on, while every --oc-safe-area-* CSS definition was
gated behind iOS-only conditions. Read the Capacitor-injected
--safe-area-inset-* vars (with env() fallback) on the Android native
shell so the header, top toasts, and connect screen clear the status
bar; both sources report 0 where the native inset still applies.
- --relay links now carry both routes: direct LAN plus relay fallback,
matching the UI's Anywhere pairing; devices prefer the direct route
- pairing sessions created by the CLI are marked with usesRelay, and the
server reconciles relay demand on a timer, so a headless instance
brings the relay up on its own after connect-url --relay
- warn with LAN_UNREACHABLE when the link's direct route points at
loopback and other devices cannot use it
- document the --relay flow and the --lan binding caveat in Connect a
Device and Remote Instances across all locales
- new Connect a Device page: one-time QR pairing, transport choices, device management
- new Private Relay page: E2EE guarantees, demand-driven lifecycle, relay vs tunnel
- rewrite mobile page around the native iOS/Android apps (TestFlight + APK)
- update remote-instances, security, tunnels, and remote-access troubleshooting to point at the new pairing flow
- translate everything across all 8 locales and update the sidebar
Tool JSON output now starts with a compact navigable summary view.
Expandable tool output includes quick open-file and diff actions for changed files.
Reasoning headers strip stray HTML comments, and navigation tools stay compact.
Changelog leads with the private relay and the native mobile apps (TestFlight
beta + Android APK links), followed by pairing v2 and the device management,
desktop multi-transport, and chat items; VS Code changelog gets the shared
chat-render entries.
- A saved host now keeps every transport its pairing link carried: direct URL
plus the relay descriptor, with one token for both (the mobile connection
model). Switching tries the direct leg and falls back to the E2EE tunnel;
list probes report Connected · Relay when only the tunnel reaches the host;
relaunch restore picks direct first
- Host switching trusts the dropdown's fresh probe instead of re-probing on
click (no doubled latency, no transient Unreachable flashes); statuses are
written once with the final outcome, survive the dropdown closing via a
last-known cache, and an unprobed host reads Checking — never Unknown
- Open-in-new-window works for relay hosts: a new IPC command boots the local
UI with the host id injected and the renderer picks the transport; the app
render holds on the relay restore so the splash shows instead of a transient
auth screen (10s safety valve)
- Relay host control socket gained protocol-level keepalive: a missed pong
window terminates and reconnects, so the relay can no longer hold a ghost
registration that leaves every client tunnel hanging; the desktop relay
probe also hard-times-out at 8s instead of hanging status flows
- Services dropdown restyled with mobile-style cards: per-provider usage
cards, per-host instance cards with a selected highlight and a toned
status line, MCP servers grouped in a card
- Each saved server row shows live reachability (Connected · Nms ping /
Unreachable / Auth required) with a status dot, probed once per list change
through the shared HTTP/relay probe (relay probing moved to desktopHosts as
probeRelayDesktopHost, reused by the host switcher)
- Section header: one short description, Import Link promoted to the primary
action; the token-storage note moved into the Add Server dialog next to the
token field it describes, and the dialog got its own description
- Probe relay hosts through a throwaway E2EE tunnel in the host switcher
instead of an HTTP probe against the relay:// pseudo-URL, which always
reported Unreachable
- Show 'via OpenChamber Relay' for relay hosts in the switcher and the
servers list instead of the raw relay:// pseudo-URL; hide the URL-centric
edit action for relay hosts (saving it would drop the tunnel descriptor)
- Pairing LAN candidate prefers the address the requesting client actually
reached the server on; interface scanning could pick an unroutable virtual
bridge (docker0), producing links whose LAN leg silently failed and forced
devices onto the relay
The tanstack rows sit in a wrapper offset with transform: translateY(), and a
transformed ancestor becomes the sticky containing block — turn headers stuck
to the wrapper's overscan-dependent top edge instead of the scroll container,
floating over the previous turn. Offset the wrapper with padding-top instead:
identical geometry, sticky computes against the scroll container again, and
the padding only changes when the virtual window shifts, not per scroll frame.
The compile fix replaced the direct topEdgeEffect API with KVC casting the
effect to UIView — but UIScrollEdgeEffect is not a UIView, so the cast
silently returned nil and the system's dark edge band stayed visible behind
the status bar. Keep the KVC (compiles with pre-26 SDKs) but toggle the
ObjC 'hidden' key on the effect as a plain NSObject, guarded by respondsTo.
The pairing session is single-use, so it leaving the pending list (polled
every 5s) means it was redeemed — close the dialog and toast success. Armed
only after the pairing has been seen in the pending list, so the stale list
at result-phase open can't blink the dialog shut; expired/cancelled sessions
close it silently. Pending-list polling now preserves the previous list on a
transient fetch failure instead of blanking it (which would also have faked
the redeem signal).
- Connect screen leads with Scan QR code plus a plain-words hint of where the
code lives; manual URL entry is collapsed behind Connect by address (expanded
automatically on web where scanning is unavailable); saved connections show a
per-row connecting spinner
- Instances sheet is list-first: the active instance shows a live status dot
and transport (Connected - Local network / Private relay), rows connect on
tap with an inline spinner, and the add/edit form hides behind Scan QR code /
Add by address
- Deleting the last instance returns to the connect screen: without a runtime
endpoint the native app no longer bootstraps against the webview's own origin
(which faked a successful connection), and the connect screen renders
regardless of a stale isConnected flag
* 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).
Adds a reusable mobile release workflow call from the release pipeline
Passes version, build number, and release tag into Android artifact publishing
Tightens release workflow shell quoting and version handling
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
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.