diff --git a/.changeset/clever-news-arrive.md b/.changeset/clever-news-arrive.md
deleted file mode 100644
index d490036adb..0000000000
--- a/.changeset/clever-news-arrive.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Disable writing in ask mode
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39ce0a73f0..e0812c66bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# 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]
- Fix a bug where aborting in the middle of file writes would not revert the write
diff --git a/README.md b/README.md
index 62338b4390..53f29e0725 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,8 @@
+
+
**Roo Code** is an AI-powered **autonomous coding agent** that lives in your editor. It can:
diff --git a/package-lock.json b/package-lock.json
index 8af12ad90c..f0d36579d6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.3.19",
+ "version": "3.3.20",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.3.19",
+ "version": "3.3.20",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0",
diff --git a/package.json b/package.json
index 8795b54d37..c43cf05b11 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,9 @@
{
"name": "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",
- "version": "3.3.19",
+ "version": "3.3.20",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
diff --git a/src/api/providers/__tests__/openai-native.test.ts b/src/api/providers/__tests__/openai-native.test.ts
index f39d49307f..d6a855849c 100644
--- a/src/api/providers/__tests__/openai-native.test.ts
+++ b/src/api/providers/__tests__/openai-native.test.ts
@@ -130,11 +130,20 @@ describe("OpenAiNativeHandler", () => {
})
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)
}
- 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
expect(mockCreate).toHaveBeenCalledWith({
@@ -156,6 +162,8 @@ describe("OpenAiNativeHandler", () => {
{ role: "developer", content: "Formatting re-enabled\n" + systemPrompt },
{ role: "user", content: "Hello!" },
],
+ stream: true,
+ stream_options: { include_usage: true },
})
})
diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts
index 1a4f9e613a..8feeafdb96 100644
--- a/src/api/providers/openai-native.ts
+++ b/src/api/providers/openai-native.ts
@@ -56,9 +56,11 @@ export class OpenAiNativeHandler implements ApiHandler, SingleCompletionHandler
},
...convertToOpenAiMessages(messages),
],
+ stream: true,
+ stream_options: { include_usage: true },
})
- yield* this.yieldResponseData(response)
+ yield* this.handleStreamResponse(response)
}
private async *handleO3FamilyMessage(
diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts
index cf5bdeaae0..5853a4abdd 100644
--- a/src/core/mentions/index.ts
+++ b/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 {
if (!mention) {
@@ -29,6 +30,8 @@ export async function openMention(mention?: string): Promise {
}
} 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\nError fetching commit info: ${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 10375ff23e..915114ab93 100644
--- a/src/shared/context-mentions.ts
+++ b/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 {
diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx
index 98d57965b7..d317efb96e 100644
--- a/webview-ui/src/components/chat/ChatTextArea.tsx
+++ b/webview-ui/src/components/chat/ChatTextArea.tsx
@@ -137,6 +137,7 @@ const ChatTextArea = forwardRef(
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(
insertValue = value || ""
} else if (type === ContextMenuOptionType.Problems) {
insertValue = "problems"
+ } else if (type === ContextMenuOptionType.Terminal) {
+ insertValue = "terminal"
} else if (type === ContextMenuOptionType.Git) {
insertValue = value || ""
}
diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx
index bd631a98d5..2bb7a8ee68 100644
--- a/webview-ui/src/components/chat/ContextMenu.tsx
+++ b/webview-ui/src/components/chat/ContextMenu.tsx
@@ -70,6 +70,8 @@ const ContextMenu: React.FC = ({
)
case ContextMenuOptionType.Problems:
return Problems
+ case ContextMenuOptionType.Terminal:
+ return Terminal
case ContextMenuOptionType.URL:
return Paste URL to fetch contents
case ContextMenuOptionType.NoResults:
@@ -133,6 +135,8 @@ const ContextMenu: React.FC = ({
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 = ({
/>
)}
{(option.type === ContextMenuOptionType.Problems ||
+ option.type === ContextMenuOptionType.Terminal ||
((option.type === ContextMenuOptionType.File ||
option.type === ContextMenuOptionType.Folder ||
option.type === ContextMenuOptionType.OpenedFile ||
diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx
index db52612b90..6513e4b10b 100644
--- a/webview-ui/src/components/welcome/WelcomeView.tsx
+++ b/webview-ui/src/components/welcome/WelcomeView.tsx
@@ -10,7 +10,13 @@ const WelcomeView = () => {
const [errorMessage, setErrorMessage] = useState(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)
if (error) {
setErrorMessage(error)
diff --git a/webview-ui/src/utils/__tests__/context-mentions.test.ts b/webview-ui/src/utils/__tests__/context-mentions.test.ts
index d1cf54181f..ee40a9f724 100644
--- a/webview-ui/src/utils/__tests__/context-mentions.test.ts
+++ b/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,
diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts
index 0fb57071ab..600a2f760e 100644
--- a/webview-ui/src/utils/context-mentions.ts
+++ b/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