Commit Graph
68 Commits
Author SHA1 Message Date
Bohdan Triapitsyn 094f728777 perf(ui): swap the session activity spinner for a dot and a turn timer
The spinner ran a CSS animation on every active row for the whole turn,
repainting a composited layer at frame rate. Rows now carry a static dot —
primary while running, info while unread — and the metadata slot on the right
shows how long the turn has been going, updating once per second in the dot's
colour. The counter is the motion the spinner used to provide, at 1 fps.
Collapsed groups, folders and projects take the dot only, since one counter
cannot speak for several running turns.

Elapsed time is measured client-side because SessionStatus carries no
timestamps, and starts are persisted so a reload resumes the same count. Two
rules keep that honest. Only a liveness stamp — refreshed while a session is
observed active, stamped as the page hides, and compared against the page's
navigation start so a slow bootstrap is not charged to the absence — and a 90s
adoption window may expire a record; a snapshot that cannot yet see a session
is not evidence its turn ended. And a busy event is never read as a turn
boundary, because OpenCode republishes busy at every step of the agent loop, so
after a reload one of those repeats normally beats the first status snapshot.
Idle and error events do end a turn, and retire the record with it.

Snapshot reconciliation walks the running turns and asks whether the snapshot
covers each one, rather than being handed everything it covers: only a live
start can settle, so the pass scales with timing work instead of with the
directory's session list, and allocates nothing per poll.

Also applied to the mobile sessions sheet and session switcher. The shared
duration ticker moves to hooks/ now that it has a second consumer.
2026-08-05 03:06:35 +03:00
Bohdan Triapitsyn e9b957ffd2 feat: add info button variant and reuse it in mobile sessions sheet
Adds a new info-styled button variant for consistent secondary actions.
Updates mobile sessions sheet actions to use the shared Button component.
Cleans up the mobile footer buttons with the shared sizing and styling system.
2026-08-04 01:54:55 +03:00
Bohdan Triapitsyn 39a321a29b fix(mobile): widen Android edge swipe zones 2026-08-02 19:31:22 +03:00
Bohdan Triapitsyn a96a2310a9 fix(mobile): support QR scanning without Play Services 2026-08-02 19:09:46 +03:00
Bohdan Triapitsyn 96c011a8ef feat(mobile): tablet layout pass and foldable-ready size class (#2569)
The tablet ran the phone layout with a half-finished iPad draft on top: two
custom sidebars, a leftover overflow menu, split Files/Changes header buttons,
and phone-width sheets stretched across a 13" screen. This brings it onto the
phone's navigation model and keeps only the differences a large screen earns.

- Sessions are a persistent resizable left sidebar; the overflow menu is gone
  and its destinations moved into that sidebar's footer (connected instance,
  settings, pending web update) and into the workspace drawer.
- The workspace (Changes / Files / Terminal / Notes / MCP) is the phone's
  drawer everywhere: a resizable right sidebar where the screen can host one
  (up to 900px) and the full-cover drawer otherwise, with its mounted panes —
  an open diff, an edited file, an attached terminal — surviving rotation.
- Header dropdowns are anchored popovers: the recents switcher mirrors the
  usage overlay on the left, and its trigger is sized to the title rather than
  to the free width.
- App-level pages (settings, instances, update, an opened plan) render as
  centered dialogs instead of covering the screen.
- Overlays center on the chat column through published insets, so the model
  and directory pickers no longer sit off-centre; the directory picker also
  stops overriding the shared width clamp.
- Wide chat layout applies to mobile surfaces, where a tablet chat column is
  finally wide enough for the setting to mean anything.

The layout gate is a live size class rather than a device check, so Android
tablets and foldables are covered by the same code:

- `enabled` when the shortest viewport side is at le
  sw600dp). The short side is what makes this a size question instead of a
  device question — a phone reports ~360-430 whichev
  unfolded book foldable ~600+, and folding shut drops back under it. iPads
  also answer on identity, since iPadOS hands out od
- `roomyForPanels` when landscape and at least 1000px wide, which is what it
  takes to host the sidebar, the panel and a readabl
  foldables miss it in BOTH orientations — their long side is barely wider
  than a tablet's short one — so they keep the portr

