From 7742cbee57d03f0110712785ba065e5a88deace5 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Thu, 23 Apr 2026 17:03:17 +0200 Subject: [PATCH] Keep Active Agents visible in a second VS Code window The Active Agents repo scan treated the opened workspace path as the repo root. In a second VS Code window opened on a linked worktree or subfolder, that hid the owning repo's active-session files. Resolve the owning repo root from git metadata before reading sessions, keep template parity, and cover the linked-worktree window with a regression test. Constraint: Second VS Code windows may open on linked worktrees or repo subfolders, not only the owning repo root Rejected: Aggregate every parent-workspace daemon session | would leak unrelated repo agents into a repo-scoped window Confidence: high Scope-risk: narrow Reversibility: clean Directive: Resolve workspace folders to owning repo roots before reading Active Agents session state Tested: NODE_PATH=/home/deadpool/Documents/recodee/gitguardex/node_modules node --test test/vscode-active-agents-session-state.test.js Tested: openspec validate agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50 --type change --strict Tested: openspec validate --specs (no items found to validate) Not-tested: Manual two-window VS Code clickthrough --- .../proposal.md | 15 ++++ .../vscode-active-agents-extension/spec.md | 25 ++++++ .../tasks.md | 35 ++++++++ .../vscode/guardex-active-agents/extension.js | 85 ++++++++++++++++++- .../vscode/guardex-active-agents/package.json | 2 +- ...vscode-active-agents-session-state.test.js | 56 ++++++++++++ vscode/guardex-active-agents/extension.js | 85 ++++++++++++++++++- vscode/guardex-active-agents/package.json | 2 +- 8 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 openspec/changes/agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50/proposal.md create mode 100644 openspec/changes/agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50/specs/vscode-active-agents-extension/spec.md create mode 100644 openspec/changes/agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50/tasks.md diff --git a/openspec/changes/agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50/proposal.md b/openspec/changes/agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50/proposal.md new file mode 100644 index 0000000..9b860d9 --- /dev/null +++ b/openspec/changes/agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50/proposal.md @@ -0,0 +1,15 @@ +# Show Active Agents in a second VS Code window + +## Why + +The Active Agents view already discovers nested repos when a parent workspace is open, but a second VS Code window opened directly on a Guardex worktree or a subfolder inside that repo can miss the owning repo's active-session state. Operators need the second window to show the same repo-local Active Agents view without leaking unrelated parent-workspace agents. + +## What Changes + +- Resolve each workspace folder to its owning Guardex repo root instead of assuming the folder path itself is the repo root. +- Keep the Active Agents view scoped to the resolved repo root so a `gitguardex` window only shows `gitguardex` agents. +- Add focused regression coverage for a second VS Code window opened on a linked Guardex worktree. + +## Impact + +This only changes repo discovery for the VS Code Active Agents companion. It does not change Guardex branch creation, locking, or finish behavior. diff --git a/openspec/changes/agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50/specs/vscode-active-agents-extension/spec.md b/openspec/changes/agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50/specs/vscode-active-agents-extension/spec.md new file mode 100644 index 0000000..4e01baf --- /dev/null +++ b/openspec/changes/agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50/specs/vscode-active-agents-extension/spec.md @@ -0,0 +1,25 @@ +## ADDED Requirements + +### Requirement: Second-window repo-root resolution + +The VS Code `gitguardex.activeAgents` view MUST resolve an opened workspace folder to its owning Guardex repo root before reading Active Agents session state, so a second VS Code window opened on a linked worktree or repo subfolder still shows the owning repo's sessions. + +#### Scenario: Linked worktree window still shows the owning repo sessions + +- **GIVEN** a Guardex repo root has active-session records under `.omx/state/active-sessions/` +- **AND** a second VS Code window is opened on a linked worktree under `.omx/agent-worktrees/...` +- **WHEN** the Active Agents view scans workspace folders for repo candidates +- **THEN** it resolves the owning repo root from the linked worktree git metadata +- **AND** it reads sessions from that owning repo root instead of the worktree path. + +### Requirement: Repo-scoped second-window filtering + +The VS Code `gitguardex.activeAgents` view MUST keep the tree scoped to the resolved repo root for the currently opened repo, so a `gitguardex` window only shows `gitguardex` agents even when the parent workspace has other Guardex repos. + +#### Scenario: Nested repo window only shows nested repo agents + +- **GIVEN** a parent workspace contains multiple Guardex repos +- **AND** a second VS Code window is opened directly on the nested `gitguardex` repo or one of its linked worktrees +- **WHEN** the Active Agents view renders the top-level repo rows +- **THEN** it only renders the resolved `gitguardex` repo root for that window +- **AND** it does not add unrelated parent-repo agent sessions to that tree. diff --git a/openspec/changes/agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50/tasks.md b/openspec/changes/agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50/tasks.md new file mode 100644 index 0000000..df42c56 --- /dev/null +++ b/openspec/changes/agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50/tasks.md @@ -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-show-active-agents-in-second-vscode-wind-2026-04-23-16-50`; branch=`agent/codex/show-active-agents-in-second-vscode-wind-2026-04-23-16-50`; scope=`VS Code Active Agents repo-root resolution for second-window worktree/subdir views, template parity, focused regression`; action=`show owning gitguardex agents when another VS Code window opens on a linked worktree or nested repo path, verify, then finish via PR merge cleanup`. + +## 1. Specification + +- [x] 1.1 Define second-window repo-root resolution requirements. +- [x] 1.2 Keep cleanup evidence requirements explicit. + +## 2. Implementation + +- [x] 2.1 Resolve workspace folders to owning repo roots before reading Active Agents sessions. +- [x] 2.2 Keep the view scoped to the resolved repo root so a gitguardex window does not show unrelated parent-repo agents. +- [x] 2.3 Mirror extension changes in `templates/vscode/guardex-active-agents/extension.js`. +- [x] 2.4 Add focused regression coverage for a linked-worktree VS Code window. + +## 3. Verification + +- [x] 3.1 Run `node --test test/vscode-active-agents-session-state.test.js`. +- [x] 3.2 Run `openspec validate agent-codex-show-active-agents-in-second-vscode-wind-2026-04-23-16-50 --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/show-active-agents-in-second-vscode-wind-2026-04-23-16-50 --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/extension.js b/templates/vscode/guardex-active-agents/extension.js index 10197d4..7d482f2 100644 --- a/templates/vscode/guardex-active-agents/extension.js +++ b/templates/vscode/guardex-active-agents/extension.js @@ -1781,6 +1781,89 @@ async function openSessionDiff(session) { } } +function readGitDirPath(targetPath) { + const normalizedTargetPath = typeof targetPath === 'string' ? targetPath.trim() : ''; + if (!normalizedTargetPath) { + return ''; + } + + const gitPath = path.join(path.resolve(normalizedTargetPath), '.git'); + try { + if (fs.statSync(gitPath).isDirectory()) { + return gitPath; + } + } catch (_error) { + return ''; + } + + try { + const gitPointer = fs.readFileSync(gitPath, 'utf8'); + const match = gitPointer.match(/^gitdir:\s*(.+)$/m); + if (match?.[1]) { + return path.resolve(path.dirname(gitPath), match[1].trim()); + } + } catch (_error) { + return ''; + } + + return ''; +} + +function resolveRepoRootFromGitDir(targetPath) { + const gitDir = readGitDirPath(targetPath); + if (!gitDir) { + return ''; + } + + let commonDir = gitDir; + try { + const commonDirPath = path.join(gitDir, 'commondir'); + if (fs.existsSync(commonDirPath)) { + const rawCommonDir = fs.readFileSync(commonDirPath, 'utf8').trim(); + if (rawCommonDir) { + commonDir = path.resolve(gitDir, rawCommonDir); + } + } + } catch (_error) { + // Fall back to the direct git dir when commondir is unreadable. + } + + return path.basename(commonDir) === '.git' + ? path.resolve(path.dirname(commonDir)) + : ''; +} + +function readGitTopLevel(targetPath) { + try { + return cp.execFileSync('git', ['-C', targetPath, 'rev-parse', '--show-toplevel'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch (_error) { + return ''; + } +} + +function resolveWorkspaceFolderRepoRoot(workspacePath) { + const normalizedWorkspacePath = typeof workspacePath === 'string' ? workspacePath.trim() : ''; + if (!normalizedWorkspacePath) { + return ''; + } + + const absoluteWorkspacePath = path.resolve(normalizedWorkspacePath); + const directRepoRoot = resolveRepoRootFromGitDir(absoluteWorkspacePath); + if (directRepoRoot) { + return directRepoRoot; + } + + const gitTopLevel = readGitTopLevel(absoluteWorkspacePath); + if (!gitTopLevel) { + return absoluteWorkspacePath; + } + + return resolveRepoRootFromGitDir(gitTopLevel) || path.resolve(gitTopLevel); +} + function repoRootFromSessionFile(filePath) { return path.resolve(path.dirname(filePath), '..', '..', '..'); } @@ -2109,7 +2192,7 @@ async function findRepoSessionEntries() { } for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { if (workspaceFolder?.uri?.fsPath) { - addRepoRootCandidate(workspaceFolder.uri.fsPath); + addRepoRootCandidate(resolveWorkspaceFolderRepoRoot(workspaceFolder.uri.fsPath)); } } diff --git a/templates/vscode/guardex-active-agents/package.json b/templates/vscode/guardex-active-agents/package.json index 32b5c28..0f7584d 100644 --- a/templates/vscode/guardex-active-agents/package.json +++ b/templates/vscode/guardex-active-agents/package.json @@ -3,7 +3,7 @@ "displayName": "GitGuardex Active Agents", "description": "Shows live Guardex sandbox sessions and repo changes in a dedicated VS Code Active Agents sidebar.", "publisher": "recodeee", - "version": "0.0.15", + "version": "0.0.16", "license": "MIT", "icon": "icon.png", "engines": { diff --git a/test/vscode-active-agents-session-state.test.js b/test/vscode-active-agents-session-state.test.js index f61d6d2..17703ad 100644 --- a/test/vscode-active-agents-session-state.test.js +++ b/test/vscode-active-agents-session-state.test.js @@ -2376,6 +2376,62 @@ test('active-agents extension surfaces plain managed worktrees from workspace fa } }); +test('active-agents extension resolves owning repo sessions when the window is opened on a linked worktree', async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-linked-worktree-view-')); + initGitRepo(tempRoot); + fs.writeFileSync(path.join(tempRoot, 'tracked.txt'), 'base\n', 'utf8'); + runGit(tempRoot, ['add', 'tracked.txt']); + runGit(tempRoot, ['commit', '-m', 'baseline']); + + const branch = 'agent/codex/linked-worktree-visible-task'; + const worktreePath = path.join( + tempRoot, + '.omx', + 'agent-worktrees', + 'agent__codex__linked-worktree-visible-task', + ); + fs.mkdirSync(path.dirname(worktreePath), { recursive: true }); + runGit(tempRoot, ['worktree', 'add', '-b', branch, worktreePath]); + fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\nchanged\n', 'utf8'); + + writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({ + repoRoot: tempRoot, + branch, + taskName: 'linked-worktree-visible-task', + agentName: 'codex', + worktreePath, + pid: process.pid, + cliName: 'codex', + state: 'working', + })); + + const { registrations, vscode } = createMockVscode(tempRoot); + vscode.workspace.workspaceFolders = [{ uri: { fsPath: worktreePath } }]; + vscode.workspace.findFiles = async () => []; + const extension = loadExtensionWithMockVscode(vscode); + const context = { subscriptions: [] }; + + extension.activate(context); + await flushAsyncWork(); + + const provider = registrations.providers[0].provider; + const [repoItem] = await provider.getChildren(); + assert.equal(repoItem.label, path.basename(tempRoot)); + assert.equal(repoItem.description, '1 working agent · 0 idle agents · 0 unassigned changes · 0 locked files · 0 conflicts'); + + const workingSection = await getSectionByLabel(provider, repoItem, 'Working now'); + const { worktreeItem, sessionItem } = await getOnlyWorktreeAndSession(provider, workingSection); + assert.equal(worktreeItem, null); + assert.equal(sessionItem.session.repoRoot, tempRoot); + assert.equal(sessionItem.session.worktreePath, worktreePath); + assert.equal(sessionItem.session.branch, branch); + assert.match(sessionItem.description, /^Working: codex · via OpenAI · 1 changed file/); + + for (const subscription of context.subscriptions) { + subscription.dispose?.(); + } +}); + test('active-agents extension decorates sessions and repo changes from the lock registry', async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-lock-decorations-')); initGitRepo(tempRoot); diff --git a/vscode/guardex-active-agents/extension.js b/vscode/guardex-active-agents/extension.js index 10197d4..7d482f2 100644 --- a/vscode/guardex-active-agents/extension.js +++ b/vscode/guardex-active-agents/extension.js @@ -1781,6 +1781,89 @@ async function openSessionDiff(session) { } } +function readGitDirPath(targetPath) { + const normalizedTargetPath = typeof targetPath === 'string' ? targetPath.trim() : ''; + if (!normalizedTargetPath) { + return ''; + } + + const gitPath = path.join(path.resolve(normalizedTargetPath), '.git'); + try { + if (fs.statSync(gitPath).isDirectory()) { + return gitPath; + } + } catch (_error) { + return ''; + } + + try { + const gitPointer = fs.readFileSync(gitPath, 'utf8'); + const match = gitPointer.match(/^gitdir:\s*(.+)$/m); + if (match?.[1]) { + return path.resolve(path.dirname(gitPath), match[1].trim()); + } + } catch (_error) { + return ''; + } + + return ''; +} + +function resolveRepoRootFromGitDir(targetPath) { + const gitDir = readGitDirPath(targetPath); + if (!gitDir) { + return ''; + } + + let commonDir = gitDir; + try { + const commonDirPath = path.join(gitDir, 'commondir'); + if (fs.existsSync(commonDirPath)) { + const rawCommonDir = fs.readFileSync(commonDirPath, 'utf8').trim(); + if (rawCommonDir) { + commonDir = path.resolve(gitDir, rawCommonDir); + } + } + } catch (_error) { + // Fall back to the direct git dir when commondir is unreadable. + } + + return path.basename(commonDir) === '.git' + ? path.resolve(path.dirname(commonDir)) + : ''; +} + +function readGitTopLevel(targetPath) { + try { + return cp.execFileSync('git', ['-C', targetPath, 'rev-parse', '--show-toplevel'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch (_error) { + return ''; + } +} + +function resolveWorkspaceFolderRepoRoot(workspacePath) { + const normalizedWorkspacePath = typeof workspacePath === 'string' ? workspacePath.trim() : ''; + if (!normalizedWorkspacePath) { + return ''; + } + + const absoluteWorkspacePath = path.resolve(normalizedWorkspacePath); + const directRepoRoot = resolveRepoRootFromGitDir(absoluteWorkspacePath); + if (directRepoRoot) { + return directRepoRoot; + } + + const gitTopLevel = readGitTopLevel(absoluteWorkspacePath); + if (!gitTopLevel) { + return absoluteWorkspacePath; + } + + return resolveRepoRootFromGitDir(gitTopLevel) || path.resolve(gitTopLevel); +} + function repoRootFromSessionFile(filePath) { return path.resolve(path.dirname(filePath), '..', '..', '..'); } @@ -2109,7 +2192,7 @@ async function findRepoSessionEntries() { } for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { if (workspaceFolder?.uri?.fsPath) { - addRepoRootCandidate(workspaceFolder.uri.fsPath); + addRepoRootCandidate(resolveWorkspaceFolderRepoRoot(workspaceFolder.uri.fsPath)); } } diff --git a/vscode/guardex-active-agents/package.json b/vscode/guardex-active-agents/package.json index 32b5c28..0f7584d 100644 --- a/vscode/guardex-active-agents/package.json +++ b/vscode/guardex-active-agents/package.json @@ -3,7 +3,7 @@ "displayName": "GitGuardex Active Agents", "description": "Shows live Guardex sandbox sessions and repo changes in a dedicated VS Code Active Agents sidebar.", "publisher": "recodeee", - "version": "0.0.15", + "version": "0.0.16", "license": "MIT", "icon": "icon.png", "engines": {