feat(site): add PWA manifest and mobile meta tags for agents page (#22650)

Adds progressive web app support for the agents page so it can be
installed as a standalone app on mobile/desktop.

## Changes

- **`manifest.json`** — Web app manifest with `display: standalone`,
`start_url: /agents`, Coder theme colors
- **PWA icons** — 192x192, 512x512 PNGs + 180x180 apple-touch-icon,
rendered from the existing favicon SVG
- **`index.html`** — Added manifest link, apple-touch-icon, and mobile
web app meta tags (`apple-mobile-web-app-capable`,
`mobile-web-app-capable`, `apple-mobile-web-app-status-bar-style`,
title)
- **Service worker** — `notificationclick` now focuses an existing
agents tab or opens `/agents` in a new window

## Testing

1. Open `/agents` on a mobile device
2. Use browser "Add to Home Screen" / "Install App"
3. App should launch in standalone mode pointing at the agents page
4. Push notifications should navigate to the agents page on click
This commit is contained in:
Kyle Carberry
2026-03-05 08:55:38 -05:00
committed by GitHub
parent c308db805d
commit f1b3eef834
7 changed files with 109 additions and 1 deletions
+2
View File
@@ -58,6 +58,7 @@ import {
hasConfiguredModelsInCatalog,
} from "./modelOptions";
import { useAgentsPageKeybindings } from "./useAgentsPageKeybindings";
import { useAgentsPWA } from "./useAgentsPWA";
import { WebPushButton } from "./WebPushButton";
/** @internal Exported for testing. */
@@ -104,6 +105,7 @@ export interface AgentsOutletContext {
}
const AgentsPage: FC = () => {
useAgentsPWA();
const queryClient = useQueryClient();
const navigate = useNavigate();
const { agentId } = useParams();
+70
View File
@@ -0,0 +1,70 @@
import { useEffect } from "react";
/**
* Injects PWA-related <head> tags while the Agents page is mounted
* (manifest, apple-touch-icon, mobile-web-app metas) and tweaks the
* viewport to prevent zooming. Everything is cleaned up on unmount.
*/
export function useAgentsPWA() {
useEffect(() => {
// -- Injected elements --------------------------------------------------
const manifest = document.createElement("link");
manifest.rel = "manifest";
manifest.href = "/manifest.json";
const appleTouchIcon = document.createElement("link");
appleTouchIcon.rel = "apple-touch-icon";
appleTouchIcon.href = "/apple-touch-icon.png";
const mobileWebAppCapable = document.createElement("meta");
mobileWebAppCapable.name = "mobile-web-app-capable";
mobileWebAppCapable.content = "yes";
const appleMobileWebAppCapable = document.createElement("meta");
appleMobileWebAppCapable.name = "apple-mobile-web-app-capable";
appleMobileWebAppCapable.content = "yes";
const appleMobileWebAppStatusBarStyle = document.createElement("meta");
appleMobileWebAppStatusBarStyle.name =
"apple-mobile-web-app-status-bar-style";
appleMobileWebAppStatusBarStyle.content = "black-translucent";
const appleMobileWebAppTitle = document.createElement("meta");
appleMobileWebAppTitle.name = "apple-mobile-web-app-title";
appleMobileWebAppTitle.content = "Agents";
const injected = [
manifest,
appleTouchIcon,
mobileWebAppCapable,
appleMobileWebAppCapable,
appleMobileWebAppStatusBarStyle,
appleMobileWebAppTitle,
];
for (const el of injected) {
document.head.appendChild(el);
}
// -- Viewport override --------------------------------------------------
const viewport = document.querySelector<HTMLMetaElement>(
'meta[name="viewport"]',
);
const prevViewportContent = viewport?.content ?? "";
if (viewport) {
viewport.content =
"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no";
}
// -- Cleanup ------------------------------------------------------------
return () => {
for (const el of injected) {
el.remove();
}
if (viewport) {
viewport.content = prevViewportContent;
}
};
}, []);
}
+13 -1
View File
@@ -33,7 +33,19 @@ self.addEventListener("push", (event) => {
);
});
// Handle notification click
// Handle notification click — navigate to the agents page
self.addEventListener("notificationclick", (event) => {
event.notification.close();
event.waitUntil(
self.clients
.matchAll({ type: "window", includeUncontrolled: true })
.then((clientList) => {
for (const client of clientList) {
if (client.url.includes("/agents") && "focus" in client) {
return client.focus();
}
}
return self.clients.openWindow("/agents");
}),
);
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+24
View File
@@ -0,0 +1,24 @@
{
"name": "Coder Agents",
"short_name": "Agents",
"description": "AI coding agents powered by Coder",
"start_url": "/agents",
"scope": "/",
"display": "standalone",
"background_color": "#17172E",
"theme_color": "#17172E",
"icons": [
{
"src": "/pwa-icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/pwa-icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB