diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index c0bb8726d7..6cce23da51 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -36,6 +36,7 @@ export interface ExtensionMessage { | "mcpServers" | "enhancedPrompt" | "commitSearchResults" + | "terminalSearchResults" | "listApiConfig" | "routerModels" | "openAiModels" @@ -144,6 +145,7 @@ export interface ExtensionMessage { }> mcpServers?: McpServer[] commits?: GitCommit[] + terminals?: TerminalInfo[] // For terminalSearchResults listApiConfig?: ProviderSettingsEntry[] mode?: string customMode?: ModeConfig @@ -336,6 +338,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 @@ -424,6 +435,7 @@ export interface WebviewMessage { | "remoteControlEnabled" | "taskSyncEnabled" | "searchCommits" + | "searchTerminals" | "setApiConfigPassword" | "mode" | "updatePrompt" diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index 2bbbf9ed0d..00418b69c8 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -66,6 +66,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)) } @@ -144,6 +152,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 }) @@ -242,6 +253,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` + } } } @@ -378,14 +397,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 5d3c9e0152..998465bde5 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1714,6 +1714,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 dd3d0d4c66..a10394daa9 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -108,6 +108,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) @@ -198,6 +199,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) } } @@ -241,6 +252,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() @@ -275,6 +296,7 @@ export const ChatTextArea = forwardRef( { type: ContextMenuOptionType.Problems, value: "problems" }, { type: ContextMenuOptionType.Terminal, value: "terminal" }, ...gitCommits, + ...terminalInstances, ...openedTabs .filter((tab) => tab.path) .map((tab) => ({ @@ -289,7 +311,7 @@ export const ChatTextArea = forwardRef( value: path, })), ] - }, [filePaths, gitCommits, openedTabs]) + }, [filePaths, gitCommits, terminalInstances, openedTabs]) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -349,7 +371,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 },