Compare commits

..

2 Commits

Author SHA1 Message Date
0xtoshii 204963a4ed changeset 2025-04-28 17:52:32 -07:00
0xtoshii 6cd9482144 cache reads 2025-04-28 17:24:03 -07:00
45 changed files with 404 additions and 5011 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
updated drag and drop text to say "drop" instead of "drag"
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Minor UX improvement to drag and drop ux
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Adding args to allow Cursor to open workspaces (for checkpoint testing/development)
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
add checkpoints after more messages
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Introduce UI library for future UI development
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
add newrule slash command
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
showing expanded task by default
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Refactor to not pass a message for showing the MCP View from the servers modal
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Migrate the addRemoteServer to protobus
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Lowering Gemini cache TTL time
-3
View File
@@ -13,7 +13,6 @@ permissions:
jobs:
codespell:
if: false
name: Check for spelling errors
runs-on: ubuntu-latest
@@ -24,5 +23,3 @@ jobs:
uses: codespell-project/codespell-problem-matcher@v1
- name: Codespell
uses: codespell-project/actions-codespell@v2
with:
only_warn: 1
+7 -6
View File
@@ -23,7 +23,7 @@ jobs:
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
node-version: 20.15.1
# Setup Python for coverage script
- name: Setup Python
@@ -70,11 +70,12 @@ jobs:
run: npm run format
# Build the extension before running tests
- name: Build Tests and Extension
run: npm run pretest
- name: Build Extension
run: npm run compile
- name: Unit Tests
run: npm run test:unit
# Disabling due to compatability with test framework and ESM modules
# - name: Unit Tests
# run: npm run test:unit
# Run extension tests with coverage
- name: Extension Tests with Coverage
@@ -131,7 +132,7 @@ jobs:
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
node-version: 20.15.1
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
+1 -1
View File
@@ -1,6 +1,6 @@
{
"extension": ["ts"],
"spec": "src/**/__tests__/*.ts",
"require": ["ts-node/register", "source-map-support/register", "./src/test/requires.ts"],
"require": ["ts-node/register", "source-map-support/register"],
"recursive": true
}
+2 -1
View File
@@ -9,9 +9,10 @@
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-workspace-trust", "${workspaceFolder}"],
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "${defaultBuildTask}",
"postDebugTask": "stop",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
-7
View File
@@ -9,7 +9,6 @@ import "common.proto";
service McpService {
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
}
message ToggleMcpServerRequest {
@@ -24,12 +23,6 @@ message UpdateMcpTimeoutRequest {
int32 timeout = 3;
}
message AddRemoteMcpServerRequest {
Metadata metadata = 1;
string server_name = 2;
string server_url = 3;
}
message McpTool {
string name = 1;
optional string description = 2;
+2 -2
View File
@@ -7,8 +7,8 @@ import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, M
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
import { ApiStream } from "../transform/stream"
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
const DEFAULT_CACHE_TTL_SECONDS = 900
// Define a default TTL for the cache (e.g., 1 hour in seconds)
const DEFAULT_CACHE_TTL_SECONDS = 3600
export class GeminiHandler implements ApiHandler {
private options: ApiHandlerOptions
-1
View File
@@ -25,7 +25,6 @@ export const toolUseNames = [
"attempt_completion",
"new_task",
"condense",
"new_rule",
] as const
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)
@@ -55,10 +55,7 @@ export function parseAssistantMessage(assistantMessage: string) {
// special case for write_to_file where file contents could contain the closing tag, in which case the param would have closed and we end up with the rest of the file contents here. To work around this, we get the string between the starting content tag and the LAST content tag.
const contentParamName: ToolParamName = "content"
if (
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
accumulator.endsWith(`</${contentParamName}>`)
) {
if (currentToolUse.name === "write_to_file" && accumulator.endsWith(`</${contentParamName}>`)) {
const toolContent = accumulator.slice(currentToolUseStartIndex)
const contentStartTag = `<${contentParamName}>`
const contentEndTag = `</${contentParamName}>`
@@ -34,24 +34,24 @@ describe("ContextManager", () => {
const messages = createMessages(11)
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
expect(result).to.deep.equal([2, 5])
expect(result).to.deep.equal([1, 4])
})
it("first truncation with quarter keep", () => {
const messages = createMessages(11)
const result = contextManager.getNextTruncationRange(messages, undefined, "quarter")
expect(result).to.deep.equal([2, 7])
expect(result).to.deep.equal([1, 6])
})
it("sequential truncation with half keep", () => {
const messages = createMessages(21)
const firstRange = contextManager.getNextTruncationRange(messages, undefined, "half")
expect(firstRange).to.deep.equal([2, 9])
expect(firstRange).to.deep.equal([1, 10])
// Pass the previous range for sequential truncation
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "half")
expect(secondRange).to.deep.equal([2, 13])
expect(secondRange).to.deep.equal([1, 14])
})
it("sequential truncation with quarter keep", () => {
@@ -60,7 +60,7 @@ describe("ContextManager", () => {
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "quarter")
expect(secondRange[0]).to.equal(2)
expect(secondRange[0]).to.equal(1)
expect(secondRange[1]).to.be.greaterThan(firstRange[1])
})
@@ -68,20 +68,20 @@ describe("ContextManager", () => {
const messages = createMessages(14)
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
// Check if the message at the end of range is an assistant message
// Check if the message at the end of range is a user message
const lastRemovedMessage = messages[result[1]]
expect(lastRemovedMessage.role).to.equal("assistant")
expect(lastRemovedMessage.role).to.equal("user")
// Check if the next message after the range is a user message
// Check if the next message after the range is an assistant message
const nextMessage = messages[result[1] + 1]
expect(nextMessage.role).to.equal("user")
expect(nextMessage.role).to.equal("assistant")
})
it("handles small message arrays", () => {
const messages = createMessages(3)
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
expect(result).to.deep.equal([2, 1])
expect(result).to.deep.equal([1, 0])
})
it("preserves the message structure when truncating", () => {
@@ -120,10 +120,9 @@ describe("ContextManager", () => {
const range: [number, number] = [1, 3]
const result = contextManager.getTruncatedMessages(messages, range)
expect(result).to.have.lengthOf(3)
expect(result).to.have.lengthOf(2)
expect(result[0]).to.deep.equal(messages[0])
expect(result[1]).to.deep.equal(messages[1])
expect(result[2]).to.deep.equal(messages[4])
expect(result[1]).to.deep.equal(messages[4])
})
it("works with a range that starts at the first message after task", () => {
@@ -132,21 +131,20 @@ describe("ContextManager", () => {
const range: [number, number] = [1, 2]
const result = contextManager.getTruncatedMessages(messages, range)
expect(result).to.have.lengthOf(3)
expect(result).to.have.lengthOf(2)
expect(result[0]).to.deep.equal(messages[0])
expect(result[1]).to.deep.equal(messages[1])
expect(result[2]).to.deep.equal(messages[3])
expect(result[1]).to.deep.equal(messages[3])
})
it("correctly handles removing a range while preserving alternation pattern", () => {
const messages = createMessages(5)
const range: [number, number] = [2, 3]
const range: [number, number] = [1, 2]
const result = contextManager.getTruncatedMessages(messages, range)
expect(result).to.have.lengthOf(3)
expect(result[0]).to.deep.equal(messages[0])
expect(result[1]).to.deep.equal(messages[1])
expect(result[1]).to.deep.equal(messages[3])
expect(result[2]).to.deep.equal(messages[4])
expect(result[0].role).to.equal("user")
@@ -7,45 +7,6 @@ import { ClineRulesToggles } from "@shared/cline-rules"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
import * as vscode from "vscode"
/**
* Converts .clinerules file to directory and places old .clinerule file inside directory, renaming it
* Doesn't do anything if .clinerules dir already exists or doesn't exist
* Returns whether there are any uncaught errors
*/
export async function ensureLocalClinerulesDirExists(cwd: string): Promise<boolean> {
const clinerulePath = path.resolve(cwd, GlobalFileNames.clineRules)
const defaultRuleFilename = "default-rules.md"
try {
const exists = await fileExistsAtPath(clinerulePath)
if (exists && !(await isDirectory(clinerulePath))) {
// logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
const content = await fs.readFile(clinerulePath, "utf8")
const tempPath = clinerulePath + ".bak"
await fs.rename(clinerulePath, tempPath) // create backup
try {
await fs.mkdir(clinerulePath, { recursive: true })
await fs.writeFile(path.join(clinerulePath, defaultRuleFilename), content, "utf8")
await fs.unlink(tempPath).catch(() => {}) // delete backup
return false // conversion successful with no errors
} catch (conversionError) {
// attempt to restore backup on conversion failure
try {
await fs.rm(clinerulePath, { recursive: true, force: true }).catch(() => {})
await fs.rename(tempPath, clinerulePath) // restore backup
} catch (restoreError) {}
return true // in either case here we consider this an error
}
}
// exists and is a dir or doesn't exist, either of these cases we dont need to handle here
return false
} catch (error) {
return true
}
}
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
if (await fileExistsAtPath(globalClineRulesFilePath)) {
if (await isDirectory(globalClineRulesFilePath)) {
+26
View File
@@ -186,6 +186,28 @@ export class Controller {
*/
async handleWebviewMessage(message: WebviewMessage) {
switch (message.type) {
case "addRemoteServer": {
try {
await this.mcpHub?.addRemoteServer(message.serverName!, message.serverUrl!)
await this.postMessageToWebview({
type: "addRemoteServerResult",
addRemoteServerResult: {
success: true,
serverName: message.serverName!,
},
})
} catch (error) {
await this.postMessageToWebview({
type: "addRemoteServerResult",
addRemoteServerResult: {
success: false,
serverName: message.serverName!,
error: error.message,
},
})
}
break
}
case "authStateChanged":
await this.setUserInfo(message.user || undefined)
await this.postStateToWebview()
@@ -435,6 +457,10 @@ export class Controller {
await this.fetchUserCreditsData()
break
}
case "showMcpView": {
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked", tab: message.tab || undefined })
break
}
case "openMcpSettings": {
const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
if (mcpSettingsFilePath) {
@@ -1,32 +0,0 @@
import { convertMcpServersToProtoMcpServers } from "@/shared/proto-conversions/mcp/mcp-server-conversion"
import type { AddRemoteMcpServerRequest, McpServers } from "../../../shared/proto/mcp"
import type { Controller } from "../index"
/**
* Adds a new remote MCP server via gRPC
* @param controller The controller instance
* @param request The request containing server name and URL
* @returns An array of McpServer objects
*/
export async function addRemoteMcpServer(controller: Controller, request: AddRemoteMcpServerRequest): Promise<McpServers> {
try {
// Validate required fields
if (!request.serverName) {
throw new Error("Server name is required")
}
if (!request.serverUrl) {
throw new Error("Server URL is required")
}
// Call the McpHub method to add the remote server
const servers = await controller.mcpHub?.addRemoteServer(request.serverName, request.serverUrl)
const protoServers = convertMcpServersToProtoMcpServers(servers)
return { mcpServers: protoServers }
} catch (error) {
console.error(`Failed to add remote MCP server ${request.serverName}:`, error)
throw error
}
}
-2
View File
@@ -3,14 +3,12 @@
// Import all method implementations
import { registerMethod } from "./index"
import { addRemoteMcpServer } from "./addRemoteMcpServer"
import { toggleMcpServer } from "./toggleMcpServer"
import { updateMcpTimeout } from "./updateMcpTimeout"
// Register all mcp service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
registerMethod("toggleMcpServer", toggleMcpServer)
registerMethod("updateMcpTimeout", updateMcpTimeout)
}
-58
View File
@@ -87,61 +87,3 @@ Example:
</explicit_instructions>\n
`
export const newRuleToolResponse = () =>
`<explicit_instructions type="new_rule">
The user has explicitly asked you to help them create a new Cline rule file inside the .clinerules top-level directory based on the conversation up to this point in time. The user may have provided instructions or additional information for you to consider when creating the new Cline rule.
When creating a new Cline rule file, you should NOT overwrite or alter an existing Cline rule file. To create the Cline rule file you MUST use the new_rule tool. The new_rule tool can be used in either of the PLAN or ACT modes.
The new_rule tool is defined below:
Description:
Your task is to create a new Cline rule file which includes guidelines on how to approach developing code in tandem with the user, which can be either project specific or cover more global rules. This includes but is not limited to: desired conversational style, favorite project dependencies, coding styles, naming conventions, architectural choices, ui/ux preferences, etc.
The Cline rule file must be formatted as markdown and be a '.md' file. The name of the file you generate must be as succinct as possible and be encompassing the main overarching concept of the rules you added to the file (e.g., 'memory-bank.md' or 'project-overview.md').
Parameters:
- Path: (required) The path of the file to write to (relative to the current working directory). This will be the Cline rule file you create, and it must be placed inside the .clinerules top-level directory (create this if it doesn't exist). The filename created CANNOT be "default-clineignore.md". For filenames, use hyphens ("-") instead of underscores ("_") to separate words.
- Content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. The content for the Cline rule file MUST be created according to the following instructions:
1. Format the Cline rule file to have distinct guideline sections, each with their own markdown heading, starting with "## Brief overview". Under each of these headings, include bullet points fully fleshing out the details, with examples and/or trigger cases ONLY when applicable.
2. These guidelines can be specific to the task(s) or project worked on thus far, or cover more high-level concepts. Guidelines can include coding conventions, general design patterns, preferred tech stack including favorite libraries and language, communication style with Cline (verbose vs concise), prompting strategies, naming conventions, testing strategies, comment verbosity, time spent on architecting prior to development, and other preferences.
3. When creating guidelines, you should not invent preferences or make assumptions based on what you think a typical user might want. These should be specific to the conversation you had with the user. Your guidelines / rules should not be overly verbose.
4. Your guidelines should NOT be a recollection of the conversation up to this point in time, meaning you should NOT be including arbitrary details of the conversation.
Usage:
<new_rule>
<path>.clinerules/{file name}.md</path>
<content>Cline rule file content here</content>
</new_rule>
Example:
<new_rule>
<path>.clinerules/project-preferences.md</path>
<content>
## Brief overview
[Brief description of the rules, including if this set of guidelines is project-specific or global]
## Communication style
- [Description, rule, preference, instruction]
- [...]
## Development workflow
- [Description, rule, preference, instruction]
- [...]
## Coding best practices
- [Description, rule, preference, instruction]
- [...]
## Project context
- [Description, rule, preference, instruction]
- [...]
## Other guidelines
- [Description, rule, preference, instruction]
- [...]
</content>
</new_rule>
Below is the user's input when they indicated that they wanted to create a new Cline rule file.
</explicit_instructions>\n
`
+5 -6
View File
@@ -1,17 +1,16 @@
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse } from "../prompts/commands"
import { newTaskToolResponse, condenseToolResponse } from "../prompts/commands"
/**
* Processes text for slash commands and transforms them with appropriate instructions
* This is called after parseMentions() to process any slash commands in the user's message
*/
export function parseSlashCommands(text: string): { processedText: string; needsClinerulesFileCheck: boolean } {
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule"]
export function parseSlashCommands(text: string): string {
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact"]
const commandReplacements: Record<string, string> = {
newtask: newTaskToolResponse(),
smol: condenseToolResponse(),
compact: condenseToolResponse(),
newrule: newRuleToolResponse(),
}
// this currently allows matching prepended whitespace prior to /slash-command
@@ -48,11 +47,11 @@ export function parseSlashCommands(text: string): { processedText: string; needs
const textWithoutSlashCommand = text.substring(0, slashCommandStartIndex) + text.substring(slashCommandEndIndex)
const processedText = commandReplacements[commandName] + textWithoutSlashCommand
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" ? true : false }
return processedText
}
}
}
// if no supported commands are found, return the original text
return { processedText: text, needsClinerulesFileCheck: false }
return text
}
+52 -127
View File
@@ -86,7 +86,6 @@ import {
getGlobalClineRules,
getLocalClineRules,
refreshClineRulesToggles,
ensureLocalClinerulesDirExists,
} from "@core/context/instructions/user-instructions/cline-rules"
import { getGlobalState } from "@core/storage/state"
import { parseSlashCommands } from "@core/slash-commands"
@@ -927,7 +926,6 @@ export class Task {
let responseImages: string[] | undefined
if (response === "messageResponse") {
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
responseText = text
responseImages = images
}
@@ -1327,7 +1325,6 @@ export class Task {
if (userFeedback) {
await this.say("user_feedback", userFeedback.text, userFeedback.images)
await this.saveCheckpoint()
return [
true,
formatResponse.toolResult(
@@ -1364,7 +1361,6 @@ export class Task {
this.autoApprovalSettings.actions.readFiles,
this.autoApprovalSettings.actions.readFilesExternally ?? false,
]
case "new_rule":
case "write_to_file":
case "replace_in_file":
return [
@@ -1690,8 +1686,6 @@ export class Task {
return `[${block.name} for creating a new task]`
case "condense":
return `[${block.name}]`
case "new_rule":
return `[${block.name} for '${block.params.path}']`
}
}
@@ -1765,7 +1759,6 @@ export class Task {
if (text || images?.length) {
pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
}
this.didRejectTool = true // Prevent further tool uses in this message
return false
@@ -1774,7 +1767,6 @@ export class Task {
if (text || images?.length) {
pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
}
return true
}
@@ -1833,7 +1825,6 @@ export class Task {
}
switch (block.name) {
case "new_rule":
case "write_to_file":
case "replace_in_file": {
const relPath: string | undefined = block.params.path
@@ -1849,7 +1840,7 @@ export class Task {
if (!accessAllowed) {
await this.say("clineignore_error", relPath)
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
await this.saveCheckpoint()
break
}
@@ -1905,7 +1896,6 @@ export class Task {
)
await this.diffViewProvider.revertChanges()
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
} else if (content) {
@@ -1963,28 +1953,21 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError(block.name, "path"))
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
if (block.name === "replace_in_file" && !diff) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("replace_in_file", "diff"))
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
if (block.name === "write_to_file" && !content) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "content"))
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
if (block.name === "new_rule" && !content) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("new_rule", "content"))
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
@@ -2043,7 +2026,6 @@ export class Task {
if (text || images?.length) {
pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
}
this.didRejectTool = true
didApprove = false
@@ -2053,14 +2035,12 @@ export class Task {
if (text || images?.length) {
pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
}
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
}
if (!didApprove) {
await this.diffViewProvider.revertChanges()
await this.saveCheckpoint()
break
}
}
@@ -2121,7 +2101,7 @@ export class Task {
await handleError("writing file", error)
await this.diffViewProvider.revertChanges()
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
}
@@ -2150,7 +2130,7 @@ export class Task {
if (!relPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
await this.saveCheckpoint()
break
}
@@ -2158,7 +2138,7 @@ export class Task {
if (!accessAllowed) {
await this.say("clineignore_error", relPath)
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
await this.saveCheckpoint()
break
}
@@ -2181,7 +2161,6 @@ export class Task {
this.removeLastPartialMessageIfExistsWithType("say", "tool")
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
await this.saveCheckpoint()
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
break
}
@@ -2194,12 +2173,12 @@ export class Task {
await this.fileContextTracker.trackFileContext(relPath, "read_tool")
pushToolResult(content)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("reading file", error)
await this.saveCheckpoint()
break
}
}
@@ -2230,7 +2209,7 @@ export class Task {
if (!relDirPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("list_files", "path"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2263,18 +2242,17 @@ export class Task {
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
await this.saveCheckpoint()
break
}
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
}
pushToolResult(result)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("listing files", error)
await this.saveCheckpoint()
break
}
}
@@ -2303,7 +2281,7 @@ export class Task {
if (!relDirPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("list_code_definition_names", "path"))
await this.saveCheckpoint()
break
}
@@ -2333,18 +2311,17 @@ export class Task {
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
await this.saveCheckpoint()
break
}
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
}
pushToolResult(result)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("parsing source code definitions", error)
await this.saveCheckpoint()
break
}
}
@@ -2377,13 +2354,13 @@ export class Task {
if (!relDirPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "path"))
await this.saveCheckpoint()
break
}
if (!regex) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "regex"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2415,18 +2392,17 @@ export class Task {
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
await this.saveCheckpoint()
break
}
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
}
pushToolResult(results)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("searching files", error)
await this.saveCheckpoint()
break
}
}
@@ -2442,7 +2418,6 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "action"))
await this.browserSession.closeBrowser()
await this.saveCheckpoint()
}
break
}
@@ -2486,7 +2461,7 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "url"))
await this.browserSession.closeBrowser()
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2502,7 +2477,6 @@ export class Task {
this.removeLastPartialMessageIfExistsWithType("say", "browser_action_launch")
const didApprove = await askApproval("browser_action_launch", url)
if (!didApprove) {
await this.saveCheckpoint()
break
}
}
@@ -2528,7 +2502,7 @@ export class Task {
await this.sayAndCreateMissingParamError("browser_action", "coordinate"),
)
await this.browserSession.closeBrowser()
await this.saveCheckpoint()
break // can't be within an inner switch
}
}
@@ -2537,7 +2511,7 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "text"))
await this.browserSession.closeBrowser()
await this.saveCheckpoint()
break
}
}
@@ -2586,7 +2560,7 @@ export class Task {
browserActionResult.screenshot ? [browserActionResult.screenshot] : [],
),
)
await this.saveCheckpoint()
break
case "close":
pushToolResult(
@@ -2594,7 +2568,7 @@ export class Task {
`The browser has been closed. You may now proceed to using other tools.`,
),
)
await this.saveCheckpoint()
break
}
@@ -2603,7 +2577,7 @@ export class Task {
} catch (error) {
await this.browserSession.closeBrowser() // if any error occurs, the browser session is terminated
await handleError("executing browser action", error)
await this.saveCheckpoint()
break
}
}
@@ -2631,7 +2605,7 @@ export class Task {
if (!command) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("execute_command", "command"))
await this.saveCheckpoint()
break
}
if (!requiresApprovalRaw) {
@@ -2639,7 +2613,7 @@ export class Task {
pushToolResult(
await this.sayAndCreateMissingParamError("execute_command", "requires_approval"),
)
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2655,7 +2629,7 @@ export class Task {
pushToolResult(
formatResponse.toolError(formatResponse.clineIgnoreError(ignoredFileAttemptedToAccess)),
)
await this.saveCheckpoint()
break
}
@@ -2687,7 +2661,6 @@ export class Task {
`${this.shouldAutoApproveTool(block.name) && requiresApprovalPerLLM ? COMMAND_REQ_APP_STRING : ""}`, // ugly hack until we refactor combineCommandSequences
)
if (!didApprove) {
await this.saveCheckpoint()
break
}
}
@@ -2723,7 +2696,7 @@ export class Task {
}
} catch (error) {
await handleError("executing command", error)
await this.saveCheckpoint()
break
}
}
@@ -2753,13 +2726,13 @@ export class Task {
if (!server_name) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "server_name"))
await this.saveCheckpoint()
break
}
if (!tool_name) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"))
await this.saveCheckpoint()
break
}
// arguments are optional, but if they are provided they must be valid JSON
@@ -2783,7 +2756,7 @@ export class Task {
formatResponse.invalidMcpToolArgumentError(server_name, tool_name),
),
)
await this.saveCheckpoint()
break
}
}
@@ -2810,7 +2783,6 @@ export class Task {
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
const didApprove = await askApproval("use_mcp_server", completeMessage)
if (!didApprove) {
await this.saveCheckpoint()
break
}
}
@@ -2862,7 +2834,7 @@ export class Task {
}
} catch (error) {
await handleError("executing MCP tool", error)
await this.saveCheckpoint()
break
}
}
@@ -2890,13 +2862,13 @@ export class Task {
if (!server_name) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "server_name"))
await this.saveCheckpoint()
break
}
if (!uri) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "uri"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2917,7 +2889,6 @@ export class Task {
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
const didApprove = await askApproval("use_mcp_server", completeMessage)
if (!didApprove) {
await this.saveCheckpoint()
break
}
}
@@ -2937,12 +2908,12 @@ export class Task {
.join("\n\n") || "(Empty response)"
await this.say("mcp_server_response", resourceResultPretty)
pushToolResult(formatResponse.toolResult(resourceResultPretty))
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("accessing MCP resource", error)
await this.saveCheckpoint()
break
}
}
@@ -2961,7 +2932,7 @@ export class Task {
if (!question) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("ask_followup_question", "question"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2998,12 +2969,12 @@ export class Task {
}
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("asking question", error)
await this.saveCheckpoint()
break
}
}
@@ -3017,7 +2988,6 @@ export class Task {
if (!context) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("new_task", "context"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -3046,12 +3016,10 @@ export class Task {
formatResponse.toolResult(`The user has created a new task with the provided context.`),
)
}
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("creating new task", error)
await this.saveCheckpoint()
break
}
}
@@ -3065,7 +3033,6 @@ export class Task {
if (!context) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("condense", "context"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -3108,12 +3075,10 @@ export class Task {
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
)
}
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("condensing context window", error)
await this.saveCheckpoint()
break
}
}
@@ -3174,7 +3139,6 @@ export class Task {
if (text || images?.length) {
telemetryService.captureOptionsIgnored(this.taskId, options.length, "plan")
await this.say("user_feedback", text ?? "", images)
await this.saveCheckpoint()
}
}
@@ -3321,14 +3285,12 @@ export class Task {
// complete command message
const didApprove = await askApproval("command", command)
if (!didApprove) {
await this.saveCheckpoint()
break
}
const [userRejected, execCommandResult] = await this.executeCommandTool(command!)
if (userRejected) {
this.didRejectTool = true
pushToolResult(execCommandResult)
await this.saveCheckpoint()
break
}
// user didn't reject, but the command may have output
@@ -3347,7 +3309,6 @@ export class Task {
break
}
await this.say("user_feedback", text ?? "", images)
await this.saveCheckpoint()
const toolResults: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
if (commandResult) {
@@ -3376,7 +3337,7 @@ export class Task {
}
} catch (error) {
await handleError("attempting completion", error)
await this.saveCheckpoint()
break
}
}
@@ -3478,6 +3439,9 @@ export class Task {
// Save checkpoint if this is the first API request
const isFirstRequest = this.clineMessages.filter((m) => m.say === "api_req_started").length === 0
if (isFirstRequest) {
await this.say("checkpoint_created") // no hash since we need to wait for CheckpointTracker to be initialized
}
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
@@ -3488,10 +3452,6 @@ export class Task {
}),
)
if (isFirstRequest) {
await this.say("checkpoint_created") // no hash since we need to wait for CheckpointTracker to be initialized
}
// use this opportunity to initialize the checkpoint tracker (can be expensive to initialize in the constructor)
// FIXME: right now we're letting users init checkpoints for old tasks, but this could be a problem if opening a task in the wrong workspace
// isNewTask &&
@@ -3522,16 +3482,7 @@ export class Task {
}
}
const [parsedUserContent, environmentDetails, clinerulesError] = await this.loadContext(userContent, includeFileDetails)
// error handling if the user uses the /newrule command & their .clinerules is a file, for file read operations didnt work properly
if (clinerulesError === true) {
await this.say(
"error",
"Issue with processing the /newrule command. Double check that, if '.clinerules' already exists, it's a directory and not a file. Otherwise there was an issue referencing this file/directory.",
)
}
const [parsedUserContent, environmentDetails] = await this.loadContext(userContent, includeFileDetails)
userContent = parsedUserContent
// add environment details as its own text block, separate from tool results
userContent.push({ type: "text", text: environmentDetails })
@@ -3816,14 +3767,11 @@ export class Task {
}
}
async loadContext(userContent: UserContent, includeFileDetails: boolean = false): Promise<[UserContent, string, boolean]> {
// Track if we need to check clinerulesFile
let needsClinerulesFileCheck = false
const processUserContent = async () => {
async loadContext(userContent: UserContent, includeFileDetails: boolean = false) {
return await Promise.all([
// This is a temporary solution to dynamically load context mentions from tool results. It checks for the presence of tags that indicate that the tool was rejected and feedback was provided (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions). However if we allow multiple tools responses in the future, we will need to parse mentions specifically within the user content tags.
// (Note: this caused the @/ import alias bug where file contents were being parsed as well, since v2 converted tool results to text blocks)
return await Promise.all(
Promise.all(
userContent.map(async (block) => {
if (block.type === "text") {
// We need to ensure any user generated content is wrapped in one of these tags so that we know to parse mentions
@@ -3834,45 +3782,22 @@ export class Task {
block.text.includes("<task>") ||
block.text.includes("<user_message>")
) {
const parsedText = await parseMentions(
block.text,
cwd,
this.urlContentFetcher,
this.fileContextTracker,
)
let parsedText = await parseMentions(block.text, cwd, this.urlContentFetcher, this.fileContextTracker)
// when parsing slash commands, we still want to allow the user to provide their desired context
const { processedText, needsClinerulesFileCheck: needsCheck } = parseSlashCommands(parsedText)
if (needsCheck) {
needsClinerulesFileCheck = true
}
parsedText = parseSlashCommands(parsedText)
return {
...block,
text: processedText,
text: parsedText,
}
}
}
return block
}),
)
}
// Run initial promises in parallel
const [processedUserContent, environmentDetails] = await Promise.all([
processUserContent(),
),
this.getEnvironmentDetails(includeFileDetails),
])
// After processing content, check clinerulesData if needed
let clinerulesError = false
if (needsClinerulesFileCheck) {
clinerulesError = await ensureLocalClinerulesDirExists(cwd)
}
// Return all results
return [processedUserContent, environmentDetails, clinerulesError]
}
async getEnvironmentDetails(includeFileDetails: boolean = false) {
+30 -22
View File
@@ -521,21 +521,6 @@ export class McpHub {
this.isConnecting = false
}
/**
* Gets sorted MCP servers based on the order defined in settings
* @param serverOrder Array of server names in the order they appear in settings
* @returns Array of McpServer objects sorted according to settings order
*/
private getSortedMcpServers(serverOrder: string[]): McpServer[] {
return [...this.connections]
.sort((a, b) => {
const indexA = serverOrder.indexOf(a.server.name)
const indexB = serverOrder.indexOf(b.server.name)
return indexA - indexB
})
.map((connection) => connection.server)
}
private async notifyWebviewOfServerChanges(): Promise<void> {
// servers should always be sorted in the order they are defined in the settings file
const settingsPath = await this.getMcpSettingsFilePath()
@@ -544,7 +529,13 @@ export class McpHub {
const serverOrder = Object.keys(config.mcpServers || {})
await this.postMessageToWebview({
type: "mcpServers",
mcpServers: this.getSortedMcpServers(serverOrder),
mcpServers: [...this.connections]
.sort((a, b) => {
const indexA = serverOrder.indexOf(a.server.name)
const indexB = serverOrder.indexOf(b.server.name)
return indexA - indexB
})
.map((connection) => connection.server),
})
}
@@ -575,7 +566,16 @@ export class McpHub {
}
const serverOrder = Object.keys(config.mcpServers || {})
return this.getSortedMcpServers(serverOrder)
const mcpServers = [...this.connections]
.sort((a, b) => {
const indexA = serverOrder.indexOf(a.server.name)
const indexB = serverOrder.indexOf(b.server.name)
return indexA - indexB
})
.map((connection) => connection.server)
return mcpServers
}
console.error(`Server "${serverName}" not found in MCP configuration`)
throw new Error(`Server "${serverName}" not found in MCP configuration`)
@@ -691,7 +691,7 @@ export class McpHub {
}
}
public async addRemoteServer(serverName: string, serverUrl: string): Promise<McpServer[]> {
public async addRemoteServer(serverName: string, serverUrl: string) {
try {
const settings = await this.readAndValidateMcpSettingsFile()
if (!settings) {
@@ -728,12 +728,12 @@ export class McpHub {
JSON.stringify({ mcpServers: { ...settings.mcpServers, [serverName]: serverConfig } }, null, 2),
)
await this.updateServerConnectionsRPC(settings.mcpServers)
await this.updateServerConnections(settings.mcpServers)
const serverOrder = Object.keys(settings.mcpServers || {})
return this.getSortedMcpServers(serverOrder)
vscode.window.showInformationMessage(`Added ${serverName} MCP server`)
} catch (error) {
console.error("Failed to add remote MCP server:", error)
throw error
}
}
@@ -791,7 +791,15 @@ export class McpHub {
await this.updateServerConnectionsRPC(config.mcpServers)
const serverOrder = Object.keys(config.mcpServers || {})
return this.getSortedMcpServers(serverOrder)
const updatedMcpServers = [...this.connections]
.sort((a, b) => {
const indexA = serverOrder.indexOf(a.server.name)
const indexB = serverOrder.indexOf(b.server.name)
return indexA - indexB
})
.map((connection) => connection.server)
return updatedMcpServers
} catch (error) {
console.error("Failed to update server timeout:", error)
if (error instanceof Error) {
+6
View File
@@ -37,6 +37,7 @@ export interface ExtensionMessage {
| "openGraphData"
| "isImageUrlResult"
| "didUpdateSettings"
| "addRemoteServerResult"
| "userCreditsBalance"
| "userCreditsUsage"
| "userCreditsPayments"
@@ -102,6 +103,11 @@ export interface ExtensionMessage {
type: "file" | "folder"
label?: string
}>
addRemoteServerResult?: {
success: boolean
serverName: string
error?: string
}
tab?: McpViewTab
grpc_response?: {
message?: any // JSON serialized protobuf message
+2
View File
@@ -9,6 +9,7 @@ import { McpViewTab } from "./mcp"
export interface WebviewMessage {
type:
| "addRemoteServer"
| "apiConfiguration"
| "webviewDidLaunch"
| "newTask"
@@ -50,6 +51,7 @@ export interface WebviewMessage {
| "downloadMcp"
| "silentlyRefreshMcpMarketplace"
| "searchCommits"
| "showMcpView"
| "fetchLatestMcpServersFromHub"
| "telemetrySetting"
| "openSettings"
-107
View File
@@ -65,12 +65,6 @@ export interface UpdateMcpTimeoutRequest {
timeout: number
}
export interface AddRemoteMcpServerRequest {
metadata?: Metadata | undefined
serverName: string
serverUrl: string
}
export interface McpTool {
name: string
description?: string | undefined
@@ -294,99 +288,6 @@ export const UpdateMcpTimeoutRequest: MessageFns<UpdateMcpTimeoutRequest> = {
},
}
function createBaseAddRemoteMcpServerRequest(): AddRemoteMcpServerRequest {
return { metadata: undefined, serverName: "", serverUrl: "" }
}
export const AddRemoteMcpServerRequest: MessageFns<AddRemoteMcpServerRequest> = {
encode(message: AddRemoteMcpServerRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.serverName !== "") {
writer.uint32(18).string(message.serverName)
}
if (message.serverUrl !== "") {
writer.uint32(26).string(message.serverUrl)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): AddRemoteMcpServerRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseAddRemoteMcpServerRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.serverName = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.serverUrl = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): AddRemoteMcpServerRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
serverName: isSet(object.serverName) ? globalThis.String(object.serverName) : "",
serverUrl: isSet(object.serverUrl) ? globalThis.String(object.serverUrl) : "",
}
},
toJSON(message: AddRemoteMcpServerRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.serverName !== "") {
obj.serverName = message.serverName
}
if (message.serverUrl !== "") {
obj.serverUrl = message.serverUrl
}
return obj
},
create<I extends Exact<DeepPartial<AddRemoteMcpServerRequest>, I>>(base?: I): AddRemoteMcpServerRequest {
return AddRemoteMcpServerRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<AddRemoteMcpServerRequest>, I>>(object: I): AddRemoteMcpServerRequest {
const message = createBaseAddRemoteMcpServerRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.serverName = object.serverName ?? ""
message.serverUrl = object.serverUrl ?? ""
return message
},
}
function createBaseMcpTool(): McpTool {
return { name: "", description: undefined, inputSchema: undefined, autoApprove: undefined }
}
@@ -996,14 +897,6 @@ export const McpServiceDefinition = {
responseStream: false,
options: {},
},
addRemoteMcpServer: {
name: "addRemoteMcpServer",
requestType: AddRemoteMcpServerRequest,
requestStream: false,
responseType: McpServers,
responseStream: false,
options: {},
},
},
} as const
-17
View File
@@ -1,17 +0,0 @@
const Module = require("module")
const originalRequire = Module.prototype.require
/**
* VSCode is not available during unit tests
* @see {@link file://./vscode-mock.ts}
*/
Module.prototype.require = function (path: string) {
if (path === "vscode") {
return require("./vscode-mock")
}
return originalRequire.call(this, path)
}
// Required to have access to String.prototype.toPosix
import "../utils/path"
-1
View File
@@ -1 +0,0 @@
export {}
+65 -4047
View File
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -15,13 +15,11 @@
},
"dependencies": {
"@floating-ui/react": "^0.27.4",
"@heroui/react": "^2.8.0-beta.2",
"@vscode/webview-ui-toolkit": "^1.4.0",
"debounce": "^2.1.1",
"dompurify": "^3.2.4",
"fast-deep-equal": "^3.1.3",
"firebase": "^11.3.0",
"framer-motion": "^12.7.4",
"fuse.js": "^7.0.0",
"fzf": "^0.5.2",
"mermaid": "^11.4.1",
@@ -44,7 +42,7 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@tailwindcss/vite": "^4.1.4",
"@tailwindcss/vite": "^4.0.12",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
@@ -62,7 +60,7 @@
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"jsdom": "^26.0.0",
"tailwindcss": "^4.1.4",
"tailwindcss": "^4.0.12",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.2.6",
+55 -60
View File
@@ -6,71 +6,64 @@ import HistoryView from "./components/history/HistoryView"
import SettingsView from "./components/settings/SettingsView"
import WelcomeView from "./components/welcome/WelcomeView"
import AccountView from "./components/account/AccountView"
import { useExtensionState } from "./context/ExtensionStateContext"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { vscode } from "./utils/vscode"
import McpView from "./components/mcp/configuration/McpConfigurationView"
import { Providers } from "./Providers"
import { McpViewTab } from "@shared/mcp"
const AppContent = () => {
const { didHydrateState, showWelcome, shouldShowAnnouncement, showMcp, mcpTab } = useExtensionState()
const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, vscMachineId } = useExtensionState()
const [showSettings, setShowSettings] = useState(false)
const hideSettings = useCallback(() => setShowSettings(false), [])
const [showHistory, setShowHistory] = useState(false)
const [showMcp, setShowMcp] = useState(false)
const [showAccount, setShowAccount] = useState(false)
const [showAnnouncement, setShowAnnouncement] = useState(false)
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
const { setShowMcp, setMcpTab } = useExtensionState()
const closeMcpView = useCallback(() => {
setShowMcp(false)
setMcpTab(undefined)
}, [setShowMcp, setMcpTab])
const handleMessage = useCallback(
(e: MessageEvent) => {
const message: ExtensionMessage = e.data
switch (message.type) {
case "action":
switch (message.action!) {
case "settingsButtonClicked":
setShowSettings(true)
setShowHistory(false)
closeMcpView()
setShowAccount(false)
break
case "historyButtonClicked":
setShowSettings(false)
setShowHistory(true)
closeMcpView()
setShowAccount(false)
break
case "mcpButtonClicked":
setShowSettings(false)
setShowHistory(false)
if (message.tab) {
setMcpTab(message.tab)
}
setShowMcp(true)
setShowAccount(false)
break
case "accountButtonClicked":
setShowSettings(false)
setShowHistory(false)
closeMcpView()
setShowAccount(true)
break
case "chatButtonClicked":
setShowSettings(false)
setShowHistory(false)
closeMcpView()
setShowAccount(false)
break
}
break
}
},
[setShowMcp, setMcpTab, closeMcpView],
)
const handleMessage = useCallback((e: MessageEvent) => {
const message: ExtensionMessage = e.data
switch (message.type) {
case "action":
switch (message.action!) {
case "settingsButtonClicked":
setShowSettings(true)
setShowHistory(false)
setShowMcp(false)
setShowAccount(false)
break
case "historyButtonClicked":
setShowSettings(false)
setShowHistory(true)
setShowMcp(false)
setShowAccount(false)
break
case "mcpButtonClicked":
setShowSettings(false)
setShowHistory(false)
if (message.tab) {
setMcpTab(message.tab)
}
setShowMcp(true)
setShowAccount(false)
break
case "accountButtonClicked":
setShowSettings(false)
setShowHistory(false)
setShowMcp(false)
setShowAccount(true)
break
case "chatButtonClicked":
setShowSettings(false)
setShowHistory(false)
setShowMcp(false)
setShowAccount(false)
break
}
break
}
}, [])
useEvent("message", handleMessage)
@@ -102,13 +95,13 @@ const AppContent = () => {
<>
{showSettings && <SettingsView onDone={hideSettings} />}
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
{showMcp && <McpView initialTab={mcpTab} onDone={closeMcpView} />}
{showMcp && <McpView initialTab={mcpTab} onDone={() => setShowMcp(false)} />}
{showAccount && <AccountView onDone={() => setShowAccount(false)} />}
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}
<ChatView
showHistoryView={() => {
setShowSettings(false)
closeMcpView()
setShowMcp(false)
setShowAccount(false)
setShowHistory(true)
}}
@@ -126,9 +119,11 @@ const AppContent = () => {
const App = () => {
return (
<Providers>
<AppContent />
</Providers>
<ExtensionStateContextProvider>
<FirebaseAuthProvider>
<AppContent />
</FirebaseAuthProvider>
</ExtensionStateContextProvider>
)
}
-15
View File
@@ -1,15 +0,0 @@
import type { ReactNode } from "react"
import { ExtensionStateContextProvider } from "./context/ExtensionStateContext"
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { HeroUIProvider } from "@heroui/react"
export function Providers({ children }: { children: ReactNode }) {
return (
<ExtensionStateContextProvider>
<FirebaseAuthProvider>
<HeroUIProvider>{children}</HeroUIProvider>
</FirebaseAuthProvider>
</ExtensionStateContextProvider>
)
}
+81 -279
View File
@@ -237,7 +237,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
) => {
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
const [isDraggingOver, setIsDraggingOver] = useState(false)
const [gitCommits, setGitCommits] = useState<GitCommit[]>([])
const [showSlashCommandsMenu, setShowSlashCommandsMenu] = useState(false)
@@ -267,10 +266,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const [menuPosition, setMenuPosition] = useState(0)
const [shownTooltipMode, setShownTooltipMode] = useState<ChatSettings["mode"] | null>(null)
const [pendingInsertions, setPendingInsertions] = useState<string[]>([])
const [showShiftDragTip, setShowShiftDragTip] = useState(false)
const shiftHoldTimerRef = useRef<NodeJS.Timeout | null>(null)
const [showUnsupportedFileError, setShowUnsupportedFileError] = useState(false)
const unsupportedFileTimerRef = useRef<NodeJS.Timeout | null>(null)
const [fileSearchResults, setFileSearchResults] = useState<SearchResult[]>([])
const [searchLoading, setSearchLoading] = useState(false)
@@ -1021,84 +1016,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
}, [showModelSelector])
// Effect for Shift key hold detection
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Shift" && !event.repeat) {
// Start timer only if Shift is pressed and not already held down
if (shiftHoldTimerRef.current === null) {
shiftHoldTimerRef.current = setTimeout(() => {
setShowShiftDragTip(true)
}, 250) // 250ms delay
}
}
}
const handleKeyUp = (event: KeyboardEvent) => {
if (event.key === "Shift") {
// Clear timer and hide tip when Shift is released
if (shiftHoldTimerRef.current !== null) {
clearTimeout(shiftHoldTimerRef.current)
shiftHoldTimerRef.current = null
}
setShowShiftDragTip(false)
}
}
// Add listeners
window.addEventListener("keydown", handleKeyDown)
window.addEventListener("keyup", handleKeyUp)
// Cleanup listeners on component unmount
return () => {
window.removeEventListener("keydown", handleKeyDown)
window.removeEventListener("keyup", handleKeyUp)
// Clear any running timer on unmount
if (shiftHoldTimerRef.current !== null) {
clearTimeout(shiftHoldTimerRef.current)
}
}
}, []) // Empty dependency array ensures this runs only once on mount/unmount
// Function to show error message for unsupported files for drag and drop
const showUnsupportedFileErrorMessage = () => {
// Show error message for unsupported files
setShowUnsupportedFileError(true)
// Clear any existing timer
if (unsupportedFileTimerRef.current) {
clearTimeout(unsupportedFileTimerRef.current)
}
// Set timer to hide error after 3 seconds
unsupportedFileTimerRef.current = setTimeout(() => {
setShowUnsupportedFileError(false)
unsupportedFileTimerRef.current = null
}, 3000)
}
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault()
setIsDraggingOver(true)
// Check if files are being dragged
if (e.dataTransfer.types.includes("Files")) {
// Check if any of the files are not images
const items = Array.from(e.dataTransfer.items)
const hasNonImageFile = items.some((item) => {
if (item.kind === "file") {
const type = item.type.split("/")[0]
return type !== "image"
}
return false
})
if (hasNonImageFile) {
showUnsupportedFileErrorMessage()
}
}
}
/**
* Handles the drag over event to allow dropping.
* Prevents the default behavior to enable drop.
@@ -1107,37 +1024,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
*/
const onDragOver = (e: React.DragEvent) => {
e.preventDefault()
// Ensure state remains true if dragging continues over the element
if (!isDraggingOver) {
setIsDraggingOver(true)
}
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
// Check if the related target is still within the drop zone; prevents flickering
const dropZone = e.currentTarget as HTMLElement
if (!dropZone.contains(e.relatedTarget as Node)) {
setIsDraggingOver(false)
// Don't clear the error message here, let it time out naturally
}
}
// Effect to detect when drag operation ends outside the component
useEffect(() => {
const handleGlobalDragEnd = () => {
// This will be triggered when the drag operation ends anywhere
setIsDraggingOver(false)
// Don't clear error message, let it time out naturally
}
document.addEventListener("dragend", handleGlobalDragEnd)
return () => {
document.removeEventListener("dragend", handleGlobalDragEnd)
}
}, [])
/**
* Handles the drop event for files and text.
* Processes dropped images and text, updating the state accordingly.
@@ -1146,14 +1034,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
*/
const onDrop = async (e: React.DragEvent) => {
e.preventDefault()
setIsDraggingOver(false) // Reset state on drop
// Clear any error message when something is actually dropped
setShowUnsupportedFileError(false)
if (unsupportedFileTimerRef.current) {
clearTimeout(unsupportedFileTimerRef.current)
unsupportedFileTimerRef.current = null
}
// --- 1. VSCode Explorer Drop Handling ---
let uris: string[] = []
@@ -1273,37 +1153,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
opacity: textAreaDisabled ? 0.5 : 1,
position: "relative",
display: "flex",
// Drag-over styles moved to DynamicTextArea
transition: "background-color 0.1s ease-in-out, border 0.1s ease-in-out",
}}
onDrop={onDrop}
onDragOver={onDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}>
{showUnsupportedFileError && (
<div
style={{
position: "absolute",
inset: "10px 15px",
backgroundColor: "rgba(var(--vscode-errorForeground-rgb), 0.1)",
border: "2px solid var(--vscode-errorForeground)",
borderRadius: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 10,
pointerEvents: "none",
}}>
<span
style={{
color: "var(--vscode-errorForeground)",
fontWeight: "bold",
fontSize: "12px",
}}>
Only image files are supported
</span>
</div>
)}
onDragOver={onDragOver}>
{showSlashCommandsMenu && (
<div ref={slashCommandsMenuContainerRef}>
<SlashCommandMenu
@@ -1398,7 +1250,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
onHeightChange?.(height)
}}
placeholder={showUnsupportedFileError ? "" : placeholderText}
placeholder={placeholderText}
maxRows={10}
autoFocus={true}
style={{
@@ -1430,13 +1282,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
cursor: textAreaDisabled ? "not-allowed" : undefined,
flex: 1,
zIndex: 1,
outline:
isDraggingOver && !showUnsupportedFileError // Only show drag outline if not showing error
? "2px dashed var(--vscode-focusBorder)"
: isTextAreaFocused
? `1px solid ${chatSettings.mode === "plan" ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)"}`
: "none",
outlineOffset: isDraggingOver && !showUnsupportedFileError ? "1px" : "0px", // Add offset for drag-over outline
outline: isTextAreaFocused
? `1px solid ${chatSettings.mode === "plan" ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)"}`
: "none",
}}
onScroll={() => updateHighlights()}
/>
@@ -1498,129 +1346,83 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</div>
<ControlsContainer>
{/* Always render both components, but control visibility with CSS */}
<div
style={{
position: "relative",
flex: 1,
minWidth: 0,
height: "28px", // Fixed height to prevent container shrinking
}}>
{/* ButtonGroup - always in DOM but visibility controlled */}
<ButtonGroup
style={{
opacity: showShiftDragTip ? 0 : 1,
pointerEvents: showShiftDragTip ? "none" : "auto",
position: "absolute",
top: 0,
left: 0,
right: 0,
transition: "opacity 0.3s ease-in-out",
transitionDelay: showShiftDragTip ? "0s" : "0.2s",
width: "100%",
height: "100%",
zIndex: showShiftDragTip ? 0 : 1,
}}>
<Tooltip tipText="Add Context" style={{ left: 0 }}>
<VSCodeButton
data-testid="context-button"
appearance="icon"
aria-label="Add Context"
disabled={textAreaDisabled}
onClick={handleContextButtonClick}
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span className="flex items-center" style={{ fontSize: "13px", marginBottom: 1 }}>
@
</span>
</ButtonContainer>
</VSCodeButton>
</Tooltip>
<ButtonGroup>
<Tooltip tipText="Add Context" style={{ left: 0 }}>
<VSCodeButton
data-testid="context-button"
appearance="icon"
aria-label="Add Context"
disabled={textAreaDisabled}
onClick={handleContextButtonClick}
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span className="flex items-center" style={{ fontSize: "13px", marginBottom: 1 }}>
@
</span>
{/* {showButtonText && <span style={{ fontSize: "10px" }}>Context</span>} */}
</ButtonContainer>
</VSCodeButton>
</Tooltip>
<Tooltip tipText="Add Images">
<VSCodeButton
data-testid="images-button"
appearance="icon"
aria-label="Add Images"
disabled={shouldDisableImages}
onClick={() => {
if (!shouldDisableImages) {
onSelectImages()
}
}}
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span
className="codicon codicon-device-camera flex items-center"
style={{ fontSize: "14px", marginBottom: -3 }}
/>
</ButtonContainer>
</VSCodeButton>
</Tooltip>
<ServersToggleModal />
<ClineRulesToggleModal />
<ModelContainer ref={modelSelectorRef}>
<ModelButtonWrapper ref={buttonRef}>
<ModelDisplayButton
role="button"
isActive={showModelSelector}
disabled={false}
title="Select Model / API Provider"
onClick={handleModelButtonClick}
tabIndex={0}>
<ModelButtonContent>{modelDisplayName}</ModelButtonContent>
</ModelDisplayButton>
</ModelButtonWrapper>
{showModelSelector && (
<ModelSelectorTooltip
arrowPosition={arrowPosition}
menuPosition={menuPosition}
style={{
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
}}>
<ApiOptions
showModelOptions={true}
apiErrorMessage={undefined}
modelIdErrorMessage={undefined}
isPopup={true}
saveImmediately={true} // Ensure popup saves immediately
/>
</ModelSelectorTooltip>
)}
</ModelContainer>
</ButtonGroup>
{/* Shift Tip - always in DOM but visibility controlled */}
<div
style={{
display: "flex",
justifyContent: "flex-start", // Left align horizontally
alignItems: "center", // Center vertically
height: "100%", // Fill the container height
padding: "4px 0", // Add padding to match button group height
boxSizing: "border-box", // Include padding in height calculation
opacity: showShiftDragTip ? 1 : 0,
pointerEvents: showShiftDragTip ? "auto" : "none",
position: "absolute",
top: 0,
left: 0,
right: 0,
transition: "opacity 0.3s ease-in-out",
transitionDelay: showShiftDragTip ? "0.2s" : "0s",
width: "100%",
zIndex: showShiftDragTip ? 1 : 0,
}}>
<span
style={{
fontSize: "10px",
color: "var(--vscode-descriptionForeground)",
whiteSpace: "nowrap",
}}>
Hold Shift to Drop Files
</span>
</div>
</div>
{/* Tooltip for Plan/Act toggle remains outside the conditional rendering */}
<Tooltip tipText="Add Images">
<VSCodeButton
data-testid="images-button"
appearance="icon"
aria-label="Add Images"
disabled={shouldDisableImages}
onClick={() => {
if (!shouldDisableImages) {
onSelectImages()
}
}}
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span
className="codicon codicon-device-camera flex items-center"
style={{ fontSize: "14px", marginBottom: -3 }}
/>
{/* {showButtonText && <span style={{ fontSize: "10px" }}>Images</span>} */}
</ButtonContainer>
</VSCodeButton>
</Tooltip>
<ServersToggleModal />
<ClineRulesToggleModal />
<ModelContainer ref={modelSelectorRef}>
<ModelButtonWrapper ref={buttonRef}>
<ModelDisplayButton
role="button"
isActive={showModelSelector}
disabled={false}
title="Select Model / API Provider"
onClick={handleModelButtonClick}
// onKeyDown={(e) => {
// if (e.key === "Enter" || e.key === " ") {
// e.preventDefault()
// handleModelButtonClick()
// }
// }}
tabIndex={0}>
<ModelButtonContent>{modelDisplayName}</ModelButtonContent>
</ModelDisplayButton>
</ModelButtonWrapper>
{showModelSelector && (
<ModelSelectorTooltip
arrowPosition={arrowPosition}
menuPosition={menuPosition}
style={{
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
}}>
<ApiOptions
showModelOptions={true}
apiErrorMessage={undefined}
modelIdErrorMessage={undefined}
isPopup={true}
saveImmediately={true} // Ensure popup saves immediately
/>
</ModelSelectorTooltip>
)}
</ModelContainer>
</ButtonGroup>
<Tooltip
style={{ zIndex: 1000 }}
visible={shownTooltipMode !== null}
@@ -1,7 +1,6 @@
import React, { useRef, useState, useEffect } from "react"
import { useClickAway, useWindowSize } from "react-use"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useNavigator } from "@/hooks/useNavigator"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import ServersToggleList from "@/components/mcp/configuration/tabs/installed/ServersToggleList"
import { vscode } from "@/utils/vscode"
@@ -10,7 +9,6 @@ import Tooltip from "@/components/common/Tooltip"
const ServersToggleModal: React.FC = () => {
const { mcpServers } = useExtensionState()
const { navigateToMcp } = useNavigator()
const [isVisible, setIsVisible] = useState(false)
const buttonRef = useRef<HTMLDivElement>(null)
const modalRef = useRef<HTMLDivElement>(null)
@@ -83,8 +81,11 @@ const ServersToggleModal: React.FC = () => {
<VSCodeButton
appearance="icon"
onClick={() => {
vscode.postMessage({
type: "showMcpView",
tab: "installed",
})
setIsVisible(false)
navigateToMcp("installed")
}}>
<span className="codicon codicon-gear text-[10px]"></span>
</VSCodeButton>
@@ -35,7 +35,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
onClose,
}) => {
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage } = useExtensionState()
const [isTaskExpanded, setIsTaskExpanded] = useState(true)
const [isTaskExpanded, setIsTaskExpanded] = useState(false)
const [isTextExpanded, setIsTextExpanded] = useState(false)
const [showSeeMore, setShowSeeMore] = useState(false)
const textContainerRef = useRef<HTMLDivElement>(null)
@@ -1,20 +1,50 @@
import { useRef, useState } from "react"
import { useCallback, useRef, useState } from "react"
import { vscode } from "@/utils/vscode"
import { VSCodeButton, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useEvent } from "react-use"
import { LINKS } from "@/constants"
import { McpServiceClient } from "@/services/grpc-client"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
import { useExtensionState } from "@/context/ExtensionStateContext"
const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) => {
const [serverName, setServerName] = useState("")
const [serverUrl, setServerUrl] = useState("")
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState("")
const [showConnectingMessage, setShowConnectingMessage] = useState(false)
const { setMcpServers } = useExtensionState()
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
// Store submitted values to check if the server was added
const submittedValues = useRef<{ name: string } | null>(null)
const handleMessage = useCallback(
(event: MessageEvent) => {
const message = event.data
if (
message.type === "addRemoteServerResult" &&
isSubmitting &&
submittedValues.current &&
message.addRemoteServerResult?.serverName === submittedValues.current.name
) {
if (message.addRemoteServerResult.success) {
// Handle success
setIsSubmitting(false)
setServerName("")
setServerUrl("")
submittedValues.current = null
onServerAdded()
setShowConnectingMessage(false)
} else {
// Handle error
setIsSubmitting(false)
setError(message.addRemoteServerResult.error || "Failed to add server")
setShowConnectingMessage(false)
}
}
},
[isSubmitting, onServerAdded],
)
useEvent("message", handleMessage)
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!serverName.trim()) {
@@ -35,29 +65,16 @@ const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) =
}
setError("")
submittedValues.current = { name: serverName.trim() }
setIsSubmitting(true)
setShowConnectingMessage(true)
try {
const servers = await McpServiceClient.addRemoteMcpServer({
serverName: serverName.trim(),
serverUrl: serverUrl.trim(),
})
setIsSubmitting(false)
const mcpServers = convertProtoMcpServersToMcpServers(servers)
setMcpServers(mcpServers)
setServerName("")
setServerUrl("")
onServerAdded()
setShowConnectingMessage(false)
} catch (error) {
setIsSubmitting(false)
setError(error instanceof Error ? error.message : "Failed to add server")
setShowConnectingMessage(false)
}
vscode.postMessage({
type: "addRemoteServer",
serverName: serverName.trim(),
serverUrl: serverUrl.trim(),
})
}
return (
@@ -11,7 +11,7 @@ import {
requestyDefaultModelInfo,
} from "../../../src/shared/api"
import { findLastIndex } from "@shared/array"
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
import { McpMarketplaceCatalog, McpServer } from "../../../src/shared/mcp"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { vscode } from "../utils/vscode"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
@@ -29,21 +29,12 @@ interface ExtensionStateContextType extends ExtensionState {
mcpMarketplaceCatalog: McpMarketplaceCatalog
filePaths: string[]
totalTasksSize: number | null
// View state
showMcp: boolean
mcpTab?: McpViewTab
// Setters
setApiConfiguration: (config: ApiConfiguration) => void
setCustomInstructions: (value?: string) => void
setTelemetrySetting: (value: TelemetrySetting) => void
setShowAnnouncement: (value: boolean) => void
setPlanActSeparateModelsSetting: (value: boolean) => void
setMcpServers: (value: McpServer[]) => void
// Navigation
setShowMcp: (value: boolean) => void
setMcpTab: (tab?: McpViewTab) => void
}
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -51,10 +42,6 @@ const ExtensionStateContext = createContext<ExtensionStateContextType | undefine
export const ExtensionStateContextProvider: React.FC<{
children: React.ReactNode
}> = ({ children }) => {
// UI view state
const [showMcp, setShowMcp] = useState(false)
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
const [state, setState] = useState<ExtensionState>({
version: "",
clineMessages: [],
@@ -213,8 +200,6 @@ export const ExtensionStateContextProvider: React.FC<{
mcpMarketplaceCatalog,
filePaths,
totalTasksSize,
showMcp,
mcpTab,
globalClineRulesToggles: state.globalClineRulesToggles || {},
localClineRulesToggles: state.localClineRulesToggles || {},
setApiConfiguration: (value) =>
@@ -243,8 +228,6 @@ export const ExtensionStateContextProvider: React.FC<{
shouldShowAnnouncement: value,
})),
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
setShowMcp,
setMcpTab,
}
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
-24
View File
@@ -1,24 +0,0 @@
import { useExtensionState } from "../context/ExtensionStateContext"
import { McpViewTab } from "@shared/mcp"
/**
* Hook for navigating between different views in the application.
*/
export const useNavigator = () => {
const { setShowMcp, setMcpTab } = useExtensionState()
/**
* Navigate to the MCP view
* @param tab Optional tab to show in the MCP view
*/
const navigateToMcp = (tab?: McpViewTab) => {
if (tab) {
setMcpTab(tab)
}
setShowMcp(true)
}
return {
navigateToMcp,
}
}
-2
View File
@@ -4,8 +4,6 @@
/* @import "tailwindcss/preflight.css" layer(base); */
@import "tailwindcss/utilities.css" layer(utilities);
@config "../tailwind.config.js";
textarea:focus {
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
}
-4
View File
@@ -12,10 +12,6 @@ export const SUPPORTED_SLASH_COMMANDS: SlashCommand[] = [
name: "smol",
description: "Condenses your current context window",
},
{
name: "newrule",
description: "Create a new Cline rule based on your conversation",
},
]
// Regex for detecting slash commands in text
-22
View File
@@ -1,22 +0,0 @@
const { heroui } = require("@heroui/react")
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}", "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
darkMode: "class",
plugins: [
heroui({
defaultTheme: "vscode",
themes: {
vscode: {
colors: {
background: "",
},
},
},
}),
],
}