From 99474955af78da85e2eab6c40cd0302b84173b62 Mon Sep 17 00:00:00 2001 From: Jesper Derehag Date: Sat, 7 Feb 2026 00:16:47 +0100 Subject: [PATCH] feat(ui): add PWA support with vite-plugin-pwa - Add vite.config.pwa.ts extending the base config with VitePWA plugin - Generate PWA icons at build time from source logo via @vite-pwa/assets-generator - Add web app manifest with name, theme color, display overrides - Add Workbox runtime caching: NetworkFirst for API, CacheFirst for assets - Set navigateFallback to null to preserve server-side auth redirects - Server build uses build:pwa for PWA-enabled output; Electron/Tauri use the base build without PWA Signed-off-by: Jesper Derehag --- packages/server/README.md | 11 ++++++ packages/server/package.json | 2 +- packages/ui/.gitignore | 1 + packages/ui/package.json | 3 ++ packages/ui/vite.config.pwa.ts | 61 ++++++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 packages/ui/vite.config.pwa.ts diff --git a/packages/server/README.md b/packages/server/README.md index e69e9059..ee976616 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -62,6 +62,17 @@ You can configure the server using flags or environment variables: Use this only when access is already protected by another layer (SSO proxy, VPN, Coder workspace auth, etc.). If you bind to `0.0.0.0` while skipping auth, anyone who can reach the port can access the API. +### Progressive Web App (PWA) +When running as a server CodeNomad can also be installed as a PWA from any supported browser, giving you a native app experience just like the Electron installation but executing on the remote server instead. + +1. Open the CodeNomad UI in a Chromium-based browser (Chrome, Edge, Brave, etc.). +2. Click the install icon in the address bar, or use the browser menu → "Install CodeNomad". +3. The app will open in a standalone window and appear in your OS app list. + +> **TLS requirement** +> Browsers require a secure (`https://`) connection for PWA installation. +> If you host CodeNomad on a remote machine, serve it behind a reverse proxy (e.g. Caddy, nginx) with a valid TLS certificate — self-signed certificates will not work. + ### Data Storage - **Config**: `~/.config/codenomad/config.json` - **Instance Data**: `~/.config/codenomad/instances` (chat history, etc.) diff --git a/packages/server/package.json b/packages/server/package.json index 6dd1e93f..b859b619 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -18,7 +18,7 @@ }, "scripts": { "build": "npm run build:ui && npm run prepare-ui && tsc -p tsconfig.json && node ./scripts/copy-auth-pages.mjs && npm run prepare-config", - "build:ui": "npm run build --prefix ../ui", + "build:ui": "npm run build:pwa --prefix ../ui", "prepare-ui": "node ./scripts/copy-ui-dist.mjs", "prepare-config": "node ./scripts/copy-opencode-config.mjs", "dev": "cross-env CODENOMAD_DEV=1 CODENOMAD_SERVER_PASSWORD=codenomad-dev CLI_UI_DEV_SERVER=http://localhost:3000 tsx src/index.ts", diff --git a/packages/ui/.gitignore b/packages/ui/.gitignore index 3ff38cc0..1761634c 100644 --- a/packages/ui/.gitignore +++ b/packages/ui/.gitignore @@ -1,3 +1,4 @@ node_modules/ dist/ .vite/ +src/renderer/public/logo.png diff --git a/packages/ui/package.json b/packages/ui/package.json index d56b42ad..acf819fc 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -7,6 +7,7 @@ "scripts": { "dev": "vite dev", "build": "vite build", + "build:pwa": "vite build -c vite.config.pwa.ts", "preview": "vite preview", "typecheck": "tsc --noEmit -p tsconfig.json" }, @@ -30,11 +31,13 @@ "solid-toast": "^0.5.0" }, "devDependencies": { + "@vite-pwa/assets-generator": "^1.0.2", "autoprefixer": "10.4.21", "postcss": "8.5.6", "tailwindcss": "3", "typescript": "^5.3.0", "vite": "^5.0.0", + "vite-plugin-pwa": "^1.2.0", "vite-plugin-solid": "^2.10.0" } } diff --git a/packages/ui/vite.config.pwa.ts b/packages/ui/vite.config.pwa.ts new file mode 100644 index 00000000..95172bf9 --- /dev/null +++ b/packages/ui/vite.config.pwa.ts @@ -0,0 +1,61 @@ +import { copyFileSync } from "fs" +import { defineConfig } from "vite" +import { VitePWA } from "vite-plugin-pwa" +import { resolve } from "path" +import baseConfig from "./vite.config" + +export default defineConfig({ + ...baseConfig, + plugins: [ + ...(baseConfig.plugins ?? []), + { + name: "copy-pwa-source-icon", + buildStart() { + // vite-pwa-assets requires the source image inside public/ + copyFileSync( + resolve(__dirname, "src/images/CodeNomad-Icon.png"), + resolve(__dirname, "src/renderer/public/logo.png"), + ) + }, + }, + VitePWA({ + registerType: "autoUpdate", + injectRegister: "auto", + pwaAssets: { + preset: "minimal-2023", + image: "public/logo.png", + }, + manifest: { + name: "CodeNomad", + short_name: "CodeNomad", + id: "/", + start_url: "/", + display: "standalone", + display_override: ["window-controls-overlay", "standalone"], + background_color: "#1a1a1a", + theme_color: "#1a1a1a", + }, + workbox: { + navigateFallback: null, + runtimeCaching: [ + { + urlPattern: /^\/api\/.*$/i, + handler: "NetworkFirst", + options: { + cacheName: "api-cache", + expiration: { maxEntries: 50, maxAgeSeconds: 60 }, + }, + }, + { + urlPattern: /.*\.(?:js|css|png|jpg|jpeg|svg|webp|woff2?)$/i, + handler: "CacheFirst", + options: { + cacheName: "asset-cache", + expiration: { maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 30 }, + }, + }, + ], + }, + }), + ], +})