mirror of
https://github.com/cline/cline.git
synced 2026-09-05 05:02:27 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98b5285628 | |||
| 7b416ccc70 | |||
| 29f3cfa894 | |||
| 978f34e30b | |||
| 445e25221a | |||
| 489a05117c | |||
| e572ee44f9 | |||
| f4e14bfe3b | |||
| bddc1b5e96 | |||
| cb0de8f17e | |||
| e1a0b244de | |||
| a9d5411bf0 | |||
| 16af9125ec | |||
| 2792e7698f | |||
| d02e5a89e5 | |||
| 20f19917d3 | |||
| 7e5cd52864 | |||
| 4622ad767b | |||
| 96048d5ac5 | |||
| facec93082 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
added copy buttons to task header and assistant messages
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Increasing file size that can be read by cline
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add org to team affiliation check
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix-bedrock-api
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix for markdown copy excessive escape characters
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Only initialize posthog in the webview if the user has opted into telemetry
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
requestVsCodeLmModels protobus migration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Allow the user to form their next message while Cline is taking action
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding mistral medium-3 model
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add Task Favorites and several proto messages related to tasks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Raise Errors when users try to upload images larger than 7500x7500 pixels
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed an issue where loading never finished when using an application inference profile for the model ID
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add support for Gemini Implicit Caching
|
||||
@@ -33,7 +33,8 @@ jobs:
|
||||
uses: morfien101/actions-authorized-user@4a3cfbf0bcb3cafe4a71710a278920c5d94bb38b
|
||||
with:
|
||||
username: ${{ github.actor }}
|
||||
team: "Deployer"
|
||||
org: ${{ github.repository_owner }}
|
||||
team: "deployer"
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check if user is authorized
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"getting-started/installing-dev-essentials",
|
||||
"getting-started/model-selection-guide",
|
||||
"getting-started/our-favorite-tech-stack",
|
||||
"getting-started/task-management",
|
||||
"getting-started/understanding-context-management",
|
||||
"getting-started/what-is-cline"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: "Task Management in Cline"
|
||||
description: "Learn how to effectively manage your task history, use favorites, and organize your work in Cline."
|
||||
---
|
||||
|
||||
# Task Management
|
||||
|
||||
As you use Cline, you'll accumulate many tasks over time. The task management system helps you organize, filter, search, and clean up your task history to keep your workspace efficient.
|
||||
|
||||
## Accessing Task History
|
||||
|
||||
You can access your task history by:
|
||||
|
||||
1. Clicking on the "History" button in the Cline sidebar
|
||||
2. Using the command palette to search for "Cline: Show Task History"
|
||||
|
||||
## Task History Features
|
||||
|
||||
The task history view provides several powerful features:
|
||||
|
||||
### Searching and Filtering
|
||||
|
||||
- **Search Bar**: Use the fuzzy search at the top to quickly find tasks by content
|
||||
- **Sort Options**: Sort tasks by:
|
||||
- Newest (default)
|
||||
- Oldest
|
||||
- Most Expensive (highest API cost)
|
||||
- Most Tokens (highest token usage)
|
||||
- Most Relevant (when searching)
|
||||
- **Favorites Filter**: Toggle to show only favorited tasks
|
||||
|
||||
### Task Actions
|
||||
|
||||
Each task in the history view has several actions available:
|
||||
|
||||
- **Open**: Click on a task to reopen it in the Cline chat
|
||||
- **Favorite**: Click the star icon to mark a task as a favorite
|
||||
- **Delete**: Remove individual tasks (favorites are protected from deletion)
|
||||
- **Export**: Export a task's conversation to markdown
|
||||
|
||||
## ⭐ Task Favorites
|
||||
|
||||
The favorites feature allows you to mark important tasks that you want to preserve and find quickly.
|
||||
|
||||
### How Favorites Work
|
||||
|
||||
- **Marking Favorites**: Click the star icon next to any task to toggle its favorite status
|
||||
- **Protection**: Favorited tasks are protected from individual and bulk deletion operations (can be overridden)
|
||||
- **Filtering**: Use the favorites filter to quickly access your important tasks
|
||||
|
||||
## Batch Operations
|
||||
|
||||
The task history view supports several batch operations:
|
||||
|
||||
- **Select Multiple**: Use the checkboxes to select multiple tasks
|
||||
- **Select All/None**: Quickly select or deselect all tasks
|
||||
- **Delete Selected**: Remove all selected tasks
|
||||
- **Delete All**: Remove all tasks from history (favorites are preserved unless you choose to include them)
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Favorite Important Tasks**: Mark reference tasks or frequently accessed conversations as favorites
|
||||
2. **Regular Cleanup**: Periodically remove old or unused tasks to improve performance
|
||||
3. **Use Search**: Leverage the fuzzy search to quickly find specific conversations
|
||||
4. **Export Valuable Tasks**: Export important tasks to markdown for external reference
|
||||
|
||||
Task management helps you maintain an organized workflow when using Cline, allowing you to quickly find past conversations, preserve important work, and keep your history clean and efficient.
|
||||
Generated
+20
-35
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.14.0",
|
||||
"version": "3.14.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.14.0",
|
||||
"version": "3.14.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
@@ -42,11 +42,11 @@
|
||||
"globby": "^14.0.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"ignore": "^7.0.3",
|
||||
"image-size": "^2.0.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"jschardet": "^3.1.4",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"node-cache": "^5.1.2",
|
||||
"ollama": "^0.5.13",
|
||||
"open-graph-scraper": "^6.9.0",
|
||||
"openai": "^4.83.0",
|
||||
@@ -12866,14 +12866,6 @@
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-deep": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
|
||||
@@ -16579,6 +16571,18 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/image-size": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz",
|
||||
"integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"image-size": "bin/image-size.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.x"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
@@ -19760,17 +19764,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/node-cache": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
|
||||
"integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==",
|
||||
"dependencies": {
|
||||
"clone": "2.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
@@ -34886,11 +34879,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="
|
||||
},
|
||||
"clone-deep": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
|
||||
@@ -37429,6 +37417,11 @@
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz",
|
||||
"integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="
|
||||
},
|
||||
"image-size": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz",
|
||||
"integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="
|
||||
},
|
||||
"immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
@@ -39565,14 +39558,6 @@
|
||||
"@types/nlcst": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node-cache": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
|
||||
"integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==",
|
||||
"requires": {
|
||||
"clone": "2.x"
|
||||
}
|
||||
},
|
||||
"node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
|
||||
+3
-3
@@ -121,7 +121,7 @@
|
||||
},
|
||||
{
|
||||
"command": "cline.generateGitCommitMessage",
|
||||
"title": "✨ Generate Commit Message with Cline",
|
||||
"title": "Generate Commit Message with Cline",
|
||||
"category": "Cline",
|
||||
"icon": "$(robot)"
|
||||
}
|
||||
@@ -314,7 +314,7 @@
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
||||
"protos": "node proto/build-proto.js && prettier src/shared/proto --write && prettier src/core/controller --write",
|
||||
"protos": "node proto/build-proto.js && prettier src/shared/proto src/core/controller webview-ui/src/services --write",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
@@ -411,11 +411,11 @@
|
||||
"globby": "^14.0.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"ignore": "^7.0.3",
|
||||
"image-size": "^2.0.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"jschardet": "^3.1.4",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"node-cache": "^5.1.2",
|
||||
"ollama": "^0.5.13",
|
||||
"open-graph-scraper": "^6.9.0",
|
||||
"openai": "^4.83.0",
|
||||
|
||||
+15
-1
@@ -10,7 +10,21 @@ import "common.proto";
|
||||
service ModelsService {
|
||||
// Fetches available models from Ollama
|
||||
rpc getOllamaModels(StringRequest) returns (StringArray);
|
||||
|
||||
// Fetches available models from LM Studio
|
||||
rpc getLmStudioModels(StringRequest) returns (StringArray);
|
||||
// Fetches available models from VS Code LM API
|
||||
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
message VsCodeLmModelsArray {
|
||||
repeated VsCodeLmModel models = 1;
|
||||
}
|
||||
|
||||
// Structure representing a VS Code LM model
|
||||
message VsCodeLmModel {
|
||||
string vendor = 1;
|
||||
string family = 2;
|
||||
string version = 3;
|
||||
string id = 4;
|
||||
}
|
||||
|
||||
+61
-1
@@ -16,9 +16,15 @@ service TaskService {
|
||||
// Creates a new task with the given text and optional images
|
||||
rpc newTask(NewTaskRequest) returns (Empty);
|
||||
// Shows a task with the specified ID
|
||||
rpc showTaskWithId(StringRequest) returns (Empty);
|
||||
rpc showTaskWithId(StringRequest) returns (TaskResponse);
|
||||
// Exports a task with the given ID to markdown
|
||||
rpc exportTaskWithId(StringRequest) returns (Empty);
|
||||
// Toggles the favorite status of a task
|
||||
rpc toggleTaskFavorite(TaskFavoriteRequest) returns (Empty);
|
||||
// Deletes all non-favorited tasks
|
||||
rpc deleteNonFavoritedTasks(EmptyRequest) returns (DeleteNonFavoritedTasksResults);
|
||||
// Gets filtered task history
|
||||
rpc getTaskHistory(GetTaskHistoryRequest) returns (TaskHistoryArray);
|
||||
}
|
||||
|
||||
// Request message for creating a new task
|
||||
@@ -28,3 +34,57 @@ message NewTaskRequest {
|
||||
repeated string images = 3;
|
||||
}
|
||||
|
||||
// Request message for toggling task favorite status
|
||||
message TaskFavoriteRequest {
|
||||
Metadata metadata = 1;
|
||||
string task_id = 2;
|
||||
bool is_favorited = 3;
|
||||
}
|
||||
|
||||
// Response for task details
|
||||
message TaskResponse {
|
||||
string id = 1;
|
||||
string task = 2;
|
||||
int64 ts = 3;
|
||||
bool is_favorited = 4;
|
||||
int64 size = 5;
|
||||
double total_cost = 6;
|
||||
int32 tokens_in = 7;
|
||||
int32 tokens_out = 8;
|
||||
int32 cache_writes = 9;
|
||||
int32 cache_reads = 10;
|
||||
}
|
||||
|
||||
// Results returned when deleting non-favorited tasks
|
||||
message DeleteNonFavoritedTasksResults {
|
||||
int32 tasks_preserved = 1;
|
||||
int32 tasks_deleted = 2;
|
||||
}
|
||||
|
||||
// Request for getting task history with filtering
|
||||
message GetTaskHistoryRequest {
|
||||
Metadata metadata = 1;
|
||||
bool favorites_only = 2;
|
||||
string search_query = 3;
|
||||
string sort_by = 4;
|
||||
}
|
||||
|
||||
// Response for task history
|
||||
message TaskHistoryArray {
|
||||
repeated TaskItem tasks = 1;
|
||||
int32 total_count = 2;
|
||||
}
|
||||
|
||||
// Task item details for history list
|
||||
message TaskItem {
|
||||
string id = 1;
|
||||
string task = 2;
|
||||
int64 ts = 3;
|
||||
bool is_favorited = 4;
|
||||
int64 size = 5;
|
||||
double total_cost = 6;
|
||||
int32 tokens_in = 7;
|
||||
int32 tokens_out = 8;
|
||||
int32 cache_writes = 9;
|
||||
int32 cache_reads = 10;
|
||||
}
|
||||
|
||||
@@ -272,10 +272,14 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate model ID, accounting for cross-region inference if enabled
|
||||
* Gets the appropriate model ID, accounting for cross-region inference if enabled.
|
||||
* If the model ID is an ARN that contains a slash, you will get the URL encoded ARN.
|
||||
*/
|
||||
async getModelId(): Promise<string> {
|
||||
if (this.options.awsUseCrossRegionInference) {
|
||||
if (this.options.awsBedrockCustomSelected && this.getModel().id.includes("/")) {
|
||||
return encodeURIComponent(this.getModel().id)
|
||||
}
|
||||
if (!this.options.awsBedrockCustomSelected && this.options.awsUseCrossRegionInference) {
|
||||
const regionPrefix = this.getRegion().slice(0, 3)
|
||||
switch (regionPrefix) {
|
||||
case "us-":
|
||||
|
||||
+6
-308
@@ -1,7 +1,6 @@
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
|
||||
import { GoogleGenAI, type Content, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
|
||||
import NodeCache from "node-cache"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
@@ -39,12 +38,6 @@ export class GeminiHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: GoogleGenAI
|
||||
|
||||
// Enhanced caching system
|
||||
private contentCaches: NodeCache // Stores cache details (key, count, etc.)
|
||||
private isCacheBusy = false
|
||||
private taskCacheNames: Map<string, string> = new Map() // Maps taskId to cache name for stable lookup
|
||||
private taskCacheTokens: Map<string, number> = new Map() // Maps taskId to total tokens in cache
|
||||
|
||||
constructor(options: GeminiHandlerOptions) {
|
||||
// Store the options
|
||||
this.options = options
|
||||
@@ -67,26 +60,13 @@ export class GeminiHandler implements ApiHandler {
|
||||
|
||||
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
|
||||
}
|
||||
|
||||
// Initialize cache with TTL and check period
|
||||
this.contentCaches = new NodeCache({
|
||||
stdTTL: DEFAULT_CACHE_TTL_SECONDS,
|
||||
checkperiod: DEFAULT_CACHE_TTL_SECONDS,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message using the Gemini API with optimized caching and split cost accounting.
|
||||
*
|
||||
* This method implements a task-based caching strategy:
|
||||
* 1. Each task gets its own cache, identified by taskId
|
||||
* 2. On first call for a task, a new cache is created
|
||||
* 3. On subsequent calls, the existing cache is reused and only new messages are sent
|
||||
* 4. Cache operations are tracked for accurate cost accounting
|
||||
* Creates a message using the Gemini API with implicit caching.
|
||||
*
|
||||
* Cost accounting:
|
||||
* - Immediate costs (returned in the usage object): Input tokens, output tokens, cache read costs
|
||||
* - Ongoing costs (tracked at task level): Cache storage costs for the TTL period
|
||||
*
|
||||
* @param systemPrompt The system prompt to use for the message
|
||||
* @param messages The conversation history to include in the message
|
||||
@@ -97,54 +77,6 @@ export class GeminiHandler implements ApiHandler {
|
||||
const { id: model, info } = this.getModel()
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
|
||||
// Ensure we have a stable cache key (taskId)
|
||||
if (!this.options.taskId) {
|
||||
console.warn("[GeminiHandler] No taskId provided, caching will be disabled")
|
||||
}
|
||||
|
||||
const taskId = this.options.taskId
|
||||
|
||||
// Calculate total content length for cache eligibility check
|
||||
const contentsLength = systemPrompt.length + this.getMessagesLength(contents)
|
||||
|
||||
// Minimum token threshold for caching (approx 4096 tokens)
|
||||
const CONTEXT_CACHE_TOKEN_MINIMUM = 4096
|
||||
|
||||
let uncachedContent: Content[] | undefined = undefined
|
||||
let cachedContent: string | undefined = undefined
|
||||
|
||||
// Check if caching is available and content is large enough to benefit from caching
|
||||
// We only enable caching for conversations above a certain size to avoid overhead for small requests
|
||||
const isCacheAvailable = info.supportsPromptCache && contentsLength > 4 * CONTEXT_CACHE_TOKEN_MINIMUM && taskId
|
||||
|
||||
// This flag tracks whether this operation involves a cache write/update
|
||||
// It's used to track task-level ongoing costs, not immediate costs
|
||||
let cacheWrite = false
|
||||
|
||||
if (isCacheAvailable) {
|
||||
// Check if we already have a cache for this task
|
||||
const existingCacheName = this.taskCacheNames.get(taskId)
|
||||
const cacheEntry = existingCacheName ? this.contentCaches.get<{ key: string; count: number }>(taskId) : undefined
|
||||
|
||||
if (cacheEntry) {
|
||||
// Use existing cache
|
||||
uncachedContent = contents.slice(cacheEntry.count, contents.length)
|
||||
cachedContent = cacheEntry.key
|
||||
console.log(
|
||||
`[GeminiHandler] using existing cache for task ${taskId}: ${cacheEntry.count} cached messages (${cacheEntry.key}) and ${uncachedContent.length} uncached messages`,
|
||||
)
|
||||
}
|
||||
|
||||
// Create or update cache only if there's new content to add
|
||||
const shouldUpdateCache = !existingCacheName || (cacheEntry && uncachedContent && uncachedContent.length > 0)
|
||||
|
||||
if (shouldUpdateCache) {
|
||||
// If we should update the cache, then there will be a cache write
|
||||
cacheWrite = true
|
||||
}
|
||||
}
|
||||
const isCacheUsed = !!cachedContent
|
||||
|
||||
// Configure thinking budget if supported
|
||||
const thinkingBudget = this.options.thinkingBudgetTokens ?? 0
|
||||
const maxBudget = info.thinkingConfig?.maxBudget ?? 0
|
||||
@@ -153,10 +85,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
const requestConfig: GenerateContentConfig = {
|
||||
// Add base URL if configured
|
||||
httpOptions: this.options.geminiBaseUrl ? { baseUrl: this.options.geminiBaseUrl } : undefined,
|
||||
|
||||
// Only include systemInstruction if NOT using the cache
|
||||
...(isCacheUsed ? {} : { systemInstruction: systemPrompt }),
|
||||
|
||||
...{ systemInstruction: systemPrompt },
|
||||
// Set temperature (default to 0)
|
||||
temperature: 0,
|
||||
}
|
||||
@@ -171,19 +100,12 @@ export class GeminiHandler implements ApiHandler {
|
||||
// Generate content using the configured parameters
|
||||
const result = await this.client.models.generateContentStream({
|
||||
model,
|
||||
contents: uncachedContent ?? contents,
|
||||
contents: contents,
|
||||
config: {
|
||||
...requestConfig,
|
||||
...(isCacheUsed ? { cachedContent } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
// Update the cache after the LLM request is already sent to avoid blocking
|
||||
// We only update the cache if we have a taskId and the cache write flag is set
|
||||
// This is a non-blocking operation and will not affect the response time
|
||||
if (cacheWrite && taskId) {
|
||||
this.updateCacheContent(taskId, model, contents, systemPrompt)
|
||||
}
|
||||
// Track usage metadata
|
||||
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
|
||||
|
||||
@@ -207,7 +129,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
const outputTokens = lastUsageMetadata.candidatesTokenCount ?? 0
|
||||
const cacheReadTokens = lastUsageMetadata.cachedContentTokenCount
|
||||
|
||||
// Calculate immediate costs only (excluding cache write/storage costs)
|
||||
// Calculate immediate costs
|
||||
const totalCost = this.calculateCost({
|
||||
info,
|
||||
inputTokens,
|
||||
@@ -215,214 +137,17 @@ export class GeminiHandler implements ApiHandler {
|
||||
cacheReadTokens,
|
||||
})
|
||||
|
||||
// Store the token count for task-level ongoing cost tracking
|
||||
// This is not included in the immediate costs returned to the user
|
||||
const cacheWriteTokens = cacheWrite ? inputTokens : undefined
|
||||
|
||||
// If this is a cache write operation, update the task's ongoing costs
|
||||
if (cacheWrite && this.options.taskId && inputTokens > 0) {
|
||||
// Log the ongoing costs for debugging
|
||||
const ongoingCosts = this.getTaskOngoingCosts(this.options.taskId)
|
||||
console.log(
|
||||
`[GeminiHandler] Task ${this.options.taskId} ongoing costs: $${ongoingCosts?.toFixed(6) ?? "unknown"}`,
|
||||
)
|
||||
}
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheWriteTokens,
|
||||
cacheWriteTokens: 0,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all caches for the current API key.
|
||||
*
|
||||
* According to the Gemini API documentation, you can retrieve metadata for all uploaded caches
|
||||
* using the caches.list() method. This is useful for monitoring cache usage and cleanup.
|
||||
*
|
||||
* @param pageSize Optional number of caches to return per page (default: 10)
|
||||
* @returns A promise that resolves to an array of cache metadata objects
|
||||
*/
|
||||
public async listCaches(pageSize: number = 10): Promise<any[]> {
|
||||
try {
|
||||
const caches: any[] = []
|
||||
const pager = await this.client.caches.list({ config: { pageSize } })
|
||||
|
||||
let page = pager.page
|
||||
while (true) {
|
||||
for (const cache of page) {
|
||||
caches.push(cache)
|
||||
}
|
||||
|
||||
if (!pager.hasNextPage()) {
|
||||
break
|
||||
}
|
||||
page = await pager.nextPage()
|
||||
}
|
||||
|
||||
return caches
|
||||
} catch (error) {
|
||||
console.error(`[GeminiHandler] Failed to list caches:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the content of a cache for a specific task.
|
||||
*
|
||||
* Since the Gemini API doesn't support incremental updates to cache content,
|
||||
* this method:
|
||||
* 1. Creates a new cache with the full content (old + new)
|
||||
* 2. Deletes the old cache if it exists
|
||||
* 3. Updates our local tracking to point to the new cache
|
||||
*
|
||||
* @param taskId The ID of the task whose cache should be updated
|
||||
* @param model The model to use for the cache
|
||||
* @param contents The full content to cache (including both old and new messages)
|
||||
* @param systemInstruction The system instruction to include in the cache
|
||||
*/
|
||||
private async updateCacheContent(
|
||||
taskId: string,
|
||||
model: string,
|
||||
contents: Content[],
|
||||
systemInstruction: string,
|
||||
): Promise<void> {
|
||||
if (this.isCacheBusy) {
|
||||
console.log(`[GeminiHandler] Cache is busy, skipping update for task ${taskId}`)
|
||||
return
|
||||
}
|
||||
|
||||
this.isCacheBusy = true
|
||||
const timestamp = Date.now()
|
||||
const existingCacheName = this.taskCacheNames.get(taskId)
|
||||
|
||||
try {
|
||||
// 1. Create a new cache with the full content
|
||||
const result = await this.client.caches.create({
|
||||
model,
|
||||
config: {
|
||||
contents,
|
||||
systemInstruction,
|
||||
ttl: `${DEFAULT_CACHE_TTL_SECONDS}s`,
|
||||
httpOptions: { timeout: 120_000 },
|
||||
},
|
||||
})
|
||||
|
||||
const { name, usageMetadata } = result
|
||||
|
||||
if (name) {
|
||||
// 2. Delete the old cache if it exists (non-blocking)
|
||||
// We don't await this operation to avoid blocking the main flow if deletion fails
|
||||
if (existingCacheName) {
|
||||
// Schedule cache deletion in the background
|
||||
setTimeout(() => {
|
||||
this.client.caches
|
||||
.delete({ name: existingCacheName })
|
||||
.then(() => {
|
||||
console.log(`[GeminiHandler] Deleted old cache ${existingCacheName} for task ${taskId}`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`[GeminiHandler] Failed to delete old cache ${existingCacheName}:`, error)
|
||||
console.log(`[GeminiHandler] Continuing without deleting old cache. It will expire after TTL.`)
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 3. Update our local tracking
|
||||
this.contentCaches.set<{ key: string; count: number }>(taskId, {
|
||||
key: name,
|
||||
count: contents.length,
|
||||
})
|
||||
this.taskCacheNames.set(taskId, name)
|
||||
|
||||
// Track total tokens in cache for ongoing cost calculation
|
||||
const totalTokens = usageMetadata?.totalTokenCount ?? 0
|
||||
this.taskCacheTokens.set(taskId, totalTokens)
|
||||
|
||||
const operation = existingCacheName ? "Updated" : "Created new"
|
||||
console.log(
|
||||
`[GeminiHandler] ${operation} cache for task ${taskId}: ${contents.length} messages (${totalTokens} tokens) in ${Date.now() - timestamp}ms`,
|
||||
)
|
||||
|
||||
return // Indicate that a cache write occurred
|
||||
}
|
||||
|
||||
return
|
||||
} catch (error) {
|
||||
console.error(`[GeminiHandler] Failed to update cache for task ${taskId}:`, error)
|
||||
return
|
||||
} finally {
|
||||
this.isCacheBusy = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the TTL of an existing cache.
|
||||
*
|
||||
* According to the Gemini API documentation, you can update the TTL of a cache
|
||||
* using the caches.update() method. This is useful for extending the lifetime
|
||||
* of a cache that's still being used.
|
||||
*
|
||||
* @param taskId The ID of the task whose cache TTL should be updated
|
||||
* @param ttlSeconds The new TTL in seconds
|
||||
* @returns A promise that resolves to the updated cache, or undefined if the update fails
|
||||
*/
|
||||
public async updateCacheTTL(taskId: string, ttlSeconds: number = DEFAULT_CACHE_TTL_SECONDS): Promise<any> {
|
||||
const cacheName = this.taskCacheNames.get(taskId)
|
||||
if (!cacheName) {
|
||||
console.warn(`[GeminiHandler] No cache found for task ${taskId}, cannot update TTL`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedCache = await this.client.caches.update({
|
||||
name: cacheName,
|
||||
config: { ttl: `${ttlSeconds}s` },
|
||||
})
|
||||
|
||||
console.log(`[GeminiHandler] Updated TTL for cache ${cacheName} to ${ttlSeconds}s`)
|
||||
return updatedCache
|
||||
} catch (error) {
|
||||
console.error(`[GeminiHandler] Failed to update TTL for cache ${cacheName}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the ongoing costs for a task based on cache storage.
|
||||
*
|
||||
* This method calculates the cost of holding tokens in cache for the TTL period.
|
||||
* These costs are separate from the immediate costs of API calls and should be
|
||||
* tracked at the task level rather than the message level.
|
||||
*
|
||||
* TODO: Surface these ongoing costs to the user in the UI, possibly in:
|
||||
* - The task header/summary
|
||||
* - A dedicated "costs" panel or tooltip
|
||||
* - As part of the total cost calculation for the task
|
||||
*
|
||||
* @param taskId The ID of the task to calculate ongoing costs for
|
||||
* @returns The ongoing cost in dollars, or undefined if no cache exists for the task
|
||||
*/
|
||||
public getTaskOngoingCosts(taskId: string): number | undefined {
|
||||
const tokens = this.taskCacheTokens.get(taskId)
|
||||
if (!tokens) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { info } = this.getModel()
|
||||
if (!info.cacheWritesPrice) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Calculate the cost of holding tokens in cache for the TTL period
|
||||
// (tokens / 1M) * (price per 1M tokens) * (cache TTL in hours)
|
||||
return info.cacheWritesPrice * (tokens / 1_000_000) * (DEFAULT_CACHE_TTL_SECONDS / 3600)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the immediate dollar cost of the API call based on token usage and model pricing.
|
||||
*
|
||||
@@ -430,21 +155,18 @@ export class GeminiHandler implements ApiHandler {
|
||||
* - Input token costs (for uncached tokens)
|
||||
* - Output token costs
|
||||
* - Cache read costs
|
||||
* - Gemini implicit caching has no write costs
|
||||
*
|
||||
* It does NOT include ongoing costs like cache storage, which are tracked separately
|
||||
* at the task level through getTaskOngoingCosts().
|
||||
*/
|
||||
public calculateCost({
|
||||
info,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheWriteTokens = 0,
|
||||
cacheReadTokens = 0,
|
||||
}: {
|
||||
info: ModelInfo
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheWriteTokens?: number
|
||||
cacheReadTokens?: number
|
||||
}) {
|
||||
// Exit early if any required pricing information is missing
|
||||
@@ -454,9 +176,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
|
||||
let inputPrice = info.inputPrice
|
||||
let outputPrice = info.outputPrice
|
||||
let cacheWritesPrice = info.cacheWritesPrice ?? 0
|
||||
// Right now, we only show the immediate costs of caching and not the ongoing costs of storing the cache
|
||||
cacheWritesPrice = 0
|
||||
let cacheReadsPrice = info.cacheReadsPrice ?? 0
|
||||
|
||||
// If there's tiered pricing then adjust prices based on the input tokens used
|
||||
@@ -465,7 +185,6 @@ export class GeminiHandler implements ApiHandler {
|
||||
if (tier) {
|
||||
inputPrice = tier.inputPrice ?? inputPrice
|
||||
outputPrice = tier.outputPrice ?? outputPrice
|
||||
cacheWritesPrice = tier.cacheWritesPrice ?? cacheWritesPrice
|
||||
cacheReadsPrice = tier.cacheReadsPrice ?? cacheReadsPrice
|
||||
}
|
||||
}
|
||||
@@ -502,27 +221,6 @@ export class GeminiHandler implements ApiHandler {
|
||||
return totalCost
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the total length of all messages for cache eligibility check
|
||||
*/
|
||||
private getMessagesLength(contents: Content[]): number {
|
||||
return contents.reduce((total, content) => {
|
||||
if (!content.parts) {
|
||||
return total
|
||||
}
|
||||
|
||||
return (
|
||||
total +
|
||||
content.parts.reduce((partTotal, part) => {
|
||||
if (typeof part.text === "string") {
|
||||
return partTotal + part.text.length
|
||||
}
|
||||
return partTotal
|
||||
}, 0)
|
||||
)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model ID and info for the current configuration
|
||||
*/
|
||||
|
||||
@@ -19,7 +19,6 @@ import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
|
||||
import { ClineAccountService } from "@services/account/ClineAccountService"
|
||||
import { BrowserSession } from "@services/browser/BrowserSession"
|
||||
import { McpHub } from "@services/mcp/McpHub"
|
||||
import { searchWorkspaceFiles } from "@services/search/file-search"
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import { ApiProvider, ModelInfo } from "@shared/api"
|
||||
import { ChatContent } from "@shared/ChatContent"
|
||||
@@ -28,11 +27,10 @@ import { ExtensionMessage, ExtensionState, Invoke, Platform } from "@shared/Exte
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { McpDownloadResponse, McpMarketplaceCatalog, McpServer } from "@shared/mcp"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { ClineCheckpointRestore, WebviewMessage } from "@shared/WebviewMessage"
|
||||
import { WebviewMessage } from "@shared/WebviewMessage"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { searchCommits, getWorkingState } from "@utils/git"
|
||||
import { getWorkingState } from "@utils/git"
|
||||
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
|
||||
import { getWorkspacePath } from "@utils/path"
|
||||
import { getTotalTasksSize } from "@utils/storage"
|
||||
import { openMention } from "../mentions"
|
||||
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
|
||||
@@ -332,10 +330,6 @@ export class Controller {
|
||||
case "resetState":
|
||||
await this.resetState()
|
||||
break
|
||||
case "requestVsCodeLmModels":
|
||||
const vsCodeLmModels = await this.getVsCodeLmModels()
|
||||
this.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
|
||||
break
|
||||
case "refreshOpenRouterModels":
|
||||
await this.refreshOpenRouterModels()
|
||||
break
|
||||
@@ -603,11 +597,18 @@ export class Controller {
|
||||
}
|
||||
case "clearAllTaskHistory": {
|
||||
const answer = await vscode.window.showWarningMessage(
|
||||
"Are you sure you want to delete all history?",
|
||||
"Delete",
|
||||
"What would you like to delete?",
|
||||
{ modal: true },
|
||||
"Delete All Except Favorites",
|
||||
"Delete Everything",
|
||||
"Cancel",
|
||||
)
|
||||
if (answer === "Delete") {
|
||||
|
||||
if (answer === "Delete All Except Favorites") {
|
||||
await this.deleteNonFavoriteTaskHistory()
|
||||
await this.postStateToWebview()
|
||||
this.refreshTotalTasksSize()
|
||||
} else if (answer === "Delete Everything") {
|
||||
await this.deleteAllTaskHistory()
|
||||
await this.postStateToWebview()
|
||||
this.refreshTotalTasksSize()
|
||||
@@ -884,18 +885,6 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
// VSCode LM API
|
||||
|
||||
private async getVsCodeLmModels() {
|
||||
try {
|
||||
const models = await vscode.lm.selectChatModels({})
|
||||
return models || []
|
||||
} catch (error) {
|
||||
console.error("Error fetching VS Code LM models:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Account
|
||||
|
||||
async fetchUserCreditsData() {
|
||||
@@ -1556,6 +1545,43 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
// await this.postStateToWebview()
|
||||
}
|
||||
|
||||
async deleteNonFavoriteTaskHistory() {
|
||||
await this.clearTask()
|
||||
|
||||
const taskHistory = ((await getGlobalState(this.context, "taskHistory")) as HistoryItem[]) || []
|
||||
const favoritedTasks = taskHistory.filter((task) => task.isFavorited === true)
|
||||
|
||||
// If user has no favorited tasks, show a warning message
|
||||
if (favoritedTasks.length === 0) {
|
||||
vscode.window.showWarningMessage("No favorited tasks found. Please favorite tasks before using this option.")
|
||||
await this.postStateToWebview()
|
||||
return
|
||||
}
|
||||
|
||||
await updateGlobalState(this.context, "taskHistory", favoritedTasks)
|
||||
|
||||
// Delete non-favorited task directories
|
||||
try {
|
||||
const preserveTaskIds = favoritedTasks.map((task) => task.id)
|
||||
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks")
|
||||
|
||||
if (await fileExistsAtPath(taskDirPath)) {
|
||||
const taskDirs = await fs.readdir(taskDirPath)
|
||||
for (const taskDir of taskDirs) {
|
||||
if (!preserveTaskIds.includes(taskDir)) {
|
||||
await fs.rm(path.join(taskDirPath, taskDir), { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Error deleting task history: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
async refreshTotalTasksSize() {
|
||||
getTotalTasksSize(this.context.globalStorageUri.fsPath)
|
||||
.then((newTotalSize) => {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { VsCodeLmModelsArray } from "../../../shared/proto/models"
|
||||
import * as vscode from "vscode"
|
||||
import { convertVsCodeNativeModelsToProtoModels } from "../../../shared/proto-conversions/models/vscode-lm-models-conversion"
|
||||
|
||||
/**
|
||||
* Fetches available models from VS Code LM API
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request
|
||||
* @returns Array of VS Code LM models
|
||||
*/
|
||||
export async function getVsCodeLmModels(controller: Controller, request: EmptyRequest): Promise<VsCodeLmModelsArray> {
|
||||
try {
|
||||
const models = await vscode.lm.selectChatModels({})
|
||||
|
||||
const protoModels = convertVsCodeNativeModelsToProtoModels(models || [])
|
||||
|
||||
return VsCodeLmModelsArray.create({ models: protoModels })
|
||||
} catch (error) {
|
||||
console.error("Error fetching VS Code LM models:", error)
|
||||
return VsCodeLmModelsArray.create({ models: [] })
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,12 @@
|
||||
import { registerMethod } from "./index"
|
||||
import { getLmStudioModels } from "./getLmStudioModels"
|
||||
import { getOllamaModels } from "./getOllamaModels"
|
||||
import { getVsCodeLmModels } from "./getVsCodeLmModels"
|
||||
|
||||
// Register all models service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("getLmStudioModels", getLmStudioModels)
|
||||
registerMethod("getOllamaModels", getOllamaModels)
|
||||
registerMethod("getVsCodeLmModels", getVsCodeLmModels)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { DeleteNonFavoritedTasksResults } from "../../../shared/proto/task"
|
||||
import { getGlobalState, updateGlobalState } from "../../storage/state"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
|
||||
/**
|
||||
* Deletes all non-favorited tasks, preserving only favorited ones
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request
|
||||
* @returns DeleteNonFavoritedTasksResults with counts of preserved and deleted tasks
|
||||
*/
|
||||
export async function deleteNonFavoritedTasks(
|
||||
controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
): Promise<DeleteNonFavoritedTasksResults> {
|
||||
try {
|
||||
// Clear current task first
|
||||
await controller.clearTask()
|
||||
|
||||
// Get existing task history
|
||||
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
|
||||
|
||||
// Filter out non-favorited tasks
|
||||
const favoritedTasks = taskHistory.filter((task) => task.isFavorited === true)
|
||||
const deletedCount = taskHistory.length - favoritedTasks.length
|
||||
|
||||
console.log(`[deleteNonFavoritedTasks] Found ${favoritedTasks.length} favorited tasks to preserve`)
|
||||
|
||||
// Update global state
|
||||
if (favoritedTasks.length > 0) {
|
||||
await updateGlobalState(controller.context, "taskHistory", favoritedTasks)
|
||||
} else {
|
||||
await updateGlobalState(controller.context, "taskHistory", undefined)
|
||||
}
|
||||
|
||||
// Handle file system cleanup for deleted tasks
|
||||
const preserveTaskIds = favoritedTasks.map((task) => task.id)
|
||||
await cleanupTaskFiles(controller, preserveTaskIds)
|
||||
|
||||
// Update webview
|
||||
try {
|
||||
await controller.postStateToWebview()
|
||||
} catch (webviewErr) {
|
||||
console.error("Error posting to webview:", webviewErr)
|
||||
}
|
||||
|
||||
return {
|
||||
tasksPreserved: favoritedTasks.length,
|
||||
tasksDeleted: deletedCount,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in deleteNonFavoritedTasks:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to cleanup task files while preserving specified tasks
|
||||
*/
|
||||
async function cleanupTaskFiles(controller: Controller, preserveTaskIds: string[]) {
|
||||
const taskDirPath = path.join(controller.context.globalStorageUri.fsPath, "tasks")
|
||||
|
||||
try {
|
||||
if (await fileExistsAtPath(taskDirPath)) {
|
||||
if (preserveTaskIds.length > 0) {
|
||||
const taskDirs = await fs.readdir(taskDirPath)
|
||||
console.debug(`[cleanupTaskFiles] Found ${taskDirs.length} task directories`)
|
||||
|
||||
// Delete only non-preserved task directories
|
||||
for (const dir of taskDirs) {
|
||||
if (!preserveTaskIds.includes(dir)) {
|
||||
await fs.rm(path.join(taskDirPath, dir), { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No tasks to preserve, delete everything
|
||||
await fs.rm(taskDirPath, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error cleaning up task files:", error)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Controller } from ".."
|
||||
import { GetTaskHistoryRequest, TaskHistoryArray } from "../../../shared/proto/task"
|
||||
import { getGlobalState } from "../../storage/state"
|
||||
|
||||
/**
|
||||
* Gets filtered task history
|
||||
* @param controller The controller instance
|
||||
* @param request Filter parameters for task history
|
||||
* @returns TaskHistoryArray with filtered task list
|
||||
*/
|
||||
export async function getTaskHistory(controller: Controller, request: GetTaskHistoryRequest): Promise<TaskHistoryArray> {
|
||||
try {
|
||||
const { favoritesOnly, searchQuery, sortBy } = request
|
||||
|
||||
// Get task history from global state
|
||||
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
|
||||
|
||||
// Apply filters
|
||||
let filteredTasks = taskHistory.filter((item) => {
|
||||
// Basic filter: must have timestamp and task content
|
||||
const hasRequiredFields = item.ts && item.task
|
||||
|
||||
// Apply favorites filter if requested
|
||||
if (favoritesOnly && hasRequiredFields) {
|
||||
return item.isFavorited === true
|
||||
}
|
||||
|
||||
return hasRequiredFields
|
||||
})
|
||||
|
||||
// Apply search if provided
|
||||
if (searchQuery) {
|
||||
// Simple search implementation
|
||||
const query = searchQuery.toLowerCase()
|
||||
filteredTasks = filteredTasks.filter((item) => item.task.toLowerCase().includes(query))
|
||||
}
|
||||
|
||||
// Calculate total count before sorting
|
||||
const totalCount = filteredTasks.length
|
||||
|
||||
// Apply sorting
|
||||
if (sortBy) {
|
||||
filteredTasks.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case "oldest":
|
||||
return a.ts - b.ts
|
||||
case "mostExpensive":
|
||||
return (b.totalCost || 0) - (a.totalCost || 0)
|
||||
case "mostTokens":
|
||||
return (
|
||||
(b.tokensIn || 0) +
|
||||
(b.tokensOut || 0) +
|
||||
(b.cacheWrites || 0) +
|
||||
(b.cacheReads || 0) -
|
||||
((a.tokensIn || 0) + (a.tokensOut || 0) + (a.cacheWrites || 0) + (a.cacheReads || 0))
|
||||
)
|
||||
case "newest":
|
||||
default:
|
||||
return b.ts - a.ts
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Default sort by newest
|
||||
filteredTasks.sort((a, b) => b.ts - a.ts)
|
||||
}
|
||||
|
||||
// Map to response format
|
||||
const tasks = filteredTasks.map((item) => ({
|
||||
id: item.id,
|
||||
task: item.task,
|
||||
ts: item.ts,
|
||||
isFavorited: item.isFavorited || false,
|
||||
size: item.size || 0,
|
||||
totalCost: item.totalCost || 0,
|
||||
tokensIn: item.tokensIn || 0,
|
||||
tokensOut: item.tokensOut || 0,
|
||||
cacheWrites: item.cacheWrites || 0,
|
||||
cacheReads: item.cacheReads || 0,
|
||||
}))
|
||||
|
||||
return {
|
||||
tasks,
|
||||
totalCount,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in getTaskHistory:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -5,18 +5,24 @@
|
||||
import { registerMethod } from "./index"
|
||||
import { cancelTask } from "./cancelTask"
|
||||
import { clearTask } from "./clearTask"
|
||||
import { deleteNonFavoritedTasks } from "./deleteNonFavoritedTasks"
|
||||
import { deleteTasksWithIds } from "./deleteTasksWithIds"
|
||||
import { exportTaskWithId } from "./exportTaskWithId"
|
||||
import { getTaskHistory } from "./getTaskHistory"
|
||||
import { newTask } from "./newTask"
|
||||
import { showTaskWithId } from "./showTaskWithId"
|
||||
import { toggleTaskFavorite } from "./toggleTaskFavorite"
|
||||
|
||||
// Register all task service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("cancelTask", cancelTask)
|
||||
registerMethod("clearTask", clearTask)
|
||||
registerMethod("deleteNonFavoritedTasks", deleteNonFavoritedTasks)
|
||||
registerMethod("deleteTasksWithIds", deleteTasksWithIds)
|
||||
registerMethod("exportTaskWithId", exportTaskWithId)
|
||||
registerMethod("getTaskHistory", getTaskHistory)
|
||||
registerMethod("newTask", newTask)
|
||||
registerMethod("showTaskWithId", showTaskWithId)
|
||||
registerMethod("toggleTaskFavorite", toggleTaskFavorite)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,73 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
import { StringRequest } from "../../../shared/proto/common"
|
||||
import { TaskResponse } from "../../../shared/proto/task"
|
||||
|
||||
/**
|
||||
* Shows a task with the specified ID
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the task ID
|
||||
* @returns Empty response
|
||||
* @returns TaskResponse with task details
|
||||
*/
|
||||
export async function showTaskWithId(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
export async function showTaskWithId(controller: Controller, request: StringRequest): Promise<TaskResponse> {
|
||||
try {
|
||||
await controller.showTaskWithId(request.value)
|
||||
return Empty.create()
|
||||
const id = request.value
|
||||
|
||||
// First check if task exists in global state for faster access
|
||||
const taskHistory = ((await controller.context.globalState.get("taskHistory")) as any[]) || []
|
||||
const historyItem = taskHistory.find((item) => item.id === id)
|
||||
|
||||
// We need to initialize the task before returning data
|
||||
if (historyItem) {
|
||||
// Always initialize the task with the history item
|
||||
await controller.initTask(undefined, undefined, historyItem)
|
||||
|
||||
// Send UI update to show the chat view
|
||||
await controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
|
||||
// Return task data for gRPC response
|
||||
return {
|
||||
id: historyItem.id,
|
||||
task: historyItem.task || "",
|
||||
ts: historyItem.ts || 0,
|
||||
isFavorited: historyItem.isFavorited || false,
|
||||
size: historyItem.size || 0,
|
||||
totalCost: historyItem.totalCost || 0,
|
||||
tokensIn: historyItem.tokensIn || 0,
|
||||
tokensOut: historyItem.tokensOut || 0,
|
||||
cacheWrites: historyItem.cacheWrites || 0,
|
||||
cacheReads: historyItem.cacheReads || 0,
|
||||
}
|
||||
}
|
||||
|
||||
// If not in global state, fetch from storage
|
||||
const { historyItem: fetchedItem } = await controller.getTaskWithId(id)
|
||||
|
||||
// Initialize the task with the fetched item
|
||||
await controller.initTask(undefined, undefined, fetchedItem)
|
||||
|
||||
// Send UI update to show the chat view
|
||||
await controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
|
||||
return {
|
||||
id: fetchedItem.id,
|
||||
task: fetchedItem.task || "",
|
||||
ts: fetchedItem.ts || 0,
|
||||
isFavorited: fetchedItem.isFavorited || false,
|
||||
size: fetchedItem.size || 0,
|
||||
totalCost: fetchedItem.totalCost || 0,
|
||||
tokensIn: fetchedItem.tokensIn || 0,
|
||||
tokensOut: fetchedItem.tokensOut || 0,
|
||||
cacheWrites: fetchedItem.cacheWrites || 0,
|
||||
cacheReads: fetchedItem.cacheReads || 0,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in showTaskWithId:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Controller } from "../"
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { TaskFavoriteRequest } from "../../../shared/proto/task"
|
||||
|
||||
export async function toggleTaskFavorite(controller: Controller, request: TaskFavoriteRequest): Promise<Empty> {
|
||||
if (!request.taskId || request.isFavorited === undefined) {
|
||||
const errorMsg = `[toggleTaskFavorite] Invalid request: taskId or isFavorited missing`
|
||||
console.error(errorMsg)
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
// Update in-memory state only
|
||||
try {
|
||||
const history = ((await controller.context.globalState.get("taskHistory")) as any[]) || []
|
||||
|
||||
const taskIndex = history.findIndex((item) => item.id === request.taskId)
|
||||
|
||||
if (taskIndex === -1) {
|
||||
console.log(`[toggleTaskFavorite] Task not found in history array!`)
|
||||
} else {
|
||||
// Create a new array instead of modifying in place to ensure state change
|
||||
const updatedHistory = [...history]
|
||||
updatedHistory[taskIndex] = {
|
||||
...updatedHistory[taskIndex],
|
||||
isFavorited: request.isFavorited,
|
||||
}
|
||||
|
||||
// Update global state and wait for it to complete
|
||||
try {
|
||||
await controller.context.globalState.update("taskHistory", updatedHistory)
|
||||
} catch (stateErr) {
|
||||
console.error("Error updating global state:", stateErr)
|
||||
}
|
||||
}
|
||||
} catch (historyErr) {
|
||||
console.error("Error processing task history:", historyErr)
|
||||
}
|
||||
|
||||
// Post to webview
|
||||
try {
|
||||
await controller.postStateToWebview()
|
||||
} catch (webviewErr) {
|
||||
console.error("Error posting to webview:", webviewErr)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in toggleTaskFavorite:", error)
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
@@ -118,6 +118,7 @@ export class Task {
|
||||
private cancelTask: () => Promise<void>
|
||||
|
||||
readonly taskId: string
|
||||
private taskIsFavorited?: boolean
|
||||
api: ApiHandler
|
||||
private terminalManager: TerminalManager
|
||||
private urlContentFetcher: UrlContentFetcher
|
||||
@@ -211,6 +212,7 @@ export class Task {
|
||||
// Initialize taskId first
|
||||
if (historyItem) {
|
||||
this.taskId = historyItem.id
|
||||
this.taskIsFavorited = historyItem.isFavorited
|
||||
this.conversationHistoryDeletedRange = historyItem.conversationHistoryDeletedRange
|
||||
} else if (task || images) {
|
||||
this.taskId = Date.now().toString()
|
||||
@@ -314,6 +316,7 @@ export class Task {
|
||||
size: taskDirSize,
|
||||
shadowGitConfigWorkTree: await this.checkpointTracker?.getShadowGitConfigWorkTree(),
|
||||
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
|
||||
isFavorited: this.taskIsFavorited,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to save cline messages:", error)
|
||||
@@ -1916,7 +1919,7 @@ export class Task {
|
||||
: "other_diff_error"
|
||||
|
||||
// Add telemetry for diff edit failure
|
||||
telemetryService.captureDiffEditFailure(this.taskId, errorType)
|
||||
telemetryService.captureDiffEditFailure(this.taskId, this.api.getModel().id, errorType)
|
||||
|
||||
pushToolResult(
|
||||
formatResponse.toolError(
|
||||
|
||||
@@ -40,7 +40,8 @@ export async function extractTextFromFile(filePath: string): Promise<string> {
|
||||
return extractTextFromIPYNB(filePath)
|
||||
default:
|
||||
const fileBuffer = await fs.readFile(filePath)
|
||||
if (fileBuffer.byteLength > 300 * 1024) {
|
||||
if (fileBuffer.byteLength > 20 * 1000 * 1024) {
|
||||
// 20MB limit (20 * 1000 * 1024 bytes, decimal MB)
|
||||
throw new Error(`File is too large to read into context.`)
|
||||
}
|
||||
const encoding = await detectEncoding(fileBuffer, fileExtension)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as vscode from "vscode"
|
||||
import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import sizeOf from "image-size"
|
||||
|
||||
export async function selectImages(): Promise<string[]> {
|
||||
const options: vscode.OpenDialogOptions = {
|
||||
@@ -17,16 +18,36 @@ export async function selectImages(): Promise<string[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
return await Promise.all(
|
||||
fileUris.map(async (uri) => {
|
||||
const imagePath = uri.fsPath
|
||||
const buffer = await fs.readFile(imagePath)
|
||||
const base64 = buffer.toString("base64")
|
||||
const mimeType = getMimeType(imagePath)
|
||||
const dataUrl = `data:${mimeType};base64,${base64}`
|
||||
return dataUrl
|
||||
}),
|
||||
)
|
||||
const processedImagePromises = fileUris.map(async (uri) => {
|
||||
const imagePath = uri.fsPath
|
||||
let buffer: Buffer
|
||||
try {
|
||||
// Read the file into a buffer first
|
||||
buffer = await fs.readFile(imagePath)
|
||||
// Convert Node.js Buffer to Uint8Array
|
||||
const uint8Array = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
|
||||
const dimensions = sizeOf(uint8Array) // Get dimensions from Uint8Array
|
||||
if (dimensions.width! > 7500 || dimensions.height! > 7500) {
|
||||
console.warn(`Image dimensions exceed 7500px, skipping: ${imagePath}`)
|
||||
vscode.window.showErrorMessage(
|
||||
`Image too large: ${path.basename(imagePath)} was skipped (dimensions exceed 7500px).`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading file or getting dimensions for ${imagePath}:`, error)
|
||||
vscode.window.showErrorMessage(`Could not read dimensions for ${path.basename(imagePath)}, skipping.`)
|
||||
return null
|
||||
}
|
||||
|
||||
// If dimensions are valid, proceed to convert the existing buffer to base64
|
||||
const base64 = buffer.toString("base64")
|
||||
const mimeType = getMimeType(imagePath)
|
||||
return `data:${mimeType};base64,${base64}`
|
||||
})
|
||||
|
||||
const dataUrlsWithNulls = await Promise.all(processedImagePromises)
|
||||
return dataUrlsWithNulls.filter((url) => url !== null) as string[] // Filter out skipped images
|
||||
}
|
||||
|
||||
function getMimeType(filePath: string): string {
|
||||
|
||||
@@ -512,13 +512,14 @@ class PostHogClient {
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param errorType Type of error that occurred (e.g., "search_not_found", "invalid_format")
|
||||
*/
|
||||
public captureDiffEditFailure(taskId: string, errorType?: string, collect: boolean = false) {
|
||||
public captureDiffEditFailure(taskId: string, modelId: string, errorType?: string, collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: PostHogClient.EVENTS.TASK.DIFF_EDIT_FAILED,
|
||||
properties: {
|
||||
taskId,
|
||||
errorType,
|
||||
modelId,
|
||||
},
|
||||
},
|
||||
collect,
|
||||
|
||||
@@ -28,8 +28,6 @@ export interface ExtensionMessage {
|
||||
| "requestyModels"
|
||||
| "mcpServers"
|
||||
| "relinquishControl"
|
||||
| "vsCodeLmModels"
|
||||
| "requestVsCodeLmModels"
|
||||
| "authCallback"
|
||||
| "mcpMarketplaceCatalog"
|
||||
| "mcpDownloadDetails"
|
||||
|
||||
@@ -11,4 +11,5 @@ export type HistoryItem = {
|
||||
size?: number
|
||||
shadowGitConfigWorkTree?: string
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
isFavorited?: boolean
|
||||
}
|
||||
|
||||
+26
-6
@@ -204,27 +204,33 @@ export const bedrockModels = {
|
||||
contextWindow: 300_000,
|
||||
supportsImages: true,
|
||||
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 3.2,
|
||||
// cacheWritesPrice: 3.2, // not written
|
||||
cacheReadsPrice: 0.2,
|
||||
},
|
||||
"amazon.nova-lite-v1:0": {
|
||||
maxTokens: 5000,
|
||||
contextWindow: 300_000,
|
||||
supportsImages: true,
|
||||
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.06,
|
||||
outputPrice: 0.24,
|
||||
// cacheWritesPrice: 0.24, // not written
|
||||
cacheReadsPrice: 0.015,
|
||||
},
|
||||
"amazon.nova-micro-v1:0": {
|
||||
maxTokens: 5000,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.035,
|
||||
outputPrice: 0.14,
|
||||
// cacheWritesPrice: 0.14, // not written
|
||||
cacheReadsPrice: 0.00875,
|
||||
},
|
||||
"anthropic.claude-3-7-sonnet-20250219-v1:0": {
|
||||
maxTokens: 8192,
|
||||
@@ -251,10 +257,10 @@ export const bedrockModels = {
|
||||
"anthropic.claude-3-5-haiku-20241022-v1:0": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: false,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.0,
|
||||
outputPrice: 5.0,
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 4.0,
|
||||
cacheWritesPrice: 1.0,
|
||||
cacheReadsPrice: 0.08,
|
||||
},
|
||||
@@ -436,16 +442,19 @@ export const vertexModels = {
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 2.5,
|
||||
outputPrice: 15,
|
||||
cacheReadsPrice: 0.31,
|
||||
tiers: [
|
||||
{
|
||||
contextWindow: 200000,
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10,
|
||||
cacheReadsPrice: 0.31,
|
||||
},
|
||||
{
|
||||
contextWindow: Infinity,
|
||||
inputPrice: 2.5,
|
||||
outputPrice: 15,
|
||||
cacheReadsPrice: 0.625,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -566,16 +575,19 @@ export const geminiModels = {
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 2.5,
|
||||
outputPrice: 15,
|
||||
cacheReadsPrice: 0.31,
|
||||
tiers: [
|
||||
{
|
||||
contextWindow: 200000,
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10,
|
||||
cacheReadsPrice: 0.31,
|
||||
},
|
||||
{
|
||||
contextWindow: Infinity,
|
||||
inputPrice: 2.5,
|
||||
outputPrice: 15,
|
||||
cacheReadsPrice: 0.625,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1387,6 +1399,14 @@ export const mistralModels = {
|
||||
inputPrice: 0.1,
|
||||
outputPrice: 0.3,
|
||||
},
|
||||
"mistral-medium-latest": {
|
||||
maxTokens: 128_000,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.4,
|
||||
outputPrice: 2.0,
|
||||
},
|
||||
"mistral-small-2501": {
|
||||
maxTokens: 32_000,
|
||||
contextWindow: 32_000,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { VsCodeLmModel } from "../../proto/models"
|
||||
|
||||
/**
|
||||
* Represents a VS Code language model in the native VS Code format
|
||||
*/
|
||||
export interface VsCodeNativeModel {
|
||||
vendor?: string
|
||||
family?: string
|
||||
version?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts VS Code native model format to protobuf format
|
||||
*/
|
||||
export function convertVsCodeNativeModelsToProtoModels(models: VsCodeNativeModel[]): VsCodeLmModel[] {
|
||||
return (models || []).map((model) => ({
|
||||
vendor: model.vendor || "",
|
||||
family: model.family || "",
|
||||
version: model.version || "",
|
||||
id: model.id || "",
|
||||
}))
|
||||
}
|
||||
+222
-1
@@ -5,10 +5,192 @@
|
||||
// source: models.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { StringArray, StringRequest } from "./common"
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
|
||||
import { EmptyRequest, StringArray, StringRequest } from "./common"
|
||||
|
||||
export const protobufPackage = "cline"
|
||||
|
||||
/** List of VS Code LM models */
|
||||
export interface VsCodeLmModelsArray {
|
||||
models: VsCodeLmModel[]
|
||||
}
|
||||
|
||||
/** Structure representing a VS Code LM model */
|
||||
export interface VsCodeLmModel {
|
||||
vendor: string
|
||||
family: string
|
||||
version: string
|
||||
id: string
|
||||
}
|
||||
|
||||
function createBaseVsCodeLmModelsArray(): VsCodeLmModelsArray {
|
||||
return { models: [] }
|
||||
}
|
||||
|
||||
export const VsCodeLmModelsArray: MessageFns<VsCodeLmModelsArray> = {
|
||||
encode(message: VsCodeLmModelsArray, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
for (const v of message.models) {
|
||||
VsCodeLmModel.encode(v!, writer.uint32(10).fork()).join()
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): VsCodeLmModelsArray {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseVsCodeLmModelsArray()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break
|
||||
}
|
||||
|
||||
message.models.push(VsCodeLmModel.decode(reader, reader.uint32()))
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): VsCodeLmModelsArray {
|
||||
return {
|
||||
models: globalThis.Array.isArray(object?.models) ? object.models.map((e: any) => VsCodeLmModel.fromJSON(e)) : [],
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: VsCodeLmModelsArray): unknown {
|
||||
const obj: any = {}
|
||||
if (message.models?.length) {
|
||||
obj.models = message.models.map((e) => VsCodeLmModel.toJSON(e))
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<VsCodeLmModelsArray>, I>>(base?: I): VsCodeLmModelsArray {
|
||||
return VsCodeLmModelsArray.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<VsCodeLmModelsArray>, I>>(object: I): VsCodeLmModelsArray {
|
||||
const message = createBaseVsCodeLmModelsArray()
|
||||
message.models = object.models?.map((e) => VsCodeLmModel.fromPartial(e)) || []
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseVsCodeLmModel(): VsCodeLmModel {
|
||||
return { vendor: "", family: "", version: "", id: "" }
|
||||
}
|
||||
|
||||
export const VsCodeLmModel: MessageFns<VsCodeLmModel> = {
|
||||
encode(message: VsCodeLmModel, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.vendor !== "") {
|
||||
writer.uint32(10).string(message.vendor)
|
||||
}
|
||||
if (message.family !== "") {
|
||||
writer.uint32(18).string(message.family)
|
||||
}
|
||||
if (message.version !== "") {
|
||||
writer.uint32(26).string(message.version)
|
||||
}
|
||||
if (message.id !== "") {
|
||||
writer.uint32(34).string(message.id)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): VsCodeLmModel {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseVsCodeLmModel()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break
|
||||
}
|
||||
|
||||
message.vendor = reader.string()
|
||||
continue
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break
|
||||
}
|
||||
|
||||
message.family = reader.string()
|
||||
continue
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break
|
||||
}
|
||||
|
||||
message.version = reader.string()
|
||||
continue
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break
|
||||
}
|
||||
|
||||
message.id = reader.string()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): VsCodeLmModel {
|
||||
return {
|
||||
vendor: isSet(object.vendor) ? globalThis.String(object.vendor) : "",
|
||||
family: isSet(object.family) ? globalThis.String(object.family) : "",
|
||||
version: isSet(object.version) ? globalThis.String(object.version) : "",
|
||||
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: VsCodeLmModel): unknown {
|
||||
const obj: any = {}
|
||||
if (message.vendor !== "") {
|
||||
obj.vendor = message.vendor
|
||||
}
|
||||
if (message.family !== "") {
|
||||
obj.family = message.family
|
||||
}
|
||||
if (message.version !== "") {
|
||||
obj.version = message.version
|
||||
}
|
||||
if (message.id !== "") {
|
||||
obj.id = message.id
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<VsCodeLmModel>, I>>(base?: I): VsCodeLmModel {
|
||||
return VsCodeLmModel.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<VsCodeLmModel>, I>>(object: I): VsCodeLmModel {
|
||||
const message = createBaseVsCodeLmModel()
|
||||
message.vendor = object.vendor ?? ""
|
||||
message.family = object.family ?? ""
|
||||
message.version = object.version ?? ""
|
||||
message.id = object.id ?? ""
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
/** Service for model-related operations */
|
||||
export type ModelsServiceDefinition = typeof ModelsServiceDefinition
|
||||
export const ModelsServiceDefinition = {
|
||||
@@ -33,5 +215,44 @@ export const ModelsServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Fetches available models from VS Code LM API */
|
||||
getVsCodeLmModels: {
|
||||
name: "getVsCodeLmModels",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: VsCodeLmModelsArray,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin
|
||||
? T
|
||||
: T extends globalThis.Array<infer U>
|
||||
? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {}
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never
|
||||
export type Exact<P, I extends P> = P extends Builtin
|
||||
? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined
|
||||
}
|
||||
|
||||
export interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T
|
||||
fromJSON(object: any): T
|
||||
toJSON(message: T): unknown
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
|
||||
}
|
||||
|
||||
+878
-1
@@ -17,6 +17,61 @@ export interface NewTaskRequest {
|
||||
images: string[]
|
||||
}
|
||||
|
||||
/** Request message for toggling task favorite status */
|
||||
export interface TaskFavoriteRequest {
|
||||
metadata?: Metadata | undefined
|
||||
taskId: string
|
||||
isFavorited: boolean
|
||||
}
|
||||
|
||||
/** Response for task details */
|
||||
export interface TaskResponse {
|
||||
id: string
|
||||
task: string
|
||||
ts: number
|
||||
isFavorited: boolean
|
||||
size: number
|
||||
totalCost: number
|
||||
tokensIn: number
|
||||
tokensOut: number
|
||||
cacheWrites: number
|
||||
cacheReads: number
|
||||
}
|
||||
|
||||
/** Results returned when deleting non-favorited tasks */
|
||||
export interface DeleteNonFavoritedTasksResults {
|
||||
tasksPreserved: number
|
||||
tasksDeleted: number
|
||||
}
|
||||
|
||||
/** Request for getting task history with filtering */
|
||||
export interface GetTaskHistoryRequest {
|
||||
metadata?: Metadata | undefined
|
||||
favoritesOnly: boolean
|
||||
searchQuery: string
|
||||
sortBy: string
|
||||
}
|
||||
|
||||
/** Response for task history */
|
||||
export interface TaskHistoryArray {
|
||||
tasks: TaskItem[]
|
||||
totalCount: number
|
||||
}
|
||||
|
||||
/** Task item details for history list */
|
||||
export interface TaskItem {
|
||||
id: string
|
||||
task: string
|
||||
ts: number
|
||||
isFavorited: boolean
|
||||
size: number
|
||||
totalCost: number
|
||||
tokensIn: number
|
||||
tokensOut: number
|
||||
cacheWrites: number
|
||||
cacheReads: number
|
||||
}
|
||||
|
||||
function createBaseNewTaskRequest(): NewTaskRequest {
|
||||
return { metadata: undefined, text: "", images: [] }
|
||||
}
|
||||
@@ -110,6 +165,790 @@ export const NewTaskRequest: MessageFns<NewTaskRequest> = {
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseTaskFavoriteRequest(): TaskFavoriteRequest {
|
||||
return { metadata: undefined, taskId: "", isFavorited: false }
|
||||
}
|
||||
|
||||
export const TaskFavoriteRequest: MessageFns<TaskFavoriteRequest> = {
|
||||
encode(message: TaskFavoriteRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.metadata !== undefined) {
|
||||
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
|
||||
}
|
||||
if (message.taskId !== "") {
|
||||
writer.uint32(18).string(message.taskId)
|
||||
}
|
||||
if (message.isFavorited !== false) {
|
||||
writer.uint32(24).bool(message.isFavorited)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): TaskFavoriteRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseTaskFavoriteRequest()
|
||||
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.taskId = reader.string()
|
||||
continue
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 24) {
|
||||
break
|
||||
}
|
||||
|
||||
message.isFavorited = reader.bool()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): TaskFavoriteRequest {
|
||||
return {
|
||||
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
|
||||
taskId: isSet(object.taskId) ? globalThis.String(object.taskId) : "",
|
||||
isFavorited: isSet(object.isFavorited) ? globalThis.Boolean(object.isFavorited) : false,
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: TaskFavoriteRequest): unknown {
|
||||
const obj: any = {}
|
||||
if (message.metadata !== undefined) {
|
||||
obj.metadata = Metadata.toJSON(message.metadata)
|
||||
}
|
||||
if (message.taskId !== "") {
|
||||
obj.taskId = message.taskId
|
||||
}
|
||||
if (message.isFavorited !== false) {
|
||||
obj.isFavorited = message.isFavorited
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<TaskFavoriteRequest>, I>>(base?: I): TaskFavoriteRequest {
|
||||
return TaskFavoriteRequest.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<TaskFavoriteRequest>, I>>(object: I): TaskFavoriteRequest {
|
||||
const message = createBaseTaskFavoriteRequest()
|
||||
message.metadata =
|
||||
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
|
||||
message.taskId = object.taskId ?? ""
|
||||
message.isFavorited = object.isFavorited ?? false
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseTaskResponse(): TaskResponse {
|
||||
return {
|
||||
id: "",
|
||||
task: "",
|
||||
ts: 0,
|
||||
isFavorited: false,
|
||||
size: 0,
|
||||
totalCost: 0,
|
||||
tokensIn: 0,
|
||||
tokensOut: 0,
|
||||
cacheWrites: 0,
|
||||
cacheReads: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export const TaskResponse: MessageFns<TaskResponse> = {
|
||||
encode(message: TaskResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== "") {
|
||||
writer.uint32(10).string(message.id)
|
||||
}
|
||||
if (message.task !== "") {
|
||||
writer.uint32(18).string(message.task)
|
||||
}
|
||||
if (message.ts !== 0) {
|
||||
writer.uint32(24).int64(message.ts)
|
||||
}
|
||||
if (message.isFavorited !== false) {
|
||||
writer.uint32(32).bool(message.isFavorited)
|
||||
}
|
||||
if (message.size !== 0) {
|
||||
writer.uint32(40).int64(message.size)
|
||||
}
|
||||
if (message.totalCost !== 0) {
|
||||
writer.uint32(49).double(message.totalCost)
|
||||
}
|
||||
if (message.tokensIn !== 0) {
|
||||
writer.uint32(56).int32(message.tokensIn)
|
||||
}
|
||||
if (message.tokensOut !== 0) {
|
||||
writer.uint32(64).int32(message.tokensOut)
|
||||
}
|
||||
if (message.cacheWrites !== 0) {
|
||||
writer.uint32(72).int32(message.cacheWrites)
|
||||
}
|
||||
if (message.cacheReads !== 0) {
|
||||
writer.uint32(80).int32(message.cacheReads)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): TaskResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseTaskResponse()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break
|
||||
}
|
||||
|
||||
message.id = reader.string()
|
||||
continue
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break
|
||||
}
|
||||
|
||||
message.task = reader.string()
|
||||
continue
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 24) {
|
||||
break
|
||||
}
|
||||
|
||||
message.ts = longToNumber(reader.int64())
|
||||
continue
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 32) {
|
||||
break
|
||||
}
|
||||
|
||||
message.isFavorited = reader.bool()
|
||||
continue
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 40) {
|
||||
break
|
||||
}
|
||||
|
||||
message.size = longToNumber(reader.int64())
|
||||
continue
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 49) {
|
||||
break
|
||||
}
|
||||
|
||||
message.totalCost = reader.double()
|
||||
continue
|
||||
}
|
||||
case 7: {
|
||||
if (tag !== 56) {
|
||||
break
|
||||
}
|
||||
|
||||
message.tokensIn = reader.int32()
|
||||
continue
|
||||
}
|
||||
case 8: {
|
||||
if (tag !== 64) {
|
||||
break
|
||||
}
|
||||
|
||||
message.tokensOut = reader.int32()
|
||||
continue
|
||||
}
|
||||
case 9: {
|
||||
if (tag !== 72) {
|
||||
break
|
||||
}
|
||||
|
||||
message.cacheWrites = reader.int32()
|
||||
continue
|
||||
}
|
||||
case 10: {
|
||||
if (tag !== 80) {
|
||||
break
|
||||
}
|
||||
|
||||
message.cacheReads = reader.int32()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): TaskResponse {
|
||||
return {
|
||||
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||
task: isSet(object.task) ? globalThis.String(object.task) : "",
|
||||
ts: isSet(object.ts) ? globalThis.Number(object.ts) : 0,
|
||||
isFavorited: isSet(object.isFavorited) ? globalThis.Boolean(object.isFavorited) : false,
|
||||
size: isSet(object.size) ? globalThis.Number(object.size) : 0,
|
||||
totalCost: isSet(object.totalCost) ? globalThis.Number(object.totalCost) : 0,
|
||||
tokensIn: isSet(object.tokensIn) ? globalThis.Number(object.tokensIn) : 0,
|
||||
tokensOut: isSet(object.tokensOut) ? globalThis.Number(object.tokensOut) : 0,
|
||||
cacheWrites: isSet(object.cacheWrites) ? globalThis.Number(object.cacheWrites) : 0,
|
||||
cacheReads: isSet(object.cacheReads) ? globalThis.Number(object.cacheReads) : 0,
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: TaskResponse): unknown {
|
||||
const obj: any = {}
|
||||
if (message.id !== "") {
|
||||
obj.id = message.id
|
||||
}
|
||||
if (message.task !== "") {
|
||||
obj.task = message.task
|
||||
}
|
||||
if (message.ts !== 0) {
|
||||
obj.ts = Math.round(message.ts)
|
||||
}
|
||||
if (message.isFavorited !== false) {
|
||||
obj.isFavorited = message.isFavorited
|
||||
}
|
||||
if (message.size !== 0) {
|
||||
obj.size = Math.round(message.size)
|
||||
}
|
||||
if (message.totalCost !== 0) {
|
||||
obj.totalCost = message.totalCost
|
||||
}
|
||||
if (message.tokensIn !== 0) {
|
||||
obj.tokensIn = Math.round(message.tokensIn)
|
||||
}
|
||||
if (message.tokensOut !== 0) {
|
||||
obj.tokensOut = Math.round(message.tokensOut)
|
||||
}
|
||||
if (message.cacheWrites !== 0) {
|
||||
obj.cacheWrites = Math.round(message.cacheWrites)
|
||||
}
|
||||
if (message.cacheReads !== 0) {
|
||||
obj.cacheReads = Math.round(message.cacheReads)
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<TaskResponse>, I>>(base?: I): TaskResponse {
|
||||
return TaskResponse.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<TaskResponse>, I>>(object: I): TaskResponse {
|
||||
const message = createBaseTaskResponse()
|
||||
message.id = object.id ?? ""
|
||||
message.task = object.task ?? ""
|
||||
message.ts = object.ts ?? 0
|
||||
message.isFavorited = object.isFavorited ?? false
|
||||
message.size = object.size ?? 0
|
||||
message.totalCost = object.totalCost ?? 0
|
||||
message.tokensIn = object.tokensIn ?? 0
|
||||
message.tokensOut = object.tokensOut ?? 0
|
||||
message.cacheWrites = object.cacheWrites ?? 0
|
||||
message.cacheReads = object.cacheReads ?? 0
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseDeleteNonFavoritedTasksResults(): DeleteNonFavoritedTasksResults {
|
||||
return { tasksPreserved: 0, tasksDeleted: 0 }
|
||||
}
|
||||
|
||||
export const DeleteNonFavoritedTasksResults: MessageFns<DeleteNonFavoritedTasksResults> = {
|
||||
encode(message: DeleteNonFavoritedTasksResults, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.tasksPreserved !== 0) {
|
||||
writer.uint32(8).int32(message.tasksPreserved)
|
||||
}
|
||||
if (message.tasksDeleted !== 0) {
|
||||
writer.uint32(16).int32(message.tasksDeleted)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): DeleteNonFavoritedTasksResults {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseDeleteNonFavoritedTasksResults()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break
|
||||
}
|
||||
|
||||
message.tasksPreserved = reader.int32()
|
||||
continue
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 16) {
|
||||
break
|
||||
}
|
||||
|
||||
message.tasksDeleted = reader.int32()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): DeleteNonFavoritedTasksResults {
|
||||
return {
|
||||
tasksPreserved: isSet(object.tasksPreserved) ? globalThis.Number(object.tasksPreserved) : 0,
|
||||
tasksDeleted: isSet(object.tasksDeleted) ? globalThis.Number(object.tasksDeleted) : 0,
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: DeleteNonFavoritedTasksResults): unknown {
|
||||
const obj: any = {}
|
||||
if (message.tasksPreserved !== 0) {
|
||||
obj.tasksPreserved = Math.round(message.tasksPreserved)
|
||||
}
|
||||
if (message.tasksDeleted !== 0) {
|
||||
obj.tasksDeleted = Math.round(message.tasksDeleted)
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<DeleteNonFavoritedTasksResults>, I>>(base?: I): DeleteNonFavoritedTasksResults {
|
||||
return DeleteNonFavoritedTasksResults.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<DeleteNonFavoritedTasksResults>, I>>(object: I): DeleteNonFavoritedTasksResults {
|
||||
const message = createBaseDeleteNonFavoritedTasksResults()
|
||||
message.tasksPreserved = object.tasksPreserved ?? 0
|
||||
message.tasksDeleted = object.tasksDeleted ?? 0
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseGetTaskHistoryRequest(): GetTaskHistoryRequest {
|
||||
return { metadata: undefined, favoritesOnly: false, searchQuery: "", sortBy: "" }
|
||||
}
|
||||
|
||||
export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
|
||||
encode(message: GetTaskHistoryRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.metadata !== undefined) {
|
||||
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
|
||||
}
|
||||
if (message.favoritesOnly !== false) {
|
||||
writer.uint32(16).bool(message.favoritesOnly)
|
||||
}
|
||||
if (message.searchQuery !== "") {
|
||||
writer.uint32(26).string(message.searchQuery)
|
||||
}
|
||||
if (message.sortBy !== "") {
|
||||
writer.uint32(34).string(message.sortBy)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): GetTaskHistoryRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseGetTaskHistoryRequest()
|
||||
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.favoritesOnly = reader.bool()
|
||||
continue
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break
|
||||
}
|
||||
|
||||
message.searchQuery = reader.string()
|
||||
continue
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break
|
||||
}
|
||||
|
||||
message.sortBy = reader.string()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): GetTaskHistoryRequest {
|
||||
return {
|
||||
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
|
||||
favoritesOnly: isSet(object.favoritesOnly) ? globalThis.Boolean(object.favoritesOnly) : false,
|
||||
searchQuery: isSet(object.searchQuery) ? globalThis.String(object.searchQuery) : "",
|
||||
sortBy: isSet(object.sortBy) ? globalThis.String(object.sortBy) : "",
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: GetTaskHistoryRequest): unknown {
|
||||
const obj: any = {}
|
||||
if (message.metadata !== undefined) {
|
||||
obj.metadata = Metadata.toJSON(message.metadata)
|
||||
}
|
||||
if (message.favoritesOnly !== false) {
|
||||
obj.favoritesOnly = message.favoritesOnly
|
||||
}
|
||||
if (message.searchQuery !== "") {
|
||||
obj.searchQuery = message.searchQuery
|
||||
}
|
||||
if (message.sortBy !== "") {
|
||||
obj.sortBy = message.sortBy
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<GetTaskHistoryRequest>, I>>(base?: I): GetTaskHistoryRequest {
|
||||
return GetTaskHistoryRequest.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<GetTaskHistoryRequest>, I>>(object: I): GetTaskHistoryRequest {
|
||||
const message = createBaseGetTaskHistoryRequest()
|
||||
message.metadata =
|
||||
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
|
||||
message.favoritesOnly = object.favoritesOnly ?? false
|
||||
message.searchQuery = object.searchQuery ?? ""
|
||||
message.sortBy = object.sortBy ?? ""
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseTaskHistoryArray(): TaskHistoryArray {
|
||||
return { tasks: [], totalCount: 0 }
|
||||
}
|
||||
|
||||
export const TaskHistoryArray: MessageFns<TaskHistoryArray> = {
|
||||
encode(message: TaskHistoryArray, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
for (const v of message.tasks) {
|
||||
TaskItem.encode(v!, writer.uint32(10).fork()).join()
|
||||
}
|
||||
if (message.totalCount !== 0) {
|
||||
writer.uint32(16).int32(message.totalCount)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): TaskHistoryArray {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseTaskHistoryArray()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break
|
||||
}
|
||||
|
||||
message.tasks.push(TaskItem.decode(reader, reader.uint32()))
|
||||
continue
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 16) {
|
||||
break
|
||||
}
|
||||
|
||||
message.totalCount = reader.int32()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): TaskHistoryArray {
|
||||
return {
|
||||
tasks: globalThis.Array.isArray(object?.tasks) ? object.tasks.map((e: any) => TaskItem.fromJSON(e)) : [],
|
||||
totalCount: isSet(object.totalCount) ? globalThis.Number(object.totalCount) : 0,
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: TaskHistoryArray): unknown {
|
||||
const obj: any = {}
|
||||
if (message.tasks?.length) {
|
||||
obj.tasks = message.tasks.map((e) => TaskItem.toJSON(e))
|
||||
}
|
||||
if (message.totalCount !== 0) {
|
||||
obj.totalCount = Math.round(message.totalCount)
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<TaskHistoryArray>, I>>(base?: I): TaskHistoryArray {
|
||||
return TaskHistoryArray.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<TaskHistoryArray>, I>>(object: I): TaskHistoryArray {
|
||||
const message = createBaseTaskHistoryArray()
|
||||
message.tasks = object.tasks?.map((e) => TaskItem.fromPartial(e)) || []
|
||||
message.totalCount = object.totalCount ?? 0
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
function createBaseTaskItem(): TaskItem {
|
||||
return {
|
||||
id: "",
|
||||
task: "",
|
||||
ts: 0,
|
||||
isFavorited: false,
|
||||
size: 0,
|
||||
totalCost: 0,
|
||||
tokensIn: 0,
|
||||
tokensOut: 0,
|
||||
cacheWrites: 0,
|
||||
cacheReads: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export const TaskItem: MessageFns<TaskItem> = {
|
||||
encode(message: TaskItem, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== "") {
|
||||
writer.uint32(10).string(message.id)
|
||||
}
|
||||
if (message.task !== "") {
|
||||
writer.uint32(18).string(message.task)
|
||||
}
|
||||
if (message.ts !== 0) {
|
||||
writer.uint32(24).int64(message.ts)
|
||||
}
|
||||
if (message.isFavorited !== false) {
|
||||
writer.uint32(32).bool(message.isFavorited)
|
||||
}
|
||||
if (message.size !== 0) {
|
||||
writer.uint32(40).int64(message.size)
|
||||
}
|
||||
if (message.totalCost !== 0) {
|
||||
writer.uint32(49).double(message.totalCost)
|
||||
}
|
||||
if (message.tokensIn !== 0) {
|
||||
writer.uint32(56).int32(message.tokensIn)
|
||||
}
|
||||
if (message.tokensOut !== 0) {
|
||||
writer.uint32(64).int32(message.tokensOut)
|
||||
}
|
||||
if (message.cacheWrites !== 0) {
|
||||
writer.uint32(72).int32(message.cacheWrites)
|
||||
}
|
||||
if (message.cacheReads !== 0) {
|
||||
writer.uint32(80).int32(message.cacheReads)
|
||||
}
|
||||
return writer
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): TaskItem {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
|
||||
let end = length === undefined ? reader.len : reader.pos + length
|
||||
const message = createBaseTaskItem()
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32()
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break
|
||||
}
|
||||
|
||||
message.id = reader.string()
|
||||
continue
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break
|
||||
}
|
||||
|
||||
message.task = reader.string()
|
||||
continue
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 24) {
|
||||
break
|
||||
}
|
||||
|
||||
message.ts = longToNumber(reader.int64())
|
||||
continue
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 32) {
|
||||
break
|
||||
}
|
||||
|
||||
message.isFavorited = reader.bool()
|
||||
continue
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 40) {
|
||||
break
|
||||
}
|
||||
|
||||
message.size = longToNumber(reader.int64())
|
||||
continue
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 49) {
|
||||
break
|
||||
}
|
||||
|
||||
message.totalCost = reader.double()
|
||||
continue
|
||||
}
|
||||
case 7: {
|
||||
if (tag !== 56) {
|
||||
break
|
||||
}
|
||||
|
||||
message.tokensIn = reader.int32()
|
||||
continue
|
||||
}
|
||||
case 8: {
|
||||
if (tag !== 64) {
|
||||
break
|
||||
}
|
||||
|
||||
message.tokensOut = reader.int32()
|
||||
continue
|
||||
}
|
||||
case 9: {
|
||||
if (tag !== 72) {
|
||||
break
|
||||
}
|
||||
|
||||
message.cacheWrites = reader.int32()
|
||||
continue
|
||||
}
|
||||
case 10: {
|
||||
if (tag !== 80) {
|
||||
break
|
||||
}
|
||||
|
||||
message.cacheReads = reader.int32()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break
|
||||
}
|
||||
reader.skip(tag & 7)
|
||||
}
|
||||
return message
|
||||
},
|
||||
|
||||
fromJSON(object: any): TaskItem {
|
||||
return {
|
||||
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||
task: isSet(object.task) ? globalThis.String(object.task) : "",
|
||||
ts: isSet(object.ts) ? globalThis.Number(object.ts) : 0,
|
||||
isFavorited: isSet(object.isFavorited) ? globalThis.Boolean(object.isFavorited) : false,
|
||||
size: isSet(object.size) ? globalThis.Number(object.size) : 0,
|
||||
totalCost: isSet(object.totalCost) ? globalThis.Number(object.totalCost) : 0,
|
||||
tokensIn: isSet(object.tokensIn) ? globalThis.Number(object.tokensIn) : 0,
|
||||
tokensOut: isSet(object.tokensOut) ? globalThis.Number(object.tokensOut) : 0,
|
||||
cacheWrites: isSet(object.cacheWrites) ? globalThis.Number(object.cacheWrites) : 0,
|
||||
cacheReads: isSet(object.cacheReads) ? globalThis.Number(object.cacheReads) : 0,
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(message: TaskItem): unknown {
|
||||
const obj: any = {}
|
||||
if (message.id !== "") {
|
||||
obj.id = message.id
|
||||
}
|
||||
if (message.task !== "") {
|
||||
obj.task = message.task
|
||||
}
|
||||
if (message.ts !== 0) {
|
||||
obj.ts = Math.round(message.ts)
|
||||
}
|
||||
if (message.isFavorited !== false) {
|
||||
obj.isFavorited = message.isFavorited
|
||||
}
|
||||
if (message.size !== 0) {
|
||||
obj.size = Math.round(message.size)
|
||||
}
|
||||
if (message.totalCost !== 0) {
|
||||
obj.totalCost = message.totalCost
|
||||
}
|
||||
if (message.tokensIn !== 0) {
|
||||
obj.tokensIn = Math.round(message.tokensIn)
|
||||
}
|
||||
if (message.tokensOut !== 0) {
|
||||
obj.tokensOut = Math.round(message.tokensOut)
|
||||
}
|
||||
if (message.cacheWrites !== 0) {
|
||||
obj.cacheWrites = Math.round(message.cacheWrites)
|
||||
}
|
||||
if (message.cacheReads !== 0) {
|
||||
obj.cacheReads = Math.round(message.cacheReads)
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<TaskItem>, I>>(base?: I): TaskItem {
|
||||
return TaskItem.fromPartial(base ?? ({} as any))
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<TaskItem>, I>>(object: I): TaskItem {
|
||||
const message = createBaseTaskItem()
|
||||
message.id = object.id ?? ""
|
||||
message.task = object.task ?? ""
|
||||
message.ts = object.ts ?? 0
|
||||
message.isFavorited = object.isFavorited ?? false
|
||||
message.size = object.size ?? 0
|
||||
message.totalCost = object.totalCost ?? 0
|
||||
message.tokensIn = object.tokensIn ?? 0
|
||||
message.tokensOut = object.tokensOut ?? 0
|
||||
message.cacheWrites = object.cacheWrites ?? 0
|
||||
message.cacheReads = object.cacheReads ?? 0
|
||||
return message
|
||||
},
|
||||
}
|
||||
|
||||
export type TaskServiceDefinition = typeof TaskServiceDefinition
|
||||
export const TaskServiceDefinition = {
|
||||
name: "TaskService",
|
||||
@@ -156,7 +995,7 @@ export const TaskServiceDefinition = {
|
||||
name: "showTaskWithId",
|
||||
requestType: StringRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseType: TaskResponse,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
@@ -169,6 +1008,33 @@ export const TaskServiceDefinition = {
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Toggles the favorite status of a task */
|
||||
toggleTaskFavorite: {
|
||||
name: "toggleTaskFavorite",
|
||||
requestType: TaskFavoriteRequest,
|
||||
requestStream: false,
|
||||
responseType: Empty,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Deletes all non-favorited tasks */
|
||||
deleteNonFavoritedTasks: {
|
||||
name: "deleteNonFavoritedTasks",
|
||||
requestType: EmptyRequest,
|
||||
requestStream: false,
|
||||
responseType: DeleteNonFavoritedTasksResults,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
/** Gets filtered task history */
|
||||
getTaskHistory: {
|
||||
name: "getTaskHistory",
|
||||
requestType: GetTaskHistoryRequest,
|
||||
requestStream: false,
|
||||
responseType: TaskHistoryArray,
|
||||
responseStream: false,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -189,6 +1055,17 @@ export type Exact<P, I extends P> = P extends Builtin
|
||||
? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
|
||||
|
||||
function longToNumber(int64: { toString(): string }): number {
|
||||
const num = globalThis.Number(int64.toString())
|
||||
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
|
||||
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER")
|
||||
}
|
||||
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
|
||||
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER")
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useEffect, type ReactNode } from "react"
|
||||
import { PostHogProvider } from "posthog-js/react"
|
||||
import posthog from "posthog-js"
|
||||
import { posthogConfig } from "@shared/services/config/posthog-config"
|
||||
import { useExtensionState } from "./context/ExtensionStateContext"
|
||||
|
||||
export function CustomPostHogProvider({ children }: { children: ReactNode }) {
|
||||
const { telemetrySetting } = useExtensionState()
|
||||
const isTelemetryEnabled = telemetrySetting === "enabled"
|
||||
|
||||
useEffect(() => {
|
||||
if (isTelemetryEnabled) {
|
||||
posthog.init(posthogConfig.apiKey, {
|
||||
api_host: posthogConfig.host,
|
||||
autocapture: false,
|
||||
disable_session_recording: true,
|
||||
})
|
||||
} else {
|
||||
posthog.opt_out_capturing()
|
||||
}
|
||||
}, [isTelemetryEnabled])
|
||||
|
||||
return <PostHogProvider client={posthog}>{children}</PostHogProvider>
|
||||
}
|
||||
@@ -1,25 +1,18 @@
|
||||
import type { ReactNode } from "react"
|
||||
import { type ReactNode } from "react"
|
||||
|
||||
import { ExtensionStateContextProvider } from "./context/ExtensionStateContext"
|
||||
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
|
||||
import { HeroUIProvider } from "@heroui/react"
|
||||
import { PostHogProvider } from "posthog-js/react"
|
||||
import { posthogConfig } from "@shared/services/config/posthog-config"
|
||||
import posthog from "posthog-js"
|
||||
|
||||
posthog.init(posthogConfig.apiKey, {
|
||||
api_host: posthogConfig.host,
|
||||
autocapture: false,
|
||||
})
|
||||
import { CustomPostHogProvider } from "./CustomPostHogProvider"
|
||||
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<ExtensionStateContextProvider>
|
||||
<PostHogProvider client={posthog}>
|
||||
<CustomPostHogProvider>
|
||||
<FirebaseAuthProvider>
|
||||
<HeroUIProvider>{children}</HeroUIProvider>
|
||||
</FirebaseAuthProvider>
|
||||
</PostHogProvider>
|
||||
</CustomPostHogProvider>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeBadge, VSCodeProgressRing, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState, MouseEvent } from "react"
|
||||
|
||||
@@ -20,6 +20,62 @@ import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { FileServiceClient } from "@/services/grpc-client"
|
||||
import { CheckmarkControl } from "@/components/common/CheckmarkControl"
|
||||
|
||||
interface CopyButtonProps {
|
||||
textToCopy: string | undefined
|
||||
}
|
||||
|
||||
const CopyButtonStyled = styled(VSCodeButton)`
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
`
|
||||
|
||||
interface WithCopyButtonProps {
|
||||
children: React.ReactNode
|
||||
textToCopy?: string
|
||||
style?: React.CSSProperties
|
||||
ref?: React.Ref<HTMLDivElement>
|
||||
onMouseUp?: (event: MouseEvent<HTMLDivElement>) => void
|
||||
}
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
position: relative;
|
||||
|
||||
&:hover ${CopyButtonStyled} {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const WithCopyButton = React.forwardRef<HTMLDivElement, WithCopyButtonProps>(
|
||||
({ children, textToCopy, style, onMouseUp, ...props }, ref) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!textToCopy) return
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 1500)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer ref={ref} onMouseUp={onMouseUp} style={style} {...props}>
|
||||
{children}
|
||||
{textToCopy && (
|
||||
<CopyButtonStyled appearance="icon" onClick={handleCopy} aria-label={copied ? "Copied" : "Copy"}>
|
||||
<span className={`codicon codicon-${copied ? "check" : "copy"}`}></span>
|
||||
</CopyButtonStyled>
|
||||
)}
|
||||
</StyledContainer>
|
||||
)
|
||||
},
|
||||
)
|
||||
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
|
||||
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
@@ -90,6 +146,7 @@ const Markdown = memo(({ markdown }: { markdown?: string }) => {
|
||||
overflowWrap: "anywhere",
|
||||
marginBottom: -15,
|
||||
marginTop: -15,
|
||||
overflow: "hidden", // contain child margins so that parent diff matches height of children
|
||||
}}>
|
||||
<MarkdownBlock markdown={markdown} />
|
||||
</div>
|
||||
@@ -901,7 +958,7 @@ export const ChatRowContent = ({
|
||||
return <McpResponseDisplay responseText={message.text || ""} />
|
||||
case "text":
|
||||
return (
|
||||
<div ref={contentRef} onMouseUp={handleMouseUp} style={{ position: "relative" }}>
|
||||
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={message.text}>
|
||||
<Markdown markdown={message.text} />
|
||||
{quoteButtonState.visible && (
|
||||
<QuoteButton
|
||||
@@ -912,7 +969,7 @@ export const ChatRowContent = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</WithCopyButton>
|
||||
)
|
||||
case "reasoning":
|
||||
return (
|
||||
@@ -1132,13 +1189,13 @@ export const ChatRowContent = ({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={contentRef} // Added ref
|
||||
onMouseUp={handleMouseUp} // Added handler
|
||||
<WithCopyButton
|
||||
ref={contentRef}
|
||||
onMouseUp={handleMouseUp}
|
||||
textToCopy={text}
|
||||
style={{
|
||||
color: "var(--vscode-charts-green)",
|
||||
paddingTop: 10,
|
||||
position: "relative", // Added position
|
||||
}}>
|
||||
<Markdown markdown={text} />
|
||||
{quoteButtonState.visible && (
|
||||
@@ -1148,7 +1205,7 @@ export const ChatRowContent = ({
|
||||
onClick={handleQuoteClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</WithCopyButton>
|
||||
{message.partial !== true && hasChanges && (
|
||||
<div style={{ paddingTop: 17 }}>
|
||||
<SuccessButton
|
||||
@@ -1295,13 +1352,13 @@ export const ChatRowContent = ({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={contentRef} // Added ref
|
||||
onMouseUp={handleMouseUp} // Added handler
|
||||
<WithCopyButton
|
||||
ref={contentRef}
|
||||
onMouseUp={handleMouseUp}
|
||||
textToCopy={text}
|
||||
style={{
|
||||
color: "var(--vscode-charts-green)",
|
||||
paddingTop: 10,
|
||||
position: "relative", // Added position
|
||||
}}>
|
||||
<Markdown markdown={text} />
|
||||
{quoteButtonState.visible && (
|
||||
@@ -1311,30 +1368,30 @@ export const ChatRowContent = ({
|
||||
onClick={handleQuoteClick}
|
||||
/>
|
||||
)}
|
||||
{message.partial !== true && hasChanges && (
|
||||
<div style={{ marginTop: 15 }}>
|
||||
<SuccessButton
|
||||
appearance="secondary"
|
||||
disabled={seeNewChangesDisabled}
|
||||
onClick={() => {
|
||||
setSeeNewChangesDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "taskCompletionViewChanges",
|
||||
number: message.ts,
|
||||
})
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-new-file"
|
||||
style={{
|
||||
marginRight: 6,
|
||||
cursor: seeNewChangesDisabled ? "wait" : "pointer",
|
||||
}}
|
||||
/>
|
||||
See new changes
|
||||
</SuccessButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</WithCopyButton>
|
||||
{message.partial !== true && hasChanges && (
|
||||
<div style={{ marginTop: 15 }}>
|
||||
<SuccessButton
|
||||
appearance="secondary"
|
||||
disabled={seeNewChangesDisabled}
|
||||
onClick={() => {
|
||||
setSeeNewChangesDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "taskCompletionViewChanges",
|
||||
number: message.ts,
|
||||
})
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-new-file"
|
||||
style={{
|
||||
marginRight: 6,
|
||||
cursor: seeNewChangesDisabled ? "wait" : "pointer",
|
||||
}}
|
||||
/>
|
||||
See new changes
|
||||
</SuccessButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
@@ -1362,7 +1419,11 @@ export const ChatRowContent = ({
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<div ref={contentRef} onMouseUp={handleMouseUp} style={{ position: "relative", paddingTop: 10 }}>
|
||||
<WithCopyButton
|
||||
ref={contentRef}
|
||||
onMouseUp={handleMouseUp}
|
||||
textToCopy={question}
|
||||
style={{ paddingTop: 10 }}>
|
||||
<Markdown markdown={question} />
|
||||
<OptionsButtons
|
||||
options={options}
|
||||
@@ -1379,7 +1440,7 @@ export const ChatRowContent = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</WithCopyButton>
|
||||
</>
|
||||
)
|
||||
case "new_task":
|
||||
@@ -1428,7 +1489,7 @@ export const ChatRowContent = ({
|
||||
response = message.text
|
||||
}
|
||||
return (
|
||||
<div ref={contentRef} onMouseUp={handleMouseUp} style={{ position: "relative" }}>
|
||||
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={response}>
|
||||
<Markdown markdown={response} />
|
||||
<OptionsButtons
|
||||
options={options}
|
||||
@@ -1445,7 +1506,7 @@ export const ChatRowContent = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</WithCopyButton>
|
||||
)
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -40,11 +40,29 @@ import { ChatSettings } from "@shared/ChatSettings"
|
||||
import ServersToggleModal from "./ServersToggleModal"
|
||||
import ClineRulesToggleModal from "../cline-rules/ClineRulesToggleModal"
|
||||
|
||||
const getImageDimensions = (dataUrl: string): Promise<{ width: number; height: number }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
if (img.naturalWidth > 7500 || img.naturalHeight > 7500) {
|
||||
reject(new Error("Image dimensions exceed maximum allowed size of 7500px."))
|
||||
} else {
|
||||
resolve({ width: img.naturalWidth, height: img.naturalHeight })
|
||||
}
|
||||
}
|
||||
img.onerror = (err) => {
|
||||
console.error("Failed to load image for dimension check:", err)
|
||||
reject(new Error("Failed to load image to check dimensions."))
|
||||
}
|
||||
img.src = dataUrl
|
||||
})
|
||||
}
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
inputValue: string
|
||||
activeQuote: string | null
|
||||
setInputValue: (value: string) => void
|
||||
textAreaDisabled: boolean
|
||||
sendingDisabled: boolean
|
||||
placeholderText: string
|
||||
selectedImages: string[]
|
||||
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>
|
||||
@@ -229,7 +247,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
inputValue,
|
||||
activeQuote,
|
||||
setInputValue,
|
||||
textAreaDisabled,
|
||||
sendingDisabled,
|
||||
placeholderText,
|
||||
selectedImages,
|
||||
setSelectedImages,
|
||||
@@ -276,6 +294,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
const shiftHoldTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const [showUnsupportedFileError, setShowUnsupportedFileError] = useState(false)
|
||||
const unsupportedFileTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const [showDimensionError, setShowDimensionError] = useState(false)
|
||||
const dimensionErrorTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const [fileSearchResults, setFileSearchResults] = useState<SearchResult[]>([])
|
||||
const [searchLoading, setSearchLoading] = useState(false)
|
||||
@@ -519,8 +539,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
const isComposing = event.nativeEvent?.isComposing ?? false
|
||||
if (event.key === "Enter" && !event.shiftKey && !isComposing) {
|
||||
event.preventDefault()
|
||||
setIsTextAreaFocused(false)
|
||||
onSend()
|
||||
|
||||
if (!sendingDisabled) {
|
||||
setIsTextAreaFocused(false)
|
||||
onSend()
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === "Backspace" && !isComposing) {
|
||||
@@ -604,6 +627,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
selectedSlashCommandsIndex,
|
||||
slashCommandsQuery,
|
||||
handleSlashCommandsSelect,
|
||||
sendingDisabled,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -729,6 +753,17 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
onFocusChange?.(false) // Call prop on blur
|
||||
}, [isMouseDownOnMenu, onFocusChange])
|
||||
|
||||
const showDimensionErrorMessage = useCallback(() => {
|
||||
setShowDimensionError(true)
|
||||
if (dimensionErrorTimerRef.current) {
|
||||
clearTimeout(dimensionErrorTimerRef.current)
|
||||
}
|
||||
dimensionErrorTimerRef.current = setTimeout(() => {
|
||||
setShowDimensionError(false)
|
||||
dimensionErrorTimerRef.current = null
|
||||
}, 3000)
|
||||
}, [])
|
||||
|
||||
const handlePaste = useCallback(
|
||||
async (e: React.ClipboardEvent) => {
|
||||
const items = e.clipboardData.items
|
||||
@@ -774,13 +809,24 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
return
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => {
|
||||
reader.onloadend = async () => {
|
||||
if (reader.error) {
|
||||
console.error("Error reading file:", reader.error)
|
||||
resolve(null)
|
||||
} else {
|
||||
const result = reader.result
|
||||
resolve(typeof result === "string" ? result : null)
|
||||
if (typeof result === "string") {
|
||||
try {
|
||||
await getImageDimensions(result)
|
||||
resolve(result)
|
||||
} catch (error) {
|
||||
console.warn((error as Error).message)
|
||||
showDimensionErrorMessage()
|
||||
resolve(null)
|
||||
}
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(blob)
|
||||
@@ -796,7 +842,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
}
|
||||
}
|
||||
},
|
||||
[shouldDisableImages, setSelectedImages, cursorPosition, setInputValue, inputValue],
|
||||
[shouldDisableImages, setSelectedImages, cursorPosition, setInputValue, inputValue, showDimensionErrorMessage],
|
||||
)
|
||||
|
||||
const handleThumbnailsHeightChange = useCallback((height: number) => {
|
||||
@@ -916,8 +962,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
useShortcut("Meta+Shift+a", onModeToggle, { disableTextInputs: false }) // important that we don't disable the text input here
|
||||
|
||||
const handleContextButtonClick = useCallback(() => {
|
||||
if (textAreaDisabled) return
|
||||
|
||||
// Focus the textarea first
|
||||
textAreaRef.current?.focus()
|
||||
|
||||
@@ -956,7 +1000,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
} as React.ChangeEvent<HTMLTextAreaElement>
|
||||
handleInputChange(event)
|
||||
updateHighlights()
|
||||
}, [inputValue, textAreaDisabled, handleInputChange, updateHighlights])
|
||||
}, [inputValue, handleInputChange, updateHighlights])
|
||||
|
||||
// Use an effect to detect menu close
|
||||
useEffect(() => {
|
||||
@@ -1228,13 +1272,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
(file) =>
|
||||
new Promise<string | null>((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => {
|
||||
reader.onloadend = async () => {
|
||||
// Make async
|
||||
if (reader.error) {
|
||||
console.error("Error reading file:", reader.error)
|
||||
resolve(null)
|
||||
} else {
|
||||
const result = reader.result
|
||||
resolve(typeof result === "string" ? result : null)
|
||||
if (typeof result === "string") {
|
||||
try {
|
||||
await getImageDimensions(result) // Check dimensions
|
||||
resolve(result)
|
||||
} catch (error) {
|
||||
console.warn((error as Error).message)
|
||||
showDimensionErrorMessage() // Show error to user
|
||||
resolve(null) // Don't add this image
|
||||
}
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
@@ -1248,7 +1304,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 15px",
|
||||
opacity: textAreaDisabled ? 0.5 : 1,
|
||||
opacity: 1,
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
// Drag-over styles moved to DynamicTextArea
|
||||
@@ -1258,6 +1314,31 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
onDragOver={onDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}>
|
||||
{showDimensionError && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: "10px 15px",
|
||||
backgroundColor: "rgba(var(--vscode-errorForeground-rgb), 0.1)",
|
||||
border: "2px solid var(--vscode-errorForeground)",
|
||||
borderRadius: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 10, // Ensure it's above other elements
|
||||
pointerEvents: "none",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-errorForeground)",
|
||||
fontWeight: "bold",
|
||||
fontSize: "12px",
|
||||
textAlign: "center",
|
||||
}}>
|
||||
Image dimensions exceed 7500px
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{showUnsupportedFileError && (
|
||||
<div
|
||||
style={{
|
||||
@@ -1358,7 +1439,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
textAreaRef.current = el
|
||||
}}
|
||||
value={inputValue}
|
||||
disabled={textAreaDisabled}
|
||||
onChange={(e) => {
|
||||
handleInputChange(e)
|
||||
updateHighlights()
|
||||
@@ -1379,7 +1459,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
}
|
||||
onHeightChange?.(height)
|
||||
}}
|
||||
placeholder={showUnsupportedFileError ? "" : placeholderText}
|
||||
placeholder={showUnsupportedFileError || showDimensionError ? "" : placeholderText}
|
||||
maxRows={10}
|
||||
autoFocus={true}
|
||||
style={{
|
||||
@@ -1408,7 +1488,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
// Instead of using boxShadow, we use a div with a border to better replicate the behavior when the textarea is focused
|
||||
// boxShadow: "0px 0px 0px 1px var(--vscode-input-border)",
|
||||
padding: "9px 28px 3px 9px",
|
||||
cursor: textAreaDisabled ? "not-allowed" : undefined,
|
||||
cursor: "text",
|
||||
flex: 1,
|
||||
zIndex: 1,
|
||||
outline:
|
||||
@@ -1466,9 +1546,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
/> */}
|
||||
<div
|
||||
data-testid="send-button"
|
||||
className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`}
|
||||
className={`input-icon-button ${sendingDisabled ? "disabled" : ""} codicon codicon-send`}
|
||||
onClick={() => {
|
||||
if (!textAreaDisabled) {
|
||||
if (!sendingDisabled) {
|
||||
setIsTextAreaFocused(false)
|
||||
onSend()
|
||||
}
|
||||
@@ -1504,7 +1584,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
data-testid="context-button"
|
||||
appearance="icon"
|
||||
aria-label="Add Context"
|
||||
disabled={textAreaDisabled}
|
||||
onClick={handleContextButtonClick}
|
||||
style={{ padding: "0px 0px", height: "20px" }}>
|
||||
<ButtonContainer>
|
||||
|
||||
@@ -41,6 +41,27 @@ interface ChatViewProps {
|
||||
showHistoryView: () => void
|
||||
}
|
||||
|
||||
// Function to clean up markdown escape characters
|
||||
function cleanupMarkdownEscapes(markdown: string): string {
|
||||
return (
|
||||
markdown
|
||||
// Handle underscores and asterisks (single or multiple)
|
||||
.replace(/\\([_*]+)/g, "$1")
|
||||
|
||||
// Handle angle brackets (for generics and XML)
|
||||
.replace(/\\([<>])/g, "$1")
|
||||
|
||||
// Handle backticks (for code)
|
||||
.replace(/\\(`)/g, "$1")
|
||||
|
||||
// Handle other common markdown special characters
|
||||
.replace(/\\([[\]()#.!])/g, "$1")
|
||||
|
||||
// Fix multiple consecutive backslashes
|
||||
.replace(/\\{2,}([_*`<>[\]()#.!])/g, "$1")
|
||||
)
|
||||
}
|
||||
|
||||
async function convertHtmlToMarkdown(html: string) {
|
||||
// Process the HTML to Markdown
|
||||
const result = await unified()
|
||||
@@ -55,10 +76,14 @@ async function convertHtmlToMarkdown(html: string) {
|
||||
rule: "-", // Use - for horizontal rules
|
||||
ruleSpaces: false, // No spaces in horizontal rules
|
||||
fences: true,
|
||||
escape: false,
|
||||
entities: false,
|
||||
})
|
||||
.process(html)
|
||||
|
||||
return String(result)
|
||||
const md = String(result)
|
||||
// Apply comprehensive cleanup of escape characters
|
||||
return cleanupMarkdownEscapes(md)
|
||||
}
|
||||
|
||||
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
||||
@@ -90,7 +115,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
const [activeQuote, setActiveQuote] = useState<string | null>(null)
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [textAreaDisabled, setTextAreaDisabled] = useState(false)
|
||||
const [sendingDisabled, setSendingDisabled] = useState(false)
|
||||
const [selectedImages, setSelectedImages] = useState<string[]>([])
|
||||
|
||||
// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
|
||||
@@ -120,7 +145,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
|
||||
// Convert HTML to Markdown
|
||||
const markdown = await convertHtmlToMarkdown(selectedHtml)
|
||||
|
||||
vscode.postMessage({ type: "copyToClipboard", text: markdown })
|
||||
e.preventDefault()
|
||||
}
|
||||
@@ -147,42 +171,42 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
const isPartial = lastMessage.partial === true
|
||||
switch (lastMessage.ask) {
|
||||
case "api_req_failed":
|
||||
setTextAreaDisabled(true)
|
||||
setSendingDisabled(true)
|
||||
setClineAsk("api_req_failed")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Retry")
|
||||
setSecondaryButtonText("Start New Task")
|
||||
break
|
||||
case "mistake_limit_reached":
|
||||
setTextAreaDisabled(false)
|
||||
setSendingDisabled(false)
|
||||
setClineAsk("mistake_limit_reached")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Proceed Anyways")
|
||||
setSecondaryButtonText("Start New Task")
|
||||
break
|
||||
case "auto_approval_max_req_reached":
|
||||
setTextAreaDisabled(true)
|
||||
setSendingDisabled(true)
|
||||
setClineAsk("auto_approval_max_req_reached")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Proceed")
|
||||
setSecondaryButtonText("Start New Task")
|
||||
break
|
||||
case "followup":
|
||||
setTextAreaDisabled(isPartial)
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("followup")
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "plan_mode_respond":
|
||||
setTextAreaDisabled(isPartial)
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("plan_mode_respond")
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "tool":
|
||||
setTextAreaDisabled(isPartial)
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("tool")
|
||||
setEnableButtons(!isPartial)
|
||||
const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
|
||||
@@ -199,28 +223,28 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
}
|
||||
break
|
||||
case "browser_action_launch":
|
||||
setTextAreaDisabled(isPartial)
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("browser_action_launch")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Approve")
|
||||
setSecondaryButtonText("Reject")
|
||||
break
|
||||
case "command":
|
||||
setTextAreaDisabled(isPartial)
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("command")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Run Command")
|
||||
setSecondaryButtonText("Reject")
|
||||
break
|
||||
case "command_output":
|
||||
setTextAreaDisabled(false)
|
||||
setSendingDisabled(false)
|
||||
setClineAsk("command_output")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Proceed While Running")
|
||||
setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "use_mcp_server":
|
||||
setTextAreaDisabled(isPartial)
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("use_mcp_server")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Approve")
|
||||
@@ -228,14 +252,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
break
|
||||
case "completion_result":
|
||||
// extension waiting for feedback. but we can just present a new task button
|
||||
setTextAreaDisabled(isPartial)
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("completion_result")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Start New Task")
|
||||
setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "resume_task":
|
||||
setTextAreaDisabled(false)
|
||||
setSendingDisabled(false)
|
||||
setClineAsk("resume_task")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Resume Task")
|
||||
@@ -243,7 +267,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
setDidClickCancel(false) // special case where we reset the cancel button state
|
||||
break
|
||||
case "resume_completed_task":
|
||||
setTextAreaDisabled(false)
|
||||
setSendingDisabled(false)
|
||||
setClineAsk("resume_completed_task")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Start New Task")
|
||||
@@ -251,14 +275,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
setDidClickCancel(false)
|
||||
break
|
||||
case "new_task":
|
||||
setTextAreaDisabled(isPartial)
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("new_task")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Start New Task with Context")
|
||||
setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "condense":
|
||||
setTextAreaDisabled(isPartial)
|
||||
setSendingDisabled(isPartial)
|
||||
setClineAsk("condense")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Condense Conversation")
|
||||
@@ -273,7 +297,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
if (secondLastMessage?.ask === "command_output") {
|
||||
// if the last ask is a command_output, and we receive an api_req_started, then that means the command has finished and we don't need input from the user anymore (in every other case, the user has to interact with input field or buttons to continue, which does the following automatically)
|
||||
setInputValue("")
|
||||
setTextAreaDisabled(true)
|
||||
setSendingDisabled(true)
|
||||
setSelectedImages([])
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
@@ -310,7 +334,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length === 0) {
|
||||
setTextAreaDisabled(false)
|
||||
setSendingDisabled(false)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
setPrimaryButtonText("Approve")
|
||||
@@ -397,7 +421,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
}
|
||||
setInputValue("")
|
||||
setActiveQuote(null) // Clear quote when sending message
|
||||
setTextAreaDisabled(true)
|
||||
setSendingDisabled(true)
|
||||
setSelectedImages([])
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
@@ -467,7 +491,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
})
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setSendingDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
@@ -516,7 +540,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
setSelectedImages([])
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setSendingDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
@@ -542,8 +566,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
vscode.postMessage({ type: "selectImages" })
|
||||
}, [])
|
||||
|
||||
const shouldDisableImages =
|
||||
!selectedModelInfo.supportsImages || textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
|
||||
const shouldDisableImages = !selectedModelInfo.supportsImages || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(e: MessageEvent) => {
|
||||
@@ -552,7 +575,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "action":
|
||||
switch (message.action!) {
|
||||
case "didBecomeVisible":
|
||||
if (!isHidden && !textAreaDisabled && !enableButtons) {
|
||||
if (!isHidden && !sendingDisabled && !enableButtons) {
|
||||
textAreaRef.current?.focus()
|
||||
}
|
||||
break
|
||||
@@ -601,7 +624,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
}
|
||||
// textAreaRef.current is not explicitly required here since react guarantees that ref will be stable across re-renders, and we're not using its value but its reference.
|
||||
},
|
||||
[isHidden, textAreaDisabled, enableButtons, handleSendMessage, handlePrimaryButtonClick, handleSecondaryButtonClick],
|
||||
[isHidden, sendingDisabled, enableButtons, handleSendMessage, handlePrimaryButtonClick, handleSecondaryButtonClick],
|
||||
)
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
@@ -613,14 +636,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (!isHidden && !textAreaDisabled && !enableButtons) {
|
||||
if (!isHidden && !sendingDisabled && !enableButtons) {
|
||||
textAreaRef.current?.focus()
|
||||
}
|
||||
}, 50)
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [isHidden, textAreaDisabled, enableButtons])
|
||||
}, [isHidden, sendingDisabled, enableButtons])
|
||||
|
||||
const visibleMessages = useMemo(() => {
|
||||
return modifiedMessages.filter((message) => {
|
||||
@@ -1096,7 +1119,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
activeQuote={activeQuote}
|
||||
inputValue={inputValue}
|
||||
setInputValue={setInputValue}
|
||||
textAreaDisabled={textAreaDisabled}
|
||||
sendingDisabled={sendingDisabled}
|
||||
placeholderText={placeholderText}
|
||||
selectedImages={selectedImages}
|
||||
setSelectedImages={setSelectedImages}
|
||||
|
||||
@@ -400,7 +400,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
</HeroTooltip>
|
||||
</div>
|
||||
{!shouldShowPromptCacheInfo() && (
|
||||
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
|
||||
<div className="flex items-center flex-wrap">
|
||||
<CopyButton taskText={task.text} />
|
||||
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{shouldShowPromptCacheInfo() && (
|
||||
@@ -452,7 +455,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
</HeroTooltip>
|
||||
)}
|
||||
</div>
|
||||
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
|
||||
<div className="flex items-center flex-wrap">
|
||||
<CopyButton taskText={task.text} />
|
||||
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
@@ -619,6 +625,36 @@ export const highlightText = (text?: string, withShadow = true) => {
|
||||
return [text]
|
||||
}
|
||||
|
||||
const CopyButton: React.FC<{
|
||||
taskText?: string
|
||||
}> = ({ taskText }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!taskText) return
|
||||
|
||||
navigator.clipboard.writeText(taskText).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<HeroTooltip content="Copy Task">
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={handleCopy}
|
||||
style={{ padding: "0px 0px" }}
|
||||
className="p-0"
|
||||
aria-label="Copy Task">
|
||||
<div className="flex items-center gap-[3px] text-[8px] font-bold opacity-60">
|
||||
<i className={`codicon codicon-${copied ? "check" : "copy"}`} />
|
||||
</div>
|
||||
</VSCodeButton>
|
||||
</HeroTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const DeleteButton: React.FC<{
|
||||
taskSize: string
|
||||
taskId?: string
|
||||
|
||||
@@ -92,6 +92,18 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
{formatDate(item.ts)}
|
||||
</span>
|
||||
</div>
|
||||
{item.isFavorited && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "12px",
|
||||
right: "12px",
|
||||
color: "var(--vscode-button-background)",
|
||||
}}>
|
||||
<span className="codicon codicon-star-full" aria-label="Favorited" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
|
||||
@@ -24,6 +24,76 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
|
||||
const [deleteAllDisabled, setDeleteAllDisabled] = useState(false)
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>([])
|
||||
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false)
|
||||
|
||||
// Keep track of pending favorite toggle operations
|
||||
const [pendingFavoriteToggles, setPendingFavoriteToggles] = useState<Record<string, boolean>>({})
|
||||
|
||||
// Load filtered task history with gRPC
|
||||
const [filteredTasks, setFilteredTasks] = useState<any[]>([])
|
||||
|
||||
// Load and refresh task history
|
||||
const loadTaskHistory = useCallback(async () => {
|
||||
try {
|
||||
const response = await TaskServiceClient.getTaskHistory({
|
||||
favoritesOnly: showFavoritesOnly,
|
||||
searchQuery: searchQuery || undefined,
|
||||
sortBy: sortOption,
|
||||
})
|
||||
setFilteredTasks(response.tasks || [])
|
||||
} catch (error) {
|
||||
console.error("Error loading task history:", error)
|
||||
// Fallback to client-side filtering
|
||||
setFilteredTasks(
|
||||
taskHistory.filter((item) => {
|
||||
const valid = item.ts && item.task
|
||||
return valid && (!showFavoritesOnly || item.isFavorited)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}, [showFavoritesOnly, searchQuery, sortOption, taskHistory])
|
||||
|
||||
// Load when filters change
|
||||
useEffect(() => {
|
||||
loadTaskHistory()
|
||||
}, [loadTaskHistory])
|
||||
|
||||
const toggleFavorite = useCallback(
|
||||
async (taskId: string, currentValue: boolean) => {
|
||||
// Optimistic UI update
|
||||
setPendingFavoriteToggles((prev) => ({ ...prev, [taskId]: !currentValue }))
|
||||
|
||||
try {
|
||||
await TaskServiceClient.toggleTaskFavorite({
|
||||
taskId,
|
||||
isFavorited: !currentValue,
|
||||
})
|
||||
|
||||
// Refresh if favorites filter is active
|
||||
if (showFavoritesOnly) {
|
||||
loadTaskHistory()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[FAVORITE_TOGGLE_UI] Error for task ${taskId}:`, err)
|
||||
// Revert optimistic update
|
||||
setPendingFavoriteToggles((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[taskId]
|
||||
return updated
|
||||
})
|
||||
} finally {
|
||||
// Clean up pending state after 1 second
|
||||
setTimeout(() => {
|
||||
setPendingFavoriteToggles((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[taskId]
|
||||
return updated
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
},
|
||||
[showFavoritesOnly, loadTaskHistory],
|
||||
)
|
||||
|
||||
const handleMessage = useCallback((event: MessageEvent<ExtensionMessage>) => {
|
||||
if (event.data.type === "relinquishControl") {
|
||||
@@ -87,9 +157,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
.toUpperCase()
|
||||
}, [])
|
||||
|
||||
const presentableTasks = useMemo(() => {
|
||||
return taskHistory.filter((item) => item.ts && item.task)
|
||||
}, [taskHistory])
|
||||
const presentableTasks = useMemo(() => filteredTasks, [filteredTasks])
|
||||
|
||||
const fuse = useMemo(() => {
|
||||
return new Fuse(presentableTasks, {
|
||||
@@ -252,7 +320,48 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
<VSCodeRadio value="mostRelevant" disabled={!searchQuery} style={{ opacity: searchQuery ? 1 : 0.5 }}>
|
||||
Most Relevant
|
||||
</VSCodeRadio>
|
||||
<div
|
||||
onClick={() => setShowFavoritesOnly(!showFavoritesOnly)}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginLeft: "6px",
|
||||
cursor: "pointer",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: "14px",
|
||||
height: "14px",
|
||||
borderRadius: "50%",
|
||||
border: "1px solid var(--vscode-checkbox-border)",
|
||||
backgroundColor: showFavoritesOnly ? "var(--vscode-checkbox-background)" : "transparent",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: "6px",
|
||||
}}>
|
||||
{showFavoritesOnly && (
|
||||
<div
|
||||
style={{
|
||||
width: "6px",
|
||||
height: "6px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--vscode-checkbox-foreground)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: "6px", userSelect: "none" }}>
|
||||
<div
|
||||
className="codicon codicon-star-full"
|
||||
style={{ color: "var(--vscode-button-background)", fontSize: "14px" }}
|
||||
/>
|
||||
Favorites
|
||||
</span>
|
||||
</div>
|
||||
</VSCodeRadioGroup>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "10px" }}>
|
||||
<VSCodeButton
|
||||
onClick={() => {
|
||||
@@ -337,43 +446,80 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
}}>
|
||||
{formatDate(item.ts)}
|
||||
</span>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteHistoryItem(item.id)
|
||||
}}
|
||||
className="delete-button"
|
||||
style={{ padding: "0px 0px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
fontSize: "11px",
|
||||
// fontWeight: "bold",
|
||||
}}>
|
||||
<span className="codicon codicon-trash"></span>
|
||||
{formatSize(item.size)}
|
||||
</div>
|
||||
</VSCodeButton>
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
{/* only show delete button if task not favorited */}
|
||||
{!(pendingFavoriteToggles[item.id] ?? item.isFavorited) && (
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteHistoryItem(item.id)
|
||||
}}
|
||||
className="delete-button"
|
||||
style={{ padding: "0px 0px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
fontSize: "11px",
|
||||
}}>
|
||||
<span className="codicon codicon-trash"></span>
|
||||
{formatSize(item.size)}
|
||||
</div>
|
||||
</VSCodeButton>
|
||||
)}
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleFavorite(item.id, item.isFavorited || false)
|
||||
}}
|
||||
style={{ padding: "0px" }}>
|
||||
<div
|
||||
className={`codicon ${
|
||||
pendingFavoriteToggles[item.id] !== undefined
|
||||
? pendingFavoriteToggles[item.id]
|
||||
? "codicon-star-full"
|
||||
: "codicon-star-empty"
|
||||
: item.isFavorited
|
||||
? "codicon-star-full"
|
||||
: "codicon-star-empty"
|
||||
}`}
|
||||
style={{
|
||||
color:
|
||||
(pendingFavoriteToggles[item.id] ?? item.isFavorited)
|
||||
? "var(--vscode-button-background)"
|
||||
: "inherit",
|
||||
opacity: (pendingFavoriteToggles[item.id] ?? item.isFavorited) ? 1 : 0.7,
|
||||
display:
|
||||
(pendingFavoriteToggles[item.id] ?? item.isFavorited)
|
||||
? "block"
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "8px", position: "relative" }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
color: "var(--vscode-foreground)",
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.task,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
color: "var(--vscode-foreground)",
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.task,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
||||
@@ -209,7 +209,15 @@ const ApiOptions = ({
|
||||
setLmStudioModels([])
|
||||
}
|
||||
} else if (selectedProvider === "vscode-lm") {
|
||||
vscode.postMessage({ type: "requestVsCodeLmModels" })
|
||||
try {
|
||||
const response = await ModelsServiceClient.getVsCodeLmModels({})
|
||||
if (response && response.models) {
|
||||
setVsCodeLmModels(response.models)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch VS Code LM models:", error)
|
||||
setVsCodeLmModels([])
|
||||
}
|
||||
}
|
||||
}, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl])
|
||||
useEffect(() => {
|
||||
@@ -222,14 +230,6 @@ const ApiOptions = ({
|
||||
selectedProvider === "ollama" || selectedProvider === "lmstudio" || selectedProvider === "vscode-lm" ? 2000 : null,
|
||||
)
|
||||
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
if (message.type === "vsCodeLmModels" && message.vsCodeLmModels) {
|
||||
setVsCodeLmModels(message.vsCodeLmModels)
|
||||
}
|
||||
}, [])
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
/*
|
||||
VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected.
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433
|
||||
@@ -822,10 +822,7 @@ const ApiOptions = ({
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Select "Custom" when using the Application Inference Profile in Bedrock. Enter the Application
|
||||
Inference Profile ID in the Model ID field. However, be sure to encode the / in the ARN as %2F.
|
||||
<br />
|
||||
Example: arn:aws:bedrock:us-west-2:<AWS Account
|
||||
ID>:application-inference-profile%2Fxxxxxxxxxxxx
|
||||
Inference Profile ARN in the Model ID field.
|
||||
</p>
|
||||
<label htmlFor="bedrock-model-input">
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useEvent } from "react-use"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import BrowserSettingsSection from "./BrowserSettingsSection"
|
||||
import TerminalSettingsSection from "./TerminalSettingsSection"
|
||||
import { useFeatureFlag } from "@/hooks/useFeatureFlag"
|
||||
import { FEATURE_FLAGS } from "@shared/services/feature-flags/feature-flags"
|
||||
const { IS_DEV } = process.env
|
||||
|
||||
@@ -146,8 +145,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
handleSubmit(true)
|
||||
}
|
||||
|
||||
const showCustomInstructions = useFeatureFlag(FEATURE_FLAGS.CUSTOM_INSTRUCTIONS)
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 left-0 right-0 bottom-0 pt-[10px] pr-0 pb-0 pl-5 flex flex-col overflow-hidden">
|
||||
<div className="flex justify-between items-center mb-[13px] pr-[17px]">
|
||||
@@ -186,24 +183,20 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCustomInstructions && (
|
||||
<div className="mb-[5px]">
|
||||
<VSCodeTextArea
|
||||
value={customInstructions ?? ""}
|
||||
className="w-full"
|
||||
resize="vertical"
|
||||
rows={4}
|
||||
placeholder={
|
||||
'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
|
||||
}
|
||||
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
|
||||
<span className="font-medium">Custom Instructions</span>
|
||||
</VSCodeTextArea>
|
||||
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
|
||||
These instructions are added to the end of the system prompt sent with every request.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-[5px]">
|
||||
<VSCodeTextArea
|
||||
value={customInstructions ?? ""}
|
||||
className="w-full"
|
||||
resize="vertical"
|
||||
rows={4}
|
||||
placeholder={'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'}
|
||||
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
|
||||
<span className="font-medium">Custom Instructions</span>
|
||||
</VSCodeTextArea>
|
||||
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
|
||||
These instructions are added to the end of the system prompt sent with every request.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-[5px]">
|
||||
<VSCodeCheckbox
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { useFeatureFlagPayload } from "posthog-js/react"
|
||||
|
||||
export const useFeatureFlag = (flagName: string): boolean => {
|
||||
const payload = useFeatureFlagPayload(flagName) as { enabled: boolean }
|
||||
if (payload && payload.enabled) {
|
||||
return payload.enabled
|
||||
const { telemetrySetting } = useExtensionState()
|
||||
|
||||
try {
|
||||
const payload = useFeatureFlagPayload(flagName) as { enabled: boolean }
|
||||
if (payload && typeof payload === "object") {
|
||||
// Check if the enabled property exists and is a boolean
|
||||
if ("enabled" in payload && typeof payload.enabled === "boolean") {
|
||||
return payload.enabled
|
||||
}
|
||||
}
|
||||
|
||||
if (telemetrySetting === "enabled") {
|
||||
console.warn(`Feature flag ${flagName} not found or missing enabled property.`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error retrieving feature flag "${flagName}":`, error)
|
||||
}
|
||||
console.warn(`Feature flag ${flagName} not found or missing enabled property.`)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ https://github.com/gitkraken/vscode-gitlens/blob/b1d71d4844523e8b2ef16f9e007068e
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
Reference in New Issue
Block a user