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

- Operators can already see live Guardex sandboxes inside the `gitguardex.activeAgents` Source Control companion, but they cannot commit the selected sandbox without dropping back to the terminal.
- The reference UX already exposes a compact header commit affordance; this view should use the same pattern instead of forcing a second workflow.

## What Changes

- Track the currently selected Active Agents session in the VS Code companion.
- Add a native SCM commit input plus header commit command that targets the selected session's `worktreePath`.
- Stage with `git add -A` while excluding `.omx/state/agent-file-locks.json`, then run `git commit -m <message>` when the user accepts the input or clicks the header affordance.
- Show an information message if the user tries to commit without selecting a session first.

## Impact

- Scope stays inside the VS Code companion bundle plus its focused regression tests.
- The commit flow shells out to `git`, so failure paths must surface clear VS Code messages instead of failing silently.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## ADDED Requirements

### Requirement: Active Agents SCM commit box targets the selected sandbox
The Guardex Active Agents VS Code companion SHALL expose a native SCM commit input that targets the currently selected `gitguardex.activeAgents` session worktree.

#### Scenario: Accept input commits the selected session worktree
- **WHEN** the operator selects a live Active Agents session and accepts the SCM input
- **THEN** the companion stages the selected session worktree with `git add -A`
- **AND** it excludes `.omx/state/agent-file-locks.json` from that stage operation
- **AND** it runs `git commit -m <message>` against the selected session's `worktreePath`.

#### Scenario: Header commit affordance uses the same selected session
- **WHEN** the operator activates the view-header commit command while a live session is selected
- **THEN** the companion uses the same SCM input message
- **AND** it commits the same selected session worktree instead of prompting for a different target.