Every consumer re-decides instead of remembering wha
open sidebar closes if the device folds shut under it. iPad behaviour is
unchanged: its landscape widths all clear the panel
ones do not, exactly as the previous orientation check did.

Hardware keyboards are read natively. iOS reports them through GCKeyboard,
published to the web layer at document start and kep
disconnect and foregrounding; the layer stops inferring once that answers. A
single early publish was not enough — the connect no
already-attached keyboard fires before the page exists, and GameController can
populate late — so the state is re-published across
resume. With a keyboard attached the draft screen keeps its starter chips and
the composer never collapses; tablets skip the colla
Runtimes with no native answer fall back to inferring it from the keyboard
bridge, and only ever conclude "hardware" from silen

Also: sidebar rows no longer sit on a differently ti
footer is no longer clipped by an over-tall content box, the resize handles
moved above the panes' own overlays so they can actu
now-unreachable overflow menu, fullscreen terminal/MCP/notes surfaces and their
locale key are deleted.

Device behaviour is unverified — the tablet layout,
keyboard bridge and the foldable size class have not been exercised on
hardware, and the 600/1000 thresholds are derived fr
rather than measured on a foldable.
2026-08-02 16:25:16 +03:00
Serhii Dziupin 5d24d6cb2a fix: dedupe shared worktree ownership
Cherry-picked from claude branch 7b3bec48 (fix: dedupe shared worktree ownership and title sessions immediately).

Only the worktree-dedupe half applies to main: partitionWorktreesByRegisteredProject and its callers (SessionSidebar, ElectronMiniChatApp, MobileApp, MobileSessionsSheet) plus tests and sidebar docs.

