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, 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, 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.

---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-21
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## Why

- The shipped VS Code companion only renders a flat `Active Agents` list, so it cannot resemble the grouped Source Control layout operators expect from the real Guardex workflow.
- Users need the companion to show active lanes in repo context, with nearby repo changes visible in the same SCM-side tree instead of forcing a separate mental model.

## What Changes

- Reshape the SCM-container tree so each repo renders as a top-level node with grouped `ACTIVE AGENTS` and `CHANGES` sections.
- Keep the existing live-agent activity copy (`thinking` / `working`, changed-file counts, elapsed time), while also deriving repo-root git changes for the new `CHANGES` group.
- Add focused regression coverage for the new grouped tree structure and update the extension/readme copy to describe the repo-context view.

## Impact

- Affected surfaces: `templates/vscode/guardex-active-agents/extension.js`, `templates/vscode/guardex-active-agents/session-schema.js`, `templates/vscode/guardex-active-agents/README.md`, `test/vscode-active-agents-session-state.test.js`, and the root `README.md`.
- Risk is narrow because the change remains read-only; it only changes how repo/session state is presented in the VS Code companion.
- If repo git state cannot be inspected, the extension should keep showing active agents and simply omit repo-change rows instead of failing the whole view.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## ADDED Requirements

### Requirement: Active Agents SCM tree keeps repo context
The Guardex Active Agents VS Code companion SHALL render live sessions under a repo-scoped tree layout so operators can see active lanes in the same SCM-side structure as nearby repo changes.

#### Scenario: Live repo renders grouped sections
- **WHEN** the companion finds one or more live Guardex sessions for a repo in the current workspace
- **THEN** the SCM tree shows a repo node for that repo
- **AND** the repo node contains an `ACTIVE AGENTS` section with the live session rows
- **AND** each session row keeps its activity state plus elapsed-time description.

#### Scenario: Repo changes render beside active agents
- **WHEN** a repo with live Guardex sessions also has local git modifications in its root working tree
- **THEN** the repo node also contains a `CHANGES` section
- **AND** the change rows reflect the repo-relative changed paths
- **AND** the change rows surface concise git status markers.

#### Scenario: Change inspection failure degrades safely
- **WHEN** the companion cannot inspect repo git status for a repo that still has live Guardex sessions
- **THEN** the `ACTIVE AGENTS` section still renders
- **AND** the repo simply omits `CHANGES` rows instead of crashing or hiding the repo node.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## 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-21 21:24Z codex owns `templates/vscode/guardex-active-agents/*`, `test/vscode-active-agents-session-state.test.js`, `README.md`, and this change workspace to ship a repo-grouped SCM tree with `ACTIVE AGENTS` plus repo `CHANGES`.

## 1. Specification

- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22`.
- [x] 1.2 Define normative requirements in `specs/vscode-active-agents-scm-provider-layout/spec.md`.

## 2. Implementation

- [x] 2.1 Replace the flat session list with a repo-scoped tree that groups `ACTIVE AGENTS` and repo `CHANGES`.
- [x] 2.2 Add/update focused regression coverage for the grouped tree structure and repo-change parsing.

## 3. Verification

- [x] 3.1 Run targeted project verification commands.
- [x] 3.2 Run `openspec validate agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22 --type change --strict`.
- [x] 3.3 Run `openspec validate --specs`.

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

- [ ] 4.1 Run the cleanup pipeline: `bash scripts/agent-branch-finish.sh --branch agent/codex/vscode-active-agents-scm-provider-layout-2026-04-21-23-22 --base main --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation.
- [ ] 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).
4 changes: 3 additions & 1 deletion templates/vscode/guardex-active-agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ Local VS Code companion for Guardex-managed repos.
What it does:

- Adds an `Active Agents` view to the Source Control container.
- Renders one row per live Guardex sandbox session.
- 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.
- 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.
- 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
167 changes: 147 additions & 20 deletions templates/vscode/guardex-active-agents/extension.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const fs = require('node:fs');
const path = require('node:path');
const vscode = require('vscode');
const { formatElapsedFrom, readActiveSessions } = require('./session-schema.js');
const { formatElapsedFrom, readActiveSessions, readRepoChanges } = require('./session-schema.js');

class InfoItem extends vscode.TreeItem {
constructor(label, description = '') {
Expand All @@ -11,17 +12,31 @@ class InfoItem extends vscode.TreeItem {
}

class RepoItem extends vscode.TreeItem {
constructor(repoRoot, sessions) {
constructor(repoRoot, sessions, changes) {
super(path.basename(repoRoot), vscode.TreeItemCollapsibleState.Expanded);
this.repoRoot = repoRoot;
this.sessions = sessions;
this.description = `${sessions.length} active`;
this.changes = changes;
const descriptionParts = [`${sessions.length} active`];
if (changes.length > 0) {
descriptionParts.push(`${changes.length} changed`);
}
this.description = descriptionParts.join(' · ');
this.tooltip = repoRoot;
this.iconPath = new vscode.ThemeIcon('repo');
this.contextValue = 'gitguardex.repo';
}
}

class SectionItem extends vscode.TreeItem {
constructor(label, items) {
super(label, vscode.TreeItemCollapsibleState.Expanded);
this.items = items;
this.description = items.length > 0 ? String(items.length) : '';
this.contextValue = 'gitguardex.section';
}
}

class SessionItem extends vscode.TreeItem {
constructor(session) {
super(session.label, vscode.TreeItemCollapsibleState.None);
Expand Down Expand Up @@ -53,10 +68,103 @@ class SessionItem extends vscode.TreeItem {
}
}

class FolderItem extends vscode.TreeItem {
constructor(label, relativePath, items) {
super(label, vscode.TreeItemCollapsibleState.Expanded);
this.relativePath = relativePath;
this.items = items;
this.tooltip = relativePath;
this.iconPath = new vscode.ThemeIcon('folder');
this.contextValue = 'gitguardex.folder';
}
}

class ChangeItem extends vscode.TreeItem {
constructor(change) {
super(path.basename(change.relativePath), vscode.TreeItemCollapsibleState.None);
this.change = change;
this.description = change.statusLabel;
this.tooltip = [
change.relativePath,
`Status ${change.statusText}`,
change.originalPath ? `Renamed from ${change.originalPath}` : '',
change.absolutePath,
].filter(Boolean).join('\n');
this.resourceUri = vscode.Uri.file(change.absolutePath);
this.contextValue = 'gitguardex.change';
this.command = {
command: 'gitguardex.activeAgents.openChange',
title: 'Open Changed File',
arguments: [change],
};
}
}

function repoRootFromSessionFile(filePath) {
return path.resolve(path.dirname(filePath), '..', '..', '..');
}

function buildChangeTreeNodes(changes) {
const root = [];

function sortNodes(nodes) {
nodes.sort((left, right) => {
const leftIsFolder = left.kind === 'folder';
const rightIsFolder = right.kind === 'folder';
if (leftIsFolder !== rightIsFolder) {
return leftIsFolder ? -1 : 1;
}
return left.label.localeCompare(right.label);
});

for (const node of nodes) {
if (node.kind === 'folder') {
sortNodes(node.children);
}
}
}

for (const change of changes) {
const segments = change.relativePath.split(/[\\/]+/).filter(Boolean);
if (segments.length <= 1) {
root.push({ kind: 'change', label: change.relativePath, change });
continue;
}

let nodes = root;
let folderPath = '';
for (const segment of segments.slice(0, -1)) {
folderPath = folderPath ? path.posix.join(folderPath, segment) : segment;
let folderNode = nodes.find((node) => node.kind === 'folder' && node.relativePath === folderPath);
if (!folderNode) {
folderNode = {
kind: 'folder',
label: segment,
relativePath: folderPath,
children: [],
};
nodes.push(folderNode);
}
nodes = folderNode.children;
}

nodes.push({ kind: 'change', label: change.relativePath, change });
}

sortNodes(root);

function materialize(nodes) {
return nodes.map((node) => {
if (node.kind === 'folder') {
return new FolderItem(node.label, node.relativePath, materialize(node.children));
}
return new ChangeItem(node.change);
});
}

return materialize(root);
}

class ActiveAgentsProvider {
constructor() {
this.onDidChangeTreeDataEmitter = new vscode.EventEmitter();
Expand Down Expand Up @@ -95,29 +203,31 @@ class ActiveAgentsProvider {

async getChildren(element) {
if (element instanceof RepoItem) {
return element.sessions.map((session) => new SessionItem(session));
const sectionItems = [
new SectionItem('ACTIVE AGENTS', element.sessions.map((session) => new SessionItem(session))),
];
if (element.changes.length > 0) {
sectionItems.push(new SectionItem('CHANGES', buildChangeTreeNodes(element.changes)));
}
return sectionItems;
}

const sessionsByRepo = await this.loadSessionsByRepo();
const sessionCount = [...sessionsByRepo.values()].reduce((total, sessions) => total + sessions.length, 0);
if (element instanceof SectionItem || element instanceof FolderItem) {
return element.items;
}

const repoEntries = await this.loadRepoEntries();
const sessionCount = repoEntries.reduce((total, entry) => total + entry.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) {
if (repoEntries.length === 0) {
return [new InfoItem('No active Guardex agents', 'Open or start a sandbox session.')];
}

if (repos.length === 1) {
return repos[0].sessions.map((session) => new SessionItem(session));
}

return repos.map((entry) => new RepoItem(entry.repoRoot, entry.sessions));
return repoEntries.map((entry) => new RepoItem(entry.repoRoot, entry.sessions, entry.changes));
}

async loadSessionsByRepo() {
async loadRepoEntries() {
const sessionFiles = await vscode.workspace.findFiles(
'**/.omx/state/active-sessions/*.json',
'**/{node_modules,.git,.omx/agent-worktrees,.omc/agent-worktrees}/**',
Expand All @@ -135,15 +245,20 @@ class ActiveAgentsProvider {
}
}

const sessionsByRepo = new Map();
const repoEntries = [];
for (const repoRoot of repoRoots) {
const sessions = readActiveSessions(repoRoot);
if (sessions.length > 0) {
sessionsByRepo.set(repoRoot, sessions);
repoEntries.push({
repoRoot,
sessions,
changes: readRepoChanges(repoRoot),
});
}
}

return sessionsByRepo;
repoEntries.sort((left, right) => left.repoRoot.localeCompare(right.repoRoot));
return repoEntries;
}
}

Expand Down Expand Up @@ -172,6 +287,18 @@ function activate(context) {
{ forceNewWindow: true },
);
}),
vscode.commands.registerCommand('gitguardex.activeAgents.openChange', async (change) => {
if (!change?.absolutePath) {
return;
}

if (!fs.existsSync(change.absolutePath)) {
vscode.window.showInformationMessage?.(`Changed path is no longer on disk: ${change.relativePath}`);
return;
}

await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(change.absolutePath));
}),
vscode.workspace.onDidChangeWorkspaceFolders(refresh),
watcher,
{ dispose: () => clearInterval(interval) },
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
@@ -1,7 +1,7 @@
{
"name": "gitguardex-active-agents",
"displayName": "GitGuardex Active Agents",
"description": "Shows live Guardex sandbox sessions inside VS Code Source Control.",
"description": "Shows live Guardex sandbox sessions and repo changes inside VS Code Source Control.",
"publisher": "recodeee",
"version": "0.0.1",
"license": "MIT",
Expand Down
Loading