Skip to content

Commit ffec4c6

Browse files
authored
Merge pull request #1006 from RooVetGit/terminal_context_mention
Terminal context mention
2 parents 36bbfd9 + fde9d9b commit ffec4c6

File tree

7 files changed

+80
-8
lines changed

7 files changed

+80
-8
lines changed

src/core/mentions/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { extractTextFromFile } from "../../integrations/misc/extract-text"
88
import { isBinaryFile } from "isbinaryfile"
99
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
1010
import { getCommitInfo, getWorkingState } from "../../utils/git"
11+
import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
1112

1213
export async function openMention(mention?: string): Promise<void> {
1314
if (!mention) {
@@ -29,6 +30,8 @@ export async function openMention(mention?: string): Promise<void> {
2930
}
3031
} else if (mention === "problems") {
3132
vscode.commands.executeCommand("workbench.actions.view.problems")
33+
} else if (mention === "terminal") {
34+
vscode.commands.executeCommand("workbench.action.terminal.focus")
3235
} else if (mention.startsWith("http")) {
3336
vscode.env.openExternal(vscode.Uri.parse(mention))
3437
}
@@ -51,6 +54,8 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
5154
return `Working directory changes (see below for details)`
5255
} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
5356
return `Git commit '${mention}' (see below for commit info)`
57+
} else if (mention === "terminal") {
58+
return `Terminal Output (see below for output)`
5459
}
5560
return match
5661
})
@@ -118,6 +123,13 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
118123
} catch (error) {
119124
parsedText += `\n\n<git_commit hash="${mention}">\nError fetching commit info: ${error.message}\n</git_commit>`
120125
}
126+
} else if (mention === "terminal") {
127+
try {
128+
const terminalOutput = await getLatestTerminalOutput()
129+
parsedText += `\n\n<terminal_output>\n${terminalOutput}\n</terminal_output>`
130+
} catch (error) {
131+
parsedText += `\n\n<terminal_output>\nError fetching terminal output: ${error.message}\n</terminal_output>`
132+
}
121133
}
122134
}
123135

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 & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,9 @@ Mention regex:
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').
2828
- `|`: Logical OR.
29-
- `problems\b`:
30-
- **Exact Word ('git-changes')**: Matches the exact word 'git-changes'.
31-
- **Word Boundary (`\b`)**: Ensures that 'git-changes' is matched as a whole word and not as part of another word.
32-
29+
- `terminal\b`:
30+
- **Exact Word ('terminal')**: Matches the exact word 'terminal'.
31+
- **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals').
3332
- `(?=[.,;:!?]?(?=[\s\r\n]|$))`:
3433
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
3534
- `[.,;:!?]?`:
@@ -43,14 +42,15 @@ Mention regex:
4342
- URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters).
4443
- The exact word 'problems'.
4544
- The exact word 'git-changes'.
45+
- The exact word 'terminal'.
4646
- 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.
4747
4848
- **Global Regex**:
4949
- `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string.
5050
5151
*/
5252
export const mentionRegex =
53-
/@((?:\/|\w+:\/\/)[^\s]+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
53+
/@((?:\/|\w+:\/\/)[^\s]+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
5454
export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
5555

5656
export interface MentionSuggestion {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
137137
const queryItems = useMemo(() => {
138138
return [
139139
{ type: ContextMenuOptionType.Problems, value: "problems" },
140+
{ type: ContextMenuOptionType.Terminal, value: "terminal" },
140141
...gitCommits,
141142
...openedTabs
142143
.filter((tab) => tab.path)
@@ -214,6 +215,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
214215
insertValue = value || ""
215216
} else if (type === ContextMenuOptionType.Problems) {
216217
insertValue = "problems"
218+
} else if (type === ContextMenuOptionType.Terminal) {
219+
insertValue = "terminal"
217220
} else if (type === ContextMenuOptionType.Git) {
218221
insertValue = value || ""
219222
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
7070
)
7171
case ContextMenuOptionType.Problems:
7272
return <span>Problems</span>
73+
case ContextMenuOptionType.Terminal:
74+
return <span>Terminal</span>
7375
case ContextMenuOptionType.URL:
7476
return <span>Paste URL to fetch contents</span>
7577
case ContextMenuOptionType.NoResults:
@@ -133,6 +135,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
133135
return "folder"
134136
case ContextMenuOptionType.Problems:
135137
return "warning"
138+
case ContextMenuOptionType.Terminal:
139+
return "terminal"
136140
case ContextMenuOptionType.URL:
137141
return "link"
138142
case ContextMenuOptionType.Git:
@@ -221,6 +225,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
221225
/>
222226
)}
223227
{(option.type === ContextMenuOptionType.Problems ||
228+
option.type === ContextMenuOptionType.Terminal ||
224229
((option.type === ContextMenuOptionType.File ||
225230
option.type === ContextMenuOptionType.Folder ||
226231
option.type === ContextMenuOptionType.OpenedFile ||

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ describe("getContextMenuOptions", () => {
7373

7474
it("should return all option types for empty query", () => {
7575
const result = getContextMenuOptions("", null, [])
76-
expect(result).toHaveLength(5)
76+
expect(result).toHaveLength(6)
7777
expect(result.map((item) => item.type)).toEqual([
7878
ContextMenuOptionType.Problems,
79+
ContextMenuOptionType.Terminal,
7980
ContextMenuOptionType.URL,
8081
ContextMenuOptionType.Folder,
8182
ContextMenuOptionType.File,

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export enum ContextMenuOptionType {
6161
File = "file",
6262
Folder = "folder",
6363
Problems = "problems",
64+
Terminal = "terminal",
6465
URL = "url",
6566
Git = "git",
6667
NoResults = "noResults",
@@ -151,6 +152,7 @@ export function getContextMenuOptions(
151152

152153
return [
153154
{ type: ContextMenuOptionType.Problems },
155+
{ type: ContextMenuOptionType.Terminal },
154156
{ type: ContextMenuOptionType.URL },
155157
{ type: ContextMenuOptionType.Folder },
156158
{ type: ContextMenuOptionType.File },
@@ -175,6 +177,9 @@ export function getContextMenuOptions(
175177
if ("problems".startsWith(lowerQuery)) {
176178
suggestions.push({ type: ContextMenuOptionType.Problems })
177179
}
180+
if ("terminal".startsWith(lowerQuery)) {
181+
suggestions.push({ type: ContextMenuOptionType.Terminal })
182+
}
178183
if (query.startsWith("http")) {
179184
suggestions.push({ type: ContextMenuOptionType.URL, value: query })
180185
}
@@ -266,8 +271,9 @@ export function shouldShowContextMenu(text: string, position: number): boolean {
266271
// Don't show the menu if it's a URL
267272
if (textAfterAt.toLowerCase().startsWith("http")) return false
268273

269-
// Don't show the menu if it's a problems
270-
if (textAfterAt.toLowerCase().startsWith("problems")) return false
274+
// Don't show the menu if it's a problems or terminal
275+
if (textAfterAt.toLowerCase().startsWith("problems") || textAfterAt.toLowerCase().startsWith("terminal"))
276+
return false
271277

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

0 commit comments

Comments
 (0)