The session-title half (title sessions immediately) remains on the claude branch: the session-title runtime and its claude-code transcript translator imports do not exist on main and cannot build there until the Claude harness lands.
2026-08-02 09:34:49 +03:00
Bohdan Triapitsyn 86ef96302d feat(mobile): mobile app navigation rework and beta-feedback closeout (#2561)
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.
2026-08-01 21:16:36 +03:00
Bohdan Triapitsyn 7a16290ff3 fix(mobile): ignore cancelled password completions
Track each native mobile password-unlock operation with a monotonic generation. Cancelling the password prompt immediately invalidates the active operation and releases the busy state.\n\nEvery asynchronous login boundary now verifies that its operation is still current before reporting errors, writing a client token, persisting connection metadata, switching the active runtime, or notifying the connected surface. A stale relay login still closes its unadopted tunnel, while its finally block cannot clear the busy state of a newer password attempt.\n\nAdd a focused regression test that models a deferred password completion after cancellation and verifies it cannot apply the runtime-switch side effect.
2026-07-30 17:43:39 +03:00
Cursor AgentandSerhii Dziupin 5b727ed53c fix(files): prevent autosave data loss on load lag and binary files
Guard FilesView autosave until the selected file has finished loading,
refuse binary/PDF/office/archive text saves, and add a persisted global
autoSaveEnabled setting (default true) under Settings → General.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-07-30 06:37:47 +00:00
𝖎𝖚𝖑𝖎𝖎𝖆andBohdan Triapitsyn c88dd16d2a fix: prevent bundled OpenCode self-upgrades (#2525)
* fix: prevent bundled OpenCode self-upgrades

* feat(vscode): support OpenCode upgrades

* fix: refresh OpenCode update status on runtime switch

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-07-29 19:59:41 +03:00
Bohdan Triapitsyn c792e1f4f4 fix: load mobile image files via runtime fetch (fix for relay)
Fetch image blobs directly for mobile file previews
Create and clean up object URLs for loaded images
Remove the separate image auth token flow
2026-07-29 10:11:35 +03:00
Bohdan Triapitsyn 9f1bd0dfa0 fix: route APNs delivery per-token by registered environment
Issue: after defaulting APNs delivery to production (#2381), development
builds installed from Xcode stopped receiving notifications entirely:
their sandbox device tokens were sent to the production APNs endpoint,
rejected as BadDeviceToken, and dropped as dead.

Fix: the iOS shell reads the aps-environment entitlement from the
embedded provisioning profile and exposes it to the web layer as a
document-start user script (added in capacitorDidLoad, since Capacitor
replaces the userContentController after webViewConfiguration(for:)).
Token registration reports the environment to the server, which stores
it per token and groups delivery by environment for both relay and
direct APNs sends. OPENCHAMBER_APNS_ENVIRONMENT remains as an explicit
override forcing every send to one environment.

TestFlight/App Store builds and older clients without the field default
to production, preserving released behavior; the relay already accepts
env per send request.
2026-07-25 01:18:41 +03:00
Bohdan Triapitsyn 89f7c37d60 fix: make session ordering follow activity lifecycle
Session lists now promote a conversation when it starts working and again when it settles, instead of reacting to every streaming timestamp update. This keeps ordering responsive without bringing back the sidebar churn removed by the recent performance work.

Apply the same user-visible order across Recent, project and worktree groups, session switchers, mobile navigation, widgets, the command palette, and the desktop tray. Preserve pinned priority, freeze timestamp fallback ordering, and keep child-session activity scoped to siblings under the same parent so it never moves the root conversation.

Seed reconnect snapshots without synthetic jumps, clear ephemeral ranks on deletion and runtime changes, and cover lifecycle transitions, mixed root/child trees, metadata-only updates, and project-group ordering with regression tests.
2026-07-23 15:45:53 +03:00
Bohdan Triapitsyn 85400459e9 perf: overhaul session loading, caching, and runtime isolation (#2360)
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
2026-07-21 20:52:20 +03:00
17f1b24709 Standardize Settings layout and save feedback (#2122)
* Group settings navigation menu

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Revert "Group settings navigation menu"

This reverts commit 5983a4e82074b8dab1084af1cadd803ba28ea65d.

* Standardize settings layout feedback

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Fix settings save status timer typing

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Retain settings save status

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Report color mode save state

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Match Appearance settings to two-column layout

Rebuild Appearance into Color mode & Theme, Localization, and Density & type sections with responsive two-column grids, consistent section headers, page description, and green save status.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Migrate settings pages to SettingsPageLayout and SettingsSection

Replace ScrollableOverlay/max-w-3xl shells with the shared settings
layout primitives across entity and static settings pages, normalize
section headers, and add settings.page.behavior.description locales.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Unify remaining settings pages on shared section chrome

Wire Appearance, Projects, and Remote Instances through SettingsSection/SettingsPageLayout so every settings surface shares the same header, divider, and page shell treatment.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Align settings UI with OpenChamber design system kit

Apply kit geometry and control specs: 840px content width, 32/48 padding, fixed 260/280 sidebars, radius/spacing tokens, settings select height, stepper dimensions, and shared field/link typography.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Migrate OpenChamberVisualSettings to shared settings primitives

Replace ad-hoc radio/checkbox/chip/field layouts with SettingsSection
shared chrome for Appearance, Density, Navigation, Chat/behavior, and
Privacy while preserving handlers and data-settings-item anchors.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Migrate settings pages to shared field/checkbox/radio primitives

Unify Defaults, Notifications, Behavior, Git, Session Retention, Passkeys,
OpenCode CLI, Commands, About, Keyboard Shortcuts, and Desktop Network on
SettingsFieldRow / SettingsCheckboxRow / SettingsRadioGroup / SettingsChipGroup
for consistent grid, spacing, and DRY layout. Also remove the GitPage double
SettingsSection wrap around GitHubSettings.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Unify remaining settings pages onto shared field primitives

Migrate Agents, Snippets, Skills, Usage, MCP identity rows and selects to
SettingsFieldRow/CheckboxRow/ChipGroup and SETTINGS_SELECT_*; align page
titles; light-touch Voice/Tunnel/Providers/Plugins without rewriting
complex OAuth, permissions, or tunnel flows.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Polish settings section dividers and transport helper text

Slightly stronger section borders for clearer group separation, and keep
message-stream transport description under the chip control.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Fix settings grid alignment, control heights, and Chat section titles

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Tighten settings grid: flat Chat 2x2 and full-width stacked selects

Message options use a flat two-column grid so row headers share a baseline.
Stacked selects fill their column; field-row selects keep a fixed sm:w-56 width.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Align mixed settings controls to shared FieldRow/CheckboxRow grid

Separate FieldRows from CheckboxRows with SettingsInset, move enum
radios into ControlGroups, and convert misplaced StackedFields to
full-width FieldRows so left edges no longer clash.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Translate Behavior response-style preset labels for es and pl

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Group settings nav into categories and improve icons/order

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Fix settings layout: fewer hrs, shared control widths, chat transport section

Remove SettingsInset top borders, align App install/Density controls to full cluster width, give Message Stream Transport its own Chat section, and fold Sessions Small Model into the first section to cut extra dividers.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Align Voice and MCP settings labels with shared heading classes

Swap form field labels to SETTINGS_FIELD_LABEL_CLASS and use
SettingsGroupTitle for MCP control-group headings (manual auth fallback,
request headers).

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Differentiate settings heading levels by context

Page titles are larger and quieter than section titles; group and field
labels use dedicated shared classes so hierarchy is consistent across
settings surfaces without ad-hoc typography mixes.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Use shared settings title classes in SettingsView home

Wire home and unavailable headings through the shared L1/L2 class
constants so they stay aligned with SettingsPageLayout.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Keep settings number steppers from stretching +/- buttons

Density & type NumberInputs no longer flex-grow across the row, and
NumberInput locks minus/plus to fixed width so the plus side cannot
inflate when the control is placed in a full-width cluster.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Place spacing and input-bar offset on their own row

Density & type now lays out as font families, then font sizes, then
Spacing Density / Input Bar Offset on the row below.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Regroup settings nav and improve Voice layout

Drop Advanced/Usage/Git nav categories: Voice and About under
Interface, Usage under OpenCode, Git under Workspace. Voice provider
chips and STT model cards use shared settings primitives with roomier
spacing and a two-column model grid.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Space out chat feature groups in visual settings

* Polish settings: save-state wiring, container-query layouts, unified dropdown triggers

- Wire shared save indicator into Behavior page saves and git identity CRUD
- Convert settings layout primitives and page rows from viewport to container
  queries so narrow panes stack instead of clipping
- Unify custom dropdown triggers (model/agent/provider pickers) and remaining
  page selects on the settings control size
- Bump global radius scale by 1px; align variant input with select size
- Migrate stray raw controls (MCP OAuth checkbox, worktree remove button,
  git identity dialog rows, skills dialog labels) onto shared primitives
- Make settings nav items span full sidebar width; trim nav to 240px

* Add General settings page, regroup nav, cap control widths, promote chat feature headers

* Mobile settings nav: plain background and touch-sized rows

* Hide secondary settings descriptions behind clickable info hints

* Move quota credentials to Usage, navigation settings to General, rename External Tunnel

* Quiet settings save indicator: silent success, delayed spinner, visible errors

* Rewrite settings-ui-patterns skill around shared primitives and refactored conventions

* Remove settings starter page; open last visited page, defaulting to General

* Settings polish: spacing, control sizes, trigger widths, readable model names, device dates

* Centralize dropdown trigger chrome, settings nav polish, mobile-only input bar offset

* Fix global line-height regression, auto-hide first-section divider, shortcut row spacing

* Global line-height 1.45, align scheduled tasks header button with select

* Hide editor toolbar and About in VS Code, animate chat render preview outside desktop dialog

* Rebuild agent tool permissions on source-of-truth model

Edit the agent's own permission map verbatim (inherit vs explicit actions, pattern rules only for pattern-capable keys), save permission-only, drop the server-side non-wildcard re-merge that resurrected deleted rules, and surface session-granted rules as read-only.

* Agents model parameters polish: row spacing, variant dropdown, unified widths, dash for unset numbers

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-07-18 00:11:05 +03:00
Bohdan Triapitsyn d4a8c4d2e1 feat(terminal): refactor runtime and add mobile workspace (#2280)
Replace the legacy terminal flow with a shared authenticated WebSocket
runtime used across web, desktop, relay, and mobile surfaces.

- introduce the v3 terminal protocol with scoped attachments, snapshots,
  ordered output, bounded replay history, reconnects, and explicit lifecycle
- harden PTY creation, restart, resize, close, force-kill, idle cleanup,
  shell selection, login mode, environment sanitization, and appearance sync
- add runtime-aware terminal APIs with relay authentication and Electron parity
- add a fullscreen mobile terminal workspace with touch scrolling,
  long-press selection, safe-area controls, quick keys, and Ctrl/Alt input
- add terminal selection attachments, preview detection, project actions,
  shell settings, and localized UI
- harden Ghostty rendering, resize recovery, Unicode handling, block
  characters, line height, and stale-row behavior
- remove the obsolete terminal SSE path and update reverse-proxy guidance
- expand terminal runtime, transport, input, selection, and store coverage
- avoid duplicate web builds when preparing mobile assets in root CI builds
2026-07-17 13:17:21 +03:00
Bohdan Triapitsyn 9b645f0e53 perf: fast relay connect on mobile and desktop + connect splash + edit-safe instances
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
2026-07-13 01:29:35 +03:00
Bohdan Triapitsyn 04307e163b fix: single relay host per machine via cooperative claim lock
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
2026-07-13 00:36:00 +03:00
Bohdan Triapitsyn afb368e11b feat: connection candidates refresh + relay identity hardening
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)
2026-07-12 18:09:54 +03:00
Bohdan Triapitsyn d738d41574 feat: persist permission auto-accept on server (#2158)
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
2026-07-12 15:03:16 +03:00
Leonidandbashrusakh 6d7ea82d86 perf(worktree): skip unchanged store updates and content-aware persist (#1992)
* perf(worktree): skip unchanged store updates and content-aware persist

- Add content-aware equality check before setState in all three discovery
  loops (SessionSidebar, ElectronMiniChatApp, MobileApp). Compares
  Map size and per-entry length + element references — avoids triggering
  16+ subscriber re-renders when discovery finds the same worktrees.
- Add content-hash guard to persistWorktreeMap subscription with try-catch.
  Avoids redundant localStorage writes when the Map reference changed but
  the content is identical. Serialization errors are caught and skipped.

Contributes to #1990

* perf(worktree): extract shared worktreeMapsEqual, fix comparison, avoid double serialization

- Extract worktreeMapsEqual() into worktreeManager.ts as a shared utility
  comparing worktree maps by path (not reference identity). This replaces
  the inline reference-comparison logic in all three discovery loops
  (SessionSidebar, ElectronMiniChatApp, MobileApp) that was ineffective
  because readStableProjectWorktrees creates new object instances on
  each call after cache expiry, making item !== value[i] always true.
- Pass pre-serialized JSON to persistWorktreeMap to avoid double
  JSON.stringify on every persist. The subscriber already computes the
  serialized string for the content-hash check; pass it through instead
  of re-serializing inside persistWorktreeMap.
- Deduplicate 3 copies of the same comparison logic into the shared util.

* refactor(worktree): make worktreeMapsEqual generic over path-bearing type

The helper's equality contract is element-wise path comparison,
not anything specific to WorktreeMetadata. Generifying on
`T extends { path: string }` documents the contract at the type
level and keeps it reusable for any future map-of-arrays shape
that has a path field. Call sites stay compatible since
WorktreeMetadata has a required `path: string`.

No runtime change.

* refactor(worktree-store): clarify persist hash name and signature

Drop the optional preSerialized parameter from persistWorktreeMap —
its only caller (the subscriber) already builds the serialized
string for the content-compare, so the dual-path body is dead code.
persistWorktreeMap now takes the serialized string directly.

Rename lastPersistedWorktreeHash → lastPersistedWorktreeSerialized
(the variable holds the full JSON string, not a hash) and drop the
try/catch around JSON.stringify: it cannot realistically throw on
Map.entries() of WorktreeMetadata (no circular refs, no BigInt, no
custom toJSON). The try/catch around setItem stays — it can throw
on quota errors.

No behavior change in the success path.

* docs(worktree): trim repeated call-site comments

Replace the 5-line explanation block (copy-pasted in all three
discovery loops) with a one-liner that points at the worktreeMapsEqual
JSDoc. The '16+ subscribers' framing is also dropped — the helper
itself is general-purpose and the precise number was fuzzy.

* fix(worktree): compare branch in worktreeMapsEqual to avoid stale sidebar label

The helper compared entries by path only. An external git checkout
between discoveries changes branch (and the derived label /
headState) while path stays the same, so the helper returned true
and the store update was skipped — leaving a stale branch label in
the sidebar until the next worktree create/remove or project switch,
since there is no periodic worktree-list refresh.

Compare branch in the inner loop alongside path. Tighten the generic
constraint to T extends { path: string; branch: string } so the
contract is documented at the type level.

worktreeStatus is intentionally NOT compared: status transitions go
through setStoredWorktreeStatus, which writes a fresh Map reference
that the persist subscriber picks up directly. Adding worktreeStatus
to the contract would also force the sidebar to detect status changes
that the persist path already handles, and would couple this helper
to a field whose semantics differ from the discovery path.

Fixes the staleness concern raised by openchamber-bot in PR #1992.

* test(worktree): cover worktreeMapsEqual edge cases

Documents the helper's equality contract and guards against
regressions in the path+branch comparison. Eight cases:

- two empty maps
- identical entries (path and branch match in order)
- same path, different branch — the F1 regression case
- different paths at the same index
- per-project array length mismatch
- project-key count mismatch
- positional reorder (helper is order-sensitive)
- non-first-entry branch difference (subset detection)

All 10 tests in the file pass (2 existing + 8 new).

* ci: retrigger checks

* test(worktree): add benchmark for worktreeMapsEqual and persist path

Documents the actual cost of the PR #1992 optimizations on representative
sizes (1-1000 worktrees per project, 1-50 projects), so future contributors
can reproduce the numbers and detect regressions in the equality helper or
the persist subscriber.

Run with: `bun run packages/ui/src/lib/worktrees/worktreeManager.bench.ts`

Measured on V8 (one example run):
- worktreeMapsEqual early-exit (50×20 with first project differing):
  412 ns/op vs 33,034 ns/op full sweep — ~80x speedup when any project
  actually changed.
- F1 path+branch overhead vs path-only (10×50): +2.3 µs (+15.8%) on a
  full sweep; on the early-exit path the F1 cost is irrelevant.
- Stringify dedup in persistWorktreeMap subscriber: 67% saved (552 µs
  per persist on 10×50). This is the main absolute win of the PR.
- Content-compare guard: 19-29 ns/op, free relative to the stringify it
  gates.

Bench file is standalone (import.meta.main guard) — does not run as part
of `bun test`, does not import React, does not touch localStorage.

---------

Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
2026-07-11 16:06:58 +03:00
Bohdan Triapitsyn 33cbd0b836 fix(mobile): pad Android app below the status bar on Android 15
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.
2026-07-10 20:25:48 +03:00
Bohdan Triapitsyn e74834a739 feat(mobile): redesign connect screen and instances sheet
- 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
2026-07-10 01:44:42 +03:00
Iuliia Ivashko 91a95bfdaa feat: pairing v2 — one-tap trusted devices over LAN and private relay (#2103)
Reworks how devices connect to an OpenChamber server, end to end.

Pairing v2:
- One-time pairing links/QR codes (openchamber://connect?v=2) carrying a set of transport candidates (LAN/tunnel/relay) and a single-use secret redeemed server-side; no tokens embedded in links
- Add-a-device dialog written for first-time users: intent-based transport choice (Anywhere / Home network only / This computer only) with plain-language descriptions, transparent fallback checkboxes, server-authoritative LAN detection, high-res QR dialog
- Private relay folded into pairing as a transport candidate with a demand-driven lifecycle (enables when a relay device is paired, disables when none remain)

Multi-transport devices:
- A saved device holds all its transports and one token; mobile re-probes on connect, resume, and network change and hot-switches LAN<->relay seamlessly (no re-pairing, no remount, session preserved)
- Desktop can import relay pairing links, switch to relay hosts through the E2EE tunnel, and restore a relay default host after relaunch

Device management:
- Device list (web + desktop) shows live per-device connectivity with the active transport (Connected - Local network / Relay) and platform badges (iOS/Android/macOS/Windows/Linux)
- One physical device = one record: stable per-install dedupe keys across pairing and password re-login; typed pairing label names the device, paired devices name the connection by the issuing server hostname
- Trusted desktop-local client manages all devices (list, revoke, clear revoked); relay host reaps dead client sockets after 3 missed keepalives

Android:
- LAN transport unblocked (cleartext + mixed content, mirroring iOS ATS exceptions); resume re-probe retries through network flux and silently auto-reconnects from a disconnected state
2026-07-10 00:12:33 +03:00
Bohdan Triapitsyn ac93a52e21 feat: iPad split layout for the Capacitor app (#2104)
* 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
2026-07-08 21:52:18 +03:00
Bohdan Triapitsyn 859b4529da feat: add private relay for end-to-end-encrypted remote access (#2087)
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
2026-07-08 03:44:02 +03:00
Bohdan Triapitsyn 40dfff4a9a fix: handle ambiguous prompt transport failures 2026-07-07 19:49:11 +03:00
Bohdan Triapitsyn 1c44146a4f feat: show android update toast 2026-07-07 13:49:26 +03:00
Bohdan Triapitsyn a6edc7baee fix(capacitor): validate mobile connection on app resume
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
2026-07-05 23:51:11 +03:00
Leonidandbashrusakh ec61cf3573 fix(auth): narrow mobile auth fallback (#2046)
Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
2026-07-05 23:33:23 +03:00
Bohdan Triapitsyn b09e073e86 fix(mobile): browser composer keyboard handling on chat and draft screens
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.
2026-07-05 09:14:55 +03:00
Bohdan Triapitsyn ed07e7de18 fix(mobile): hold the boot-matched splash while the browser connects
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.
2026-07-05 03:08:18 +03:00
Bohdan Triapitsyn aecf7af892 fix(mobile): restore the password unlock page in mobile browsers
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.
2026-07-05 02:01:02 +03:00
Bohdan Triapitsyn 2c220f3c52 perf(mobile): constant-viewport chat scroller across keyboard transitions
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.
2026-07-05 01:41:44 +03:00
Bohdan Triapitsyn dbcb655e43 feat(mobile): synchronized pill composer transitions and draft screen fixes
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.
2026-07-05 00:05:38 +03:00
Bohdan Triapitsyn 531039b690 feat(mobile): transform-based keyboard choreography on iOS
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.
2026-07-04 13:55:12 +03:00
Bohdan Triapitsyn d71aec54db fix: stabilize chat history prepend scroll preservation on mobile and desktop
- 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
2026-07-03 01:34:04 +03:00
Bohdan Triapitsyn 3bd785a10a fix: prevent mobile session resync flicker
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
2026-07-02 22:44:00 +03:00
Bohdan Triapitsyn 8f685f8db7 feat: improve mobile header and toast placement
Move mobile and VS Code toasts to the top
Respect safe-area spacing for Capacitor toasts
Add dedicated mobile metadata button and update header icons
2026-07-01 20:13:53 +03:00
Bohdan Triapitsyn 7a60a2e601 fix: restore iOS QR code scanning
Skip Android-only scanner module setup on iOS
Keep Android QR scanner module preparation unchanged
2026-07-01 19:34:52 +03:00
Bohdan Triapitsyn 4b1e05160f fix: recover mobile and sync state after resume
Reconnect sync stream when native mobile app resumes
Materialize incomplete sessions with explicit recovery reasons
Add low-noise debug breadcrumb for scoped recovery
2026-07-01 18:32:24 +03:00
Bohdan Triapitsyn 61a4a23add feat: native iOS & Android mobile apps (Capacitor) (#1954)
* 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.
2026-07-01 09:55:41 +03:00
00821700de chore: remove dead code (59 unused files + ~125 unused exports) (#1835)
* chore: remove dead/unreferenced files across ui, vscode

Remove 59 unused source files (components, hooks, lib utils, stores,
barrels, and orphaned vscode github modules) that are not imported by
any entry-reachable code. Also drop a stale test mock for the removed
execCommands module.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove unused exported symbols (types, functions, consts, hooks)

Remove exported symbols whose identifier is referenced nowhere in the
repository (verified via repo-wide search), across ui types/contracts,
lib utilities, sync layer, stores, and components. Also drop the few
imports/private helpers orphaned by these removals.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove more unused exports (desktop, shortcuts, worktree, vscode)

Continue removing repo-wide unreferenced exported functions, consts and
types across lib/desktop, shortcuts, worktreeSessionCreator, sync, and
vscode gitService, with cascading orphaned helpers/imports cleaned up.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore: add dead-code cleanup tooling

* refactor: checkpoint dead-code cleanup

* refactor: remove dead-code suppressions

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-26 19:27:53 +03:00
bashrusakhandLeonid Skorobogatyy a25fc4c25a fix(sync): reflect share status from global store after cancel (#1709)
* fix(sync): reflect share status from global store after cancel

Fix #1551: unshareSession() called updateLiveSession() which silently
fails when the child store doesn't exist. The sidebar rendered from the
child store first, showing stale share data. Now overlays the global
session's share field at merge points.

* fix(sync): extract shared mergeLiveSessionWithGlobalSession helper

Extracted the share-field overlay into a single shared helper in
useGlobalSessionsStore.ts. All 3 merge sites now use the helper
instead of duplicating the overlay logic.

* test(sync): add unit tests for mergeLiveSessionWithGlobalSession helper

---------

Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
2026-06-23 22:18:44 +03:00
307808bec2 fix(mobile): use exact directory matching for session grouping (#1687)
* fix(mobile): use exact directory matching for session grouping

The new mobile sessions sheet used startsWith prefix matching to
assign sessions to projects, which caused child-directory sessions
(e.g. /root/repos/opencode) to be grouped into parent projects
(e.g. /root/repos). Switch to exact directory matching (project root
or registered worktree paths only) to match the desktop sidebar
behavior.

Also exclude sub-agent sessions (those with parentID) from the
totalSessions badge count so the displayed number reflects only
top-level sessions.

* fix: align mobile session project matching

---------

Co-authored-by: lilyzhaun <lilyzhaun@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-23 21:27:54 +03:00
Bohdan Triapitsyn f3aff42b8a feat: show context usage as circular progress
Replaces static context icons with live circular progress indicators
Applies consistent context progress in desktop, mobile, VS Code, and mini-chat headers
Keeps usage coloring tied to existing status thresholds
2026-06-16 14:34:02 +03:00
Bohdan Triapitsyn a45376d585 perf: migrate chat rendering to virtua (#1651)
* refactor: migrate chat history virtualization to virtua

* refactor: render loaded chat history directly

* refactor: finish virtua migration

* perf: defer tool body rendering

* perf: queue deferred tool body mounts

* perf: quiet and defer markdown file probes

* perf: defer markdown code highlighting

* perf: stabilize markdown plugin lists

* perf: defer mermaid markdown rendering

* perf: delay markdown file reference annotation

* perf: attach markdown table listeners on demand

* perf: trim markdown render overhead
2026-06-15 03:29:40 +03:00
lilyzhaunandlilyzhaun c9cd25d18f fix(ui): keep a close control in empty mobile changes states (#1525)
The newer mobile Changes surface returns early for non-interactive list states (no directory selected, repository status still loading, or current directory is not a Git repository). Those branches rendered only MobileChangesState and skipped the standard header row entirely.

On Android PWA this leaves the sheet with no visible close/back affordance. Because the surface is hosted inside MobileSurfaceShell as a modal sheet, the browser/system back gesture does not reliably dismiss it, so users were forced to tap sparse overlay whitespace to escape.

Fix the regression by wrapping those early-return states in the same top header used by the normal Changes list, including the close button and current path label. This keeps dismissal available even when the new mobile UI is showing an empty/error state. While touching the file, switch the remaining direct @remixicon/react usages in this component to the shared Icon system to match current UI conventions.

Validated with packages/ui type-check and a packages/web build.

Co-authored-by: lilyzhaun <lilyzhaun@users.noreply.github.com>
2026-06-11 21:51:01 +03:00
Bohdan Triapitsyn cc97710f9e fix: show update overlay in mobile layout
Shows the animated logo while OpenCode reloads on mobile
Keeps mobile update behavior aligned with desktop and web
2026-06-10 15:55:54 +03:00
Bohdan Triapitsyn eff6f46ad9 feat: improve mobile UX (#1591)
Added a mobile MCP overlay so MCP tools can be opened and managed from the mobile UI without relying on desktop-only dropdown behavior.
Improved mobile session panel touch handling so tapping the status/session area opens the right panel reliably on phones and tablets.
Cleaned up mobile usage provider metadata by removing duplicate rows, hiding unset providers, and showing provider logos consistently.
Added eager loading for provider logos used in mobile usage views to avoid delayed or missing icons when the panel opens.
Refined the mobile update and about flows in OpenChamber settings so release/update information is easier to read on small screens.
Adjusted related layout, header, VS Code layout, command palette, and settings text/localization details needed for the mobile polish.
2026-06-10 12:00:10 +03:00