Commit Graph
51 Commits
Author SHA1 Message Date
Bohdan Triapitsyn 22d5ad3814 fix: mobile goal UX — bottom-sheet dialog, keyboard-safe target button, capacitor safe-top for overlays
- the goal dialog renders as the shared MobileOverlayPanel bottom sheet
  on mobile instead of a centered dialog
- tapping the target button to ARM keeps the soft keyboard open (the next
  message is the objective; same guard as the attachment/mic buttons),
  while opening the manage sheet lets the keyboard close as usual
- capacitor: bottom-sheet overlays cap their height by the keyboard inset
  AND the top safe area — with the keyboard raised while typing inside a
  sheet, 100dvh does not shrink (native resize is off) and the panel could
  slide under the notch/status bar; applies to every MobileOverlayPanel
2026-07-12 17:01:40 +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 24c40b4930 fix: iOS PWA safe areas, viewport stability, and keyboard/overlay choreography
- Add standalone-only safe-area padding for the composer (bottom floor +
  fullscreen top inset) and top toast offset; env() reports 0 on iOS 26
  standalone so a fixed floor is required
- Pin the mobile shell to 100lvh: WebKit leaves 100dvh stuck at the
  keyboard-shrunk value after dismissal
- Clamp visual-viewport pinning to documentElement.clientHeight to guard
  against stale visualViewport metrics
- Defer the composer blur flip (120ms) so taps on composer controls
  survive the keyboard-resize reflow; transition the bottom padding so
  the late flip reads as a slide, not a dip
