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.
117 lines
3.4 KiB
TypeScript
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();
|
|
}
|