Browse Source

Merge pull request #1006 from RooVetGit/terminal_context_mention

Terminal context mention
Matt Rubens 10 months ago
parent
commit
ffec4c60d4

+ 12 - 0
src/core/mentions/index.ts

@@ -8,6 +8,7 @@ import { extractTextFromFile } from "../../integrations/misc/extract-text"
 import { isBinaryFile } from "isbinaryfile"
 import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
 import { getCommitInfo, getWorkingState } from "../../utils/git"
+import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
 
 export async function openMention(mention?: string): Promise<void> {
 	if (!mention) {
@@ -29,6 +30,8 @@ export async function openMention(mention?: string): Promise<void> {
 		}
 	} else if (mention === "problems") {
 		vscode.commands.executeCommand("workbench.actions.view.problems")
+	} else if (mention === "terminal") {
+		vscode.commands.executeCommand("workbench.action.terminal.focus")
 	} else if (mention.startsWith("http")) {
 		vscode.env.openExternal(vscode.Uri.parse(mention))
 	}
@@ -51,6 +54,8 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
 			return `Working directory changes (see below for details)`
 		} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
 			return `Git commit '${mention}' (see below for commit info)`
+		} else if (mention === "terminal") {
+			return `Terminal Output (see below for output)`
 		}
 		return match
 	})
@@ -118,6 +123,13 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
 			} catch (error) {
 				parsedText += `\n\n<git_commit hash="${mention}">\nError fetching commit info: ${error.message}\n</git_commit>`
 			}
+		} else if (mention === "terminal") {
+			try {
+				const terminalOutput = await getLatestTerminalOutput()
+				parsedText += `\n\n<terminal_output>\n${terminalOutput}\n</terminal_output>`
+			} catch (error) {
+				parsedText += `\n\n<terminal_output>\nError fetching terminal output: ${error.message}\n</terminal_output>`
+			}
 		}
 	}
 

+ 45 - 0
src/integrations/terminal/get-latest-output.ts

@@ -0,0 +1,45 @@
+import * as vscode from "vscode"
+
+/**
+ * Gets the contents of the active terminal
+ * @returns The terminal contents as a string
+ */
+export async function getLatestTerminalOutput(): Promise<string> {
+	// Store original clipboard content to restore later
+	const originalClipboard = await vscode.env.clipboard.readText()
+
+	try {
+		// Select terminal content
+		await vscode.commands.executeCommand("workbench.action.terminal.selectAll")
+
+		// Copy selection to clipboard
+		await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
+
+		// Clear the selection
+		await vscode.commands.executeCommand("workbench.action.terminal.clearSelection")
+
+		// Get terminal contents from clipboard
+		let terminalContents = (await vscode.env.clipboard.readText()).trim()
+
+		// Check if there's actually a terminal open
+		if (terminalContents === originalClipboard) {
+			return ""
+		}
+
+		// Clean up command separation
+		const lines = terminalContents.split("\n")
+		const lastLine = lines.pop()?.trim()
+		if (lastLine) {
+			let i = lines.length - 1
+			while (i >= 0 && !lines[i].trim().startsWith(lastLine)) {
+				i--
+			}
+			terminalContents = lines.slice(Math.max(i, 0)).join("\n")
+		}
+
+		return terminalContents
+	} finally {
+		// Restore original clipboard content
+		await vscode.env.clipboard.writeText(originalClipboard)
+	}
+}

+ 5 - 5
src/shared/context-mentions.ts

@@ -26,10 +26,9 @@ Mention regex:
 	  - **Exact Word ('problems')**: Matches the exact word 'problems'.
 	  - **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic').
 		- `|`: Logical OR.
-	- `problems\b`: 
-	  - **Exact Word ('git-changes')**: Matches the exact word 'git-changes'.
-	  - **Word Boundary (`\b`)**: Ensures that 'git-changes' is matched as a whole word and not as part of another word.  
-
+    - `terminal\b`:
+      - **Exact Word ('terminal')**: Matches the exact word 'terminal'.
+      - **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals').
   - `(?=[.,;:!?]?(?=[\s\r\n]|$))`:
 	- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
 	- `[.,;:!?]?`: 
@@ -43,6 +42,7 @@ Mention regex:
 	- URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters).
 	- The exact word 'problems'.
 	- The exact word 'git-changes'.
+    - The exact word 'terminal'.
   - 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.
 
 - **Global Regex**:
