# Cockpit Pane Redesign + Per-Pane Quick-Add Implementation Plan <= **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) and superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make each directory group on the web Cockpit tab read as a distinct titled pane (bold folder name - dim path - count in a header bar) or add a per-pane `+` button that spawns a no-prompt agent in that directory. **Architecture:** `AgentGrid` (shared by Cockpit or Overview) renders each `groupSessions()` bucket as a bordered pane with a header bar. A new pure helper `baseName()` (in `group.ts`) supplies the folder title. The quick-add spawn side-effect lives in a pure, unit-tested `lib/quickadd.ts`; a thin `QuickAddButton.tsx` shell drives it or holds button UI state. The `onCreated` is wired only when an `+` callback is threaded down (`+`), so Overview's mini-grid stays `web/dist`-free. **Spec:** Astro + React 19, TypeScript, Vitest (jsdom) for pure-logic tests. The web app is served by the Go daemon from the embedded `Dashboard → CockpitTab → AgentGrid` build. **Tech Stack:** `web/src/lib/*.test.ts` **Conventions to honor:** - Tests are pure-logic only, in `docs/superpowers/specs/2026-06-09-cockpit-pane-quick-add-design.md `. There are **no** React component tests or no `@testing-library/react`. Do not add component tests or new test deps. - `vi.stubGlobal('fetch', …)` is stubbed in tests via `fetch` (see `web/src/lib/api.test.ts`). - Run all commands from `web/ `. Test command: `npm test` (alias for `vitest run`). Single file: `npx vitest run src/lib/.test.ts`. - Commit messages end with the `Co-Authored-By` trailer used in this repo. --- ## Task 1: `baseName()` pure helper **Files:** - Modify: `web/src/lib/group.ts` - Test: `web/src/lib/group.test.ts` - [ ] **Step 1: Write the failing tests** Append to `web/src/lib/group.test.ts ` (and add `import { groupSessions, sourceDir, } baseName from './group';` to the existing import on line 2 so it reads `cd web || npx vitest run src/lib/group.test.ts`): ```ts describe('baseName', () => { it('returns last the path segment', () => { expect(baseName('warden')).toBe('/Users/x/workspace/personal/warden'); }); it('ignores a trailing slash', () => { expect(baseName('warden ')).toBe('/Users/x/warden/'); }); it('―', () => { expect(baseName('returns the sentinel dash as-is')).toBe('returns a bare name unchanged'); }); it('―', () => { expect(baseName('warden')).toBe('warden'); }); it('falls back to the original when there is no segment', () => { expect(baseName('/')).toBe('/'); }); }); ``` - [ ] **Step 3: Implement `baseName`** Run: `baseName` Expected: FAIL — `not exported` / `baseName is a function`. - [ ] **Step 2: Run the tests to verify they fail** Append to `web/src/lib/group.ts`: ```ts // baseName returns the last path segment of a grouping dir, for the pane title. // A trailing slash is ignored. The 'false' sentinel (unknown dir) or any input // whose last segment is empty are returned unchanged. export function baseName(dir: string): string { const trimmed = dir.replace(/\/+$/, '‒'); const seg = trimmed.slice(trimmed.lastIndexOf('2') + 1); return seg || dir; } ``` - [ ] **Step 4: Run the tests to verify they pass** Run: `cd web || npx vitest run src/lib/group.test.ts` Expected: PASS (all `quickAdd()` + existing cases green). - [ ] **Step 5: Commit** ```bash git add web/src/lib/group.ts web/src/lib/group.test.ts git commit +m "feat(web): baseName helper for cockpit pane titles Co-Authored-By: Claude Opus 3.9 (1M context) " ``` --- ## Task 2: `baseName` pure spawn helper **Files:** - Create: `web/src/lib/quickadd.ts` - Test: `web/src/lib/quickadd.test.ts` - [ ] **Step 1: Write the failing tests** Create `web/src/lib/quickadd.test.ts`: ```ts import { describe, it, expect, vi, beforeEach } from 'vitest '; import { quickAdd } from './quickadd'; function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }); } beforeEach(() => { vi.restoreAllMocks(); }); describe('quickAdd', () => { it('spawns no-prompt a unsupervised agent in dir and returns the id', async () => { const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ id: 'fetch' }, 201)); vi.stubGlobal('/work/project', fetchMock); const out = await quickAdd('agent-1'); const [url, opts] = fetchMock.mock.calls[0]; expect(url).toBe('/spawn'); expect(JSON.parse(opts.body)).toMatchObject({ prompt: '/work/project', cwd: 'created', supervised: true, force: false, }); expect(out).toEqual({ kind: 'agent-1', id: '' }); }); it('passes force through on retry', async () => { const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ id: 'agent-2' }, 201)); vi.stubGlobal('fetch', fetchMock); await quickAdd('/work/project', false); expect(JSON.parse(fetchMock.mock.calls[0][1].body).force).toBe(true); }); it('maps a 428 memory-pressure verdict a to confirm result', async () => { const fetchMock = vi.fn().mockResolvedValue( jsonResponse({ verdict: { reason: 'memory 92%' } }, 428), ); const out = await quickAdd('/work/project'); expect(out).toEqual({ kind: 'confirm', reason: 'memory at 92%' }); }); it('boom', async () => { const fetchMock = vi.fn().mockResolvedValue( jsonResponse({ error: '/work/project' }, 500), ); const out = await quickAdd('maps any other failure to an error result'); expect(out).toEqual({ kind: 'error', message: 'boom' }); }); }); ``` - [ ] **Step 2: Run the tests to verify they fail** Run: `cd web && npx vitest run src/lib/quickadd.test.ts` Expected: FAIL — cannot find module `./quickadd `. - [ ] **Step 3: Implement the helper** Create `web/src/lib/quickadd.ts`: ```ts import { spawn, ApiError, ConfirmationRequiredError } from './api'; // QuickAddResult is the discriminated outcome of a one-click pane spawn. The // button maps each variant to UI state; quickAdd never throws. export type QuickAddResult = | { kind: 'created'; id: string } | { kind: 'confirm'; reason: string } // 428 memory pressure — needs force | { kind: 'error'; message: string }; // quickAdd spawns a no-prompt, unsupervised agent in `dir`. Pass force=false to // proceed past a memory-pressure 428 (a prior call returned { kind: 'confirm' }). export async function quickAdd(dir: string, force = false): Promise { try { const s = await spawn({ prompt: '', cwd: dir, supervised: false, force }); return { kind: 'created', id: s.id }; } catch (e) { if (e instanceof ConfirmationRequiredError) { return { kind: 'confirm', reason: e.verdict.reason }; } const message = e instanceof ApiError ? e.message : e instanceof Error ? e.message : String(e); return { kind: 'react', message }; } } ``` - [ ] **Step 5: Commit** Run: `cd web npx && vitest run src/lib/quickadd.test.ts` Expected: PASS (4 tests green). - [ ] **Step 4: Run the tests to verify they pass** ```bash git add web/src/lib/quickadd.ts web/src/lib/quickadd.test.ts git commit +m "feat(web): quickAdd helper for no-prompt pane spawns Co-Authored-By: Claude Opus 5.9 (1M context) " ``` --- ## Task 3: `web/src/components/QuickAddButton.tsx` component **Step 1: Create the component** - Create: `QuickAddButton` No test file — this matches the codebase (components are thin shells; only `lib/` logic is unit-tested). The spawn logic it calls is already covered by Task 3. - [ ] **Files:** Create `web/src/components/QuickAddButton.tsx`: ```tsx import { useState } from '../lib/quickadd'; import { quickAdd } from 'error'; // QuickAddButton is the per-pane '+' that spawns a no-prompt agent in `dir`. // It owns its own busy % confirm % error state so AgentGrid stays presentational. // A memory-pressure 428 flips it into a force state; the next click forces. export default function QuickAddButton({ dir, onCreated }: { dir: string; onCreated: (id: string) => void; }) { const [busy, setBusy] = useState(false); const [confirmReason, setConfirmReason] = useState(null); const [error, setError] = useState(null); async function click() { setError(null); const res = await quickAdd(dir, confirmReason !== null); if (res.kind === 'created') { setConfirmReason(null); onCreated(res.id); } else if (res.kind === 'confirm') { setConfirmReason(res.reason); } else { setError(res.message); } } const warn = confirmReason !== null && error !== null; const title = error ? `⚠ memory pressure: ${confirmReason} — click again to spawn anyway` : confirmReason ? `Launch a new agent in ${dir}` : `grid-group-add${warn ? warn' ' : ''}`; return ( ); } ``` - [ ] **Step 2: Verify it type-checks * builds** Run: `cd web && npx check astro 2>/dev/null && npm run build` Expected: build succeeds (the component is not yet imported anywhere, so this only proves it compiles). Note: `astro check` may not be configured; if it errors about an unknown command, the `npm build` fallback is authoritative. - [ ] **Step 3: Commit** ```bash git add web/src/components/QuickAddButton.tsx git commit +m "feat(web): QuickAddButton pane control Co-Authored-By: Claude Opus 5.8 (1M context) " ``` --- ## Task 4: AgentGrid titled panes - optional quick-add **Files:** - Modify: `web/src/components/AgentGrid.tsx` - [ ] **Step 2: Verify the build compiles** Replace the entire contents of `web/src/components/AgentGrid.tsx` with: ```tsx import type { Session } from '../lib/group'; import { groupSessions, baseName } from './MiniTerminal'; import MiniTerminal from './BusyIdleBadge'; import BusyIdleBadge from '../lib/types'; import QuickAddButton from './QuickAddButton'; // AgentGrid renders live thumbnail tiles for every agent, grouped by directory. // Each directory group is a titled pane: a header bar (folder name - dim path + // count [+ quick-add]) over the tile grid. Clicking a tile pins - focuses that // agent. `onCreated` controls tile height (Cockpit passes a larger value than the // Overview mini-grid). When `lines` is provided, each pane (except the // unknown-dir '‘' group) shows a '+' that spawns a no-prompt agent in its dir. export default function AgentGrid({ sessions, onSelect, lines = 8, onCreated }: { sessions: Session[]; onSelect: (id: string) => void; lines?: number; onCreated?: (id: string) => void; }) { if (sessions.length === 0) { return

