Skip to content

Latest commit

 

History

History
143 lines (109 loc) · 8.09 KB

File metadata and controls

143 lines (109 loc) · 8.09 KB

Electron Guidelines

Navigation: Root AGENTS.md | CLAUDE.mdElectron

Read docs/DEVELOPMENT_STANDARDS.md §12 Electron Security first. It is the canonical source for Electron security, IPC, sandboxing, and platform standards. The rules below are the area-specific overlay.

Desktop application wrapping the NodeTool web UI with native capabilities (local file system, SQLite, Python/Conda integration).

Prerequisites

  • Node.js 22.22.1 required. Matches Electron 39's embedded Node so dev and packaged app run on the same Node version (same APIs, same V8). The backend runs on vanilla Node, so better-sqlite3 (the only source-built native module) is rebuilt against Node headers by electron/scripts/rebuild-native.mjs, invoked from the repo root postinstall.
  • Use nvm use from the repo root (reads .nvmrc).
  • npm install/npm ci runs the rebuild automatically (from the root postinstall, after reify). To force a rebuild: npm run rebuild:native (root) or npm --prefix electron run rebuild:native.

Build, Lint & Test

cd electron
npm install              # Install dependencies
npm start                # Start Electron app (requires built web)
npm run build            # Production build (tsc + vite + electron-builder)
npm run vite:build       # Vite build only (main + preload)
npm run typecheck        # TypeScript check
npm run lint             # ESLint
npm run lint:fix         # Auto-fix lint issues
npm test                 # Run unit tests
npm run test:coverage    # Unit tests with coverage

Packaged file layout

The packaged backend is one esbuild bundle (resources/backend/server.mjs), so every file the backend resolves relative to import.meta.url — provider *-manifest.json files, example workflows, package:// assets — must be staged next to it by the repo-root scripts/bundle-backend.mjs. In dev these resolve through normal package resolution, so a staging gap only breaks the packaged app.

Data files a package loads at runtime go through one guarded path: declare the file in PACKAGE_RUNTIME_ASSETS (packages/config/src/package-asset-registry.ts) and load it with loadPackageAssetJson from @nodetool-ai/config — never with readFileSync(new URL(...)) or createRequire directly. The accessor resolves both layouts, rejects unregistered files immediately in dev, and records outcomes for diagnostics (getPackageAssetResolutions). The registry also drives packaging: the repo-root scripts/bundle-backend.mjs stages every entry (and fails on a dist/*-manifest.json that isn't registered), and scripts/verify-backend-bundle.mjs (run automatically after bundling, also npm run verify:backend-bundle) re-checks the final artifact — manifests referenced by server.mjs, examples, assets, webgpu dawn binaries.

Development Modes

# From repo root:
npm run electron            # Build web + Electron, then start
npm run electron:dev        # Start against web Vite server (requires active conda env)

Tests

This workspace has no Playwright suite — main-process behavior is covered by the Jest tests in src/__tests__/. Browser-level E2E lives in web/.

Key Technologies

Library Version Purpose
Electron 39.8.10 Desktop shell
React 19.2 UI framework
TypeScript 5.9 Type safety
Zustand 5.0 State management
Vite 8.0 Build tool (main + preload processes)
better-sqlite3 Local SQLite database
sharp Image processing
sqlite-vec Vector embeddings

Architecture

electron/src/
├── main.ts              # Main process entry point
├── preload.ts           # Preload script (contextBridge)
├── config.ts            # Conda environment detection
├── WorkflowRunner.ts    # Workflow execution in main process
└── components/          # React components for Electron UI

Process model: Main process (Node.js) ↔ Preload script (contextBridge) ↔ Renderer process (React).

Vaults (multiple databases)

A vault is a switchable, isolated data store — its own SQLite database plus its own assets and vector-store directories. It lets a user keep separate sets of workflows/data apart and switch between them from the desktop app. This is distinct from the in-database nodetool_workspaces concept (a per-user working directory for file tools); vaults sit one level above the database.

  • vaults.ts — source of truth: the vault list + active id are persisted in settings.yaml (vaults, activeVaultId). The built-in Default vault has null paths, meaning "use the backend defaults", so existing installs are untouched. getActiveVaultEnv() returns DB_PATH/ASSET_FOLDER/ VECTORSTORE_DB_PATH overrides for the active vault.
  • server.ts merges those overrides into the backend's environment at startup (skipped when an external DATABASE_URL is set — vaults are SQLite-only).
  • vaultSwitch.tsapplyVaultSwitch(id): persist active id → restart the backend (so it opens the new database) → re-register shortcuts → reload the main window. Restarting the process is intentional: the backend holds its SQLite connection in a singleton, so a clean restart beats a live swap.
  • UI: a native Vaults menu (quick switch + "Manage Vaults…", which opens the in-app settings) and a Vaults section in the web settings (web/src/components/menus/VaultsSettings.tsx) for create / rename / delete / switch, over window.api.vaults.* (IPC VAULT_*).

Rules

Security (Critical — non-negotiable)

See DEVELOPMENT_STANDARDS §12 for the full checklist (CSP, auto-update signing, setWindowOpenHandler, sandbox flag, webSecurity).

  • contextIsolation: true on every BrowserWindow. No exceptions.
  • nodeIntegration: false on every BrowserWindow. No exceptions.
  • sandbox: true on renderer windows whenever possible.
  • webSecurity: true — never disable.
  • Always use contextBridge.exposeInMainWorld for IPC. Never assign to window directly.
  • Validate every IPC input with Zod in the main process before acting on it. Untrusted renderer is the attacker model.
  • shell.openExternal only with an allowlisted URL scheme (https:, mailto:). Never pass user input directly.
  • webContents.setWindowOpenHandler denies all by default; allow specific URLs only.
  • No eval, no new Function anywhere in main or preload.
  • Auto-update must verify code signatures (electron-updater with publisher verification).
  • CSP is set in production builds. target: drop 'unsafe-inline' for scripts.

IPC Communication

  • Define IPC channels as string constants — never use string literals inline.
  • Use ipcMain.handle / ipcRenderer.invoke for request-response (async).
  • Use webContents.send / ipcRenderer.on for main-to-renderer events.
  • Always clean up IPC listeners when windows are destroyed.
  • IPC handlers wrap their body in try/catch and return a discriminated { ok: true, data } | { ok: false, error } — never throw across the IPC boundary.
  • Every IPC handler is span-instrumented for tracing. See DEVELOPMENT_STANDARDS §17.

Platform Code

  • Guard platform-specific code with process.platform checks (darwin, win32, linux).
  • Test on macOS, Windows, and Linux.
  • Use path.join() for file paths — never hardcode path separators.

Performance

  • Use worker threads for heavy operations in the main process.
  • Implement proper cleanup on app quit (close DB connections, stop servers).
  • Use lazy loading for non-critical modules in the main process.
  • The Electron app bundles the backend server — it starts on launch and stops on quit.

Conda Integration

  • The app detects the active Conda environment via config.ts.
  • Python-based nodes (HuggingFace, MLX) require a Conda environment with the correct dependencies.
  • See environment.yml in the repo root for the Conda spec.