diff --git a/.changeset/angry-lions-sneeze.md b/.changeset/angry-lions-sneeze.md
deleted file mode 100644
index 091739140b..0000000000
--- a/.changeset/angry-lions-sneeze.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"claude-dev": patch
----
-
-Add translation to CODE_OF_CONDUCT, CONTRIBUTING and README to portuguese pt-BR.
diff --git a/.changeset/fair-snails-confess.md b/.changeset/fair-snails-confess.md
new file mode 100644
index 0000000000..f54c839141
--- /dev/null
+++ b/.changeset/fair-snails-confess.md
@@ -0,0 +1,10 @@
+---
+"claude-dev": patch
+---
+
+Improve Requesty provider integration
+
+- Adding Cline headers to API requests, to enable targeted optimizations
+- Read o3 reasoning effort from Cline config, not model name
+- Show token information in task header
+- Get total cost from response when available
diff --git a/.changeset/long-masks-notice.md b/.changeset/long-masks-notice.md
deleted file mode 100644
index 76079fbed6..0000000000
--- a/.changeset/long-masks-notice.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"claude-dev": minor
----
-
-Advanced Setting to disable browser tool
diff --git a/.changeset/orange-eels-unite.md b/.changeset/orange-eels-unite.md
deleted file mode 100644
index e0eab5d394..0000000000
--- a/.changeset/orange-eels-unite.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"claude-dev": patch
----
-
-Fix AWS Bedrock Profiles. When configuring the AnthropicBedrock Client you must pass AWS credentials in a specific way, otherwise the client will default to reading credentials from the default AWS profile.
diff --git a/.changeset/poor-cobras-destroy.md b/.changeset/poor-cobras-destroy.md
deleted file mode 100644
index 991c7324cc..0000000000
--- a/.changeset/poor-cobras-destroy.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"claude-dev": minor
----
-
-Add extension setting for chromium executable path
diff --git a/.changeset/silly-cats-appear.md b/.changeset/silly-cats-appear.md
deleted file mode 100644
index 6e6925a4c9..0000000000
--- a/.changeset/silly-cats-appear.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"claude-dev": minor
----
-
-Add new ability to send message that is in Input field during Plan/Act Mode Change to Act.
diff --git a/.changeset/slimy-toes-wait.md b/.changeset/slimy-toes-wait.md
deleted file mode 100644
index 059978604f..0000000000
--- a/.changeset/slimy-toes-wait.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"claude-dev": minor
----
-
-Added api key field for litellm api provider in settings
diff --git a/.changeset/stale-lizards-poke.md b/.changeset/stale-lizards-poke.md
deleted file mode 100644
index 40e3308e8b..0000000000
--- a/.changeset/stale-lizards-poke.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"claude-dev": minor
----
-
-qwen platform adds deepseek-r1/v3 support
diff --git a/.changeset/ten-books-act.md b/.changeset/ten-books-act.md
deleted file mode 100644
index f37fcf0e9a..0000000000
--- a/.changeset/ten-books-act.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"claude-dev": minor
----
-
-Adding .clineignore guide
diff --git a/.changeset/thirty-eyes-appear.md b/.changeset/thirty-eyes-appear.md
deleted file mode 100644
index 2cfb8405d6..0000000000
--- a/.changeset/thirty-eyes-appear.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"claude-dev": minor
----
-
-Advanced Configuration for OpenAI Compatible Providers
diff --git a/.changeset/wise-phones-mate.md b/.changeset/wise-phones-mate.md
deleted file mode 100644
index 7be57b25e9..0000000000
--- a/.changeset/wise-phones-mate.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"claude-dev": minor
----
-
-Allowing the user to give feedback when approving a tool use.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1146e374ae..a1eaebc759 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,21 @@
# Changelog
+## [3.4.0]
+
+- Send current textfield contents as additional feedback when toggling from Plan to Act Mode, or when hitting 'Approve' button
+- Add 'Terminal' context mention to reference the active terminal's contents
+- Add 'Git Commits' context mention to reference current working changes or specific commits (thanks @mrubens!)
+- Add advanced configuration options for OpenAI Compatible (context window, max output, pricing, etc.)
+- Add Alibaba Qwen 2.5 coder models, VL models, and DeepSeek-R1/V3 support
+- Improve support for AWS Bedrock Profiles
+- Fix Mistral provider support for non-codestral models
+- Add advanced setting to disable browser tool
+- Add advanced setting to set chromium executable path for browser tool
+
## [3.3.2]
- Fix bug where OpenRouter requests would periodically not return cost/token stats, leading to context window limit errors
-- Make checkpoints more visible and keep track of restored checkpoints
+- Make checkpoints more visible and keep track of restored checkpoints
## [3.3.0]
@@ -34,7 +46,7 @@
## [3.2.10]
-- Improve support for DeepSeek-R1 (deepseek-reasoner) model for OpenRouter, OpenAI-compatible, and DeepSeek direct
+- Improve support for DeepSeek-R1 (deepseek-reasoner) model for OpenRouter, OpenAI-compatible, and DeepSeek direct (thanks @Szpadel!)
- Show Reasoning tokens for models that support it
- Fix issues with switching models between Plan/Act modes
diff --git a/README.md b/README.md
index f1c48eebfd..4f48c5ad41 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# Cline – \#1 on OpenRouter
diff --git a/package-lock.json b/package-lock.json
index 7a4b80d77b..d89b7dc8dd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,19 @@
{
"name": "claude-dev",
- "version": "3.3.2",
+ "version": "3.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
- "version": "3.3.2",
+ "version": "3.4.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0",
"@anthropic-ai/vertex-sdk": "^0.4.1",
"@google/generative-ai": "^0.18.0",
- "@mistralai/mistralai": "^1.3.6",
+ "@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.0.1",
"@types/clone-deep": "^4.0.4",
"@types/get-folder-size": "^3.0.4",
@@ -4098,13 +4098,25 @@
}
},
"node_modules/@mistralai/mistralai": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.4.0.tgz",
- "integrity": "sha512-xA3DAtIDh4Qgr1EoSuiGVE+2ABNrxpcTeC0kSXYbkDNUGdthalLAH7DgbG0fkKZ7TN8xdWXQq2WiIghp/O96Eg==",
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.0.tgz",
+ "integrity": "sha512-AIn8pwAwA/fDvEUvmkt+40zH1ZmfaG3Q7oUWl17GUEC1tU7ZPwYz8Cv9P59lyS1SisHdDSu81oknO7f1ywkz8Q==",
+ "dependencies": {
+ "zod-to-json-schema": "^3.24.1"
+ },
"peerDependencies": {
"zod": ">= 3"
}
},
+ "node_modules/@mistralai/mistralai/node_modules/zod-to-json-schema": {
+ "version": "3.24.1",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
+ "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.24.1"
+ }
+ },
"node_modules/@mixmark-io/domino": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
diff --git a/package.json b/package.json
index 0c9e7e41b0..5decc223d0 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
- "version": "3.3.2",
+ "version": "3.4.0",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
@@ -236,7 +236,7 @@
"@anthropic-ai/sdk": "^0.26.0",
"@anthropic-ai/vertex-sdk": "^0.4.1",
"@google/generative-ai": "^0.18.0",
- "@mistralai/mistralai": "^1.3.6",
+ "@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.0.1",
"@types/clone-deep": "^4.0.4",
"@types/get-folder-size": "^3.0.4",
diff --git a/src/api/providers/mistral.ts b/src/api/providers/mistral.ts
index 28c2331c60..4be68e2e5e 100644
--- a/src/api/providers/mistral.ts
+++ b/src/api/providers/mistral.ts
@@ -22,7 +22,6 @@ export class MistralHandler implements ApiHandler {
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new Mistral({
- serverURL: "https://api.mistral.ai",
apiKey: this.options.mistralApiKey,
})
}
diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts
index 34dd2b4d67..218d4785ad 100644
--- a/src/api/providers/requesty.ts
+++ b/src/api/providers/requesty.ts
@@ -5,7 +5,6 @@ import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../..
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
-import { convertToR1Format } from "../transform/r1-format"
export class RequestyHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -16,30 +15,32 @@ export class RequestyHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://router.requesty.ai/v1",
apiKey: this.options.requestyApiKey,
+ defaultHeaders: {
+ "HTTP-Referer": "https://cline.bot",
+ "X-Title": "Cline",
+ },
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.options.requestyModelId ?? ""
- const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
- if (isDeepseekReasoner) {
- openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
- }
-
+ // @ts-ignore-next-line
const stream = await this.client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature: 0,
stream: true,
stream_options: { include_usage: true },
+ ...(modelId === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
})
+
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
@@ -56,11 +57,25 @@ export class RequestyHandler implements ApiHandler {
}
}
+ // Requesty usage includes an extra field for Anthropic use cases.
+ // Safely cast the prompt token details section to the appropriate structure.
+ interface RequestyUsage extends OpenAI.CompletionUsage {
+ prompt_tokens_details?: {
+ caching_tokens?: number
+ cached_tokens?: number
+ }
+ total_cost?: number
+ }
+
if (chunk.usage) {
+ const usage = chunk.usage as RequestyUsage
yield {
type: "usage",
- inputTokens: chunk.usage.prompt_tokens || 0,
- outputTokens: chunk.usage.completion_tokens || 0,
+ inputTokens: usage.prompt_tokens || 0,
+ outputTokens: usage.completion_tokens || 0,
+ cacheWriteTokens: usage.prompt_tokens_details?.caching_tokens || undefined,
+ cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined,
+ totalCost: usage.total_cost || undefined,
}
}
}
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 1e9ea6bbb4..acecbd4fe6 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -1539,33 +1539,44 @@ export class Cline {
this.didAlreadyUseTool = true
}
+ // The user can approve, reject, or provide feedback (rejection). However the user may also send a message along with an approval, in which case we add a separate user message with this feedback.
+ const pushAdditionalToolFeedback = (feedback?: string, images?: string[]) => {
+ if (!feedback && !images) {
+ return
+ }
+ const content = formatResponse.toolResult(
+ `The user provided the following feedback:\n\n${feedback}\n`,
+ images,
+ )
+ if (typeof content === "string") {
+ this.userMessageContent.push({
+ type: "text",
+ text: content,
+ })
+ } else {
+ this.userMessageContent.push(...content)
+ }
+ }
+
const askApproval = async (type: ClineAsk, partialMessage?: string) => {
const { response, text, images } = await this.ask(type, partialMessage, false)
if (response !== "yesButtonClicked") {
- // User did NOT approve (rejected)
- if (response === "messageResponse") {
- // Rejection WITH feedback
- await this.say("user_feedback", text, images)
- pushToolResult(formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images))
-
- this.didRejectTool = true
- return false
- }
- // Rejection WITHOUT explicit feedback
+ // User pressed reject button or responded with a message, which we treat as a rejection
pushToolResult(formatResponse.toolDenied())
-
+ if (text || images?.length) {
+ pushAdditionalToolFeedback(text, images)
+ await this.say("user_feedback", text, images)
+ }
this.didRejectTool = true // Prevent further tool uses in this message
return false
+ } else {
+ // User hit the approve button, and may have provided feedback
+ if (text || images?.length) {
+ pushAdditionalToolFeedback(text, images)
+ await this.say("user_feedback", text, images)
+ }
+ return true
}
-
- // Handle yesButtonClicked with text (Acceptance WITH feedback)
- if (text) {
- await this.say("user_feedback", text, images)
- pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images)) // Structured feedback to model on approval
- }
-
- // User approved without feedback
- return true
}
const showNotificationForApprovalIfAutoApprovalEnabled = (message: string) => {
@@ -1804,37 +1815,23 @@ export class Cline {
let didApprove = true
const { response, text, images } = await this.ask("tool", completeMessage, false)
if (response !== "yesButtonClicked") {
- // User did NOT approve (rejected)
-
+ // User either sent a message or pressed reject button
// TODO: add similar context for other tool denial responses, to emphasize ie that a command was not run
const fileDeniedNote = fileExists
? "The file was not updated, and maintains its original contents."
: "The file was not created."
- if (response === "messageResponse") {
- // Rejection WITH feedback
+ pushToolResult(`The user denied this operation. ${fileDeniedNote}`)
+ if (text || images?.length) {
+ pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
- pushToolResult(
- formatResponse.toolResult(
- `The user denied this operation. ${fileDeniedNote}\nThe user provided the following feedback:\n\n${text}\n`,
- images,
- ),
- )
- this.didRejectTool = true
- didApprove = false
- } else {
- pushToolResult(`The user denied this operation. ${fileDeniedNote}`)
- this.didRejectTool = true
- didApprove = false
}
+ this.didRejectTool = true
+ didApprove = false
} else {
- // User approved
-
- // Handle yesButtonClicked with text (Acceptance WITH feedback)
- if (text) {
+ // User hit the approve button, and may have provided feedback
+ if (text || images?.length) {
+ pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
- pushToolResult(
- formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images),
- )
}
}
@@ -2712,13 +2709,15 @@ export class Cline {
// }
this.isAwaitingPlanResponse = true
- const { text, images } = await this.ask("plan_mode_response", response, false)
+ let { text, images } = await this.ask("plan_mode_response", response, false)
this.isAwaitingPlanResponse = false
+ // webview invoke sendMessage will send this marker in order to put webview into the proper state (responding to an ask) and as a flag to extension that the user switched to ACT mode.
+ if (text === "PLAN_MODE_TOGGLE_RESPONSE") {
+ text = ""
+ }
+
if (this.didRespondToPlanAskBySwitchingMode) {
- if (text) {
- await this.say("user_feedback", text ?? "", images)
- }
pushToolResult(
formatResponse.toolResult(
`[The user has switched to ACT MODE, so you may now proceed with the task.]` +
@@ -2728,6 +2727,13 @@ export class Cline {
images,
),
)
+ } else {
+ // if we didn't switch to ACT MODE, then we can just send the user_feedback message
+ pushToolResult(formatResponse.toolResult(`\n${text}\n`, images))
+ }
+
+ if (text || images?.length) {
+ await this.say("user_feedback", text ?? "", images)
}
//
diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts
index 1c9c122d1d..146e823f34 100644
--- a/src/core/mentions/index.ts
+++ b/src/core/mentions/index.ts
@@ -7,27 +7,32 @@ 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"
+import { getCommitInfo } from "../../utils/git"
+import { getWorkingState } from "../../utils/git"
export function openMention(mention?: string): void {
if (!mention) {
return
}
+ const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
+ if (!cwd) {
+ return
+ }
+
if (mention.startsWith("/")) {
const relPath = mention.slice(1)
- const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
- if (!cwd) {
- return
- }
const absPath = path.resolve(cwd, relPath)
if (mention.endsWith("/")) {
vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath))
- // vscode.commands.executeCommand("vscode.openFolder", , { forceNewWindow: false }) opens in new window
} else {
openFile(absPath)
}
} 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 +51,12 @@ 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)`
+ } else if (mention === "git-changes") {
+ 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)`
}
return match
})
@@ -99,6 +110,27 @@ 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`
+ }
+ } else if (mention === "git-changes") {
+ try {
+ const workingState = await getWorkingState(cwd)
+ parsedText += `\n\n\n${workingState}\n`
+ } catch (error) {
+ parsedText += `\n\n\nError fetching working state: ${error.message}\n`
+ }
+ } else if (/^[a-f0-9]{7,40}$/.test(mention)) {
+ try {
+ const commitInfo = await getCommitInfo(mention, cwd)
+ parsedText += `\n\n\n${commitInfo}\n`
+ } catch (error) {
+ parsedText += `\n\n\nError fetching commit info: ${error.message}\n`
+ }
}
}
diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts
index 8a14acc02b..bf5db6bbe7 100644
--- a/src/core/prompts/responses.ts
+++ b/src/core/prompts/responses.ts
@@ -6,12 +6,6 @@ import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreCo
export const formatResponse = {
toolDenied: () => `The user denied this operation.`,
- toolDeniedWithFeedback: (feedback?: string) =>
- `The user denied this operation and provided the following feedback:\n\n${feedback}\n`,
-
- toolApprovedWithFeedback: (feedback?: string) =>
- `The user approved this operation and provided the following feedback:\n\n${feedback}\n`,
-
toolError: (error?: string) => `The tool execution failed with the following error:\n\n${error}\n`,
clineIgnoreError: (path: string) =>
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 7a87370a44..525f41792e 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -30,6 +30,7 @@ import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shar
import { BrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../shared/BrowserSettings"
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "../../shared/ChatSettings"
import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider"
+import { searchCommits } from "../../utils/git"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -801,7 +802,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
- text: message.chatContent?.message || "[Proceeding with the task...]",
+ text: message.chatContent?.message || "PLAN_MODE_TOGGLE_RESPONSE",
images: message.chatContent?.images,
})
} else {
@@ -1027,6 +1028,21 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
+ case "searchCommits": {
+ const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
+ if (cwd) {
+ try {
+ const commits = await searchCommits(message.text || "", cwd)
+ await this.postMessageToWebview({
+ type: "commitSearchResults",
+ commits,
+ })
+ } catch (error) {
+ console.error(`Error searching commits: ${JSON.stringify(error)}`)
+ }
+ }
+ break
+ }
case "openExtensionSettings": {
const settingsFilter = message.text || ""
await vscode.commands.executeCommand(
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/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts
index 8e00540534..8fcff1f293 100644
--- a/src/shared/ExtensionMessage.ts
+++ b/src/shared/ExtensionMessage.ts
@@ -1,5 +1,6 @@
// type that represents json data that is sent from extension to webview, called ExtensionMessage and has 'type' enum which can be 'plusButtonClicked' or 'settingsButtonClicked' or 'hello'
+import { GitCommit } from "../utils/git"
import { ApiConfiguration, ModelInfo } from "./api"
import { AutoApprovalSettings } from "./AutoApprovalSettings"
import { BrowserSettings } from "./BrowserSettings"
@@ -28,6 +29,7 @@ export interface ExtensionMessage {
| "emailSubscribed"
| "mcpMarketplaceCatalog"
| "mcpDownloadDetails"
+ | "commitSearchResults"
text?: string
action?:
| "chatButtonClicked"
@@ -51,6 +53,7 @@ export interface ExtensionMessage {
mcpMarketplaceCatalog?: McpMarketplaceCatalog
error?: string
mcpDownloadDetails?: McpDownloadResponse
+ commits?: GitCommit[]
}
export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown"
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index 2d82474382..5c6ea5a3d5 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -47,6 +47,7 @@ export interface WebviewMessage {
| "downloadMcp"
| "openMcpMarketplaceServerDetails"
| "silentlyRefreshMcpMarketplace"
+ | "searchCommits"
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 4794327437..444242433d 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -451,85 +451,145 @@ export const deepSeekModels = {
export type QwenModelId = keyof typeof qwenModels
export const qwenDefaultModelId: QwenModelId = "qwen-coder-plus-latest"
export const qwenModels = {
+ "qwen2.5-coder-32b-instruct": {
+ maxTokens: 8_192,
+ contextWindow: 131_072,
+ supportsImages: false,
+ supportsPromptCache: false,
+ inputPrice: 0.002,
+ outputPrice: 0.006,
+ cacheWritesPrice: 0.002,
+ cacheReadsPrice: 0.006,
+ },
+ "qwen2.5-coder-14b-instruct": {
+ maxTokens: 8_192,
+ contextWindow: 131_072,
+ supportsImages: false,
+ supportsPromptCache: false,
+ inputPrice: 0.002,
+ outputPrice: 0.006,
+ cacheWritesPrice: 0.002,
+ cacheReadsPrice: 0.006,
+ },
+ "qwen2.5-coder-7b-instruct": {
+ maxTokens: 8_192,
+ contextWindow: 131_072,
+ supportsImages: false,
+ supportsPromptCache: false,
+ inputPrice: 0.001,
+ outputPrice: 0.002,
+ cacheWritesPrice: 0.001,
+ cacheReadsPrice: 0.002,
+ },
+ "qwen2.5-coder-3b-instruct": {
+ maxTokens: 8_192,
+ contextWindow: 32_768,
+ supportsImages: false,
+ supportsPromptCache: false,
+ inputPrice: 0.0,
+ outputPrice: 0.0,
+ cacheWritesPrice: 0.0,
+ cacheReadsPrice: 0.0,
+ },
+ "qwen2.5-coder-1.5b-instruct": {
+ maxTokens: 8_192,
+ contextWindow: 32_768,
+ supportsImages: false,
+ supportsPromptCache: false,
+ inputPrice: 0.0,
+ outputPrice: 0.0,
+ cacheWritesPrice: 0.0,
+ cacheReadsPrice: 0.0,
+ },
+ "qwen2.5-coder-0.5b-instruct": {
+ maxTokens: 8_192,
+ contextWindow: 32_768,
+ supportsImages: false,
+ supportsPromptCache: false,
+ inputPrice: 0.0,
+ outputPrice: 0.0,
+ cacheWritesPrice: 0.0,
+ cacheReadsPrice: 0.0,
+ },
"qwen-coder-plus-latest": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
- inputPrice: 0.0035,
- outputPrice: 0.007,
- cacheWritesPrice: 0.0035,
- cacheReadsPrice: 0.007,
+ inputPrice: 3.5,
+ outputPrice: 7,
+ cacheWritesPrice: 3.5,
+ cacheReadsPrice: 7,
},
"qwen-plus-latest": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
- inputPrice: 0.0008,
- outputPrice: 0.002,
- cacheWritesPrice: 0.0004,
- cacheReadsPrice: 0.001,
+ inputPrice: 0.8,
+ outputPrice: 2,
+ cacheWritesPrice: 0.8,
+ cacheReadsPrice: 0.2,
},
"qwen-turbo-latest": {
maxTokens: 1_000_000,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
- inputPrice: 0.0003,
- outputPrice: 0.0006,
- cacheWritesPrice: 0.00015,
- cacheReadsPrice: 0.0003,
+ inputPrice: 0.8,
+ outputPrice: 2,
+ cacheWritesPrice: 0.8,
+ cacheReadsPrice: 2,
},
"qwen-max-latest": {
maxTokens: 30_720,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
- inputPrice: 0.0112,
- outputPrice: 0.0448,
- cacheWritesPrice: 0.0056,
- cacheReadsPrice: 0.0224,
+ inputPrice: 2.4,
+ outputPrice: 9.6,
+ cacheWritesPrice: 2.4,
+ cacheReadsPrice: 9.6,
},
"qwen-coder-plus": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
- inputPrice: 0.0035,
- outputPrice: 0.007,
- cacheWritesPrice: 0.0035,
- cacheReadsPrice: 0.007,
+ inputPrice: 3.5,
+ outputPrice: 7,
+ cacheWritesPrice: 3.5,
+ cacheReadsPrice: 7,
},
"qwen-plus": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
- inputPrice: 0.0008,
- outputPrice: 0.002,
- cacheWritesPrice: 0.0004,
- cacheReadsPrice: 0.001,
+ inputPrice: 0.8,
+ outputPrice: 2,
+ cacheWritesPrice: 0.8,
+ cacheReadsPrice: 0.2,
},
"qwen-turbo": {
maxTokens: 1_000_000,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
- inputPrice: 0.0003,
- outputPrice: 0.0006,
- cacheWritesPrice: 0.00015,
- cacheReadsPrice: 0.0003,
+ inputPrice: 0.3,
+ outputPrice: 0.6,
+ cacheWritesPrice: 0.3,
+ cacheReadsPrice: 0.6,
},
"qwen-max": {
maxTokens: 30_720,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
- inputPrice: 0.0112,
- outputPrice: 0.0448,
- cacheWritesPrice: 0.0056,
- cacheReadsPrice: 0.0224,
+ inputPrice: 2.4,
+ outputPrice: 9.6,
+ cacheWritesPrice: 2.4,
+ cacheReadsPrice: 9.6,
},
"deepseek-v3": {
maxTokens: 8_000,
@@ -551,6 +611,46 @@ export const qwenModels = {
cacheWritesPrice: 0.55,
cacheReadsPrice: 0.14,
},
+ "qwen-vl-max": {
+ maxTokens: 30_720,
+ contextWindow: 32_768,
+ supportsImages: true,
+ supportsPromptCache: false,
+ inputPrice: 3,
+ outputPrice: 9,
+ cacheWritesPrice: 3,
+ cacheReadsPrice: 9,
+ },
+ "qwen-vl-max-latest": {
+ maxTokens: 129_024,
+ contextWindow: 131_072,
+ supportsImages: true,
+ supportsPromptCache: false,
+ inputPrice: 3,
+ outputPrice: 9,
+ cacheWritesPrice: 3,
+ cacheReadsPrice: 9,
+ },
+ "qwen-vl-plus": {
+ maxTokens: 6_000,
+ contextWindow: 8_000,
+ supportsImages: true,
+ supportsPromptCache: false,
+ inputPrice: 1.5,
+ outputPrice: 4.5,
+ cacheWritesPrice: 1.5,
+ cacheReadsPrice: 4.5,
+ },
+ "qwen-vl-plus-latest": {
+ maxTokens: 129_024,
+ contextWindow: 131_072,
+ supportsImages: true,
+ supportsPromptCache: false,
+ inputPrice: 1.5,
+ outputPrice: 4.5,
+ cacheWritesPrice: 1.5,
+ cacheReadsPrice: 4.5,
+ },
} as const satisfies Record
// Mistral
diff --git a/src/shared/context-mentions.ts b/src/shared/context-mentions.ts
index 3912868b10..eff8f0a5c0 100644
--- a/src/shared/context-mentions.ts
+++ b/src/shared/context-mentions.ts
@@ -7,42 +7,48 @@ Mention regex:
- **Regex Breakdown**:
- `/@`:
- - **@**: The mention must start with the '@' symbol.
+ - **@**: The mention must start with the '@' symbol.
- `((?:\/|\w+:\/\/)[^\s]+?|problems\b)`:
- - **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
- - `(?:\/|\w+:\/\/)`:
- - **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
- - `\/`:
- - **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
- - `|`: Logical OR.
- - `\w+:\/\/`:
- - **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc.
- - `[^\s]+?`:
- - **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace.
- - **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
- - `|`: Logical OR.
- - `problems\b`:
+ - **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
+ - `(?:\/|\w+:\/\/)`:
+ - **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
+ - `\/`:
+ - **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
+ - `|`: Logical OR.
+ - `\w+:\/\/`:
+ - **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc.
+ - `[^\s]+?`:
+ - **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace.
+ - **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
+ - `|`: Logical OR.
+ - `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.
- - `[.,;:!?]?`:
- - **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
- - `(?=[\s\r\n]|$)`:
- - **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
+ - **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
+ - `[.,;:!?]?`:
+ - **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
+ - `(?=[\s\r\n]|$)`:
+ - **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
- **Summary**:
- The regex effectively matches:
- - 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'.
+ - 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'.
+ - The exact word 'git-changes'.
- 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]+?|[a-f0-9]{7,40}\b|problems\b|terminal\b|git-changes\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
diff --git a/src/utils/git.ts b/src/utils/git.ts
new file mode 100644
index 0000000000..7ab6d67a07
--- /dev/null
+++ b/src/utils/git.ts
@@ -0,0 +1,177 @@
+import { exec } from "child_process"
+import { promisify } from "util"
+
+const execAsync = promisify(exec)
+const GIT_OUTPUT_LINE_LIMIT = 500
+
+export interface GitCommit {
+ hash: string
+ shortHash: string
+ subject: string
+ author: string
+ date: string
+}
+
+async function checkGitRepo(cwd: string): Promise {
+ try {
+ await execAsync("git rev-parse --git-dir", { cwd })
+ return true
+ } catch (error) {
+ return false
+ }
+}
+
+async function checkGitInstalled(): Promise {
+ try {
+ await execAsync("git --version")
+ return true
+ } catch (error) {
+ return false
+ }
+}
+
+export async function searchCommits(query: string, cwd: string): Promise {
+ try {
+ const isInstalled = await checkGitInstalled()
+ if (!isInstalled) {
+ console.error("Git is not installed")
+ return []
+ }
+
+ const isRepo = await checkGitRepo(cwd)
+ if (!isRepo) {
+ console.error("Not a git repository")
+ return []
+ }
+
+ // Search commits by hash or message, limiting to 10 results
+ const { stdout } = await execAsync(
+ `git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--grep="${query}" --regexp-ignore-case`,
+ { cwd },
+ )
+
+ let output = stdout
+ if (!output.trim() && /^[a-f0-9]+$/i.test(query)) {
+ // If no results from grep search and query looks like a hash, try searching by hash
+ const { stdout: hashStdout } = await execAsync(
+ `git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--author-date-order ${query}`,
+ { cwd },
+ ).catch(() => ({ stdout: "" }))
+
+ if (!hashStdout.trim()) {
+ return []
+ }
+
+ output = hashStdout
+ }
+
+ const commits: GitCommit[] = []
+ const lines = output
+ .trim()
+ .split("\n")
+ .filter((line) => line !== "--")
+
+ for (let i = 0; i < lines.length; i += 5) {
+ commits.push({
+ hash: lines[i],
+ shortHash: lines[i + 1],
+ subject: lines[i + 2],
+ author: lines[i + 3],
+ date: lines[i + 4],
+ })
+ }
+
+ return commits
+ } catch (error) {
+ console.error("Error searching commits:", error)
+ return []
+ }
+}
+
+export async function getCommitInfo(hash: string, cwd: string): Promise {
+ try {
+ const isInstalled = await checkGitInstalled()
+ if (!isInstalled) {
+ return "Git is not installed"
+ }
+
+ const isRepo = await checkGitRepo(cwd)
+ if (!isRepo) {
+ return "Not a git repository"
+ }
+
+ // Get commit info, stats, and diff separately
+ const { stdout: info } = await execAsync(`git show --format="%H%n%h%n%s%n%an%n%ad%n%b" --no-patch ${hash}`, {
+ cwd,
+ })
+ const [fullHash, shortHash, subject, author, date, body] = info.trim().split("\n")
+
+ const { stdout: stats } = await execAsync(`git show --stat --format="" ${hash}`, { cwd })
+
+ const { stdout: diff } = await execAsync(`git show --format="" ${hash}`, { cwd })
+
+ const summary = [
+ `Commit: ${shortHash} (${fullHash})`,
+ `Author: ${author}`,
+ `Date: ${date}`,
+ `\nMessage: ${subject}`,
+ body ? `\nDescription:\n${body}` : "",
+ "\nFiles Changed:",
+ stats.trim(),
+ "\nFull Changes:",
+ ].join("\n")
+
+ const output = summary + "\n\n" + diff.trim()
+ return truncateOutput(output)
+ } catch (error) {
+ console.error("Error getting commit info:", error)
+ return `Failed to get commit info: ${error instanceof Error ? error.message : String(error)}`
+ }
+}
+
+export async function getWorkingState(cwd: string): Promise {
+ try {
+ const isInstalled = await checkGitInstalled()
+ if (!isInstalled) {
+ return "Git is not installed"
+ }
+
+ const isRepo = await checkGitRepo(cwd)
+ if (!isRepo) {
+ return "Not a git repository"
+ }
+
+ // Get status of working directory
+ const { stdout: status } = await execAsync("git status --short", { cwd })
+ if (!status.trim()) {
+ return "No changes in working directory"
+ }
+
+ // Get all changes (both staged and unstaged) compared to HEAD
+ const { stdout: diff } = await execAsync("git diff HEAD", { cwd })
+ const output = `Working directory changes:\n\n${status}\n\n${diff}`.trim()
+ return truncateOutput(output)
+ } catch (error) {
+ console.error("Error getting working state:", error)
+ return `Failed to get working state: ${error instanceof Error ? error.message : String(error)}`
+ }
+}
+
+function truncateOutput(content: string): string {
+ if (!GIT_OUTPUT_LINE_LIMIT) {
+ return content
+ }
+
+ const lines = content.split("\n")
+ if (lines.length <= GIT_OUTPUT_LINE_LIMIT) {
+ return content
+ }
+
+ const beforeLimit = Math.floor(GIT_OUTPUT_LINE_LIMIT * 0.2) // 20% of lines before
+ const afterLimit = GIT_OUTPUT_LINE_LIMIT - beforeLimit // remaining 80% after
+ return [
+ ...lines.slice(0, beforeLimit),
+ `\n[...${lines.length - GIT_OUTPUT_LINE_LIMIT} lines omitted...]\n`,
+ ...lines.slice(-afterLimit),
+ ].join("\n")
+}
diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json
index 4a06dc2e45..e1015aca29 100644
--- a/webview-ui/package-lock.json
+++ b/webview-ui/package-lock.json
@@ -13,6 +13,7 @@
"debounce": "^2.1.1",
"fast-deep-equal": "^3.1.3",
"fuse.js": "^7.0.0",
+ "fzf": "^0.5.2",
"pretty-bytes": "^6.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -9825,6 +9826,12 @@
"node": ">=10"
}
},
+ "node_modules/fzf": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz",
+ "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
diff --git a/webview-ui/package.json b/webview-ui/package.json
index 2f023747d1..8f15b44187 100644
--- a/webview-ui/package.json
+++ b/webview-ui/package.json
@@ -8,6 +8,7 @@
"debounce": "^2.1.1",
"fast-deep-equal": "^3.1.3",
"fuse.js": "^7.0.0",
+ "fzf": "^0.5.2",
"pretty-bytes": "^6.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
diff --git a/webview-ui/scripts/build-react-no-split.js b/webview-ui/scripts/build-react-no-split.js
index f021a12104..28f37108be 100644
--- a/webview-ui/scripts/build-react-no-split.js
+++ b/webview-ui/scripts/build-react-no-split.js
@@ -98,18 +98,26 @@ config.module.rules[1].oneOf.forEach((rule) => {
}
})
-// Disable code splitting
-config.optimization.splitChunks = {
- cacheGroups: {
- default: false,
+// Force all code into a single bundle for VS Code webview compatibility.
+// This is necessary for:
+// 1. Mermaid.js to work properly (prevents async chunk loading)
+// 2. Consistent CSP nonce handling (single bundle = single nonce)
+config.optimization = {
+ ...config.optimization,
+ splitChunks: {
+ cacheGroups: {
+ default: false,
+ },
+ name: "main", // Forces all chunks (dynamic import() calls, for example those used by Mermaid) into one bundle - this is what actually prevents code splitting
},
+ runtimeChunk: false,
}
-// Disable code chunks
-config.optimization.runtimeChunk = false
-
-// Rename main.{hash}.js to main.js
-config.output.filename = "static/js/[name].js"
+// Ensure all chunks are named 'main' to match our CSP nonce setup
+config.output = {
+ ...config.output,
+ filename: "static/js/[name].js",
+}
// Rename main.{hash}.css to main.css
config.plugins[5].options.filename = "static/css/[name].css"
diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx
index e86697405f..2e642f9a46 100644
--- a/webview-ui/src/components/chat/ChatTextArea.tsx
+++ b/webview-ui/src/components/chat/ChatTextArea.tsx
@@ -1,9 +1,10 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
import DynamicTextArea from "react-textarea-autosize"
-import { useClickAway, useWindowSize } from "react-use"
+import { useClickAway, useEvent, useWindowSize } from "react-use"
import styled from "styled-components"
import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions"
+import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
import { useExtensionState } from "../../context/ExtensionStateContext"
import {
ContextMenuOptionType,
@@ -12,16 +13,15 @@ import {
removeMention,
shouldShowContextMenu,
} from "../../utils/context-mentions"
+import { useMetaKeyDetection, useShortcut } from "../../utils/hooks"
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
import { vscode } from "../../utils/vscode"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import Thumbnails from "../common/Thumbnails"
+import Tooltip from "../common/Tooltip"
import ApiOptions, { normalizeApiConfiguration } from "../settings/ApiOptions"
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
import ContextMenu from "./ContextMenu"
-import { useShortcut } from "../../utils/hooks"
-import Tooltip from "../common/Tooltip"
-import { useMetaKeyDetection } from "../../utils/hooks"
interface ChatTextAreaProps {
inputValue: string
@@ -215,6 +215,8 @@ const ChatTextArea = forwardRef(
) => {
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
+ const [gitCommits, setGitCommits] = useState([])
+
const [thumbnailsHeight, setThumbnailsHeight] = useState(0)
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState(undefined)
const [showContextMenu, setShowContextMenu] = useState(false)
@@ -240,9 +242,40 @@ const ChatTextArea = forwardRef(
// Add a ref to track previous menu state
const prevShowModelSelector = useRef(showModelSelector)
+ // Fetch git commits when Git is selected or when typing a hash
+ useEffect(() => {
+ if (selectedType === ContextMenuOptionType.Git || /^[a-f0-9]+$/i.test(searchQuery)) {
+ vscode.postMessage({
+ type: "searchCommits",
+ text: searchQuery || "",
+ })
+ }
+ }, [selectedType, searchQuery])
+
+ const handleMessage = useCallback((event: MessageEvent) => {
+ const message: ExtensionMessage = event.data
+ switch (message.type) {
+ case "commitSearchResults": {
+ const commits =
+ message.commits?.map((commit: any) => ({
+ type: ContextMenuOptionType.Git,
+ value: commit.hash,
+ label: commit.subject,
+ description: `${commit.shortHash} by ${commit.author} on ${commit.date}`,
+ })) || []
+ setGitCommits(commits)
+ break
+ }
+ }
+ }, [])
+
+ useEvent("message", handleMessage)
+
const queryItems = useMemo(() => {
return [
{ type: ContextMenuOptionType.Problems, value: "problems" },
+ { type: ContextMenuOptionType.Terminal, value: "terminal" },
+ ...gitCommits,
...filePaths
.map((file) => "/" + file)
.map((path) => ({
@@ -250,7 +283,7 @@ const ChatTextArea = forwardRef(
value: path,
})),
]
- }, [filePaths])
+ }, [filePaths, gitCommits])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@@ -274,7 +307,11 @@ const ChatTextArea = forwardRef(
return
}
- if (type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder) {
+ if (
+ type === ContextMenuOptionType.File ||
+ type === ContextMenuOptionType.Folder ||
+ type === ContextMenuOptionType.Git
+ ) {
if (!value) {
setSelectedType(type)
setSearchQuery("")
@@ -293,6 +330,10 @@ 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 || ""
}
const { newValue, mentionIndex } = insertMention(textAreaRef.current.value, cursorPosition, insertValue)
@@ -895,7 +936,7 @@ const ChatTextArea = forwardRef(
borderTop: 0,
borderColor: "transparent",
borderBottom: `${thumbnailsHeight + 6}px solid transparent`,
- padding: "9px 49px 3px 9px",
+ padding: "9px 28px 3px 9px",
}}
/>
= ({
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:
return No results found
+ case ContextMenuOptionType.Git:
+ if (option.value) {
+ return (
+
+ {option.label}
+
+ {option.description}
+
+
+ )
+ } else {
+ return Git Commits
+ }
case ContextMenuOptionType.File:
case ContextMenuOptionType.Folder:
if (option.value) {
@@ -85,8 +108,12 @@ const ContextMenu: React.FC = ({
return "folder"
case ContextMenuOptionType.Problems:
return "warning"
+ case ContextMenuOptionType.Terminal:
+ return "terminal"
case ContextMenuOptionType.URL:
return "link"
+ case ContextMenuOptionType.Git:
+ return "git-commit"
case ContextMenuOptionType.NoResults:
return "info"
default:
@@ -161,7 +188,9 @@ const ContextMenu: React.FC = ({
/>
{renderOptionContent(option)}
- {(option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) &&
+ {(option.type === ContextMenuOptionType.File ||
+ option.type === ContextMenuOptionType.Folder ||
+ option.type === ContextMenuOptionType.Git) &&
!option.value && (
= ({
/>
)}
{(option.type === ContextMenuOptionType.Problems ||
- ((option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) &&
+ option.type === ContextMenuOptionType.Terminal ||
+ ((option.type === ContextMenuOptionType.File ||
+ option.type === ContextMenuOptionType.Folder ||
+ option.type === ContextMenuOptionType.Git) &&
option.value)) && (
0 ? folders : [{ type: ContextMenuOptionType.NoResults }]
}
+ if (selectedType === ContextMenuOptionType.Git) {
+ const commits = queryItems.filter((item) => item.type === ContextMenuOptionType.Git)
+ return commits.length > 0 ? [workingChanges, ...commits] : [workingChanges]
+ }
+
return [
{ type: ContextMenuOptionType.URL },
{ type: ContextMenuOptionType.Problems },
+ { type: ContextMenuOptionType.Terminal },
+ { type: ContextMenuOptionType.Git },
{ type: ContextMenuOptionType.Folder },
{ type: ContextMenuOptionType.File },
]
}
const lowerQuery = query.toLowerCase()
+ const suggestions: ContextMenuQueryItem[] = []
+ // Check for top-level option matches
+ if ("git".startsWith(lowerQuery)) {
+ suggestions.push({
+ type: ContextMenuOptionType.Git,
+ label: "Git Commits",
+ description: "Search repository history",
+ })
+ } else if ("git-changes".startsWith(lowerQuery)) {
+ suggestions.push(workingChanges)
+ }
+ if ("problems".startsWith(lowerQuery)) {
+ suggestions.push({ type: ContextMenuOptionType.Problems })
+ }
if (query.startsWith("http")) {
- return [{ type: ContextMenuOptionType.URL, value: query }]
- } else {
- const matchingItems = queryItems.filter((item) => item.value?.toLowerCase().includes(lowerQuery))
+ suggestions.push({ type: ContextMenuOptionType.URL, value: query })
+ }
- if (matchingItems.length > 0) {
- return matchingItems.map((item) => ({
- type: item.type,
- value: item.value,
- }))
+ // Add exact SHA matches to suggestions
+ if (/^[a-f0-9]{7,40}$/i.test(lowerQuery)) {
+ const exactMatches = queryItems.filter(
+ (item) => item.type === ContextMenuOptionType.Git && item.value?.toLowerCase() === lowerQuery,
+ )
+ if (exactMatches.length > 0) {
+ suggestions.push(...exactMatches)
} else {
- return [{ type: ContextMenuOptionType.NoResults }]
+ // If no exact match but valid SHA format, add as option
+ suggestions.push({
+ type: ContextMenuOptionType.Git,
+ value: lowerQuery,
+ label: `Commit ${lowerQuery}`,
+ description: "Git commit hash",
+ })
}
}
+
+ // Create searchable strings array for fzf
+ const searchableItems = queryItems.map((item) => ({
+ original: item,
+ searchStr: [item.value, item.label, item.description].filter(Boolean).join(" "),
+ }))
+
+ // Initialize fzf instance for fuzzy search
+ const fzf = new Fzf(searchableItems, {
+ selector: (item) => item.searchStr,
+ })
+
+ // Get fuzzy matching items
+ const matchingItems = query ? fzf.find(query).map((result) => result.item.original) : []
+
+ // Separate matches by type
+ const fileMatches = matchingItems.filter(
+ (item) => item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.Folder,
+ )
+ const gitMatches = matchingItems.filter((item) => item.type === ContextMenuOptionType.Git)
+ const otherMatches = matchingItems.filter(
+ (item) =>
+ item.type !== ContextMenuOptionType.File &&
+ item.type !== ContextMenuOptionType.Folder &&
+ item.type !== ContextMenuOptionType.Git,
+ )
+
+ // Combine suggestions with matching items in the desired order
+ if (suggestions.length > 0 || matchingItems.length > 0) {
+ const allItems = [...suggestions, ...fileMatches, ...gitMatches, ...otherMatches]
+
+ // Remove duplicates based on type and value
+ const seen = new Set()
+ const deduped = allItems.filter((item) => {
+ const key = `${item.type}-${item.value}`
+ if (seen.has(key)) return false
+ seen.add(key)
+ return true
+ })
+
+ return deduped
+ }
+
+ return [{ type: ContextMenuOptionType.NoResults }]
}
export function shouldShowContextMenu(text: string, position: number): boolean {
@@ -121,8 +205,8 @@ 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