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,15 @@
## Why

The VS Code Active Agents tree already exposes session-scoped inline actions, but the current mix still forces the user back into raw repo surfaces for the most common runtime operation: jump straight to the terminal where the agent is running. The inline `Open Diff` action is lower-value in this operator loop, and the current `Stop` action runs a background stop command instead of signaling the live terminal first.

## What Changes

- Replace the session-row `Open Diff` inline action with a `Show Terminal` action that reveals the matching integrated terminal for the selected session when one is available.
- Fallback to opening a worktree-scoped terminal when no live integrated terminal can be matched to the session yet.
- Update the `Stop` action so it sends `Ctrl+C` to the matching session terminal first and only falls back to `gx agents stop --pid` when no live terminal can be found.
- Refresh the focused Active Agents tests and extension manifests for the new terminal-first operator flow.

## Impact

- Affected surfaces: `vscode/guardex-active-agents/*`, `templates/vscode/guardex-active-agents/*`, `test/vscode-active-agents-session-state.test.js`, and this change workspace.
- No Active Agents session-schema changes are required; the extension can match live terminals from the existing session `pid`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## ADDED Requirements

### Requirement: Active Agents exposes terminal-first inline session controls
The VS Code Active Agents companion SHALL prioritize jumping into the live session terminal over opening a per-file diff from the session row.

#### Scenario: Session row offers terminal access instead of diff access
- **WHEN** the extension contributes inline actions for a `gitguardex.session` row
- **THEN** it contributes a `Show Terminal` action for that row
- **AND** it does NOT contribute the old `Open Diff` inline action for that row.

### Requirement: Show Terminal focuses the live session terminal when possible
The VS Code Active Agents companion SHALL reveal the live integrated terminal that owns the selected session whenever the session metadata can be matched to a VS Code terminal process.

#### Scenario: Session `pid` matches an open terminal
- **GIVEN** a session record has a positive integer `pid`
- **AND** VS Code already has an integrated terminal whose `processId` resolves to that same pid
- **WHEN** the operator triggers `Show Terminal`
- **THEN** the extension reveals that existing terminal with focus
- **AND** it does NOT open a replacement terminal for the session.

#### Scenario: No live terminal match exists yet
- **WHEN** the operator triggers `Show Terminal` for a session without a matching live terminal
- **THEN** the extension opens a new integrated terminal rooted at the session worktree
- **AND** it focuses that terminal so the operator lands in the task sandbox immediately.

### Requirement: Stop signals the terminal before falling back to the CLI stopper
The VS Code Active Agents companion SHALL stop live sessions through the matched terminal first so the operator sees and controls the running task directly.

#### Scenario: Stop uses terminal interrupt when a live terminal is known
- **GIVEN** the selected session matches an open integrated terminal by `pid`
- **WHEN** the operator confirms `Stop`
- **THEN** the extension reveals that terminal
- **AND** it sends `Ctrl+C` to that terminal instead of spawning a separate `gx agents stop --pid` process.

#### Scenario: Stop falls back when no terminal can be matched
- **WHEN** the operator confirms `Stop` for a session without a matching live terminal
- **THEN** the extension falls back to `gx agents stop --pid <pid>`
- **AND** it preserves the existing repo-targeted stop behavior for that fallback path.
Original file line number Diff line number Diff line change
@@ -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: 2026-04-23 15:11Z codex owns `vscode/guardex-active-agents/*`, `templates/vscode/guardex-active-agents/*`, `test/vscode-active-agents-session-state.test.js`, and this change workspace to replace the low-value `Open Diff` inline action with terminal-first runtime controls.

## 1. Specification

- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-active-agents-terminal-controls-2026-04-23-17-11`.
- [x] 1.2 Define normative requirements in `specs/vscode-active-agents-extension/spec.md`.

## 2. Implementation

- [x] 2.1 Replace the session-row `Open Diff` inline action with `Show Terminal` in the live/template Active Agents manifests.
- [x] 2.2 Reveal the matching integrated terminal for a session when the stored session `pid` matches a VS Code terminal `processId`, and open a worktree terminal when no live match exists.
- [x] 2.3 Update `Stop` to send `Ctrl+C` to the matched session terminal first and keep `gx agents stop --pid` as the no-terminal fallback.
- [x] 2.4 Refresh focused tests plus mock terminal plumbing for the new terminal-first behavior.

## 3. Verification

- [x] 3.1 Run `node --test test/vscode-active-agents-session-state.test.js`.
- [x] 3.2 Run `openspec validate agent-codex-active-agents-terminal-controls-2026-04-23-17-11 --type change --strict`.
- [x] 3.3 Run `openspec validate --specs`.

Verification notes:
- `node --test test/vscode-active-agents-session-state.test.js` passed `52/52`.
- `openspec validate agent-codex-active-agents-terminal-controls-2026-04-23-17-11 --type change --strict` returned `Change 'agent-codex-active-agents-terminal-controls-2026-04-23-17-11' is valid`.
- `openspec validate --specs` returned `No items found to validate.` in this checkout.

## 4. Cleanup

- [ ] 4.1 Run `gx branch finish --branch "agent/codex/active-agents-terminal-controls-2026-04-23-17-11" --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).
167 changes: 100 additions & 67 deletions templates/vscode/guardex-active-agents/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -1670,6 +1670,82 @@ function runSessionTerminalCommand(session, actionLabel, iconId, commandText) {
terminal.sendText(commandText, true);
}

function sessionTerminalLabel(session) {
return `GitGuardex Terminal: ${sessionDisplayLabel(session)}`;
}

function listWindowTerminals() {
return Array.isArray(vscode.window.terminals) ? vscode.window.terminals : [];
}

function focusTerminal(terminal) {
terminal?.show?.(false);
}

async function terminalProcessId(terminal) {
if (!terminal?.processId) {
return null;
}

try {
const pid = await terminal.processId;
return Number.isInteger(pid) && pid > 0 ? pid : null;
} catch (_error) {
return null;
}
}

function findFallbackSessionTerminal(session) {
const label = sessionTerminalLabel(session);
return listWindowTerminals().find((terminal) => terminal?.name === label) || null;
}

async function findSessionTerminal(session) {
const pid = Number(session?.pid);
if (!Number.isInteger(pid) || pid <= 0) {
return null;
}

for (const terminal of listWindowTerminals()) {
if (await terminalProcessId(terminal) === pid) {
return terminal;
}
}

return null;
}

function openFallbackSessionTerminal(session, worktreePath) {
const existingTerminal = findFallbackSessionTerminal(session);
if (existingTerminal) {
focusTerminal(existingTerminal);
return existingTerminal;
}

const terminal = vscode.window.createTerminal({
name: sessionTerminalLabel(session),
cwd: worktreePath,
iconPath: new vscode.ThemeIcon('terminal'),
});
focusTerminal(terminal);
return terminal;
}

async function showSessionTerminal(session) {
const worktreePath = ensureSessionWorktree(session, 'show terminal');
if (!worktreePath) {
return;
}

const terminal = await findSessionTerminal(session);
if (terminal) {
focusTerminal(terminal);
return;
}

openFallbackSessionTerminal(session, worktreePath);
}

function finishSession(session) {
if (!session?.branch) {
showSessionMessage('Cannot finish session: missing branch name.');
Expand Down Expand Up @@ -1708,6 +1784,14 @@ function execFileAsync(command, args, options = {}) {
});
}

function buildStopSessionCommandText(session, pid) {
const parts = ['gx', 'agents', 'stop', '--pid', String(pid)];
if (session?.repoRoot) {
parts.push('--target', session.repoRoot);
}
return parts.map(shellQuote).join(' ');
}

