feat: add terminal instance selection for @terminal context mention

- Add TerminalInfo interface and terminalSearchResults message type
- Update context-mentions.ts to show terminal sub-selection menu
- Add searchTerminals handler in webviewMessageHandler.ts
- Update ChatTextArea.tsx to fetch terminals when Terminal type selected
- Update parseMentions to handle @terminal:name syntax
- Update getLatestTerminalOutput to support specific terminal selection
- Update openMention to focus specific terminal when clicked

Addresses #10622
This commit is contained in:
Roo Code 2026-01-12 11:20:35 +00:00
parent 632b86cfbe
commit a6253d69cd
5 changed files with 104 additions and 4 deletions

View file

@ -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"

View file

@ -66,6 +66,14 @@ export async function openMention(cwd: string, mention?: string): Promise<void>
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<terminal_output>\nError fetching terminal output: ${error.message}\n</terminal_output>`
}
} else if (mention.startsWith("terminal:")) {
const terminalName = mention.slice(9)
try {
const terminalOutput = await getLatestTerminalOutput(terminalName)
parsedText += `\n\n<terminal_output name="${terminalName}">\n${terminalOutput}\n</terminal_output>`
} catch (error) {
parsedText += `\n\n<terminal_output name="${terminalName}">\nError fetching terminal output: ${error.message}\n</terminal_output>`
}
}
}
@ -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<string> {
export async function getLatestTerminalOutput(terminalName?: string): Promise<string> {
// 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")

View file

@ -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()

View file

@ -108,6 +108,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}, [listApiConfigMeta, currentApiConfigName])
const [gitCommits, setGitCommits] = useState<any[]>([])
const [terminalInstances, setTerminalInstances] = useState<any[]>([])
const [showDropdown, setShowDropdown] = useState(false)
const [fileSearchResults, setFileSearchResults] = useState<SearchResult[]>([])
const [searchLoading, setSearchLoading] = useState(false)
@ -198,6 +199,16 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
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<HTMLTextAreaElement, ChatTextAreaProps>(
}
}, [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<HTMLTextAreaElement, ChatTextAreaProps>(
{ 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<HTMLTextAreaElement, ChatTextAreaProps>(
value: path,
})),
]
}, [filePaths, gitCommits, openedTabs])
}, [filePaths, gitCommits, terminalInstances, openedTabs])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@ -349,7 +371,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
if (
type === ContextMenuOptionType.File ||
type === ContextMenuOptionType.Folder ||
type === ContextMenuOptionType.Git
type === ContextMenuOptionType.Git ||
type === ContextMenuOptionType.Terminal
) {
if (!value) {
setSelectedType(type)

View file

@ -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 },