diff --git a/openspec/changes/agent-codex-vscode-active-agents-welcome-view-2026-04-22-10-58/.openspec.yaml b/openspec/changes/agent-codex-vscode-active-agents-welcome-view-2026-04-22-10-58/.openspec.yaml new file mode 100644 index 0000000..25345f4 --- /dev/null +++ b/openspec/changes/agent-codex-vscode-active-agents-welcome-view-2026-04-22-10-58/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-22 diff --git a/openspec/changes/agent-codex-vscode-active-agents-welcome-view-2026-04-22-10-58/notes.md b/openspec/changes/agent-codex-vscode-active-agents-welcome-view-2026-04-22-10-58/notes.md new file mode 100644 index 0000000..94b1ad2 --- /dev/null +++ b/openspec/changes/agent-codex-vscode-active-agents-welcome-view-2026-04-22-10-58/notes.md @@ -0,0 +1,26 @@ +# agent-codex-vscode-active-agents-welcome-view-2026-04-22-10-58 (minimal / T1) + +Branch: `agent/codex/vscode-active-agents-welcome-view-2026-04-22-10-58` + +Replace the Active Agents empty-state placeholder with a native VS Code welcome view that exposes direct actions for starting a sandbox, opening the guide, and refreshing the panel. + +Scope: +- Add `contributes.viewsWelcome` plus a `gitguardex.activeAgents.startAgent` command in both extension manifests. +- Remove the `InfoItem` empty-state fallback so the tree returns empty and VS Code renders the welcome content. +- Prompt for task + agent name, then send `gx branch start '' ''` to an integrated terminal. +- Add a stable `README.md#quick-start` anchor for the guide link. +- Extend the active-agents regression suite to cover the new empty-state and command flow. + +Verification: +- `node --test test/vscode-active-agents-session-state.test.js` + +## Handoff + +- Handoff: change=`agent-codex-vscode-active-agents-welcome-view-2026-04-22-10-58`; branch=`agent/codex/vscode-active-agents-welcome-view-2026-04-22-10-58`; scope=`templates/vscode/guardex-active-agents/*, vscode/guardex-active-agents/*, test/vscode-active-agents-session-state.test.js, openspec/changes/agent-codex-vscode-active-agents-welcome-view-2026-04-22-10-58/*`; action=`finish this sandbox via PR merge + cleanup after targeted verification`. +- Copy prompt: Continue `agent-codex-vscode-active-agents-welcome-view-2026-04-22-10-58` on branch `agent/codex/vscode-active-agents-welcome-view-2026-04-22-10-58`. Work inside the existing sandbox, review `openspec/changes/agent-codex-vscode-active-agents-welcome-view-2026-04-22-10-58/notes.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-welcome-view-2026-04-22-10-58 --base main --via-pr --wait-for-merge --cleanup`. + +## Cleanup + +- [ ] Run: `gx branch finish --branch agent/codex/vscode-active-agents-welcome-view-2026-04-22-10-58 --base main --via-pr --wait-for-merge --cleanup` +- [ ] Record PR URL + `MERGED` state in the completion handoff. +- [ ] Confirm sandbox worktree is gone (`git worktree list`, `git branch -a`). diff --git a/templates/vscode/guardex-active-agents/README.md b/templates/vscode/guardex-active-agents/README.md index b63a8c3..23228d9 100644 --- a/templates/vscode/guardex-active-agents/README.md +++ b/templates/vscode/guardex-active-agents/README.md @@ -2,6 +2,19 @@ Local VS Code companion for Guardex-managed repos. +## Quick Start + +Use the welcome view in Source Control to create or inspect Guardex sandboxes quickly. + +1. Install from a Guardex-wired repo: + +```sh +node scripts/install-vscode-active-agents-extension.js +``` + +2. Reload the VS Code window. +3. In Source Control -> `Active Agents`, use `Start agent` to enter a task + agent name and run `gx branch start`. + What it does: - Adds an `Active Agents` view to the Source Control container. @@ -12,11 +25,3 @@ What it does: - Derives `thinking` versus `working` from the live sandbox worktree, surfaces working counts in the repo/header summary, and shows changed-file counts for active edits. - Uses VS Code's native animated `loading~spin` icon for the running-state affordance. - Reads repo-local presence files from `.omx/state/active-sessions/`. - -Install from a Guardex-wired repo: - -```sh -node scripts/install-vscode-active-agents-extension.js -``` - -Then reload the VS Code window. diff --git a/templates/vscode/guardex-active-agents/extension.js b/templates/vscode/guardex-active-agents/extension.js index cdad978..1abba6a 100644 --- a/templates/vscode/guardex-active-agents/extension.js +++ b/templates/vscode/guardex-active-agents/extension.js @@ -74,14 +74,6 @@ class SessionDecorationProvider { } } -class InfoItem extends vscode.TreeItem { - constructor(label, description = '') { - super(label, vscode.TreeItemCollapsibleState.None); - this.description = description; - this.iconPath = new vscode.ThemeIcon('info'); - } -} - class RepoItem extends vscode.TreeItem { constructor(repoRoot, sessions, changes) { super(path.basename(repoRoot), vscode.TreeItemCollapsibleState.Expanded); @@ -462,6 +454,83 @@ function countWorkingSessions(sessions) { return sessions.filter((session) => session.activityKind === 'working').length; } +function shellQuote(value) { + return `'${String(value).replace(/'/g, `'\\''`)}'`; +} + +async function pickRepoRoot() { + const workspaceFolders = vscode.workspace.workspaceFolders || []; + if (workspaceFolders.length === 0) { + vscode.window.showInformationMessage?.('Open a Guardex workspace folder to start an agent.'); + return null; + } + + if (workspaceFolders.length === 1) { + return workspaceFolders[0].uri.fsPath; + } + + const picks = workspaceFolders.map((folder) => ({ + label: path.basename(folder.uri.fsPath), + description: folder.uri.fsPath, + repoRoot: folder.uri.fsPath, + })); + const selection = await vscode.window.showQuickPick?.(picks, { + placeHolder: 'Select the Guardex repo where gx branch start should run.', + }); + return selection?.repoRoot || null; +} + +async function promptStartAgentDetails() { + const taskName = await vscode.window.showInputBox?.({ + prompt: 'Task for gx branch start', + placeHolder: 'vscode active agents welcome view', + ignoreFocusOut: true, + validateInput: (value) => value.trim() ? undefined : 'Task is required.', + }); + if (!taskName) { + return null; + } + + const agentName = await vscode.window.showInputBox?.({ + prompt: 'Agent name for gx branch start', + placeHolder: 'codex', + value: 'codex', + ignoreFocusOut: true, + validateInput: (value) => value.trim() ? undefined : 'Agent name is required.', + }); + if (!agentName) { + return null; + } + + return { + taskName: taskName.trim(), + agentName: agentName.trim(), + }; +} + +async function startAgentFromPrompt(refresh) { + const repoRoot = await pickRepoRoot(); + if (!repoRoot) { + return; + } + + const details = await promptStartAgentDetails(); + if (!details) { + return; + } + + const terminal = vscode.window.createTerminal?.({ + name: `GitGuardex: ${path.basename(repoRoot)}`, + cwd: repoRoot, + }); + terminal?.show(true); + terminal?.sendText( + `gx branch start ${shellQuote(details.taskName)} ${shellQuote(details.agentName)}`, + true, + ); + refresh(); +} + function buildActiveAgentGroupNodes(sessions) { const workingSessions = sessions .filter((session) => session.activityKind === 'working') @@ -511,9 +580,7 @@ class ActiveAgentsProvider { + (workingCount > 0 ? ` · ${workingCount} working now` : ''), } : undefined; - this.treeView.message = sessionCount > 0 - ? undefined - : 'Start a sandbox session to populate this view.'; + this.treeView.message = undefined; } async syncRepoEntries() { @@ -569,7 +636,7 @@ class ActiveAgentsProvider { const repoEntries = await this.syncRepoEntries(); if (repoEntries.length === 0) { - return [new InfoItem('No active Guardex agents', 'Open or start a sandbox session.')]; + return []; } return repoEntries.map((entry) => new RepoItem(entry.repoRoot, entry.sessions, entry.changes)); @@ -638,6 +705,7 @@ function activate(context) { context.subscriptions.push( treeView, vscode.window.registerFileDecorationProvider(decorationProvider), + vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)), vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh), vscode.commands.registerCommand('gitguardex.activeAgents.openWorktree', async (session) => { if (!session?.worktreePath) { diff --git a/templates/vscode/guardex-active-agents/package.json b/templates/vscode/guardex-active-agents/package.json index d38c39e..7d89c87 100644 --- a/templates/vscode/guardex-active-agents/package.json +++ b/templates/vscode/guardex-active-agents/package.json @@ -19,6 +19,10 @@ "main": "./extension.js", "contributes": { "commands": [ + { + "command": "gitguardex.activeAgents.startAgent", + "title": "Start Guardex Agent" + }, { "command": "gitguardex.activeAgents.refresh", "title": "Refresh Active Agents" @@ -57,6 +61,12 @@ } ] }, + "viewsWelcome": [ + { + "view": "gitguardex.activeAgents", + "contents": "No active Guardex agents are visible in this workspace yet.\n\n[Start agent](command:gitguardex.activeAgents.startAgent)\n[Open guide](https://github.com/recodeee/gitguardex/blob/main/vscode/guardex-active-agents/README.md#quick-start)\n[Refresh](command:gitguardex.activeAgents.refresh)" + } + ], "menus": { "view/title": [ { diff --git a/test/vscode-active-agents-session-state.test.js b/test/vscode-active-agents-session-state.test.js index 3b79300..ac2dc7f 100644 --- a/test/vscode-active-agents-session-state.test.js +++ b/test/vscode-active-agents-session-state.test.js @@ -70,6 +70,9 @@ function createMockVscode(tempRoot) { openedDocuments: [], shownDocuments: [], infoMessages: [], + inputResponses: [], + quickPickCalls: [], + quickPickResponse: undefined, warningMessages: [], watchers: [], }; @@ -206,6 +209,11 @@ function createMockVscode(tempRoot) { registrations.warningMessages.push(args); return undefined; }, + showInputBox: async () => registrations.inputResponses.shift(), + showQuickPick: async (items, options) => { + registrations.quickPickCalls.push({ items, options }); + return registrations.quickPickResponse; + }, createTerminal: (options) => { const terminal = { options, @@ -448,12 +456,42 @@ 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'); - const [rootItem] = await provider.getChildren(); - assert.equal(rootItem.label, 'No active Guardex agents'); - assert.equal(provider.getTreeItem(rootItem), rootItem); + const rootItems = await provider.getChildren(); + assert.deepEqual(rootItems, []); assert.equal(registrations.treeViews[0].badge, undefined); - assert.equal(registrations.treeViews[0].message, 'Start a sandbox session to populate this view.'); + assert.equal(registrations.treeViews[0].message, undefined); + + for (const subscription of context.subscriptions) { + subscription.dispose?.(); + } +}); + +test('active-agents extension startAgent command prompts and runs gx branch start in a terminal', async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-vscode-start-agent-')); + const { registrations, vscode } = createMockVscode(tempRoot); + registrations.inputResponses.push('demo task', 'codex'); + const extension = loadExtensionWithMockVscode(vscode); + const context = { subscriptions: [] }; + + extension.activate(context); + + await registrations.commands.get('gitguardex.activeAgents.startAgent')(); + + assert.equal(registrations.terminals.length, 1); + assert.deepEqual(registrations.terminals[0].options, { + name: `GitGuardex: ${path.basename(tempRoot)}`, + cwd: tempRoot, + }); + assert.equal(registrations.terminals[0].shown, true); + assert.deepEqual(registrations.terminals[0].sentTexts, [ + { + text: "gx branch start 'demo task' 'codex'", + addNewLine: true, + }, + ]); + assert.deepEqual(registrations.quickPickCalls, []); for (const subscription of context.subscriptions) { subscription.dispose?.(); diff --git a/vscode/guardex-active-agents/README.md b/vscode/guardex-active-agents/README.md index b63a8c3..23228d9 100644 --- a/vscode/guardex-active-agents/README.md +++ b/vscode/guardex-active-agents/README.md @@ -2,6 +2,19 @@ Local VS Code companion for Guardex-managed repos. +## Quick Start + +Use the welcome view in Source Control to create or inspect Guardex sandboxes quickly. + +1. Install from a Guardex-wired repo: + +```sh +node scripts/install-vscode-active-agents-extension.js +``` + +2. Reload the VS Code window. +3. In Source Control -> `Active Agents`, use `Start agent` to enter a task + agent name and run `gx branch start`. + What it does: - Adds an `Active Agents` view to the Source Control container. @@ -12,11 +25,3 @@ What it does: - Derives `thinking` versus `working` from the live sandbox worktree, surfaces working counts in the repo/header summary, and shows changed-file counts for active edits. - Uses VS Code's native animated `loading~spin` icon for the running-state affordance. - Reads repo-local presence files from `.omx/state/active-sessions/`. - -Install from a Guardex-wired repo: - -```sh -node scripts/install-vscode-active-agents-extension.js -``` - -Then reload the VS Code window. diff --git a/vscode/guardex-active-agents/extension.js b/vscode/guardex-active-agents/extension.js index cdad978..1abba6a 100644 --- a/vscode/guardex-active-agents/extension.js +++ b/vscode/guardex-active-agents/extension.js @@ -74,14 +74,6 @@ class SessionDecorationProvider { } } -class InfoItem extends vscode.TreeItem { - constructor(label, description = '') { - super(label, vscode.TreeItemCollapsibleState.None); - this.description = description; - this.iconPath = new vscode.ThemeIcon('info'); - } -} - class RepoItem extends vscode.TreeItem { constructor(repoRoot, sessions, changes) { super(path.basename(repoRoot), vscode.TreeItemCollapsibleState.Expanded); @@ -462,6 +454,83 @@ function countWorkingSessions(sessions) { return sessions.filter((session) => session.activityKind === 'working').length; } +function shellQuote(value) { + return `'${String(value).replace(/'/g, `'\\''`)}'`; +} + +async function pickRepoRoot() { + const workspaceFolders = vscode.workspace.workspaceFolders || []; + if (workspaceFolders.length === 0) { + vscode.window.showInformationMessage?.('Open a Guardex workspace folder to start an agent.'); + return null; + } + + if (workspaceFolders.length === 1) { + return workspaceFolders[0].uri.fsPath; + } + + const picks = workspaceFolders.map((folder) => ({ + label: path.basename(folder.uri.fsPath), + description: folder.uri.fsPath, + repoRoot: folder.uri.fsPath, + })); + const selection = await vscode.window.showQuickPick?.(picks, { + placeHolder: 'Select the Guardex repo where gx branch start should run.', + }); + return selection?.repoRoot || null; +} + +async function promptStartAgentDetails() { + const taskName = await vscode.window.showInputBox?.({ + prompt: 'Task for gx branch start', + placeHolder: 'vscode active agents welcome view', + ignoreFocusOut: true, + validateInput: (value) => value.trim() ? undefined : 'Task is required.', + }); + if (!taskName) { + return null; + } + + const agentName = await vscode.window.showInputBox?.({ + prompt: 'Agent name for gx branch start', + placeHolder: 'codex', + value: 'codex', + ignoreFocusOut: true, + validateInput: (value) => value.trim() ? undefined : 'Agent name is required.', + }); + if (!agentName) { + return null; + } + + return { + taskName: taskName.trim(), + agentName: agentName.trim(), + }; +} + +async function startAgentFromPrompt(refresh) { + const repoRoot = await pickRepoRoot(); + if (!repoRoot) { + return; + } + + const details = await promptStartAgentDetails(); + if (!details) { + return; + } + + const terminal = vscode.window.createTerminal?.({ + name: `GitGuardex: ${path.basename(repoRoot)}`, + cwd: repoRoot, + }); + terminal?.show(true); + terminal?.sendText( + `gx branch start ${shellQuote(details.taskName)} ${shellQuote(details.agentName)}`, + true, + ); + refresh(); +} + function buildActiveAgentGroupNodes(sessions) { const workingSessions = sessions .filter((session) => session.activityKind === 'working') @@ -511,9 +580,7 @@ class ActiveAgentsProvider { + (workingCount > 0 ? ` · ${workingCount} working now` : ''), } : undefined; - this.treeView.message = sessionCount > 0 - ? undefined - : 'Start a sandbox session to populate this view.'; + this.treeView.message = undefined; } async syncRepoEntries() { @@ -569,7 +636,7 @@ class ActiveAgentsProvider { const repoEntries = await this.syncRepoEntries(); if (repoEntries.length === 0) { - return [new InfoItem('No active Guardex agents', 'Open or start a sandbox session.')]; + return []; } return repoEntries.map((entry) => new RepoItem(entry.repoRoot, entry.sessions, entry.changes)); @@ -638,6 +705,7 @@ function activate(context) { context.subscriptions.push( treeView, vscode.window.registerFileDecorationProvider(decorationProvider), + vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)), vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh), vscode.commands.registerCommand('gitguardex.activeAgents.openWorktree', async (session) => { if (!session?.worktreePath) { diff --git a/vscode/guardex-active-agents/package.json b/vscode/guardex-active-agents/package.json index d38c39e..7d89c87 100644 --- a/vscode/guardex-active-agents/package.json +++ b/vscode/guardex-active-agents/package.json @@ -19,6 +19,10 @@ "main": "./extension.js", "contributes": { "commands": [ + { + "command": "gitguardex.activeAgents.startAgent", + "title": "Start Guardex Agent" + }, { "command": "gitguardex.activeAgents.refresh", "title": "Refresh Active Agents" @@ -57,6 +61,12 @@ } ] }, + "viewsWelcome": [ + { + "view": "gitguardex.activeAgents", + "contents": "No active Guardex agents are visible in this workspace yet.\n\n[Start agent](command:gitguardex.activeAgents.startAgent)\n[Open guide](https://github.com/recodeee/gitguardex/blob/main/vscode/guardex-active-agents/README.md#quick-start)\n[Refresh](command:gitguardex.activeAgents.refresh)" + } + ], "menus": { "view/title": [ {