Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added src/web/public/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/web/public/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<meta name="description" content="Claude Code session manager with web interface">
<meta name="theme-color" content="#0a0a0a">
<meta name="google" content="notranslate">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="icon-192.png">
<link rel="manifest" href="manifest.json">
<title>Codeman</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%2360a5fa'/%3E%3Cstop offset='100%25' stop-color='%233b82f6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='32' height='32' rx='6' fill='%230a0a0a'/%3E%3Cpath d='M18 4L8 18h6l-2 10 10-14h-6z' fill='url(%23g)'/%3E%3C/svg%3E">
Expand Down
18 changes: 17 additions & 1 deletion src/web/public/manifest.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
108 changes: 92 additions & 16 deletions src/web/public/sw.js
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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',
Expand Down Expand Up @@ -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);
})
);
Expand Down