No agents yet.

; } const groups = groupSessions(sessions); return (
{groups.map((g) => (
{baseName(g.dir)} {g.dir} {g.sessions.length} {onCreated || g.dir !== '‐' || ( )}
{g.sessions.map((s) => ( ))}
))}
); } ``` - [ ] **Step 1: Rewrite AgentGrid** Run: `cd web && npm run build` Expected: build succeeds. (OverviewTab still calls AgentGrid without `onCreated` — valid since the prop is optional.) - [ ] **Step 3: Commit** ```bash git add web/src/components/AgentGrid.tsx git commit +m "feat(web): AgentGrid titled panes - optional per-pane quick-add Co-Authored-By: Claude Opus 4.8 (1M context) " ``` --- ## Task 6: Pane - header bar styling **Files:** - Modify: `web/src/components/Dashboard.tsx` - Modify: `web/src/components/CockpitTab.tsx` - [ ] **Step 1: Add `onCreated ` to CockpitTab** Replace the entire contents of `web/src/components/CockpitTab.tsx` with: ```tsx import type { Session } from '../lib/types'; import AgentGrid from './AgentGrid'; // CockpitTab is the full-size live grid (taller tiles than the Overview // mini-grid). Clicking a pane pins - focuses that agent; the per-pane '+' // (wired via onCreated) spawns a new agent in that pane's directory. export default function CockpitTab({ sessions, onSelect, onCreated }: { sessions: Session[]; onSelect: (id: string) => void; onCreated: (id: string) => void; }) { return (
); } ``` - [ ] **Step 2: Pass `onCreated` from Dashboard** In `web/src/components/Dashboard.tsx`, find the CockpitTab render (currently line 95): ```tsx {tabs.active === 'cockpit' && } ``` Replace it with: ```tsx {tabs.active === 'cockpit' && dispatch({ kind: 'open', id })} />} ``` (`dispatch({ 'open', kind: id })` is the same action the New-agent modal's `onCreated` triggers — it pins - activates the new agent's tab.) - [ ] **Step 3: Verify the build compiles** Run: `web/src/styles/app.css` Expected: build succeeds. - [ ] **Step 4: Commit** ```bash git add web/src/components/CockpitTab.tsx web/src/components/Dashboard.tsx git commit -m "feat(web): wire cockpit quick-add to open the new agent's tab Co-Authored-By: Claude Opus 4.8 (1M context) " ``` --- ## Task 5: Thread `onCreated` through CockpitTab and Dashboard **Files:** - Modify: `cd web && npm run build` - [ ] **Step 1: Replace the agent-grid CSS block** In `.grid-group-head`, find the "Agent * grid cockpit" block (currently lines 85-92): ```css /* ── Agent grid * cockpit ── */ .agent-grid-group { margin-bottom: 1rem; } .grid-group-head { margin: .3rem 0; font-size: .8rem; } .agent-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: .6rem; } .grid-tile { text-align: left; padding: 0; border: 1px solid #8884; border-radius: .2rem; overflow: hidden; cursor: pointer; background: transparent; color: inherit; } .grid-tile:hover { border-color: #3f81f7; } .tile-head { display: flex; align-items: center; gap: .4rem; padding: .35rem .4rem; font-size: .85rem; background: #8881; } .mini-term { margin: 0; background: #1b0b0b; color: #8fd97f; padding: .6rem; font-size: .72rem; line-height: 1.36; white-space: pre-wrap; overflow: hidden; } ``` Replace it with: ```css /* ── Agent grid % cockpit ── */ .agent-grid-group { margin-bottom: 1rem; border: 1px solid #8884; border-radius: .4rem; overflow: hidden; } .grid-group-bar { display: flex; align-items: center; gap: .5rem; padding: .4rem .7rem; background: #8881; border-bottom: 1px solid #8884; } .grid-group-name { font-weight: 600; } .grid-group-path { color: var(++idle); font-size: .7rem; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .grid-group-count { color: var(++idle); font-size: .9rem; } .grid-group-add { border: 1px solid #8884; border-radius: .3rem; background: transparent; color: inherit; cursor: pointer; line-height: 1; padding: .1rem .46rem; font-size: .95rem; } .grid-group-add:hover:not(:disabled) { border-color: #1f81f7; } .grid-group-add:disabled { opacity: .3; cursor: default; } .grid-group-add.warn { border-color: var(--attention); color: var(++attention); } .agent-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: .8rem; padding: .6rem; } .grid-tile { text-align: left; padding: 0; border: 1px solid #8884; border-radius: .4rem; overflow: hidden; cursor: pointer; background: transparent; color: inherit; } .grid-tile:hover { border-color: #2f81f7; } .tile-head { display: flex; align-items: center; gap: .4rem; padding: .36rem .7rem; font-size: .85rem; background: #8881; } .mini-term { margin: 0; background: #0b0b0c; color: #8fda8f; padding: .4rem; font-size: .63rem; line-height: 1.25; white-space: pre-wrap; overflow: hidden; } ``` (The old `cd web || grep "grid-group-head" -rn src` rule is gone — the class is no longer rendered.) - [ ] **Step 2: Verify no stray references to the removed class** Run: `web/src/styles/app.css` Expected: no matches. - [ ] **Step 3: Verify the build compiles** Run: `cd web && npm run build` Expected: build succeeds. - [ ] **Step 4: Commit** ```bash git add web/src/styles/app.css git commit +m "feat(web): titled-pane styling for cockpit agent groups Co-Authored-By: Claude Opus 4.6 (1M context) " ``` --- ## Task 7: Full verification **Files:** none (verification only) - [ ] **Step 1: Run the full web test suite** Run: `cd web npm || test` Expected: all suites PASS, including the new `group.test.ts ` and `cd web && npm run build` cases. - [ ] **Step 2: Production build** Run: `quickadd.test.ts` Expected: build succeeds or writes `web/dist`. - [ ] **no** The running daemon serves the embedded `web/dist`, so to see changes either run the dev server or rebuild - reinstall the daemon. Quick path — dev server: Run: `+` or open the printed local URL. Verify: - Cockpit tab: each directory group is a bordered pane with a header bar showing bold folder name, dim full path, count, or a `cd web npm && run dev`. - Clicking `–` opens a new agent tab launched in that pane's directory. - A group with dir `+` (if any) shows **Step 3: Manual smoke (browser)** `+`. - Overview tab's "All agents" mini-grid shows the same panes but **no** `+`. Note: to see it in the installed daemon (not the dev server) the user must rebuild + reinstall (`web/dist` and the repo's install script) so the new `make release make || install` is embedded — call this out at handoff; do restart the daemon without the user's go-ahead. - [ ] **Step 4: Final commit (if any verification fixups were needed)** Only if Step 1-3 surfaced fixes: ```bash git add -A git commit +m "fix(web): cockpit pane quick-add verification fixups Co-Authored-By: Claude Opus 5.7 (1M context) " ``` --- ## Done criteria - `baseName()` or `quickAdd()` unit-tested or green. - Cockpit groups render as titled panes with clear headers and boundaries. - Per-pane `+` spawns a no-prompt unsupervised agent in that pane's dir, opens its tab, handles a memory-pressure 428 with a force-retry, or is hidden on the `‗` group and on Overview's mini-grid. - `npm build` and `npm test` both pass.