async function stopSession(session, refresh) {
const pid = Number(session?.pid);
if (!Number.isInteger(pid) || pid <= 0) {
Expand All @@ -1719,15 +1803,29 @@ async function stopSession(session, refresh) {
return;
}

const sessionTerminal = await findSessionTerminal(session);
const stopCommandText = buildStopSessionCommandText(session, pid);
const confirmed = await vscode.window.showWarningMessage(
`Stop ${sessionDisplayLabel(session)}?`,
{ modal: true, detail: `Run gx agents stop --pid ${pid}.` },
{
modal: true,
detail: sessionTerminal
? 'Send Ctrl+C to the live session terminal.'
: `No live session terminal found. Run ${stopCommandText}.`,
},
'Stop',
);
if (confirmed !== 'Stop') {
return;
}

if (sessionTerminal) {
focusTerminal(sessionTerminal);
sessionTerminal.sendText('\u0003', false);
refresh();
return;
}

try {
const commandCwd = session?.repoRoot || sessionWorktreePath(session) || process.cwd();
const args = ['agents', 'stop', '--pid', String(pid)];
Expand All @@ -1747,71 +1845,6 @@ async function stopSession(session, refresh) {
}
}

function sessionChangedPaths(session) {
const directPaths = Array.isArray(session?.changedPaths)
? session.changedPaths.map(normalizeRelativePath).filter(Boolean)
: [];
if (directPaths.length > 0) {
return [...new Set(directPaths)];
}
if (!session?.repoRoot || !session?.branch) {
return [];
}

const liveSession = readActiveSessions(session.repoRoot)
.find((entry) => sessionSelectionKey(entry) === sessionSelectionKey(session));
return Array.isArray(liveSession?.changedPaths)
? [...new Set(liveSession.changedPaths.map(normalizeRelativePath).filter(Boolean))]
: [];
}

async function pickSessionDiffPath(session) {
const changedPaths = sessionChangedPaths(session);
if (changedPaths.length === 0) {
return '';
}
if (changedPaths.length === 1 || !vscode.window.showQuickPick) {
return changedPaths[0];
}

const picks = changedPaths.map((relativePath) => ({
label: path.basename(relativePath),
description: relativePath,
relativePath,
}));
const selection = await vscode.window.showQuickPick(picks, {
placeHolder: `Select a changed file for ${sessionDisplayLabel(session)}`,
ignoreFocusOut: true,
});
return selection?.relativePath || '';
}

async function openSessionDiff(session) {
const worktreePath = ensureSessionWorktree(session, 'open diff');
if (!worktreePath) {
return;
}

const relativePath = await pickSessionDiffPath(session);
if (!relativePath) {
showSessionMessage(`No changed files to diff for ${sessionDisplayLabel(session)}.`);
return;
}

const repoRoot = session?.repoRoot || worktreePath;
const absolutePath = path.resolve(repoRoot, relativePath);
const resourceUri = vscode.Uri.file(absolutePath);
try {
await vscode.commands.executeCommand('git.openChange', resourceUri);
} catch (error) {
if (fs.existsSync(absolutePath)) {
await vscode.commands.executeCommand('vscode.open', resourceUri);
return;
}
showSessionMessage(`Failed to open diff for ${sessionDisplayLabel(session)}: ${formatGitCommandFailure(error)}`);
}
}

function readGitDirPath(targetPath) {
const normalizedTargetPath = typeof targetPath === 'string' ? targetPath.trim() : '';
if (!normalizedTargetPath) {
Expand Down Expand Up @@ -3321,10 +3354,10 @@ function activate(context) {
vscode.commands.registerCommand('gitguardex.activeAgents.inspect', (session) => {
inspectPanelManager.open(session || provider.getSelectedSession());
}),
vscode.commands.registerCommand('gitguardex.activeAgents.showSessionTerminal', showSessionTerminal),
vscode.commands.registerCommand('gitguardex.activeAgents.finishSession', finishSession),
vscode.commands.registerCommand('gitguardex.activeAgents.syncSession', syncSession),
vscode.commands.registerCommand('gitguardex.activeAgents.stopSession', (session) => stopSession(session, refresh)),
vscode.commands.registerCommand('gitguardex.activeAgents.openSessionDiff', openSessionDiff),
vscode.workspace.onDidChangeWorkspaceFolders(handleWorkspaceFoldersChanged),
activeSessionsWatcher,
lockWatcher,
Expand Down
16 changes: 8 additions & 8 deletions templates/vscode/guardex-active-agents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.16",
"version": "0.0.17",
"license": "MIT",
"icon": "icon.png",
"engines": {
Expand Down Expand Up @@ -66,9 +66,9 @@
"icon": "$(debug-stop)"
},
{
"command": "gitguardex.activeAgents.openSessionDiff",
"title": "Open Diff",
"icon": "$(diff)"
"command": "gitguardex.activeAgents.showSessionTerminal",
"title": "Show Terminal",
"icon": "$(terminal)"
}
],
"viewsContainers": {
Expand Down Expand Up @@ -134,22 +134,22 @@
"group": "inline"
},
{
"command": "gitguardex.activeAgents.finishSession",
"command": "gitguardex.activeAgents.showSessionTerminal",
"when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
"group": "inline"
},
{
"command": "gitguardex.activeAgents.syncSession",
"command": "gitguardex.activeAgents.finishSession",
"when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
"group": "inline"
},
{
"command": "gitguardex.activeAgents.stopSession",
"command": "gitguardex.activeAgents.syncSession",
"when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
"group": "inline"
},
{
"command": "gitguardex.activeAgents.openSessionDiff",
"command": "gitguardex.activeAgents.stopSession",
"when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
"group": "inline"
}
Expand Down
Loading