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

- The Active Agents companion already groups sessions by activity, but the tree still flattens sessions directly under those state buckets.
- The requested VS Code shape is worktree-first: users should be able to scan a worktree row and then expand to the agents running inside it.
- The current `CHANGES` grouping already knows ownership through `worktreePath` and `changedPaths`, so the missing behavior is tree presentation, not new runtime telemetry.

## What Changes

- Add a worktree row above agent rows inside `ACTIVE AGENTS`.
- Group `CHANGES` by worktree first, then by owning agent session, while leaving unmatched files in `Repo root`.
- Keep the existing debounced watcher model, session actions, lock decorations, and selected-session SCM commit flow unchanged.
- Mirror the tree-shape change in both shipped extension sources and focused regression tests.

## Impact

- Affected surfaces: `vscode/guardex-active-agents/extension.js`, `templates/vscode/guardex-active-agents/extension.js`, focused extension tests, and this OpenSpec change.
- Risk is limited to tree rendering and test expectations. No session-state schema or watcher lifecycle contract changes are required.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## ADDED Requirements

### Requirement: Worktree-first Active Agents rows

The VS Code `gitguardex.activeAgents` view MUST group agent session rows under their owning worktree rows before rendering session-owned file details.

#### Scenario: ACTIVE AGENTS shows worktree rows inside activity groups

- **GIVEN** the companion reads one or more live sessions for the same repo
- **WHEN** it renders an activity bucket such as `WORKING NOW` or `THINKING`
- **THEN** each child row under that bucket is a worktree row derived from `worktreePath`
- **AND** expanding the worktree row reveals the agent/session rows for that worktree
- **AND** expanding a session row reveals that session's touched-file rows.

#### Scenario: CHANGES shows worktree rows before session-owned files

- **GIVEN** repo changes belong to managed agent worktrees
- **WHEN** the companion renders `CHANGES`
- **THEN** it groups those changes under worktree rows first
- **AND** expanding a worktree row reveals the owning session row
- **AND** expanding that session row reveals the localized changed-file rows
- **AND** files not owned by any active worktree remain under `Repo root`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## 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

- Handoff: change=`agent-codex-group-active-agents-by-worktree-2026-04-22-18-12`; branch=`agent/codex/group-active-agents-by-worktree-2026-04-22-18-12`; scope=`worktree-first Active Agents tree grouping in live/template extension copies plus focused regression coverage`; action=`add worktree rows above agent rows, verify focused tests/specs, then finish via PR merge cleanup`.

## 1. Specification

- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-group-active-agents-by-worktree-2026-04-22-18-12`.
- [x] 1.2 Define normative requirements in `specs/vscode-active-agents-extension/spec.md`.

## 2. Implementation

- [x] 2.1 Add a worktree tree item and regroup `ACTIVE AGENTS` by `worktreePath` before agent/session rows.
- [x] 2.2 Regroup `CHANGES` by worktree first, then by owning session, while keeping unmatched files in `Repo root`.
- [x] 2.3 Mirror the tree-shape change in `templates/vscode/guardex-active-agents/extension.js`.
- [x] 2.4 Add/update focused regression coverage for the new tree shape.

## 3. Verification

- [x] 3.1 Run `node --test test/vscode-active-agents-session-state.test.js`.
- [x] 3.2 Run `openspec validate agent-codex-group-active-agents-by-worktree-2026-04-22-18-12 --type change --strict`.
- [x] 3.3 Run `openspec validate --specs`.

## 4. Cleanup (mandatory; run before claiming completion)

- [ ] 4.1 Run `gx branch finish --branch agent/codex/group-active-agents-by-worktree-2026-04-22-18-12 --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).
125 changes: 105 additions & 20 deletions templates/vscode/guardex-active-agents/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,11 +421,49 @@ class SectionItem extends vscode.TreeItem {
}
}

class WorktreeItem extends vscode.TreeItem {
constructor(worktreePath, sessions, items = [], options = {}) {
const normalizedWorktreePath = typeof worktreePath === 'string' ? worktreePath.trim() : '';
const sessionList = Array.isArray(sessions) ? sessions : [];
const changedCount = Number.isInteger(options.changedCount)
? options.changedCount
: sessionList.reduce((total, session) => total + (session.changeCount || 0), 0);
const descriptionParts = [formatCountLabel(sessionList.length, 'agent')];
if (changedCount > 0) {
descriptionParts.push(`${changedCount} changed`);
}
super(
path.basename(normalizedWorktreePath || '') || 'worktree',
items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None,
);
this.worktreePath = normalizedWorktreePath;
this.sessions = sessionList;
this.items = items;
this.description = options.description || descriptionParts.join(' · ');
this.tooltip = [
normalizedWorktreePath,
...sessionList.map((session) => session.branch).filter(Boolean),
].filter(Boolean).join('\n');
this.iconPath = new vscode.ThemeIcon('folder');
this.contextValue = 'gitguardex.worktree';
if (sessionList[0]?.worktreePath) {
this.command = {
command: 'gitguardex.activeAgents.openWorktree',
title: 'Open Agent Worktree',
arguments: [sessionList[0]],
};
}
}
}

