diff --git a/AGENTS.md b/AGENTS.md index 9847b339..d889ce3a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,149 @@ - Package (examples): `pnpm build:mac`, `pnpm build:linux`. - Lint: `pnpm lint`; Type-check: `pnpm typecheck` (runs per package). - Tests (E2E): `pnpm test`, `pnpm test:ui`, CI configs in `playwright.ci*.config.ts`. -- Main unit tests (if added): `pnpm --filter main test`, coverage: `pnpm --filter main run test:coverage`. +- Main unit tests: `pnpm --filter main test` (Vitest), coverage: `pnpm --filter main run test:coverage`. +- Frontend unit tests: `pnpm --filter frontend test` (Vitest). +- New dependency → `pnpm run generate-notices` and commit `NOTICES`. + +## Architecture Invariants + +Read this section before writing code. Each item below is a place where the +obvious change is silently incomplete. + +### Adding an IPC channel is a 7-file dance +IPC handlers do **not** call `ipcMain.handle` directly. They register against +`commandRegistry` and the file binds its channel list to `ipcMain` at the +bottom (`main/src/ipc/git.ts` is the canonical example). A channel is not +reachable until every step below is done; missing step 4 fails silently at +runtime, missing step 7 fails loudly in CI. + +1. `main/src/ipc/.ts` — add the string to that file's + `DAEMON_*_CHANNELS` array **and** `commandRegistry.register(name, fn)`. + The array is what `commandRegistry.bindChannels(ipcMain, ...)` consumes. +2. `main/src/ipc/index.ts` — only if you added a new `register*Handlers` file. +3. `shared/types/daemon.ts` — `DAEMON_OWNED_CHANNEL_PREFIXES` / + `DAEMON_OWNED_EXACT_CHANNELS`. Skip only when an existing prefix + (`sessions:`, `panels:`, `projects:`, `terminal:`, …) already covers it. +4. `main/src/preload.ts` — this file **duplicates** the daemon-owned lists + inline, because a sandboxed preload cannot require local modules. If you + touched step 3 you MUST mirror it here, or the channel bypasses the remote + daemon bridge and only works locally. +5. `main/src/preload.ts` — add the method to the matching + `contextBridge.exposeInMainWorld('electronAPI', …)` group. +6. `frontend/src/types/electron.d.ts` + `frontend/src/utils/api.ts` — typed + signature and `API.*` wrapper. +7. `main/src/ipc/daemonRegistryBindings.test.ts` — the per-domain channel + arrays are asserted with `toEqual`. Adding a channel without updating this + file fails `pnpm --filter main test`. + +Also update `tests/electronApiMock.ts` when a Playwright spec exercises the flow. + +**Shortcut for panel-internal channels:** `window.electronAPI.invoke(channel, …)` +is a generic passthrough (see `TerminalPanel.tsx` calling `terminal:getState`). +It skips steps 5–6 — acceptable for internal plumbing, not for a public `API.*` +surface. + +### `pnpm install` shadows the system `claude` binary +`main` depends on `@anthropic-ai/claude-code`, so `pnpm install` writes a +`claude` shim into `node_modules/.bin/`. Launching the app through a pnpm +script puts that directory at the front of `PATH`, and every agent terminal the +app spawns inherits it — so agents run the *bundled* Claude Code version rather +than the user's installed one. A version skew there surfaces as +`404 {"type":"not_found_error","message":"model: opus"}` or similar. + +Nothing needs the shim: `claudeCodeManager` resolves the executable via +`findExecutableInPath('claude')` or the configured `claudeExecutablePath`, and +the package itself is only imported as a library. After a fresh install, delete +`node_modules/.bin/claude*` and `main/node_modules/.bin/claude*`. + +### On Windows, dev builds write to the *production* data directory +`getAppDirectory()` auto-isolates to `~/.pane_dev` only when +`__CFBundleIdentifier === 'com.dcouple.pane'` — a **macOS-only** environment +variable. On Windows and Linux it falls through to `~/.pane`, so a dev run +shares the installed app's database and sockets. Always launch dev builds with +an explicit directory: `PANE_DIR=~/.pane_test pnpm dev`. + +### Secondary terminal views must match the PTY's dimensions +Agent TUIs paint with absolute cursor positioning sized to the real PTY. +Replaying that byte stream into a terminal of a different width wraps every +line and each repaint pushes the viewport down — the console appears to scroll +without end. A read-only viewer must create its xterm at the PTY's exact +`cols`/`rows` (exposed on `TerminalPanelSnapshot`) and scale to fit with a CSS +transform. Never use `FitAddon` for a secondary view. + +### Every git/shell read goes through `CommandRunner` +`CommandRunner` transparently wraps commands for WSL and remote hosts. Never +call `execSync`/`child_process` directly from a service. Obtain a runner from +`sessionManager.getProjectContext(sessionId).commandRunner` or +`getProjectContextByProjectId(projectId).commandRunner`. Both can return `null` +for orphaned sessions or projects without sessions — return an error result, +do not throw. + +### `main/src/database/migrations/*.sql` is dead code +Nothing executes those files; `copy:assets` ships them to `dist` and they are +ignored. Real schema lives in exactly two places: +- `main/src/database/schema.sql` — executed statement-by-statement (split on + `;`) on every startup. Must be idempotent (`CREATE TABLE IF NOT EXISTS`), and + must never contain a `;` inside a comment or string literal. Prefer `--` + comments. +- `DatabaseService.runMigrations()` in `main/src/database/database.ts` — + hand-written TypeScript using `PRAGMA table_info(...)` feature detection. + Column additions, index creation and backfills go here. + +For ad-hoc queries in a new service use `databaseService.getDb()` — the +sanctioned escape hatch (see `main/src/services/scrollbackRetention.ts`) — +rather than growing the ~5,000-line `database.ts` facade. + +### Navigation has no router +`frontend/src/stores/navigationStore.ts` holds a single `activeView` enum. A +new full-page view means touching four places: +1. the `activeView` union — declared **twice** in that file (state interface and + `setActiveView` signature) — plus a `navigateToX()` action, +2. the render switch in `frontend/src/components/SessionView.tsx`, near the + `pane-chat` branch, +3. `frontend/src/components/Sidebar.tsx` — the **compact rail**, +4. `frontend/src/components/ProjectSessionList.tsx` — the **expanded tree**. + +Sidebar entries live in two separate files; updating only one is the classic +miss. `PaneChatView.tsx` is the reference implementation of a full-page view. + +### Adding a `ToolPanelType` is ~14 touchpoints +`PanelContainer` (lazy import + switch), `PanelTabBar` (`getPanelIcon`, +`typeOrder`, the create menu), `PanelTabStrip` (a **second, duplicated** +`getPanelIcon`), `PanelLoadingFallback`, the `PanelGroupView` keep-alive list, +`PANEL_CAPABILITIES` in `shared/types/panels.ts`, the `checkInitialized` switch +in `main/src/ipc/panels.ts`, and a `panelManager.ensureXxxPanel` helper. Panels +are keyed by `sessionId`; prefer a new `activeView` for anything that is not +scoped to a single session. + +### Agents are terminal panels, not a panel type +An "agent" is a `terminal` panel whose `customState.isCliPanel === true`, with +`agentType: 'claude' | 'codex'`. `frontend/src/components/panels/cli/` is dead +code. "Is it running?" is answered by the agent-status pipeline, **not** +`Session.status`: `terminalPanelManager.pollAgentStatus` → `detectAgentState` → +`panel:agentStatus` event → `App.tsx` listener → `usePanelStore.setAgentStatus`. +Roll several panels up with `frontend/src/utils/agentStatus.ts`; read via +`frontend/src/hooks/useAgentStatus.ts`. States: `blocked | working | idle | unknown`. + +### Terminals: WebGL and the shared texture atlas +xterm instances that share font and theme **share a WebGL texture atlas**; +clearing it from one corrupts the others (see the header comment in +`frontend/src/components/panels/TerminalPanel.tsx`). Never call +`clearTextureAtlas()`. Secondary or read-only terminal views must not load +`WebglAddon` at all — Chromium also caps live WebGL contexts at ~16. +`terminalPanelManager.setVisibility(panelId, visible, viewerId)` is refcounted +per viewer: always pass a distinct, prefixed `viewerId`, and always release it +on unmount. + +### Lint rules that will fail your PR +- `@typescript-eslint/no-explicit-any` is an **error** in both packages. Parse + untrusted JSON as `unknown` and narrow with hand-written type guards. +- The frontend enforces ~13 `jsx-a11y` rules as errors, notably + `click-events-have-key-events` and `no-static-element-interactions`. Never put + `onClick` on a `
` or an SVG element — wrap the row in + ` + + {showRemoteDesktopLink && onRemoteDesktopClick && ( + + + + {showRemoteDesktopLink && ( + {/* + Within a project the worktree is the session's real identity — three + sessions on "Super Forum" are only told apart by their branch. + */} + {branchLabel && ( + + + )} + + {isFocused && ( + + )} + {showLive + ? + :