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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
id: bugfix-1170
title: vscode-agents-tree-group-heade
protocol: bugfix
phase: pr
plan_phases: []
current_plan_phase: null
gates:
pr:
status: approved
requested_at: '2026-07-13T03:59:15.458Z'
approved_at: '2026-07-13T06:03:29.092Z'
iteration: 1
build_complete: false
history: []
started_at: '2026-07-13T03:46:12.160Z'
updated_at: '2026-07-13T06:03:29.092Z'
pr_ready_for_human: false
43 changes: 43 additions & 0 deletions codev/state/bugfix-1170_thread.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# bugfix-1170 — vscode Agents tree group-header contextValue collision

## Investigate

**Bug**: Right-clicking an Agents-tree group header (architect/phase/area axis) surfaces 7
builder-scoped context-menu entries that no-op on a group header.

**Root cause (confirmed)**: `packages/vscode/src/views/area-group-tree-item.ts:34` sets
`this.contextValue = `${kind}-group``. `BuilderGroupTreeItem` calls `super(groupName, 'builder', ...)`
(builder-tree-item.ts:54), so kind='builder' → contextValue `builder-group`. The 7 offending menu
entries in package.json (lines 537–587) gate on `/^(builder|blocked-builder|awaiting-builder)-/`,
which matches `builder-group`. The 3 protocol-scoped entries (viewSpec/Plan/ReviewFile) already
require a protocol suffix, so they're unaffected.

**Fix**: line 34 → `this.contextValue = `group-${kind}``. Yields `group-builder` / `group-backlog`,
neither matches the offending regexes. No package.json change. `id` (line 33) stays `${kind}-group`
(unrelated — drives expansion persistence).

**Tests**: grep found NO existing assertion on the literal `builder-group` string, so nothing to
update. The regression home is `packages/vscode/src/__tests__/menu-when-clauses.test.ts` — add a
block asserting the 7 builder-scoped regexes reject `group-builder` / `group-backlog`.

Scope: 1-line src change + new regression test. Comfortably within BUGFIX.

## Fix

- `area-group-tree-item.ts:34`: `${kind}-group` → `group-${kind}`. Build + tests green
(`porch check`: build 6.2s, tests 21s; 566 tests pass).
- Regression test `group-header-context-value.test.ts` derives contextValue from the real
`AreaGroupTreeItem` and feeds it to the actual package.json menu regexes. Verified it fails
without the fix (8 failures) and passes with it.
- Note: unit tests need `@cluesmith/codev-core` + `@cluesmith/codev-types` built first, else
suites fail at import resolution (pre-existing, unrelated). `porch check` runs the build.

## PR

- PR #1172 opened, `Fixes #1170`.
- CMAP (all three APPROVE, HIGH confidence, zero issues): gemini=APPROVE, codex=APPROVE
(couldn't re-run vitest in its sandbox, approved on inspection), claude=APPROVE.
- Consult snag: `consult --type pr` auto-detect failed from the worktree ("Multiple projects
found" — it scanned the full `codev/projects/`). Adding `--issue 1170` disambiguated. Worth
a look for other bugfix builders.
- Requested `pr` gate via `porch done`. Awaiting human approval to merge.
108 changes: 108 additions & 0 deletions packages/vscode/src/__tests__/group-header-context-value.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Group-header rows must NOT surface builder-scoped context-menu entries (#1170).
*
* Agents-tree group headers (architect / phase / area axis, plus the Backlog
* view) are `AreaGroupTreeItem`s. Their `contextValue` is what VSCode matches
* the `view/item/context` menu `when` regexes against. The seven builder-scoped
* commands gate on the state-family prefix `/^(builder|blocked-builder|
* awaiting-builder)-/`.
*
* The bug: the header contextValue was built as `${kind}-group`, so a builder
* group header got `builder-group` — which matches `^builder-` and surfaced all
* seven entries. Each silently no-ops on a header (the handlers narrow via
* `instanceof BuilderTreeItem`), so the menu was pure noise. The fix flips the
* template to `group-${kind}` (`group-builder`), moving the family token off the
* front so none of the regexes match.
*
* This test derives the contextValue from the REAL `AreaGroupTreeItem` and feeds
* it to the actual package.json regexes. Reverting the source to `${kind}-group`
* makes the header value `builder-group` again, matches the regexes, and fails
* these assertions — which a hardcoded string wouldn't catch.
*/

import { describe, it, expect, vi } from 'vitest';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

vi.mock('vscode', () => {
class TreeItem {
id?: string;
contextValue?: string;
constructor(public label: string, public collapsibleState?: number) {}
}
return {
TreeItem,
TreeItemCollapsibleState: { None: 0, Collapsed: 1, Expanded: 2 },
};
});

// Import AFTER the mock is registered.
const vscode = await import('vscode');
const { AreaGroupTreeItem } = await import('../views/area-group-tree-item.js');

const PKG = JSON.parse(
readFileSync(resolve(__dirname, '../../package.json'), 'utf8'),
);
const viewItemMenuEntries: Array<{ command: string; when: string }> =
PKG.contributes.menus['view/item/context'];

/**
* The seven builder-scoped commands whose `when` gates on the state-family
* prefix regex. Two (run/stopWorktreeDev) append `&& codev.hasDevCommand`, so
* the regex isn't at the tail of the clause — extract it tolerantly.
*/
const BUILDER_SCOPED = [
'codev.openBuilderById',
'codev.viewDiff',
'codev.openWorktreeWindow',
'codev.openWorktreeFolder',
'codev.runWorktreeSetup',
'codev.runWorktreeDev',
'codev.stopWorktreeDev',
] as const;

function viewItemRegex(command: string): RegExp {
const entry = viewItemMenuEntries.find(
e => e.command === command && e.when.includes('view == codev.agents'));
if (!entry) {
throw new Error(`No agents view/item/context entry for ${command}`);
}
const m = entry.when.match(/viewItem =~ \/(.+?)\/(?:\s|$)/);
if (!m) {
throw new Error(`No viewItem regex in when clause: ${entry.when}`);
}
return new RegExp(m[1]);
}

/** contextValue of a group header for the given kind, from the real class. */
function headerContextValue(kind: 'builder' | 'backlog'): string {
const header = new AreaGroupTreeItem(
'area/tower', kind, 2, vscode.TreeItemCollapsibleState.Expanded);
return header.contextValue!;
}

describe('group-header rows reject builder-scoped menu entries (#1170)', () => {
it('AreaGroupTreeItem puts the group token first (group-<kind>)', () => {
expect(headerContextValue('builder')).toBe('group-builder');
expect(headerContextValue('backlog')).toBe('group-backlog');
});

for (const command of BUILDER_SCOPED) {
const regex = viewItemRegex(command);

it(`${command} is hidden for a builder group header`, () => {
expect(regex.test(headerContextValue('builder')),
`${command} menu for builder group header`).toBe(false);
});

it(`${command} is hidden for a backlog group header`, () => {
expect(regex.test(headerContextValue('backlog')),
`${command} menu for backlog group header`).toBe(false);
});

it(`${command} still shows for a builder row (guards against over-tightening)`, () => {
expect(regex.test('builder-bugfix'),
`${command} for builder-bugfix`).toBe(true);
});
}
});
2 changes: 1 addition & 1 deletion packages/vscode/src/views/area-group-tree-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ export class AreaGroupTreeItem extends vscode.TreeItem {
) {
super(`${uppercaseAreaName(groupName)} (${count})`, collapsibleState);
this.id = `${kind}-group:${groupName}`;
this.contextValue = `${kind}-group`;
this.contextValue = `group-${kind}`;
}
}
Loading