mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
merge conflicts resolved
This commit is contained in:
commit
e95cb30ebd
33 changed files with 764 additions and 213 deletions
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add translation to CODE_OF_CONDUCT, CONTRIBUTING and README to portuguese pt-BR.
|
||||
10
.changeset/fair-snails-confess.md
Normal file
10
.changeset/fair-snails-confess.md
Normal file
|
|
@ -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
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Advanced Setting to disable browser tool
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add extension setting for chromium executable path
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Added api key field for litellm api provider in settings
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
qwen platform adds deepseek-r1/v3 support
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Adding .clineignore guide
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Advanced Configuration for OpenAI Compatible Providers
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Allowing the user to give feedback when approving a tool use.
|
||||
16
CHANGELOG.md
16
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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div align="center"><sub>
|
||||
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/pt-BR/README.md" target="_blank"> Português </a>
|
||||
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a>
|
||||
</sub></div>
|
||||
|
||||
# Cline – \#1 on OpenRouter
|
||||
|
|
|
|||
24
package-lock.json
generated
24
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<feedback>\n${feedback}\n</feedback>`,
|
||||
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<feedback>\n${text}\n</feedback>`,
|
||||
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(`<user_message>\n${text}\n</user_message>`, images))
|
||||
}
|
||||
|
||||
if (text || images?.length) {
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
|
||||
}
|
||||
} 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>`
|
||||
}
|
||||
} else if (mention === "git-changes") {
|
||||
try {
|
||||
const workingState = await getWorkingState(cwd)
|
||||
parsedText += `\n\n<git_working_state>\n${workingState}\n</git_working_state>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<git_working_state>\nError fetching working state: ${error.message}\n</git_working_state>`
|
||||
}
|
||||
} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
|
||||
try {
|
||||
const commitInfo = await getCommitInfo(mention, cwd)
|
||||
parsedText += `\n\n<git_commit hash="${mention}">\n${commitInfo}\n</git_commit>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<git_commit hash="${mention}">\nError fetching commit info: ${error.message}\n</git_commit>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<feedback>\n${feedback}\n</feedback>`,
|
||||
|
||||
toolApprovedWithFeedback: (feedback?: string) =>
|
||||
`The user approved this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
|
||||
|
||||
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
|
||||
|
||||
clineIgnoreError: (path: string) =>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
45
src/integrations/terminal/get-latest-output.ts
Normal file
45
src/integrations/terminal/get-latest-output.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export interface WebviewMessage {
|
|||
| "downloadMcp"
|
||||
| "openMcpMarketplaceServerDetails"
|
||||
| "silentlyRefreshMcpMarketplace"
|
||||
| "searchCommits"
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
|
|
|
|||
|
|
@ -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<string, ModelInfo>
|
||||
|
||||
// Mistral
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
177
src/utils/git.ts
Normal file
177
src/utils/git.ts
Normal file
|
|
@ -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<boolean> {
|
||||
try {
|
||||
await execAsync("git rev-parse --git-dir", { cwd })
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function checkGitInstalled(): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git --version")
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchCommits(query: string, cwd: string): Promise<GitCommit[]> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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")
|
||||
}
|
||||
7
webview-ui/package-lock.json
generated
7
webview-ui/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
) => {
|
||||
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||
const [gitCommits, setGitCommits] = useState<any[]>([])
|
||||
|
||||
const [thumbnailsHeight, setThumbnailsHeight] = useState(0)
|
||||
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
|
||||
const [showContextMenu, setShowContextMenu] = useState(false)
|
||||
|
|
@ -240,9 +242,40 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
// 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<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
value: path,
|
||||
})),
|
||||
]
|
||||
}, [filePaths])
|
||||
}, [filePaths, gitCommits])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
|
|
@ -274,7 +307,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
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<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 || ""
|
||||
}
|
||||
|
||||
const { newValue, mentionIndex } = insertMention(textAreaRef.current.value, cursorPosition, insertValue)
|
||||
|
|
@ -895,7 +936,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
borderTop: 0,
|
||||
borderColor: "transparent",
|
||||
borderBottom: `${thumbnailsHeight + 6}px solid transparent`,
|
||||
padding: "9px 49px 3px 9px",
|
||||
padding: "9px 28px 3px 9px",
|
||||
}}
|
||||
/>
|
||||
<DynamicTextArea
|
||||
|
|
|
|||
|
|
@ -48,10 +48,33 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||
switch (option.type) {
|
||||
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:
|
||||
return <span>No results found</span>
|
||||
case ContextMenuOptionType.Git:
|
||||
if (option.value) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
||||
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
opacity: 0.7,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
lineHeight: "1.2",
|
||||
}}>
|
||||
{option.description}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <span>Git Commits</span>
|
||||
}
|
||||
case ContextMenuOptionType.File:
|
||||
case ContextMenuOptionType.Folder:
|
||||
if (option.value) {
|
||||
|
|
@ -85,8 +108,12 @@ 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:
|
||||
return "git-commit"
|
||||
case ContextMenuOptionType.NoResults:
|
||||
return "info"
|
||||
default:
|
||||
|
|
@ -161,7 +188,9 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||
/>
|
||||
{renderOptionContent(option)}
|
||||
</div>
|
||||
{(option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) &&
|
||||
{(option.type === ContextMenuOptionType.File ||
|
||||
option.type === ContextMenuOptionType.Folder ||
|
||||
option.type === ContextMenuOptionType.Git) &&
|
||||
!option.value && (
|
||||
<i
|
||||
className="codicon codicon-chevron-right"
|
||||
|
|
@ -173,7 +202,10 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||
/>
|
||||
)}
|
||||
{(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)) && (
|
||||
<i
|
||||
className="codicon codicon-add"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { mentionRegex } from "../../../src/shared/context-mentions"
|
||||
import { Fzf } from "fzf"
|
||||
|
||||
export function insertMention(text: string, position: number, value: string): { newValue: string; mentionIndex: number } {
|
||||
const beforeCursor = text.slice(0, position)
|
||||
|
|
@ -46,13 +47,17 @@ export enum ContextMenuOptionType {
|
|||
File = "file",
|
||||
Folder = "folder",
|
||||
Problems = "problems",
|
||||
Terminal = "terminal",
|
||||
URL = "url",
|
||||
Git = "git",
|
||||
NoResults = "noResults",
|
||||
}
|
||||
|
||||
export interface ContextMenuQueryItem {
|
||||
type: ContextMenuOptionType
|
||||
value?: string
|
||||
label?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function getContextMenuOptions(
|
||||
|
|
@ -60,6 +65,13 @@ export function getContextMenuOptions(
|
|||
selectedType: ContextMenuOptionType | null = null,
|
||||
queryItems: ContextMenuQueryItem[],
|
||||
): ContextMenuQueryItem[] {
|
||||
const workingChanges: ContextMenuQueryItem = {
|
||||
type: ContextMenuOptionType.Git,
|
||||
value: "git-changes",
|
||||
label: "Working changes",
|
||||
description: "Current uncommitted changes",
|
||||
}
|
||||
|
||||
if (query === "") {
|
||||
if (selectedType === ContextMenuOptionType.File) {
|
||||
const files = queryItems
|
||||
|
|
@ -81,30 +93,102 @@ export function getContextMenuOptions(
|
|||
return folders.length > 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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue