From 584af643344b28ac878d2315797df1feb9dc1da7 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 14 Feb 2025 18:48:47 -0800 Subject: [PATCH] Add terminal context mention (#1805) * Add terminal context mention * Create fuzzy-moose-punch.md --- .changeset/fuzzy-moose-punch.md | 5 +++ src/core/mentions/index.ts | 12 +++++ .../terminal/get-latest-output.ts | 45 +++++++++++++++++++ src/shared/context-mentions.ts | 6 ++- .../src/components/chat/ChatTextArea.tsx | 3 ++ .../src/components/chat/ContextMenu.tsx | 5 +++ webview-ui/src/utils/context-mentions.ts | 6 ++- 7 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 .changeset/fuzzy-moose-punch.md create mode 100644 src/integrations/terminal/get-latest-output.ts diff --git a/.changeset/fuzzy-moose-punch.md b/.changeset/fuzzy-moose-punch.md new file mode 100644 index 0000000000..7034341375 --- /dev/null +++ b/.changeset/fuzzy-moose-punch.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Add terminal context mention diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index 1c9c122d1d..ea9e2afd79 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -7,6 +7,7 @@ import fs from "fs/promises" import { extractTextFromFile } from "../../integrations/misc/extract-text" import { isBinaryFile } from "isbinaryfile" import { diagnosticsToProblemsString } from "../../integrations/diagnostics" +import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output" export function openMention(mention?: string): void { if (!mention) { @@ -28,6 +29,8 @@ export function openMention(mention?: string): 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)) } @@ -46,6 +49,8 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher : `'${mentionPath}' (see below for file content)` } else if (mention === "problems") { return `Workspace Problems (see below for diagnostics)` + } else if (mention === "terminal") { + return `Terminal Output (see below for output)` } return match }) @@ -99,6 +104,13 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher } catch (error) { parsedText += `\n\n\nError fetching diagnostics: ${error.message}\n` } + } else if (mention === "terminal") { + try { + const terminalOutput = await getLatestTerminalOutput() + parsedText += `\n\n\n${terminalOutput}\n` + } catch (error) { + parsedText += `\n\n\nError fetching terminal output: ${error.message}\n` + } } } diff --git a/src/integrations/terminal/get-latest-output.ts b/src/integrations/terminal/get-latest-output.ts new file mode 100644 index 0000000000..0c869e7fad --- /dev/null +++ b/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 { + // 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) + } +} diff --git a/src/shared/context-mentions.ts b/src/shared/context-mentions.ts index 3912868b10..5444b903ef 100644 --- a/src/shared/context-mentions.ts +++ b/src/shared/context-mentions.ts @@ -25,6 +25,9 @@ Mention regex: - `problems\b`: - **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'). + - `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. @@ -38,11 +41,12 @@ Mention regex: - Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path). - URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters). - The exact word 'problems'. + - 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**: - `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string. */ -export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+?|problems\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/ +export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+?|problems\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/ export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g") diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index e86697405f..180332028b 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -243,6 +243,7 @@ const ChatTextArea = forwardRef( const queryItems = useMemo(() => { return [ { type: ContextMenuOptionType.Problems, value: "problems" }, + { type: ContextMenuOptionType.Terminal, value: "terminal" }, ...filePaths .map((file) => "/" + file) .map((path) => ({ @@ -293,6 +294,8 @@ const ChatTextArea = forwardRef( insertValue = value || "" } else if (type === ContextMenuOptionType.Problems) { insertValue = "problems" + } else if (type === ContextMenuOptionType.Terminal) { + insertValue = "terminal" } const { newValue, mentionIndex } = insertMention(textAreaRef.current.value, cursorPosition, insertValue) diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx index 25a7c0902f..8c0b28c513 100644 --- a/webview-ui/src/components/chat/ContextMenu.tsx +++ b/webview-ui/src/components/chat/ContextMenu.tsx @@ -48,6 +48,8 @@ const ContextMenu: React.FC = ({ switch (option.type) { case ContextMenuOptionType.Problems: return Problems + case ContextMenuOptionType.Terminal: + return Terminal case ContextMenuOptionType.URL: return Paste URL to fetch contents case ContextMenuOptionType.NoResults: @@ -85,6 +87,8 @@ const ContextMenu: React.FC = ({ return "folder" case ContextMenuOptionType.Problems: return "warning" + case ContextMenuOptionType.Terminal: + return "terminal" case ContextMenuOptionType.URL: return "link" case ContextMenuOptionType.NoResults: @@ -173,6 +177,7 @@ const ContextMenu: React.FC = ({ /> )} {(option.type === ContextMenuOptionType.Problems || + option.type === ContextMenuOptionType.Terminal || ((option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) && option.value)) && (