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.
This commit is contained in:
Bohdan Triapitsyn
2026-08-01 21:16:36 +03:00
committed by GitHub
parent ea8cc5d7b0
commit 86ef96302d
69 changed files with 5006 additions and 4291 deletions
+11 -117
View File
@@ -1,43 +1,5 @@
/* Mobile Adaptations - All Devices */
/* Set global device type variables */
:root {
--is-mobile: 0;
--device-type: 'desktop';
--font-scale: 1;
}
@media (max-width: 1024px) {
:root.mobile-pointer:not(.desktop-runtime) {
--is-mobile: 1;
--device-type: 'mobile';
--font-scale: 0.9;
}
}
/* Utility classes for device-specific styling */
.desktop-only {
display: block;
}
.mobile-only {
display: none;
}
@media (max-width: 1024px) {
:root.mobile-pointer:not(.desktop-runtime) .desktop-only {
display: none !important;
}
:root.desktop-runtime .mobile-only {
display: none !important;
}
:root.mobile-pointer:not(.desktop-runtime) .mobile-only {
display: block !important;
}
}
/* General mobile improvements for all mobile devices */
@media (max-width: 1024px) {
/* Override CSS custom properties for mobile */
@@ -82,7 +44,10 @@
/* Improve touch targets for mobile */
:root.mobile-pointer:not(.desktop-runtime) button:not([role="radio"]):not([role="checkbox"]):not([role="switch"]),
:root.mobile-pointer:not(.desktop-runtime) .btn,
:root.mobile-pointer:not(.desktop-runtime) [role="button"] {
:root.mobile-pointer:not(.desktop-runtime) [role="button"],
/* Static chat tool rows: not interactive as a whole, but they must share
the 36px rhythm of the [role="button"] expandable/reasoning rows. */
:root.mobile-pointer:not(.desktop-runtime) .oc-static-tool-row {
min-height: 36px;
min-width: 36px;
}
@@ -144,17 +109,6 @@
-webkit-touch-callout: default;
}
/* Improve mobile spacing */
:root.mobile-pointer:not(.desktop-runtime) .px-4 {
padding-left: 1rem !important;
padding-right: 1rem !important;
}
:root.mobile-pointer:not(.desktop-runtime) .py-2 {
padding-top: 0.75rem !important;
padding-bottom: 0.75rem !important;
}
/* Fix mobile scroll containers */
:root.mobile-pointer:not(.desktop-runtime) .overflow-hidden {
overflow-x: hidden !important;
@@ -205,30 +159,6 @@
min-height: 0;
}
/* Mobile sidebar positioning */
:root.mobile-pointer:not(.desktop-runtime) .mobile-sidebar-top {
top: var(--header-height, 3rem);
height: calc(100vh - var(--header-height, 3rem));
}
/* Set header height variable */
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
--header-height: 3rem;
}
/* For mobile devices with larger header */
@media (max-width: 768px) {
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
--header-height: 3.5rem;
}
}
/* For tablet devices */
@media (min-width: 769px) and (max-width: 1024px) {
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
--header-height: 3.25rem;
}
}
}
/* iOS PWA Adaptations */
@@ -241,7 +171,7 @@
--oc-safe-area-top: env(safe-area-inset-top, 0);
--oc-safe-area-right: env(safe-area-inset-right, 0);
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.35), 12px);
--oc-safe-area-left: env(safe-area-inset-left, 0);
}
@@ -258,25 +188,10 @@
/* Phase 1: iOS PWA safe area handling - Enhanced positioning approach */
@media (display-mode: standalone) {
/* iOS-specific safe area handling with CSS variable support */
/* iOS-specific safe area handling. The --oc-safe-area-* tokens and the
.header-safe-area padding are already set by the unconditional iOS block
above; only the standalone-specific rules live here. */
@supports (-webkit-touch-callout: none) {
/* Safe area handling for fixed positioned elements */
:root.device-mobile:not(.desktop-runtime),
:root.device-tablet:not(.desktop-runtime),
:root.mobile-pointer:not(.desktop-runtime) {
--oc-safe-area-top: env(safe-area-inset-top, 0);
--oc-safe-area-right: env(safe-area-inset-right, 0);
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
--oc-safe-area-left: env(safe-area-inset-left, 0);
}
:root.device-mobile:not(.desktop-runtime) .header-safe-area,
:root.device-tablet:not(.desktop-runtime) .header-safe-area,
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
padding-top: var(--oc-safe-area-top);
}
:root.device-mobile:not(.desktop-runtime) .main-content-safe-area,
:root.device-tablet:not(.desktop-runtime) .main-content-safe-area,
:root.mobile-pointer:not(.desktop-runtime) .main-content-safe-area {
@@ -293,14 +208,6 @@
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
}
/* Drawer safe area - top already offset by header height */
:root.device-mobile:not(.desktop-runtime) .drawer-safe-area,
:root.device-tablet:not(.desktop-runtime) .drawer-safe-area,
:root.mobile-pointer:not(.desktop-runtime) .drawer-safe-area {
padding-top: 0;
padding-bottom: var(--oc-safe-area-bottom-visual);
}
/* Fix iOS viewport issues */
:root.device-mobile:not(.desktop-runtime) .flex.flex-col.h-screen,
:root.device-tablet:not(.desktop-runtime) .flex.flex-col.h-screen,
@@ -318,19 +225,6 @@
-webkit-overflow-scrolling: touch;
}
/* Update header height for iOS */
:root.device-mobile:not(.desktop-runtime) .header-safe-area,
:root.device-tablet:not(.desktop-runtime) .header-safe-area,
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
--header-height: 4.5rem;
}
:root.device-mobile:not(.desktop-runtime) .mobile-sidebar-top,
:root.device-tablet:not(.desktop-runtime) .mobile-sidebar-top,
:root.mobile-pointer:not(.desktop-runtime) .mobile-sidebar-top {
--header-height: 4.5rem;
}
/* Mobile typography improvements */
.typography-markdown {
font-size: 1rem !important;
@@ -370,7 +264,7 @@
--oc-safe-area-top: env(safe-area-inset-top, 0);
--oc-safe-area-right: env(safe-area-inset-right, 0);
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.35), 12px);
--oc-safe-area-left: env(safe-area-inset-left, 0);
}
@@ -537,7 +431,7 @@
--oc-safe-area-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
--oc-safe-area-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
--oc-safe-area-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.35), 12px);
--oc-safe-area-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
}
@@ -784,7 +678,7 @@
collapsing browser chrome, so the dynamic and large viewports are the same
thing — pin the shell to 100lvh, which always reports full screen height.
Initial geometry is identical (lvh == dvh on load). */
:root:not(.oc-capacitor-app) .h-\[100dvh\] {
:root:not(.oc-capacitor-app) .oc-mobile-app-shell {
height: 100lvh;
}