Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface ExtensionMessage {
| "mcpServers"
| "enhancedPrompt"
| "commitSearchResults"
| "terminalSearchResults"
| "listApiConfig"
| "routerModels"
| "openAiModels"
Expand Down Expand Up @@ -144,6 +145,7 @@ export interface ExtensionMessage {
}>
mcpServers?: McpServer[]
commits?: GitCommit[]
terminals?: TerminalInfo[] // For terminalSearchResults
listApiConfig?: ProviderSettingsEntry[]
mode?: string
customMode?: ModeConfig
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -424,6 +435,7 @@ export interface WebviewMessage {
| "remoteControlEnabled"
| "taskSyncEnabled"
| "searchCommits"
| "searchTerminals"
| "setApiConfigPassword"
| "mode"
| "updatePrompt"
Expand Down
36 changes: 34 additions & 2 deletions src/core/mentions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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
})
Expand Down Expand Up @@ -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>`
}
}
}

Expand Down Expand Up @@ -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))
Comment on lines +415 to +418
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 100ms delay is an arbitrary heuristic that may not reliably ensure the terminal is focused before selectAll executes. Terminal.show() makes the terminal visible but doesn't guarantee it becomes the active terminal synchronously. On slower machines or busy VS Code instances, this timing may be insufficient, potentially causing content to be copied from the wrong terminal.

Consider using a polling approach with vscode.window.activeTerminal to verify the terminal is actually active, or document this as a known limitation. A more robust pattern might be:

targetTerminal.show()
const maxRetries = 10
for (let i = 0; i < maxRetries; i++) {
	if (vscode.window.activeTerminal === targetTerminal) break
	await new Promise(resolve => setTimeout(resolve, 50))
}

Fix it with Roo Code or mention @roomote and request a fix.

}

// Select terminal content
await vscode.commands.executeCommand("workbench.action.terminal.selectAll")

Expand Down
20 changes: 20 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
27 changes: 25 additions & 2 deletions webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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) => ({
Expand All @@ -289,7 +311,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
value: path,
})),
]
}, [filePaths, gitCommits, openedTabs])
}, [filePaths, gitCommits, terminalInstances, openedTabs])

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions webview-ui/src/utils/context-mentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +251 to +252
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This filtering will include duplicate entries. The queryItems array already contains a default { type: Terminal, value: "terminal" } entry (added in ChatTextArea.tsx line 297), and then you prepend another "All Terminals" option with the same value. When the terminal sub-menu is displayed, users will see two items that both represent "All Terminals" - one with an explicit label and one that falls back to default rendering.

Consider filtering out the default terminal item before adding the explicit "All Terminals" option:

Suggested change
if (selectedType === ContextMenuOptionType.Terminal) {
const terminals = queryItems.filter((item) => item.type === ContextMenuOptionType.Terminal)
if (selectedType === ContextMenuOptionType.Terminal) {
const terminals = queryItems.filter(
(item) => item.type === ContextMenuOptionType.Terminal && item.value !== "terminal"
)

Fix it with Roo Code or mention @roomote and request a fix.

// 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 },
Expand Down
Loading