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,15 @@
## Why

- The Active Agents SCM contribution can render session rows, but it cannot render the header count badge shown in the intended VS Code screenshot because it only registers a tree-data provider.
- The view also depends on prior user view-state persistence, so the section may stay hidden instead of appearing by default in Source Control.

## What Changes

- Create the SCM view with `createTreeView(...)` so the extension can set a live badge and empty-state message.
- Mark the contributed SCM view as visible by default.
- Add regression coverage for both the empty-state message and the live-session badge count.

## Impact

- Affected surfaces: `templates/vscode/guardex-active-agents/package.json`, `templates/vscode/guardex-active-agents/extension.js`, and `test/vscode-active-agents-session-state.test.js`.
- Risk is narrow because the change stays inside the VS Code companion and does not alter Guardex session-state generation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## ADDED Requirements

### Requirement: Active Agents SCM view exposes header state
The Guardex Active Agents VS Code companion SHALL create the `gitguardex.activeAgents` SCM view through a tree-view handle so the view can expose header state in addition to item rows.

#### Scenario: Live sessions set a header badge
- **WHEN** one or more live Guardex sessions are available in the current workspace
- **THEN** the SCM view shows the session rows
- **AND** the view header badge reflects the live session count.

#### Scenario: Empty state sets a view message
- **WHEN** no live Guardex sessions are available in the current workspace
- **THEN** the SCM view remains available in Source Control
- **AND** the view exposes an empty-state message that tells the operator to start a sandbox session.

### Requirement: Active Agents SCM view is visible by default
The `gitguardex.activeAgents` SCM contribution SHALL default to visible so operators do not need to discover it manually in the SCM views menu on first install.

#### Scenario: First load shows the section
- **WHEN** the extension is installed in a workspace with Source Control open
- **THEN** the Active Agents section is available in the SCM container without requiring a manual enable step.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Definition of Done