class SessionItem extends vscode.TreeItem {
constructor(session, items = []) {
constructor(session, items = [], options = {}) {
const lockCount = Number.isFinite(session.lockCount) ? session.lockCount : 0;
const label = typeof options.label === 'string' && options.label.trim()
? options.label.trim()
: session.label;
super(
session.label,
label,
items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None,
);
this.session = session;
Expand Down Expand Up @@ -544,6 +582,10 @@ function sessionDisplayLabel(session) {
return session?.taskName || session?.label || session?.branch || path.basename(session?.worktreePath || '') || 'session';
}

function sessionTreeLabel(session) {
return session?.branch || sessionDisplayLabel(session);
}

function sessionWorktreePath(session) {
return typeof session?.worktreePath === 'string' ? session.worktreePath.trim() : '';
}
Expand Down Expand Up @@ -1151,6 +1193,36 @@ function countChangedPaths(repoRoot, sessions, changes) {
return changedKeys.size;
}

function groupSessionsByWorktree(sessions) {
const sessionsByWorktree = new Map();

for (const session of sessions || []) {
const worktreePath = sessionWorktreePath(session);
const key = worktreePath || session?.branch || `session-${sessionsByWorktree.size + 1}`;
if (!sessionsByWorktree.has(key)) {
sessionsByWorktree.set(key, {
worktreePath,
sessions: [],
});
}
sessionsByWorktree.get(key).sessions.push(session);
}

return [...sessionsByWorktree.values()]
.map((entry) => ({
...entry,
sessions: entry.sessions.sort((left, right) => (
sessionTreeLabel(left).localeCompare(sessionTreeLabel(right))
)),
}))
.sort((left, right) => {
const leftLabel = path.basename(left.worktreePath || '') || '';
const rightLabel = path.basename(right.worktreePath || '') || '';
return leftLabel.localeCompare(rightLabel)
|| (left.worktreePath || '').localeCompare(right.worktreePath || '');
});
}

function buildGroupedChangeTreeNodes(sessions, changes) {
const changesBySession = new Map();
const sessionByChangedPath = new Map();
Expand Down Expand Up @@ -1183,15 +1255,22 @@ function buildGroupedChangeTreeNodes(sessions, changes) {
changesBySession.get(session.branch).push(localizedChange);
}

const items = sessions
.map((session) => {
const sessionChanges = changesBySession.get(session.branch) || [];
if (sessionChanges.length === 0) {
return null;
}
return new SessionItem(session, buildChangeTreeNodes(sessionChanges));
})
.filter(Boolean);
const items = groupSessionsByWorktree(
sessions.filter((session) => (changesBySession.get(session.branch) || []).length > 0),
).map(({ worktreePath, sessions: worktreeSessions }) => {
const sessionItems = worktreeSessions.map((session) => (
new SessionItem(
session,
buildChangeTreeNodes(changesBySession.get(session.branch) || []),
{ label: sessionTreeLabel(session) },
)
));
const changedCount = worktreeSessions.reduce(
(total, session) => total + ((changesBySession.get(session.branch) || []).length),
0,
);
return new WorktreeItem(worktreePath, worktreeSessions, sessionItems, { changedCount });
});

if (repoRootChanges.length > 0) {
items.push(new SectionItem('Repo root', buildChangeTreeNodes(repoRootChanges), {
Expand Down Expand Up @@ -1319,14 +1398,20 @@ function commitWorktree(worktreePath, message) {
function buildActiveAgentGroupNodes(sessions) {
const groups = [];
for (const group of SESSION_ACTIVITY_GROUPS) {
const groupSessions = sessions
.filter((session) => session.activityKind === group.kind)
.map((session) => new SessionItem(
session,
buildChangeTreeNodes(session.touchedChanges || []),
));
if (groupSessions.length > 0) {
groups.push(new SectionItem(group.label, groupSessions));
const groupSessions = sessions.filter((session) => session.activityKind === group.kind);
const worktreeItems = groupSessionsByWorktree(groupSessions).map(({ worktreePath, sessions: worktreeSessions }) => (
new WorktreeItem(
worktreePath,
worktreeSessions,
worktreeSessions.map((session) => new SessionItem(
session,
buildChangeTreeNodes(session.touchedChanges || []),
{ label: sessionTreeLabel(session) },
)),
)
));
if (worktreeItems.length > 0) {
groups.push(new SectionItem(group.label, worktreeItems));
}
}

Expand Down Expand Up @@ -1488,7 +1573,7 @@ class ActiveAgentsProvider {
return sectionItems;
}

if (element instanceof SectionItem || element instanceof FolderItem || element instanceof SessionItem) {
if (element instanceof SectionItem || element instanceof FolderItem || element instanceof WorktreeItem || element instanceof SessionItem) {
return element.items;
}

Expand Down
2 changes: 1 addition & 1 deletion templates/vscode/guardex-active-agents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "GitGuardex Active Agents",
"description": "Shows live Guardex sandbox sessions and repo changes inside VS Code Source Control.",
"publisher": "recodeee",
"version": "0.0.6",
"version": "0.0.7",
"license": "MIT",
"icon": "icon.png",
"engines": {
Expand Down
Loading