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.
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.
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.
Adds `mobile:sim:dev` / `sim:dev` to build, install, launch, and stream the iOS simulator in one command
Updates mobile docs and serve-sim guidance with the new workflow and correct preview URL behavior
Bumps `serve-sim` to a newer version
Adds a new docs page explaining the OpenChamber agent control tool and its capabilities.
Includes localized versions for supported languages and adds the page to the docs sidebar.
Updates the lockfile with the new better-sqlite3 dependency.
Users can now attach Microsoft Office documents (.docx, .pptx, and .xlsx) and OpenDocument files (.odt, .odp, and .ods) from the shared web, desktop, mobile, and VS Code chat surfaces.
Document text is extracted locally and sent as a text/plain file part with the original filename, keeping the visible user message clean. Supported embedded PNG, JPEG, GIF, and WebP images are sent as separate image parts, with matching [filename] citations preserved near their source paragraph, slide object, spreadsheet cell anchor, or OpenDocument position. Presentation notes, spreadsheet values, headers, and footers are included where available.
Document expansion is metadata-validated and bounded against oversized entries, excessive uncompressed data, unsafe paths, invalid image signatures, attachment-name races, and dangling citations after truncation. Generated document parts are published to the composer atomically.
Add fflate for worker-backed ZIP extraction and narrowly allow blob workers in the VS Code webview CSP without permitting blob scripts. Include focused fixtures for every supported format, extraction limits, positional citations, collision recovery, atomic attachment state, and CSP behavior.
Improve OpenChamber responsiveness under large session workloads while fixing
cache, synchronization, and persistence correctness across runtimes, projects,
directories, and worktrees.
- prioritize selected and visible sessions during bootstrap and defer
non-critical enrichment work
- reduce redundant message loading, event processing, store publication, and
hidden sidebar work
- prevent stale session and message requests from overwriting newer
authoritative state
- preserve existing data when authoritative fetches fail instead of treating
failures as successful empty responses
- scope session materialization, messages, drafts, queues, todos, pins,
permissions, folders, tabs, Git state, and pull request data by runtime and
directory identity
- harden runtime switching, reconnect, cleanup, mutation reconciliation, and
persisted-state ordering
- preserve live subagent Task linkage when metadata arrives after an older
message request or while streaming parts are suspended
- coalesce overlapping tail refreshes without losing newer refresh demand
- improve cold-session loading by moving deferrable work out of the critical
bootstrap path
- isolate URL authentication, mobile credentials, native secrets, and other
runtime-owned state across endpoint changes
- bound long-lived caches and remove avoidable allocations from event and
rendering hot paths
- limit virtualization to archive collections where it improves rendering
without disrupting active sidebar layout
- stabilize session folders, pin ordering, expanded state, and persisted
sidebar behavior
- open skill files through the same secure editor and outside-workspace grant
flow used by file navigation, including worktree sessions
- expand regression coverage for stale completions, runtime collisions,
reconnect behavior, persistence races, authoritative empty results, and
subagent refresh ordering
- document the updated synchronization, cache ownership, performance, and
runtime-isolation invariants
Updates the SDK dependency across web, UI, and VS Code packages.
Refreshes the lockfile and package versions to match the release.
Adds unreleased changelog notes for the current set of user-facing changes.
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.
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)
- 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
Bundle the official OpenCode CLI into Electron desktop builds instead of relying on whichever opencode executable happens to be first on PATH. Pin @opencode-ai/sdk to an exact version and use that version as the source of truth for the downloaded CLI artifact.
Add an Electron prepare script that maps the current platform/arch to the official OpenCode release artifact, downloads it from GitHub releases, caches the archive under packages/electron/.cache, stages the binary under resources/opencode-cli, verifies opencode --version, and skips work when the staged binary already matches.
Prefer explicit OpenCode binary overrides first, then the bundled Electron CLI, then PATH/system installs. Keep rejecting the Windows OpenCode desktop app executable as a CLI candidate and add resolver tests for bundled priority, explicit override priority, resourcesPath lookup, and desktop-app rejection.
Suppress OpenCode CLI update prompts when the active CLI source is bundled. The server now reports upgrade-status as unavailable for bundled CLI while still returning the current OpenCode version for About, and rejects direct upgrade attempts with a 409 instead of trying to mutate the bundled binary.
Update desktop release, smoke, and manual macOS DMG workflows to prepare and verify the bundled CLI before packaging, verify the packaged app contains the expected CLI, cache downloads by OS/arch/OpenCode version, and align the Windows smoke runner with production windows-2022.
Document desktop bundling behavior, ignore generated CLI/cache files, add oc-dev helpers, and keep Web/VS Code behavior dependent on installed OpenCode CLI rather than desktop bundled resources.
Resyncs active sessions after hidden upstream stream reconnects
Recovers orphaned streaming parts with active-session snapshots
Adds coverage for event-stream reconnect behavior
Adds a cross-platform oc-dev menu for web, mobile, Electron, VS Code, and release workflows
Supports user-level config for remote deploys, iOS device preferences, and maintainer-only release tools
Ports local deploy flows from Bash snippets to Node-native operations
* feat(mobile): add Capacitor native shell
* docs: add serve-sim workflow guidance
* docs(mobile): add implementation handoff
* chore(mobile): clean up generated defaults
* feat(mobile): add connection onboarding
* feat(mobile): manage saved instances
* feat(mobile): refine connection management UI
* chore(mobile): upgrade Capacitor 8
* fix(mobile): reliable saved-instance auth with secure token storage
- store client tokens in the OS secure store (iOS Keychain / Android Keystore)
per instance URL via direct native plugin calls; keep only token-less metadata
in localStorage. Bound every secure call so a stalled bridge can't hang unlock.
- bypass the secure-storage JS wrapper's lazy platform load (which stalled in the
webview) by calling internalSetItem/internalGetItem/internalRemoveItem directly.
- harden the shared connect/unlock controller (health + session + progressive
password) and drop the heavy pre-connect hydration that stalled no-token hosts.
- await token persistence before switching runtime endpoints (no fire-and-forget).
- sync native iOS/Android projects + Keyboard/StatusBar config for Capacitor 8.
* fix(mobile): keep UI stable across connection churn (no transport hardcoding)
The "reload every ~10s" was a UX bug, not a transport one:
- MobileSurfaceShell received a fresh inline onClose each parent render, so any
re-render (e.g. an SSE/WS event) re-ran the focus effect and refocused the first
element — stealing focus from the active input and collapsing the keyboard
mid-edit. onClose now lives in a ref so the focus/keydown effect depends only on
`open`. Fixes all sheets (Instances/Files/Changes/Settings).
- Gate the mobile shell on connectionPhase, not the live isConnected flag, so a
transient reconnect keeps MobileShell mounted instead of flashing the loader.
- Instances form: populate fields imperatively on edit/cancel/save instead of via
an effect keyed on the derived connection, so list churn can't wipe input.
Transport stays on `auto` (WS-first with SSE fallback) — no hardcoded override, so
WS-only Quick Tunnels and SSE-capable proxies both keep working.
* feat(mobile): add native QR pairing-code scanner
Wire the connection onboarding + Instances scan buttons to a real native
scanner via @capacitor-mlkit/barcode-scanning, which registers as the
BarcodeScanner plugin the existing mobileQrScan helper already resolves at
runtime. Add NSCameraUsageDescription and bump the iOS deployment target to
15.5 (GoogleMLKit 8 requirement).
* fix(cli): repair connect-url host resolution
Define the missing isWildcardBindHost helper that connect-url called but was
never declared, which crashed any link generation that reached host
resolution. Also treat a full http(s) --host value as a public server URL so
'--host https://example.com' produces a correct link instead of
'http://https://example.com:port'.
* fix(mobile): make input follow the keyboard across all surfaces
Switch the native Capacitor Keyboard plugin to resize: 'none' and drive the
layout from an --oc-keyboard-inset CSS variable set on keyboardWillShow, which
fires at the start of the iOS keyboard animation. A transition tuned to the
native keyboard curve/duration (0.25s, cubic-bezier(0.38, 0.7, 0.125, 1)) makes
the layout rise together with the keyboard instead of snapping into place after
the built-in 'native' resize finished (~1.5s lag).
The inset is consumed by every surface that can hold a focused input:
- chat shell shrinks its height;
- portal sheets/overlays raise their bottom edge;
- the full-screen connect/login view caps its height so it actually scrolls
(and is now generally scrollable for long saved-connection lists).
* feat(mobile): rounder chat composer + native bottom safe area
Round the mobile chat composer corners a touch more (1rem), and reserve a small
app-level bottom safe area for the native shell via the --oc-app-bottom-safe
token so controls clear the phone's rounded hardware corners. The reservation
folds into the keyboard inset (no gap above the keyboard), and the composer's
own bottom padding tightens while the keyboard is open.
* fix(mobile): remove iOS 26 dark status-bar band; polish composer
The dark band behind the status bar in system Dark Mode was iOS 26's automatic
scroll edge effect (Liquid Glass) dimming the WebView's top edge beneath the
status bar — appearance-coloured, so it tracked the system theme regardless of
the in-app theme. Hide it via UIScrollView.topEdgeEffect/bottomEdgeEffect on the
WebView's scroll view (iOS 26+), and make the WebView non-opaque so the themed
web background shows under the overlaid status bar.
Also: re-assert the status-bar overlay on resume, paint the document canvas with
the theme background in the native shell, round the composer corners to 1.5rem,
and enlarge the app-level bottom safe area so controls clear the rounded corners.
* feat(mobile): logo splash until first paint is final (no FOUT / layout shift)
Cold start flashed the fallback font and then reflowed once the real font and
persisted appearance prefs landed, and text jumped a frame after mount because the
mobile typography classes were applied from a hook effect. Fix it on three fronts:
- apply device classes (device-mobile / mobile-pointer) synchronously in
renderMobileApp before the first React paint, so mobile --text-* sizes are in
effect from the start;
- hold a logo splash (useFontsReady) until the UI web font has loaded;
- gate that splash on appBootReady too, resolved once async appearance/typography
preferences are applied, plus a double rAF so styles commit before reveal.
All under a 2.5s safety timeout so a slow/offline CDN can't block startup.
* feat(mobile): native local notifications; APNs implemented but frozen
The native app now delivers agent ready/error/question/permission events as iOS
(and Android) Local Notifications: a native notifications API backed by
@capacitor/local-notifications replaces the Web Notifications API (which doesn't
display in a WKWebView), driven by the notification SSE stream now subscribed in
the mobile app. Tapping a notification opens its session. Also fix the settings
toggle, which treated the Capacitor app as a browser and gated 'Enable
Notifications' on the absent Web Notification permission, leaving it un-toggleable.
Remote APNs push is implemented end-to-end (dependency-free HTTP/2 + ES256 JWT
server runtime, token routes, client registration, iOS native config) but kept
dormant: config-gated so it never fires, client registration not wired, and the
aps-environment entitlement / background mode removed so the app builds with no
Apple push setup. It will be reused once OpenChamber ships its own encrypted
relay so users don't each configure APNs. See notifications/APNS.md.
WKWebView can't use web push (unlike an installed PWA), so true
background-when-suspended delivery on native requires APNs via that relay.
* feat(mobile): APNs relay-mode background push
Deliver native iOS background push through the central relay: the server posts
device tokens + generic, model-based text to api.openchamber.dev/v1/push/send
(default), which holds the single APNs key and signs+sends; dead tokens (410)
are dropped from the per-session store. Direct APNs (HTTP/2 + ES256 JWT) stays
as a fallback when OPENCHAMBER_PUSH_RELAY_DISABLED=true. The mobile push payload
is generic only (model + scenario) so no session content crosses the relay.
Re-enable the client token registration (useNativePushRegistration) and the
aps-environment entitlement (alert pushes need no background mode). Wired into
the same fanout as web push; focus-suppressed and only when tokens exist.
* fix(mobile): APNs-only native notifications, generic templates, no foreground
Make APNs the single notification channel for the native app and fix delivery:
- Remove local notifications entirely (the @capacitor/local-notifications plugin
and the SSE-driven path). A WKWebView can't tell foreground from background
(document.hasFocus() is unreliable), so local notifications leaked while the app
was open; the in-app dispatch is no-op'd on native.
- Stop gating APNs on UI visibility — a backgrounded WebView can't report 'hidden'
before iOS suspends it, which dropped background push. Instead always send and let
iOS suppress the foreground banner (PushNotifications presentationOptions: []).
- Fix a ReferenceError (out-of-scope 'variables') that crashed maybeSendPushForTrigger
before any push was sent.
- Mobile push text is generic: a scenario title ('Agent response is ready' / 'needs
your input' / 'needs permission' / 'hit an error') + the session name, no model or
message content.
- Hide the focus toggle, templates, and test button in mobile notification settings.
* feat(push): sign relay requests + bind tokens per server
Each OpenChamber server now auto-generates an ECDSA P-256 keypair (persisted in settings,
like the VAPID keys) and uses it to:
- bind every newly-seen device token to the server on the relay
(POST /v1/push/register-token, signed), and
- sign every push send (publicKeyJwk + ts + signature over ts.sortedTokens.title).
The relay derives serverId = SHA-256(publicKey), verifies the signature + timestamp, and
only delivers to tokens bound to that server. Result: a leaked device token alone can no
longer be used to push to a device — the sender also needs the server's private key. Stays
zero-config (the keypair generates on first use). Drops the soft PUSH_RELAY_TOKEN bearer.
* docs(push): describe relay data-confidentiality model
Document that the push payload is not application-encrypted (TLS-in-transit only), what the
relay and Apple can see (generic scenario title + session name, plus token/sessionId), that
the signature is authentication rather than encryption, and what an end-to-end encrypted
payload would require.
* fix: invalid skill description
* feat(push): app-icon badge for native notifications
Send an absolute aps.badge with each native push = the count of distinct
collapse-ids (tag) pushed since the app was last foregrounded, mirroring the
lock-screen banner stack. Cleared server-side on user engagement (session view,
message-sent, visibility beacon) and on-device via sceneDidBecomeActive.
* feat(mobile): auto-connect last instance on launch + notification deep-links
Cold launch silently reconnects to the most-recent saved instance (when reachable
and a token is saved), holding the splash instead of flashing the connect screen;
falls back to the connect screen when there's no saved instance, it's unreachable,
or it needs a re-login. Notification-tap deep-links are now captured unconditionally
(even before connect / on cold launch) and applied once the app is ready, so a tap
opens the target session instead of being lost on the login screen.
* fix(mobile): resolve theme background before first paint on cold launch
The mobile shell entry (mobile.html) had no pre-paint theme step, so a cold
launch flashed the WebView's default light canvas, then the baked
design-system default (.dark { --background: #151313 }) via body.bg-background,
before React's theme system injected the real theme vars. Add a blocking script
that resolves dark/light from the persisted theme + system preference and sets
--background (plus color-scheme and the element background) inline on the root,
so the very first paint matches the resolved theme. Falls back to the default
flexoki backgrounds when no theme has been persisted yet.
* feat(mobile): openchamber:// deep-link foundation + arm64 simulator build
Add a typed deep-link vocabulary (deepLinks.ts: parse/build + DeepLinkIntent)
and a single native navigation layer (deepLinkNavigation.ts) that handles both
the openchamber:// URL scheme (App.appUrlOpen — widgets, Live Activities,
external links) and notification taps, normalising each into an intent. Session
and new-session resolve against the store; shell surfaces (sessions/settings/
views/changes) register handlers. Cold-launch intents stash until the app is
ready. Replaces the push-only useNativePushDeepLink and keeps backwards
compatibility with bare sessionId payloads.
Register the openchamber:// scheme in Info.plist.
Dev tooling: with-mobile-env now honours xcode-select (-p) instead of hardcoding
Xcode.app, so an Xcode beta is used. build:ios:simulator runs a new
ios-sim-build script that temporarily drops the MLKit barcode-scanning pod
(no arm64-simulator slice) so the app builds an arm64 binary installable on
Apple Silicon simulators, then restores the Podfile + Pods for device builds.
QR scanning already degrades cleanly when the native plugin is absent.
* feat(mobile): iOS home/lock/Control Center widgets + push-driven refresh
Add a Widget Extension (OpenChamberWidget) and a Notification Service Extension
(OpenChamberNotificationService), wired into the Xcode project, sharing an App
Group with the app.
Widgets:
- Overview (medium): recent sessions with read/unread dots + four quick actions
(new, status, instances, settings).
- Sessions (large): session list with per-session project label, attention count
and a new-session button in the header.
- Quick Actions (small): New chat pill + status/instances.
- Lock Screen (accessoryCircular x2): brand logo to new session, attention counter.
- Control Center control: brand logo (custom SF Symbol) to new session.
Data: the app writes a session-overview snapshot (attention count + recent
sessions with project labels) to the App Group on scene activate/resign; the NSE
refreshes it from each push (aps.badge + sessionId) so widgets update even when
the app is closed (needs aps mutable-content, added to the server + relay).
Deep links: add openchamber://status (session status panel) and reuse
view/instances; all widget taps route through the existing deep-link channel.
* feat(mobile): large Sessions widget lists 6 sessions with project labels
* feat(mobile): edge-swipe to switch sessions with directional slide+fade
* fix(mobile): keep widgets in sync via reload-on-change + periodic refresh
Widgets sharing the app's WidgetKit reload budget refreshed unevenly, leaving the
large Sessions widget stale (no unread dot / attention count) while medium updated.
Drop the per-call updatedAt from the snapshot, only write + reloadAllTimelines when
the session overview actually changed (so we don't burn the budget on every scene
activate/resign), and give each widget a periodic timeline refresh so a missed
reload self-corrects.
* feat(mobile): Android support — chrome fixes, SSE lock, icon, QR scan
Cosmetics:
- Status bar: on Android inset the WebView below the bar (overlay:false) and
paint it with the resolved theme background + correct content Style, since
Android doesn't feed env(safe-area-inset-top) to CSS.
- Keyboard: skip the manual --oc-keyboard-inset on Android (the window resizes
natively, so applying it double-counted and floated the composer); declare
windowSoftInputMode=adjustResize and disable the shell height transition on
Android so the header no longer bounces on keyboard open.
Transport: lock Capacitor apps to SSE — native WebSocket streaming is unreliable
on Android (events only arrive once a run finishes). Forced in sync-context and
the other options are disabled in the Chat settings UI.
Push: gate APNs registration to iOS only; on Android @capacitor/push-notifications
register() needs Firebase/FCM (not configured) and crashes at launch.
QR pairing: declare CAMERA permission + the ML Kit barcode_ui dependency, and
install/await the Google barcode scanner module (with a post-install retry) before
scanning so the first scan works without a manual retry.
Icon: Android adaptive launcher icon generated from the cube logo (full-bleed
white background, no edge artifact on One UI). Source assets under mobile/assets.
Tooling: adb-based android-device.mjs + android:* scripts for device deploy.
* feat(notifications): presence-aware push routing (don't spam the phone)
Only push to a device when the notification would otherwise be missed there. A
notification is suppressed on devices where the user is already present.
- Tag every client's visibility beacon and web-push subscription with a platform
('ios' | 'android' | 'vscode' | 'desktop' | 'web') via getClientPlatform().
- Server tracks visibility per client (keyed by oc_ui_session) with the platform,
and exposes isAnyInteractiveClientVisible() = any visible non-mobile client.
- Native push (APNs) and mobile PWA web-push are now suppressed when an
interactive (desktop/web/vscode) client is visible — it already shows the
in-app notification. Gated on the desktop's visibility (reliable), never the
phone's own (a backgrounded WKWebView can't report "hidden").
- Desktop/web web-push keeps the any-visible gate (a visible client absorbs it).
- Skipping APNs also skips the badge increment so it doesn't drift.
Fixes the case where every session on a shared instance pushed to the phone even
while the user was actively working on desktop.
* feat(mobile): Android FCM push notifications
Enable native background push on Android via Firebase Cloud Messaging, in parallel
with the existing iOS APNs path.
- Add google-services.json + declare POST_NOTIFICATIONS (Android 13+). The Google
Services Gradle plugin is applied when the file is present, so register() returns
an FCM token instead of crashing.
- Un-gate native push registration to iOS OR Android, and tag the registered token
with its platform ('ios' | 'android') so the relay routes it to APNs vs FCM.
- Server stores the platform per device token and binds it to the relay (platform
included in the signed register message).
- Notification small icon: monochrome cube silhouette with a mark on the top face,
set as the FCM default_notification_icon so the status-bar icon reads as the logo.
Relay-side FCM sending ships in openchamber-website.
* docs(mobile): refresh HANDOFF with current state, dev/deploy process, and CI gap
* chore(mobile): iOS store-review prerequisites (privacy manifest, encryption flag)
- Add the app's PrivacyInfo.xcprivacy (no tracking; required-reason UserDefaults for the App
Group snapshot shared with the widget + notification service extension) and wire it into the
App target's resources — Apple requires an app-level privacy manifest.
- Set ITSAppUsesNonExemptEncryption=false to skip the per-build export-compliance prompt.
- HANDOFF: add a store-review-readiness checklist (in-repo vs release-time console/infra items).
Verified: plist lint, xcodebuild parse, and an iOS simulator build with PrivacyInfo.xcprivacy
bundled into App.app.
* refactor(mobile): dedupe capacitor detection + make beacon guard explicit
Addresses non-blocking PR review notes:
- Consolidate the repeated Capacitor-native check (mobileConnections, deepLinkNavigation,
usePushVisibilityBeacon each redefined it) onto the single isCapacitorApp() in lib/platform.
- usePushVisibilityBeacon now guards on isWebRuntime() OR isCapacitorApp() instead of relying on
isWebRuntime() being true for Capacitor, so the beacon can't silently stop if that changes.
react-syntax-highlighter (and its @types) is no longer imported anywhere —
code highlighting is handled by the Shiki web worker. Drop the dead deps.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Adds a 'Cron' schedule type option to the scheduled task editor, allowing
users to create and edit cron-based schedules through the UI.
- Add cron expression input with inline validation (cron-parser)
- Show next 4 upcoming run times as a preview
- Provide clickable example chips (every 5min, hourly, Monday 9am, etc.)
- Preserve cron expressions when editing existing cron tasks
- Add cron.ts utility module for validation and next-run computation
- Add i18n keys across all 8 locale files
Closes#1586
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
Route all non-markdown code highlighting through the off-main-thread Shiki
worker, removing react-syntax-highlighter and prismjs entirely.
- Extend the worker with highlightLines: tokenize a whole block once and return
per-line inner HTML, so per-line layouts (diffs, gutters, virtualization) make
one worker call instead of one highlighter per line.
- Add shared WorkerHighlightedCode (whole-block) and useWorkerHighlightedLines
(per-line) primitives. Colors resolve via the --md-syntax-* CSS variables, so
theme changes never re-highlight.
- Migrate all 12 react-syntax-highlighter call sites: PermissionCard,
ToolPart, ContextSidebarTab, ToolOutputDialog (whole block) and
DiffPreview/WritePreview (per line).
- Migrate VirtualizedCodeBlock off prismjs to the worker, keeping virtua
virtualization; whole-block tokenization also restores cross-line syntax
context that per-line highlighting lost.
- Drop react-syntax-highlighter (+types) from ui and web, prismjs (+types) from
ui, and the orphaned create-element type shim.
Tokenize closed code blocks in a dedicated Shiki Web Worker instead of calling
the shared highlighter synchronously on the UI thread. This removes the
one-shot main-thread highlight stall when a code fence closes on a large block.
Streaming behavior is unchanged: the open (streaming) fence still renders as
plain text and is highlighted once on close. On any worker failure the block
keeps its escaped plain code — highlighting never falls back onto the main
thread.
- Add markdownShikiThemeDefinition (dependency-free CSS-variable theme) so the
worker can use the theme without pulling in @pierre/diffs / React.
- Add markdown-worker-protocol, markdown-shiki.worker, and the main-thread
markdown-worker client.
- Route highlightCodeBlocks through the worker; keep the size/VSCode line guard
and mermaid skip on the main thread.
- Add shiki as a direct dependency (was transitive via @pierre/diffs).
Upgraded @opencode-ai/sdk dependency from ^1.17.0 to ^1.17.7 across all packages
Added unreleased changelog entries for VSCode startup parity, mobile tool card fix, and files workspace directory fix
Refined VSCode changelog to remove inaccurate project-level actions note
* feat: add draw.io diagram editor integration
Embed draw.io editor via react-drawio (MIT, zero deps) for inline
editing of .drawio files. Changes auto-save to disk. Includes
inline editor in FilesView with Visual/Source toggle, dark mode
support, template picker for new files, and chat file attachment
integration.
* fix: debounce diagram autosave to prevent reload loop
* fix: ignore watcher-triggered xml prop changes to prevent reload loop
* fix: remove auto-save-to-disk, add manual save button for diagrams
Autosave writes triggered file watcher cascade that reloaded the
draw.io iframe and reset zoom. Replaced with explicit Save button
in the toolbar (floppy disk icon). Editor XML is stable on mount
and ignores watcher-triggered prop changes.
* fix: remove auto-save write from DiagramView, add save button
* fix: hide draw.io save/exit buttons in editor
* fix: also hide save-and-exit button
* fix: brighten save button styling, add saved confirmation
* fix: remove autoSaveStatus toggle on diagram save to prevent toolbar collapse
* fix: add local save confirmation state for diagram button
* fix: remount drawio iframe on theme change, persisting XML across mounts
* fix: clear persisted xml on mount to prevent leaking between files
* fix: initialize dark mode synchronously, preserve edits across theme remount
* fix: auto-focus drawio iframe on mount/theme-change for keyboard shortcuts
* fix: add diagram i18n keys to Traditional Chinese locale
* fix: restore upstream HMR host and LAN address support
* fix: load sub-agent sessions on bootstrap for sidebar visibility
Two-phase session load: first fetch root sessions (for accurate
sessionTotal), then fetch all sessions and include child sessions
(sub-agent delegations). This ensures sub-agent sessions appear
in the sidebar immediately instead of relying on the async global
session store.
* remove opencode-drawio from PR branch
* fix: atomic file writes to prevent concurrent read/write truncation
Three-layer defense against the O_TRUNC race:
1. Write side (server): replace direct writeFile with write-to-temp-
then-rename. fs.rename is atomic on POSIX.
2. Read side (server): retry up to 3 times with 50ms backoff when
readFile returns empty but stat reported non-zero size.
3. FilesView client: refuse to save empty draftContent when the
original fileContent was non-empty.
* fix(dev): clean up orphaned OpenCode processes on Ctrl+C
* fix: allow empty file saves, log warning instead of blocking
Replaces the hard block on saving empty content with a console.warn.
The atomic write + read retry on the server side handle the O_TRUNC
race properly. The previous guard caused a UX regression by silently
preventing users from clearing a file and saving.
* fix: remove time window from sub-agent fallback for live tasks
While a task tool is active, the fallback now matches any session
with the correct parentID regardless of creation time. This allows
late-appearing child sessions to be found when the OpenCode server
is slow or the SSE event pipeline is delayed. The time window is
still applied once the task tool has completed, as a final sanity
check.
* fix: three diagram editor bugs from Greptile review
1. stableXmlRef now resets when xml prop changes — switching
between .drawio files renders the correct content.
2. Focus effect only runs on mount, not on isDark changes —
theme toggle no longer steals keyboard focus 600ms later.
3. saveDiagram updates xml state after writing — dirty-check
guard works correctly for subsequent saves.
* fix: route session.created SSE events to correct directory
Three-layer fix for sub-agent sessions not appearing in sidebar and
inline chat:
1. protocol.js: parseSseEventEnvelope now extracts directory from
properties.info.directory (where session.created/updated events
carry it) in addition to properties.directory. WS frames relayed
to the browser now carry the real directory instead of 'global',
so child sessions routed to the correct directory store.
2. event-pipeline.ts: same fallback in resolveEventDirectory for
defense-in-depth when SSE events bypass the WS relay.
3. resolveFallbackTaskSessionId.ts: time window lower bound now
allows 2s grace before taskStartTime to accommodate server timing
jitter (child session creation timestamps consistently precede the
tool's recorded start by ~6-9ms), fixing the 'Open subtask'
button not rendering in OpenChamber's inline chat.
* fix: sub-agent sidebar visibility, file zeroing guard, inline badge fallback
- Sync watchdog: periodic child session discovery poll (every 15s) detects
sessions created by other OpenCode instances, triggers parent materialization
- protocol.js: parseSseEventEnvelope extracts directory from
properties.info.directory for session.created/updated events
- event-pipeline.ts: same fallback in resolveEventDirectory for defense-in-depth
- resolveFallbackTaskSessionId: don't require taskStartTime (cross-OpenCode);
pick most recent child when multiple idle candidates exist
- readTaskSessionIdFromOutput: parse <task id="ses_xxx"> format from output
- FilesView: reinstate empty-draft guard (block save when draftContent='' but
fileContent had content) to prevent file zeroing on tab switch
* Fix diagram autosave reload loop
* Highlight drawio files as XML
* Use diff-compatible highlighting for drawio files
* Restore drawio file icon mapping
* Stabilize drawio source preview toggle
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
Preserves session directory metadata across live updates
Keeps desktop and mobile session lists in project groups
Adds regression coverage for session grouping
Electron updater now uses Electron release metadata only
Removed legacy Tauri package and migration workflow
Replaced Tauri shim usage with the desktop bridge