diff --git a/src/web/public/icon-192.png b/src/web/public/icon-192.png new file mode 100644 index 00000000..d85d5795 Binary files /dev/null and b/src/web/public/icon-192.png differ diff --git a/src/web/public/icon-512.png b/src/web/public/icon-512.png new file mode 100644 index 00000000..41daa02e Binary files /dev/null and b/src/web/public/icon-512.png differ diff --git a/src/web/public/index.html b/src/web/public/index.html index 42f95d3f..a7a20b8b 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -6,6 +6,9 @@ + + + Codeman diff --git a/src/web/public/manifest.json b/src/web/public/manifest.json index 410d4912..79b6ea64 100644 --- a/src/web/public/manifest.json +++ b/src/web/public/manifest.json @@ -1,8 +1,24 @@ { "name": "Codeman", "short_name": "Codeman", + "description": "Claude Code session manager", "start_url": "/", "display": "standalone", + "orientation": "any", "background_color": "#0a0a0a", - "theme_color": "#0a0a0a" + "theme_color": "#0a0a0a", + "icons": [ + { + "src": "icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] } diff --git a/src/web/public/sw.js b/src/web/public/sw.js index 5d87bbb3..5c6e8039 100644 --- a/src/web/public/sw.js +++ b/src/web/public/sw.js @@ -1,30 +1,106 @@ /** - * @fileoverview Service worker for Web Push notifications. + * @fileoverview Service worker for PWA install + Web Push notifications. * - * Receives push events from the Codeman server (via web-push library) and displays - * OS-level notifications. Handles notification clicks to focus an existing Codeman - * tab or open a new one. Supports action buttons, per-session deep linking, and - * critical notification persistence (requireInteraction). + * App-shell caching: on install, precaches the core UI assets so the app + * launches instantly and works offline (or on flaky connections). Uses a + * network-first strategy for navigation and API calls, cache-first for + * static assets. * - * Lifecycle: skipWaiting on install, claim clients on activate — ensures the latest - * service worker takes control immediately without waiting for tab refresh. + * Push notifications: receives push events from the Codeman server (via + * web-push library) and displays OS-level notifications. Handles notification + * clicks to focus an existing Codeman tab or open a new one. + * + * Lifecycle: skipWaiting on install, claim clients on activate -- ensures the + * latest service worker takes control immediately without waiting for tab + * refresh. * * @dependency None (runs in ServiceWorkerGlobalScope, isolated from page scripts) - * @see src/push-store.ts — server-side VAPID key management and subscription CRUD + * @see src/push-store.ts -- server-side VAPID key management and subscription CRUD */ -// Codeman Service Worker — Web Push notifications -// This service worker receives push events from the server and displays OS-level notifications. -// It also handles notification clicks to focus or open the Codeman tab. +const CACHE_NAME = 'codeman-v1'; + +// Core app shell -- cached on install for instant startup +const APP_SHELL = [ + '/', + '/styles.css', + '/mobile.css', + '/constants.js', + '/app.js', + '/api-client.js', + '/terminal-ui.js', + '/session-ui.js', + '/settings-ui.js', + '/panels-ui.js', + '/notification-manager.js', + '/mobile-handlers.js', + '/keyboard-accessory.js', + '/voice-input.js', + '/vendor/xterm.min.js', + '/vendor/xterm-addon-fit.min.js', + '/vendor/xterm-addon-unicode11.min.js', + '/vendor/xterm-zerolag-input.js', + '/vendor/xterm.css', + '/icon-192.png', + '/icon-512.png', + '/manifest.json', +]; + +// --- Install: precache app shell --- -self.addEventListener('install', () => { +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + // Use addAll but don't fail install if some assets 404 (hashed filenames) + return Promise.allSettled( + APP_SHELL.map((url) => cache.add(url).catch(() => {})) + ); + }) + ); self.skipWaiting(); }); +// --- Activate: clean old caches, claim clients --- + self.addEventListener('activate', (event) => { - event.waitUntil(self.clients.claim()); + event.waitUntil( + caches.keys().then((keys) => + Promise.all( + keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)) + ) + ).then(() => self.clients.claim()) + ); }); +// --- Fetch: network-first for API/navigation, cache-first for static --- + +self.addEventListener('fetch', (event) => { + const { request } = event; + + // Skip non-GET, WebSocket upgrades, and SSE streams + if (request.method !== 'GET') return; + if (request.headers.get('upgrade') === 'websocket') return; + if (request.headers.get('accept') === 'text/event-stream') return; + if (request.url.includes('/api/')) return; + + event.respondWith( + caches.match(request).then((cached) => { + // Return cache immediately, refresh in background (stale-while-revalidate) + const fetchPromise = fetch(request).then((response) => { + if (response && response.ok) { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); + } + return response; + }).catch(() => cached); + + return cached || fetchPromise; + }) + ); +}); + +// --- Push notifications --- + self.addEventListener('push', (event) => { if (!event.data) return; @@ -40,8 +116,8 @@ self.addEventListener('push', (event) => { const options = { body: body || '', tag: tag || 'codeman-default', - icon: '/favicon.ico', - badge: '/favicon.ico', + icon: '/icon-192.png', + badge: '/icon-192.png', data: { sessionId, url: sessionId ? `/?session=${sessionId}` : '/' }, renotify: true, requireInteraction: urgency === 'critical', @@ -75,7 +151,7 @@ self.addEventListener('notificationclick', (event) => { return client.focus(); } } - // No existing tab — open a new one + // No existing tab -- open a new one return self.clients.openWindow(targetUrl); }) );