---
id: task-19
title: Frontend dynamic integration — routes/nav/slots/ws + PluginSlot
status: done
wave: B
depends_on: [task-18]
plan: docs/plans/plugins/plan.md
---
# Frontend dynamic integration
## Title
Wire the plugin registry into the live app: dynamic SPA routes, nav items, slot
host component, WS handler bridge, and boot the plugin host on app start.
## Inputs
- `PLUGIN-API.md` "Integration points the app must add".
- task-18 exports: `PluginRegistry` singleton + `usePluginRegistry()`,
`installPluginGlobal`, `loadPlugins`, `buildHostApi`.
- Seams: `apps/web/src/spa-routes.tsx` (static route resolver — add registry
fallthrough before not-found), `apps/web/src/settings-routes.tsx`
(`/settings/plugins/{id}/*` → registry), app sidebar nav component (grep the
file that renders nav items, e.g. `components/app-sidebar/*`), `lib/ws/router.ts`
+ `lib/ws/client.ts` (forward decoded messages to registry WS handlers),
app root that reads boot payload and renders providers (grep for `readBootPayload`
/ `StateProvider` mount — likely `apps/web/app/layout.tsx` or a client bootstrap).
## Acceptance
1. `apps/web/components/plugins/plugin-slot.tsx`: `` renders
all `registry.getSlotComponents(name)` (each wrapped in an error boundary).
Mount `` in the task detail sidebar and
`` in the settings nav (minimal, additive).
2. `apps/web/components/plugins/plugin-nav-items.tsx`: `` renders
`registry.getNavItems()` (section main) in the sidebar; clicking navigates to
`item.path`.
3. SPA route resolver: unknown path → check `registry.getRoutes()`; render the
plugin component inside the normal app shell. Settings resolver: `/settings/
plugins/{id}/*` → registry settings routes.
4. WS bridge: incoming messages also dispatch to `registry.getWsHandlers(action)`.
5. Boot: on app start, after store + boot payload are ready,
`installPluginGlobal(...)` then `loadPlugins(bootPayload.plugins, buildHostApi)`
— gated on `useFeature("plugins")` / `bootPayload.plugins` present. Idempotent
(don't double-load on re-render).
6. Tests: PluginSlot renders registered components + isolates a throwing one;
nav items render + navigate; route resolver returns a registered plugin route;
WS bridge forwards to a registered handler.
## Files
- `apps/web/components/plugins/plugin-slot.tsx` + test
- `apps/web/components/plugins/plugin-nav-items.tsx` + test
- `apps/web/lib/plugins/boot.ts` (`bootPlugins(bootPayload, storeApi, feature)`) + test
- edits: `src/spa-routes.tsx`, `src/settings-routes.tsx`, sidebar nav file,
`lib/ws/router.ts`, app bootstrap file
## Verification
- `cd apps/web && pnpm run typecheck && pnpm lint && pnpm test -- plugin`
- `/mobile-parity`: nav items + slot render responsively.
## Output contract
Report each seam edited + how idempotent boot is guaranteed + slot names live.
Keep edits additive (don't regress existing routes/nav). Follow apps/web/AGENTS.md.
## Dependencies
task-18.