mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
@@ -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 |
@@ -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 |
Reference in New Issue
Block a user