feat: make UI installable as a PWA (#1417)

* feat: make UI installable as a PWA

Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com>

* fix(pwa): address review feedback

- Regenerate bun.lock for new PWA deps (Vercel builds with bun).
- Switch SW to prompt registration so deploys don't reload tabs mid-run.
- Remove orphaned public/manifest.json; app links the generated /manifest.webmanifest.
- Drop json from workbox globPatterns to avoid precaching stray JSON.

Verified bun build emits sw.js, manifest.webmanifest, and precaches _shell.html.

* fix(pwa): bun.lock, prompt SW registration, tighten globPatterns

- Regenerate bun.lock for new PWA deps (Vercel builds with bun).
- Switch SW to prompt registration so deploys don't reload tabs mid-run.
- Drop json from workbox globPatterns to avoid precaching stray JSON.

Verified bun build emits sw.js, manifest.webmanifest, and precaches _shell.html.

---------

Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com>
This commit is contained in:
Johannes du Plessis 2026-06-04 15:52:06 -07:00 • committed by GitHub
parent 9754c2d791
commit 02cfdbda5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 3821 additions and 117 deletions

File diff suppressed because it is too large Load diff

View file

@ -57,7 +57,10 @@
"prettier-plugin-tailwindcss": "^0.7.2", "prettier-plugin-tailwindcss": "^0.7.2",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-plugin-pwa": "^1.3.0",
"vitest": "^4.1.0", "vitest": "^4.1.0",
"web-vitals": "^5.1.0" "web-vitals": "^5.1.0",
"workbox-build": "^7.4.1",
"workbox-window": "^7.4.1"
} }
} }

BIN
ui/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View file

@ -1,25 +0,0 @@
{
"short_name": "TanStack App",
"name": "Create TanStack App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

15
ui/src/client.tsx Normal file
View file

@ -0,0 +1,15 @@
import { StartClient } from "@tanstack/react-start/client"
import { StrictMode } from "react"
import { hydrateRoot } from "react-dom/client"
import { registerSW } from "virtual:pwa-register"
// prompt mode: a new SW installs in the background and takes over on the next
// load, so deploys never reload a tab mid-agent-run.
registerSW()
hydrateRoot(
document,
<StrictMode>
<StartClient />
</StrictMode>
)

View file

@ -1,22 +1,29 @@
import { HeadContent, Outlet, Scripts, createRootRoute } from "@tanstack/react-router"; import {
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; HeadContent,
import { TanStackDevtools } from "@tanstack/react-devtools"; Outlet,
import { QueryClientProvider } from "@tanstack/react-query"; Scripts,
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; createRootRoute,
import { useState } from "react"; } from "@tanstack/react-router"
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"
import { TanStackDevtools } from "@tanstack/react-devtools"
import { QueryClientProvider } from "@tanstack/react-query"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import { useState } from "react"
import appCss from "../styles.css?url"; import appCss from "../styles.css?url"
import { makeQueryClient } from "@/lib/query"; import { makeQueryClient } from "@/lib/query"
export const Route = createRootRoute({ export const Route = createRootRoute({
head: () => ({ head: () => ({
meta: [ meta: [
{ charSet: "utf-8" }, { charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" }, { name: "viewport", content: "width=device-width, initial-scale=1" },
{ name: "theme-color", content: "#000000" },
{ title: "open-swe" }, { title: "open-swe" },
], ],
links: [ links: [
{ rel: "stylesheet", href: appCss }, { rel: "stylesheet", href: appCss },
{ rel: "manifest", href: "/manifest.webmanifest" },
{ rel: "icon", type: "image/png", href: "/favicon.png" }, { rel: "icon", type: "image/png", href: "/favicon.png" },
{ rel: "apple-touch-icon", href: "/apple-touch-icon.png" }, { rel: "apple-touch-icon", href: "/apple-touch-icon.png" },
], ],
@ -24,14 +31,16 @@ export const Route = createRootRoute({
notFoundComponent: () => ( notFoundComponent: () => (
<main className="container mx-auto p-4 pt-16"> <main className="container mx-auto p-4 pt-16">
<h1 className="text-2xl font-medium">404</h1> <h1 className="text-2xl font-medium">404</h1>
<p className="text-muted-foreground">The requested page could not be found.</p> <p className="text-muted-foreground">
The requested page could not be found.
</p>
</main> </main>
), ),
shellComponent: RootDocument, shellComponent: RootDocument,
}); })
function RootDocument({ children }: { children: React.ReactNode }) { function RootDocument({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => makeQueryClient()); const [queryClient] = useState(() => makeQueryClient())
return ( return (
<html lang="en"> <html lang="en">
<head> <head>
@ -43,7 +52,10 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<TanStackDevtools <TanStackDevtools
config={{ position: "bottom-right" }} config={{ position: "bottom-right" }}
plugins={[ plugins={[
{ name: "Tanstack Router", render: <TanStackRouterDevtoolsPanel /> }, {
name: "Tanstack Router",
render: <TanStackRouterDevtoolsPanel />,
},
]} ]}
/> />
<ReactQueryDevtools initialIsOpen={false} /> <ReactQueryDevtools initialIsOpen={false} />
@ -51,5 +63,5 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<Scripts /> <Scripts />
</body> </body>
</html> </html>
); )
} }

View file

@ -16,7 +16,7 @@
"jsx": "react-jsx", "jsx": "react-jsx",
"module": "ESNext", "module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"], "lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"], "types": ["vite/client", "vite-plugin-pwa/client"],
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",

View file

@ -5,17 +5,57 @@ import viteReact from "@vitejs/plugin-react"
import viteTsConfigPaths from "vite-tsconfig-paths" import viteTsConfigPaths from "vite-tsconfig-paths"
import tailwindcss from "@tailwindcss/vite" import tailwindcss from "@tailwindcss/vite"
import { nitro } from "nitro/vite" import { nitro } from "nitro/vite"
import { VitePWA } from "vite-plugin-pwa"
const config = defineConfig({ const config = defineConfig({
plugins: [ plugins: [
devtools(), devtools(),
nitro(), nitro(),
// this is the plugin that enables path aliases
viteTsConfigPaths({ viteTsConfigPaths({
projects: ["./tsconfig.json"], projects: ["./tsconfig.json"],
}), }),
tailwindcss(), tailwindcss(),
tanstackStart({ spa: { enabled: true } }), tanstackStart({ spa: { enabled: true } }),
VitePWA({
injectRegister: false,
registerType: "prompt",
outDir: ".output/public",
integration: {
closeBundleOrder: "pre",
},
manifest: {
id: "/",
name: "Open SWE",
short_name: "Open SWE",
description: "Open-source coding agents for Slack, Linear, and GitHub.",
start_url: "/",
scope: "/",
display: "standalone",
theme_color: "#000000",
background_color: "#ffffff",
icons: [
{
src: "/favicon.png",
sizes: "192x192",
type: "image/png",
},
{
src: "/logo512.png",
sizes: "512x512",
type: "image/png",
purpose: "any maskable",
},
],
},
workbox: {
navigateFallback: "/_shell.html",
navigateFallbackDenylist: [/^\/dashboard\/api\//, /^\/_serverFn\//],
globPatterns: ["**/*.{js,css,png,svg,ico,webmanifest}"],
additionalManifestEntries: [
{ url: "/_shell.html", revision: new Date().toISOString() },
],
},
}),
viteReact(), viteReact(),
], ],
}) })

File diff suppressed because it is too large Load diff