Skip to content

Commit a11bcb0

Browse files
authored
Merge pull request #59 from aakhter/feat/pwa-support
feat: add PWA support for Android/iOS home screen install
2 parents 47fd9a9 + 6a12a72 commit a11bcb0

5 files changed

Lines changed: 112 additions & 17 deletions

File tree

src/web/public/icon-192.png

1.75 KB
Loading

src/web/public/icon-512.png

4.7 KB
Loading

src/web/public/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
<meta name="description" content="Claude Code session manager with web interface">
77
<meta name="theme-color" content="#0a0a0a">
88
<meta name="google" content="notranslate">
9+
<meta name="apple-mobile-web-app-capable" content="yes">
10+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
11+
<link rel="apple-touch-icon" href="icon-192.png">
912
<link rel="manifest" href="manifest.json">
1013
<title>Codeman</title>
1114
<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">

src/web/public/manifest.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
{
22
"name": "Codeman",
33
"short_name": "Codeman",
4+
"description": "Claude Code session manager",
45
"start_url": "/",
56
"display": "standalone",
7+
"orientation": "any",
68
"background_color": "#0a0a0a",
7-
"theme_color": "#0a0a0a"
9+
"theme_color": "#0a0a0a",
10+
"icons": [
11+
{
12+
"src": "icon-192.png",
13+
"sizes": "192x192",
14+
"type": "image/png",
15+
"purpose": "any maskable"
16+
},
17+
{
18+
"src": "icon-512.png",
19+
"sizes": "512x512",
20+
"type": "image/png",
21+
"purpose": "any maskable"
22+
}
23+
]
824
}

src/web/public/sw.js

Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,106 @@
11
/**
2-
* @fileoverview Service worker for Web Push notifications.
2+
* @fileoverview Service worker for PWA install + Web Push notifications.
33
*
4-
* Receives push events from the Codeman server (via web-push library) and displays
5-
* OS-level notifications. Handles notification clicks to focus an existing Codeman
6-
* tab or open a new one. Supports action buttons, per-session deep linking, and
7-
* critical notification persistence (requireInteraction).
4+
* App-shell caching: on install, precaches the core UI assets so the app
5+
* launches instantly and works offline (or on flaky connections). Uses a
6+
* network-first strategy for navigation and API calls, cache-first for
7+
* static assets.
88
*
9-
* Lifecycle: skipWaiting on install, claim clients on activate — ensures the latest
10-
* service worker takes control immediately without waiting for tab refresh.
9+
* Push notifications: receives push events from the Codeman server (via
10+
* web-push library) and displays OS-level notifications. Handles notification
11+
* clicks to focus an existing Codeman tab or open a new one.
12+
*
13+
* Lifecycle: skipWaiting on install, claim clients on activate -- ensures the
14+
* latest service worker takes control immediately without waiting for tab
15+
* refresh.
1116
*
1217
* @dependency None (runs in ServiceWorkerGlobalScope, isolated from page scripts)
13-
* @see src/push-store.ts server-side VAPID key management and subscription CRUD
18+
* @see src/push-store.ts -- server-side VAPID key management and subscription CRUD
1419
*/
1520

16-
// Codeman Service Worker — Web Push notifications
17-
// This service worker receives push events from the server and displays OS-level notifications.
18-
// It also handles notification clicks to focus or open the Codeman tab.
21+
const CACHE_NAME = 'codeman-v1';
22+
23+
// Core app shell -- cached on install for instant startup
24+
const APP_SHELL = [
25+
'/',
26+
'/styles.css',
27+
'/mobile.css',
28+
'/constants.js',
29+
'/app.js',
30+
'/api-client.js',
31+
'/terminal-ui.js',
32+
'/session-ui.js',
33+
'/settings-ui.js',
34+
'/panels-ui.js',
35+
'/notification-manager.js',
36+
'/mobile-handlers.js',
37+
'/keyboard-accessory.js',
38+
'/voice-input.js',
39+
'/vendor/xterm.min.js',
40+
'/vendor/xterm-addon-fit.min.js',
41+
'/vendor/xterm-addon-unicode11.min.js',
42+
'/vendor/xterm-zerolag-input.js',
43+
'/vendor/xterm.css',
44+
'/icon-192.png',
45+
'/icon-512.png',
46+
'/manifest.json',
47+
];
48+
49+
// --- Install: precache app shell ---
1950

20-
self.addEventListener('install', () => {
51+
self.addEventListener('install', (event) => {
52+
event.waitUntil(
53+
caches.open(CACHE_NAME).then((cache) => {
54+
// Use addAll but don't fail install if some assets 404 (hashed filenames)
55+
return Promise.allSettled(
56+
APP_SHELL.map((url) => cache.add(url).catch(() => {}))
57+
);
58+
})
59+
);
2160
self.skipWaiting();
2261
});
2362

63+
// --- Activate: clean old caches, claim clients ---
64+
2465
self.addEventListener('activate', (event) => {
25-
event.waitUntil(self.clients.claim());
66+
event.waitUntil(
67+
caches.keys().then((keys) =>
68+
Promise.all(
69+
keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
70+
)
71+
).then(() => self.clients.claim())
72+
);
2673
});
2774

75+
// --- Fetch: network-first for API/navigation, cache-first for static ---
76+
77+
self.addEventListener('fetch', (event) => {
78+
const { request } = event;
79+
80+
// Skip non-GET, WebSocket upgrades, and SSE streams
81+
if (request.method !== 'GET') return;
82+
if (request.headers.get('upgrade') === 'websocket') return;
83+
if (request.headers.get('accept') === 'text/event-stream') return;
84+
if (request.url.includes('/api/')) return;
85+
86+
event.respondWith(
87+
caches.match(request).then((cached) => {
88+
// Return cache immediately, refresh in background (stale-while-revalidate)
89+
const fetchPromise = fetch(request).then((response) => {
90+
if (response && response.ok) {
91+
const clone = response.clone();
92+
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
93+
}
94+
return response;
95+
}).catch(() => cached);
96+
97+
return cached || fetchPromise;
98+
})
99+
);
100+
});
101+
102+
// --- Push notifications ---
103+
28104
self.addEventListener('push', (event) => {
29105
if (!event.data) return;
30106

@@ -40,8 +116,8 @@ self.addEventListener('push', (event) => {
40116
const options = {
41117
body: body || '',
42118
tag: tag || 'codeman-default',
43-
icon: '/favicon.ico',
44-
badge: '/favicon.ico',
119+
icon: '/icon-192.png',
120+
badge: '/icon-192.png',
45121
data: { sessionId, url: sessionId ? `/?session=${sessionId}` : '/' },
46122
renotify: true,
47123
requireInteraction: urgency === 'critical',
@@ -75,7 +151,7 @@ self.addEventListener('notificationclick', (event) => {
75151
return client.focus();
76152
}
77153
}
78-
// No existing tab open a new one
154+
// No existing tab -- open a new one
79155
return self.clients.openWindow(targetUrl);
80156
})
81157
);

0 commit comments

Comments
 (0)