From aa6203cea55fce54eb08f31e76f781682cb75af5 Mon Sep 17 00:00:00 2001 From: Alexey Ozerov Date: Tue, 10 Feb 2026 11:48:26 +0300 Subject: [PATCH 1/4] feat: add Cursor support for Send to Agent - Add Cursor provider in providers.ts (appNameMatch, native chat commands) - Fix default agent in Cursor: do not use saved Copilot when unavailable (boardViewProvider, boardEditorPanel) - CodeLens Send to Agent: use AgentRegistry instead of hardcoded switch so Cursor works from brainfile.md --- CHANGELOG.md | 12 +++++++ package-lock.json | 4 +-- package.json | 2 +- src/board/__tests__/types.test.ts | 2 +- src/board/agents/providers.ts | 13 ++++++- src/boardEditorPanel.ts | 13 ++++--- src/boardViewProvider.ts | 15 +++++--- src/extension.ts | 58 ++++++++++++------------------- 8 files changed, 71 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38f433e..e7c3e31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to the Brainfile VSCode extension will be documented in this The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.10.3] - 2026-02-10 + +### Added + +#### Send to Agent — Cursor +- **Cursor** added as Tier 1 agent: native chat via `workbench.action.chat.open`, detected by app name + +### Fixed + +#### Last-used agent selection +- When saved "last used" agent is not available in current environment (e.g. Copilot in Cursor), extension now falls back to default agent and persists it instead of leaving invalid selection + ## [0.10.1] - 2025-11-26 ### Fixed diff --git a/package-lock.json b/package-lock.json index cdad062..d29ccd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "brainfile", - "version": "0.10.0", + "version": "0.10.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "brainfile", - "version": "0.10.0", + "version": "0.10.3", "license": "MIT", "dependencies": { "@brainfile/core": "^0.7.0", diff --git a/package.json b/package.json index 1710f58..87fa361 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "brainfile", "displayName": "Brainfile", "description": "Protocol-first task management for AI-assisted development. Visual kanban board for brainfile.md files.", - "version": "0.10.1", + "version": "0.10.3", "publisher": "brainfile", "license": "MIT", "icon": "icon.png", diff --git a/src/board/__tests__/types.test.ts b/src/board/__tests__/types.test.ts index 2014899..d8ad389 100644 --- a/src/board/__tests__/types.test.ts +++ b/src/board/__tests__/types.test.ts @@ -14,7 +14,7 @@ import { describe("board/types", () => { describe("AgentType constants", () => { it("AGENT_TYPES contains all expected agents", () => { - expect(AGENT_TYPES).toEqual(["copilot", "claude-code", "copy"]) + expect(AGENT_TYPES).toEqual(["copilot", "claude-code", "cursor", "copy"]) }) it("AGENT_LABELS has label for each agent type", () => { diff --git a/src/board/agents/providers.ts b/src/board/agents/providers.ts index acfa796..9e7c9ec 100644 --- a/src/board/agents/providers.ts +++ b/src/board/agents/providers.ts @@ -87,10 +87,11 @@ export interface AgentProvider { * Currently supported (Tier 1 - reliable native APIs): * - GitHub Copilot (native VS Code chat API) * - Claude Code (native VS Code chat API) + * - Cursor (native chat, detected by app name) * * Future consideration (Tier 2 - requires more research): * - Cline, Roo Code, Kilo Code (Cline forks) - * - Continue, Cursor + * - Continue */ export const AGENT_PROVIDERS: AgentProvider[] = [ // ============================================================================ @@ -117,6 +118,16 @@ export const AGENT_PROVIDERS: AgentProvider[] = [ }, focusDelay: 400, }, + { + id: "cursor", + label: "Cursor", + appNameMatch: "cursor", + priority: 3, + commands: { + openWithPrompt: "workbench.action.chat.open", + newTask: "workbench.action.chat.newChat", + }, + }, // ============================================================================ // Fallback - Always available diff --git a/src/boardEditorPanel.ts b/src/boardEditorPanel.ts index a4b26aa..f933477 100644 --- a/src/boardEditorPanel.ts +++ b/src/boardEditorPanel.ts @@ -463,14 +463,19 @@ export class BoardEditorPanel { // Agent detection methods - using AgentRegistry private _postAvailableAgents() { const registry = getAgentRegistry() + const agents = registry.getAvailableAgents() + const defaultAgent = registry.getDefaultAgent() + const availableIds = new Set(agents.map((agent) => agent.id)) - // Sync last used from workspace state - if (this._lastUsedAgent) { + // Only use saved last-used if that agent is currently available (e.g. Cursor has no Copilot) + if (this._lastUsedAgent && availableIds.has(this._lastUsedAgent)) { registry.setLastUsed(this._lastUsedAgent) + } else { + this._lastUsedAgent = defaultAgent + registry.setLastUsed(defaultAgent) + this._context.workspaceState.update("brainfile.lastUsedAgent", defaultAgent) } - const agents = registry.getAvailableAgents() - const defaultAgent = registry.getDefaultAgent() this._panel.webview.postMessage({ type: "agentsDetected", agents, diff --git a/src/boardViewProvider.ts b/src/boardViewProvider.ts index 57ad7bd..81dc6b6 100644 --- a/src/boardViewProvider.ts +++ b/src/boardViewProvider.ts @@ -98,14 +98,21 @@ export class BoardViewProvider implements vscode.WebviewViewProvider { private postAvailableAgents() { if (!this._view) return const registry = getAgentRegistry() + const agents = registry.getAvailableAgents() + const defaultAgent = registry.getDefaultAgent() + const availableIds = new Set(agents.map((agent) => agent.id)) - // Sync last used from workspace state - if (this._lastUsedAgent) { + // Only use saved last-used if that agent is currently available (e.g. Cursor has no Copilot) + if (this._lastUsedAgent && availableIds.has(this._lastUsedAgent)) { registry.setLastUsed(this._lastUsedAgent) + } else { + this._lastUsedAgent = defaultAgent + registry.setLastUsed(defaultAgent) + if (this._context) { + this._context.workspaceState.update("brainfile.lastUsedAgent", defaultAgent) + } } - const agents = registry.getAvailableAgents() - const defaultAgent = registry.getDefaultAgent() this._view.webview.postMessage({ type: "agentsDetected", agents, diff --git a/src/extension.ts b/src/extension.ts index 31c831a..b943bfb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,7 +9,7 @@ import { type Task, } from "@brainfile/core" import * as vscode from "vscode" -import { buildAgentPrompt } from "./board" +import { buildAgentPrompt, getAgentRegistry } from "./board" import { BoardEditorPanel, BoardEditorPanelSerializer } from "./boardEditorPanel" import { BoardViewProvider } from "./boardViewProvider" import { BrainfileCodeLensProvider } from "./codeLensProvider" @@ -488,45 +488,33 @@ function registerCodeLensCommands(context: vscode.ExtensionContext, boardProvide task: targetTask, }) - // Show quick pick to choose agent - const agents = [ - { label: "$(comment-discussion) Copilot", description: "Send to GitHub Copilot Chat", agent: "copilot" }, - { label: "$(terminal) Cursor", description: "Send to Cursor AI", agent: "cursor" }, - { label: "$(hubot) Claude Code", description: "Send to Claude Code terminal", agent: "claude-code" }, - { label: "$(clippy) Copy Prompt", description: "Copy to clipboard", agent: "copy" }, - ] + const registry = getAgentRegistry() + const availableAgents = registry.getAvailableAgents() + const defaultAgentId = registry.getDefaultAgent() + + const agentItems: vscode.QuickPickItem[] = availableAgents.map((a) => ({ + label: `$(debug-start) ${a.label}`, + description: a.id === defaultAgentId ? "Default" : undefined, + detail: a.id, + })) + agentItems.push({ + label: "$(clippy) Copy to Clipboard", + description: "Copy prompt to clipboard", + detail: "copy", + }) - const selected = await vscode.window.showQuickPick(agents, { + const selected = await vscode.window.showQuickPick(agentItems, { placeHolder: "Choose where to send the task", }) - if (!selected) return - - // Send to the selected agent - switch (selected.agent) { - case "copilot": - case "cursor": - try { - await vscode.commands.executeCommand("workbench.action.chat.newChat") - await new Promise((resolve) => setTimeout(resolve, 100)) - await vscode.commands.executeCommand("workbench.action.chat.open", prompt) - } catch (_err) { - await vscode.env.clipboard.writeText(prompt) - vscode.window.showInformationMessage("Prompt copied. Paste into chat.") - } - break + if (!selected || !selected.detail) return - case "claude-code": { - const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\n/g, "\\n") - const terminal = vscode.window.createTerminal("Claude Code") - terminal.show() - terminal.sendText(`claude "${escapedPrompt}"`) - break - } - default: - await vscode.env.clipboard.writeText(prompt) - vscode.window.showInformationMessage("Prompt copied to clipboard.") - break + const agentId = selected.detail + const result = await registry.sendToAgent(agentId, prompt) + if (!result.success) { + vscode.window.showErrorMessage(result.message || "Failed to send to agent") + } else if (result.copiedToClipboard && result.message) { + vscode.window.showInformationMessage(result.message) } } catch (error) { vscode.window.showErrorMessage(`Failed to send to agent: ${error}`) From 8fe7f1230b91bdca765412568c89810d0c1b2745 Mon Sep 17 00:00:00 2001 From: Alexey Ozerov Date: Tue, 10 Feb 2026 11:53:43 +0300 Subject: [PATCH 2/4] fix: restore Claude Code terminal flow in CodeLens Send to Agent When Claude Code is selected in the brainfile.md CodeLens menu, send the prompt to the terminal via 'claude "..."' again instead of using the registry's UI paste path. --- src/extension.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/extension.ts b/src/extension.ts index b943bfb..8094c0e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -510,6 +510,16 @@ function registerCodeLensCommands(context: vscode.ExtensionContext, boardProvide if (!selected || !selected.detail) return const agentId = selected.detail + + // Claude Code: send to terminal (legacy CodeLens behavior) + if (agentId === "claude-code") { + const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\n/g, "\\n") + const terminal = vscode.window.createTerminal("Claude Code") + terminal.show() + terminal.sendText(`claude "${escapedPrompt}"`) + return + } + const result = await registry.sendToAgent(agentId, prompt) if (!result.success) { vscode.window.showErrorMessage(result.message || "Failed to send to agent") From b02babe7a66bf8718c7fc113102b72e966ef8384 Mon Sep 17 00:00:00 2001 From: Alexey Ozerov Date: Tue, 10 Feb 2026 12:14:44 +0300 Subject: [PATCH 3/4] feat: add per-agent icons to registry and use in Quick Pick - Add optional icon (Codicon name) to AgentProvider and set for copilot, claude-code, cursor, copy - Add icon to DetectedAgent and pass through in registry.detectAgents() - CodeLens and sidebar task Quick Pick now show agent-specific icons (comment-discussion, hubot, terminal, clippy) --- src/board/agents/providers.ts | 7 +++++++ src/board/agents/registry.ts | 1 + src/board/types.ts | 2 ++ src/boardViewProvider.ts | 2 +- src/extension.ts | 8 ++++---- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/board/agents/providers.ts b/src/board/agents/providers.ts index 9e7c9ec..b12527e 100644 --- a/src/board/agents/providers.ts +++ b/src/board/agents/providers.ts @@ -28,6 +28,9 @@ export interface AgentProvider { /** Display name shown in UI */ label: string + /** VS Code Codicon name for UI (e.g. "comment-discussion", "terminal", "hubot"). Optional. */ + icon?: string + /** VS Code extension ID for detection */ extensionId?: string @@ -100,6 +103,7 @@ export const AGENT_PROVIDERS: AgentProvider[] = [ { id: "copilot", label: "GitHub Copilot", + icon: "comment-discussion", extensionId: "github.copilot-chat", priority: 1, commands: { @@ -110,6 +114,7 @@ export const AGENT_PROVIDERS: AgentProvider[] = [ { id: "claude-code", label: "Claude Code", + icon: "hubot", extensionId: "anthropic.claude-code", priority: 2, commands: { @@ -121,6 +126,7 @@ export const AGENT_PROVIDERS: AgentProvider[] = [ { id: "cursor", label: "Cursor", + icon: "terminal", appNameMatch: "cursor", priority: 3, commands: { @@ -135,6 +141,7 @@ export const AGENT_PROVIDERS: AgentProvider[] = [ { id: "copy", label: "Copy to Clipboard", + icon: "clippy", priority: 99, commands: {}, }, diff --git a/src/board/agents/registry.ts b/src/board/agents/registry.ts index dc0cc87..8b8c8ce 100644 --- a/src/board/agents/registry.ts +++ b/src/board/agents/registry.ts @@ -253,6 +253,7 @@ export class AgentRegistry { id: provider.id, type: provider.id, // Webview uses 'type' field label: provider.label, + icon: provider.icon, available: isProviderAvailable(provider), priority: provider.priority, })) diff --git a/src/board/types.ts b/src/board/types.ts index d7657e7..e8dd02c 100644 --- a/src/board/types.ts +++ b/src/board/types.ts @@ -21,6 +21,8 @@ export interface DetectedAgent { id: string type: string // Same as id, for webview compatibility label: string + /** VS Code Codicon name for UI. Optional. */ + icon?: string available: boolean priority: number } diff --git a/src/boardViewProvider.ts b/src/boardViewProvider.ts index 81dc6b6..1c59a0f 100644 --- a/src/boardViewProvider.ts +++ b/src/boardViewProvider.ts @@ -2118,7 +2118,7 @@ columns: items.push({ kind: vscode.QuickPickItemKind.Separator, label: "Send to Agent" } as any) // Type assertion due to kind property availableAgents.forEach((agent) => { items.push({ - label: `$(debug-start) ${agent.label}`, + label: `$(${agent.icon ?? "debug-start"}) ${agent.label}`, description: agent.type === agentRegistry.getDefaultAgent() ? "Default" : undefined, action: "send-agent", agentType: agent.type, diff --git a/src/extension.ts b/src/extension.ts index 8094c0e..eb347d9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -492,10 +492,10 @@ function registerCodeLensCommands(context: vscode.ExtensionContext, boardProvide const availableAgents = registry.getAvailableAgents() const defaultAgentId = registry.getDefaultAgent() - const agentItems: vscode.QuickPickItem[] = availableAgents.map((a) => ({ - label: `$(debug-start) ${a.label}`, - description: a.id === defaultAgentId ? "Default" : undefined, - detail: a.id, + const agentItems: vscode.QuickPickItem[] = availableAgents.map((agent) => ({ + label: `$(${agent.icon ?? "debug-start"}) ${agent.label}`, + description: agent.id === defaultAgentId ? "Default" : undefined, + detail: agent.id, })) agentItems.push({ label: "$(clippy) Copy to Clipboard", From fab0571412abcade481f05f4ecb63b027e43cc95 Mon Sep 17 00:00:00 2001 From: Alexey Ozerov Date: Tue, 10 Feb 2026 12:20:01 +0300 Subject: [PATCH 4/4] Reverted unnecessary changes in labels --- src/extension.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index eb347d9..faeafd3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -498,8 +498,8 @@ function registerCodeLensCommands(context: vscode.ExtensionContext, boardProvide detail: agent.id, })) agentItems.push({ - label: "$(clippy) Copy to Clipboard", - description: "Copy prompt to clipboard", + label: "$(clippy) Copy Prompt", + description: "Copy to clipboard", detail: "copy", })