- Restore the keyboard after mobile overlays close: MobileOverlayPanel
  dispatches synchronous open/close events, ChatInput refocuses within
  the same gesture, holds focus through iOS's tap-settle dismissal,
  guards the pill collapse via DOM focus, and reveals the composer form
  above the keyboard (programmatic focus skips iOS's native reveal)
2026-07-07 01:25:28 +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 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 b19f65f9b6 fix(mobile): steady overlay entrance while the keyboard dismisses
Opening a bottom-sheet overlay usually dismisses the keyboard in the same
moment, so the panel's keyboard-inset bottom anchor was transitioning down
underneath the panel's own rise animation — a jerky, offset entrance. The
anchor now snaps to its final position for the duration of the enter
animation and resumes animating afterwards (so an already-open overlay
still lifts smoothly when the keyboard reopens).
2026-07-05 00:17:19 +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 fb839b66a9 feat(mobile): collapsed pill composer and mobile input redesign
Mobile composer redesign: when the keyboard is closed the input collapses
into a narrow pill (sessions, attach, placeholder, mic) with a round
new-session button that fades away on the draft screen. Model and agent
selectors move into a row above the textarea; the draft project/branch
pickers and the attachment menu become searchable bottom sheets reusing
MobileOverlayPanel; a drag handle (also available while dictating) swipes
the composer into and out of a fullscreen mode.

Keyboard-lifecycle hardening: composer controls (agent cycle, dictation
and its overlay controls) no longer steal focus and dismiss the keyboard;
overlays reopen the keyboard on close via a debounced restore chain that
survives menu-to-picker handoffs and skips the native file picker; open
overlays and dictation keep the composer expanded. Dictation starts
directly from the pill and its overlay content fades in after the shape
settles. The keyboard slide compensates the pill-to-full height change in
one motion, and the mobile highlight mirror is disabled so the caret
always matches the text layout.
2026-07-04 16:41:17 +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 37a9179656 chore: remove bundled IBM Plex fonts 2026-07-02 00:45:59 +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 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
Bohdan Triapitsyn bf1d0d3a75 feat(mobile): polish composer model/agent controls and selection overlay
Redesign the mobile composer model and agent buttons as borderless, full-bleed
labels that hug their content, truncate with an ellipsis when space is tight,
and show the provider logo inline before the model name. Tighten the footer
action buttons (sessions / attach / auto-accept) so they sit close together,
with a small left inset on the group. In the mobile model selection overlay,
make the thinking-variant control text-only with a chevron, vertically center
the variant and favorite controls in each row, and place the provider logo
inline with the model name.
2026-06-28 13:33:55 +03:00
Bohdan Triapitsyn c5eab5ac1e Revert "fix(ui): actually shrink typography classes on mobile viewports (#1791)"
This reverts commit 1ac8fbad70.
2026-06-28 12:33:10 +03:00
1ac8fbad70 fix(ui): actually shrink typography classes on mobile viewports (#1791)
The mobile stylesheet at packages/ui/src/styles/mobile.css defines a
@media (max-width: 1024px) block that overrides the --text-ui-header,
--text-ui-label, --text-meta, and --text-micro custom properties to
shrink labels on phones. The values it was setting were equal to (or
only 1px smaller than) the desktop defaults, so the model selector,
agent selector, and any other element using these classes rendered at
the same size on a 360px-wide phone as on a 1920px desktop.

Fix: tighten the mobile values to 0.875rem (ui-header), 0.75rem
(ui-label, meta), and 0.6875rem (micro), so that mobile is genuinely
smaller than desktop. The same change is applied to the iOS PWA
standalone block lower in the file so both code paths produce the same
size.

Fixes #504

Signed-off-by: Bohdan Triapitsyn <artmore@protonmail.com>
Co-authored-by: James Pinnell <james@example.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-28 01:39:44 +03:00
Bohdan Triapitsyn 08a851f902 fix(markdown): restore paragraph spacing in assistant messages
Wire the unused --markdown-paragraph-spacing token to .markdown-content p so
adjacent paragraphs no longer collapse into a single visual line (Tailwind
preflight had zeroed the default <p> margins).

The renderer wraps each block in a display:contents [data-md-block] element, so
the message-level last-child margin nullifiers target the wrapper, not the
paragraph. Drop the trailing margin on the last paragraph of the last block
directly so messages don't gain extra bottom space. Keep tool-card and
reasoning markdown compact.
2026-06-18 01:00:05 +03:00
bashrusakh 59ecd86b4b perf: isolate chat streaming renders and reduce sidebar render cost (#1672)
Reworks the chat and session-sidebar render paths to cut render cascades, memory
  churn, and UI jank on large sessions and big session trees. Behavior is preserved;
  the changes are about *when* and *how much* the UI re-renders.

  ## Chat streaming
  - Freeze the streaming message's parts in the bulk turn projection during streaming,
    and re-inject live parts only in an isolated tail leaf, so a ~60/sec delta stream
    no longer re-runs the whole-session projection or re-renders unrelated rows.
    session with referential reuse of unchanged turns.
  - Memoize message rows with field-aware comparators instead of reference equality.
  - Replace the manual child-session polling in the task tool with the live SSE
    stream + a one-shot load, removing a fetch/settle state machine.

  ## History loading & scroll
  - Load an initial page fast, then prepend one older page in the background so the
    scroll container has headroom and "load older on scroll-up" fires before the user
    hits the absolute top.
  - Compensate scroll synchronously (in a layout effect, before paint) for prepends —
    including background prepends that don't originate from a user scroll — so the
    viewport stays stable instead of judder-correcting on the next frame.

  ## Markdown rendering
  - Render markdown synchronously *styled* on first paint (paragraphs, lists, code
    cards, tables, inline code) instead of raw escaped text; the async pass then only
    upgrades syntax-highlight colors. Eliminates the flash of full-width raw text.
  - Load KaTeX CSS eagerly with the main bundle instead of inside the lazy markdown
    chunk, avoiding a late stylesheet injection on first render.

  ## Sidebar
  - Hoist per-row recursive tree walks out of row comparators into per-group
    precomputed sets/keys; batch live-session lookups into a single map; add a
    group-level memo boundary.
  - Isolate rename drafts so per-keystroke typing doesn't repaint the row tree.

  ## Sync layer
  - Add a staleness guard so a slow message fetch can't repopulate a session the user
    navigated away from.
  - Throw on fetch failure for authoritative loaders so a transient blip can't read as
    an empty server response.

  ## Cleanup
  - Remove dead code (unused hooks, params, duplicated inline types) surfaced while
    reworking the above.

  ## Known issue
  - A rare, purely cosmetic first-paint width flash can still appear on large sessions;
    it has no behavioral or data impact and is tracked for a follow-up runtime trace.
2026-06-18 00:43:16 +03:00
Bohdan Triapitsyn 3541bc63f9 feat(desktop): macOS vibrancy for the left sidebar with a toggle
Add native macOS vibrancy behind the left sidebar (the only translucent
surface; header/chat/right sidebar stay opaque), plus a setting to turn it off.

- Window created with vibrancy applied after first show (avoids the cold-launch
  no-composite quirk); minimize/restore suppress the frost during the genie
  animation. Renderer frosts the sidebar via --sidebar-vibrancy-overlay once
  data-oc-vibrancy[-ready] are set; project-actions pill matches when open.
- data-oc-vibrancy-ready defaults are set in cssGenerator (DOM guaranteed),
  not the preload (document-start race left the sidebar un-frosted on launch).
- prefers-reduced-transparency falls back to solid surfaces.
- Appearance settings (macOS desktop only): a checkbox to enable/disable
  vibrancy, persisted to settings.json and applied via a Save & restart button
  (vibrancy is a window-creation option, so it needs a relaunch).
2026-06-09 18:38:01 +03:00
9b52222ef1 Add folder-level revert action in Git changes tree (#1390)
* feat: add folder-level revert for git changes

* Fix bot comments. Fix confirmation dialog for mobile view.

* fix

---------

Co-authored-by: Konstantin Zolin <zolin_ka@vk.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-08 15:55:56 +03:00
Bohdan Triapitsyn 7adbaca797 fix: mobile dialog and overlay panel positioning
Center PWA dialogs again by dropping the stale iOS offset that pushed a
flex-centered (position: relative) dialog down by half the viewport.

Seat the mobile overlay panel flush to the bottom (drop the float margin),
drop its bottom border in favor of a scroll shadow on the content, and raise
it above sheets (z-60) so an overlay opened over a surface sheet renders on
top instead of behind it.
2026-06-02 20:26:48 +03:00
lilyzhaunandlilyzhaun 90b3d4760e fix(ui): keep PWA dialogs visible on Android (#1370)
* fix(ui): keep PWA dialogs visible on Android

The PWA dialog adjustments under
@media (display-mode: standalone) and (max-width: 768px) were originally
written to clear the iOS status bar / notch by adding a top offset and a
translateY override. The rule was not scoped to iOS, so Android PWAs
matched it too:

- The extra top: 50% + (safe-area-top * 0.22) plus
  --tw-translate-y: -50% + (safe-area-top * 0.6) pushed the dialog
  below the visible center on Android, where the parent already centers
  the popup via flex items-center justify-center on Base UI's portal.
- max-height was computed against 100vh. Android Chrome's collapsible
  URL bar makes 100vh larger than the visible viewport, so when the
  bar is shown the dialog's action buttons can be pushed off-screen
  (for example the Install button in the Skills install dialog).

Fix:

- Use 100dvh (with the existing safe-area subtractions) so max-height
  tracks the dynamic visible viewport on Android.
- Move the top / --tw-translate-y override into
  @supports (-webkit-touch-callout: none) so it only applies on iOS,
  where it was always intended.

Verified by rebuilding packages/web and inspecting the emitted CSS:
the common .pwa-dialog-content rule now ships with max-height using
100dvh and no top/translate overrides; the iOS-only block keeps the
original offsets.

Tested manually on Android Chrome PWA against
https://aion.xsim.uk: 'About OpenChamber' and 'Install skill' dialogs
now center vertically and their footer buttons stay on-screen as the
URL bar collapses/expands.

* fix(ui): restore vh fallback for PWA dialog max-height

Address review feedback on #1370: the previous patch dropped the
original `max-height: calc(100vh - ...)` line entirely and only kept
`100dvh`. On browsers that do not understand the `dvh` unit the
declaration is invalid and dropped, which would leave the dialog
without any `max-height` cap inside this media block — potentially
worse than before the fix.

Reinstate the canonical progressive-enhancement pattern: ship the
`100vh` declaration first so older engines have a usable value, then
override with `100dvh` on the next line for browsers that do support
it. New comment makes the two-line pattern explicit so it isn't pruned
again as accidental duplication.

Verified via `packages/web` build: the emitted CSS now contains both
declarations in order on the shared `.pwa-dialog-content` rule:

  max-height: calc(100vh - ...);
  max-height: calc(100dvh - ...);

`type-check` and `lint` in `packages/ui` remain clean.

---------

Co-authored-by: lilyzhaun <lilyzhaun@users.noreply.github.com>
2026-05-24 14:19:19 +03:00
Roberto Bertó 49bf1dbff3 feat: widen chat input column with Wide Chat Layout setting (#1318)
The 'Wide Chat Layout' setting toggles a 'wide-chat-layout' class on
the document root that widens '.chat-column' and '.chat-message-column'
from 48rem to 64rem, but '.chat-input-column' (which wraps the
textarea) was missing the matching rule, so the prompt input stayed
narrow while the messages above it widened.

Add the missing '.wide-chat-layout .chat-input-column { width:
min(100%, 64rem) }' selector so the input column tracks the message
column under the same setting.
2026-05-23 20:59:06 +03:00
Bohdan Triapitsyn 581f7c34dc fix: align iPad PWA window controls spacing
Adds tablet standalone PWA detection
Aligns header and sidebar action padding
Extends safe-area handling for tablet PWAs
2026-05-04 00:04:29 +03:00
Bohdan Triapitsyn e6c7cc5589 feat(chat): add wide layout setting 2026-04-30 00:45:25 +03:00
vhqtvnandvhqtvn 4b171ee207 Prevent page overscroll in chat layout (#1034)
Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com>
2026-04-27 12:14:09 +03:00
Bohdan Triapitsyn c8eb9a6c5f feat: widen chat content area
Chat messages use more horizontal space
Chat input keeps its previous max width
2026-04-26 18:04:42 +03:00
Bohdan Triapitsyn 6176ee2b0f refactor: simplify mobile keyboard handling
Removed fragile global keyboard viewport manager
Restored native mobile viewport behavior
Fixed textarea content shifting outside inputs
2026-04-26 17:44:33 +03:00
87db2ea210 fix(ui): stabilize mobile keyboard viewport handling (#1026)
* fix(ui): stabilize mobile keyboard viewport handling

* fix(ui): tighten mobile keyboard viewport handling

* Avoid redundant keyboard open store writes

---------

Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-04-25 20:58:40 +03:00
Bohdan Triapitsyn 6a70d51ea7 refactor: drop mobile keyboard/viewport hacks, rely on browser
Remove body position:fixed lock, visualViewport listener in MainLayout
(keyboard-inset heuristics, scroll-to-zero lock, focusin/focusout RAF),
[data-keyboard-avoid-active] translateY rule, and related CSS vars
(--oc-keyboard-inset, --oc-keyboard-avoid-offset, --oc-keyboard-home-indicator,
--oc-visual-viewport-offset-top). Strip data-keyboard-avoid* attrs and
keyboardAvoid props from Dialog/ScrollableOverlay and consumers. Drop
isKeyboardOpen from useUIStore.

With body unlocked and the layout plain flex-col (h-dvh), the browser
shrinks the viewport on keyboard open naturally — composer sits above
the keyboard, header stops lagging, input no longer jitters.

FilesView.nudgeEditorSelectionAboveKeyboard now derives the occluded
bottom locally from visualViewport + documentElement.clientHeight.
2026-04-24 12:30:11 +03:00
Muhammad Rasyid Rafi'i bf110527a5 feat: implement window controls overlay layout and styles (#1001) 2026-04-23 15:25:32 +03:00
Bohdan Triapitsyn 4506c18f53 UI refresh: Base UI migration + flat tinted button language + mobile polish (#960)
* deps: bump @opencode-ai/sdk to 1.4.6 and add @base-ui/react

* build: align vite configs with Base UI and packages update

* feat(ui): migrate primitives to Base UI

Replace Radix-based wrappers (button, checkbox, collapsible, command,
dialog, dropdown-menu, input, radio, scroll-area, select, separator,
sonner, switch, textarea, toggle, tooltip) with @base-ui/react
equivalents and a refreshed visual language: flexoki theme tokens,
typography utilities, and a redesigned CommandPalette. Add new
fancy-button variant set and a local slot helper for asChild composition.

* refactor(ui consumers): adopt Base UI primitives across the app

Update chat, layout, git views, settings, multirun, session and agent
management screens to use the new Base UI-backed primitives (anchor
width token rename, dropdown/select/dialog usage adjustments, new
component APIs).

* fix(ScrollableOverlay): forward `disabled` to inner element

Add `disabled` to the type so it propagates via rest spread to the
underlying element (e.g. textarea in simple mode). Resolves a type
error reported after the main merge.

* refactor(dropdown consumers): migrate preventDefault sites to closeOnClick

Base UI's Menu.Item does not check `event.defaultPrevented` before
closing the menu, so the Radix-era `onSelect={(e) => e.preventDefault()}`
pattern no longer keeps the menu open. Replace those sites with the
Base UI `closeOnClick={false}` prop (VSCodeLayout rate-limit info
rows; AgentGroupDetail worktree actions).

* docs(AGENTS): note Base UI as primary source for UI primitives

* fix(ContextPanel): center expand/close buttons in tab header

Header (h-8) had the action row pinned with items-end + pb-1.5, which
placed the h-7 buttons ~2px above the header top and made them look
cropped. Switch to items-center so they sit on the same baseline as
the tabs.

* style(tabs): drop transitions on default variant, soften active pill

Default (underline) variant now switches instantly — no transform/width
transition on the underline indicator, no color transition on tab
buttons. Active pill bg is color-mixed down to 55% of
--interactive-selection so light-theme selection doesn't scream.

* style(button): pill-style elevation and introduce chip variant

Solid variants (default/destructive/neutral/primary/basic) now use the
active-pill shadow stack — hairline navy outer ring + stacked tight/soft
drops — instead of the pure-black inset/border approach. Outline
variant (and FancyButton basic) gets the same elevation without the
white top highlight (no fill to catch light).

Add a new chip variant: flat `border border-border/60` button without
elevation, intended for one-of-N toggle groups where a raised
button would read as the wrong affordance.

* style(header): apply pill-style shadow to OpenIn and Project Actions

Both split-button wrappers previously used a 1px CSS border for
definition. Replace that with the pill elevation stack so the header
buttons match the new Button variants and feel less hard-edged.

* refactor(toggles): migrate one-of-N segmented groups to chip variant

Color Mode, When-sessions-expire, voice/STT providers, auth method,
agent mode, MCP transport, tunnel type and terminal Ctrl/Cmd modifier
toggles all read as flat chip selectors rather than raised buttons.
Switch them from variant="outline" to the new chip variant so the
raised pill-style elevation doesn't fight the selected-state tint.

* fix(elevation): theme-adaptive pill-style shadow for dark mode

The navy (rgba(14,18,27,*)) outer ring was invisible on dark surfaces,
erasing the button/pill edge. Add dark: variants that use a subtle
white outer ring (rgba(255,255,255,0.10)) and darker drop shadows
(rgba(0,0,0,0.30/0.20)) so the elevation reads on both themes.

Applies to Button solid/outline variants, FancyButton equivalents,
the OpenIn and Project Actions header split-buttons, and the active
pill indicator in SortableTabsStrip.

* style(button): flat tinted variants with squircle corner-shape

Replace elevated pill-shadow design with a flat tinted language: pale tinted
fill + hairline tinted border + saturated tinted text for default/destructive.
Add CSS corner-shape: squircle with @supports gate so Chrome 136+ gets the
softened superellipse corners at 50px, Safari falls back to a slightly larger
round radius.

* style(tabs): replace active pill shadow with tinted border

* style(header): flat border and squircle corners for OpenIn / Actions

Drop pill-style shadow stacks in favor of a plain hairline border and apply
the same squircle corner-shape treatment as Button so header controls match
the new visual language.

* style(checkbox): drop filled background for tinted checkmark

Checked / indeterminate states now keep the transparent box, show a
primary-base check/minus glyph, and use a softened primary-tinted inset
border (50% mix) so the control reads as selected without a solid swatch.

* style(layout): widen sidebar-to-chat corner radius to 10px

* style(button): retune tinted variants and unify outline fill

- Bump primary fill toward noticeable but still soft (light 10/16/22,
  dark 16/22/30) and weaken both primary and destructive borders
  (12% light / 20% dark) so the tone reads without shouting.
- Further calm destructive across fill and border so revert-style
  actions match the muted reference.
- Align the outline variant fill with --surface-elevated so git-view
  generate/commit buttons share the header action button surface.

* style(tabs): align pill variant with new button language

Active pill now uses --surface-elevated fill and border-border/60 to
match the outline button. Track and tab/pill geometry adopt the same
squircle treatment as Button: 9-10px fallback with 50px radius under
@supports(corner-shape:squircle). Also tighten track padding (inline
2px, block 2px) and grow active tab height so the indicator fills
the track instead of floating inside it.

* refactor(theme): hardcode radius scale, drop per-theme override

Radius values are now fixed in the design system (flexoki-dark's scale
becomes the app default). Themes can no longer override --radius-* — the
config.radius block is removed from every theme JSON, dropped from the
Theme type, and no longer emitted by the CSS generator.

* chore(ui): lint/type cleanup across Base UI wrappers

* style(tabs): dial pill track background down to near-invisible

* refactor(chip): aria-pressed drives the selected tinted state

The chip variant now carries the tinted primary palette (same as the
default filled button) under aria-pressed, so consumers toggle selection
via a single aria attribute instead of repeating the border/text/bg
override classes. All existing chip consumers migrate to aria-pressed
and drop their hand-rolled selected className.

* chore(defaults): humanize variant label in thinking selector

* fix(diff): render all lines in single-file mode

Single-file mode wrapped PierreDiffViewer in its own layout=fill which
creates a nested virtual-root; combined with the outer CSS containment
on the wrapper, Pierre's virtualizer measured a clamped viewport and
stopped rendering lines past ~150. Mirror the stacked-mode approach:
hoist a ScrollableOverlay as the sole virtual root/content and render
PierreDiffViewer inline, so the shared virtualizer sees the real scroll
viewport and paints every line.

Also drops an unused cn import in SessionRetentionSettings.

* feat(files): collapse floating editor controls behind icon button

The floating file editor toolbar now starts as a single pill icon button
and expands into the full control row on hover or click. Clicking outside
or moving the pointer away collapses it back, keeping the editor surface
unobstructed while the file is being read/edited. Fullscreen overlay
keeps its always-visible toolbar.

* fix(chat): align mobile session status bar radius with chat input

* fix(mobile): keep radio/checkbox/switch at their native size

Mobile CSS forced min 36x36px on every <button>, which swallowed our
Radio and Checkbox primitives (also buttons with role=radio/checkbox)
and ballooned them on touch devices. Exclude role=radio/checkbox/switch
from the touch-target rule and pin the Radio/Checkbox box size with
arbitrary px so padding-scale overrides cannot stretch them either.

* chore(mobile): drop unused cornerRadius prop plumbing

The status bar now hardcodes var(--radius-lg) on both views, so the
cornerRadius prop and the useUIStore selector that sourced it in
ChatInput are dead weight.
2026-04-19 15:52:54 +03:00
Bohdan Triapitsyn 952132a08d fix: remove desktop translucency to reduce macOS compositing cost 2026-04-07 21:46:38 +03:00
Bohdan Triapitsyn a516650f96 fix: stabilize chat auto-scroll and bottom resume
- Unify send and session resumes around the latest chat tail
- Keep smooth follow active during assistant message growth
Remove staged chat rendering from the main scroll path
2026-04-06 20:18:20 +03:00
Bohdan Triapitsyn dfc61d7671 refactor: replace chat markdown renderer
- Replace Streamdown-based chat markdown rendering with the new renderer path
- Preserve OpenChamber table, code block, and mermaid presentation
- Remove leftover Streamdown dependencies and compatibility hooks
2026-04-05 19:24:23 +03:00
Bohdan Triapitsyn a36b11d54e fix: limit PWA input spacing behavior to mobile
- Prevent desktop chat input from inheriting mobile bottom safe-area gap
- Apply chat input safe-area classes only on mobile
- Scope standalone PWA safe-area and blur overlay CSS to mobile runtime
2026-04-03 12:45:00 +03:00
JovinesandJovines f032df2298 fix(mobile): close settings drawers and remove extra top spacing (#770)
* fix(mobile): close side drawers when opening settings

* fix(mobile): remove duplicate top inset in drawers

---------

Co-authored-by: Jovines <jovines@qq.com>
2026-04-01 15:32:32 +03:00
Bohdan Triapitsyn c38e00bd85 perf(desktop): reduce CPU/GPU overhead in Tauri shell
- SSH monitor: adaptive polling 2s→10s after stabilization, cheap TCP
  probe before expensive SSH subprocess check
- SSH setup: exponential backoff in wait_for_master_ready and
  wait_local_forward_ready (250ms→2s cap)
- Health checks: exponential backoff (100ms→1s / 250ms→2s) instead of
  flat intervals
- Startup recovery poll: cap at 15 retries instead of infinite
- Remove webview log target in release builds (eliminates IPC overhead)
- Set global NO_PROXY env var at startup for all loopback addresses
- Remove reqwest::blocking feature; use raw TCP for sidecar shutdown and
  SSH health checks
- Disable pinch-to-zoom on macOS via WKWebView.setAllowsMagnification
- Add WebView2 browser args on Windows (proxy bypass + disable unused
  UI features)
- Add Cargo release profile: thin LTO, codegen-units=1, strip
- Extract apply_platform_window_config for consistent window setup
- Add vibrancy toggle in Appearance settings (macOS desktop only) with
  solid background fallback when disabled
2026-04-01 11:54:35 +03:00
Bohdan TriapitsynandIuliia Ivashko 321cc7252a Major UI refresh: sidebar redesign, theme expansion, and chat performance optimizations (#706)
## Summary
Complete sidebar redesign and comprehensive UI polish pass with performance optimizations, theme system refinements, and desktop integration improvements.

## Key Changes

**Sidebar & Navigation Redesign**
- Redesigned sessions sidebar layout with unified button primitives
- Added activity sections with project grouping and improved session organization
- Refined sidebar corners, spacing, and visual hierarchy
- Removed NavRail component in favor of streamlined sidebar
- Stabilized sessions bar toggle position in fullscreen mode

**Performance Optimizations**
- Reduced chat streaming CPU usage and storage churn
- Optimized task tool polling and live timers with debouncing
- Prevented chat state races and reduced background request load
- Debounced draft writes and coalesced session reloads
- Optimized message store updates and turn tracking

**Theme & Visual System**
- Added theme-aware window corners (desktop) and border radius tokens
- Introduced glassmorphism effects on desktop sidebar
- Added backdrop blur to UI elements

**Chat Experience**
- Added session-based permission auto-accept toggle in chat input
- Polished permission shield UX with improved icon sizing and spacing
- Fixed chat scroll-to-bottom behavior and timeline tracking
- Enhanced tool output display with better path label detection
- Removed duplicate draft context details in chat header
- Added text selection menu to chat messages

**Git Improvements**
- Refreshed git history visual design with cleaner dividers
- Added remote removal action in sync selector
- Stabilized git polling to prevent excessive requests
- Improved tool output rendering for git operations

**Settings & Panels**
- Fixed mobile scrolling on settings pages
- Made outside-click settings close instantly
- Reduced settings load churn and CPU spikes
- Improved services dropdown layout and spacing
- Softened panel resize handles

**Desktop Integration**
- Synced macOS window theme with app theme
- Restored window dragging in sidebar header zones
- Fixed system window corners on macOS
- Improved header session metadata and action controls

**Button & Component Standardization**
- Unified button primitives across all components
- Standardized destructive action patterns
- Removed unused button variants (button-large, button-small)
- Aligned context tab close hit areas

---------

Co-authored-by: Iuliia Ivashko <yulia.ivashko@gmail.com>
2026-03-20 01:01:03 +02:00
Bohdan Triapitsyn fab32ca992 refactor(chat): complete turn-based pipeline and stabilize streaming, scroll, and tool UX (#629)
* refactor: add canonical turn projection pipeline for chat

Centralizes turn/activity/summary derivation, migrates render consumers to shared projection sources, and adds projector regression coverage for retries, malformed parent fallbacks, and activity segmentation.

* refactor: stream assistant text live in turn-first render path

* refactor: complete phase-3 turn timeline history/navigation

* refactor: complete phase-4 turn pipeline cutover

Remove legacy grouping adapters and lock key edge cases with executable regression tests so turn-centric rendering stays stable across retry/history/navigation flows.

* fix: align chat history loading path with opencode parity

* fix: stabilize chat streaming and reduce render churn

* feat: add blurIn streaming text animation and smooth lerp-based auto-scroll

Streaming markdown now uses Streamdown blurIn animation (150ms, ease-out) for per-word reveal
Auto-scroll during streaming uses continuous exponential smoothing (lerp) instead of restarting spring animations
One-shot scroll-to-bottom (button click) uses motion spring for natural deceleration

* fix: respect user scroll-up during streaming and re-pin on scroll back to bottom

* fix: stabilize activity stream and tool progress rendering

Activity now stays chronologically consistent and avoids duplicate reasoning/justification.
Tool rows show running state and duration earlier with smoother pending behavior.
Removed legacy trim/timestamp paths to prevent message drops and simplify Activity UI.

* fix: reverted regression of revert functionality

* fix: stabilize chat activity layout and tool progress behavior

Stopped user messages from re-animating when new user messages appear.
Made Activity and aggregated tool rows wrap inline consistently without misaligned headers/icons.
Fixed tool part matching so duration and shine no longer reset across pending/in-progress status updates.

* fix: restore activity mode behavior and stabilize tool rendering

Re-enabled Collapsed/Summary/Detailed/Changes defaults for Activity and tool expansion.
Fixed tool row wrapping/alignment, removed user fade re-animations, and disabled FadeInOnReveal animations.
Improved tool status handling to reduce timer/shine resets and made reasoning bold render as normal text.

* refactor: simplify chat tool and message rendering

Replace activity-group rendering with flat inline part rendering
Remove obsolete tool output and justification activity settings
Keep user message animation and improve edit/apply-patch file path headers

* style: narrow chat and composer columns equally

* fix: Tool status checks and abort flow regression

* fix: improve multi-file apply patch header rendering

* fix: make timeline message jump scroll to selected chat entry

Conversation Timeline clicks now reliably scroll to the selected message in chat
Timeline dialog waits for navigation result before closing to avoid silent failures
Chat message scrolling now resolves the active chat scroller consistently

* chore: remove branch-added chat tests and clean lint leftovers

Removes newly added chat and message test files from this branch
Fixes lint blockers in ProgressiveGroup, ToolPart, and messageStore
Keeps type-check, lint, and build passing after cleanup

* fix: keep composer status row closest to the input

Status/todo row now renders below queued messages and attachments.
Linked GitHub issue/PR chips stay above status so input context order is consistent.
Improves chat composer readability while assistant is working.

* fix: disable assistant header animations and polish chat status behavior

Assistant message header now renders without transition effects
Removed a lint issue from the working placeholder props
Chat status and message flow updates improve consistency during active turns

* feat: increase bottom spacer in mobile chat for better scrolling

* feat: add sorted chat mode with progressive activity rendering

Adds global chat render mode settings for sorted and live behavior
Renders non-stop assistant output in Activity as messages complete
Disables streaming text animation in sorted mode and restores classic reasoning/justification blocks

* fix: stabilize chat timeline navigation and older history loading

Prevents auto-loading older history from scroll and keeps loading behind the button
Fixes timeline jump-to-message behavior that snapped back to the bottom
Reduces chat list flicker and empty-state glitches on large session pagination

* fix: stabilize sorted activity text and hide reasoning timers

Prevented sorted justification from appearing before message completion by requiring explicit non-stop finish.
Removed duration values for sorted Thinking and Justification blocks.
Kept smooth sorted auto-follow behavior tuned to reduce bottom-jump jitter.

* refactor: simplify chat tool rendering paths and add parts docs

Centralized tool icon mapping into a shared presentation helper used by grouped and expandable rows.
Removed static-tool-specific branches from ToolPart to keep it focused on expandable tools.
Added a new DOCUMENTATION.md explaining where to change tool descriptions and rendering behavior.

* feat: enhance event stream handling for message parts

- Refactor event key building functions to improve clarity and reduce redundancy.
- Introduce `applyPartDelta` method in message store for handling delta updates to message parts.
- Update `useEventStream` to utilize the new `applyPartDelta` method for processing incoming deltas.
- Enhance text extraction logic to accommodate new part types and improve merging behavior.
- Implement logic to skip delta-driven updates for ongoing text/reasoning parts to prevent overwriting.
- Adjust server-side text merging logic to handle trimmed text comparisons and improve consistency.

* feat: enhance OpencodeService with delta handling and stale delta management

- Introduced globalSseStaleDeltas to track stale deltas.
- Added methods to generate keys for delta and updated part events.
- Updated global SSE loop to skip processing stale deltas.
- Refactored global SSE event handling to improve performance and reliability.

refactor: remove unused streaming part handling in messageStore

- Eliminated streaming part queue and associated logic.
- Simplified message merging logic by removing redundant checks.
- Streamlined message handling to improve clarity and maintainability.

chore: clean up unused streaming text normalization functions

- Removed deprecated functions related to streaming text identity and merging.
- Simplified normalizeStreamingTextPayload to focus on current functionality.

* refactor: simplify streaming event passthrough

Use SDK-native global event streaming in the UI client
Remove dead server-side SSE normalization helpers
Keep streaming payloads closer to upstream behavior

* revert: remove message memory settings UI

Drop the restored message memory settings screen
Keep session settings focused on current defaults
Preserve chat history limits as internal behavior

* feat: add compact collapsed Activity preview in sorted mode

Collapsed Activity now shows a live preview of the latest 7 entries
Older activity rows roll up behind a +N more indicator
The +N more indicator is clickable to expand the full Activity

* fix: adjusted activity changed files text prominence

* fix: make sorted mode snap to bottom when not streaming

* perf: show chat scrollbar only on user-driven scrolling

Chat scrollbar now stays hidden during automatic follow-to-bottom
Scrollbar visibility is gated by real user scroll intent
Reduced scroll overhead by removing timer-based intent tracking

* refactor: align web and vscode session activity streaming

Route VS Code SSE through SDK-based proxy flow
Restore controlled web session activity tracking and cooldowns
Keep chat activity behavior consistent across runtimes

* fix: adjusted muted foreground themes color

* fix: stabilize session activity and attention updates

Keep streamed chat state in sync as sessions complete
Restore reliable attention indicators for inactive sessions
Tighten server and UI event handling for chat updates

* feat: add chat render previews and file icon visibility setting

Added animated chat render mode previews in Chat settings.
Added a new toggle to show or hide file icons across tool rows.
Persisted chat render, activity, and mermaid modes to shared settings file.

* fix: update default GitHub OAuth client ID

Use the new OpenChamber org GitHub OAuth app ID by default.
Keep web server and VS Code auth defaults in sync.

* fix: sync chat settings across VS Code chat views

Broadcast settings changes to both sidebar and editor chat webviews.
Apply synced settings in each webview via shared settings refresh.
Ensures chat render mode updates appear immediately in editor tabs.

* fix: stop writing session folders file in VS Code workspaces

Disable disk persistence for session folders in VS Code webview runtime
Keep session folder state in VS Code/webview storage instead of project files
Preserve existing behavior for non-VS Code runtimes

* style: polish expanded tool panels and bash output readability

Removed extra borders/labels in expanded tool cards for a cleaner UI
Added softer scroll shadows and full-height decorative activity line
Switched bash command/output to code-style typography with tighter spacing

* feat: add default-open tool controls for chat activity

Added Chat settings toggles to open Bash and Edit tools by default
Default-open behavior now applies across both sorted and live activity rendering
Users can still manually collapse default-open tools, plus bash output height was reduced

* fix: align chat loading skeleton with message layout

Match skeleton width and horizontal alignment to chat message column
Add one-line tool-style skeleton rows with subtle circular placeholders
Increase top spacing so loading state no longer touches chat header

* fix: prevent blank screen when adding inline diff comments

Sanitize persisted inline comment drafts to ignore invalid data
Harden diff annotations against malformed line numbers
Fail safely when annotation updates error instead of crashing the view

* fix: streamline VS Code archived sessions sidebar

Show archived sessions in VS Code only for the active workspace directory.
Render archived sessions as a flat list in VS Code without project-root subfolders.
Hide the "Open in Side Panel" session action in VS Code.
2026-03-12 23:45:45 +02:00
Nelson PiresandBohdan Triapitsyn 73e533a315 fix(chat): align code block actions and restore horizontal scrolling across desktop and mobile (#564)
* fix(chat): use streamdown code controls for markdown blocks

* fix(chat): keep code block actions in header row and body scrollable

* fix(chat): allow horizontal scrolling in virtualized code output

* fix(chat): preserve mobile horizontal scrolling in code blocks

* fix: refine chat code block header and actions styling

* fix: restore themed code highlighting and polish chat code blocks

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-03-01 15:54:02 +02:00
Bohdan Triapitsyn 95c71789c4 Project actions: run commands from header on web + mobile, with SSH-forward URL opening (#542)
* feat: add project actions in header with reliable terminal run flow

- Add per-project Actions settings with icons, platform filters, and default action
- Run/stop actions from header using terminal tabs, including Ctrl+C then force-kill fallback
- Improve terminal UX with stable selection, resize handling, and smarter URL auto-open for localhost ports

* feat: add project actions UI with refined header dropdown behavior

* feat: add parent-session back button in chat

* fix: make web and desktop dev modes reliable and conflict-free

- Separate desktop and web dev ports to avoid collisions
- Add robust process-tree shutdown so Ctrl+C cleans sidecars
- Add true web HMR mode and keep service worker out of dev

* fix: linux safe scripts for dev

* feat: run project actions on web and add desktop SSH forward URL opening

* feat: add mobile project actions button with terminal tabs and tighter tab UI

* revert: remove experimental mobile terminal selection UI

* feat: show Add action button in header when empty

* fix: make retry countdown human-readable in status row

* fix: prevent nav rail actions from firing through overlays
2026-02-27 20:45:03 +02:00
Bohdan Triapitsyn 10851bd7ac feat: move projects to sidebar rail and speed up session switching (#506)
* feat: move projects from header tabs to left sidebar rail

* refactor: remove variant from git generation session context

* feat: implemented drandrop action for navrails

* refactor: improve sidebar drag-and-drop smoothness and remove floating overlay

* fix: prevent mobile nav rail touch from closing drawer and opening menu

* fix: hide header tabs on desktop

* fix: prevent session flicker when switching projects

* style: soften chat panel divider borders

* style: improve icon contrast across core navigation

* fix: stabilize session selection during project switching

* style: unify sidebar surfaces and transparent section layers

* style: remove UI shadows and keep only scroll shadow

* perf: speed up project switching with cached session loading

* perf: speed up Git changes view and background refresh

* perf: make session switching lighter and less aggressive

* perf: optimized sessions list loading while project switching

* perf: smooth chat rendering and reduce interaction spikes

* fix: restore reliable load older messages visibility
2026-02-25 14:32:23 +02:00
25d616f009 feat(mobile): refactor drawer system and session status bar (#494)
* feat(mobile): refactor drawer system with swipe gestures and improved status bar

- Add DrawerContext for centralized drawer state management
- Implement swipe gesture support for left/right drawers
- Refactor MobileSessionStatusBar with token usage indicator
- Update Header component with drawer toggle props
- Improve RightSidebar touch handling
- Add mobile-specific styling improvements
- Update UI store for drawer state management

* feat(mobile): update empty session hint text to clarify swipe gesture area

* fix(mobile): restore MobileAgentButton tap-to-cycle and long-press behavior

- Revert removal of tap-to-cycle agent switching functionality
- Re-add onCycleAgent prop and long-press detection (500ms)
- Click now cycles through primary agents, long-press opens selector panel
- Fixes regression from commit 569cc411

* feat(mobile): add project switcher bar and fix button interactions

- Add ProjectBar component to MobileSessionStatusBar for quick project switching
- Add useProjectStatus hook to track project-level session indicators
- Integrate project store with directory store for project management
- Add project status indicators (running/unread) to session status bar
- Fix MobileAgentButton pointer event handling with preventDefault
- Support horizontal scroll in project bar with touch gesture isolation

* feat(mobile): add long-press to remove projects and filter sessions by project

* fix: improve mobile agent switching UX

* fix: move drawer swipe hook to dedicated file

* refactor(mobile): simplify session status bar, remove More/Less toggle button

* fix: git diff navigation on mobile in sidebar mode

* feat(mobile): add configurable status bar and refine mobile chat controls

---------

Co-authored-by: Jovines <jovines@qq.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-02-24 11:44:43 +02:00
Bohdan Triapitsyn 47c943b487 feat(ui): redesign workspace shell with context panel, tabbed sidebars, and faster diff UX (#433)
* feat: tabbed right sidebar, context panel, floating diff comments

* fix: auto-close left sidebar when context panel opens

- Increase default context panel width from 520 to 600 pixels
- Increase sidebar minimum width from 200 to 300 pixels
- Replace collapsible component with custom button in diff view

* refactoring: rework sidebars, tabs, and file tree layout

- Rewrite AnimatedTabs as segment-style with sliding indicator
- Upgrade SidebarFilesTree to match FilesView features (context menus,
  git status, file icons, CRUD dialogs, fuzzy search ranking)
- Restructure FilesView header: tabs row + actions row, remove breadcrumbs
- Show relative path in context panel header, track active tab
- Allow left sidebar to stay open alongside context panel
- Hide diff/files tabs from header on desktop (mobile-only)
- Move chevron after group name in session sidebar
- Compact tab heights in right sidebar and git view
- Size PreviewToggleButton to match other action buttons
- Remove directory loading spinner from folder icons

* feat: add project icon and color customization

- Enable users to assign custom icons to projects
- Allow users to choose accent colors for projects
- Stabilize repo status UI during project switching

* feat: add scroll fade indicators to editor tabs

* style: reduce spacing and icon sizes in header

* style: adjust tab component padding from uniform to vertical-horizontal

* feat: Add session state indicators to project tabs

* feat: Enhance session status handling and improve UI responsiveness

* fix: preserve upstream tracking on branch rename

* fix: improve initial remote selection for pull requests

- Uses saved remote name from previous session when available
- Selects remote based on tracking branch when possible
- Falls back to origin or first available remote

* perf(diff): faster highlight, stable stacked scroll

- split/unified Pierre worker pools; prefer shiki-wasm
- align diff CSS line-height; disable scroll anchoring; drop WebKit compositing hacks
- harden stacked pin/align (cancel on user scroll/input); prevent overscroll
- make overlay scrollbar MutationObserver optional; disable for diff container

* feat: handle binary files in diff view

* fix: adjust project tabs layout and drag regions

* style: update drag overlay visual styling

* feat: enable number keys to switch projects in the sidebar

* fix: recognize octet-stream as text-based MIME type

* feat: add keyboard navigation to context panel

* feat: add session pinning to sidebar

- Pin important sessions to keep them at the top
- Pinned sessions persist across browser sessions

* refactor: move context usage display from chat input to header
2026-02-16 14:15:19 +02:00
Bohdan Triapitsyn 83ffb1af34 refactor(desktop): make Tauri thin shell running web sidecar (#273)
## What / Why
This PR finishes the desktop refactor: the Tauri app is now a thin shell that launches the web server as a sidecar and loads the UI from `http://127.0.0.1:<port>`. All real backend logic lives in `packages/web/server/index.js`; desktop Rust keeps only native integrations (menu/dialog/notifications/updater/deep-link + window chrome).
This unblocks:
- consistent behavior across web/desktop/vscode (single backend)
- simpler desktop maintenance (no duplicated Rust backend)
- host switching between Local + remote instances in desktop
- reliable cold-start behavior on slow machines (VSCode + desktop)
## Key changes
- Desktop sidecar runtime
  - build pipeline to bundle web dist + `openchamber-server` sidecar (`packages/desktop/scripts/build-sidecar.mjs`)
  - robust local port selection (prefer saved/default, fallback to random; persisted in `~/.config/openchamber/settings.json`)
  - improved PATH handling so the sidecar can locate `opencode` CLI (incl `~/.opencode/bin`, overrides, common bins)
  - disable native right-click context menu in production builds (dev keeps it)
- Desktop instance switcher (Tauri-only)
  - header button + modal to add/edit/delete remote hosts, set default, probe status/ping, switch back to Local escape hatch
  - auth gate includes host switcher so you can recover when a remote host is broken/auth-required
  - host list stored desktop-locally (not tied to the currently selected remote server)
- Notifications
  - decision logic moved server-side; desktop notifications emitted via sidecar stdout and shown natively by Tauri
  - prevent double-notifications on desktop Local origin (UI ignores SSE notification when native path is active)
  - restore macOS notification sound
- Updates
  - Tauri updater used only when viewing Local instance in desktop shell (avoid “remote web update” triggering desktop restart)
- Settings persistence & UX polish
  - persist model favorites/recents via `/api/config/settings` (works for web + desktop; not origin-dependent)
  - persist per-project sidebar collapse state in `projects[].sidebarCollapsed` via `/api/config/settings` (with debounce on toggles)
  - macOS header sizing/traffic-lights offsets fixed (marketing macOS major injected from desktop; MultiRun header aligned)
  - VSCode cold-start: keep retrying provider/agent loads after connection to avoid empty UI on slow machines
  - misc lint/type fixes + bun.lock sync
- Desktop bootstrap / resiliency
  - show onboarding screen when OpenCode CLI is missing (desktop Local origin), with retry hook to restart OpenCode after install
## Testing notes
- Desktop (macOS): switch Local <-> remote, set default host, verify auth gate recovery, native notifications (with sound), updater gated to Local
- Web: favorites/recents + per-project collapsed state persist across reload/restart
- VSCode: slow startup no longer results in missing providers/agents/models
2026-02-05 01:59:49 +02:00
gsxdsm 66da4ad8f4 feat(text-selection): add text selection menu with quick actions (#283)
Add TextSelectionMenu component that appears when selecting text in messages.
Provides quick actions: Add to chat, New session, Copy.

- Add TextSelectionMenu.tsx component with desktop and mobile layouts
- Integrate menu into MessageBody with message-content-text ref
- Add mobile CSS for text selection behavior
2026-02-04 11:13:49 +02:00
Bohdan Triapitsyn ddedc02687 feat: introduced a token-based theming system across the UI
* feat: added themes system

* feat: smart sidebar auto-hide for files/diff tabs + lower files sidebar threshold

* feat: added Checkbox component and update theming

- Add reusable Checkbox component for toggles across UI
- Replace several inputs with Checkbox in settings and commands panels
- Add DiffIcon and apply surface/border theming to key UI areas

* feat: Add convert-vscode-theme.cjs to convert VS Code themes to OpenChamber format

* refactor: remove unused permission logic from ChatInput

- Remove unused permission rules parsing logic from ChatInput
- Memoize renderTheme in DiffWorkerProvider to avoid unnecessary recalculations
- Remove forceOpaque helper in vscode theme adapter

* fix: guard VSCode theme loading in MarkdownRenderer

* feat: add custom user themes loading and reload

- Load user themes from ~/.config/openchamber/themes at runtime
- Expose /api/config/themes to fetch custom themes
- Allow theme reloading from Settings → Theme → Reload themes in the UI
2026-02-01 18:29:34 +02:00
Bohdan Triapitsyn 1442e44e16 fix: update sidebar sticky header background to match sidebar color 2026-01-30 14:32:43 +02:00
Bohdan Triapitsyn 1720a8d6fd feat: add iOS safe area support for PWA header
Introduce CSS variables for safe-area in iOS PWA environments
Apply header padding based on safe-area top to header-safe-area element
2026-01-24 18:37:03 +02:00
Bohdan Triapitsyn d97181bcd2 feat(UI): Introduce new UI components for file attachments and related views (#191)
Add file management API and UI components
Implement directory listing, search and CRUD operations in desktop backend
Expose new Files API on frontend to list, search, and modify files
2026-01-22 12:21:19 +02:00