Files
openchamber/packages/web/src/main.tsx
T
Bohdan Triapitsyn 86ef96302d feat(mobile): mobile app navigation rework and beta-feedback closeout (#2561)
Navigation model rebuilt around two full-width drawers and a minimal
header (sessions / title-switcher / usage ring / workspace):

- Left sessions drawer: cross-project tree with live status indicators,
  swipe actions on sessions (rename/archive/delete) and on group headers
  (project edit / two-step close, worktree delete), reorder-only edit
  mode with collapsible project cards and draggable worktrees, app-level
  footer (connected instance, settings, pending web update).
- Right workspace drawer: Changes / Files / Terminal / Notes / MCP as
  pill tabs (inactive tabs icon-only); panes stay mounted once visited.
  The full desktop file editor serves the Files tab; read/skill tool taps
  in chat open the file there at the requested line.
- Header session switcher on title tap: 10 cross-project recents with
  live busy/attention indicators and project · branch metadata; the
  usage ring opens a metadata overlay with an explicit loading state.
- The overflow menu is gone on phones (its destinations moved into the
  drawers); iPad keeps it until its dedicated layout pass.

Correctness and continuity:

- /auth/session answers bearer-first, so a stale WebView cookie can no
  longer mask a revoked device token; cold launches classify failures
  fast and land on an explicit connect screen.
- Authoritative session snapshots raise frozen ordering baselines and
  stale live ranks — recents stay truthful after the app slept.
- Cold launches reopen the last active session per instance (persisted
  pointer, confirmed against a sessions snapshot; a user-opened draft
  clears it), with a logo hold instead of a draft flash.

Also: collapsed pill composer gains the stop control; chat tool rows
share one 36px rhythm; Task subtool rows truncate; larger bottom safe
area so the composer clears big-screen corner radii; Capacitor build
hides About/Update (store updates apply there); widgets link to the
sessions drawer with a list icon; MobileApp split into focused modules;
five mobile-surface detectors unified; translucent borders normalized to
70%; all new strings translated across the 10 locales.

iPad and foldable layouts are intentionally untouched - separate next version PR.
2026-08-01 21:16:36 +03:00

117 lines
3.4 KiB
TypeScript

import { createConfiguredWebAPIs, getDesktopRelayRestoreReady } from './runtimeConfig';
import { registerSW } from 'virtual:pwa-register';
import type { RuntimeAPIs } from '@openchamber/ui/lib/api/types';
import { resolveHostedSurface, type HostedSurface } from '@openchamber/ui/lib/runtimeSurface';
import {
isEmbeddedSessionChat,
requestEmbeddedSessionRuntimeBootstrap,
} from '@openchamber/ui/components/layout/contextPanelEmbeddedChat';
import '@openchamber/ui/index.css';
import '@openchamber/ui/styles/fonts';
declare global {
interface Window {
__OPENCHAMBER_RUNTIME_APIS__?: RuntimeAPIs;
__OPENCHAMBER_SURFACE__?: HostedSurface;
}
}
const hostedSurface: HostedSurface = resolveHostedSurface();
type PrerenderingDocument = Document & {
prerendering?: boolean;
};
const canUseServiceWorker = (): boolean => {
if (!('serviceWorker' in navigator)) return false;
if (!window.isSecureContext) return false;
if (window.location.protocol !== 'http:' && window.location.protocol !== 'https:') return false;
const documentState = document as PrerenderingDocument;
if (documentState.prerendering || String(document.visibilityState) === 'prerender') {
return false;
}
return true;
};
const runWhenDocumentCanRegisterServiceWorker = (task: () => void): void => {
let completed = false;
const run = () => {
if (completed) return;
if (canUseServiceWorker()) {
completed = true;
task();
}
};
const afterLoad = () => {
setTimeout(run, 0);
};
if (document.readyState === 'complete') {
afterLoad();
} else {
window.addEventListener('load', afterLoad, { once: true });
}
const documentState = document as PrerenderingDocument;
if (documentState.prerendering || String(document.visibilityState) === 'prerender') {
document.addEventListener('visibilitychange', run, { once: true });
}
};
const registerPwaServiceWorker = (): void => {
runWhenDocumentCanRegisterServiceWorker(() => {
try {
registerSW({
onRegisterError(error: unknown) {
console.warn('[PWA] service worker registration skipped:', error);
},
});
} catch (error) {
console.warn('[PWA] service worker registration skipped:', error);
}
});
};
const unregisterDevelopmentServiceWorkers = (): void => {
runWhenDocumentCanRegisterServiceWorker(() => {
void navigator.serviceWorker.getRegistrations()
.then((registrations) => Promise.all(registrations.map((registration) => registration.unregister())))
.catch(() => {});
});
};
const start = async (): Promise<void> => {
const embeddedBootstrap = isEmbeddedSessionChat()
? await requestEmbeddedSessionRuntimeBootstrap()
: null;
window.__OPENCHAMBER_RUNTIME_APIS__ = createConfiguredWebAPIs(embeddedBootstrap);
if (hostedSurface === 'mobile') {
const { renderMobileApp } = await import('@openchamber/ui/apps/renderMobileApp');
renderMobileApp(window.__OPENCHAMBER_RUNTIME_APIS__);
return;
}
// Hold the render until a desktop relay-host restore has picked its transport.
await getDesktopRelayRestoreReady();
await import('@openchamber/ui/main');
};
void start();
if (import.meta.hot) {
import.meta.hot.on('openchamber:theme-updated', (theme: unknown) => {
window.dispatchEvent(new CustomEvent('openchamber:theme-hmr', { detail: theme }));
});
}
if (import.meta.env.PROD) {
registerPwaServiceWorker();
} else {
unregisterDevelopmentServiceWorkers();
}