diff --git a/public/manifest.json b/public/manifest.json index 347ae17..1449def 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,12 +1,24 @@ { "name": "PinchChat", "short_name": "PinchChat", - "description": "A sleek, dark-themed webchat UI for OpenClaw", + "description": "A sleek, dark-themed webchat UI for OpenClaw — monitor sessions, stream responses, and inspect tool calls in real-time.", "start_url": "/", "display": "standalone", "background_color": "#1e1e24", "theme_color": "#1e1e24", + "orientation": "any", + "categories": ["developer", "productivity", "utilities"], "icons": [ + { + "src": "/favicon-16.png", + "sizes": "16x16", + "type": "image/png" + }, + { + "src": "/favicon-32.png", + "sizes": "32x32", + "type": "image/png" + }, { "src": "/logo-192.png", "sizes": "192x192", diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..883a853 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,62 @@ +// PinchChat Service Worker — cache static assets for offline/instant load +const CACHE_NAME = 'pinchchat-v1'; + +// Cache static assets on install +self.addEventListener('install', (event) => { + self.skipWaiting(); +}); + +// Clean old caches on activate +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) + ) + ); + self.clients.claim(); +}); + +// Stale-while-revalidate for static assets, network-first for API/WS +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url); + + // Skip non-GET, WebSocket upgrades, and chrome-extension requests + if (event.request.method !== 'GET') return; + if (url.protocol === 'chrome-extension:') return; + + // Don't cache API calls or WebSocket-related requests + if (url.pathname.startsWith('/api') || url.pathname.startsWith('/ws')) return; + + // For navigation requests (HTML), always go network-first to get latest SPA shell + if (event.request.mode === 'navigate') { + event.respondWith( + fetch(event.request) + .then((response) => { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); + return response; + }) + .catch(() => caches.match(event.request)) + ); + return; + } + + // Static assets (JS, CSS, images, fonts): stale-while-revalidate + if ( + url.pathname.match(/\.(js|css|png|jpg|jpeg|gif|webp|svg|woff2?|ttf|ico|json)$/) || + url.pathname.startsWith('/assets/') + ) { + event.respondWith( + caches.open(CACHE_NAME).then((cache) => + cache.match(event.request).then((cached) => { + const fetchPromise = fetch(event.request).then((response) => { + if (response.ok) cache.put(event.request, response.clone()); + return response; + }); + return cached || fetchPromise; + }) + ) + ); + return; + } +}); diff --git a/src/main.tsx b/src/main.tsx index 1f04d87..86abb44 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,15 @@ import { ErrorBoundary } from './components/ErrorBoundary' import { ThemeProvider } from './contexts/ThemeContext' import './index.css' +// Register service worker for PWA support (offline caching, installability) +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js').catch(() => { + // SW registration failed — app works fine without it + }); + }); +} + ReactDOM.createRoot(document.getElementById('root')!).render(