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/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..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 @@ -87,10 +90,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[] = [ // ============================================================================ @@ -99,6 +103,7 @@ export const AGENT_PROVIDERS: AgentProvider[] = [ { id: "copilot", label: "GitHub Copilot", + icon: "comment-discussion", extensionId: "github.copilot-chat", priority: 1, commands: { @@ -109,6 +114,7 @@ export const AGENT_PROVIDERS: AgentProvider[] = [ { id: "claude-code", label: "Claude Code", + icon: "hubot", extensionId: "anthropic.claude-code", priority: 2, commands: { @@ -117,6 +123,17 @@ export const AGENT_PROVIDERS: AgentProvider[] = [ }, focusDelay: 400, }, + { + id: "cursor", + label: "Cursor", + icon: "terminal", + appNameMatch: "cursor", + priority: 3, + commands: { + openWithPrompt: "workbench.action.chat.open", + newTask: "workbench.action.chat.newChat", + }, + }, // ============================================================================ // Fallback - Always available @@ -124,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/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..1c59a0f 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, @@ -2111,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 31c831a..faeafd3 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,43 @@ 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((agent) => ({ + label: `$(${agent.icon ?? "debug-start"}) ${agent.label}`, + description: agent.id === defaultAgentId ? "Default" : undefined, + detail: agent.id, + })) + agentItems.push({ + label: "$(clippy) Copy Prompt", + description: "Copy 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 + + // 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") + } else if (result.copiedToClipboard && result.message) { + vscode.window.showInformationMessage(result.message) } } catch (error) { vscode.window.showErrorMessage(`Failed to send to agent: ${error}`)