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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ To install the real companion into local VS Code from a GitGuardex-wired repo:
node scripts/install-vscode-active-agents-extension.js
```

It adds an `Active Agents` view to the Source Control container, groups each live repo into `ACTIVE AGENTS` and `CHANGES` sections, reads `.omx/state/active-sessions/*.json`, derives `thinking` versus `working` from each live sandbox worktree, and uses VS Code's native `loading~spin` codicon for the running-state affordance. Reload the VS Code window after install.
It adds an `Active Agents` view to the Source Control container, groups each live repo into `ACTIVE AGENTS` and `CHANGES` sections, splits `ACTIVE AGENTS` into `WORKING NOW` and `THINKING` when both states are present, reads `.omx/state/active-sessions/*.json`, derives `thinking` versus `working` from each live sandbox worktree, and surfaces a working-count summary in the repo/header affordances. Reload the VS Code window after install.

---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## Why

The VS Code Active Agents companion already shows per-row `working` versus `thinking`, but the busy lanes still blend into one flat `ACTIVE AGENTS` list. When several sandboxes are live, the user has to inspect each row one by one to find the branches actively editing files.

## What Changes

- Split the `ACTIVE AGENTS` tree into visible `WORKING NOW` and `THINKING` subgroups.
- Surface a repo-level working count in the repo summary row and the SCM badge tooltip.
- Use a distinct VS Code codicon for actively working lanes so they stand out from thinking-only sessions.
- Update README/test coverage to lock the new grouping and summary behavior.

## Impact

- Affected surfaces: `templates/vscode/guardex-active-agents/extension.js`, `templates/vscode/guardex-active-agents/README.md`, `test/vscode-active-agents-session-state.test.js`, and the root `README.md`.
- No runtime/session-file schema changes; the companion still reads the existing `.omx/state/active-sessions/*.json` records.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## ADDED Requirements

### Requirement: Active Agents highlights currently working lanes
The VS Code Active Agents companion SHALL separate actively editing Guardex lanes from idle-thinking lanes inside the `ACTIVE AGENTS` section.

#### Scenario: Working and thinking sessions render in separate groups
- **WHEN** a repo has both live `working` and `thinking` Guardex sessions
- **THEN** the repo node contains an `ACTIVE AGENTS` section
- **AND** that section contains `WORKING NOW` and `THINKING` child groups
- **AND** the working group appears before the thinking group.

#### Scenario: Repo summary exposes working counts
- **WHEN** a repo has one or more live working sessions
- **THEN** the repo row description includes the working count in addition to the active session count
- **AND** the Source Control badge tooltip mentions how many active sessions are currently working.

#### Scenario: Working sessions use a distinct visual affordance
- **WHEN** a live Guardex session is inferred as `working`
- **THEN** its row uses a distinct codicon from `thinking` rows
- **AND** the row still keeps the existing activity/count/elapsed description text.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Definition of Done

This change is complete only when **all** of the following are true:

- Every checkbox below is checked.
- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff.
- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline.

Handoff: 2026-04-22 09:05Z codex owns `templates/vscode/guardex-active-agents/*`, `test/vscode-active-agents-session-state.test.js`, `README.md`, and this change workspace to make actively working Guardex lanes easier to spot in VS Code.

## 1. Specification

- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-vscode-working-agents-groups-2026-04-22-09-05`.
- [x] 1.2 Define normative requirements in `specs/vscode-working-agents-groups/spec.md`.

## 2. Implementation

- [x] 2.1 Split the `ACTIVE AGENTS` section into visible `WORKING NOW` and `THINKING` groups, preserving live session rows.
- [x] 2.2 Surface working counts in the repo row / view badge summary and add a distinct icon for working lanes.
- [x] 2.3 Update README guidance and focused regression tests for the new grouping behavior.

## 3. Verification

- [x] 3.1 Run `node --test test/vscode-active-agents-session-state.test.js`.
- [x] 3.2 Run `openspec validate agent-codex-vscode-working-agents-groups-2026-04-22-09-05 --type change --strict`.
- [x] 3.3 Run `openspec validate --specs`.

## 4. Cleanup

- [ ] 4.1 Run the cleanup pipeline: `bash scripts/agent-branch-finish.sh --branch agent/codex/vscode-working-agents-groups-2026-04-22-09-05 --base main --via-pr --wait-for-merge --cleanup`.
- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff.
- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch).
5 changes: 3 additions & 2 deletions templates/vscode/guardex-active-agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ What it does:

- Adds an `Active Agents` view to the Source Control container.
- Renders one repo node per live Guardex workspace with grouped `ACTIVE AGENTS` and `CHANGES` sections.
- Shows one row per live Guardex sandbox session inside the repo's `ACTIVE AGENTS` section.
- Splits live sessions inside `ACTIVE AGENTS` into `WORKING NOW` and `THINKING` groups so active edit lanes stand out immediately.
- Shows one row per live Guardex sandbox session inside those activity groups.
- Shows repo-root git changes in a sibling `CHANGES` section when the guarded repo itself is dirty.
- Derives `thinking` versus `working` from the live sandbox worktree and shows changed-file counts for active edits.
- Derives `thinking` versus `working` from the live sandbox worktree, surfaces working counts in the repo/header summary, and shows changed-file counts for active edits.
- Uses VS Code's native animated `loading~spin` icon for the running-state affordance.
- Reads repo-local presence files from `.omx/state/active-sessions/`.

Expand Down
58 changes: 49 additions & 9 deletions templates/vscode/guardex-active-agents/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,29 @@ class RepoItem extends vscode.TreeItem {
this.sessions = sessions;
this.changes = changes;
const descriptionParts = [`${sessions.length} active`];
const workingCount = countWorkingSessions(sessions);
if (workingCount > 0) {
descriptionParts.push(`${workingCount} working`);
}
if (changes.length > 0) {
descriptionParts.push(`${changes.length} changed`);
}
this.description = descriptionParts.join(' · ');
this.tooltip = repoRoot;
this.tooltip = [
repoRoot,
this.description,
].join('\n');
this.iconPath = new vscode.ThemeIcon('repo');
this.contextValue = 'gitguardex.repo';
}
}

class SectionItem extends vscode.TreeItem {
constructor(label, items) {
constructor(label, items, options = {}) {
super(label, vscode.TreeItemCollapsibleState.Expanded);
this.items = items;
this.description = items.length > 0 ? String(items.length) : '';
this.description = options.description
|| (items.length > 0 ? String(items.length) : '');
this.contextValue = 'gitguardex.section';
}
}
Expand All @@ -58,7 +66,9 @@ class SessionItem extends vscode.TreeItem {
session.worktreePath,
];
this.tooltip = tooltipLines.filter(Boolean).join('\n');
this.iconPath = new vscode.ThemeIcon('loading~spin');
this.iconPath = session.activityKind === 'working'
? new vscode.ThemeIcon('edit')
: new vscode.ThemeIcon('loading~spin');
this.contextValue = 'gitguardex.session';
this.command = {
command: 'gitguardex.activeAgents.openWorktree',
Expand Down Expand Up @@ -165,6 +175,29 @@ function buildChangeTreeNodes(changes) {
return materialize(root);
}

function countWorkingSessions(sessions) {
return sessions.filter((session) => session.activityKind === 'working').length;
}

function buildActiveAgentGroupNodes(sessions) {
const workingSessions = sessions
.filter((session) => session.activityKind === 'working')
.map((session) => new SessionItem(session));
const thinkingSessions = sessions
.filter((session) => session.activityKind !== 'working')
.map((session) => new SessionItem(session));
const groups = [];

if (workingSessions.length > 0) {
groups.push(new SectionItem('WORKING NOW', workingSessions));
}
if (thinkingSessions.length > 0) {
groups.push(new SectionItem('THINKING', thinkingSessions));
}

return groups;
}

class ActiveAgentsProvider {
constructor() {
this.onDidChangeTreeDataEmitter = new vscode.EventEmitter();
Expand All @@ -178,18 +211,19 @@ class ActiveAgentsProvider {

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

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

this.treeView.badge = sessionCount > 0
? {
value: sessionCount,
tooltip: `${sessionCount} active agent${sessionCount === 1 ? '' : 's'}`,
tooltip: `${sessionCount} active agent${sessionCount === 1 ? '' : 's'}`
+ (workingCount > 0 ? ` · ${workingCount} working now` : ''),
}
: undefined;
this.treeView.message = sessionCount > 0
Expand All @@ -204,7 +238,9 @@ class ActiveAgentsProvider {
async getChildren(element) {
if (element instanceof RepoItem) {
const sectionItems = [
new SectionItem('ACTIVE AGENTS', element.sessions.map((session) => new SessionItem(session))),
new SectionItem('ACTIVE AGENTS', buildActiveAgentGroupNodes(element.sessions), {
description: String(element.sessions.length),
}),
];
if (element.changes.length > 0) {
sectionItems.push(new SectionItem('CHANGES', buildChangeTreeNodes(element.changes)));
Expand All @@ -218,7 +254,11 @@ class ActiveAgentsProvider {

const repoEntries = await this.loadRepoEntries();
const sessionCount = repoEntries.reduce((total, entry) => total + entry.sessions.length, 0);
this.updateViewState(sessionCount);
const workingCount = repoEntries.reduce(
(total, entry) => total + countWorkingSessions(entry.sessions),
0,
);
this.updateViewState(sessionCount, workingCount);

if (repoEntries.length === 0) {
return [new InfoItem('No active Guardex agents', 'Open or start a sandbox session.')];
Expand Down
98 changes: 96 additions & 2 deletions test/vscode-active-agents-session-state.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,10 +361,15 @@ test('active-agents extension groups live sessions under a repo node', async ()

const [agentsSection] = await provider.getChildren(repoItem);
assert.equal(agentsSection.label, 'ACTIVE AGENTS');
assert.equal(agentsSection.description, '1');

const [sessionItem] = await provider.getChildren(agentsSection);
const [thinkingSection] = await provider.getChildren(agentsSection);
assert.equal(thinkingSection.label, 'THINKING');

const [sessionItem] = await provider.getChildren(thinkingSection);
assert.equal(sessionItem.label, 'live-task');
assert.match(sessionItem.description, /^thinking · \d+[smhd]/);
assert.equal(sessionItem.iconPath.id, 'loading~spin');
assert.deepEqual(registrations.treeViews[0].badge, {
value: 1,
tooltip: '1 active agent',
Expand Down Expand Up @@ -417,14 +422,23 @@ test('active-agents extension shows grouped repo changes beside active agents',

const provider = registrations.providers[0].provider;
const [repoItem] = await provider.getChildren();
assert.equal(repoItem.description, '1 active · 1 working · 1 changed');
const [agentsSection, changesSection] = await provider.getChildren(repoItem);
assert.equal(agentsSection.label, 'ACTIVE AGENTS');
assert.equal(changesSection.label, 'CHANGES');

const [sessionItem] = await provider.getChildren(agentsSection);
const [workingSection] = await provider.getChildren(agentsSection);
assert.equal(workingSection.label, 'WORKING NOW');

const [sessionItem] = await provider.getChildren(workingSection);
assert.equal(sessionItem.label, path.basename(worktreePath));
assert.match(sessionItem.description, /^working · 2 files · /);
assert.match(sessionItem.tooltip, /Changed 2 files: new-file\.txt, tracked\.txt/);
assert.equal(sessionItem.iconPath.id, 'edit');
assert.deepEqual(registrations.treeViews[0].badge, {
value: 1,
tooltip: '1 active agent · 1 working now',
});

const [changeItem] = await provider.getChildren(changesSection);
assert.equal(changeItem.label, 'root-file.txt');
Expand All @@ -435,3 +449,83 @@ test('active-agents extension shows grouped repo changes beside active agents',
subscription.dispose?.();
}
});

test('active-agents extension splits working and thinking sessions into separate groups', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-mixed-view-'));

const workingPath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-mixed-working-'));
initGitRepo(workingPath);
fs.writeFileSync(path.join(workingPath, 'tracked.txt'), 'base\n', 'utf8');
runGit(workingPath, ['add', 'tracked.txt']);
runGit(workingPath, ['commit', '-m', 'baseline']);
fs.writeFileSync(path.join(workingPath, 'tracked.txt'), 'base\nchanged\n', 'utf8');

const thinkingPath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-mixed-thinking-'));
initGitRepo(thinkingPath);
fs.writeFileSync(path.join(thinkingPath, 'tracked.txt'), 'base\n', 'utf8');
runGit(thinkingPath, ['add', 'tracked.txt']);
runGit(thinkingPath, ['commit', '-m', 'baseline']);

const workingSessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, 'agent/codex/working-task');
fs.mkdirSync(path.dirname(workingSessionPath), { recursive: true });
fs.writeFileSync(
workingSessionPath,
`${JSON.stringify(sessionSchema.buildSessionRecord({
repoRoot: tempRoot,
branch: 'agent/codex/working-task',
taskName: 'working-task',
agentName: 'codex',
worktreePath: workingPath,
pid: process.pid,
cliName: 'codex',
}), null, 2)}\n`,
'utf8',
);

const thinkingSessionPath = sessionSchema.sessionFilePathForBranch(tempRoot, 'agent/codex/thinking-task');
fs.writeFileSync(
thinkingSessionPath,
`${JSON.stringify(sessionSchema.buildSessionRecord({
repoRoot: tempRoot,
branch: 'agent/codex/thinking-task',
taskName: 'thinking-task',
agentName: 'codex',
worktreePath: thinkingPath,
pid: process.pid,
cliName: 'codex',
}), null, 2)}\n`,
'utf8',
);

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

extension.activate(context);

const provider = registrations.providers[0].provider;
const [repoItem] = await provider.getChildren();
assert.equal(repoItem.description, '2 active · 1 working');

const [agentsSection] = await provider.getChildren(repoItem);
const [workingSection, thinkingSection] = await provider.getChildren(agentsSection);
assert.equal(workingSection.label, 'WORKING NOW');
assert.equal(thinkingSection.label, 'THINKING');

const [workingItem] = await provider.getChildren(workingSection);
const [thinkingItem] = await provider.getChildren(thinkingSection);
assert.match(workingItem.description, /^working · 1 file · /);
assert.match(thinkingItem.description, /^thinking · \d+[smhd]/);
assert.deepEqual(registrations.treeViews[0].badge, {
value: 2,
tooltip: '2 active agents · 1 working now',
});

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