--- name: desktop description: >- Grida Desktop Electron shell and release-impact work: BrowserWindow, preload, `window.grida`, menus, protocol/deep links, file associations, Forge, path-scoped bridge security, Electron-only UI bugs, and CDP / Playwright verification. Use for `desktop/`, `editor/app/desktop/**`, `editor/scaffolds/desktop/**`, `editor/lib/desktop/**`, `/desktop/*` CSP, GRIDA-SEC-004, and deciding whether linked-package or hosted-renderer changes require a native Desktop version bump or coordinated release. For implementing daemon/agent-tenant core behavior, use `agent-system` as well. --- # Grida Desktop - Electron Shell This skill is for the Electron shell only: main process, windows, menus, preload, native protocol/file entry points, and the renderer bridge surface. The renderer is still the editor's Next.js app under `editor/app/desktop/`; Electron URL-loads it from `http://localhost:3000/desktop/*` in dev and `https://grida.co/desktop/*` in prod. Use [`agent-system`](../agent-system/SKILL.md) for the daemon + agent-tenant core, sessions, workspaces, providers, tool execution, HTTP routes, and tests in `packages/grida-daemon` and `packages/grida-ai-agent`. > Adjacent: [`security`](../security/SKILL.md) for the GRIDA-SEC-004 trust > boundary, [`code-react`](../code-react/SKILL.md) for React code under > `editor/app/desktop/**` and `editor/scaffolds/desktop/**`. ## When to use this skill - Editing `desktop/src/**`, `desktop/forge.config.ts`, `desktop/Info.plist`, or desktop packaging/dev-server wiring. - Changing `desktop/src/main.ts`, `window.ts`, `menu.ts`, preload, bridge contract, app branding, host-app integration, deep links, file associations, single-instance behavior, or multi-window routing. - Touching `editor/app/desktop/**`, `editor/scaffolds/desktop/**`, or `editor/lib/desktop/**` because the UI depends on `window.grida`. - Debugging "works in browser but not in desktop" or "only repros in Electron" issues. - Verifying the desktop app through CDP / Playwright. - Touching CSP or proxy behavior for `/desktop/*`. - Auditing whether linked-package or hosted Desktop renderer changes require a native version bump or coordinated release. Use `agent-system` to implement core agent behavior that can be tested without Electron: `packages/grida-daemon/**`, `packages/grida-ai-agent/**`, daemon HTTP routes, sessions, files/workspaces, providers, tools, runtime, skills, and BYOK/secrets. Also use this skill when auditing whether that work changes the packaged Desktop payload or its compatibility with the hosted renderer. --- ## Shape ``` Electron main (desktop/src/main.ts) - single-instance lock - BrowserWindow.loadURL(`${EDITOR_BASE_URL}/desktop/welcome`) - grida:// protocol router, open-file/argv queue - starts/supervises the AgentSidecar adapter | | contextBridge.exposeInMainWorld("grida", ...) | only for /desktop or /desktop/* v Renderer (editor/app/desktop/**) - DesktopBridgeGate renders desktop UI only when bridge is present - web visitors get OpenInDesktopCta - CSP strict, no analytics, no third-party scripts | v AgentSidecar loopback service - owned by the agent-system skill ``` Four invariants: 1. `pnpm dev` in `desktop/` does not serve the renderer. Run the editor dev server on `:3000` separately. 2. The preload is path-scoped. Outside `/desktop/*`, `window.grida` is intentionally undefined. 3. Keep `contextIsolation: true`, `nodeIntegration: false`, and `sandbox: true`. 4. Electron code adapts and supervises native shell behavior. Core behavior that can be tested without Electron belongs in `agent-system`. --- ## Running Locally Authenticated renderer flows use the repository's default [local development setup](../../../CONTRIBUTING.md#authenticated-local-development): local Supabase plus `NEXT_PUBLIC_GRIDA_USE_INSIDERS_AUTH=1` in `editor/.env.local`. Do not point the editor at hosted Supabase unless authentication itself is under test. The editor flag selects the insiders sign-in path. The Electron `dev:insiders` command selects Insiders app branding; it does not configure renderer authentication. Use two terminals: ```sh # Terminal 1 - renderer pnpm --filter editor dev # Terminal 2 - Electron shell pnpm --dir desktop dev # Optional Insiders-branded shell: pnpm --dir desktop dev:insiders ``` `EDITOR_BASE_URL` lives in `desktop/src/env.ts`. It resolves to `http://localhost:3000` in development and `https://grida.co` otherwise. `electron-forge start` can return the shell prompt while Electron stays alive. Confirm with: ```sh lsof -iTCP:9222 -sTCP:LISTEN ps -A | grep grida/desktop/node_modules/electron ``` Kill cleanly: ```sh pkill -f "grida/desktop/node_modules/electron" pkill -f "grida/desktop/node_modules/.bin/electron-forge" ``` --- ## CDP / Playwright Verification Launch with CDP enabled: ```sh cd desktop && pnpm dev -- --remote-debugging-port=9222 ``` The `--` is required so Forge forwards the flag to Electron. Probe targets: ```sh curl -s http://127.0.0.1:9222/json/version curl -s http://127.0.0.1:9222/json ``` Preferred client: - One-off probe: direct CDP with Node's built-in `WebSocket` and `fetch`. - Scripted verification: `chromium.connectOverCDP("http://127.0.0.1:9222")`. - Owned Electron lifecycle or native menus/dialogs: Playwright `_electron` plus `electron-playwright-helpers`. Minimal probe: ```js import { chromium } from "playwright-core"; const browser = await chromium.connectOverCDP("http://127.0.0.1:9222"); const page = browser.contexts()[0].pages()[0]; console.log("url:", page.url()); console.log("hasBridge:", await page.evaluate(() => typeof window.grida)); await page.screenshot({ path: "/tmp/grida-desktop.png" }); await browser.close(); ``` If `playwright-core` does not resolve at repo root, run the probe from `editor/`, import the package from pnpm's `.pnpm` path, or add Playwright to `desktop/package.json`. Inspect main process: ```sh cd desktop && pnpm dev -- --inspect-electron ``` Then open `chrome://inspect/#devices`, add `localhost:5858`, and inspect. --- ## Bridge The renderer's native-capability surface is the typed client in `editor/lib/desktop/bridge.ts`. React code reads it via `useDesktopBridge()`, an SSR-safe `useSyncExternalStore` wrapper. Hard gate: ```tsx const bridge = useDesktopBridge(); if (!bridge) return null; return