| name | desktop |
| description | Pure Electron work for Grida Desktop: 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, and GRIDA-SEC-004. For AgentHost, sessions, tools, providers, or `packages/grida-ai-agent/**`, use `agent-system`. |
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 for the AgentHost core,
sessions, workspaces, providers, tool execution, HTTP routes, and tests in
packages/grida-ai-agent.
Adjacent: security for the GRIDA-SEC-004 trust
boundary, code-react 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/*.
Skip this skill when the change is core agent behavior that can be tested
without Electron. Use agent-system for packages/grida-ai-agent/**,
AgentHost HTTP routes, sessions, files/workspaces, providers, tools, runtime,
skills, and BYOK/secrets.
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:
pnpm dev in desktop/ does not serve the renderer. Run the editor dev
server on :3000 separately.
- The preload is path-scoped. Outside
/desktop/*, window.grida is
intentionally undefined.
- Keep
contextIsolation: true, nodeIntegration: false, and
sandbox: true.
- Electron code adapts and supervises native shell behavior. Core behavior
that can be tested without Electron belongs in
agent-system.
Running Locally
Use two terminals:
pnpm --filter editor dev
cd desktop && pnpm dev
cd desktop && pnpm 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:
lsof -iTCP:9222 -sTCP:LISTEN
ps -A | grep grida/desktop/node_modules/electron
Kill cleanly:
pkill -f "grida/desktop/node_modules/electron"
pkill -f "grida/desktop/node_modules/.bin/electron-forge"
CDP / Playwright Verification
Launch with CDP enabled:
cd desktop && pnpm dev -- --remote-debugging-port=9222
The -- is required so Forge forwards the flag to Electron.
Probe targets:
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:
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:
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:
const bridge = useDesktopBridge();
if (!bridge) return null;
return <Button onClick={() => bridge.dialog.open(...)} />;
Soft branch:
const bridge = useDesktopBridge();
const onSave = bridge
? () => bridge.files.write(docId, svg)
: () => downloadAsBlob(svg);
DesktopBridgeGate in editor/scaffolds/desktop/ gates whole desktop pages.
Verify from CDP:
await page.evaluate(() => ({
bridge: typeof window.grida,
version: window.grida?.app?.version,
caps: window.grida?.caps,
}));
The preload runs after Next.js streams the first HTML. A screenshot taken
immediately after load can catch the temporary CTA before hydration observes
window.grida; wait for a post-hydration element or a short timeout before
judging the bridge state.
Boundaries
| Concern | Lives in | Notes |
|---|
| Window/menu/dialog wiring | desktop/src/main/** | Native shell behavior |
| Protocol and file routing | desktop/src/main/**, desktop/Info.plist | grida://, open-file, argv queue |
| Agent sidecar supervision | desktop/src/main/**, desktop/src/agent-sidecar.ts | Adapter only; core is agent-system |
| Preload bridge | desktop/src/preload.ts | Path-scoped window.grida, auth held in closure |
| Renderer desktop routes | editor/app/desktop/** | No server actions, no next/headers |
| Renderer scaffolds | editor/scaffolds/desktop/** | UI using @/lib/desktop/* |
| Typed bridge client | editor/lib/desktop/** | Pure TS client, no server-only imports |
Do not put OPFS or IndexedDB desktop storage in editor/app/desktop/**.
Desktop storage goes through the bridge and agent host.
Do not add core behavior in Electron main/preload. If it is testable without
Electron, move it to the agent-system package.
Security Boundary: GRIDA-SEC-004
When changing the bridge or navigation rules, review SECURITY.md and the
security skill. The relevant desktop layers are:
- Path-scoped preload:
window.grida only on /desktop or /desktop/* at
document load time.
- Navigation allowlist in
desktop/src/window.ts.
- CSP-strict
/desktop/* route group.
- Per-launch bridge credentials held in the preload closure, never exposed on
window.grida.
- No bridge method may exfiltrate secrets or run arbitrary local code.
Verification
For non-trivial desktop changes:
- Run both the editor dev server and Electron shell.
- Use CDP / Playwright to touch the changed surface.
- Cold reload or relaunch Electron once to exercise preload and hydration.
- Run owner checks:
- Electron adapter:
pnpm --dir desktop typecheck and
pnpm --dir desktop test.
- Renderer desktop UI:
pnpm --filter editor typecheck.
- Agent core changes: switch to
agent-system.
Pointers
- Electron main:
desktop/src/main.ts
- Window and navigation policy:
desktop/src/window.ts
- Preload bridge:
desktop/src/preload.ts
- Forge config:
desktop/forge.config.ts
- File associations:
desktop/Info.plist
- Renderer routes:
editor/app/desktop/
- Renderer scaffolds:
editor/scaffolds/desktop/
- Typed bridge client:
editor/lib/desktop/bridge.ts
- CSP for
/desktop/*: editor/proxy.ts
- Threat model:
SECURITY.md (GRIDA-SEC-004)