diff --git a/README.md b/README.md index b94a82c..551c9ab 100644 --- a/README.md +++ b/README.md @@ -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. --- diff --git a/openspec/changes/agent-codex-vscode-working-agents-groups-2026-04-22-09-05/proposal.md b/openspec/changes/agent-codex-vscode-working-agents-groups-2026-04-22-09-05/proposal.md new file mode 100644 index 0000000..f08c6c7 --- /dev/null +++ b/openspec/changes/agent-codex-vscode-working-agents-groups-2026-04-22-09-05/proposal.md @@ -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. diff --git a/openspec/changes/agent-codex-vscode-working-agents-groups-2026-04-22-09-05/specs/vscode-working-agents-groups/spec.md b/openspec/changes/agent-codex-vscode-working-agents-groups-2026-04-22-09-05/specs/vscode-working-agents-groups/spec.md new file mode 100644 index 0000000..ee321d9 --- /dev/null +++ b/openspec/changes/agent-codex-vscode-working-agents-groups-2026-04-22-09-05/specs/vscode-working-agents-groups/spec.md @@ -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. diff --git a/openspec/changes/agent-codex-vscode-working-agents-groups-2026-04-22-09-05/tasks.md b/openspec/changes/agent-codex-vscode-working-agents-groups-2026-04-22-09-05/tasks.md new file mode 100644 index 0000000..bd57762 --- /dev/null +++ b/openspec/changes/agent-codex-vscode-working-agents-groups-2026-04-22-09-05/tasks.md @@ -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). diff --git a/templates/vscode/guardex-active-agents/README.md b/templates/vscode/guardex-active-agents/README.md index 06bb43e..b63a8c3 100644 --- a/templates/vscode/guardex-active-agents/README.md +++ b/templates/vscode/guardex-active-agents/README.md @@ -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/`. diff --git a/templates/vscode/guardex-active-agents/extension.js b/templates/vscode/guardex-active-agents/extension.js index c67eb05..a375c52 100644 --- a/templates/vscode/guardex-active-agents/extension.js +++ b/templates/vscode/guardex-active-agents/extension.js @@ -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'; } } @@ -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', @@ -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(); @@ -178,10 +211,10 @@ class ActiveAgentsProvider { attachTreeView(treeView) { this.treeView = treeView; - this.updateViewState(0); + this.updateViewState(0, 0); } - updateViewState(sessionCount) { + updateViewState(sessionCount, workingCount) { if (!this.treeView) { return; } @@ -189,7 +222,8 @@ class ActiveAgentsProvider { 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 @@ -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))); @@ -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.')]; diff --git a/test/vscode-active-agents-session-state.test.js b/test/vscode-active-agents-session-state.test.js index 698b022..2687e65 100644 --- a/test/vscode-active-agents-session-state.test.js +++ b/test/vscode-active-agents-session-state.test.js @@ -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', @@ -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'); @@ -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?.(); + } +});