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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ VITE_PORT=5173
# Use 127.0.0.1 to restrict to localhost only
HOST=0.0.0.0

# Base path prefix for all routes and assets (default: /)
# Useful when serving behind a reverse proxy at a sub-path.
# Requires a frontend rebuild (npm run build) after changing.
# BASE_PATH=/cloudcli

# Uncomment the following line if you have a custom claude cli path other than the default "claude"
# CLAUDE_CLI_PATH=claude

Expand Down
14 changes: 9 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,20 @@
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>

<!-- Service Worker Registration -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
// Derive base path from the manifest link href which Vite transforms with the correct base
var m = document.querySelector('link[rel="manifest"]');
var manifestHref = m ? m.getAttribute('href') : '';
var base = manifestHref ? manifestHref.replace(/\/manifest\.json(?:[?#].*)?$/, '') : '';
window.addEventListener('load', function() {
navigator.serviceWorker.register(base + '/sw.js')
.then(function(registration) {
console.log('SW registered: ', registration);
})
.catch(registrationError => {
.catch(function(registrationError) {
console.log('SW registration failed: ', registrationError);
});
});
Expand Down
22 changes: 11 additions & 11 deletions public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,60 @@
"name": "CloudCLI UI",
"short_name": "CloudCLI UI",
"description": "CloudCLI UI web application",
"start_url": "/",
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff",
"orientation": "portrait-primary",
"scope": "/",
"scope": "./",
"icons": [
{
"src": "/icons/icon-72x72.png",
"src": "icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-96x96.png",
"src": "icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-128x128.png",
"src": "icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-144x144.png",
"src": "icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-152x152.png",
"src": "icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-192x192.png",
"src": "icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-384x384.png",
"src": "icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-512x512.png",
"src": "icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
]
}
}
25 changes: 15 additions & 10 deletions public/sw.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
// Service Worker for Claude Code UI PWA
// Cache only manifest (needed for PWA install). HTML and JS are never pre-cached
// so a rebuild + refresh always picks up the latest assets.
const CACHE_NAME = 'claude-ui-v2';

// Derive base path from service worker URL (e.g. /prefix/sw.js → /prefix)
const BASE_PATH = new URL('.', self.location).pathname.replace(/\/$/, '');

const CACHE_PREFIX = 'claude-ui';
const CACHE_NAME = `${CACHE_PREFIX}:${encodeURIComponent(BASE_PATH || '/')}:v2`;
const urlsToCache = [
'/manifest.json'
`${BASE_PATH}/manifest.json`
];

// Install event
Expand All @@ -20,24 +25,24 @@ self.addEventListener('fetch', event => {
const url = event.request.url;

// Never intercept API requests or WebSocket upgrades
if (url.includes('/api/') || url.includes('/ws')) {
if (url.includes(`${BASE_PATH}/api/`) || url.includes(`${BASE_PATH}/ws`)) {
return;
}

// Navigation requests (HTML) — always go to network, no caching
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => caches.match('/manifest.json').then(() =>
fetch(event.request).catch(() =>
new Response('<h1>Offline</h1><p>Please check your connection.</p>', {
headers: { 'Content-Type': 'text/html' }
})
))
)
);
return;
}

// Hashed assets (JS/CSS in /assets/) — cache-first since filenames change per build
if (url.includes('/assets/')) {
if (url.includes(`${BASE_PATH}/assets/`)) {
event.respondWith(
caches.match(event.request).then(cached => {
if (cached) return cached;
Expand All @@ -63,7 +68,7 @@ self.addEventListener('activate', event => {
caches.keys().then(cacheNames =>
Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.filter(name => name.startsWith(CACHE_PREFIX) && name !== CACHE_NAME)
.map(name => caches.delete(name))
)
)
Expand All @@ -84,8 +89,8 @@ self.addEventListener('push', event => {

const options = {
body: payload.body || '',
icon: '/logo-256.png',
badge: '/logo-128.png',
icon: `${BASE_PATH}/logo-256.png`,
badge: `${BASE_PATH}/logo-128.png`,
data: payload.data || {},
tag: payload.data?.tag || `${payload.data?.sessionId || 'global'}:${payload.data?.code || 'default'}`,
renotify: true
Expand All @@ -102,7 +107,7 @@ self.addEventListener('notificationclick', event => {

const sessionId = event.notification.data?.sessionId;
const provider = event.notification.data?.provider || null;
const urlPath = sessionId ? `/session/${sessionId}` : '/';
const urlPath = sessionId ? `${BASE_PATH}/session/${sessionId}` : `${BASE_PATH}/`;

event.waitUntil(
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(async clientList => {
Expand Down
Loading