const DEFAULT_PWA_APP_NAME = 'OpenChamber - AI Coding Assistant'; const mapPwaOrientationToManifest = (value) => { if (value === 'portrait') { return 'portrait-primary'; } if (value === 'landscape') { return 'landscape-primary'; } return undefined; }; export const registerPwaManifestRoute = (app, dependencies) => { const { process, resolveProjectDirectory, buildOpenCodeUrl, getOpenCodeAuthHeaders, readSettingsFromDiskMigrated, normalizePwaAppName, normalizePwaOrientation, } = dependencies; const recentPwaSessionsCache = new Map(); const getRecentPwaSessionShortcuts = async (req) => { const now = Date.now(); const resolvedDirectoryResult = await resolveProjectDirectory(req).catch(() => ({ directory: null })); const preferredDirectory = typeof resolvedDirectoryResult?.directory === 'string' ? resolvedDirectoryResult.directory : null; const cacheKey = preferredDirectory ? `dir:${preferredDirectory}` : 'global'; const cached = recentPwaSessionsCache.get(cacheKey); if (cached && now - cached.at < 5000) { return cached.data; } const normalizeShortcutTitle = (value, fallback) => { const normalized = normalizePwaAppName(value, fallback); return normalized.length > 48 ? normalized.slice(0, 48) : normalized; }; const toFiniteNumber = (value) => { if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string' && value.trim().length > 0) { const parsed = Number(value); if (Number.isFinite(parsed)) { return parsed; } } return null; }; const normalizeDirectory = (value) => { if (typeof value !== 'string') { return ''; } const trimmed = value.trim(); if (!trimmed) { return ''; } const normalized = trimmed.replace(/\\/g, '/'); if (normalized === '/') { return '/'; } return normalized.length > 1 ? normalized.replace(/\/+$/, '') : normalized; }; const sessionUpdatedAt = (session) => { const time = session && typeof session.time === 'object' ? session.time : null; return toFiniteNumber(time?.updated) ?? toFiniteNumber(time?.created) ?? 0; }; const filterSessionsByDirectory = (sessions, directory) => { const normalizedDirectory = normalizeDirectory(directory); if (!normalizedDirectory) { return sessions; } const prefix = normalizedDirectory === '/' ? '/' : `${normalizedDirectory}/`; return sessions.filter((session) => { const sessionDirectory = normalizeDirectory(session?.directory); if (!sessionDirectory) { return false; } return sessionDirectory === normalizedDirectory || sessionDirectory.startsWith(prefix); }); }; const listSessions = async (directory) => { const query = (() => { if (typeof directory !== 'string' || directory.length === 0) { return ''; } const preparedDirectory = process.platform === 'win32' ? directory.replace(/\//g, '\\\\') : directory; return `?directory=${encodeURIComponent(preparedDirectory)}`; })(); const response = await fetch(buildOpenCodeUrl(`/session${query}`, ''), { method: 'GET', headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders(), }, signal: AbortSignal.timeout(2500), }); if (!response.ok) { return []; } const payload = await response.json().catch(() => null); return Array.isArray(payload) ? payload : []; }; try { let payload = []; if (preferredDirectory) { const scopedPayload = await listSessions(preferredDirectory); const filteredScopedPayload = filterSessionsByDirectory(scopedPayload, preferredDirectory); if (filteredScopedPayload.length > 0) { payload = filteredScopedPayload; } else { const globalPayload = await listSessions(null); const filteredGlobalPayload = filterSessionsByDirectory(globalPayload, preferredDirectory); payload = filteredGlobalPayload; } } else { payload = await listSessions(null); } const seen = new Set(); const rows = []; for (const item of payload) { if (!item || typeof item !== 'object') { continue; } const id = typeof item.id === 'string' ? item.id.trim().slice(0, 160) : ''; if (!id || seen.has(id)) { continue; } seen.add(id); const title = normalizeShortcutTitle(item.title, `Session ${rows.length + 1}`); const updatedAt = sessionUpdatedAt(item); rows.push({ id, title, updatedAt }); } rows.sort((a, b) => b.updatedAt - a.updatedAt); const shortcuts = rows.slice(0, 3).map((session) => ({ name: session.title, short_name: session.title.length > 32 ? session.title.slice(0, 32) : session.title, description: 'Open recent session', url: `/?session=${encodeURIComponent(session.id)}`, icons: [{ src: '/pwa-192.png', sizes: '192x192', type: 'image/png' }], })); recentPwaSessionsCache.set(cacheKey, { at: now, data: shortcuts }); return shortcuts; } catch { recentPwaSessionsCache.set(cacheKey, { at: now, data: [] }); return []; } }; app.get('/manifest.webmanifest', async (req, res) => { const hasQueryOverride = typeof req.query?.pwa_name === 'string' || typeof req.query?.app_name === 'string' || typeof req.query?.appName === 'string'; let queryValueRaw = ''; if (typeof req.query?.pwa_name === 'string') { queryValueRaw = req.query.pwa_name; } else if (typeof req.query?.app_name === 'string') { queryValueRaw = req.query.app_name; } else if (typeof req.query?.appName === 'string') { queryValueRaw = req.query.appName; } const queryOverrideName = normalizePwaAppName(queryValueRaw, ''); const hasOrientationOverride = typeof req.query?.orientation === 'string'; const queryOverrideOrientation = normalizePwaOrientation(req.query?.orientation, 'system'); let storedName = ''; let storedOrientation = 'system'; try { const settings = await readSettingsFromDiskMigrated(); storedName = normalizePwaAppName(settings?.pwaAppName, ''); storedOrientation = normalizePwaOrientation(settings?.pwaOrientation, 'system'); } catch { storedName = ''; storedOrientation = 'system'; } const appName = hasQueryOverride ? (queryOverrideName || DEFAULT_PWA_APP_NAME) : (storedName || DEFAULT_PWA_APP_NAME); const manifestOrientation = mapPwaOrientationToManifest( hasOrientationOverride ? queryOverrideOrientation : storedOrientation ); const shortName = appName.length > 30 ? appName.slice(0, 30) : appName; const recentSessionShortcuts = await getRecentPwaSessionShortcuts(req); const manifest = { name: appName, short_name: shortName, description: 'Web interface companion for OpenCode AI coding agent', id: '/', start_url: '/', scope: '/', display: 'standalone', display_override: ['window-controls-overlay'], background_color: '#151313', theme_color: '#edb449', ...(manifestOrientation ? { orientation: manifestOrientation } : {}), icons: [ { src: '/pwa-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, { src: '/pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, { src: '/pwa-maskable-192.png', sizes: '192x192', type: 'image/png', purpose: 'any maskable' }, { src: '/pwa-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' }, { src: '/apple-touch-icon-180x180.png', sizes: '180x180', type: 'image/png', purpose: 'any' }, { src: '/apple-touch-icon-152x152.png', sizes: '152x152', type: 'image/png', purpose: 'any' }, { src: '/favicon-32.png', sizes: '32x32', type: 'image/png' }, { src: '/favicon-16.png', sizes: '16x16', type: 'image/png' }, ], shortcuts: [ { name: 'Appearance Settings', short_name: 'Settings', description: 'Open appearance settings', url: '/?settings=appearance', icons: [{ src: '/pwa-192.png', sizes: '192x192', type: 'image/png' }], }, ...recentSessionShortcuts, ], categories: ['developer', 'tools', 'productivity'], lang: 'en', }; res.setHeader('Cache-Control', 'no-store, must-revalidate'); res.type('application/manifest+json'); res.send(JSON.stringify(manifest)); }); };