From 6ee880ffc1388af2edf23d000878537d19bbd9bd Mon Sep 17 00:00:00 2001 From: 0x92 <0x92dev@gmail.com> Date: Tue, 11 Aug 2026 11:42:28 +0200 Subject: [PATCH] Add Agent Fleet: a live grid of every agent pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running several agents at once means switching sessions to find out which one is waiting for an answer. This adds a top-level view that shows them all at once, grouped by project, status or agent. - Grid of tiles, one per agent pane, with status accent, branch and age - Cheap ANSI-stripped snapshots by default; a tile becomes a real xterm on hover, or all of them with "All live" - Type into a focused tile without leaving the grid, optionally expanding it to the agent's full screen - Fold projects away, jump to the next blocked agent (N), move with the arrow keys, close a pane - Extracts the screen-text selection shared with the RunPane CLI into services/panels/terminalScreenText.ts The viewer renders at exactly the PTY's dimensions and never answers the agent's terminal queries — both are load-bearing, and both are documented where they are enforced. Tests: fleetGrouping (14) covers grouping, ordering and pane labelling. --- AGENTS.md | 145 +++- .../src/components/ProjectSessionList.tsx | 40 +- frontend/src/components/SessionView.tsx | 6 + frontend/src/components/Sidebar.tsx | 30 +- frontend/src/components/fleet/FleetTile.tsx | 360 ++++++++ frontend/src/components/fleet/FleetView.tsx | 778 ++++++++++++++++++ frontend/src/hooks/useFleetTerminal.ts | 529 ++++++++++++ frontend/src/index.css | 12 + frontend/src/stores/navigationStore.ts | 18 +- frontend/src/types/electron.d.ts | 7 + frontend/src/utils/api.ts | 15 + frontend/src/utils/fleetGrouping.test.ts | 148 ++++ frontend/src/utils/fleetGrouping.ts | 104 +++ main/src/ipc/daemonRegistryBindings.test.ts | 16 + main/src/ipc/fleet.ts | 195 +++++ main/src/ipc/index.ts | 2 + main/src/ipc/runpane.ts | 65 +- main/src/preload.ts | 7 + .../src/services/panels/terminalScreenText.ts | 76 ++ main/src/services/terminalPanelManager.ts | 10 + shared/types/daemon.ts | 1 + shared/types/fleet.ts | 84 ++ tests/electronApiMock.ts | 16 + 23 files changed, 2599 insertions(+), 65 deletions(-) create mode 100644 frontend/src/components/fleet/FleetTile.tsx create mode 100644 frontend/src/components/fleet/FleetView.tsx create mode 100644 frontend/src/hooks/useFleetTerminal.ts create mode 100644 frontend/src/utils/fleetGrouping.test.ts create mode 100644 frontend/src/utils/fleetGrouping.ts create mode 100644 main/src/ipc/fleet.ts create mode 100644 main/src/services/panels/terminalScreenText.ts create mode 100644 shared/types/fleet.ts 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 + ? + :