Skip to content

Commit 0e06acf

Browse files
JohnMcLearclaude
andcommitted
docs(agents): comprehensive guide of architecture, conventions, gotchas
Replaces the minimal AGENTS.md with a full session-experience guide so future Claude/AI work picks up the architecture and the 17 known gotchas (CSP+HMR, exactOptionalPropertyTypes, Vitest workspace path, TS composite paths/baseUrl, electron-log lazy load, Playwright menu accelerator strategy, pad-view z-stacking, tab visibility invariant, language URL param, Zustand selector stability, quit-time destroyed windows, pnpm action-setup conflict, menu handler completeness, etc.) without re-discovering them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 889cdb5 commit 0e06acf

1 file changed

Lines changed: 118 additions & 16 deletions

File tree

AGENTS.md

Lines changed: 118 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,129 @@
11
# AGENTS.md
22

3-
Guidance for AI agents working in this repo.
3+
Guidance for AI agents (Claude, Copilot, Cursor, etc.) working in this repo.
4+
5+
## TL;DR
6+
7+
- TypeScript strict end-to-end, React 18 + Zustand renderer, Electron 35 main.
8+
- All persistent state lives in main-process stores; renderer talks to disk only via IPC.
9+
- IPC payloads are Zod-validated; channels live in `src/shared/ipc/channels.ts`.
10+
- Pad content runs in `WebContentsView`s isolated by per-workspace partitions.
11+
- Tests are click-driven and outcome-asserting (no fluffy mounts).
12+
- `pnpm test` (vitest), `pnpm test:e2e` (Playwright Electron), `pnpm package` (electron-builder).
413

514
## Dev loop
615

