* refactor(desktop): Capabilities is a module at /capabilities The Capabilities page lived in `app/skills/`, was exported as `SkillsView` and was routed at `/skills`, although Skills is only one of its four tabs. The name sent every reader to the wrong place. - `app/skills/` becomes `app/capabilities/`, with one folder per topic: `skills/`, `plugins/`, `mcp/`, and `catalog/` for the browser that the skills and plugins tabs share. - `SkillsView` becomes `CapabilitiesView`, in the plugin SDK too. The bundled bots plugin reads the new export name. - The route, its id and its view become `/capabilities` and `capabilities`. The keybind action becomes `nav.capabilities` and the sidebar item id follows, with its i18n keys in every locale. - The `hermes://open/...` link that the plugin notice fires follows. No alias is kept for the old route. A remembered `/skills` route no longer matches a page; the restore path already drops a route it cannot validate and opens the last session. No behaviour change otherwise. * refactor(desktop): each Capabilities tab owns its list, detail and writes `CapabilitiesView` was one 770-line function. It held the tab routing, the whole Skills tab and the whole Tools tab, while the Plugins and MCP tabs already lived in their own files. - `capabilities/index.tsx` is the page shell: tab selection, the search header, the profile and connection scope, the refresh hotkey, and one table that maps a tab to its component (1190 lines down to 230). - `skills/` and `toolsets/` each hold their tab, detail pane, data hooks and helpers. `scope-selector.tsx` holds the scope hook and its selector. `primitives.tsx` holds what two tabs share. - The shell still fetches the two installed lists, because the tab pills count them for the tab the user is not on. Query keys are unchanged; `store/hub-actions.ts` imports the skills key instead of copying it. - Every tab is keyed on the scope, so a profile or connection switch mounts a fresh tab. This replaces the epoch counters and manual resets. One small change follows: a return to a scope seen before selects the first row, not the row selected last time.
Desktop MCP OAuth integration checks
Run from a checkout with the Python MCP dependencies and Desktop Node dependencies installed. Outputs are local receipts; keep them outside the checkout. Neither harness uses real provider credentials.
python3 evals/desktop_mcp_oauth/backend_http_fixture.py --repo . --output /tmp/mcp-backend.json
node evals/desktop_mcp_oauth/renderer_lifecycle.mjs "$PWD" /tmp/mcp-renderer.json approved
node evals/desktop_mcp_oauth/renderer_lifecycle.mjs "$PWD" /tmp/mcp-scope.json cancel
node evals/desktop_mcp_oauth/renderer_lifecycle.mjs "$PWD" /tmp/mcp-unmount.json unmount
The backend harness creates disposable HOME/HERMES_HOME directories and a local HTTP OAuth/MCP provider. It exercises production session functions, discovery, dynamic registration, PKCE code exchange, token persistence and fresh-process authenticated MCP access. Wrong state, callback replay, wrong server, wrong profile and cancellation are negative controls. It removes temporary token stores and reports only booleans/paths, never token values.
The renderer harness bundles the real McpTab and its dependencies in Chromium, runs the production native loopback listener with a registration-only Electron IPC adapter, and uses a fixture WebSocket backend. cancel changes the component's scope; unmount removes it entirely. Both must cancel the original backend session and close its native listener without relaying a callback. approved must use the native relay, not REST auth. Set CHROMIUM_EXECUTABLE to use an existing Chromium executable; otherwise Playwright's installed browser is used.
These are complementary integration probes, not a single end-to-end Electron/provider test. The renderer backend and Electron registration boundary are fixtures; the Python probe uses production session functions rather than gateway RPC transport. No native Electron application launch or hosted-provider consent is claimed.
For A/B, run the same renderer harness against a baseline checkout by replacing its first positional argument. The approved assertion fails before the shared relay change. For the lifecycle regression, a checkout immediately before the scoped-tab cleanup fails both abandonment assertions. Backend cross-profile callbacks must be rejected even when the session ID and valid state are supplied; the original owner must still be able to finish afterward.