From 96d7500dc17b5826b34de093f3d099a9c5a62551 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Wed, 22 Apr 2026 11:05:01 +0200 Subject: [PATCH 1/2] Show actionable welcome controls in the active agents view The empty state now uses VS Code's welcome view so users can start a sandbox, open the companion guide, or refresh without reading a placeholder tree item. The extension also prompts for task and agent names before sending gx branch start to an integrated terminal, and the test harness now covers the empty-state and start-agent flow. Constraint: The Source Control companion ships from both vscode/ and templates/vscode/, so both trees must stay in sync Rejected: Keep the InfoItem placeholder and add a second welcome message | duplicate empty-state UI in the same view Confidence: high Scope-risk: narrow Directive: Keep the checked-in extension and the install template byte-for-byte aligned when changing this companion Tested: node --test test/vscode-active-agents-session-state.test.js Not-tested: Full npm test did not complete within the interactive window --- .../vscode/guardex-active-agents/README.md | 21 +++-- .../vscode/guardex-active-agents/extension.js | 92 ++++++++++++++++--- .../vscode/guardex-active-agents/package.json | 10 ++ ...vscode-active-agents-session-state.test.js | 46 +++++++++- vscode/guardex-active-agents/README.md | 21 +++-- vscode/guardex-active-agents/extension.js | 92 ++++++++++++++++--- vscode/guardex-active-agents/package.json | 10 ++ 7 files changed, 248 insertions(+), 44 deletions(-) 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": [ { From 3aa5306b6ec0905d0309f51618a90d3acaa65f93 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Wed, 22 Apr 2026 11:10:57 +0200 Subject: [PATCH 2/2] Record the welcome-view change in OpenSpec notes The branch already carried the implementation commit, but it was missing the repo's required T1 notes bundle. This commit captures the scope, verification, and cleanup handoff so the finish pipeline has the expected artifact. Constraint: This repo requires an OpenSpec change record for every code change, even when the implementation is already complete Rejected: Fold the note into the earlier implementation commit | amending an existing commit was not requested Confidence: high Scope-risk: narrow Directive: Keep small T1 extension changes paired with notes.md and .openspec.yaml before running branch finish Tested: node --test test/vscode-active-agents-session-state.test.js Not-tested: Full npm test remains outside this metadata-only follow-up --- .../.openspec.yaml | 2 ++ .../notes.md | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 openspec/changes/agent-codex-vscode-active-agents-welcome-view-2026-04-22-10-58/.openspec.yaml create mode 100644 openspec/changes/agent-codex-vscode-active-agents-welcome-view-2026-04-22-10-58/notes.md 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`).