7-
- Install: `pnpm install`
8-
- Run: `pnpm dev`
9-
- Test: `pnpm test` (unit) and `pnpm test:e2e` (E2E — needs Etherpad on :9003)
10-
- Typecheck: `pnpm typecheck`
11-
- Lint: `pnpm lint`
12-
- Format: `pnpm format`
13-
- Package: `pnpm package`
16+
| Command | Purpose |
17+
|---|---|
18+
| `pnpm install` | Install deps. Uses pnpm; canonical version pinned in `packageManager`. |
19+
| `pnpm dev` | electron-vite dev mode. Main + preload bundled, renderer served at :5173. |
20+
| `pnpm typecheck` | `tsc -b` across all 4 leaf tsconfigs. Must exit 0. |
21+
| `pnpm lint` | ESLint over `src/` + `tests/`. |
22+
| `pnpm format` | Prettier write. |
23+
| `pnpm test` | Vitest unit + component tests (single run). |
24+
| `pnpm test:watch` | Vitest watch mode. |
25+
| `pnpm test:e2e` | Playwright Electron E2E. Requires Etherpad on `:9003` (auto-spun by global setup). |
26+
| `pnpm build` | Production build → `out/main`, `out/preload`, `out/renderer`. |
27+
| `pnpm package` | Build + electron-builder → AppImage + .deb under `release/`. |
28+
29+
After main-process source changes, **restart `pnpm dev`** — Vite HMR only covers the renderer. Main + preload changes need a full restart.
30+
31+
## Architecture
32+
33+
- **Main** (`src/main/`): app lifecycle, native menu, single-instance lock, stores, IPC handlers, BaseWindow + WebContentsView orchestration. Bundled to CJS (`out/main/index.cjs`).
34+
- **Preload** (`src/preload/`): one file. `contextBridge.exposeInMainWorld('etherpadDesktop', api)`. Bundled to CJS.
35+
- **Renderer** (`src/renderer/`): React + Zustand shell. Communicates with main only via the typed `etherpadDesktop` bridge.
36+
- **Shared** (`src/shared/`): types + Zod schemas + IPC channel constants. Imported by all 3 contexts via the `@shared/*` alias.
37+
- **Window model**: each app window is a `BaseWindow` containing one shell `WebContentsView` (the React UI) + N pad `WebContentsView`s (one per open tab). Pad views are positioned over the shell's "main area" rect by `TabManager`.
38+
- **Visibility invariant** (`TabManager`): exactly one pad view is `setVisible(true)` at a time = the active tab. All others are `setVisible(false)`. When a dialog is open, all pad views are hidden so the dialog (which lives in the shell view's HTML) shows through.
39+
- **Per-workspace partitions**: `partitionFor(workspaceId)``'persist:ws-${id}'`. Cookies, localStorage, IndexedDB are isolated per workspace.
1440

1541
## Conventions
1642

17-
- TypeScript strict; no JS in the source tree.
18-
- All persistent state goes through main-process stores. Renderer never touches disk.
19-
- IPC payloads are Zod-validated in main; channels are defined in `src/shared/ipc/channels.ts`.
20-
- Each `WebContentsView` is created via `pad-view-factory.ts` — that is the seam for future offline + embedded-server work.
21-
- E2E tests use port `9003` for Etherpad, never `9001`.
43+
- TypeScript strict; no `.js` files in source tree.
44+
- All persistent state goes through main-process stores (`workspace-store`, `pad-history-store`, `settings-store`, `window-state-store`). Renderer NEVER touches disk.
45+
- IPC payloads are Zod-validated in main via `wrapHandler(channel, schema, handler)`. Channels are constants in `src/shared/ipc/channels.ts` (`CH.WORKSPACE_LIST` etc.).
46+
- Each `WebContentsView` is created via `pad-view-factory.ts` — single seam for future offline-cache / embedded-server work.
47+
- E2E tests use port `9003` for the Etherpad fixture (NEVER `9001` — that's reserved for the user's ad-hoc local testing).
48+
- Commits: conventional style (`feat(scope): …`, `fix(scope): …`, `test(e2e): …`, `docs(scope): …`).
49+
- Push to `origin/feat/linux-mvp` after every fix or feature commit. Don't batch.
2250

2351
## House rules
2452

25-
- Match the spec at `docs/superpowers/specs/2026-05-03-etherpad-desktop-linux-mvp-design.md`.
26-
- Never log pad content or pad names. Workspace IDs (UUIDs) are fine.
27-
- Never use the name "etherpad-lite" in new code, packaging, or docs — the project is "etherpad".
53+
- Match the spec at `docs/superpowers/specs/2026-05-03-etherpad-desktop-linux-mvp-design.md` and the plan at `docs/superpowers/plans/2026-05-03-etherpad-desktop-linux-mvp.md`.
54+
- Never log pad content, pad names, or server URLs. Workspace IDs (UUIDs) are fine.
55+
- Never use the name "etherpad-lite" in new code/packaging/docs — the project is "etherpad". Legacy URL refs in code (e.g. `github.com/ether/etherpad-lite` for upstream attribution) can stay.
56+
- `i18n` for ALL user-facing strings. Desktop shell uses `t.<section>.<key>` in `src/renderer/i18n/`. Pad webview honours `?lang=<code>` URL params.
57+
58+
## Testing standards
59+
60+
The bar is **solid, not fluffy**:
61+
62+
- Tests must drive events (`userEvent.click`, `fireEvent.change`, `<page>.click()`), not state injection (state injection is fine for setting up preconditions, but not for the assertion path).
63+
- Tests must assert behavioural outcomes — IPC calls with the right shape, store mutations, dialog dismissal, side effects. NOT just "heading exists."
64+
- Every IPC handler has a happy-path test AND a failure-path test (invalid payload, missing entity, store error).
65+
- Every event subscription in `App.tsx` has a renderer test that fires the event and asserts the resulting store/IPC reaction.
66+
- Every menu item has both an IPC-level test (in `keyboard-shortcuts.spec.ts`) AND a real `Menu.getApplicationMenu().items[…].click()` test (in `menu-click.spec.ts`).
67+
- Coverage audit at `docs/test-coverage-audit.md` — keep it current as new surface lands.
68+
69+
## Known gotchas (paid for in production-bug currency)
70+
71+
These are real bugs we've hit and fixed in this codebase. Keep them in mind:
72+
73+
1. **CSP must allow `'unsafe-eval'`** in dev so Vite HMR works. The shell renderer's CSP is in `src/renderer/index.html`. The relaxation only affects shell-bundled JS; pad content runs in separate `WebContentsView`s.
74+
75+
2. **`exactOptionalPropertyTypes: true`** mismatches with Zod's `.optional()` (which infers `T | undefined` rather than `T?`). Where this bites: `PadHistoryEntry.title?` vs `padHistoryEntrySchema.title.optional()` → cast at the boundary. See `src/main/pads/pad-history-store.ts`.
76+
77+
3. **Vitest 2.x**`workspace` field on `defineConfig` is a PATH to a workspace file, not an inline array. Split is intentional: `vitest.config.ts` (base) + `vitest.workspace.ts` (project list).
78+
79+
4. **TypeScript composite projects** — leaf configs (`main`, `preload`, `renderer`) use `references: [{ path: './tsconfig.shared.json' }]` AND `paths: { '@shared/*': ['src/shared/*'] }`. Do NOT add `'src/shared/**'` to leaf `include` arrays — that double-compiles shared sources and corrupts cross-project type checking once shared has real types.
80+
81+
5. **`baseUrl: '.'` is required** in any tsconfig that uses non-relative `paths` patterns (TS5090).
82+
83+
6. **`electron-log/main` imports `electron` eagerly.** A test running in node-env Vitest can't load it. The logger module dynamic-imports it inside async `configureLogging`/`getLogger`, keeping the `redactForLog` pure-function export available for tests without electron present.
84+
85+
7. **Pure functions used by both lifecycle and tests must be in standalone files** that don't import `electron`. Example: `src/main/app/quit-state.ts` exports `serializeWindowsForQuit` so the test in `tests/main/app/lifecycle-quit.spec.ts` can import without triggering electron module load.
86+
87+
8. **Native menu accelerators are not deliverable via Playwright in xvfb/headless.** To test menu-fired actions, use either:
88+
- `app.evaluate(({ webContents }) => webContents.getAllWebContents().forEach(...))` to fire the IPC channel directly (faster but doesn't exercise the menu's click handler).
89+
- `app.evaluate(({ Menu }) => Menu.getApplicationMenu().items.find(...).submenu.items.find(...).click())` to programmatically click the actual menu item (covers the `cb.<x>()``broadcastShell` path).
90+
We have BOTH in the suite (`keyboard-shortcuts.spec.ts` and `menu-click.spec.ts`).
91+
92+
9. **Pad views stack ON TOP of the shell view** in `BaseWindow.contentView` (later-added = on top). HTML rendered by the shell renderer (e.g. dialog overlays) cannot paint over a pad view. Solution: `setPadViewsHidden(true)` whenever `openDialog !== null`; `applyVisibility()` restores after.
93+
94+
10. **Tab visibility invariant** — exactly one pad view is `setVisible(true)` at a time = the active tab. `TabManager.applyVisibility()` is the single enforcer; every method that mutates state calls it.
95+
96+
11. **Renderer must subscribe to event broadcasts** to keep its store fresh. Main process only persists; the renderer has its own copy of state in Zustand and ONLY refreshes when it receives an `EV_*` event. Forget to subscribe → "Save doesn't save" symptoms.
97+
98+
12. **Pad URL must include `?lang=<code>`** to set Etherpad's pad UI language. Pure `webContents.reload()` does NOT pick up a setting change because the URL is unchanged. Use `webContents.loadURL(newUrl)` to apply language changes.
99+
100+
13. **Zustand selectors that build new arrays/objects per call** (e.g. `s => s.padHistory[wsId] ?? []`) cause infinite re-render loops. Use a stable empty-value sentinel: `const EMPTY: never[] = []`; return that instead of `[]` literals. Pattern in `PadSidebar.tsx` and `OpenPadDialog.tsx`.
101+
102+
14. **`window.etherpadDesktop` capture in `renderer/ipc/api.ts` is lazy** (a getter), so test mocks (`window.etherpadDesktop = {…}` in `beforeEach`) take effect on each call. Don't capture eagerly at module top.
103+
104+
15. **Quit must be defensive** — a window may already be destroyed when `before-quit` fires (user hit X). Use `serializeWindowsForQuit` which filters `!w.window.isDestroyed()` before serialising. The `closed` event on each `BaseWindow` calls `windowManager.forget(win)` so the manager drops stale refs without calling `.destroy()` again.
105+
106+
16. **CI: `pnpm/action-setup`** — do NOT pass `version:` when `package.json` has a `packageManager` field. pnpm-action-setup v4 errors on the conflict. Just `- uses: pnpm/action-setup@v4` with no `with:` block. The `packageManager` field is canonical.
107+
108+
17. **Menu item handlers without renderer wiring are dead.** When adding a `menu.<x>` IPC channel, also add the `if (k === 'menu.<x>') …` branch in `App.tsx`'s `onMenuShellMessage` callback. The menu-click E2E test catches this.
109+
110+
## Tests as documentation
111+
112+
When in doubt about a behaviour, search the test names — they describe the contract. `tests/e2e/*.spec.ts` are the user-flow contracts; `tests/renderer/*.spec.tsx` are the per-component contracts; `tests/main/*.spec.ts` are the per-module contracts.
113+
114+
## When changing surface area
115+
116+
If you add a new IPC channel, dialog, menu item, event, or component:
117+
1. Add the implementation.
118+
2. Add a unit test (vitest) that drives the new surface.
119+
3. Add an E2E test (Playwright) if there's a user-visible flow.
120+
4. Update `docs/test-coverage-audit.md` with the new surface entry.
121+
5. Update this `AGENTS.md` if it introduces a new gotcha or convention.
122+
6. Push.
123+
124+
## Pointers
125+
126+
- Spec: `docs/superpowers/specs/2026-05-03-etherpad-desktop-linux-mvp-design.md`
127+
- Plan: `docs/superpowers/plans/2026-05-03-etherpad-desktop-linux-mvp.md`
128+
- Coverage audit: `docs/test-coverage-audit.md`
129+
- Manual smoke checklist: `docs/smoke-test.md`

0 commit comments

Comments
 (0)