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.
100 lines
4.4 KiB
TypeScript
100 lines
4.4 KiB
TypeScript
import { StrictMode } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import '@/styles/fonts';
|
|
import '@/index.css';
|
|
import '@/lib/debug';
|
|
import { DiffWorkerProvider } from '@/contexts/DiffWorkerProvider';
|
|
import { ThemeProvider } from '@/components/providers/ThemeProvider';
|
|
import { ThemeSystemProvider } from '@/contexts/ThemeSystemContext';
|
|
import type { RuntimeAPIs } from '@/lib/api/types';
|
|
import { startAppearanceAutoSave } from '@/lib/appearanceAutoSave';
|
|
import { getDeviceInfo } from '@/lib/device';
|
|
import { markAppBootReady } from './appBootReady';
|
|
import { installMobileWidgetSnapshotBridge } from './mobileWidgetSnapshot';
|
|
import { applyPersistedDirectoryPreferences } from '@/lib/directoryPersistence';
|
|
import { initializeLocale, I18nProvider } from '@/lib/i18n';
|
|
import { initializeAppearancePreferences, syncDesktopSettings } from '@/lib/persistence';
|
|
import { startModelPrefsAutoSave } from '@/lib/modelPrefsAutoSave';
|
|
import { startTypographyWatcher } from '@/lib/typographyWatcher';
|
|
import { preloadMarkdownRenderer } from '@/components/chat/markdownRendererLoader';
|
|
import { SessionAuthGate } from '@/components/auth/SessionAuthGate';
|
|
import { MobileApp } from './MobileApp';
|
|
|
|
const initializeSharedPreferences = () => {
|
|
initializeLocale();
|
|
|
|
void initializeAppearancePreferences().then(() => {
|
|
void Promise.all([
|
|
syncDesktopSettings(),
|
|
applyPersistedDirectoryPreferences(),
|
|
]).catch((err) => {
|
|
console.error('[mobile-main] settings init failed:', err);
|
|
});
|
|
|
|
startAppearanceAutoSave();
|
|
startModelPrefsAutoSave();
|
|
startTypographyWatcher();
|
|
}).catch((err) => {
|
|
console.error('[mobile-main] appearance init failed:', err);
|
|
}).finally(() => {
|
|
// Persisted typography/appearance is now applied — release the splash gate so the
|
|
// first UI paint is already at its final sizes.
|
|
markAppBootReady();
|
|
});
|
|
};
|
|
|
|
export function renderMobileApp(apis: RuntimeAPIs) {
|
|
// Stamp the surface before anything else reads it: perf tuning, sync paging,
|
|
// and device info all key off isMobileSurfaceRuntime(), and without the stamp
|
|
// a wide native device (iPad landscape) would fall out of the mobile branch.
|
|
window.__OPENCHAMBER_SURFACE__ = 'mobile';
|
|
preloadMarkdownRenderer();
|
|
initializeSharedPreferences();
|
|
|
|
// Expose the widget snapshot builder so the native shell can read the session overview
|
|
// (attention count + recent sessions) and feed the home/lock-screen/Control Center widgets.
|
|
installMobileWidgetSnapshotBridge();
|
|
|
|
// Apply the device classes (`device-mobile`, `mobile-pointer`) to <html> BEFORE the
|
|
// first React paint. They gate the mobile typography rules in mobile.css (larger
|
|
// --text-* sizes); applied late from a hook effect, they bumped text size a frame
|
|
// after mount and shifted the layout (connect / scan / saved-connection labels).
|
|
getDeviceInfo();
|
|
|
|
const rootElement = document.getElementById('root');
|
|
if (!rootElement) {
|
|
throw new Error('Root element not found');
|
|
}
|
|
|
|
// The native Capacitor app delivers notifications via APNs only (background, server-side
|
|
// focus-gated). Disable the in-app notification dispatch on native with a no-op
|
|
// notifications API: scheduling local notifications can't tell foreground from background
|
|
// in a WKWebView and leaked while the app was open. (The Web Notifications API the web
|
|
// runtime uses also doesn't display inside a WKWebView.)
|
|
const capacitor = (window as typeof window & { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor;
|
|
const isNativeShell = capacitor?.isNativePlatform?.() === true || window.location.protocol === 'capacitor:';
|
|
const resolvedApis = isNativeShell
|
|
? { ...apis, notifications: { notifyAgentCompletion: async () => false, canNotify: () => false } }
|
|
: apis;
|
|
|
|
// Auth gating differs by shell: the native Capacitor app authenticates via
|
|
// its own instance-connect flow (MobileConnectionWelcome asks for the
|
|
// password per instance), while the plain mobile BROWSER against a
|
|
// --ui-password server must keep the classic SessionAuthGate unlock page.
|
|
const app = <MobileApp apis={resolvedApis} />;
|
|
|
|
createRoot(rootElement).render(
|
|
<StrictMode>
|
|
<I18nProvider>
|
|
<ThemeSystemProvider>
|
|
<ThemeProvider>
|
|
<DiffWorkerProvider>
|
|
{isNativeShell ? app : <SessionAuthGate>{app}</SessionAuthGate>}
|
|
</DiffWorkerProvider>
|
|
</ThemeProvider>
|
|
</ThemeSystemProvider>
|
|
</I18nProvider>
|
|
</StrictMode>,
|
|
);
|
|
}
|