Преглед на файлове

Merge branch 'RooVetGit:main' into mistral

Dominik Oswald преди 10 месеца
родител
ревизия
e9f6cb4f75

+ 0 - 5
.changeset/clever-news-arrive.md

@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Disable writing in ask mode

+ 8 - 0
CHANGELOG.md

@@ -1,5 +1,13 @@
 # Roo Code Changelog
 # Roo Code Changelog
 
 
+## [3.3.20]
+
+- Support project-specific custom modes in a .roomodes file
+- Add more Mistral models (thanks @d-oit and @bramburn!)
+- By popular request, make it so Ask mode can't write to Markdown files and is purely for chatting with
+- Add a setting to control the number of open editor tabs to tell the model about (665 is probably too many!)
+- Fix race condition bug with entering API key on the welcome screen
+
 ## [3.3.19]
 ## [3.3.19]
 
 
 - Fix a bug where aborting in the middle of file writes would not revert the write
 - Fix a bug where aborting in the middle of file writes would not revert the write

+ 2 - 0
README.md

@@ -15,6 +15,8 @@
 <a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline" target="_blank"><img src="https://img.shields.io/badge/Download%20on%20VS%20Marketplace-blue?style=for-the-badge&logo=visualstudiocode&logoColor=white" alt="Download on VS Marketplace"></a>
 <a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline" target="_blank"><img src="https://img.shields.io/badge/Download%20on%20VS%20Marketplace-blue?style=for-the-badge&logo=visualstudiocode&logoColor=white" alt="Download on VS Marketplace"></a>
 <a href="https://github.com/RooVetGit/Roo-Code/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><img src="https://img.shields.io/badge/Feature%20Requests-yellow?style=for-the-badge" alt="Feature Requests"></a>
 <a href="https://github.com/RooVetGit/Roo-Code/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><img src="https://img.shields.io/badge/Feature%20Requests-yellow?style=for-the-badge" alt="Feature Requests"></a>
 <a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline&ssr=false#review-details" target="_blank"><img src="https://img.shields.io/badge/Rate%20%26%20Review-green?style=for-the-badge" alt="Rate & Review"></a>
 <a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline&ssr=false#review-details" target="_blank"><img src="https://img.shields.io/badge/Rate%20%26%20Review-green?style=for-the-badge" alt="Rate & Review"></a>
+<a href="https://docs.roocode.com" target="_blank"><img src="https://img.shields.io/badge/Documentation-6B46C1?style=for-the-badge&logo=readthedocs&logoColor=white" alt="Documentation"></a>
+
 </div>
 </div>
 
 
 **Roo Code** is an AI-powered **autonomous coding agent** that lives in your editor. It can:
 **Roo Code** is an AI-powered **autonomous coding agent** that lives in your editor. It can:

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
 {
 	"name": "roo-cline",
 	"name": "roo-cline",
-	"version": "3.3.19",
+	"version": "3.3.20",
 	"lockfileVersion": 3,
 	"lockfileVersion": 3,
 	"requires": true,
 	"requires": true,
 	"packages": {
 	"packages": {
 		"": {
 		"": {
 			"name": "roo-cline",
 			"name": "roo-cline",
-			"version": "3.3.19",
+			"version": "3.3.20",
 			"dependencies": {
 			"dependencies": {
 				"@anthropic-ai/bedrock-sdk": "^0.10.2",
 				"@anthropic-ai/bedrock-sdk": "^0.10.2",
 				"@anthropic-ai/sdk": "^0.26.0",
 				"@anthropic-ai/sdk": "^0.26.0",

+ 2 - 2
package.json

@@ -1,9 +1,9 @@
 {
 {
 	"name": "roo-cline",
 	"name": "roo-cline",
 	"displayName": "Roo Code (prev. Roo Cline)",
 	"displayName": "Roo Code (prev. Roo Cline)",
-	"description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.",
+	"description": "An AI-powered autonomous coding agent that lives in your editor.",
 	"publisher": "RooVeterinaryInc",
 	"publisher": "RooVeterinaryInc",
-	"version": "3.3.19",
+	"version": "3.3.20",
 	"icon": "assets/icons/rocket.png",
 	"icon": "assets/icons/rocket.png",
 	"galleryBanner": {
 	"galleryBanner": {
 		"color": "#617A91",
 		"color": "#617A91",

+ 17 - 9
src/api/providers/__tests__/openai-native.test.ts

@@ -130,11 +130,20 @@ describe("OpenAiNativeHandler", () => {
 			})
 			})
 
 
 			mockCreate.mockResolvedValueOnce({
 			mockCreate.mockResolvedValueOnce({
-				choices: [{ message: { content: null } }],
-				usage: {
-					prompt_tokens: 0,
-					completion_tokens: 0,
-					total_tokens: 0,
+				[Symbol.asyncIterator]: async function* () {
+					yield {
+						choices: [
+							{
+								delta: { content: null },
+								index: 0,
+							},
+						],
+						usage: {
+							prompt_tokens: 0,
+							completion_tokens: 0,
+							total_tokens: 0,
+						},
+					}
 				},
 				},
 			})
 			})
 
 
@@ -144,10 +153,7 @@ describe("OpenAiNativeHandler", () => {
 				results.push(result)
 				results.push(result)
 			}
 			}
 
 
-			expect(results).toEqual([
-				{ type: "text", text: "" },
-				{ type: "usage", inputTokens: 0, outputTokens: 0 },
-			])
+			expect(results).toEqual([{ type: "usage", inputTokens: 0, outputTokens: 0 }])
 
 
 			// Verify developer role is used for system prompt with o1 model
 			// Verify developer role is used for system prompt with o1 model
 			expect(mockCreate).toHaveBeenCalledWith({
 			expect(mockCreate).toHaveBeenCalledWith({
@@ -156,6 +162,8 @@ describe("OpenAiNativeHandler", () => {
 					{ role: "developer", content: "Formatting re-enabled\n" + systemPrompt },
 					{ role: "developer", content: "Formatting re-enabled\n" + systemPrompt },
 					{ role: "user", content: "Hello!" },
 					{ role: "user", content: "Hello!" },
 				],
 				],
+				stream: true,
+				stream_options: { include_usage: true },
 			})
 			})
 		})
 		})
 
 

+ 3 - 1
src/api/providers/openai-native.ts

@@ -56,9 +56,11 @@ export class OpenAiNativeHandler implements ApiHandler, SingleCompletionHandler
 				},
 				},
 				...convertToOpenAiMessages(messages),
 				...convertToOpenAiMessages(messages),
 			],
 			],
+			stream: true,
+			stream_options: { include_usage: true },
 		})
 		})
 
 
-		yield* this.yieldResponseData(response)
+		yield* this.handleStreamResponse(response)
 	}
 	}
 
 
 	private async *handleO3FamilyMessage(
 	private async *handleO3FamilyMessage(

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

@@ -8,6 +8,7 @@ import { extractTextFromFile } from "../../integrations/misc/extract-text"
 import { isBinaryFile } from "isbinaryfile"
 import { isBinaryFile } from "isbinaryfile"
 import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
 import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
 import { getCommitInfo, getWorkingState } from "../../utils/git"
 import { getCommitInfo, getWorkingState } from "../../utils/git"
+import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
 
 
 export async function openMention(mention?: string): Promise<void> {
 export async function openMention(mention?: string): Promise<void> {
 	if (!mention) {
 	if (!mention) {
@@ -29,6 +30,8 @@ export async function openMention(mention?: string): Promise<void> {
 		}
 		}
 	} else if (mention === "problems") {
 	} else if (mention === "problems") {
 		vscode.commands.executeCommand("workbench.actions.view.problems")
 		vscode.commands.executeCommand("workbench.actions.view.problems")
+	} else if (mention === "terminal") {
+		vscode.commands.executeCommand("workbench.action.terminal.focus")
 	} else if (mention.startsWith("http")) {
 	} else if (mention.startsWith("http")) {
 		vscode.env.openExternal(vscode.Uri.parse(mention))
 		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)`
 			return `Working directory changes (see below for details)`
 		} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
 		} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
 			return `Git commit '${mention}' (see below for commit info)`
 			return `Git commit '${mention}' (see below for commit info)`
+		} else if (mention === "terminal") {
+			return `Terminal Output (see below for output)`
 		}
 		}
 		return match
 		return match
 	})
 	})
@@ -118,6 +123,13 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
 			} catch (error) {
 			} catch (error) {
 				parsedText += `\n\n<git_commit hash="${mention}">\nError fetching commit info: ${error.message}\n</git_commit>`
 				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'.
 	  - **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').
 	  - **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic').
 		- `|`: Logical OR.
 		- `|`: 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]|$))`:
   - `(?=[.,;:!?]?(?=[\s\r\n]|$))`:
 	- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
 	- **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).
 	- URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters).
 	- The exact word 'problems'.
 	- The exact word 'problems'.
 	- The exact word 'git-changes'.
 	- 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.
   - 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**:
 - **Global Regex**:
@@ -50,7 +50,7 @@ Mention regex:
 
 
 */
 */
 export const mentionRegex =
 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 const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
 
 
 export interface MentionSuggestion {
 export interface MentionSuggestion {

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

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

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

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

+ 7 - 1
webview-ui/src/components/welcome/WelcomeView.tsx

@@ -10,7 +10,13 @@ const WelcomeView = () => {
 
 
 	const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
 	const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
 
 
-	const handleSubmit = () => {
+	const handleSubmit = async () => {
+		// Focus the active element's parent to trigger blur
+		document.activeElement?.parentElement?.focus()
+
+		// Small delay to let blur events complete
+		await new Promise((resolve) => setTimeout(resolve, 50))
+
 		const error = validateApiConfiguration(apiConfiguration)
 		const error = validateApiConfiguration(apiConfiguration)
 		if (error) {
 		if (error) {
 			setErrorMessage(error)
 			setErrorMessage(error)

+ 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", () => {
 	it("should return all option types for empty query", () => {
 		const result = getContextMenuOptions("", null, [])
 		const result = getContextMenuOptions("", null, [])
-		expect(result).toHaveLength(5)
+		expect(result).toHaveLength(6)
 		expect(result.map((item) => item.type)).toEqual([
 		expect(result.map((item) => item.type)).toEqual([
 			ContextMenuOptionType.Problems,
 			ContextMenuOptionType.Problems,
+			ContextMenuOptionType.Terminal,
 			ContextMenuOptionType.URL,
 			ContextMenuOptionType.URL,
 			ContextMenuOptionType.Folder,
 			ContextMenuOptionType.Folder,
 			ContextMenuOptionType.File,
 			ContextMenuOptionType.File,

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

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