Add terminal context mention

This commit is contained in:
Saoud Rizwan 2025-02-14 18:48:47 -08:00 committed by Matt Rubens
parent 7eada8bb6d
commit 17157c46cb
6 changed files with 78 additions and 7 deletions

View file

@ -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>`
}
}
}

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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 || ""
}

View file

@ -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 ||

View file

@ -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