Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions codev/projects/1108-vscode-clicking-an-architect-g/status.yaml
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions codev/state/air-1108_thread.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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<string, unknown> = {};

vi.mock('vscode', () => {
class FakeEventEmitter<T> {
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>): 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<typeof BuildersProvider>) {
const roots = await provider.getChildren();
return roots.filter(r => r instanceof BuilderGroupTreeItem) as InstanceType<typeof BuilderGroupTreeItem>[];
}

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'];
}
});
});
17 changes: 17 additions & 0 deletions packages/vscode/src/views/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,13 +295,30 @@ export class BuildersProvider implements vscode.TreeDataProvider<vscode.TreeItem
// Groups always render Expanded (#913) — no persisted state. VSCode's
// native per-id memory keeps a user-collapsed group collapsed for the
// rest of the session; on a fresh session this default applies again.
//
// Architect-axis headers get a click-to-open-terminal `command` (#1108):
// the architect is first-class in this mode, so its header should match the
// builder-row affordance (row click opens the terminal; the chevron still
// toggles expand/collapse — VSCode routes the two gestures independently).
// Stage/area headers stay pure containers — they name no launchable entity.
// The `g.key` (architect name; null owners folded into `main` by
// architectGrouping) is the optional name arg `codev.openArchitectTerminal`
// already accepts; it warns gracefully on a stale owner not in the roster.
const isArchitectAxis = grouping.id === 'architect';
return groups.map(g => {
const groupItem = new BuilderGroupTreeItem(
g.key,
g.items.length,
vscode.TreeItemCollapsibleState.Expanded,
rollupGroupState(g.items, now),
);
if (isArchitectAxis) {
groupItem.command = {
command: 'codev.openArchitectTerminal',
title: 'Open Architect Terminal',
arguments: [g.key],
};
}
for (const b of g.items) {
this.groupParentByBuilderId.set(b.id, groupItem);
}
Expand Down
Loading