fix: PWA improvements — clean up SW, fix chin bar, add shortcuts, improve mobile support

- Remove dead precache manifest from SW (no fetch handler to consume it)
- Set empty globPatterns in vite-plugin-pwa config
- Fix bottom chin bar padding: use scaled safe-area token instead of raw inset
- Add 'New Session' PWA shortcut (client + server manifest)
- Add PWA meta tags to mobile.html (manifest, Apple icons, theme-color)
- Fix static site.webmanifest: add id/scope, correct description
- Remove duplicate apple-mobile-web-app-title from index.html
- Move SW registration to module scope (earlier, no window.load delay)
- Update manifest route tests for new shortcut order
This commit is contained in:
2026-09-07 21:37:06 +00:00
parent 4b95b327dd
commit b464500310
9 changed files with 69 additions and 27 deletions
+1 -1
View File
@@ -716,7 +716,7 @@
} }
:root:not(.oc-capacitor-app):not(.oc-browser-keyboard-open) .oc-mobile-composer { :root:not(.oc-capacitor-app):not(.oc-browser-keyboard-open) .oc-mobile-composer {
padding-bottom: calc(0.75rem + max(16px, var(--oc-safe-area-bottom, env(safe-area-inset-bottom, 0px)))) !important; padding-bottom: calc(0.75rem + var(--oc-safe-area-bottom-visual, 0px)) !important;
} }
/* Fullscreen composer: ChatInput pins the form to the visual viewport from /* Fullscreen composer: ChatInput pins the form to the visual viewport from
+7 -1
View File
@@ -192,6 +192,13 @@
const buildShortcuts = (recentSessions) => { const buildShortcuts = (recentSessions) => {
const shortcuts = [ const shortcuts = [
{
name: 'New Session',
short_name: 'New',
description: 'Start a new coding session',
url: `${baseUrl}/`,
icons: [{ src: `${baseUrl}/pwa-192.png`, sizes: '192x192', type: 'image/png' }],
},
{ {
name: 'Appearance Settings', name: 'Appearance Settings',
short_name: 'Settings', short_name: 'Settings',
@@ -427,7 +434,6 @@
<title>OpenChamber - AI Coding Assistant</title> <title>OpenChamber - AI Coding Assistant</title>
<meta name="description" content="Web interface companion for OpenCode AI coding agent" /> <meta name="description" content="Web interface companion for OpenCode AI coding agent" />
<meta name="application-name" content="OpenChamber" /> <meta name="application-name" content="OpenChamber" />
<meta name="apple-mobile-web-app-title" content="OpenChamber" />
<!-- Inline CSS for loading screen and Nerd Fonts (before Tailwind loads) --> <!-- Inline CSS for loading screen and Nerd Fonts (before Tailwind loads) -->
<style> <style>
+26
View File
@@ -5,6 +5,32 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>OpenChamber Mobile</title> <title>OpenChamber Mobile</title>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" href="/favicon-32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/favicon-16.png" sizes="16x16" />
<link rel="mask-icon" href="/favicon.svg" color="#edb449" />
<!-- Apple touch icon - PNG format required for iOS PWA support -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png" />
<link rel="apple-touch-icon" sizes="167x167" href="/apple-touch-icon-167x167.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png" />
<!-- Web app manifest -->
<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials" />
<!-- Theme color -->
<meta name="theme-color" content="#151313" />
<meta name="theme-color" content="#151313" media="(prefers-color-scheme: dark)" />
<!-- iOS Safari PWA styling -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="OpenChamber" />
<meta name="description" content="Web interface companion for OpenCode AI coding agent" />
<meta name="application-name" content="OpenChamber" />
<script> <script>
// Blocking script: resolve the theme and paint the correct background BEFORE first // Blocking script: resolve the theme and paint the correct background BEFORE first
// paint. Without this the WebView shows its default (light) canvas until React mounts // paint. Without this the WebView shows its default (light) canvas until React mounts
+3 -1
View File
@@ -1,8 +1,10 @@
{ {
"name": "OpenChamber", "name": "OpenChamber",
"short_name": "OpenChamber", "short_name": "OpenChamber",
"description": "OpenChamber desktop companion for the OpenCode AI coding agent", "description": "Web interface companion for OpenCode AI coding agent",
"id": "/",
"start_url": "/", "start_url": "/",
"scope": "/",
"display": "standalone", "display": "standalone",
"background_color": "#151313", "background_color": "#151313",
"theme_color": "#edb449", "theme_color": "#edb449",
@@ -237,6 +237,13 @@ export const registerPwaManifestRoute = (app, dependencies) => {
{ src: '/favicon-16.png', sizes: '16x16', type: 'image/png' }, { src: '/favicon-16.png', sizes: '16x16', type: 'image/png' },
], ],
shortcuts: [ shortcuts: [
{
name: 'New Session',
short_name: 'New',
description: 'Start a new coding session',
url: '/',
icons: [{ src: '/pwa-192.png', sizes: '192x192', type: 'image/png' }],
},
{ {
name: 'Appearance Settings', name: 'Appearance Settings',
short_name: 'Settings', short_name: 'Settings',
@@ -65,6 +65,13 @@ describe('PWA manifest route', () => {
const manifest = JSON.parse(res.body); const manifest = JSON.parse(res.body);
expect(fetchCalls).toHaveLength(2); expect(fetchCalls).toHaveLength(2);
expect(manifest.shortcuts).toEqual([ expect(manifest.shortcuts).toEqual([
{
name: 'New Session',
short_name: 'New',
description: 'Start a new coding session',
url: '/',
icons: [{ src: '/pwa-192.png', sizes: '192x192', type: 'image/png' }],
},
{ {
name: 'Appearance Settings', name: 'Appearance Settings',
short_name: 'Settings', short_name: 'Settings',
+10 -11
View File
@@ -63,17 +63,16 @@ const runWhenDocumentCanRegisterServiceWorker = (task: () => void): void => {
}; };
const registerPwaServiceWorker = (): void => { const registerPwaServiceWorker = (): void => {
runWhenDocumentCanRegisterServiceWorker(() => { if (!canUseServiceWorker()) return;
try { try {
registerSW({ registerSW({
onRegisterError(error: unknown) { onRegisterError(error: unknown) {
console.warn('[PWA] service worker registration skipped:', error); console.warn('[PWA] service worker registration skipped:', error);
}, },
}); });
} catch (error) { } catch (error) {
console.warn('[PWA] service worker registration skipped:', error); console.warn('[PWA] service worker registration skipped:', error);
} }
});
}; };
const unregisterDevelopmentServiceWorkers = (): void => { const unregisterDevelopmentServiceWorkers = (): void => {
+5 -10
View File
@@ -1,16 +1,11 @@
/// <reference lib="webworker" /> /// <reference lib="webworker" />
// NOTE: keep the Workbox injection point so vite-plugin-pwa can build. // Minimal service worker: push notifications and click routing only.
// We intentionally do not use Workbox runtime helpers here: iOS Safari can be // No fetch handler, no precache — the app relies on the server for assets
// fragile with more complex SW bundles. For push notifications we only need a // and the event pipeline handles reconnection. iOS Safari is fragile with
// minimal SW. // complex SW bundles, so we keep this as lean as possible.
declare const self: ServiceWorkerGlobalScope & { declare const self: ServiceWorkerGlobalScope;
__WB_MANIFEST: Array<string | { url: string; revision?: string }>;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const __precacheManifest = self.__WB_MANIFEST;
type PushPayload = { type PushPayload = {
title?: string; title?: string;
+3 -3
View File
@@ -73,10 +73,10 @@ export default defineConfig({
injectRegister: false, injectRegister: false,
manifest: false, manifest: false,
injectManifest: { injectManifest: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,otf,eot}'], // No precache — the SW has no fetch handler. Only the IIFE bundle
// iOS Safari/PWA is much more reliable with a classic (non-module) SW bundle. // is produced so vite-plugin-pwa can build the registration script.
globPatterns: [],
rollupFormat: 'iife', rollupFormat: 'iife',
// We already keep a custom manifest in index.html
injectionPoint: undefined, injectionPoint: undefined,
}, },
devOptions: { devOptions: {