mirror of
https://github.com/cline/cline.git
synced 2026-09-14 11:29:25 +08:00
Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ebc3eb825 | ||
|
|
5953a212f0 | ||
|
|
50496e4fd5 | ||
|
|
acc12df665 | ||
|
|
14230e7221 | ||
|
|
4b697d8695 | ||
|
|
deeda6e273 | ||
|
|
7e7844529f | ||
|
|
4196c14c9c | ||
|
|
5294e78dde | ||
|
|
d97424fcab | ||
|
|
2b3c0bb633 | ||
|
|
b8e2fd669d | ||
|
|
df7f9fcba4 | ||
|
|
db0b022b6e | ||
|
|
459adf0450 | ||
|
|
df37f29746 | ||
|
|
60c210b017 | ||
|
|
d4bd755e60 | ||
|
|
aed152b530 | ||
|
|
5a8e9d8fa8 | ||
|
|
7610cecde6 | ||
|
|
bb26b3b64d | ||
|
|
aabdeba0f3 | ||
|
|
fd68a81a26 | ||
|
|
0e07b92be2 | ||
|
|
4addffe94c |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Make sure protos run before dev build scripts
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
create clinerules folder if its currently a file and creating new rule
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add markdown copy to chat
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Move updateMcpTimeout message to protobus
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": minor
|
||||
---
|
||||
|
||||
Add support for custom API request timeout. Previously, timeouts were hardcoded to 30 seconds for providers like Ollama or 15 seconds for OpenRouter and Cline. Now users can set a custom timeout value in milliseconds through the settings interface.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
enabling mcp marketplace download counts
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add dependsOn to more blocks in the tasks.json
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding args to allow Cursor to open workspaces (for checkpoint testing/development)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add truncation notice when truncating manually
|
||||
@@ -2,4 +2,4 @@
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add smol command
|
||||
add newrule slash command
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
protobus migration for openImage
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add cache ui for open router and cline provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
showing expanded task by default
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add tooltips and improve contrast
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Refactor to not pass a message for showing the MCP View from the servers modal
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate the addRemoteServer to protobus
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Lowering Gemini cache TTL time
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding UI to show openrouter balance next to provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix cost calculation
|
||||
@@ -23,3 +23,5 @@ jobs:
|
||||
uses: codespell-project/codespell-problem-matcher@v1
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
only_warn: 1
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.15.1
|
||||
node-version: 22
|
||||
|
||||
# Setup Python for coverage script
|
||||
- name: Setup Python
|
||||
@@ -73,9 +73,8 @@ jobs:
|
||||
- name: Build Extension
|
||||
run: npm run compile
|
||||
|
||||
# Disabling due to compatability with test framework and ESM modules
|
||||
# - name: Unit Tests
|
||||
# run: npm run test:unit
|
||||
- name: Unit Tests
|
||||
run: npm run test:unit
|
||||
|
||||
# Run extension tests with coverage
|
||||
- name: Extension Tests with Coverage
|
||||
@@ -132,7 +131,7 @@ jobs:
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.15.1
|
||||
node-version: 22
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "src/**/__tests__/*.ts",
|
||||
"require": ["ts-node/register", "source-map-support/register"],
|
||||
"require": ["ts-node/register", "source-map-support/register", "./src/test/requires.ts"],
|
||||
"recursive": true
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -9,7 +9,7 @@
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-workspace-trust", "${workspaceFolder}"],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"postDebugTask": "stop",
|
||||
|
||||
Vendored
+13
-15
@@ -22,7 +22,7 @@
|
||||
"label": "watch",
|
||||
"dependsOn": ["npm: protos", "npm: build:webview", "npm: dev:webview", "npm: watch:tsc", "npm: watch:esbuild"],
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
"reveal": "always"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
@@ -39,7 +39,7 @@
|
||||
"npm: watch:esbuild:test"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
"reveal": "always"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
@@ -53,8 +53,7 @@
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
@@ -69,10 +68,10 @@
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
"label": "npm: build:webview:test",
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
@@ -104,10 +103,10 @@
|
||||
],
|
||||
"isBackground": true,
|
||||
"label": "npm: dev:webview",
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
@@ -125,8 +124,7 @@
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
@@ -144,8 +142,7 @@
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
@@ -161,10 +158,10 @@
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"label": "npm: watch:tsc",
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
"reveal": "always"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -172,8 +169,9 @@
|
||||
"script": "watch-tests",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"dependsOn": ["npm: protos"],
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
"reveal": "always",
|
||||
"group": "watchers"
|
||||
},
|
||||
"group": "build"
|
||||
|
||||
+7
-1
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## [3.13.3]
|
||||
|
||||
- Add download counts to MCP marketplace items
|
||||
- Add `/compact` command
|
||||
- Add prompt caching to gemini models in cline / openrouter providers
|
||||
- Add tooltips to bottom row menu
|
||||
|
||||
## [3.13.2]
|
||||
|
||||
- Add Gemini 2.5 Flash model to Vertex and Gemini Providers (Thanks monotykamary!)
|
||||
@@ -19,7 +26,6 @@
|
||||
- Fix Mermaid syntax error in documentation (Thanks tuki0918!)
|
||||
- Remove supportsComputerUse restriction and support browser use through any model that supports images (Thanks arafatkatze!)
|
||||
|
||||
|
||||
## [3.13.1]
|
||||
|
||||
- Fix bug where task cancellation during thinking stream would result in error state
|
||||
|
||||
+1
-1
@@ -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.13.2",
|
||||
"version": "3.13.3",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
// Service for account-related operations
|
||||
service AccountService {
|
||||
// Handles the user clicking the login link in the UI.
|
||||
// Generates a secure nonce for state validation, stores it in secrets,
|
||||
// and opens the authentication URL in the external browser.
|
||||
rpc accountLoginClicked(EmptyRequest) returns (String);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
@@ -8,6 +10,8 @@ service BrowserService {
|
||||
rpc getBrowserConnectionInfo(EmptyRequest) returns (BrowserConnectionInfo);
|
||||
rpc testBrowserConnection(StringRequest) returns (BrowserConnection);
|
||||
rpc discoverBrowser(EmptyRequest) returns (BrowserConnection);
|
||||
rpc getDetectedChromePath(EmptyRequest) returns (ChromePath);
|
||||
rpc updateBrowserSettings(UpdateBrowserSettingsRequest) returns (Boolean);
|
||||
}
|
||||
|
||||
message BrowserConnectionInfo {
|
||||
@@ -21,3 +25,26 @@ message BrowserConnection {
|
||||
string message = 2;
|
||||
optional string endpoint = 3;
|
||||
}
|
||||
|
||||
message ChromePath {
|
||||
string path = 1;
|
||||
bool is_bundled = 2;
|
||||
}
|
||||
|
||||
message Viewport {
|
||||
int32 width = 1;
|
||||
int32 height = 2;
|
||||
}
|
||||
|
||||
message BrowserSettings {
|
||||
Viewport viewport = 1;
|
||||
optional string remote_browser_host = 2;
|
||||
optional bool remote_browser_enabled = 3;
|
||||
}
|
||||
|
||||
message UpdateBrowserSettingsRequest {
|
||||
Metadata metadata = 1;
|
||||
Viewport viewport = 2;
|
||||
optional string remote_browser_host = 3;
|
||||
optional bool remote_browser_enabled = 4;
|
||||
}
|
||||
|
||||
@@ -77,10 +77,11 @@ async function generateMethodRegistrations() {
|
||||
console.log(chalk.cyan("Generating method registration files..."))
|
||||
|
||||
const serviceDirs = [
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "mcp"),
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "account"),
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "browser"),
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "checkpoints"),
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "file"),
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "mcp"),
|
||||
path.join(ROOT_DIR, "src", "core", "controller", "task"),
|
||||
// Add more service directories here as needed
|
||||
]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message Metadata {
|
||||
}
|
||||
@@ -38,3 +40,12 @@ message BytesRequest {
|
||||
message Bytes {
|
||||
bytes value = 1;
|
||||
}
|
||||
|
||||
message BooleanRequest {
|
||||
Metadata metadata = 1;
|
||||
bool value = 2;
|
||||
}
|
||||
|
||||
message Boolean {
|
||||
bool value = 1;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
@@ -8,4 +10,7 @@ import "common.proto";
|
||||
service FileService {
|
||||
// Opens a file in the editor
|
||||
rpc openFile(StringRequest) returns (Empty);
|
||||
|
||||
// Opens an image in the system viewer
|
||||
rpc openImage(StringRequest) returns (Empty);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
service McpService {
|
||||
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
|
||||
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
|
||||
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
|
||||
}
|
||||
|
||||
message ToggleMcpServerRequest {
|
||||
@@ -21,6 +24,12 @@ 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;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
|
||||
@@ -74,6 +74,8 @@ export class ClineHandler implements ApiHandler {
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
@@ -105,6 +107,9 @@ export class ClineHandler implements ApiHandler {
|
||||
const generation = response.data
|
||||
return {
|
||||
type: "usage",
|
||||
// at this time there's no support for gatting cached_tokens from generation endpoint
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
|
||||
@@ -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., 1 hour in seconds)
|
||||
const DEFAULT_CACHE_TTL_SECONDS = 3600
|
||||
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
|
||||
const DEFAULT_CACHE_TTL_SECONDS = 900
|
||||
|
||||
export class GeminiHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
|
||||
@@ -21,8 +21,9 @@ export class OllamaHandler implements ApiHandler {
|
||||
|
||||
try {
|
||||
// Create a promise that rejects after timeout
|
||||
const timeoutMs = this.options.requestTimeoutMs || 30000
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error("Ollama request timed out after 120 seconds")), 120000)
|
||||
setTimeout(() => reject(new Error(`Ollama request timed out after ${timeoutMs / 1000} seconds`)), timeoutMs)
|
||||
})
|
||||
|
||||
// Create the actual API request promise
|
||||
@@ -63,7 +64,8 @@ export class OllamaHandler implements ApiHandler {
|
||||
} catch (error: any) {
|
||||
// Check if it's a timeout error
|
||||
if (error.message && error.message.includes("timed out")) {
|
||||
throw new Error("Ollama request timed out after 120 seconds")
|
||||
const timeoutMs = this.options.requestTimeoutMs || 30000
|
||||
throw new Error(`Ollama request timed out after ${timeoutMs / 1000} seconds`)
|
||||
}
|
||||
|
||||
// Enhance error reporting
|
||||
|
||||
@@ -76,6 +76,8 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
@@ -103,8 +105,9 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
// console.log("OpenRouter generation details:", generation)
|
||||
return {
|
||||
type: "usage",
|
||||
// cacheWriteTokens: 0,
|
||||
// cacheReadTokens: 0,
|
||||
// at this time there's no support for gatting cached_tokens from generation endpoint
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
|
||||
@@ -20,7 +20,8 @@ export async function createOpenRouterStream(
|
||||
]
|
||||
|
||||
// prompt caching: https://openrouter.ai/docs/prompt-caching
|
||||
// this is specifically for claude models (some models may 'support prompt caching' automatically without this)
|
||||
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
|
||||
// gemini models only use the last breakpoint for caching, so the others will be ignored
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
@@ -39,6 +40,10 @@ export async function createOpenRouterStream(
|
||||
case "anthropic/claude-3-haiku:beta":
|
||||
case "anthropic/claude-3-opus":
|
||||
case "anthropic/claude-3-opus:beta":
|
||||
case "google/gemini-2.5-pro-preview-03-25":
|
||||
case "google/gemini-2.0-flash-001":
|
||||
case "google/gemini-flash-1.5":
|
||||
case "google/gemini-pro-1.5":
|
||||
openAiMessages[0] = {
|
||||
role: "system",
|
||||
content: [
|
||||
|
||||
@@ -25,6 +25,7 @@ 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,7 +55,10 @@ 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" && accumulator.endsWith(`</${contentParamName}>`)) {
|
||||
if (
|
||||
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
|
||||
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([1, 4])
|
||||
expect(result).to.deep.equal([2, 5])
|
||||
})
|
||||
|
||||
it("first truncation with quarter keep", () => {
|
||||
const messages = createMessages(11)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "quarter")
|
||||
|
||||
expect(result).to.deep.equal([1, 6])
|
||||
expect(result).to.deep.equal([2, 7])
|
||||
})
|
||||
|
||||
it("sequential truncation with half keep", () => {
|
||||
const messages = createMessages(21)
|
||||
const firstRange = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
expect(firstRange).to.deep.equal([1, 10])
|
||||
expect(firstRange).to.deep.equal([2, 9])
|
||||
|
||||
// Pass the previous range for sequential truncation
|
||||
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "half")
|
||||
expect(secondRange).to.deep.equal([1, 14])
|
||||
expect(secondRange).to.deep.equal([2, 13])
|
||||
})
|
||||
|
||||
it("sequential truncation with quarter keep", () => {
|
||||
@@ -60,7 +60,7 @@ describe("ContextManager", () => {
|
||||
|
||||
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "quarter")
|
||||
|
||||
expect(secondRange[0]).to.equal(1)
|
||||
expect(secondRange[0]).to.equal(2)
|
||||
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 a user message
|
||||
// Check if the message at the end of range is an assistant message
|
||||
const lastRemovedMessage = messages[result[1]]
|
||||
expect(lastRemovedMessage.role).to.equal("user")
|
||||
expect(lastRemovedMessage.role).to.equal("assistant")
|
||||
|
||||
// Check if the next message after the range is an assistant message
|
||||
// Check if the next message after the range is a user message
|
||||
const nextMessage = messages[result[1] + 1]
|
||||
expect(nextMessage.role).to.equal("assistant")
|
||||
expect(nextMessage.role).to.equal("user")
|
||||
})
|
||||
|
||||
it("handles small message arrays", () => {
|
||||
const messages = createMessages(3)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
expect(result).to.deep.equal([1, 0])
|
||||
expect(result).to.deep.equal([2, 1])
|
||||
})
|
||||
|
||||
it("preserves the message structure when truncating", () => {
|
||||
@@ -120,9 +120,10 @@ describe("ContextManager", () => {
|
||||
const range: [number, number] = [1, 3]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
|
||||
expect(result).to.have.lengthOf(2)
|
||||
expect(result).to.have.lengthOf(3)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
expect(result[1]).to.deep.equal(messages[4])
|
||||
expect(result[1]).to.deep.equal(messages[1])
|
||||
expect(result[2]).to.deep.equal(messages[4])
|
||||
})
|
||||
|
||||
it("works with a range that starts at the first message after task", () => {
|
||||
@@ -131,20 +132,21 @@ describe("ContextManager", () => {
|
||||
const range: [number, number] = [1, 2]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
|
||||
expect(result).to.have.lengthOf(2)
|
||||
expect(result).to.have.lengthOf(3)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
expect(result[1]).to.deep.equal(messages[3])
|
||||
expect(result[1]).to.deep.equal(messages[1])
|
||||
expect(result[2]).to.deep.equal(messages[3])
|
||||
})
|
||||
|
||||
it("correctly handles removing a range while preserving alternation pattern", () => {
|
||||
const messages = createMessages(5)
|
||||
|
||||
const range: [number, number] = [1, 2]
|
||||
const range: [number, number] = [2, 3]
|
||||
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[3])
|
||||
expect(result[1]).to.deep.equal(messages[1])
|
||||
expect(result[2]).to.deep.equal(messages[4])
|
||||
|
||||
expect(result[0].role).to.equal("user")
|
||||
|
||||
@@ -7,6 +7,45 @@ 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)) {
|
||||
@@ -182,7 +221,14 @@ export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: s
|
||||
filePath = path.join(globalClineRulesFilePath, filename)
|
||||
} else {
|
||||
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
|
||||
const hasError = await ensureLocalClinerulesDirExists(cwd)
|
||||
if (hasError === true) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
|
||||
await fs.mkdir(localClineRulesFilePath, { recursive: true })
|
||||
|
||||
filePath = path.join(localClineRulesFilePath, filename)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as vscode from "vscode"
|
||||
import crypto from "crypto"
|
||||
import { Controller } from "../index"
|
||||
import { storeSecret } from "../../storage/state"
|
||||
|
||||
/**
|
||||
* Handles the user clicking the login link in the UI.
|
||||
* Generates a secure nonce for state validation, stores it in secrets,
|
||||
* and opens the authentication URL in the external browser.
|
||||
*
|
||||
* @param controller The controller instance.
|
||||
* @returns The login URL as a string.
|
||||
*/
|
||||
export async function accountLoginClicked(controller: Controller): Promise<String> {
|
||||
// Generate nonce for state validation
|
||||
const nonce = crypto.randomBytes(32).toString("hex")
|
||||
await storeSecret(controller.context, "authNonce", nonce)
|
||||
|
||||
// Open browser for authentication with state param
|
||||
console.log("Login button clicked in account page")
|
||||
console.log("Opening auth page with state param")
|
||||
|
||||
const uriScheme = vscode.env.uriScheme
|
||||
|
||||
const authUrl = vscode.Uri.parse(
|
||||
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
|
||||
)
|
||||
vscode.env.openExternal(authUrl)
|
||||
return authUrl.toString()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
const accountService = createServiceRegistry("account")
|
||||
|
||||
// Export the method handler type and registration function
|
||||
export type AccountMethodHandler = ServiceMethodHandler
|
||||
export const registerMethod = accountService.registerMethod
|
||||
|
||||
// Export the request handler
|
||||
export const handleAccountServiceRequest = accountService.handleRequest
|
||||
|
||||
// Register all account methods
|
||||
registerAllMethods()
|
||||
@@ -0,0 +1,12 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { accountLoginClicked } from "./accountLoginClicked"
|
||||
|
||||
// Register all account service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("accountLoginClicked", accountLoginClicked)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ChromePath } from "../../../shared/proto/browser"
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { Controller } from "../index"
|
||||
import { getAllExtensionState } from "../../storage/state"
|
||||
import { BrowserSession } from "../../../services/browser/BrowserSession"
|
||||
|
||||
/**
|
||||
* Get the detected Chrome executable path
|
||||
* @param controller The controller instance
|
||||
* @param request The empty request message
|
||||
* @returns The detected Chrome path and whether it's bundled
|
||||
*/
|
||||
export async function getDetectedChromePath(controller: Controller, request: EmptyRequest): Promise<ChromePath> {
|
||||
try {
|
||||
const { browserSettings } = await getAllExtensionState(controller.context)
|
||||
const browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
const result = await browserSession.getDetectedChromePath()
|
||||
|
||||
return {
|
||||
path: result.path,
|
||||
isBundled: result.isBundled,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting detected Chrome path:", error)
|
||||
return {
|
||||
path: "",
|
||||
isBundled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,16 @@
|
||||
import { registerMethod } from "./index"
|
||||
import { discoverBrowser } from "./discoverBrowser"
|
||||
import { getBrowserConnectionInfo } from "./getBrowserConnectionInfo"
|
||||
import { getDetectedChromePath } from "./getDetectedChromePath"
|
||||
import { testBrowserConnection } from "./testBrowserConnection"
|
||||
import { updateBrowserSettings } from "./updateBrowserSettings"
|
||||
|
||||
// Register all browser service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("discoverBrowser", discoverBrowser)
|
||||
registerMethod("getBrowserConnectionInfo", getBrowserConnectionInfo)
|
||||
registerMethod("getDetectedChromePath", getDetectedChromePath)
|
||||
registerMethod("testBrowserConnection", testBrowserConnection)
|
||||
registerMethod("updateBrowserSettings", updateBrowserSettings)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { UpdateBrowserSettingsRequest } from "../../../shared/proto/browser"
|
||||
import { Boolean } from "../../../shared/proto/common"
|
||||
import { Controller } from "../index"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
import { BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings"
|
||||
|
||||
/**
|
||||
* Update browser settings
|
||||
* @param controller The controller instance
|
||||
* @param request The browser settings request message
|
||||
* @returns Success response
|
||||
*/
|
||||
export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise<Boolean> {
|
||||
try {
|
||||
// Convert from protobuf format to shared format
|
||||
const browserSettings: SharedBrowserSettings = {
|
||||
viewport: {
|
||||
width: request.viewport?.width || 900,
|
||||
height: request.viewport?.height || 600,
|
||||
},
|
||||
remoteBrowserEnabled: request.remoteBrowserEnabled || false,
|
||||
remoteBrowserHost: request.remoteBrowserHost || undefined,
|
||||
}
|
||||
|
||||
// Update global state with new settings
|
||||
await updateGlobalState(controller.context, "browserSettings", browserSettings)
|
||||
|
||||
// Update task browser settings if task exists
|
||||
if (controller.task) {
|
||||
controller.task.browserSettings = browserSettings
|
||||
controller.task.browserSession.browserSettings = browserSettings
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return {
|
||||
value: true,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating browser settings:", error)
|
||||
return {
|
||||
value: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,11 @@
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { openFile } from "./openFile"
|
||||
import { openImage } from "./openImage"
|
||||
|
||||
// Register all file service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("openFile", openFile)
|
||||
registerMethod("openImage", openImage)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "@shared/proto/common"
|
||||
import { openImage as openImageIntegration } from "@integrations/misc/open-file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
|
||||
/**
|
||||
* Opens an image in the system viewer
|
||||
* @param controller The controller instance
|
||||
* @param request The request message containing the image path or data URI in the 'value' field
|
||||
* @returns Empty response
|
||||
*/
|
||||
export const openImage: FileMethodHandler = async (controller: Controller, request: StringRequest): Promise<Empty> => {
|
||||
if (request.value) {
|
||||
await openImageIntegration(request.value)
|
||||
}
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Controller } from "./index"
|
||||
import { handleAccountServiceRequest } from "./account"
|
||||
import { handleBrowserServiceRequest } from "./browser/index"
|
||||
import { handleFileServiceRequest } from "./file"
|
||||
import { handleTaskServiceRequest } from "./task"
|
||||
@@ -31,6 +32,11 @@ export class GrpcHandler {
|
||||
}> {
|
||||
try {
|
||||
switch (service) {
|
||||
case "cline.AccountService":
|
||||
return {
|
||||
message: await handleAccountServiceRequest(this.controller, method, message),
|
||||
request_id: requestId,
|
||||
}
|
||||
case "cline.BrowserService":
|
||||
return {
|
||||
message: await handleBrowserServiceRequest(this.controller, method, message),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import axios from "axios"
|
||||
import type { AxiosRequestConfig } from "axios"
|
||||
import crypto from "crypto"
|
||||
|
||||
import fs from "fs/promises"
|
||||
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
import pWaitFor from "p-wait-for"
|
||||
@@ -12,7 +12,6 @@ import { buildApiHandler } from "@api/index"
|
||||
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
|
||||
import { downloadTask } from "@integrations/misc/export-markdown"
|
||||
import { fetchOpenGraphData, isImageUrl } from "@integrations/misc/link-preview"
|
||||
import { openImage } from "@integrations/misc/open-file"
|
||||
import { handleFileServiceRequest } from "./file"
|
||||
import { selectImages } from "@integrations/misc/process-images"
|
||||
import { getTheme } from "@integrations/theme/getTheme"
|
||||
@@ -187,28 +186,6 @@ 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()
|
||||
@@ -311,22 +288,6 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
break
|
||||
case "browserSettings":
|
||||
if (message.browserSettings) {
|
||||
// remoteBrowserEnabled now means "enable remote browser connection"
|
||||
// commenting out since this is being done in BrowserSettingsSection updateRemoteBrowserEnabled
|
||||
// if (!message.browserSettings.remoteBrowserEnabled) {
|
||||
// // If disabling remote browser connection, clear the remoteBrowserHost
|
||||
// message.browserSettings.remoteBrowserHost = undefined
|
||||
// }
|
||||
await updateGlobalState(this.context, "browserSettings", message.browserSettings)
|
||||
if (this.task) {
|
||||
this.task.browserSettings = message.browserSettings
|
||||
this.task.browserSession.browserSettings = message.browserSettings
|
||||
}
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
break
|
||||
case "togglePlanActMode":
|
||||
if (message.chatSettings) {
|
||||
await this.togglePlanActModeWithChatSettings(message.chatSettings, message.chatContent)
|
||||
@@ -409,9 +370,6 @@ export class Controller {
|
||||
await refreshClineRulesToggles(this.context, cwd)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "openImage":
|
||||
openImage(message.text!)
|
||||
break
|
||||
case "openInBrowser":
|
||||
if (message.url) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url))
|
||||
@@ -465,23 +423,6 @@ export class Controller {
|
||||
case "getLatestState":
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "accountLoginClicked": {
|
||||
// Generate nonce for state validation
|
||||
const nonce = crypto.randomBytes(32).toString("hex")
|
||||
await storeSecret(this.context, "authNonce", nonce)
|
||||
|
||||
// Open browser for authentication with state param
|
||||
console.log("Login button clicked in account page")
|
||||
console.log("Opening auth page with state param")
|
||||
|
||||
const uriScheme = vscode.env.uriScheme
|
||||
|
||||
const authUrl = vscode.Uri.parse(
|
||||
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
|
||||
)
|
||||
vscode.env.openExternal(authUrl)
|
||||
break
|
||||
}
|
||||
case "accountLogoutClicked": {
|
||||
await this.handleSignOut()
|
||||
break
|
||||
@@ -494,10 +435,6 @@ 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) {
|
||||
@@ -732,21 +669,6 @@ export class Controller {
|
||||
this.postMessageToWebview({ type: "relinquishControl" })
|
||||
break
|
||||
}
|
||||
case "getDetectedChromePath": {
|
||||
try {
|
||||
const { browserSettings } = await getAllExtensionState(this.context)
|
||||
const browserSession = new BrowserSession(this.context, browserSettings)
|
||||
const { path, isBundled } = await browserSession.getDetectedChromePath()
|
||||
await this.postMessageToWebview({
|
||||
type: "detectedChromePath",
|
||||
text: path,
|
||||
isBundled,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error getting detected Chrome path:", error)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "getRelativePaths": {
|
||||
if (message.uris && message.uris.length > 0) {
|
||||
const resolvedPaths = await Promise.all(
|
||||
@@ -853,6 +775,15 @@ export class Controller {
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "copyToClipboard": {
|
||||
try {
|
||||
await vscode.env.clipboard.writeText(message.text || "")
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error)
|
||||
}
|
||||
break
|
||||
}
|
||||
// Add more switch case statements here as more webview message commands
|
||||
// are created within the webview context (i.e. inside media/main.js)
|
||||
}
|
||||
@@ -1502,6 +1433,14 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
modelInfo.cacheWritesPrice = 0.14
|
||||
modelInfo.cacheReadsPrice = 0.014
|
||||
break
|
||||
case "google/gemini-2.5-pro-preview-03-25":
|
||||
case "google/gemini-2.0-flash-001":
|
||||
case "google/gemini-flash-1.5":
|
||||
case "google/gemini-pro-1.5":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
|
||||
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
|
||||
break
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,14 @@
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -87,3 +87,61 @@ 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
|
||||
`
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { newTaskToolResponse, condenseToolResponse } from "../prompts/commands"
|
||||
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse } 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): string {
|
||||
const SUPPORTED_COMMANDS = ["newtask", "smol"]
|
||||
export function parseSlashCommands(text: string): { processedText: string; needsClinerulesFileCheck: boolean } {
|
||||
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule"]
|
||||
|
||||
const commandReplacements: Record<string, string> = {
|
||||
newtask: newTaskToolResponse(),
|
||||
smol: condenseToolResponse(),
|
||||
compact: condenseToolResponse(),
|
||||
newrule: newRuleToolResponse(),
|
||||
}
|
||||
|
||||
// this currently allows matching prepended whitespace prior to /slash-command
|
||||
@@ -46,11 +48,11 @@ export function parseSlashCommands(text: string): string {
|
||||
const textWithoutSlashCommand = text.substring(0, slashCommandStartIndex) + text.substring(slashCommandEndIndex)
|
||||
const processedText = commandReplacements[commandName] + textWithoutSlashCommand
|
||||
|
||||
return processedText
|
||||
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" ? true : false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no supported commands are found, return the original text
|
||||
return text
|
||||
return { processedText: text, needsClinerulesFileCheck: false }
|
||||
}
|
||||
|
||||
@@ -75,5 +75,6 @@ export type GlobalStateKey =
|
||||
| "reasoningEffort"
|
||||
| "planActSeparateModelsSetting"
|
||||
| "favoritedModelIds"
|
||||
| "requestTimeoutMs"
|
||||
|
||||
export type LocalStateKey = "localClineRulesToggles"
|
||||
|
||||
@@ -125,6 +125,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
planActSeparateModelsSettingRaw,
|
||||
favoritedModelIds,
|
||||
globalClineRulesToggles,
|
||||
requestTimeoutMs,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
|
||||
@@ -198,6 +199,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
|
||||
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
getGlobalState(context, "requestTimeoutMs") as Promise<number | undefined>,
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
@@ -297,6 +299,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs,
|
||||
},
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
@@ -434,6 +437,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
await storeSecret(context, "clineApiKey", clineApiKey)
|
||||
await storeSecret(context, "sambanovaApiKey", sambanovaApiKey)
|
||||
await updateGlobalState(context, "favoritedModelIds", favoritedModelIds)
|
||||
await updateGlobalState(context, "requestTimeoutMs", apiConfiguration.requestTimeoutMs)
|
||||
}
|
||||
|
||||
export async function resetExtensionState(context: vscode.ExtensionContext) {
|
||||
|
||||
+63
-8
@@ -86,6 +86,7 @@ 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"
|
||||
@@ -1361,6 +1362,7 @@ export class Task {
|
||||
this.autoApprovalSettings.actions.readFiles,
|
||||
this.autoApprovalSettings.actions.readFilesExternally ?? false,
|
||||
]
|
||||
case "new_rule":
|
||||
case "write_to_file":
|
||||
case "replace_in_file":
|
||||
return [
|
||||
@@ -1505,6 +1507,10 @@ export class Task {
|
||||
"quarter", // Force aggressive truncation
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await this.contextManager.triggerApplyStandardContextTruncationNoticeChange(
|
||||
Date.now(),
|
||||
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
)
|
||||
|
||||
this.didAutomaticallyRetryFailedApiRequest = true
|
||||
} else if (isOpenRouter && !this.didAutomaticallyRetryFailedApiRequest) {
|
||||
@@ -1515,6 +1521,10 @@ export class Task {
|
||||
"quarter", // Force aggressive truncation
|
||||
)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
await this.contextManager.triggerApplyStandardContextTruncationNoticeChange(
|
||||
Date.now(),
|
||||
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
)
|
||||
}
|
||||
|
||||
console.log("first chunk failed, waiting 1 second before retrying")
|
||||
@@ -1678,6 +1688,8 @@ 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}']`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1817,6 +1829,7 @@ export class Task {
|
||||
}
|
||||
|
||||
switch (block.name) {
|
||||
case "new_rule":
|
||||
case "write_to_file":
|
||||
case "replace_in_file": {
|
||||
const relPath: string | undefined = block.params.path
|
||||
@@ -1962,6 +1975,13 @@ export class Task {
|
||||
|
||||
break
|
||||
}
|
||||
if (block.name === "new_rule" && !content) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("new_rule", "content"))
|
||||
await this.diffViewProvider.reset()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
this.consecutiveMistakeCount = 0
|
||||
|
||||
@@ -3474,7 +3494,16 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
const [parsedUserContent, environmentDetails] = await this.loadContext(userContent, includeFileDetails)
|
||||
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.",
|
||||
)
|
||||
}
|
||||
|
||||
userContent = parsedUserContent
|
||||
// add environment details as its own text block, separate from tool results
|
||||
userContent.push({ type: "text", text: environmentDetails })
|
||||
@@ -3759,11 +3788,14 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
async loadContext(userContent: UserContent, includeFileDetails: boolean = false) {
|
||||
return await Promise.all([
|
||||
async loadContext(userContent: UserContent, includeFileDetails: boolean = false): Promise<[UserContent, string, boolean]> {
|
||||
// Track if we need to check clinerulesFile
|
||||
let needsClinerulesFileCheck = false
|
||||
|
||||
const processUserContent = async () => {
|
||||
// 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)
|
||||
Promise.all(
|
||||
return await 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
|
||||
@@ -3774,22 +3806,45 @@ export class Task {
|
||||
block.text.includes("<task>") ||
|
||||
block.text.includes("<user_message>")
|
||||
) {
|
||||
let parsedText = await parseMentions(block.text, cwd, this.urlContentFetcher, this.fileContextTracker)
|
||||
const 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
|
||||
parsedText = parseSlashCommands(parsedText)
|
||||
const { processedText, needsClinerulesFileCheck: needsCheck } = parseSlashCommands(parsedText)
|
||||
|
||||
if (needsCheck) {
|
||||
needsClinerulesFileCheck = true
|
||||
}
|
||||
|
||||
return {
|
||||
...block,
|
||||
text: parsedText,
|
||||
text: processedText,
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
||||
+22
-30
@@ -521,6 +521,21 @@ 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()
|
||||
@@ -529,13 +544,7 @@ export class McpHub {
|
||||
const serverOrder = Object.keys(config.mcpServers || {})
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpServers",
|
||||
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),
|
||||
mcpServers: this.getSortedMcpServers(serverOrder),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -566,16 +575,7 @@ export class McpHub {
|
||||
}
|
||||
|
||||
const serverOrder = Object.keys(config.mcpServers || {})
|
||||
|
||||
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
|
||||
return this.getSortedMcpServers(serverOrder)
|
||||
}
|
||||
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) {
|
||||
public async addRemoteServer(serverName: string, serverUrl: string): Promise<McpServer[]> {
|
||||
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.updateServerConnections(settings.mcpServers)
|
||||
await this.updateServerConnectionsRPC(settings.mcpServers)
|
||||
|
||||
vscode.window.showInformationMessage(`Added ${serverName} MCP server`)
|
||||
const serverOrder = Object.keys(settings.mcpServers || {})
|
||||
return this.getSortedMcpServers(serverOrder)
|
||||
} catch (error) {
|
||||
console.error("Failed to add remote MCP server:", error)
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -791,15 +791,7 @@ export class McpHub {
|
||||
await this.updateServerConnectionsRPC(config.mcpServers)
|
||||
|
||||
const serverOrder = Object.keys(config.mcpServers || {})
|
||||
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
|
||||
return this.getSortedMcpServers(serverOrder)
|
||||
} catch (error) {
|
||||
console.error("Failed to update server timeout:", error)
|
||||
if (error instanceof Error) {
|
||||
|
||||
@@ -37,14 +37,12 @@ export interface ExtensionMessage {
|
||||
| "openGraphData"
|
||||
| "isImageUrlResult"
|
||||
| "didUpdateSettings"
|
||||
| "addRemoteServerResult"
|
||||
| "userCreditsBalance"
|
||||
| "userCreditsUsage"
|
||||
| "userCreditsPayments"
|
||||
| "totalTasksSize"
|
||||
| "addToInput"
|
||||
| "browserConnectionResult"
|
||||
| "detectedChromePath"
|
||||
| "scrollToSettings"
|
||||
| "browserRelaunchResult"
|
||||
| "relativePathsResponse" // Handles single and multiple path responses
|
||||
@@ -58,7 +56,6 @@ export interface ExtensionMessage {
|
||||
| "settingsButtonClicked"
|
||||
| "historyButtonClicked"
|
||||
| "didBecomeVisible"
|
||||
| "accountLoginClicked"
|
||||
| "accountLogoutClicked"
|
||||
| "accountButtonClicked"
|
||||
| "focusChatInput"
|
||||
@@ -105,11 +102,6 @@ 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
|
||||
|
||||
@@ -9,7 +9,6 @@ import { McpViewTab } from "./mcp"
|
||||
|
||||
export interface WebviewMessage {
|
||||
type:
|
||||
| "addRemoteServer"
|
||||
| "apiConfiguration"
|
||||
| "webviewDidLaunch"
|
||||
| "newTask"
|
||||
@@ -24,7 +23,6 @@ export interface WebviewMessage {
|
||||
| "resetState"
|
||||
| "requestOllamaModels"
|
||||
| "requestLmStudioModels"
|
||||
| "openImage"
|
||||
| "openInBrowser"
|
||||
| "createRuleFile"
|
||||
| "openMention"
|
||||
@@ -37,7 +35,6 @@ export interface WebviewMessage {
|
||||
| "restartMcpServer"
|
||||
| "deleteMcpServer"
|
||||
| "autoApprovalSettings"
|
||||
| "browserSettings"
|
||||
| "browserRelaunchResult"
|
||||
| "togglePlanActMode"
|
||||
| "taskCompletionViewChanges"
|
||||
@@ -45,7 +42,6 @@ export interface WebviewMessage {
|
||||
| "requestVsCodeLmModels"
|
||||
| "toggleToolAutoApprove"
|
||||
| "getLatestState"
|
||||
| "accountLoginClicked"
|
||||
| "accountLogoutClicked"
|
||||
| "showAccountViewClicked"
|
||||
| "authStateChanged"
|
||||
@@ -54,7 +50,6 @@ export interface WebviewMessage {
|
||||
| "downloadMcp"
|
||||
| "silentlyRefreshMcpMarketplace"
|
||||
| "searchCommits"
|
||||
| "showMcpView"
|
||||
| "fetchLatestMcpServersFromHub"
|
||||
| "telemetrySetting"
|
||||
| "openSettings"
|
||||
@@ -68,8 +63,6 @@ export interface WebviewMessage {
|
||||
| "requestTotalTasksSize"
|
||||
| "relaunchChromeDebugMode"
|
||||
| "taskFeedback"
|
||||
| "getDetectedChromePath"
|
||||
| "detectedChromePath"
|
||||
| "scrollToSettings"
|
||||
| "getRelativePaths" // Handles single and multiple URI resolution
|
||||
| "searchFiles"
|
||||
@@ -77,6 +70,7 @@ export interface WebviewMessage {
|
||||
| "grpc_request"
|
||||
| "toggleClineRule"
|
||||
| "deleteClineRule"
|
||||
| "copyToClipboard"
|
||||
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
|
||||
@@ -80,6 +80,7 @@ export interface ApiHandlerOptions {
|
||||
thinkingBudgetTokens?: number
|
||||
reasoningEffort?: string
|
||||
sambanovaApiKey?: string
|
||||
requestTimeoutMs?: number
|
||||
}
|
||||
|
||||
export type ApiConfiguration = ApiHandlerOptions & {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.7.0
|
||||
// protoc v3.19.1
|
||||
// source: account.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { EmptyRequest, String } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
/** Service for account-related operations */
|
||||
export type AccountServiceDefinition = typeof AccountServiceDefinition
|
||||
export const AccountServiceDefinition = {
|
||||
name: "AccountService",
|
||||
fullName: "cline.AccountService",
|
||||
methods: {
|
||||
/**
|
||||
* Handles the user clicking the login link in the UI.
|
||||
* Generates a secure nonce for state validation, stores it in secrets,
|
||||
* and opens the authentication URL in the external browser.
|
||||
*/
|
||||
accountLoginClicked: {
|
||||
name: "accountLoginClicked",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: String,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
+399
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
|
||||
import { EmptyRequest, StringRequest } from "./common"
|
||||
import { Boolean, EmptyRequest, Metadata, StringRequest } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
@@ -22,6 +22,29 @@ export interface BrowserConnection {
|
||||
endpoint?: string | undefined
|
||||
}
|
||||
|
||||
export interface ChromePath {
|
||||
path: string
|
||||
isBundled: boolean
|
||||
}
|
||||
|
||||
export interface Viewport {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface BrowserSettings {
|
||||
viewport?: Viewport | undefined
|
||||
remoteBrowserHost?: string | undefined
|
||||
remoteBrowserEnabled?: boolean | undefined
|
||||
}
|
||||
|
||||
export interface UpdateBrowserSettingsRequest {
|
||||
metadata?: Metadata | undefined
|
||||
viewport?: Viewport | undefined
|
||||
remoteBrowserHost?: string | undefined
|
||||
remoteBrowserEnabled?: boolean | undefined
|
||||
}
|
||||
|
||||
function createBaseBrowserConnectionInfo(): BrowserConnectionInfo {
|
||||
return { isConnected: false, isRemote: false, host: undefined }
|
||||
}
|
||||
@@ -206,6 +229,365 @@ export const BrowserConnection: MessageFns<BrowserConnection> = {
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseChromePath(): ChromePath {
|
||||
return { path: "", isBundled: false }
|
||||
}
|
||||
|
||||
export const ChromePath: MessageFns<ChromePath> = {
|
||||
encode(message: ChromePath, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.path !== "") {
|
||||
writer.uint32(10).string(message.path)
|
||||
}
|
||||
if (message.isBundled !== false) {
|
||||
writer.uint32(16).bool(message.isBundled)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): ChromePath {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseChromePath()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break
|
||||
}
|
||||
|
||||
message.path = reader.string()
|
||||
continue
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 16) {
|
||||
break
|
||||
}
|
||||
|
||||
message.isBundled = reader.bool()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): ChromePath {
|
||||
return {
|
||||
path: isSet(object.path) ? globalThis.String(object.path) : "",
|
||||
isBundled: isSet(object.isBundled) ? globalThis.Boolean(object.isBundled) : false,
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: ChromePath): unknown {
|
||||
const obj: any = {}
|
||||
if (message.path !== "") {
|
||||
obj.path = message.path
|
||||
}
|
||||
if (message.isBundled !== false) {
|
||||
obj.isBundled = message.isBundled
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ChromePath>, I>>(base?: I): ChromePath {
|
||||
return ChromePath.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<ChromePath>, I>>(object: I): ChromePath {
|
||||
const message = createBaseChromePath()
|
||||
message.path = object.path ?? ""
|
||||
message.isBundled = object.isBundled ?? false
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseViewport(): Viewport {
|
||||
return { width: 0, height: 0 }
|
||||
}
|
||||
|
||||
export const Viewport: MessageFns<Viewport> = {
|
||||
encode(message: Viewport, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.width !== 0) {
|
||||
writer.uint32(8).int32(message.width)
|
||||
}
|
||||
if (message.height !== 0) {
|
||||
writer.uint32(16).int32(message.height)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): Viewport {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseViewport()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break
|
||||
}
|
||||
|
||||
message.width = reader.int32()
|
||||
continue
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 16) {
|
||||
break
|
||||
}
|
||||
|
||||
message.height = reader.int32()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): Viewport {
|
||||
return {
|
||||
width: isSet(object.width) ? globalThis.Number(object.width) : 0,
|
||||
height: isSet(object.height) ? globalThis.Number(object.height) : 0,
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: Viewport): unknown {
|
||||
const obj: any = {}
|
||||
if (message.width !== 0) {
|
||||
obj.width = Math.round(message.width)
|
||||
}
|
||||
if (message.height !== 0) {
|
||||
obj.height = Math.round(message.height)
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<Viewport>, I>>(base?: I): Viewport {
|
||||
return Viewport.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<Viewport>, I>>(object: I): Viewport {
|
||||
const message = createBaseViewport()
|
||||
message.width = object.width ?? 0
|
||||
message.height = object.height ?? 0
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseBrowserSettings(): BrowserSettings {
|
||||
return { viewport: undefined, remoteBrowserHost: undefined, remoteBrowserEnabled: undefined }
|
||||
}
|
||||
|
||||
export const BrowserSettings: MessageFns<BrowserSettings> = {
|
||||
encode(message: BrowserSettings, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.viewport !== undefined) {
|
||||
Viewport.encode(message.viewport, writer.uint32(10).fork()).join()
|
||||
}
|
||||
if (message.remoteBrowserHost !== undefined) {
|
||||
writer.uint32(18).string(message.remoteBrowserHost)
|
||||
}
|
||||
if (message.remoteBrowserEnabled !== undefined) {
|
||||
writer.uint32(24).bool(message.remoteBrowserEnabled)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): BrowserSettings {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseBrowserSettings()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break
|
||||
}
|
||||
|
||||
message.viewport = Viewport.decode(reader, reader.uint32())
|
||||
continue
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break
|
||||
}
|
||||
|
||||
message.remoteBrowserHost = reader.string()
|
||||
continue
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 24) {
|
||||
break
|
||||
}
|
||||
|
||||
message.remoteBrowserEnabled = reader.bool()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): BrowserSettings {
|
||||
return {
|
||||
viewport: isSet(object.viewport) ? Viewport.fromJSON(object.viewport) : undefined,
|
||||
remoteBrowserHost: isSet(object.remoteBrowserHost) ? globalThis.String(object.remoteBrowserHost) : undefined,
|
||||
remoteBrowserEnabled: isSet(object.remoteBrowserEnabled)
|
||||
? globalThis.Boolean(object.remoteBrowserEnabled)
|
||||
: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: BrowserSettings): unknown {
|
||||
const obj: any = {}
|
||||
if (message.viewport !== undefined) {
|
||||
obj.viewport = Viewport.toJSON(message.viewport)
|
||||
}
|
||||
if (message.remoteBrowserHost !== undefined) {
|
||||
obj.remoteBrowserHost = message.remoteBrowserHost
|
||||
}
|
||||
if (message.remoteBrowserEnabled !== undefined) {
|
||||
obj.remoteBrowserEnabled = message.remoteBrowserEnabled
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<BrowserSettings>, I>>(base?: I): BrowserSettings {
|
||||
return BrowserSettings.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<BrowserSettings>, I>>(object: I): BrowserSettings {
|
||||
const message = createBaseBrowserSettings()
|
||||
message.viewport =
|
||||
object.viewport !== undefined && object.viewport !== null ? Viewport.fromPartial(object.viewport) : undefined
|
||||
message.remoteBrowserHost = object.remoteBrowserHost ?? undefined
|
||||
message.remoteBrowserEnabled = object.remoteBrowserEnabled ?? undefined
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseUpdateBrowserSettingsRequest(): UpdateBrowserSettingsRequest {
|
||||
return { metadata: undefined, viewport: undefined, remoteBrowserHost: undefined, remoteBrowserEnabled: undefined }
|
||||
}
|
||||
|
||||
export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsRequest> = {
|
||||
encode(message: UpdateBrowserSettingsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.metadata !== undefined) {
|
||||
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
|
||||
}
|
||||
if (message.viewport !== undefined) {
|
||||
Viewport.encode(message.viewport, writer.uint32(18).fork()).join()
|
||||
}
|
||||
if (message.remoteBrowserHost !== undefined) {
|
||||
writer.uint32(26).string(message.remoteBrowserHost)
|
||||
}
|
||||
if (message.remoteBrowserEnabled !== undefined) {
|
||||
writer.uint32(32).bool(message.remoteBrowserEnabled)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): UpdateBrowserSettingsRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseUpdateBrowserSettingsRequest()
|
||||
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.viewport = Viewport.decode(reader, reader.uint32())
|
||||
continue
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break
|
||||
}
|
||||
|
||||
message.remoteBrowserHost = reader.string()
|
||||
continue
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 32) {
|
||||
break
|
||||
}
|
||||
|
||||
message.remoteBrowserEnabled = reader.bool()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): UpdateBrowserSettingsRequest {
|
||||
return {
|
||||
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
|
||||
viewport: isSet(object.viewport) ? Viewport.fromJSON(object.viewport) : undefined,
|
||||
remoteBrowserHost: isSet(object.remoteBrowserHost) ? globalThis.String(object.remoteBrowserHost) : undefined,
|
||||
remoteBrowserEnabled: isSet(object.remoteBrowserEnabled)
|
||||
? globalThis.Boolean(object.remoteBrowserEnabled)
|
||||
: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: UpdateBrowserSettingsRequest): unknown {
|
||||
const obj: any = {}
|
||||
if (message.metadata !== undefined) {
|
||||
obj.metadata = Metadata.toJSON(message.metadata)
|
||||
}
|
||||
if (message.viewport !== undefined) {
|
||||
obj.viewport = Viewport.toJSON(message.viewport)
|
||||
}
|
||||
if (message.remoteBrowserHost !== undefined) {
|
||||
obj.remoteBrowserHost = message.remoteBrowserHost
|
||||
}
|
||||
if (message.remoteBrowserEnabled !== undefined) {
|
||||
obj.remoteBrowserEnabled = message.remoteBrowserEnabled
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<UpdateBrowserSettingsRequest>, I>>(base?: I): UpdateBrowserSettingsRequest {
|
||||
return UpdateBrowserSettingsRequest.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<UpdateBrowserSettingsRequest>, I>>(object: I): UpdateBrowserSettingsRequest {
|
||||
const message = createBaseUpdateBrowserSettingsRequest()
|
||||
message.metadata =
|
||||
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
|
||||
message.viewport =
|
||||
object.viewport !== undefined && object.viewport !== null ? Viewport.fromPartial(object.viewport) : undefined
|
||||
message.remoteBrowserHost = object.remoteBrowserHost ?? undefined
|
||||
message.remoteBrowserEnabled = object.remoteBrowserEnabled ?? undefined
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
export type BrowserServiceDefinition = typeof BrowserServiceDefinition
|
||||
export const BrowserServiceDefinition = {
|
||||
name: "BrowserService",
|
||||
@@ -235,6 +617,22 @@ export const BrowserServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
getDetectedChromePath: {
|
||||
name: "getDetectedChromePath",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: ChromePath,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
updateBrowserSettings: {
|
||||
name: "updateBrowserSettings",
|
||||
requestType: UpdateBrowserSettingsRequest,
|
||||
requestStream: false,
|
||||
responseType: Boolean,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -44,6 +44,15 @@ export interface Bytes {
|
||||
value: Buffer
|
||||
}
|
||||
|
||||
export interface BooleanRequest {
|
||||
metadata?: Metadata | undefined
|
||||
value: boolean
|
||||
}
|
||||
|
||||
export interface Boolean {
|
||||
value: boolean
|
||||
}
|
||||
|
||||
function createBaseMetadata(): Metadata {
|
||||
return {}
|
||||
}
|
||||
@@ -594,6 +603,141 @@ export const Bytes: MessageFns<Bytes> = {
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseBooleanRequest(): BooleanRequest {
|
||||
return { metadata: undefined, value: false }
|
||||
}
|
||||
|
||||
export const BooleanRequest: MessageFns<BooleanRequest> = {
|
||||
encode(message: BooleanRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.metadata !== undefined) {
|
||||
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
|
||||
}
|
||||
if (message.value !== false) {
|
||||
writer.uint32(16).bool(message.value)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): BooleanRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseBooleanRequest()
|
||||
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 !== 16) {
|
||||
break
|
||||
}
|
||||
|
||||
message.value = reader.bool()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): BooleanRequest {
|
||||
return {
|
||||
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
|
||||
value: isSet(object.value) ? globalThis.Boolean(object.value) : false,
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: BooleanRequest): unknown {
|
||||
const obj: any = {}
|
||||
if (message.metadata !== undefined) {
|
||||
obj.metadata = Metadata.toJSON(message.metadata)
|
||||
}
|
||||
if (message.value !== false) {
|
||||
obj.value = message.value
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<BooleanRequest>, I>>(base?: I): BooleanRequest {
|
||||
return BooleanRequest.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<BooleanRequest>, I>>(object: I): BooleanRequest {
|
||||
const message = createBaseBooleanRequest()
|
||||
message.metadata =
|
||||
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
|
||||
message.value = object.value ?? false
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseBoolean(): Boolean {
|
||||
return { value: false }
|
||||
}
|
||||
|
||||
export const Boolean: MessageFns<Boolean> = {
|
||||
encode(message: Boolean, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.value !== false) {
|
||||
writer.uint32(8).bool(message.value)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): Boolean {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseBoolean()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break
|
||||
}
|
||||
|
||||
message.value = reader.bool()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): Boolean {
|
||||
return { value: isSet(object.value) ? globalThis.Boolean(object.value) : false }
|
||||
},
|
||||
|
||||
toJSON(message: Boolean): unknown {
|
||||
const obj: any = {}
|
||||
if (message.value !== false) {
|
||||
obj.value = message.value
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<Boolean>, I>>(base?: I): Boolean {
|
||||
return Boolean.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<Boolean>, I>>(object: I): Boolean {
|
||||
const message = createBaseBoolean()
|
||||
message.value = object.value ?? false
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
function bytesFromBase64(b64: string): Uint8Array {
|
||||
return Uint8Array.from(globalThis.Buffer.from(b64, "base64"))
|
||||
}
|
||||
|
||||
@@ -24,5 +24,14 @@ export const FileServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Opens an image in the system viewer */
|
||||
openImage: {
|
||||
name: "openImage",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -65,6 +65,12 @@ export interface UpdateMcpTimeoutRequest {
|
||||
timeout: number
|
||||
}
|
||||
|
||||
export interface AddRemoteMcpServerRequest {
|
||||
metadata?: Metadata | undefined
|
||||
serverName: string
|
||||
serverUrl: string
|
||||
}
|
||||
|
||||
export interface McpTool {
|
||||
name: string
|
||||
description?: string | undefined
|
||||
@@ -288,6 +294,99 @@ 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 }
|
||||
}
|
||||
@@ -897,6 +996,14 @@ export const McpServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
addRemoteMcpServer: {
|
||||
name: "addRemoteMcpServer",
|
||||
requestType: AddRemoteMcpServerRequest,
|
||||
requestStream: false,
|
||||
responseType: McpServers,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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"
|
||||
@@ -0,0 +1 @@
|
||||
export {}
|
||||
+1
-1
@@ -27,7 +27,7 @@ function calculateApiCostInternal(
|
||||
}
|
||||
|
||||
// Determine effective output price
|
||||
let effectiveOutputPrice = 0
|
||||
let effectiveOutputPrice = modelInfo.outputPrice || 0
|
||||
// Check if thinking budget was used and has a specific price
|
||||
if (usedThinkingBudget && modelInfo.thinkingConfig?.outputPrice !== undefined) {
|
||||
effectiveOutputPrice = modelInfo.thinkingConfig.outputPrice
|
||||
|
||||
Generated
+951
-56
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,11 @@
|
||||
"react-use": "^17.6.0",
|
||||
"react-virtuoso": "^4.12.3",
|
||||
"rehype-highlight": "^7.0.1",
|
||||
"rehype-parse": "^9.0.1",
|
||||
"rehype-remark": "^10.0.1",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"styled-components": "^6.1.15",
|
||||
"unified": "^11.0.5",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+55
-48
@@ -10,60 +10,67 @@ import { ExtensionStateContextProvider, useExtensionState } from "./context/Exte
|
||||
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
|
||||
import { vscode } from "./utils/vscode"
|
||||
import McpView from "./components/mcp/configuration/McpConfigurationView"
|
||||
import { McpViewTab } from "@shared/mcp"
|
||||
|
||||
const AppContent = () => {
|
||||
const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, vscMachineId } = useExtensionState()
|
||||
const { didHydrateState, showWelcome, shouldShowAnnouncement, showMcp, mcpTab } = 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 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
|
||||
}
|
||||
}, [])
|
||||
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],
|
||||
)
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
@@ -95,13 +102,13 @@ const AppContent = () => {
|
||||
<>
|
||||
{showSettings && <SettingsView onDone={hideSettings} />}
|
||||
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
|
||||
{showMcp && <McpView initialTab={mcpTab} onDone={() => setShowMcp(false)} />}
|
||||
{showMcp && <McpView initialTab={mcpTab} onDone={closeMcpView} />}
|
||||
{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)
|
||||
setShowMcp(false)
|
||||
closeMcpView()
|
||||
setShowAccount(false)
|
||||
setShowHistory(true)
|
||||
}}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { memo } from "react"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { AccountServiceClient } from "@/services/grpc-client"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
|
||||
const AccountOptions = () => {
|
||||
const handleAccountClick = () => {
|
||||
vscode.postMessage({ type: "accountLoginClicked" })
|
||||
AccountServiceClient.accountLoginClicked(EmptyRequest.create()).catch((err) =>
|
||||
console.error("Failed to get login URL:", err),
|
||||
)
|
||||
}
|
||||
|
||||
// Call handleAccountClick immediately when component mounts
|
||||
|
||||
@@ -8,6 +8,8 @@ import CountUp from "react-countup"
|
||||
import CreditsHistoryTable from "./CreditsHistoryTable"
|
||||
import { UsageTransaction, PaymentTransaction } from "@shared/ClineAccount"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { AccountServiceClient } from "@/services/grpc-client"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
|
||||
type AccountViewProps = {
|
||||
onDone: () => void
|
||||
@@ -68,7 +70,9 @@ export const ClineAccountView = () => {
|
||||
}, [user])
|
||||
|
||||
const handleLogin = () => {
|
||||
vscode.postMessage({ type: "accountLoginClicked" })
|
||||
AccountServiceClient.accountLoginClicked(EmptyRequest.create()).catch((err) =>
|
||||
console.error("Failed to get login URL:", err),
|
||||
)
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import styled from "styled-components"
|
||||
import { BROWSER_VIEWPORT_PRESETS } from "@shared/BrowserSettings"
|
||||
import { BrowserAction, BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "@shared/ExtensionMessage"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { FileServiceClient } from "@/services/grpc-client"
|
||||
import { BrowserSettingsMenu } from "@/components/browser/BrowserSettingsMenu"
|
||||
import { CheckpointControls } from "@/components/common/CheckpointControls"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
@@ -397,10 +397,9 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
alt="Browser screenshot"
|
||||
style={imgScreenshotStyle}
|
||||
onClick={() =>
|
||||
vscode.postMessage({
|
||||
type: "openImage",
|
||||
text: displayState.screenshot,
|
||||
})
|
||||
FileServiceClient.openImage({ value: displayState.screenshot }).catch((err) =>
|
||||
console.error("Failed to open image:", err),
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -28,6 +28,10 @@ import ChatRow from "@/components/chat/ChatRow"
|
||||
import ChatTextArea from "@/components/chat/ChatTextArea"
|
||||
import TaskHeader from "@/components/chat/TaskHeader"
|
||||
import TelemetryBanner from "@/components/common/TelemetryBanner"
|
||||
import { unified } from "unified"
|
||||
import remarkStringify from "remark-stringify"
|
||||
import rehypeRemark from "rehype-remark"
|
||||
import rehypeParse from "rehype-parse"
|
||||
|
||||
interface ChatViewProps {
|
||||
isHidden: boolean
|
||||
@@ -36,6 +40,26 @@ interface ChatViewProps {
|
||||
showHistoryView: () => void
|
||||
}
|
||||
|
||||
async function convertHtmlToMarkdown(html: string) {
|
||||
// Process the HTML to Markdown
|
||||
const result = await unified()
|
||||
.use(rehypeParse as any, { fragment: true }) // Parse HTML fragments
|
||||
.use(rehypeRemark as any) // Convert HTML to Markdown AST
|
||||
.use(remarkStringify as any, {
|
||||
// Convert Markdown AST to text
|
||||
bullet: "-", // Use - for unordered lists
|
||||
emphasis: "*", // Use * for emphasis
|
||||
strong: "_", // Use _ for strong
|
||||
listItemIndent: "one", // Use one space for list indentation
|
||||
rule: "-", // Use - for horizontal rules
|
||||
ruleSpaces: false, // No spaces in horizontal rules
|
||||
fences: true,
|
||||
})
|
||||
.process(html)
|
||||
|
||||
return String(result)
|
||||
}
|
||||
|
||||
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
||||
|
||||
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
|
||||
@@ -79,6 +103,33 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
||||
const [isAtBottom, setIsAtBottom] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleCopy = async (e: ClipboardEvent) => {
|
||||
if (window.getSelection) {
|
||||
const selection = window.getSelection()
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
// Get the selected HTML content
|
||||
const range = selection.getRangeAt(0)
|
||||
const clonedSelection = range.cloneContents()
|
||||
const div = document.createElement("div")
|
||||
div.appendChild(clonedSelection)
|
||||
const selectedHtml = div.innerHTML
|
||||
|
||||
// Convert HTML to Markdown
|
||||
const markdown = await convertHtmlToMarkdown(selectedHtml)
|
||||
|
||||
vscode.postMessage({ type: "copyToClipboard", text: markdown })
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener("copy", handleCopy)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("copy", handleCopy)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// UI layout depends on the last 2 messages
|
||||
// (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
|
||||
const lastMessage = useMemo(() => messages.at(-1), [messages])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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"
|
||||
@@ -9,6 +10,7 @@ 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)
|
||||
@@ -81,11 +83,8 @@ 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(false)
|
||||
const [isTaskExpanded, setIsTaskExpanded] = useState(true)
|
||||
const [isTextExpanded, setIsTextExpanded] = useState(false)
|
||||
const [showSeeMore, setShowSeeMore] = useState(false)
|
||||
const textContainerRef = useRef<HTMLDivElement>(null)
|
||||
@@ -138,6 +138,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
const shouldShowPromptCacheInfo =
|
||||
doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter" && apiConfiguration?.apiProvider !== "cline"
|
||||
|
||||
const shouldShowPromptCacheInfoClineOR =
|
||||
doesModelSupportPromptCache &&
|
||||
(apiConfiguration?.apiProvider === "openrouter" || apiConfiguration?.apiProvider === "cline")
|
||||
|
||||
const ContextWindowComponent = (
|
||||
<>
|
||||
{isTaskExpanded && contextWindow && (
|
||||
@@ -406,6 +410,33 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{shouldShowPromptCacheInfoClineOR && cacheReads !== undefined && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
flexWrap: "wrap",
|
||||
}}>
|
||||
<span style={{ fontWeight: "bold" }}>Cache:</span>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-arrow-right"
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
fontWeight: "bold",
|
||||
marginBottom: 0,
|
||||
}}
|
||||
/>
|
||||
{formatLargeNumber(cacheReads || 0)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{shouldShowPromptCacheInfo &&
|
||||
(cacheReads !== undefined ||
|
||||
cacheWrites !== undefined ||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"
|
||||
import mermaid from "mermaid"
|
||||
import { useDebounceEffect } from "@/utils/useDebounceEffect"
|
||||
import styled from "styled-components"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { FileServiceClient } from "@/services/grpc-client"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
const MERMAID_THEME = {
|
||||
@@ -131,10 +131,7 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
|
||||
|
||||
try {
|
||||
const pngDataUrl = await svgToPng(svgEl)
|
||||
vscode.postMessage({
|
||||
type: "openImage",
|
||||
text: pngDataUrl,
|
||||
})
|
||||
FileServiceClient.openImage({ value: pngDataUrl }).catch((err) => console.error("Failed to open image:", err))
|
||||
} catch (err) {
|
||||
console.error("Error converting SVG to PNG:", err)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useRef, useLayoutEffect, memo } from "react"
|
||||
import { useWindowSize } from "react-use"
|
||||
import { FileServiceClient } from "@/services/grpc-client"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
interface ThumbnailsProps {
|
||||
@@ -33,7 +34,7 @@ const Thumbnails = ({ images, style, setImages, onHeightChange }: ThumbnailsProp
|
||||
const isDeletable = setImages !== undefined
|
||||
|
||||
const handleImageClick = (image: string) => {
|
||||
vscode.postMessage({ type: "openImage", text: image })
|
||||
FileServiceClient.openImage({ value: image }).catch((err) => console.error("Failed to open image:", err))
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+28
-45
@@ -1,50 +1,20 @@
|
||||
import { useCallback, useRef, useState } from "react"
|
||||
import { 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()
|
||||
|
||||
// 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>) => {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!serverName.trim()) {
|
||||
@@ -65,16 +35,29 @@ const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) =
|
||||
}
|
||||
|
||||
setError("")
|
||||
|
||||
submittedValues.current = { name: serverName.trim() }
|
||||
|
||||
setIsSubmitting(true)
|
||||
setShowConnectingMessage(true)
|
||||
vscode.postMessage({
|
||||
type: "addRemoteServer",
|
||||
serverName: serverName.trim(),
|
||||
serverUrl: serverUrl.trim(),
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -60,6 +60,7 @@ import VSCodeButtonLink from "@/components/common/VSCodeButtonLink"
|
||||
import OpenRouterModelPicker, { ModelDescriptionMarkdown, OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
|
||||
import { ClineAccountInfoCard } from "./ClineAccountInfoCard"
|
||||
import RequestyModelPicker from "./RequestyModelPicker"
|
||||
import { useOpenRouterKeyInfo } from "../ui/hooks/useOpenRouterKeyInfo"
|
||||
|
||||
interface ApiOptionsProps {
|
||||
showModelOptions: boolean
|
||||
@@ -69,6 +70,44 @@ interface ApiOptionsProps {
|
||||
saveImmediately?: boolean // Add prop to control immediate saving
|
||||
}
|
||||
|
||||
const OpenRouterBalanceDisplay = ({ apiKey }: { apiKey: string }) => {
|
||||
const { data: keyInfo, isLoading, error } = useOpenRouterKeyInfo(apiKey)
|
||||
|
||||
if (isLoading) {
|
||||
return <span style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>Loading...</span>
|
||||
}
|
||||
|
||||
if (error || !keyInfo || keyInfo.limit === null) {
|
||||
// Don't show anything if there's an error, no info, or no limit set
|
||||
return null
|
||||
}
|
||||
|
||||
// Calculate remaining balance
|
||||
const remainingBalance = keyInfo.limit - keyInfo.usage
|
||||
const formattedBalance = remainingBalance.toLocaleString("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 4,
|
||||
})
|
||||
|
||||
return (
|
||||
<VSCodeLink
|
||||
href="https://openrouter.ai/settings/keys"
|
||||
title={`Remaining balance: ${formattedBalance}\nLimit: ${keyInfo.limit.toLocaleString("en-US", { style: "currency", currency: "USD" })}\nUsage: ${keyInfo.usage.toLocaleString("en-US", { style: "currency", currency: "USD" })}`}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-foreground)",
|
||||
textDecoration: "none",
|
||||
fontWeight: 500,
|
||||
paddingLeft: 4,
|
||||
cursor: "pointer",
|
||||
}}>
|
||||
Balance: {formattedBalance}
|
||||
</VSCodeLink>
|
||||
)
|
||||
}
|
||||
|
||||
// This is necessary to ensure dropdown opens downward, important for when this is used in popup
|
||||
const DROPDOWN_Z_INDEX = OPENROUTER_MODEL_PICKER_Z_INDEX + 2 // Higher than the OpenRouterModelPicker's and ModelSelectorTooltip's z-index
|
||||
|
||||
@@ -530,7 +569,12 @@ const ApiOptions = ({
|
||||
type="password"
|
||||
onInput={handleInputChange("openRouterApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>OpenRouter API Key</span>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%" }}>
|
||||
<span style={{ fontWeight: 500 }}>OpenRouter API Key</span>
|
||||
{apiConfiguration?.openRouterApiKey && (
|
||||
<OpenRouterBalanceDisplay apiKey={apiConfiguration.openRouterApiKey} />
|
||||
)}
|
||||
</div>
|
||||
</VSCodeTextField>
|
||||
{!apiConfiguration?.openRouterApiKey && (
|
||||
<VSCodeButtonLink
|
||||
@@ -1574,6 +1618,31 @@ const ApiOptions = ({
|
||||
</p>
|
||||
)}
|
||||
|
||||
{selectedProvider === "ollama" && showModelOptions && (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.requestTimeoutMs ? apiConfiguration.requestTimeoutMs.toString() : "30000"}
|
||||
style={{ width: "100%" }}
|
||||
onInput={(e: any) => {
|
||||
const value = e.target.value
|
||||
// Convert to number, with validation
|
||||
const numValue = parseInt(value, 10)
|
||||
if (!isNaN(numValue) && numValue > 0) {
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
requestTimeoutMs: numValue,
|
||||
})
|
||||
}
|
||||
}}
|
||||
placeholder="Default: 30000 (30 seconds)">
|
||||
<span style={{ fontWeight: 500 }}>Request Timeout (ms)</span>
|
||||
</VSCodeTextField>
|
||||
<p style={{ fontSize: "12px", marginTop: 3, color: "var(--vscode-descriptionForeground)" }}>
|
||||
Maximum time in milliseconds to wait for API responses before timing out.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(selectedProvider === "openrouter" || selectedProvider === "cline") && showModelOptions && (
|
||||
<>
|
||||
<VSCodeCheckbox
|
||||
|
||||
@@ -59,9 +59,6 @@ export const BrowserSettingsSection: React.FC = () => {
|
||||
message: message.text,
|
||||
})
|
||||
setDebugMode(false)
|
||||
} else if (message.type === "detectedChromePath") {
|
||||
setDetectedChromePath(message.text)
|
||||
setIsBundled(message.isBundled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,9 +80,15 @@ export const BrowserSettingsSection: React.FC = () => {
|
||||
|
||||
// Request detected Chrome path on mount
|
||||
useEffect(() => {
|
||||
vscode.postMessage({
|
||||
type: "getDetectedChromePath",
|
||||
})
|
||||
// Use gRPC for getDetectedChromePath
|
||||
BrowserServiceClient.getDetectedChromePath({})
|
||||
.then((result) => {
|
||||
setDetectedChromePath(result.path)
|
||||
setIsBundled(result.isBundled)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error getting detected Chrome path:", error)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Debounced connection check function
|
||||
@@ -136,38 +139,65 @@ export const BrowserSettingsSection: React.FC = () => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const selectedSize = BROWSER_VIEWPORT_PRESETS[target.value as keyof typeof BROWSER_VIEWPORT_PRESETS]
|
||||
if (selectedSize) {
|
||||
vscode.postMessage({
|
||||
type: "browserSettings",
|
||||
browserSettings: {
|
||||
...browserSettings,
|
||||
viewport: selectedSize,
|
||||
BrowserServiceClient.updateBrowserSettings({
|
||||
metadata: {},
|
||||
viewport: {
|
||||
width: selectedSize.width,
|
||||
height: selectedSize.height,
|
||||
},
|
||||
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: browserSettings.remoteBrowserHost,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
console.error("Failed to update browser settings")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating browser settings:", error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateRemoteBrowserEnabled = (enabled: boolean) => {
|
||||
// Also update browserSettings to ensure task settings are updated
|
||||
vscode.postMessage({
|
||||
type: "browserSettings",
|
||||
browserSettings: {
|
||||
...browserSettings,
|
||||
remoteBrowserEnabled: enabled,
|
||||
// If disabling, also clear the host in browserSettings
|
||||
...(enabled ? {} : { remoteBrowserHost: undefined }),
|
||||
BrowserServiceClient.updateBrowserSettings({
|
||||
metadata: {},
|
||||
viewport: {
|
||||
width: browserSettings.viewport.width,
|
||||
height: browserSettings.viewport.height,
|
||||
},
|
||||
remoteBrowserEnabled: enabled,
|
||||
// If disabling, also clear the host
|
||||
remoteBrowserHost: enabled ? browserSettings.remoteBrowserHost : undefined,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
console.error("Failed to update browser settings")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating browser settings:", error)
|
||||
})
|
||||
}
|
||||
|
||||
const updateRemoteBrowserHost = (host: string | undefined) => {
|
||||
// Also update browserSettings to ensure task settings are updated
|
||||
vscode.postMessage({
|
||||
type: "browserSettings",
|
||||
browserSettings: {
|
||||
...browserSettings,
|
||||
remoteBrowserHost: host,
|
||||
BrowserServiceClient.updateBrowserSettings({
|
||||
metadata: {},
|
||||
viewport: {
|
||||
width: browserSettings.viewport.width,
|
||||
height: browserSettings.viewport.height,
|
||||
},
|
||||
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: host,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
console.error("Failed to update browser settings")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating browser settings:", error)
|
||||
})
|
||||
}
|
||||
|
||||
// Function to check connection once without changing UI state immediately
|
||||
|
||||
@@ -2,6 +2,8 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useFirebaseAuth } from "@/context/FirebaseAuthContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { AccountServiceClient } from "@/services/grpc-client"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
|
||||
export const ClineAccountInfoCard = () => {
|
||||
const { user: firebaseUser, handleSignOut } = useFirebaseAuth()
|
||||
@@ -10,7 +12,9 @@ export const ClineAccountInfoCard = () => {
|
||||
let user = apiConfiguration?.clineApiKey ? firebaseUser || userInfo : undefined
|
||||
|
||||
const handleLogin = () => {
|
||||
vscode.postMessage({ type: "accountLoginClicked" })
|
||||
AccountServiceClient.accountLoginClicked(EmptyRequest.create()).catch((err) =>
|
||||
console.error("Failed to get login URL:", err),
|
||||
)
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
|
||||
@@ -252,7 +252,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
{IS_DEV && (
|
||||
<>
|
||||
<div className="mt-[10px] mb-1">Debug</div>
|
||||
<VSCodeButton onClick={handleResetState} className="mt-[5px] w-auto">
|
||||
<VSCodeButton
|
||||
onClick={handleResetState}
|
||||
className="mt-[5px] w-auto"
|
||||
style={{ backgroundColor: "var(--vscode-errorForeground)", color: "black" }}>
|
||||
Reset State
|
||||
</VSCodeButton>
|
||||
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { z } from "zod"
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
const CACHE_DURATION_MS = 30 * 1000 // 30 seconds
|
||||
const OpenRouterBaseURL = "https://openrouter.ai/api/v1"
|
||||
|
||||
// Define schema for OpenRouter key response
|
||||
const openRouterKeyInfoSchema = z.object({
|
||||
data: z.object({
|
||||
label: z.string().nullable(),
|
||||
usage: z.number(),
|
||||
is_free_tier: z.boolean(),
|
||||
is_provisioning_key: z.boolean(),
|
||||
rate_limit: z.object({
|
||||
requests: z.number(),
|
||||
interval: z.string(),
|
||||
}),
|
||||
limit: z.number().nullable(),
|
||||
}),
|
||||
})
|
||||
export type OpenRouterKeyInfo = z.infer<typeof openRouterKeyInfoSchema>["data"]
|
||||
|
||||
// Module-level cache variables
|
||||
let moduleCachedData: OpenRouterKeyInfo | null = null
|
||||
let moduleLastFetchTime: number | null = null
|
||||
|
||||
async function getOpenRouterKeyInfo(apiKey: string, signal: AbortSignal): Promise<OpenRouterKeyInfo | null> {
|
||||
try {
|
||||
const keyEndpoint = `${OpenRouterBaseURL}/key`
|
||||
const response = await fetch(keyEndpoint, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
console.warn("OpenRouter API key is invalid or unauthorized.")
|
||||
} else {
|
||||
console.error(`Error fetching OpenRouter key info: HTTP ${response.status}`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const responseData = await response.json()
|
||||
const result = openRouterKeyInfoSchema.safeParse(responseData)
|
||||
if (!result.success) {
|
||||
console.error("OpenRouter API key info validation failed:", result.error.flatten().fieldErrors)
|
||||
return null
|
||||
}
|
||||
return result.data.data
|
||||
} catch (error: any) {
|
||||
if (error.name !== "AbortError") {
|
||||
console.error("Error fetching OpenRouter key info:", error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to fetch OpenRouter API key information.
|
||||
* Implements stale-while-revalidate caching using module-level variables.
|
||||
* @param apiKey The OpenRouter API key.
|
||||
* @returns An object containing the key info data, loading state, and error state.
|
||||
*/
|
||||
export const useOpenRouterKeyInfo = (apiKey?: string) => {
|
||||
// State reflects the currently displayed data, initialized from cache
|
||||
const [data, setData] = useState<OpenRouterKeyInfo | null>(moduleCachedData)
|
||||
// Loading is true only if there's no initial cache and a key is provided
|
||||
const [isLoading, setIsLoading] = useState<boolean>(!moduleCachedData && !!apiKey)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
const signal = controller.signal
|
||||
|
||||
if (!apiKey) {
|
||||
// Clear state and cache if API key is removed
|
||||
setData(null)
|
||||
moduleCachedData = null
|
||||
moduleLastFetchTime = null
|
||||
setIsLoading(false)
|
||||
setError(null)
|
||||
return () => controller.abort()
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const cacheAge = moduleLastFetchTime ? now - moduleLastFetchTime : Infinity
|
||||
const isCacheStale = cacheAge >= CACHE_DURATION_MS
|
||||
const hasCache = !!moduleCachedData
|
||||
|
||||
// Use cached data immediately if available
|
||||
if (hasCache) {
|
||||
// Ensure local state matches module cache if it hasn't updated yet
|
||||
// This handles cases where the hook re-renders before the effect runs
|
||||
if (data !== moduleCachedData) {
|
||||
setData(moduleCachedData)
|
||||
}
|
||||
setIsLoading(false)
|
||||
} else {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
// Fetch if cache is stale or doesn't exist
|
||||
if (isCacheStale || !hasCache) {
|
||||
const isBackgroundFetch = hasCache && isCacheStale // Fetching while showing stale data
|
||||
|
||||
// Don't set loading true for background fetches
|
||||
if (!isBackgroundFetch) {
|
||||
setIsLoading(true)
|
||||
}
|
||||
|
||||
getOpenRouterKeyInfo(apiKey, signal)
|
||||
.then((result) => {
|
||||
if (!signal.aborted) {
|
||||
moduleCachedData = result // Update module cache
|
||||
moduleLastFetchTime = Date.now() // Update module fetch time
|
||||
setData(result) // Update state
|
||||
setError(null) // Clear error on success
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!signal.aborted) {
|
||||
console.error("[useOpenRouterKeyInfo] Fetch error:", err)
|
||||
setError(err instanceof Error ? err : new Error("An unknown error occurred"))
|
||||
if (!isBackgroundFetch) {
|
||||
setData(null)
|
||||
moduleCachedData = null
|
||||
moduleLastFetchTime = null
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!signal.aborted) {
|
||||
setIsLoading(false) // Stop loading indicator
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
controller.abort()
|
||||
}
|
||||
}, [apiKey]) // Re-run effect only when apiKey changes
|
||||
|
||||
return { data, isLoading, error }
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { validateApiConfiguration } from "@/utils/validate"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import ApiOptions from "@/components/settings/ApiOptions"
|
||||
import ClineLogoWhite from "@/assets/ClineLogoWhite"
|
||||
import { AccountServiceClient } from "@/services/grpc-client"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
|
||||
const WelcomeView = memo(() => {
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
@@ -14,7 +16,9 @@ const WelcomeView = memo(() => {
|
||||
const disableLetsGoButton = apiErrorMessage != null
|
||||
|
||||
const handleLogin = () => {
|
||||
vscode.postMessage({ type: "accountLoginClicked" })
|
||||
AccountServiceClient.accountLoginClicked(EmptyRequest.create()).catch((err) =>
|
||||
console.error("Failed to get login URL:", err),
|
||||
)
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
requestyDefaultModelInfo,
|
||||
} from "../../../src/shared/api"
|
||||
import { findLastIndex } from "@shared/array"
|
||||
import { McpMarketplaceCatalog, McpServer } from "../../../src/shared/mcp"
|
||||
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
|
||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||
import { vscode } from "../utils/vscode"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
@@ -29,12 +29,21 @@ 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)
|
||||
@@ -42,6 +51,10 @@ 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: [],
|
||||
@@ -200,6 +213,8 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
mcpMarketplaceCatalog,
|
||||
filePaths,
|
||||
totalTasksSize,
|
||||
showMcp,
|
||||
mcpTab,
|
||||
globalClineRulesToggles: state.globalClineRulesToggles || {},
|
||||
localClineRulesToggles: state.localClineRulesToggles || {},
|
||||
setApiConfiguration: (value) =>
|
||||
@@ -228,6 +243,8 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
shouldShowAnnouncement: value,
|
||||
})),
|
||||
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
|
||||
setShowMcp,
|
||||
setMcpTab,
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { vscode } from "../utils/vscode"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { AccountServiceDefinition } from "@shared/proto/account"
|
||||
import { BrowserServiceDefinition } from "@shared/proto/browser"
|
||||
import { CheckpointsServiceDefinition } from "@shared/proto/checkpoints"
|
||||
import { FileServiceDefinition } from "@shared/proto/file"
|
||||
import { TaskServiceDefinition } from "@shared/proto/task"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { FileServiceDefinition } from "@shared/proto/file"
|
||||
import { McpServiceDefinition } from "@shared/proto/mcp"
|
||||
import { TaskServiceDefinition } from "@shared/proto/task"
|
||||
// Generic type for any protobuf service definition
|
||||
type ProtoService = {
|
||||
name: string
|
||||
@@ -95,10 +96,18 @@ function createGrpcClient<T extends ProtoService>(service: T): GrpcClientType<T>
|
||||
return client
|
||||
}
|
||||
|
||||
const AccountServiceClient = createGrpcClient(AccountServiceDefinition)
|
||||
const BrowserServiceClient = createGrpcClient(BrowserServiceDefinition)
|
||||
const CheckpointsServiceClient = createGrpcClient(CheckpointsServiceDefinition)
|
||||
const FileServiceClient = createGrpcClient(FileServiceDefinition)
|
||||
const TaskServiceClient = createGrpcClient(TaskServiceDefinition)
|
||||
const McpServiceClient = createGrpcClient(McpServiceDefinition)
|
||||
const TaskServiceClient = createGrpcClient(TaskServiceDefinition)
|
||||
|
||||
export { BrowserServiceClient, CheckpointsServiceClient, FileServiceClient, TaskServiceClient, McpServiceClient }
|
||||
export {
|
||||
AccountServiceClient,
|
||||
BrowserServiceClient,
|
||||
CheckpointsServiceClient,
|
||||
FileServiceClient,
|
||||
TaskServiceClient,
|
||||
McpServiceClient,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user