- [x] 1.1 Capture the SCM badge and default-visibility acceptance criteria in the proposal/spec.
- [x] 2.1 Create the Active Agents SCM view with badge/message support.
- [x] 2.2 Default the SCM view contribution to visible.
- [x] 2.3 Add regression coverage for empty and live SCM view states.
- [x] 3.1 Run `node --test test/vscode-active-agents-session-state.test.js`.
- [x] 3.2 Run `openspec validate agent-codex-vscode-active-agents-scm-badge-visibilit-2026-04-21-18-31 --type change --strict`.
- [x] 3.3 Run `openspec validate --specs`.
- [ ] 4.1 Run the Guardex finish flow with PR merge + cleanup, or record a `BLOCKED:` note.
33 changes: 31 additions & 2 deletions templates/vscode/guardex-active-agents/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,34 @@ class ActiveAgentsProvider {
constructor() {
this.onDidChangeTreeDataEmitter = new vscode.EventEmitter();
this.onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event;
this.treeView = null;
}

getTreeItem(element) {
return element;
}

attachTreeView(treeView) {
this.treeView = treeView;
this.updateViewState(0);
}

updateViewState(sessionCount) {
if (!this.treeView) {
return;
}

this.treeView.badge = sessionCount > 0
? {
value: sessionCount,
tooltip: `${sessionCount} active agent${sessionCount === 1 ? '' : 's'}`,
}
: undefined;
this.treeView.message = sessionCount > 0
? undefined
: 'Start a sandbox session to populate this view.';
}

refresh() {
this.onDidChangeTreeDataEmitter.fire();
}
Expand All @@ -67,13 +89,15 @@ class ActiveAgentsProvider {
}

const sessionsByRepo = await this.loadSessionsByRepo();
const sessionCount = [...sessionsByRepo.values()].reduce((total, sessions) => total + sessions.length, 0);
this.updateViewState(sessionCount);
const repos = [...sessionsByRepo.entries()]
.map(([repoRoot, sessions]) => ({ repoRoot, sessions }))
.filter((entry) => entry.sessions.length > 0)
.sort((left, right) => left.repoRoot.localeCompare(right.repoRoot));

if (repos.length === 0) {
return [new InfoItem('No active Guardex agents', 'Start a sandbox session to populate this view.')];
return [new InfoItem('No active Guardex agents', 'Open or start a sandbox session.')];
}

if (repos.length === 1) {
Expand Down Expand Up @@ -115,12 +139,17 @@ class ActiveAgentsProvider {

function activate(context) {
const provider = new ActiveAgentsProvider();
const treeView = vscode.window.createTreeView('gitguardex.activeAgents', {
treeDataProvider: provider,
showCollapseAll: true,
});
provider.attachTreeView(treeView);
const refresh = () => provider.refresh();
const watcher = vscode.workspace.createFileSystemWatcher('**/.omx/state/active-sessions/*.json');
const interval = setInterval(refresh, 5_000);

context.subscriptions.push(
vscode.window.registerTreeDataProvider('gitguardex.activeAgents', provider),
treeView,
vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh),
vscode.commands.registerCommand('gitguardex.activeAgents.openWorktree', async (session) => {
if (!session?.worktreePath) {
Expand Down
3 changes: 2 additions & 1 deletion templates/vscode/guardex-active-agents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"scm": [
{
"id": "gitguardex.activeAgents",
"name": "Active Agents"
"name": "Active Agents",
"visibility": "visible"
}
]
},
Expand Down
56 changes: 56 additions & 0 deletions test/vscode-active-agents-session-state.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function loadExtensionWithMockVscode(mockVscode) {
function createMockVscode(tempRoot) {
const registrations = {
providers: [],
treeViews: [],
};

class TreeItem {
Expand Down Expand Up @@ -95,6 +96,18 @@ function createMockVscode(tempRoot) {
file: (fsPath) => ({ fsPath }),
},
window: {
createTreeView: (viewId, options) => {
const treeView = {
viewId,
options,
badge: undefined,
message: undefined,
dispose() {},
};
registrations.treeViews.push(treeView);
registrations.providers.push({ viewId, provider: options.treeDataProvider });
return treeView;
},
registerTreeDataProvider: (viewId, provider) => {
registrations.providers.push({ viewId, provider });
return disposable();
Expand Down Expand Up @@ -228,6 +241,8 @@ test('active-agents extension registers a provider with getTreeItem', async () =

extension.activate(context);

assert.equal(registrations.treeViews.length, 1);
assert.equal(registrations.treeViews[0].viewId, 'gitguardex.activeAgents');
assert.equal(registrations.providers.length, 1);
assert.equal(registrations.providers[0].viewId, 'gitguardex.activeAgents');

Expand All @@ -237,6 +252,47 @@ test('active-agents extension registers a provider with getTreeItem', async () =
const [rootItem] = await provider.getChildren();
assert.equal(rootItem.label, 'No active Guardex agents');
assert.equal(provider.getTreeItem(rootItem), rootItem);
assert.equal(registrations.treeViews[0].badge, undefined);
assert.equal(registrations.treeViews[0].message, 'Start a sandbox session to populate this view.');

for (const subscription of context.subscriptions) {
subscription.dispose?.();
}
});

test('active-agents extension updates the SCM badge for live sessions', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-live-view-'));
const sessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, 'agent/codex/live-task');
fs.mkdirSync(path.dirname(sessionPath), { recursive: true });
fs.writeFileSync(
sessionPath,
`${JSON.stringify(sessionSchema.buildSessionRecord({
repoRoot: tempRoot,
branch: 'agent/codex/live-task',
taskName: 'live-task',
agentName: 'codex',
worktreePath: path.join(tempRoot, '.omx', 'agent-worktrees', 'live-task'),
pid: process.pid,
cliName: 'codex',
}), null, 2)}\n`,
'utf8',
);

const { registrations, vscode } = createMockVscode(tempRoot);
vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }];
const extension = loadExtensionWithMockVscode(vscode);
const context = { subscriptions: [] };

extension.activate(context);

const provider = registrations.providers[0].provider;
const [sessionItem] = await provider.getChildren();
assert.equal(sessionItem.label, 'live-task');
assert.deepEqual(registrations.treeViews[0].badge, {
value: 1,
tooltip: '1 active agent',
});
assert.equal(registrations.treeViews[0].message, undefined);

for (const subscription of context.subscriptions) {
subscription.dispose?.();
Expand Down