diff --git a/README.md b/README.md index 8f05ff0..294b83e 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, 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. --- diff --git a/openspec/changes/agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22/.openspec.yaml b/openspec/changes/agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22/.openspec.yaml new file mode 100644 index 0000000..4b8c565 --- /dev/null +++ b/openspec/changes/agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-21 diff --git a/openspec/changes/agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22/proposal.md b/openspec/changes/agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22/proposal.md new file mode 100644 index 0000000..9202ec7 --- /dev/null +++ b/openspec/changes/agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22/proposal.md @@ -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. diff --git a/openspec/changes/agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22/specs/vscode-active-agents-scm-provider-layout/spec.md b/openspec/changes/agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22/specs/vscode-active-agents-scm-provider-layout/spec.md new file mode 100644 index 0000000..a167ba1 --- /dev/null +++ b/openspec/changes/agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22/specs/vscode-active-agents-scm-provider-layout/spec.md @@ -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. diff --git a/openspec/changes/agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22/tasks.md b/openspec/changes/agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22/tasks.md new file mode 100644 index 0000000..aab6ad3 --- /dev/null +++ b/openspec/changes/agent-codex-vscode-active-agents-scm-provider-layout-2026-04-21-23-22/tasks.md @@ -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). diff --git a/templates/vscode/guardex-active-agents/README.md b/templates/vscode/guardex-active-agents/README.md index aa9fabf..06bb43e 100644 --- a/templates/vscode/guardex-active-agents/README.md +++ b/templates/vscode/guardex-active-agents/README.md @@ -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/`. diff --git a/templates/vscode/guardex-active-agents/extension.js b/templates/vscode/guardex-active-agents/extension.js index 2c89e51..c67eb05 100644 --- a/templates/vscode/guardex-active-agents/extension.js +++ b/templates/vscode/guardex-active-agents/extension.js @@ -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 = '') { @@ -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); @@ -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(); @@ -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}/**', @@ -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; } } @@ -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) }, diff --git a/templates/vscode/guardex-active-agents/package.json b/templates/vscode/guardex-active-agents/package.json index 3c3b0d1..da83ad5 100644 --- a/templates/vscode/guardex-active-agents/package.json +++ b/templates/vscode/guardex-active-agents/package.json @@ -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", diff --git a/templates/vscode/guardex-active-agents/session-schema.js b/templates/vscode/guardex-active-agents/session-schema.js index bd689af..aaeef7b 100644 --- a/templates/vscode/guardex-active-agents/session-schema.js +++ b/templates/vscode/guardex-active-agents/session-schema.js @@ -6,6 +6,7 @@ const ACTIVE_SESSIONS_RELATIVE_DIR = path.join('.omx', 'state', 'active-sessions const SESSION_SCHEMA_VERSION = 1; const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json'); const MAX_CHANGED_PATH_PREVIEW = 3; +const ACTIVE_SESSIONS_FILTER_PREFIX = ACTIVE_SESSIONS_RELATIVE_DIR.split(path.sep).join('/'); function toNonEmptyString(value, fallback = '') { const normalized = typeof value === 'string' ? value.trim() : String(value || '').trim(); @@ -41,8 +42,7 @@ function splitOutputLines(output) { return output .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean); + .filter((line) => line.trim().length > 0); } function runGitLines(worktreePath, args) { @@ -57,6 +57,23 @@ function runGitLines(worktreePath, args) { } } +function unquoteGitPath(value) { + if (typeof value !== 'string') { + return ''; + } + + const trimmed = value.trim(); + if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) { + return trimmed; + } + + try { + return JSON.parse(trimmed); + } catch (_error) { + return trimmed.slice(1, -1); + } +} + function formatFileCount(count) { return `${count} file${count === 1 ? '' : 's'}`; } @@ -74,6 +91,80 @@ function previewChangedPaths(paths) { return `${preview}, +${paths.length - MAX_CHANGED_PATH_PREVIEW} more`; } +function deriveRepoChangeStatus(statusPair) { + if (statusPair === '??') { + return { + statusCode: '??', + statusLabel: 'U', + statusText: 'Untracked', + }; + } + + const code = [statusPair[1], statusPair[0]].find((value) => value && value !== ' ') || 'M'; + const statusTextByCode = { + A: 'Added', + C: 'Copied', + D: 'Deleted', + M: 'Modified', + R: 'Renamed', + T: 'Type changed', + U: 'Conflicted', + }; + + return { + statusCode: code, + statusLabel: code, + statusText: statusTextByCode[code] || 'Changed', + }; +} + +function parseRepoChangeLine(repoRoot, line) { + if (typeof line !== 'string' || line.length < 4) { + return null; + } + + const statusPair = line.slice(0, 2); + if (statusPair === '!!') { + return null; + } + + const rawPath = line.slice(3).trim(); + if (!rawPath) { + return null; + } + + let relativePath = rawPath; + let originalPath = ''; + if (rawPath.includes(' -> ')) { + const parts = rawPath.split(' -> '); + if (parts.length === 2) { + originalPath = unquoteGitPath(parts[0]); + relativePath = parts[1]; + } + } + + relativePath = unquoteGitPath(relativePath); + if (!relativePath) { + return null; + } + + const normalizedRelativePath = relativePath.split(path.sep).join('/'); + if ( + normalizedRelativePath === ACTIVE_SESSIONS_FILTER_PREFIX + || normalizedRelativePath.startsWith(`${ACTIVE_SESSIONS_FILTER_PREFIX}/`) + ) { + return null; + } + + const status = deriveRepoChangeStatus(statusPair); + return { + ...status, + originalPath, + relativePath, + absolutePath: path.join(path.resolve(repoRoot), relativePath), + }; +} + function collectWorktreeChangedPaths(worktreePath) { const changedGroups = [ runGitLines(worktreePath, ['diff', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`]), @@ -281,6 +372,18 @@ function readActiveSessions(repoRoot, options = {}) { return sessions; } +function readRepoChanges(repoRoot) { + const statusLines = runGitLines(repoRoot, ['status', '--porcelain=v1', '--untracked-files=all']); + if (!statusLines) { + return []; + } + + return statusLines + .map((line) => parseRepoChangeLine(repoRoot, line)) + .filter(Boolean) + .sort((left, right) => left.relativePath.localeCompare(right.relativePath)); +} + module.exports = { ACTIVE_SESSIONS_RELATIVE_DIR, SESSION_SCHEMA_VERSION, @@ -293,8 +396,11 @@ module.exports = { formatFileCount, isPidAlive, normalizeSessionRecord, + parseRepoChangeLine, previewChangedPaths, readActiveSessions, + readRepoChanges, + deriveRepoChangeStatus, sanitizeBranchForFile, sessionFileNameForBranch, sessionFilePathForBranch, diff --git a/test/vscode-active-agents-session-state.test.js b/test/vscode-active-agents-session-state.test.js index a55519f..698b022 100644 --- a/test/vscode-active-agents-session-state.test.js +++ b/test/vscode-active-agents-session-state.test.js @@ -112,6 +112,7 @@ function createMockVscode(tempRoot) { file: (fsPath) => ({ fsPath }), }, window: { + showInformationMessage: async () => {}, createTreeView: (viewId, options) => { const treeView = { viewId, @@ -262,6 +263,26 @@ test('session-schema derives working activity from dirty sandbox worktrees', () assert.equal(session.activitySummary, 'new-file.txt, tracked.txt'); }); +test('session-schema derives repo change rows from root git status', () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-active-session-root-status-')); + initGitRepo(tempRoot); + fs.writeFileSync(path.join(tempRoot, 'tracked.txt'), 'base\n', 'utf8'); + runGit(tempRoot, ['add', 'tracked.txt']); + runGit(tempRoot, ['commit', '-m', 'baseline']); + + fs.writeFileSync(path.join(tempRoot, 'tracked.txt'), 'base\nchanged\n', 'utf8'); + fs.writeFileSync(path.join(tempRoot, 'new-file.txt'), 'new\n', 'utf8'); + + const changes = sessionSchema.readRepoChanges(tempRoot); + assert.deepEqual( + changes.map((change) => [change.relativePath, change.statusLabel]), + [ + ['new-file.txt', 'U'], + ['tracked.txt', 'M'], + ], + ); +}); + test('install-vscode-active-agents-extension installs the current extension version and prunes older copies', () => { const tempExtensionsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-ext-')); const staleDir = path.join(tempExtensionsDir, 'recodeee.gitguardex-active-agents-0.0.0'); @@ -308,7 +329,7 @@ test('active-agents extension registers a provider with getTreeItem', async () = } }); -test('active-agents extension updates the SCM badge for live sessions', async () => { +test('active-agents extension groups live sessions under a repo node', 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 }); @@ -334,7 +355,14 @@ test('active-agents extension updates the SCM badge for live sessions', async () extension.activate(context); const provider = registrations.providers[0].provider; - const [sessionItem] = await provider.getChildren(); + const [repoItem] = await provider.getChildren(); + assert.equal(repoItem.label, path.basename(tempRoot)); + assert.equal(repoItem.description, '1 active'); + + const [agentsSection] = await provider.getChildren(repoItem); + assert.equal(agentsSection.label, 'ACTIVE AGENTS'); + + const [sessionItem] = await provider.getChildren(agentsSection); assert.equal(sessionItem.label, 'live-task'); assert.match(sessionItem.description, /^thinking · \d+[smhd]/); assert.deepEqual(registrations.treeViews[0].badge, { @@ -348,9 +376,15 @@ test('active-agents extension updates the SCM badge for live sessions', async () } }); -test('active-agents extension shows working rows when the sandbox has changes', async () => { +test('active-agents extension shows grouped repo changes beside active agents', async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-working-view-')); - const worktreePath = path.join(tempRoot, 'sandbox'); + initGitRepo(tempRoot); + fs.writeFileSync(path.join(tempRoot, 'root-file.txt'), 'base\n', 'utf8'); + runGit(tempRoot, ['add', 'root-file.txt']); + runGit(tempRoot, ['commit', '-m', 'baseline']); + fs.writeFileSync(path.join(tempRoot, 'root-file.txt'), 'base\nchanged\n', 'utf8'); + + const worktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-working-session-')); initGitRepo(worktreePath); fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\n', 'utf8'); runGit(worktreePath, ['add', 'tracked.txt']); @@ -382,11 +416,21 @@ test('active-agents extension shows working rows when the sandbox has changes', extension.activate(context); const provider = registrations.providers[0].provider; - const [sessionItem] = await provider.getChildren(); - assert.equal(sessionItem.label, 'sandbox'); + const [repoItem] = await provider.getChildren(); + const [agentsSection, changesSection] = await provider.getChildren(repoItem); + assert.equal(agentsSection.label, 'ACTIVE AGENTS'); + assert.equal(changesSection.label, 'CHANGES'); + + const [sessionItem] = await provider.getChildren(agentsSection); + 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/); + const [changeItem] = await provider.getChildren(changesSection); + assert.equal(changeItem.label, 'root-file.txt'); + assert.equal(changeItem.description, 'M'); + assert.match(changeItem.tooltip, /Status Modified/); + for (const subscription of context.subscriptions) { subscription.dispose?.(); }