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,2 @@
schema: spec-driven
created: 2026-04-23
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## Why

- Operators currently have to reload the whole VS Code window when they want GitGuardex Active Agents to restart.
- The extension details page is the place users reach for lifecycle actions, but GitGuardex Active Agents does not currently expose any restart affordance there.

## What Changes

- Add a `Restart Active Agents` command that restarts the extension host instead of reloading the whole window.
- Surface that command on the closest supported VS Code surfaces:
- the Extensions view gear/context menu for `recodeee.gitguardex-active-agents`
- the Active Agents view title
- Keep live/template extension files, manifest versions, and focused regression coverage in sync.

## Impact

- Scope is limited to the Active Agents extension manifest, command registration, and focused VS Code extension tests.
- Session discovery, locking, finish flow, and telemetry payloads stay unchanged.
- VS Code does not expose a custom button slot next to built-in `Disable` / `Uninstall`, so the restart affordance must live in the extension gear/context menu instead of the top action row.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## ADDED Requirements

### Requirement: Active Agents exposes a restart action from extension management surfaces
The VS Code `recodeee.gitguardex-active-agents` extension MUST expose a `Restart Active Agents` action anywhere VS Code allows contributed extension-management commands, so operators can restart the extension host without reloading the full window.

#### Scenario: Restart command appears on the extension details gear menu
- **GIVEN** GitGuardex Active Agents is installed
- **WHEN** the operator opens the extension details page or extension context menu
- **THEN** the extension contributes a `Restart Active Agents` action for `recodeee.gitguardex-active-agents`
- **AND** the action does not appear for unrelated extensions.

#### Scenario: Restart command restarts the extension host
- **GIVEN** the operator invokes `Restart Active Agents`
- **WHEN** the command runs
- **THEN** it executes `workbench.action.restartExtensionHost`
- **AND** it does not require `workbench.action.reloadWindow`.

### Requirement: Active Agents exposes restart from its own sidebar
The VS Code `gitguardex.activeAgents` view MUST expose the same `Restart Active Agents` action from the view title so operators can restart the extension without leaving the sidebar.

#### Scenario: Restart command appears in the Active Agents view title
- **GIVEN** the Active Agents view is visible
- **WHEN** the view title actions render
- **THEN** `Restart Active Agents` is available alongside the other view-level actions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## 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-vscode-active-agents-subrepo-path-labels-2026-04-23-16-46`; branch=`agent/codex/vscode-active-agents-subrepo-path-labels-2026-04-23-16-46`; scope=`VS Code Active Agents top-level repo labels for nested git repos and single-subproject session lanes, template parity, manifest bump, focused regression`; action=`show slash-delimited workspace/subrepo labels like recodee/gitguardex, verify, then finish via PR merge cleanup`.
- Copy prompt: Continue `agent-codex-vscode-active-agents-subrepo-path-labels-2026-04-23-16-46` on branch `agent/codex/vscode-active-agents-subrepo-path-labels-2026-04-23-16-46`. Work inside the existing sandbox, review `openspec/changes/agent-codex-vscode-active-agents-subrepo-path-labels-2026-04-23-16-46/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/codex/vscode-active-agents-subrepo-path-labels-2026-04-23-16-46 --base main --via-pr --wait-for-merge --cleanup`.

## 1. Specification

- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-vscode-active-agents-subrepo-path-labels-2026-04-23-16-46`.
- [x] 1.2 Define normative requirements in `specs/vscode-active-agents-extension/spec.md`.

## 2. Implementation

- [x] 2.1 Render top-level repo labels as slash-delimited workspace-relative paths.
- [x] 2.2 Promote a shared nested `projectPath` onto the repo row when all visible sessions in that repo target the same subproject.
- [x] 2.3 Mirror extension changes in `templates/vscode/guardex-active-agents/extension.js` and bump live/template manifests.
- [x] 2.4 Add/update focused regression coverage.

## 3. Verification

- [x] 3.1 Run `node --test test/vscode-active-agents-session-state.test.js`.
- [x] 3.2 Run `openspec validate agent-codex-vscode-active-agents-subrepo-path-labels-2026-04-23-16-46 --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 the cleanup pipeline: `gx branch finish --branch agent/codex/vscode-active-agents-subrepo-path-labels-2026-04-23-16-46 --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).
35 changes: 34 additions & 1 deletion templates/vscode/guardex-active-agents/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agen
const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js');
const RELOAD_WINDOW_ACTION = 'Reload Window';
const UPDATE_LATER_ACTION = 'Later';
const ACTIVE_AGENTS_EXTENSION_ID = 'recodeee.gitguardex-active-agents';
const RESTART_EXTENSION_HOST_COMMAND = 'workbench.action.restartExtensionHost';
const REFRESH_POLL_INTERVAL_MS = 30_000;
const INSPECT_PANEL_VIEW_TYPE = 'gitguardex.activeAgents.inspect';
const GIT_CONFIGURATION_SECTION = 'git';
Expand Down Expand Up @@ -830,7 +832,7 @@ function repoRootDisplayLabel(repoRoot) {
return [
workspaceLabel,
...relativePath.split('/').filter(Boolean),
].join(' -> ');
].join('/');
}

function sessionSnapshotKey(session) {
Expand Down Expand Up @@ -1501,6 +1503,28 @@ function worktreeProjectRelativePath(sessions) {
return projectPaths.length === 1 ? projectPaths[0] : '';
}

function repoEntryDisplayLabel(repoRoot, sessions) {
const repoLabel = repoRootDisplayLabel(repoRoot);
const projectPaths = uniqueStringList((sessions || [])
.map((session) => resolveSessionProjectRelativePath(session))
.filter(Boolean));
if (projectPaths.length !== 1) {
return repoLabel;
}

const [projectRelativePath] = projectPaths;
const hasRootScopedSession = (sessions || []).some(
(session) => !resolveSessionProjectRelativePath(session),
);
if (!projectRelativePath || hasRootScopedSession) {
return repoLabel;
}
if (repoLabel.endsWith(`/${projectRelativePath}`)) {
return repoLabel;
}
return `${repoLabel}/${projectRelativePath}`;
}

function buildProjectScopedDescription(entries) {
const sessions = (entries || []).flatMap((entry) => Array.isArray(entry?.sessions) ? entry.sessions : []);
if (sessions.length === 0) {
Expand Down Expand Up @@ -1663,6 +1687,13 @@ function syncSession(session) {
runSessionTerminalCommand(session, 'Sync', 'sync', 'gx sync');
}

async function restartActiveAgents(extensionId) {
if (extensionId && extensionId !== ACTIVE_AGENTS_EXTENSION_ID) {
return;
}
await vscode.commands.executeCommand(RESTART_EXTENSION_HOST_COMMAND);
}

function execFileAsync(command, args, options = {}) {
return new Promise((resolve, reject) => {
cp.execFile(command, args, options, (error, stdout = '', stderr = '') => {
Expand Down Expand Up @@ -2966,6 +2997,7 @@ class ActiveAgentsProvider {
}

return repoEntries.map((entry) => new RepoItem(entry.repoRoot, entry.sessions, entry.changes, {
label: repoEntryDisplayLabel(entry.repoRoot, entry.sessions),
overview: entry.overview,
unassignedChanges: entry.unassignedChanges,
lockEntries: entry.lockEntries,
Expand Down Expand Up @@ -3258,6 +3290,7 @@ function activate(context) {
vscode.window.registerFileDecorationProvider(decorationProvider),
vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)),
vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh),
vscode.commands.registerCommand('gitguardex.activeAgents.restart', restartActiveAgents),
vscode.commands.registerCommand('gitguardex.activeAgents.focus', async () => {
await vscode.commands.executeCommand('workbench.view.extension.gitguardex.activeAgentsContainer');
}),
Expand Down
17 changes: 17 additions & 0 deletions templates/vscode/guardex-active-agents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
"command": "gitguardex.activeAgents.refresh",
"title": "Refresh Active Agents"
},
{
"command": "gitguardex.activeAgents.restart",
"title": "Restart Active Agents",
"icon": "$(debug-restart)"
},
{
"command": "gitguardex.activeAgents.commitSelectedSession",
"title": "Commit Selected Session",
Expand Down Expand Up @@ -99,12 +104,24 @@
"when": "view == gitguardex.activeAgents && guardex.hasAgents",
"group": "navigation@1"
},
{
"command": "gitguardex.activeAgents.restart",
"when": "view == gitguardex.activeAgents",
"group": "navigation@8"
},
{
"command": "gitguardex.activeAgents.refresh",
"when": "view == gitguardex.activeAgents",
"group": "navigation@9"
}
],
"extension/context": [
{
"command": "gitguardex.activeAgents.restart",
"when": "extension == recodeee.gitguardex-active-agents && extensionStatus == installed",
"group": "2_configure@2"
}
],
"view/item/context": [
{
"command": "gitguardex.activeAgents.openWorktree",
Expand Down
63 changes: 62 additions & 1 deletion test/vscode-active-agents-session-state.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,43 @@ test('active-agents manifest does not contribute a file icon theme', () => {
assert.equal(manifest.contributes.iconThemes, undefined);
});

test('active-agents manifest contributes restart actions for extension management and view title', () => {
const manifest = readExtensionManifest();
const templateManifest = readExtensionManifest(templateExtensionManifestPath);

const restartCommand = manifest.contributes.commands.find(
(entry) => entry.command === 'gitguardex.activeAgents.restart',
);
assert.deepEqual(restartCommand, {
command: 'gitguardex.activeAgents.restart',
title: 'Restart Active Agents',
icon: '$(debug-restart)',
});

const restartViewTitleAction = manifest.contributes.menus['view/title'].find(
(entry) => entry.command === 'gitguardex.activeAgents.restart',
);
assert.deepEqual(restartViewTitleAction, {
command: 'gitguardex.activeAgents.restart',
when: 'view == gitguardex.activeAgents',
group: 'navigation@8',
});

const restartExtensionAction = manifest.contributes.menus['extension/context'].find(
(entry) => entry.command === 'gitguardex.activeAgents.restart',
);
assert.deepEqual(restartExtensionAction, {
command: 'gitguardex.activeAgents.restart',
when: 'extension == recodeee.gitguardex-active-agents && extensionStatus == installed',
group: '2_configure@2',
});

assert.deepEqual(
manifest.contributes.menus['extension/context'],
templateManifest.contributes.menus['extension/context'],
);
});

test('active-agents extension auto-installs a newer workspace build and offers reload', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-autoupdate-'));
const repoManifest = {
Expand Down Expand Up @@ -1412,6 +1449,7 @@ test('active-agents extension registers tree and decoration providers', async ()
const provider = registrations.providers[0].provider;
assert.equal(typeof provider.getTreeItem, 'function');
assert.equal(typeof registrations.commands.get('gitguardex.activeAgents.startAgent'), 'function');
assert.equal(typeof registrations.commands.get('gitguardex.activeAgents.restart'), 'function');
assert.equal(typeof registrations.commands.get('gitguardex.activeAgents.inspect'), 'function');

const rootItems = await provider.getChildren();
Expand All @@ -1425,6 +1463,28 @@ test('active-agents extension registers tree and decoration providers', async ()
}
});

test('active-agents restart command restarts the extension host for this extension only', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-restart-command-'));
const { registrations, vscode } = createMockVscode(tempRoot);
const extension = loadExtensionWithMockVscode(vscode);
const context = { subscriptions: [] };

extension.activate(context);
await flushAsyncWork();

await registrations.commands.get('gitguardex.activeAgents.restart')('recodeee.gitguardex-active-agents');
await registrations.commands.get('gitguardex.activeAgents.restart')('someone.else');

const restartCalls = registrations.executedCommands.filter(
(entry) => entry.command === 'workbench.action.restartExtensionHost',
);
assert.equal(restartCalls.length, 1);

for (const subscription of context.subscriptions) {
subscription.dispose?.();
}
});

test('active-agents focus command opens the dedicated sidebar container', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-focus-view-'));
const { registrations, vscode } = createMockVscode(tempRoot);
Expand Down Expand Up @@ -1721,7 +1781,7 @@ test('active-agents extension discovers nested managed-worktree subprojects unde

const provider = registrations.providers[0].provider;
const [repoItem] = await provider.getChildren();
assert.equal(repoItem.label, `${path.basename(tempRoot)} -> gitguardex`);
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');

Expand Down Expand Up @@ -2148,6 +2208,7 @@ test('active-agents extension surfaces live managed worktrees from AGENT.lock fa

const provider = registrations.providers[0].provider;
const [repoItem] = await provider.getChildren();
assert.equal(repoItem.label, `${path.basename(tempRoot)}/gitguardex`);
assert.equal(repoItem.description, '1 working agent · 0 idle agents · 0 unassigned changes · 0 locked files · 0 conflicts');

assert.deepEqual((await provider.getChildren(repoItem)).map((item) => item.label), [
Expand Down
35 changes: 34 additions & 1 deletion vscode/guardex-active-agents/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agen
const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js');
const RELOAD_WINDOW_ACTION = 'Reload Window';
const UPDATE_LATER_ACTION = 'Later';
const ACTIVE_AGENTS_EXTENSION_ID = 'recodeee.gitguardex-active-agents';
const RESTART_EXTENSION_HOST_COMMAND = 'workbench.action.restartExtensionHost';
const REFRESH_POLL_INTERVAL_MS = 30_000;
const INSPECT_PANEL_VIEW_TYPE = 'gitguardex.activeAgents.inspect';
const GIT_CONFIGURATION_SECTION = 'git';
Expand Down Expand Up @@ -830,7 +832,7 @@ function repoRootDisplayLabel(repoRoot) {
return [
workspaceLabel,
...relativePath.split('/').filter(Boolean),
].join(' -> ');
].join('/');
}

function sessionSnapshotKey(session) {
Expand Down Expand Up @@ -1501,6 +1503,28 @@ function worktreeProjectRelativePath(sessions) {
return projectPaths.length === 1 ? projectPaths[0] : '';
}

function repoEntryDisplayLabel(repoRoot, sessions) {
const repoLabel = repoRootDisplayLabel(repoRoot);
const projectPaths = uniqueStringList((sessions || [])
.map((session) => resolveSessionProjectRelativePath(session))
.filter(Boolean));
if (projectPaths.length !== 1) {
return repoLabel;
}

const [projectRelativePath] = projectPaths;
const hasRootScopedSession = (sessions || []).some(
(session) => !resolveSessionProjectRelativePath(session),
);
if (!projectRelativePath || hasRootScopedSession) {
return repoLabel;
}
if (repoLabel.endsWith(`/${projectRelativePath}`)) {
return repoLabel;
}
return `${repoLabel}/${projectRelativePath}`;
}

function buildProjectScopedDescription(entries) {
const sessions = (entries || []).flatMap((entry) => Array.isArray(entry?.sessions) ? entry.sessions : []);
if (sessions.length === 0) {
Expand Down Expand Up @@ -1663,6 +1687,13 @@ function syncSession(session) {
runSessionTerminalCommand(session, 'Sync', 'sync', 'gx sync');
}

async function restartActiveAgents(extensionId) {
if (extensionId && extensionId !== ACTIVE_AGENTS_EXTENSION_ID) {
return;
}
await vscode.commands.executeCommand(RESTART_EXTENSION_HOST_COMMAND);
}

function execFileAsync(command, args, options = {}) {
return new Promise((resolve, reject) => {
cp.execFile(command, args, options, (error, stdout = '', stderr = '') => {
Expand Down Expand Up @@ -2966,6 +2997,7 @@ class ActiveAgentsProvider {
}

return repoEntries.map((entry) => new RepoItem(entry.repoRoot, entry.sessions, entry.changes, {
label: repoEntryDisplayLabel(entry.repoRoot, entry.sessions),
overview: entry.overview,
unassignedChanges: entry.unassignedChanges,
lockEntries: entry.lockEntries,
Expand Down Expand Up @@ -3258,6 +3290,7 @@ function activate(context) {
vscode.window.registerFileDecorationProvider(decorationProvider),
vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)),
vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh),
vscode.commands.registerCommand('gitguardex.activeAgents.restart', restartActiveAgents),
vscode.commands.registerCommand('gitguardex.activeAgents.focus', async () => {
await vscode.commands.executeCommand('workbench.view.extension.gitguardex.activeAgentsContainer');
}),
Expand Down
Loading