diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index b20539afe4..91fa11b186 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -41,6 +41,7 @@ export interface ExtensionMessage { | "mcpServers" | "enhancedPrompt" | "commitSearchResults" + | "terminalSearchResults" | "listApiConfig" | "routerModels" | "openAiModels" @@ -143,6 +144,7 @@ export interface ExtensionMessage { vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[] mcpServers?: McpServer[] commits?: GitCommit[] + terminals?: TerminalInfo[] // For terminalSearchResults listApiConfig?: ProviderSettingsEntry[] mode?: string customMode?: ModeConfig @@ -392,6 +394,15 @@ export interface Command { argumentHint?: string } +/** + * Terminal instance information for terminal selection + */ +export interface TerminalInfo { + id: number + name: string + isActive: boolean +} + /** * WebviewMessage * Webview | CLI -> Extension @@ -478,6 +489,7 @@ export interface WebviewMessage { | "editMessageConfirm" | "taskSyncEnabled" | "searchCommits" + | "searchTerminals" | "setApiConfigPassword" | "mode" | "updatePrompt" diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index 1bfb90d23f..a65872dbaf 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -38,6 +38,14 @@ export async function openMention(cwd: string, mention?: string): Promise vscode.commands.executeCommand("workbench.actions.view.problems") } else if (mention === "terminal") { vscode.commands.executeCommand("workbench.action.terminal.focus") + } else if (mention.startsWith("terminal:")) { + const terminalName = mention.slice(9) + const targetTerminal = vscode.window.terminals.find((t) => t.name === terminalName) + if (targetTerminal) { + targetTerminal.show() + } else { + vscode.commands.executeCommand("workbench.action.terminal.focus") + } } else if (mention.startsWith("http")) { vscode.env.openExternal(vscode.Uri.parse(mention)) } @@ -176,6 +184,9 @@ export async function parseMentions( return `Git commit '${mention}' (see below for commit info)` } else if (mention === "terminal") { return `Terminal Output (see below for output)` + } else if (mention.startsWith("terminal:")) { + const terminalName = mention.slice(9) + return `Terminal Output from '${terminalName}' (see below for output)` } return match }) @@ -228,6 +239,14 @@ export async function parseMentions( } catch (error) { parsedText += `\n\n\nError fetching terminal output: ${error.message}\n` } + } else if (mention.startsWith("terminal:")) { + const terminalName = mention.slice(9) + try { + const terminalOutput = await getLatestTerminalOutput(terminalName) + parsedText += `\n\n\n${terminalOutput}\n` + } catch (error) { + parsedText += `\n\n\nError fetching terminal output: ${error.message}\n` + } } } @@ -410,14 +429,27 @@ async function getWorkspaceProblems( } /** - * Gets the contents of the active terminal + * Gets the contents of a terminal + * @param terminalName Optional name of the terminal to get output from. If not provided, gets output from the active terminal. * @returns The terminal contents as a string */ -export async function getLatestTerminalOutput(): Promise { +export async function getLatestTerminalOutput(terminalName?: string): Promise { // Store original clipboard content to restore later const originalClipboard = await vscode.env.clipboard.readText() try { + // If a specific terminal name is provided, find and focus it first + if (terminalName) { + const targetTerminal = vscode.window.terminals.find((t) => t.name === terminalName) + if (!targetTerminal) { + return `Terminal '${terminalName}' not found` + } + // Show the terminal to make it active + targetTerminal.show() + // Small delay to ensure terminal is focused + await new Promise((resolve) => setTimeout(resolve, 100)) + } + // Select terminal content await vscode.commands.executeCommand("workbench.action.terminal.selectAll") diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index e3b8c1bea8..2043ae44ce 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1763,6 +1763,26 @@ export const webviewMessageHandler = async ( } break } + case "searchTerminals": { + try { + // Get all VS Code terminal instances + const vsCodeTerminals = vscode.window.terminals + const terminals = vsCodeTerminals.map((terminal, index) => ({ + id: index, + name: terminal.name, + isActive: vscode.window.activeTerminal === terminal, + })) + await provider.postMessageToWebview({ + type: "terminalSearchResults", + terminals, + }) + } catch (error) { + provider.log( + `Error searching terminals: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + } + break + } case "searchFiles": { const workspacePath = getCurrentCwd() diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index e72c1726f3..ed8b7f9dc3 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -111,6 +111,7 @@ export const ChatTextArea = forwardRef( }, [listApiConfigMeta, currentApiConfigName]) const [gitCommits, setGitCommits] = useState([]) + const [terminalInstances, setTerminalInstances] = useState([]) const [showDropdown, setShowDropdown] = useState(false) const [fileSearchResults, setFileSearchResults] = useState([]) const [searchLoading, setSearchLoading] = useState(false) @@ -201,6 +202,16 @@ export const ChatTextArea = forwardRef( if (message.requestId === searchRequestId) { setFileSearchResults(message.results || []) } + } else if (message.type === "terminalSearchResults") { + const terminals = + message.terminals?.map((terminal: { id: number; name: string; isActive: boolean }) => ({ + type: ContextMenuOptionType.Terminal, + value: `terminal:${terminal.name}`, + label: terminal.name, + description: terminal.isActive ? "Active terminal" : "Terminal instance", + icon: "$(terminal)", + })) || [] + setTerminalInstances(terminals) } } @@ -244,6 +255,16 @@ export const ChatTextArea = forwardRef( } }, [selectedType, searchQuery]) + // Fetch terminal instances when Terminal is selected. + useEffect(() => { + if (selectedType === ContextMenuOptionType.Terminal) { + const message: WebviewMessage = { + type: "searchTerminals", + } as const + vscode.postMessage(message) + } + }, [selectedType]) + const handleEnhancePrompt = useCallback(() => { const trimmedInput = inputValue.trim() @@ -278,6 +299,7 @@ export const ChatTextArea = forwardRef( { type: ContextMenuOptionType.Problems, value: "problems" }, { type: ContextMenuOptionType.Terminal, value: "terminal" }, ...gitCommits, + ...terminalInstances, ...openedTabs .filter((tab) => tab.path) .map((tab) => ({ @@ -292,7 +314,7 @@ export const ChatTextArea = forwardRef( value: path, })), ] - }, [filePaths, gitCommits, openedTabs]) + }, [filePaths, gitCommits, terminalInstances, openedTabs]) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -352,7 +374,8 @@ export const ChatTextArea = forwardRef( if ( type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder || - type === ContextMenuOptionType.Git + type === ContextMenuOptionType.Git || + type === ContextMenuOptionType.Terminal ) { if (!value) { setSelectedType(type) diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts index 22dba8864f..ae3b8da29a 100644 --- a/webview-ui/src/utils/context-mentions.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -248,6 +248,19 @@ export function getContextMenuOptions( return commits.length > 0 ? [workingChanges, ...commits] : [workingChanges] } + if (selectedType === ContextMenuOptionType.Terminal) { + const terminals = queryItems.filter((item) => item.type === ContextMenuOptionType.Terminal) + // Always include "All Terminals" option first, then specific terminals + const allTerminalsOption: ContextMenuQueryItem = { + type: ContextMenuOptionType.Terminal, + value: "terminal", + label: "All Terminals", + description: "Output from all terminal instances", + icon: "$(terminal)", + } + return terminals.length > 0 ? [allTerminalsOption, ...terminals] : [allTerminalsOption] + } + return [ { type: ContextMenuOptionType.Problems }, { type: ContextMenuOptionType.Terminal },