diff --git a/openspec/changes/agent-codex-show-subproject-active-agents-2026-04-23-16-04/proposal.md b/openspec/changes/agent-codex-show-subproject-active-agents-2026-04-23-16-04/proposal.md new file mode 100644 index 0000000..7ec4b85 --- /dev/null +++ b/openspec/changes/agent-codex-show-subproject-active-agents-2026-04-23-16-04/proposal.md @@ -0,0 +1,17 @@ +# Show nested Active Agents subprojects + +## Why + +The Active Agents view can show the parent workspace, such as `recodee`, while hiding active managed worktrees that belong to a nested repo such as `recodee/gitguardex` when that nested repo has plain managed worktrees but no active-session files or `AGENT.lock` files. Operators need the top-level view to show the full workspace path so it is clear that work is happening under `recodee -> gitguardex`. + +## What Changes + +- Discover nested repo roots from managed worktree `.git` files under `.omx/agent-worktrees` and `.omc/agent-worktrees`. +- Keep workspace roots in the scan even when other session files are found. +- Label nested repo roots relative to their workspace folder, for example `recodee -> gitguardex`. +- Watch managed worktree `.git` files so new plain worktrees refresh the Active Agents view. +- Keep live/template VS Code extension copies and focused tests in sync. + +## Impact + +This is limited to the VS Code Active Agents companion tree. It does not change Guardex branch creation, locking, or finish behavior. diff --git a/openspec/changes/agent-codex-show-subproject-active-agents-2026-04-23-16-04/specs/vscode-active-agents-extension/spec.md b/openspec/changes/agent-codex-show-subproject-active-agents-2026-04-23-16-04/specs/vscode-active-agents-extension/spec.md new file mode 100644 index 0000000..00bf91f --- /dev/null +++ b/openspec/changes/agent-codex-show-subproject-active-agents-2026-04-23-16-04/specs/vscode-active-agents-extension/spec.md @@ -0,0 +1,36 @@ +## ADDED Requirements + +### Requirement: Nested subproject Active Agents discovery + +The VS Code `gitguardex.activeAgents` view MUST discover nested repository roots under the open workspace when those nested repositories have managed agent worktrees under `.omx/agent-worktrees` or `.omc/agent-worktrees`, even when those worktrees only expose a worktree `.git` file and do not expose active-session JSON or `AGENT.lock` telemetry. + +#### Scenario: Top-level view includes a nested repo with plain managed worktrees + +- **GIVEN** a workspace folder such as `recodee` +- **AND** a nested repository such as `recodee/gitguardex` +- **AND** the nested repository has a managed worktree under `.omx/agent-worktrees` +- **WHEN** the Active Agents view scans the workspace +- **THEN** it includes the nested repository in the top-level repo list +- **AND** it reads the nested repository sessions from that nested repository root. + +### Requirement: Workspace-relative nested repo labels + +The VS Code `gitguardex.activeAgents` view MUST label nested repository roots relative to the open workspace folder so operators can see the workspace-to-subproject path at the top level. + +#### Scenario: Nested repo label shows workspace and subproject + +- **GIVEN** the open workspace folder is `recodee` +- **AND** the discovered active repo root is `recodee/gitguardex` +- **WHEN** the Active Agents view renders the repo row +- **THEN** the repo row label is `recodee -> gitguardex`. + +### Requirement: Managed worktree discovery refresh + +The VS Code `gitguardex.activeAgents` view MUST refresh when managed worktree `.git` files are created, changed, or deleted under `.omx/agent-worktrees` or `.omc/agent-worktrees`. + +#### Scenario: New plain managed worktree appears without active-session telemetry + +- **GIVEN** a nested repository has no active-session JSON and no `AGENT.lock` telemetry +- **WHEN** a managed worktree `.git` file appears under `.omx/agent-worktrees` +- **THEN** the Active Agents view schedules a refresh +- **AND** the nested repository becomes visible if it has readable managed worktree sessions. diff --git a/openspec/changes/agent-codex-show-subproject-active-agents-2026-04-23-16-04/tasks.md b/openspec/changes/agent-codex-show-subproject-active-agents-2026-04-23-16-04/tasks.md new file mode 100644 index 0000000..438de1b --- /dev/null +++ b/openspec/changes/agent-codex-show-subproject-active-agents-2026-04-23-16-04/tasks.md @@ -0,0 +1,38 @@ +## 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-subproject-active-agents-2026-04-23-16-04`; branch=`agent/codex/show-subproject-active-agents-2026-04-23-16-04`; scope=`VS Code Active Agents nested subproject discovery, labels, watchers, template parity, and focused regression`; action=`show nested gitguardex-style managed worktrees at top level as workspace -> subproject, verify, then finish via PR merge cleanup`. + +## 1. Specification + +- [x] 1.1 Define nested subproject discovery and labeling requirements. +- [x] 1.2 Keep completion and cleanup evidence requirements explicit. + +## 2. Implementation + +- [x] 2.1 Discover plain managed-worktree repos from `.omx/.omc/agent-worktrees/*/.git`. +- [x] 2.2 Keep workspace roots in the candidate scan while filtering nested managed-worktree copies. +- [x] 2.3 Render nested repo labels as `workspace -> subproject`. +- [x] 2.4 Add a managed-worktree `.git` watcher for refresh. +- [x] 2.5 Mirror extension changes in `templates/vscode/guardex-active-agents/extension.js`. +- [x] 2.6 Bump live/template Active Agents manifests for extension install refresh. + +## 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-subproject-active-agents-2026-04-23-16-04 --type change --strict`. +- [x] 3.3 Run `openspec validate --specs`. +- [x] 3.4 Run `npm test`. + +## 4. Cleanup (mandatory; run before claiming completion) + +- [ ] 4.1 Run `gx branch finish --branch agent/codex/show-subproject-active-agents-2026-04-23-16-04 --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 17b391e..5c573cc 100644 --- a/templates/vscode/guardex-active-agents/extension.js +++ b/templates/vscode/guardex-active-agents/extension.js @@ -17,6 +17,7 @@ const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json'); const ACTIVE_SESSION_FILES_GLOB = '**/.omx/state/active-sessions/*.json'; const AGENT_FILE_LOCKS_GLOB = '**/.omx/state/agent-file-locks.json'; const WORKTREE_AGENT_LOCKS_GLOB = '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock'; +const MANAGED_WORKTREE_GIT_FILES_GLOB = '**/{.omx,.omc}/agent-worktrees/*/.git'; const MANAGED_WORKTREE_RELATIVE_ROOTS = [ path.join('.omx', 'agent-worktrees'), path.join('.omc', 'agent-worktrees'), @@ -24,6 +25,7 @@ const MANAGED_WORKTREE_RELATIVE_ROOTS = [ const AGENT_LOG_FILES_GLOB = '**/.omx/logs/*.log'; const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.omc/agent-worktrees}/**'; const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**'; +const MANAGED_WORKTREE_GIT_SCAN_EXCLUDE_GLOB = '**/node_modules/**'; const SESSION_SCAN_LIMIT = 200; const REFRESH_DEBOUNCE_MS = 250; const RECENTLY_ACTIVE_WINDOW_MS = 10 * 60 * 1000; @@ -735,6 +737,30 @@ function buildRepoTooltip(repoRoot, summary) { ].join('\n'); } +function repoRootDisplayLabel(repoRoot) { + const normalizedRepoRoot = path.resolve(repoRoot); + const matchingWorkspaceRoots = (vscode.workspace.workspaceFolders || []) + .map((folder) => (typeof folder?.uri?.fsPath === 'string' ? path.resolve(folder.uri.fsPath) : '')) + .filter((workspaceRoot) => workspaceRoot && isPathWithin(workspaceRoot, normalizedRepoRoot)) + .sort((left, right) => right.length - left.length); + + const workspaceRoot = matchingWorkspaceRoots[0]; + if (!workspaceRoot) { + return path.basename(normalizedRepoRoot); + } + + const workspaceLabel = path.basename(workspaceRoot); + const relativePath = normalizeRelativePath(path.relative(workspaceRoot, normalizedRepoRoot)); + if (!relativePath) { + return workspaceLabel; + } + + return [ + workspaceLabel, + ...relativePath.split('/').filter(Boolean), + ].join(' -> '); +} + function sessionSnapshotKey(session) { return `${session?.repoRoot || ''}::${session?.branch || ''}`; } @@ -1094,7 +1120,10 @@ class DetailItem extends vscode.TreeItem { class RepoItem extends vscode.TreeItem { constructor(repoRoot, sessions, changes, options = {}) { - super(path.basename(repoRoot), vscode.TreeItemCollapsibleState.Expanded); + const label = typeof options.label === 'string' && options.label.trim() + ? options.label.trim() + : repoRootDisplayLabel(repoRoot); + super(label, vscode.TreeItemCollapsibleState.Expanded); this.repoRoot = repoRoot; this.sessions = sessions; this.changes = changes; @@ -1684,6 +1713,10 @@ function repoRootFromWorktreeLockFile(filePath) { return path.resolve(path.dirname(filePath), '..', '..', '..'); } +function repoRootFromManagedWorktreeGitFile(filePath) { + return path.resolve(path.dirname(filePath), '..', '..', '..'); +} + function repoRootFromLockFile(filePath) { return path.resolve(path.dirname(filePath), '..', '..'); } @@ -1947,7 +1980,7 @@ function localizeChangeForSession(session, change) { } async function findRepoSessionEntries() { - const [sessionFiles, worktreeLockFiles] = await Promise.all([ + const [sessionFiles, worktreeLockFiles, managedWorktreeGitFiles] = await Promise.all([ vscode.workspace.findFiles( ACTIVE_SESSION_FILES_GLOB, SESSION_SCAN_EXCLUDE_GLOB, @@ -1958,22 +1991,49 @@ async function findRepoSessionEntries() { WORKTREE_LOCK_SCAN_EXCLUDE_GLOB, SESSION_SCAN_LIMIT, ), + vscode.workspace.findFiles( + MANAGED_WORKTREE_GIT_FILES_GLOB, + MANAGED_WORKTREE_GIT_SCAN_EXCLUDE_GLOB, + SESSION_SCAN_LIMIT, + ), ]); const repoRoots = new Set(); + const addRepoRootCandidate = (repoRoot) => { + if (typeof repoRoot !== 'string' || !repoRoot.trim()) { + return; + } + + const normalizedRepoRoot = path.resolve(repoRoot); + const isInsideWorkspaceManagedWorktree = (vscode.workspace.workspaceFolders || []) + .map((folder) => (typeof folder?.uri?.fsPath === 'string' ? path.resolve(folder.uri.fsPath) : '')) + .filter(Boolean) + .some((workspaceRoot) => MANAGED_WORKTREE_RELATIVE_ROOTS.some((relativeRoot) => ( + isPathWithin(path.join(workspaceRoot, relativeRoot), normalizedRepoRoot) + ))); + if (!isInsideWorkspaceManagedWorktree) { + repoRoots.add(normalizedRepoRoot); + } + }; + for (const uri of sessionFiles) { - repoRoots.add(repoRootFromSessionFile(uri.fsPath)); + addRepoRootCandidate(repoRootFromSessionFile(uri.fsPath)); } for (const uri of worktreeLockFiles) { if (path.basename(uri.fsPath) !== 'AGENT.lock') { continue; } - repoRoots.add(repoRootFromWorktreeLockFile(uri.fsPath)); + addRepoRootCandidate(repoRootFromWorktreeLockFile(uri.fsPath)); } - - if (repoRoots.size === 0) { - for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { - repoRoots.add(workspaceFolder.uri.fsPath); + for (const uri of managedWorktreeGitFiles) { + if (path.basename(uri.fsPath) !== '.git') { + continue; + } + addRepoRootCandidate(repoRootFromManagedWorktreeGitFile(uri.fsPath)); + } + for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { + if (workspaceFolder?.uri?.fsPath) { + addRepoRootCandidate(workspaceFolder.uri.fsPath); } } @@ -2951,6 +3011,7 @@ function activate(context) { const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB); const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB); const worktreeLockWatcher = vscode.workspace.createFileSystemWatcher(WORKTREE_AGENT_LOCKS_GLOB); + const managedWorktreeGitWatcher = vscode.workspace.createFileSystemWatcher(MANAGED_WORKTREE_GIT_FILES_GLOB); const logWatcher = vscode.workspace.createFileSystemWatcher(AGENT_LOG_FILES_GLOB); const updateCommitInput = (session) => { sourceControl.inputBox.enabled = true; @@ -3076,6 +3137,7 @@ function activate(context) { activeSessionsWatcher, lockWatcher, worktreeLockWatcher, + managedWorktreeGitWatcher, logWatcher, { dispose: () => clearInterval(interval) }, ); @@ -3084,6 +3146,7 @@ function activate(context) { ...bindRefreshWatcher(activeSessionsWatcher, scheduleRefresh), ...bindRefreshWatcher(lockWatcher, refreshLockRegistry), ...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh), + ...bindRefreshWatcher(managedWorktreeGitWatcher, scheduleRefresh), ...bindRefreshWatcher(logWatcher, scheduleRefresh), ); void ensureManagedRepoScanIgnores(); diff --git a/test/vscode-active-agents-session-state.test.js b/test/vscode-active-agents-session-state.test.js index dda64c6..c529594 100644 --- a/test/vscode-active-agents-session-state.test.js +++ b/test/vscode-active-agents-session-state.test.js @@ -1387,13 +1387,14 @@ test('active-agents extension registers tree and decoration providers', async () assert.equal(registrations.providers.length, 1); assert.equal(registrations.providers[0].viewId, 'gitguardex.activeAgents'); assert.equal(registrations.decorationProviders.length, 1); - assert.equal(registrations.fileWatchers.length, 4); + assert.equal(registrations.fileWatchers.length, 5); assert.deepEqual( registrations.fileWatchers.map((watcher) => watcher.pattern), [ '**/.omx/state/active-sessions/*.json', '**/.omx/state/agent-file-locks.json', '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock', + '**/{.omx,.omc}/agent-worktrees/*/.git', '**/.omx/logs/*.log', ], ); @@ -1669,6 +1670,69 @@ test('active-agents extension groups live sessions under a repo node', async () } }); +test('active-agents extension discovers nested managed-worktree subprojects under workspace roots', async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-subprojects-')); + const nestedRepoRoot = path.join(tempRoot, 'gitguardex'); + initGitRepo(nestedRepoRoot); + fs.writeFileSync(path.join(nestedRepoRoot, 'tracked.txt'), 'base\n', 'utf8'); + runGit(nestedRepoRoot, ['add', 'tracked.txt']); + runGit(nestedRepoRoot, ['commit', '-m', 'baseline']); + + const worktreePath = path.join( + nestedRepoRoot, + '.omx', + 'agent-worktrees', + 'agent__codex__nested-visible-task', + ); + fs.mkdirSync(path.dirname(worktreePath), { recursive: true }); + runGit(nestedRepoRoot, [ + 'worktree', + 'add', + '-b', + 'agent/codex/nested-visible-task', + worktreePath, + ]); + fs.writeFileSync(path.join(worktreePath, 'tracked.txt'), 'base\nchanged\n', 'utf8'); + + const managedWorktreeGitFile = path.join(worktreePath, '.git'); + assert.equal(fs.statSync(managedWorktreeGitFile).isFile(), true); + + const { registrations, vscode } = createMockVscode(tempRoot); + vscode.workspace.findFiles = async (pattern) => { + if (pattern === '**/{.omx,.omc}/agent-worktrees/*/.git') { + return [{ fsPath: managedWorktreeGitFile }]; + } + return []; + }; + 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)} -> gitguardex`); + assert.equal(repoItem.repoRoot, nestedRepoRoot); + 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, nestedRepoRoot); + assert.equal(sessionItem.session.worktreePath, worktreePath); + assert.equal(sessionItem.session.branch, 'agent/codex/nested-visible-task'); + assert.match(sessionItem.description, /^Working: codex · via OpenAI · 1 changed file/); + assert.deepEqual(registrations.treeViews[0].badge, { + value: 1, + tooltip: repoItem.description, + }); + + for (const subscription of context.subscriptions) { + subscription.dispose?.(); + } +}); + test('active-agents extension shows provider and snapshot identity badges', async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-provider-badges-')); const codexWorktreePath = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-provider-codex-')); @@ -2686,6 +2750,7 @@ test('active-agents extension watches active sessions, lock files, logs, and ses '**/.omx/state/active-sessions/*.json', '**/.omx/state/agent-file-locks.json', '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock', + '**/{.omx,.omc}/agent-worktrees/*/.git', '**/.omx/logs/*.log', path.join(worktreePath, '.git', 'index'), ], @@ -2697,7 +2762,7 @@ test('active-agents extension watches active sessions, lock files, logs, and ses await new Promise((resolve) => setTimeout(resolve, 350)); await flushAsyncWork(); - assert.equal(registrations.fileWatchers[4].disposed, true); + assert.equal(registrations.fileWatchers[5].disposed, true); for (const subscription of context.subscriptions) { subscription.dispose?.(); @@ -2719,7 +2784,8 @@ test('active-agents extension debounces refresh events with a trailing 250ms tim registrations.fileWatchers[0].fireChange({ fsPath: path.join(tempRoot, '.omx', 'state', 'active-sessions', 'a.json') }); registrations.fileWatchers[1].fireChange({ fsPath: path.join(tempRoot, '.omx', 'state', 'agent-file-locks.json') }); registrations.fileWatchers[2].fireChange({ fsPath: path.join(tempRoot, '.omx', 'agent-worktrees', 'agent__codex__a', 'AGENT.lock') }); - registrations.fileWatchers[3].fireChange({ fsPath: path.join(tempRoot, '.omx', 'logs', 'agent-agent__codex__a.log') }); + registrations.fileWatchers[3].fireChange({ fsPath: path.join(tempRoot, '.omx', 'agent-worktrees', 'agent__codex__a', '.git') }); + registrations.fileWatchers[4].fireChange({ fsPath: path.join(tempRoot, '.omx', 'logs', 'agent-agent__codex__a.log') }); assert.equal(provider.onDidChangeTreeDataEmitter.fireCount, 0); await new Promise((resolve) => setTimeout(resolve, 300)); diff --git a/vscode/guardex-active-agents/extension.js b/vscode/guardex-active-agents/extension.js index 17b391e..5c573cc 100644 --- a/vscode/guardex-active-agents/extension.js +++ b/vscode/guardex-active-agents/extension.js @@ -17,6 +17,7 @@ const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json'); const ACTIVE_SESSION_FILES_GLOB = '**/.omx/state/active-sessions/*.json'; const AGENT_FILE_LOCKS_GLOB = '**/.omx/state/agent-file-locks.json'; const WORKTREE_AGENT_LOCKS_GLOB = '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock'; +const MANAGED_WORKTREE_GIT_FILES_GLOB = '**/{.omx,.omc}/agent-worktrees/*/.git'; const MANAGED_WORKTREE_RELATIVE_ROOTS = [ path.join('.omx', 'agent-worktrees'), path.join('.omc', 'agent-worktrees'), @@ -24,6 +25,7 @@ const MANAGED_WORKTREE_RELATIVE_ROOTS = [ const AGENT_LOG_FILES_GLOB = '**/.omx/logs/*.log'; const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.omc/agent-worktrees}/**'; const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**'; +const MANAGED_WORKTREE_GIT_SCAN_EXCLUDE_GLOB = '**/node_modules/**'; const SESSION_SCAN_LIMIT = 200; const REFRESH_DEBOUNCE_MS = 250; const RECENTLY_ACTIVE_WINDOW_MS = 10 * 60 * 1000; @@ -735,6 +737,30 @@ function buildRepoTooltip(repoRoot, summary) { ].join('\n'); } +function repoRootDisplayLabel(repoRoot) { + const normalizedRepoRoot = path.resolve(repoRoot); + const matchingWorkspaceRoots = (vscode.workspace.workspaceFolders || []) + .map((folder) => (typeof folder?.uri?.fsPath === 'string' ? path.resolve(folder.uri.fsPath) : '')) + .filter((workspaceRoot) => workspaceRoot && isPathWithin(workspaceRoot, normalizedRepoRoot)) + .sort((left, right) => right.length - left.length); + + const workspaceRoot = matchingWorkspaceRoots[0]; + if (!workspaceRoot) { + return path.basename(normalizedRepoRoot); + } + + const workspaceLabel = path.basename(workspaceRoot); + const relativePath = normalizeRelativePath(path.relative(workspaceRoot, normalizedRepoRoot)); + if (!relativePath) { + return workspaceLabel; + } + + return [ + workspaceLabel, + ...relativePath.split('/').filter(Boolean), + ].join(' -> '); +} + function sessionSnapshotKey(session) { return `${session?.repoRoot || ''}::${session?.branch || ''}`; } @@ -1094,7 +1120,10 @@ class DetailItem extends vscode.TreeItem { class RepoItem extends vscode.TreeItem { constructor(repoRoot, sessions, changes, options = {}) { - super(path.basename(repoRoot), vscode.TreeItemCollapsibleState.Expanded); + const label = typeof options.label === 'string' && options.label.trim() + ? options.label.trim() + : repoRootDisplayLabel(repoRoot); + super(label, vscode.TreeItemCollapsibleState.Expanded); this.repoRoot = repoRoot; this.sessions = sessions; this.changes = changes; @@ -1684,6 +1713,10 @@ function repoRootFromWorktreeLockFile(filePath) { return path.resolve(path.dirname(filePath), '..', '..', '..'); } +function repoRootFromManagedWorktreeGitFile(filePath) { + return path.resolve(path.dirname(filePath), '..', '..', '..'); +} + function repoRootFromLockFile(filePath) { return path.resolve(path.dirname(filePath), '..', '..'); } @@ -1947,7 +1980,7 @@ function localizeChangeForSession(session, change) { } async function findRepoSessionEntries() { - const [sessionFiles, worktreeLockFiles] = await Promise.all([ + const [sessionFiles, worktreeLockFiles, managedWorktreeGitFiles] = await Promise.all([ vscode.workspace.findFiles( ACTIVE_SESSION_FILES_GLOB, SESSION_SCAN_EXCLUDE_GLOB, @@ -1958,22 +1991,49 @@ async function findRepoSessionEntries() { WORKTREE_LOCK_SCAN_EXCLUDE_GLOB, SESSION_SCAN_LIMIT, ), + vscode.workspace.findFiles( + MANAGED_WORKTREE_GIT_FILES_GLOB, + MANAGED_WORKTREE_GIT_SCAN_EXCLUDE_GLOB, + SESSION_SCAN_LIMIT, + ), ]); const repoRoots = new Set(); + const addRepoRootCandidate = (repoRoot) => { + if (typeof repoRoot !== 'string' || !repoRoot.trim()) { + return; + } + + const normalizedRepoRoot = path.resolve(repoRoot); + const isInsideWorkspaceManagedWorktree = (vscode.workspace.workspaceFolders || []) + .map((folder) => (typeof folder?.uri?.fsPath === 'string' ? path.resolve(folder.uri.fsPath) : '')) + .filter(Boolean) + .some((workspaceRoot) => MANAGED_WORKTREE_RELATIVE_ROOTS.some((relativeRoot) => ( + isPathWithin(path.join(workspaceRoot, relativeRoot), normalizedRepoRoot) + ))); + if (!isInsideWorkspaceManagedWorktree) { + repoRoots.add(normalizedRepoRoot); + } + }; + for (const uri of sessionFiles) { - repoRoots.add(repoRootFromSessionFile(uri.fsPath)); + addRepoRootCandidate(repoRootFromSessionFile(uri.fsPath)); } for (const uri of worktreeLockFiles) { if (path.basename(uri.fsPath) !== 'AGENT.lock') { continue; } - repoRoots.add(repoRootFromWorktreeLockFile(uri.fsPath)); + addRepoRootCandidate(repoRootFromWorktreeLockFile(uri.fsPath)); } - - if (repoRoots.size === 0) { - for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { - repoRoots.add(workspaceFolder.uri.fsPath); + for (const uri of managedWorktreeGitFiles) { + if (path.basename(uri.fsPath) !== '.git') { + continue; + } + addRepoRootCandidate(repoRootFromManagedWorktreeGitFile(uri.fsPath)); + } + for (const workspaceFolder of vscode.workspace.workspaceFolders || []) { + if (workspaceFolder?.uri?.fsPath) { + addRepoRootCandidate(workspaceFolder.uri.fsPath); } } @@ -2951,6 +3011,7 @@ function activate(context) { const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB); const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB); const worktreeLockWatcher = vscode.workspace.createFileSystemWatcher(WORKTREE_AGENT_LOCKS_GLOB); + const managedWorktreeGitWatcher = vscode.workspace.createFileSystemWatcher(MANAGED_WORKTREE_GIT_FILES_GLOB); const logWatcher = vscode.workspace.createFileSystemWatcher(AGENT_LOG_FILES_GLOB); const updateCommitInput = (session) => { sourceControl.inputBox.enabled = true; @@ -3076,6 +3137,7 @@ function activate(context) { activeSessionsWatcher, lockWatcher, worktreeLockWatcher, + managedWorktreeGitWatcher, logWatcher, { dispose: () => clearInterval(interval) }, ); @@ -3084,6 +3146,7 @@ function activate(context) { ...bindRefreshWatcher(activeSessionsWatcher, scheduleRefresh), ...bindRefreshWatcher(lockWatcher, refreshLockRegistry), ...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh), + ...bindRefreshWatcher(managedWorktreeGitWatcher, scheduleRefresh), ...bindRefreshWatcher(logWatcher, scheduleRefresh), ); void ensureManagedRepoScanIgnores();