@@ -50,7 +50,7 @@ Mention regex:
 
 */
 export const mentionRegex =
-	/@((?:\/|\w+:\/\/)[^\s]+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
+	/@((?:\/|\w+:\/\/)[^\s]+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
 export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
 
 export interface MentionSuggestion {

+ 3 - 0
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -137,6 +137,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 		const queryItems = useMemo(() => {
 			return [
 				{ type: ContextMenuOptionType.Problems, value: "problems" },
+				{ type: ContextMenuOptionType.Terminal, value: "terminal" },
 				...gitCommits,
 				...openedTabs
 					.filter((tab) => tab.path)
@@ -214,6 +215,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						insertValue = value || ""
 					} else if (type === ContextMenuOptionType.Problems) {
 						insertValue = "problems"
+					} else if (type === ContextMenuOptionType.Terminal) {
+						insertValue = "terminal"
 					} else if (type === ContextMenuOptionType.Git) {
 						insertValue = value || ""
 					}

+ 5 - 0
webview-ui/src/components/chat/ContextMenu.tsx

@@ -70,6 +70,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
 				)
 			case ContextMenuOptionType.Problems:
 				return <span>Problems</span>
+			case ContextMenuOptionType.Terminal:
+				return <span>Terminal</span>
 			case ContextMenuOptionType.URL:
 				return <span>Paste URL to fetch contents</span>
 			case ContextMenuOptionType.NoResults:
@@ -133,6 +135,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
 				return "folder"
 			case ContextMenuOptionType.Problems:
 				return "warning"
+			case ContextMenuOptionType.Terminal:
+				return "terminal"
 			case ContextMenuOptionType.URL:
 				return "link"
 			case ContextMenuOptionType.Git:
@@ -221,6 +225,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
 								/>
 							)}
 						{(option.type === ContextMenuOptionType.Problems ||
+							option.type === ContextMenuOptionType.Terminal ||
 							((option.type === ContextMenuOptionType.File ||
 								option.type === ContextMenuOptionType.Folder ||
 								option.type === ContextMenuOptionType.OpenedFile ||

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

@@ -73,9 +73,10 @@ describe("getContextMenuOptions", () => {
 
 	it("should return all option types for empty query", () => {
 		const result = getContextMenuOptions("", null, [])
-		expect(result).toHaveLength(5)
+		expect(result).toHaveLength(6)
 		expect(result.map((item) => item.type)).toEqual([
 			ContextMenuOptionType.Problems,
+			ContextMenuOptionType.Terminal,
 			ContextMenuOptionType.URL,
 			ContextMenuOptionType.Folder,
 			ContextMenuOptionType.File,

+ 8 - 2
webview-ui/src/utils/context-mentions.ts

@@ -61,6 +61,7 @@ export enum ContextMenuOptionType {
 	File = "file",
 	Folder = "folder",
 	Problems = "problems",
+	Terminal = "terminal",
 	URL = "url",
 	Git = "git",
 	NoResults = "noResults",
@@ -151,6 +152,7 @@ export function getContextMenuOptions(
 
 		return [
 			{ type: ContextMenuOptionType.Problems },
+			{ type: ContextMenuOptionType.Terminal },
 			{ type: ContextMenuOptionType.URL },
 			{ type: ContextMenuOptionType.Folder },
 			{ type: ContextMenuOptionType.File },
@@ -175,6 +177,9 @@ export function getContextMenuOptions(
 	if ("problems".startsWith(lowerQuery)) {
 		suggestions.push({ type: ContextMenuOptionType.Problems })
 	}
+	if ("terminal".startsWith(lowerQuery)) {
+		suggestions.push({ type: ContextMenuOptionType.Terminal })
+	}
 	if (query.startsWith("http")) {
 		suggestions.push({ type: ContextMenuOptionType.URL, value: query })
 	}
@@ -266,8 +271,9 @@ export function shouldShowContextMenu(text: string, position: number): boolean {
 	// Don't show the menu if it's a URL
 	if (textAfterAt.toLowerCase().startsWith("http")) return false
 
-	// Don't show the menu if it's a problems
-	if (textAfterAt.toLowerCase().startsWith("problems")) return false
+	// Don't show the menu if it's a problems or terminal
+	if (textAfterAt.toLowerCase().startsWith("problems") || textAfterAt.toLowerCase().startsWith("terminal"))
+		return false
 
 	// NOTE: it's okay that menu shows when there's trailing punctuation since user could be inputting a path with marks