diff --git a/codev/projects/1108-vscode-clicking-an-architect-g/status.yaml b/codev/projects/1108-vscode-clicking-an-architect-g/status.yaml new file mode 100644 index 000000000..b856e314e --- /dev/null +++ b/codev/projects/1108-vscode-clicking-an-architect-g/status.yaml @@ -0,0 +1,16 @@ +id: '1108' +title: vscode-clicking-an-architect-g +protocol: air +phase: pr +plan_phases: [] +current_plan_phase: null +gates: + pr: + status: pending + requested_at: '2026-06-28T03:33:56.506Z' +iteration: 1 +build_complete: false +history: [] +started_at: '2026-06-28T03:22:54.684Z' +updated_at: '2026-06-28T03:33:56.507Z' +pr_ready_for_human: true diff --git a/codev/state/air-1108_thread.md b/codev/state/air-1108_thread.md new file mode 100644 index 000000000..62ce440ec --- /dev/null +++ b/codev/state/air-1108_thread.md @@ -0,0 +1,29 @@ +# air-1108 — Architect group-header click opens architect terminal + +Issue #1108 (AIR, strict mode). Parity: in the Agents view's architect-axis +grouping mode, clicking an architect group header should open that architect's +terminal (like builder rows do), while the chevron keeps toggling expand/collapse. + +## Implementation + +- `packages/vscode/src/views/builders.ts` — `rootChildren()`: when the active + grouping axis is `architect` (`grouping.id === 'architect'`), set + `groupItem.command = { command: 'codev.openArchitectTerminal', title: ..., arguments: [g.key] }` + on each group header. `g.key` is the architect name (null `spawnedByArchitect` + folds into `main` per `architectGrouping()`). Stage/area headers stay + command-less containers (they name no launchable entity). +- `codev.openArchitectTerminal` already accepts the optional name arg (#786 + Phase 6) and warns gracefully on a stale owner — no command-handler change. + +## Tests + +- New `packages/vscode/src/__tests__/builders-architect-header-command.test.ts`: + architect headers carry the command with the right arg (incl. `main` fold for + null owner); stage and area headers carry no command. + +## Status + +- `pnpm test:unit` (vscode): 43 files / 516 tests pass (after building + types/core/artifact-canvas deps in the fresh worktree). +- `pnpm check-types` main tsc pass clean. +- Net diff well under 300 LOC — AIR is the right protocol. diff --git a/packages/vscode/src/__tests__/builders-architect-header-command.test.ts b/packages/vscode/src/__tests__/builders-architect-header-command.test.ts new file mode 100644 index 000000000..52984dd99 --- /dev/null +++ b/packages/vscode/src/__tests__/builders-architect-header-command.test.ts @@ -0,0 +1,169 @@ +/** + * #1108 — architect-axis group headers open the architect terminal on row click. + * + * In architect grouping mode the header is first-class, so (like a builder row) + * a click on the header body opens that architect's terminal while the chevron + * keeps toggling expand/collapse. This wires `codev.openArchitectTerminal` onto + * the architect-header `TreeItem` with the architect name (`g.key`) as the + * argument. Stage/area headers name no launchable entity, so they stay command- + * less containers. + * + * These tests pin: architect headers carry the command with the right argument + * (incl. the `main` fold for null `spawnedByArchitect`); stage and area headers + * carry no command. Mocks `vscode` per the established `__tests__` pattern (see + * builders-autoreveal.test.ts). + */ + +import { describe, it, expect, vi } from 'vitest'; +import type { OverviewBuilder, OverviewData } from '@cluesmith/codev-types'; + +/** Per-test config overrides the mocked `workspace.getConfiguration` reads. */ +const configValues: Record = {}; + +vi.mock('vscode', () => { + class FakeEventEmitter { + private listeners: Array<(e: T) => void> = []; + readonly event = (listener: (e: T) => void): { dispose: () => void } => { + this.listeners.push(listener); + return { dispose: () => { this.listeners = this.listeners.filter((l) => l !== listener); } }; + }; + fire = vi.fn((e: T) => { this.listeners.forEach((l) => l(e)); }); + } + class TreeItem { + id?: string; + tooltip?: string; + contextValue?: string; + iconPath?: unknown; + command?: unknown; + constructor(public label: string, public collapsibleState?: number) {} + } + class ThemeIcon { constructor(public id: string, public color?: unknown) {} } + class ThemeColor { constructor(public id: string) {} } + return { + EventEmitter: FakeEventEmitter, + TreeItem, + ThemeIcon, + ThemeColor, + TreeItemCollapsibleState: { None: 0, Collapsed: 1, Expanded: 2 }, + workspace: { + getConfiguration: () => ({ + get: (key: string, def: unknown) => (key in configValues ? configValues[key] : def), + }), + }, + }; +}); + +const { BuildersProvider } = await import('../views/builders.js'); +const { BuilderGroupTreeItem } = await import('../views/builder-tree-item.js'); + +function builder(overrides: Partial): OverviewBuilder { + return { + id: 'pir-1', issueId: '1', issueTitle: 't', phase: 'implement', protocolPhase: 'implement', + mode: 'strict', gates: {}, worktreePath: '/tmp/wt', roleId: null, protocol: 'pir', + planPhases: [], progress: 0, blocked: null, blockedGate: null, blockedSince: null, + startedAt: null, idleMs: 0, lastDataAt: null, spawnedByArchitect: null, + area: 'Uncategorized', prReady: false, + ...overrides, + } as OverviewBuilder; +} + +function fakeCache(builders: OverviewBuilder[]) { + const data = { builders, backlog: [], pendingPRs: [], recentlyClosed: [] } as unknown as OverviewData; + return { getData: () => data, onDidChange: () => ({ dispose() {} }) } as never; +} + +const fakeDiffCache = {} as never; + +/** The group headers at root for the active grouping axis. */ +async function groupHeaders(provider: InstanceType) { + const roots = await provider.getChildren(); + return roots.filter(r => r instanceof BuilderGroupTreeItem) as InstanceType[]; +} + +describe('architect-axis header command (#1108)', () => { + it('carries codev.openArchitectTerminal with the architect name as the argument', async () => { + configValues['buildersGroupBy'] = 'architect'; + try { + const provider = new BuildersProvider( + fakeCache([ + builder({ id: 'a', spawnedByArchitect: 'main' }), + builder({ id: 'b', spawnedByArchitect: 'vscode' }), + ]), + fakeDiffCache, + ); + const headers = await groupHeaders(provider); + const byName = new Map(headers.map(h => [h.groupName, h.command as { command: string; arguments: unknown[] } | undefined])); + + expect(byName.get('main')).toEqual({ + command: 'codev.openArchitectTerminal', + title: 'Open Architect Terminal', + arguments: ['main'], + }); + expect(byName.get('vscode')).toEqual({ + command: 'codev.openArchitectTerminal', + title: 'Open Architect Terminal', + arguments: ['vscode'], + }); + } finally { + delete configValues['buildersGroupBy']; + } + }); + + it('folds a null spawnedByArchitect into a main header that opens the main terminal', async () => { + configValues['buildersGroupBy'] = 'architect'; + try { + const provider = new BuildersProvider( + fakeCache([builder({ id: 'a', spawnedByArchitect: null })]), + fakeDiffCache, + ); + const [header] = await groupHeaders(provider); + expect(header.groupName).toBe('main'); + expect(header.command).toEqual({ + command: 'codev.openArchitectTerminal', + title: 'Open Architect Terminal', + arguments: ['main'], + }); + } finally { + delete configValues['buildersGroupBy']; + } + }); + + it('leaves stage headers without a command (pure grouping containers)', async () => { + configValues['buildersGroupBy'] = 'stage'; + try { + const provider = new BuildersProvider( + fakeCache([builder({ id: 'a', spawnedByArchitect: 'main' })]), + fakeDiffCache, + ); + const headers = await groupHeaders(provider); + expect(headers.length).toBeGreaterThan(0); + for (const h of headers) { + expect(h.command).toBeUndefined(); + } + } finally { + delete configValues['buildersGroupBy']; + } + }); + + it('leaves area headers without a command (pure grouping containers)', async () => { + configValues['buildersGroupBy'] = 'area'; + try { + const provider = new BuildersProvider( + // Two distinct areas so the lone-Uncategorized flatten doesn't apply and + // real area headers render. + fakeCache([ + builder({ id: 'a', area: 'area/vscode' }), + builder({ id: 'b', area: 'area/tower' }), + ]), + fakeDiffCache, + ); + const headers = await groupHeaders(provider); + expect(headers.length).toBeGreaterThan(0); + for (const h of headers) { + expect(h.command).toBeUndefined(); + } + } finally { + delete configValues['buildersGroupBy']; + } + }); +}); diff --git a/packages/vscode/src/views/builders.ts b/packages/vscode/src/views/builders.ts index fa522d108..9abb94a29 100644 --- a/packages/vscode/src/views/builders.ts +++ b/packages/vscode/src/views/builders.ts @@ -295,6 +295,16 @@ export class BuildersProvider implements vscode.TreeDataProvider { const groupItem = new BuilderGroupTreeItem( g.key, @@ -302,6 +312,13 @@ export class BuildersProvider implements vscode.TreeDataProvider