#### Scenario: Missing selection degrades safely
- **WHEN** the operator accepts the SCM input or clicks the header commit affordance without a selected session
- **THEN** the companion does not run any git command
- **AND** it shows an information message telling the operator to pick a session first.
Original file line number Diff line number Diff line change
@@ -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-vscode-active-agents-scm-commit-input-2026-04-22-10-55`; branch=`agent/codex/vscode-active-agents-scm-commit-input-2026-04-22-10-55`; scope=`templates/vscode/guardex-active-agents/*`, `vscode/guardex-active-agents/*`, `test/vscode-active-agents-session-state.test.js`; action=`add a selected-session SCM commit input and header affordance to the Active Agents companion`.
- Copy prompt: Continue `agent-codex-vscode-active-agents-scm-commit-input-2026-04-22-10-55` on branch `agent/codex/vscode-active-agents-scm-commit-input-2026-04-22-10-55`. Work inside the existing sandbox, review `openspec/changes/agent-codex-vscode-active-agents-scm-commit-input-2026-04-22-10-55/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-scm-commit-input-2026-04-22-10-55 --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-scm-commit-input-2026-04-22-10-55`.
- [x] 1.2 Define normative requirements in `specs/vscode-active-agents-scm-commit-input/spec.md`.

## 2. Implementation

- [x] 2.1 Track the currently selected Active Agents session and surface the native SCM commit box/header affordance for that selection.
- [x] 2.2 Stage and commit the selected worktree with the agent lock-file exclusion and a no-selection information message.
- [x] 2.3 Keep the source and template extension bundles in sync.

## 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-scm-commit-input-2026-04-22-10-55 --type change --strict`.
- [x] 3.3 Run `openspec validate --specs`.

## 4. Cleanup (mandatory; run before claiming completion)

- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent/codex/vscode-active-agents-scm-commit-input-2026-04-22-10-55 --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).
120 changes: 120 additions & 0 deletions templates/vscode/guardex-active-agents/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ function countWorkingSessions(sessions) {
return sessions.filter((session) => session.activityKind === 'working').length;
}

<<<<<<< HEAD
function shellQuote(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}
Expand Down Expand Up @@ -529,6 +530,38 @@ async function startAgentFromPrompt(refresh) {
true,
);
refresh();
=======
function sessionSelectionKey(session) {
if (!session?.repoRoot || !session?.branch) {
return '';
}

return `${session.repoRoot}::${session.branch}`;
}

function formatGitCommandFailure(error) {
for (const value of [error?.stderr, error?.stdout, error?.message]) {
if (typeof value === 'string' && value.trim().length > 0) {
return value.trim();
}
}
return 'Git command failed.';
}

function runGitCommand(worktreePath, args) {
return cp.execFileSync('git', ['-C', worktreePath, ...args], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
}

function stageWorktreeForCommit(worktreePath) {
runGitCommand(worktreePath, ['add', '-A', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`]);
}

function commitWorktree(worktreePath, message) {
runGitCommand(worktreePath, ['commit', '-m', message]);
>>>>>>> 60c38c6 (Let operators commit the selected sandbox from the Active Agents SCM view)
}

function buildActiveAgentGroupNodes(sessions) {
Expand All @@ -555,8 +588,11 @@ class ActiveAgentsProvider {
this.decorationProvider = decorationProvider;
this.onDidChangeTreeDataEmitter = new vscode.EventEmitter();
this.onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event;
this.onDidChangeSelectedSessionEmitter = new vscode.EventEmitter();
this.onDidChangeSelectedSession = this.onDidChangeSelectedSessionEmitter.event;
this.treeView = null;
this.lockRegistryByRepoRoot = new Map();
this.selectedSession = null;
}

getTreeItem(element) {
Expand All @@ -566,6 +602,35 @@ class ActiveAgentsProvider {
attachTreeView(treeView) {
this.treeView = treeView;
this.updateViewState(0, 0);
treeView.onDidChangeSelection?.((event) => {
const sessionItem = event.selection.find((item) => item instanceof SessionItem);
this.setSelectedSession(sessionItem?.session || null);
});
}

setSelectedSession(session) {
const nextSession = session?.worktreePath ? { ...session } : null;
const currentKey = sessionSelectionKey(this.selectedSession);
const nextKey = sessionSelectionKey(nextSession);
this.selectedSession = nextSession;
if (currentKey !== nextKey) {
this.onDidChangeSelectedSessionEmitter.fire(this.selectedSession);
}
}

getSelectedSession() {
return this.selectedSession ? { ...this.selectedSession } : null;
}

syncSelectedSession(repoEntries) {
if (!this.selectedSession) {
return;
}

const nextSession = repoEntries
.flatMap((entry) => entry.sessions)
.find((session) => sessionSelectionKey(session) === sessionSelectionKey(this.selectedSession));
this.setSelectedSession(nextSession || null);
}

updateViewState(sessionCount, workingCount) {
Expand Down Expand Up @@ -634,6 +699,7 @@ class ActiveAgentsProvider {
}

const repoEntries = await this.syncRepoEntries();
this.syncSelectedSession(repoEntries);

if (repoEntries.length === 0) {
return [];
Expand Down Expand Up @@ -688,12 +754,62 @@ function activate(context) {
treeDataProvider: provider,
showCollapseAll: true,
});
const sourceControl = vscode.scm.createSourceControl(
'gitguardex.activeAgents.commitInput',
'Active Agents Commit',
);
provider.attachTreeView(treeView);
const refresh = () => {
void provider.refresh();
};
const sessionWatcher = vscode.workspace.createFileSystemWatcher('**/.omx/state/active-sessions/*.json');
const lockWatcher = vscode.workspace.createFileSystemWatcher('**/.omx/state/agent-file-locks.json');
const updateCommitInput = (session) => {
sourceControl.inputBox.enabled = true;
sourceControl.inputBox.visible = true;
sourceControl.inputBox.placeholder = session?.label
? `Commit ${session.label} (Ctrl+Enter)`
: 'Pick an Active Agents session to commit its worktree.';
};
updateCommitInput(null);
const commitSelectedSession = async () => {
const selectedSession = provider.getSelectedSession();
if (!selectedSession?.worktreePath) {
vscode.window.showInformationMessage?.('Pick an Active Agents session first.');
return;
}

const message = String(sourceControl.inputBox.value || '').trim();
if (!message) {
vscode.window.showInformationMessage?.('Enter a commit message first.');
return;
}

if (!fs.existsSync(selectedSession.worktreePath)) {
vscode.window.showInformationMessage?.(
`Selected session worktree is no longer on disk: ${selectedSession.worktreePath}`,
);
return;
}

try {
stageWorktreeForCommit(selectedSession.worktreePath);
commitWorktree(selectedSession.worktreePath, message);
sourceControl.inputBox.value = '';
refresh();
} catch (error) {
const failure = formatGitCommandFailure(error);
if (/nothing to commit|no changes added to commit/i.test(failure)) {
vscode.window.showInformationMessage?.(`No changes to commit in ${selectedSession.label}.`);
return;
}
vscode.window.showErrorMessage?.(`Active Agents commit failed: ${failure}`);
}
};
sourceControl.acceptInputCommand = {
command: 'gitguardex.activeAgents.commitSelectedSession',
title: 'Commit Selected Session',
};
const interval = setInterval(refresh, 5_000);
const refreshLockRegistry = (uri) => {
if (uri?.fsPath) {
Expand All @@ -702,11 +818,15 @@ function activate(context) {
refresh();
};

provider.onDidChangeSelectedSession(updateCommitInput);

context.subscriptions.push(
treeView,
sourceControl,
vscode.window.registerFileDecorationProvider(decorationProvider),
vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)),
vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh),
vscode.commands.registerCommand('gitguardex.activeAgents.commitSelectedSession', commitSelectedSession),
vscode.commands.registerCommand('gitguardex.activeAgents.openWorktree', async (session) => {
if (!session?.worktreePath) {
return;
Expand Down
12 changes: 11 additions & 1 deletion templates/vscode/guardex-active-agents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
"command": "gitguardex.activeAgents.refresh",
"title": "Refresh Active Agents"
},
{
"command": "gitguardex.activeAgents.commitSelectedSession",
"title": "Commit Selected Session",
"icon": "$(check)"
},
{
"command": "gitguardex.activeAgents.openWorktree",
"title": "Open Agent Worktree"
Expand Down Expand Up @@ -69,10 +74,15 @@
],
"menus": {
"view/title": [
{
"command": "gitguardex.activeAgents.commitSelectedSession",
"when": "view == gitguardex.activeAgents",
"group": "navigation@1"
},
{
"command": "gitguardex.activeAgents.refresh",
"when": "view == gitguardex.activeAgents",
"group": "navigation"
"group": "navigation@9"
}
],
"view/item/context": [
Expand Down
Loading