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
4 changes: 2 additions & 2 deletions scripts/agent-branch-finish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ is_local_branch_delete_error() {

is_remote_branch_missing_error() {
local output="$1"
if [[ "$output" == *"remote ref does not exist"* ]] || [[ "$output" == *"failed to push some refs"* ]]; then
if [[ "$output" == *"remote ref does not exist"* ]]; then
return 0
fi
return 1
Expand Down Expand Up @@ -893,8 +893,8 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
if is_remote_branch_missing_error "$remote_delete_output"; then
echo "[agent-branch-finish] Remote branch '${SOURCE_BRANCH}' was already deleted; continuing cleanup." >&2
else
echo "[agent-branch-finish] Warning: remote branch cleanup failed for '${SOURCE_BRANCH}' after merge; continuing local cleanup." >&2
echo "$remote_delete_output" >&2
exit 1
fi
fi
fi
Expand Down
66 changes: 66 additions & 0 deletions templates/vscode/guardex-active-agents/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -2395,6 +2395,51 @@ function isPathWithin(parentPath, targetPath) {
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
}

function normalizeAbsolutePath(value) {
return typeof value === 'string' && value.trim() ? path.resolve(value) : '';
}

function removeDeletedWorktreeWorkspaceFolder(worktreePath) {
if (typeof vscode.workspace.updateWorkspaceFolders !== 'function') {
return false;
}

const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
if (!normalizedWorktreePath) {
return false;
}

const workspaceFolders = vscode.workspace.workspaceFolders || [];
const folderIndex = workspaceFolders.findIndex((folder) => (
normalizeAbsolutePath(folder?.uri?.fsPath) === normalizedWorktreePath
));
if (folderIndex < 0) {
return false;
}

try {
return vscode.workspace.updateWorkspaceFolders(folderIndex, 1) === true;
} catch (_error) {
return false;
}
}

async function closeDeletedWorktreeRepository(worktreePath) {
const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
if (!normalizedWorktreePath || fs.existsSync(normalizedWorktreePath)) {
return false;
}

try {
await vscode.commands.executeCommand('git.close', vscode.Uri.file(normalizedWorktreePath));
} catch (_error) {
// The Git extension may have already removed this repository.
}

removeDeletedWorktreeWorkspaceFolder(normalizedWorktreePath);
return true;
}

function localizeChangeForSession(session, change) {
if (!change?.absolutePath || !isPathWithin(session.worktreePath, change.absolutePath)) {
return null;
Expand Down Expand Up @@ -3434,6 +3479,7 @@ class ActiveAgentsRefreshController {
this.inspectPanelManager = inspectPanelManager;
this.refreshTimer = null;
this.sessionWatchers = new Map();
this.closedMissingWorktreeRepositories = new Set();
}

scheduleRefresh() {
Expand All @@ -3458,6 +3504,16 @@ class ActiveAgentsRefreshController {

for (const entry of repoEntries) {
for (const session of entry.sessions) {
const worktreePath = sessionWorktreePath(session);
const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
if (normalizedWorktreePath && !fs.existsSync(normalizedWorktreePath)) {
await this.closeMissingWorktreeRepository(normalizedWorktreePath);
continue;
}
if (normalizedWorktreePath) {
this.closedMissingWorktreeRepositories.delete(normalizedWorktreePath);
}

const sessionKey = resolveSessionWatcherKey(session);
liveSessionKeys.add(sessionKey);
if (this.sessionWatchers.has(sessionKey)) {
Expand All @@ -3483,6 +3539,16 @@ class ActiveAgentsRefreshController {
}
}

async closeMissingWorktreeRepository(worktreePath) {
const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
if (!normalizedWorktreePath || this.closedMissingWorktreeRepositories.has(normalizedWorktreePath)) {
return;
}

this.closedMissingWorktreeRepositories.add(normalizedWorktreePath);
await closeDeletedWorktreeRepository(normalizedWorktreePath);
}

dispose() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
Expand Down
2 changes: 1 addition & 1 deletion 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": "Recodee",
"version": "0.0.20",
"version": "0.0.21",
"license": "MIT",
"icon": "icon.png",
"engines": {
Expand Down
60 changes: 60 additions & 0 deletions test/vscode-active-agents-session-state.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ function createMockVscode(tempRoot) {
fileWatchers: [],
watchers: [],
workspaceFolderListeners: [],
workspaceFolderUpdates: [],
configurationUpdates: [],
workspaceConfigurationValues: new Map(),
};
Expand Down Expand Up @@ -643,6 +644,11 @@ function createMockVscode(tempRoot) {
}
});
},
updateWorkspaceFolders(start, deleteCount, ...folders) {
registrations.workspaceFolderUpdates.push({ start, deleteCount, folders });
this.workspaceFolders.splice(start, deleteCount, ...folders);
return true;
},
workspaceFolders: [{ uri: { fsPath: tempRoot } }],
},
ConfigurationTarget,
Expand Down Expand Up @@ -1507,6 +1513,60 @@ test('active-agents extension registers tree and decoration providers', async ()
}
});

test('active-agents extension closes deleted worktree repositories during refresh', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-close-deleted-'));
const worktreePath = path.join(tempRoot, '.omx', 'agent-worktrees', 'deleted-task');
fs.mkdirSync(worktreePath, { recursive: true });
const sessionPath = writeSessionRecord(tempRoot, sessionSchema.buildSessionRecord({
repoRoot: tempRoot,
branch: 'agent/codex/deleted-task',
taskName: 'deleted-task',
agentName: 'codex',
worktreePath,
pid: process.pid,
cliName: 'codex',
}));

const { registrations, vscode } = createMockVscode(tempRoot);
vscode.workspace.workspaceFolders = [
{ uri: { fsPath: tempRoot } },
{ uri: { fsPath: worktreePath } },
];
vscode.workspace.findFiles = async () => [{ fsPath: sessionPath }];
const extension = loadExtensionWithMockVscode(vscode);
const context = { subscriptions: [] };

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

const gitCloseCalls = () => registrations.executedCommands.filter((entry) => (
entry.command === 'git.close'
));
assert.equal(gitCloseCalls().length, 0);

fs.rmSync(worktreePath, { recursive: true, force: true });
await registrations.commands.get('gitguardex.activeAgents.refresh')();
await flushAsyncWork();

assert.equal(gitCloseCalls().length, 1);
assert.equal(gitCloseCalls()[0].args[0].fsPath, path.resolve(worktreePath));
assert.deepEqual(registrations.workspaceFolderUpdates, [
{ start: 1, deleteCount: 1, folders: [] },
]);
assert.deepEqual(
vscode.workspace.workspaceFolders.map((folder) => folder.uri.fsPath),
[tempRoot],
);

await registrations.commands.get('gitguardex.activeAgents.refresh')();
await flushAsyncWork();
assert.equal(gitCloseCalls().length, 1);

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

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);
Expand Down
66 changes: 66 additions & 0 deletions vscode/guardex-active-agents/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -2395,6 +2395,51 @@ function isPathWithin(parentPath, targetPath) {
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
}

function normalizeAbsolutePath(value) {
return typeof value === 'string' && value.trim() ? path.resolve(value) : '';
}

function removeDeletedWorktreeWorkspaceFolder(worktreePath) {
if (typeof vscode.workspace.updateWorkspaceFolders !== 'function') {
return false;
}

const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
if (!normalizedWorktreePath) {
return false;
}

const workspaceFolders = vscode.workspace.workspaceFolders || [];
const folderIndex = workspaceFolders.findIndex((folder) => (
normalizeAbsolutePath(folder?.uri?.fsPath) === normalizedWorktreePath
));
if (folderIndex < 0) {
return false;
}

try {
return vscode.workspace.updateWorkspaceFolders(folderIndex, 1) === true;
} catch (_error) {
return false;
}
}

async function closeDeletedWorktreeRepository(worktreePath) {
const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
if (!normalizedWorktreePath || fs.existsSync(normalizedWorktreePath)) {
return false;
}

try {
await vscode.commands.executeCommand('git.close', vscode.Uri.file(normalizedWorktreePath));
} catch (_error) {
// The Git extension may have already removed this repository.
}

removeDeletedWorktreeWorkspaceFolder(normalizedWorktreePath);
return true;
}

function localizeChangeForSession(session, change) {
if (!change?.absolutePath || !isPathWithin(session.worktreePath, change.absolutePath)) {
return null;
Expand Down Expand Up @@ -3434,6 +3479,7 @@ class ActiveAgentsRefreshController {
this.inspectPanelManager = inspectPanelManager;
this.refreshTimer = null;
this.sessionWatchers = new Map();
this.closedMissingWorktreeRepositories = new Set();
}

scheduleRefresh() {
Expand All @@ -3458,6 +3504,16 @@ class ActiveAgentsRefreshController {

for (const entry of repoEntries) {
for (const session of entry.sessions) {
const worktreePath = sessionWorktreePath(session);
const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
if (normalizedWorktreePath && !fs.existsSync(normalizedWorktreePath)) {
await this.closeMissingWorktreeRepository(normalizedWorktreePath);
continue;
}
if (normalizedWorktreePath) {
this.closedMissingWorktreeRepositories.delete(normalizedWorktreePath);
}

const sessionKey = resolveSessionWatcherKey(session);
liveSessionKeys.add(sessionKey);
if (this.sessionWatchers.has(sessionKey)) {
Expand All @@ -3483,6 +3539,16 @@ class ActiveAgentsRefreshController {
}
}

async closeMissingWorktreeRepository(worktreePath) {
const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
if (!normalizedWorktreePath || this.closedMissingWorktreeRepositories.has(normalizedWorktreePath)) {
return;
}

this.closedMissingWorktreeRepositories.add(normalizedWorktreePath);
await closeDeletedWorktreeRepository(normalizedWorktreePath);
}

dispose() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
Expand Down
2 changes: 1 addition & 1 deletion 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": "Recodee",
"version": "0.0.20",
"version": "0.0.21",
"license": "MIT",
"icon": "icon.png",
"engines": {
Expand Down
Loading