Skip to content

Commit 584af64

Browse files
authored
Add terminal context mention (RooCodeInc#1805)
* Add terminal context mention * Create fuzzy-moose-punch.md
1 parent ee06c08 commit 584af64

File tree

7 files changed

+79
-3
lines changed

7 files changed

+79
-3
lines changed

.changeset/fuzzy-moose-punch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": patch
3+
---
4+
5+
Add terminal context mention

src/core/mentions/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import fs from "fs/promises"
77
import { extractTextFromFile } from "../../integrations/misc/extract-text"
88
import { isBinaryFile } from "isbinaryfile"
99
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
10+
import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
1011

1112
export function openMention(mention?: string): void {
1213
if (!mention) {
@@ -28,6 +29,8 @@ export function openMention(mention?: string): void {
2829
}
2930
} else if (mention === "problems") {
3031
vscode.commands.executeCommand("workbench.actions.view.problems")
32+
} else if (mention === "terminal") {
33+
vscode.commands.executeCommand("workbench.action.terminal.focus")
3134
} else if (mention.startsWith("http")) {
3235
vscode.env.openExternal(vscode.Uri.parse(mention))
3336
}
@@ -46,6 +49,8 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
4649
: `'${mentionPath}' (see below for file content)`
4750
} else if (mention === "problems") {
4851
return `Workspace Problems (see below for diagnostics)`
52+
} else if (mention === "terminal") {
53+
return `Terminal Output (see below for output)`
4954
}
5055
return match
5156
})
@@ -99,6 +104,13 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
99104
} catch (error) {
100105
parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
101106
}
107+
} else if (mention === "terminal") {
108+
try {
109+
const terminalOutput = await getLatestTerminalOutput()
110+
parsedText += `\n\n<terminal_output>\n${terminalOutput}\n</terminal_output>`
111+
} catch (error) {
112+
parsedText += `\n\n<terminal_output>\nError fetching terminal output: ${error.message}\n</terminal_output>`
113+
}
102114
}
103115
}
104116

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as vscode from "vscode"
2+
3+
/**
4+
* Gets the contents of the active terminal
5+
* @returns The terminal contents as a string
6+
*/
7+
export async function getLatestTerminalOutput(): Promise<string> {
8+
// Store original clipboard content to restore later
9+
const originalClipboard = await vscode.env.clipboard.readText()
10+
11+
try {
12+
// Select terminal content
13+
await vscode.commands.executeCommand("workbench.action.terminal.selectAll")
14+
15+
// Copy selection to clipboard
16+
await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
17+
18+
// Clear the selection
19+
await vscode.commands.executeCommand("workbench.action.terminal.clearSelection")
20+
21+
// Get terminal contents from clipboard
22+
let terminalContents = (await vscode.env.clipboard.readText()).trim()
23+
24+
// Check if there's actually a terminal open
25+
if (terminalContents === originalClipboard) {
26+
return ""
27+
}
28+
29+
// Clean up command separation
30+
const lines = terminalContents.split("\n")
31+
const lastLine = lines.pop()?.trim()
32+
if (lastLine) {
33+
let i = lines.length - 1
34+
while (i >= 0 && !lines[i].trim().startsWith(lastLine)) {
35+
i--
36+
}
37+
terminalContents = lines.slice(Math.max(i, 0)).join("\n")
38+
}
39+
40+
return terminalContents
41+
} finally {
42+
// Restore original clipboard content
43+
await vscode.env.clipboard.writeText(originalClipboard)
44+
}
45+
}

src/shared/context-mentions.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ Mention regex:
2525
- `problems\b`:
2626
- **Exact Word ('problems')**: Matches the exact word 'problems'.
2727
- **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic').
28+
- `terminal\b`:
29+
- **Exact Word ('terminal')**: Matches the exact word 'terminal'.
30+
- **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals').
2831
2932
- `(?=[.,;:!?]?(?=[\s\r\n]|$))`:
3033
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
@@ -38,11 +41,12 @@ Mention regex:
3841
- Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path).
3942
- URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters).
4043
- The exact word 'problems'.
44+
- The exact word 'terminal'.
4145
- It ensures that any trailing punctuation marks (such as ',', '.', '!', etc.) are not included in the matched mention, allowing the punctuation to follow the mention naturally in the text.
4246
4347
- **Global Regex**:
4448
- `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string.
4549
4650
*/
47-
export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+?|problems\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
51+
export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+?|problems\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
4852
export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
243243
const queryItems = useMemo(() => {
244244
return [
245245
{ type: ContextMenuOptionType.Problems, value: "problems" },
246+
{ type: ContextMenuOptionType.Terminal, value: "terminal" },
246247
...filePaths
247248
.map((file) => "/" + file)
248249
.map((path) => ({
@@ -293,6 +294,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
293294
insertValue = value || ""
294295
} else if (type === ContextMenuOptionType.Problems) {
295296
insertValue = "problems"
297+
} else if (type === ContextMenuOptionType.Terminal) {
298+
insertValue = "terminal"
296299
}
297300

298301
const { newValue, mentionIndex } = insertMention(textAreaRef.current.value, cursorPosition, insertValue)

webview-ui/src/components/chat/ContextMenu.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
4848
switch (option.type) {
4949
case ContextMenuOptionType.Problems:
5050
return <span>Problems</span>
51+
case ContextMenuOptionType.Terminal:
52+
return <span>Terminal</span>
5153
case ContextMenuOptionType.URL:
5254
return <span>Paste URL to fetch contents</span>
5355
case ContextMenuOptionType.NoResults:
@@ -85,6 +87,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
8587
return "folder"
8688
case ContextMenuOptionType.Problems:
8789
return "warning"
90+
case ContextMenuOptionType.Terminal:
91+
return "terminal"
8892
case ContextMenuOptionType.URL:
8993
return "link"
9094
case ContextMenuOptionType.NoResults:
@@ -173,6 +177,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
173177
/>
174178
)}
175179
{(option.type === ContextMenuOptionType.Problems ||
180+
option.type === ContextMenuOptionType.Terminal ||
176181
((option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) &&
177182
option.value)) && (
178183
<i

webview-ui/src/utils/context-mentions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export enum ContextMenuOptionType {
4646
File = "file",
4747
Folder = "folder",
4848
Problems = "problems",
49+
Terminal = "terminal",
4950
URL = "url",
5051
NoResults = "noResults",
5152
}
@@ -84,6 +85,7 @@ export function getContextMenuOptions(
8485
return [
8586
{ type: ContextMenuOptionType.URL },
8687
{ type: ContextMenuOptionType.Problems },
88+
{ type: ContextMenuOptionType.Terminal },
8789
{ type: ContextMenuOptionType.Folder },
8890
{ type: ContextMenuOptionType.File },
8991
]
@@ -121,8 +123,8 @@ export function shouldShowContextMenu(text: string, position: number): boolean {
121123
// Don't show the menu if it's a URL
122124
if (textAfterAt.toLowerCase().startsWith("http")) return false
123125

124-
// Don't show the menu if it's a problems
125-
if (textAfterAt.toLowerCase().startsWith("problems")) return false
126+
// Don't show the menu if it's a problems or terminal
127+
if (textAfterAt.toLowerCase().startsWith("problems") || textAfterAt.toLowerCase().startsWith("terminal")) return false
126128

127129
// NOTE: it's okay that menu shows when there's trailing punctuation since user could be inputting a path with marks
128130

0 commit comments

Comments
 (0)