mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Merge remote-tracking branch 'upstream/main' into sbc_add_subtasks
This commit is contained in:
commit
c8ec2b6b70
22 changed files with 2823 additions and 988 deletions
2
.github/workflows/changeset-release.yml
vendored
2
.github/workflows/changeset-release.yml
vendored
|
|
@ -37,7 +37,7 @@ jobs:
|
|||
cache: 'npm'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm run install:all
|
||||
run: npm run install:ci
|
||||
|
||||
# Check if there are any new changesets to process
|
||||
- name: Check for changesets
|
||||
|
|
|
|||
18
.github/workflows/code-qa.yml
vendored
18
.github/workflows/code-qa.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
node-version: '18'
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: npm run install:all
|
||||
run: npm run install:ci
|
||||
- name: Compile
|
||||
run: npm run compile
|
||||
- name: Check types
|
||||
|
|
@ -39,7 +39,7 @@ jobs:
|
|||
node-version: '18'
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: npm run install:all
|
||||
run: npm run install:ci
|
||||
- name: Run knip checks
|
||||
run: npm run knip
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ jobs:
|
|||
node-version: '18'
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: npm run install:all
|
||||
run: npm run install:ci
|
||||
- name: Run unit tests
|
||||
run: npx jest --silent
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ jobs:
|
|||
node-version: '18'
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: npm run install:all
|
||||
run: npm run install:ci
|
||||
- name: Run unit tests
|
||||
working-directory: webview-ui
|
||||
run: npx jest --silent
|
||||
|
|
@ -108,9 +108,11 @@ jobs:
|
|||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
- name: Create env.integration file
|
||||
run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.integration
|
||||
- name: Install dependencies
|
||||
run: npm run install:all
|
||||
run: npm run install:ci
|
||||
- name: Create env.integration file
|
||||
working-directory: e2e
|
||||
run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.integration
|
||||
- name: Run integration tests
|
||||
run: xvfb-run -a npm run test:integration
|
||||
working-directory: e2e
|
||||
run: xvfb-run -a npm run ci
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
.vscode/**
|
||||
.vscode-test/**
|
||||
out/**
|
||||
out-integration/**
|
||||
e2e/**
|
||||
node_modules/**
|
||||
src/**
|
||||
.gitignore
|
||||
|
|
@ -25,7 +27,6 @@ demo.gif
|
|||
.roomodes
|
||||
cline_docs/**
|
||||
coverage/**
|
||||
out-integration/**
|
||||
|
||||
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
|
||||
webview-ui/src/**
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { defineConfig } from '@vscode/test-cli';
|
|||
|
||||
export default defineConfig({
|
||||
label: 'integrationTest',
|
||||
files: 'out-integration/test/**/*.test.js',
|
||||
files: 'out/suite/**/*.test.js',
|
||||
workspaceFolder: '.',
|
||||
mocha: {
|
||||
ui: 'tdd',
|
||||
|
|
@ -11,8 +11,8 @@ The integration tests use the `@vscode/test-electron` package to run tests in a
|
|||
### Directory Structure
|
||||
|
||||
```
|
||||
src/test/
|
||||
├── runTest.ts # Main test runner
|
||||
e2e/src/
|
||||
├── runTest.ts # Main test runner
|
||||
├── suite/
|
||||
│ ├── index.ts # Test suite configuration
|
||||
│ ├── modes.test.ts # Mode switching tests
|
||||
2387
e2e/package-lock.json
generated
Normal file
2387
e2e/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
21
e2e/package.json
Normal file
21
e2e/package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "e2e",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cd .. && npm run build",
|
||||
"compile": "tsc -p tsconfig.json",
|
||||
"lint": "eslint src --ext ts",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "npm run compile && npx dotenvx run -f .env.integration -- node ./out/runTest.js",
|
||||
"ci": "npm run build && npm run test"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"mocha": "^11.1.0",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import * as path from "path"
|
||||
import Mocha from "mocha"
|
||||
import { glob } from "glob"
|
||||
import { ClineAPI } from "../../exports/cline"
|
||||
import { ClineProvider } from "../../core/webview/ClineProvider"
|
||||
import { ClineAPI, ClineProvider } from "../../../src/exports/cline"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
declare global {
|
||||
|
|
@ -9,9 +9,8 @@
|
|||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"rootDir": "src",
|
||||
"outDir": "out-integration"
|
||||
"outDir": "out"
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": [".vscode-test", "benchmark", "dist", "**/node_modules/**", "out", "out-integration", "webview-ui"]
|
||||
"include": ["src", "../src/exports/cline.d.ts"],
|
||||
"exclude": [".vscode-test", "**/node_modules/**", "out"]
|
||||
}
|
||||
|
|
@ -16,7 +16,9 @@
|
|||
"src/activate/**",
|
||||
"src/exports/**",
|
||||
"src/extension.ts",
|
||||
".vscode-test.mjs"
|
||||
"e2e/.vscode-test.mjs",
|
||||
"e2e/src/runTest.ts",
|
||||
"e2e/src/suite/index.ts"
|
||||
],
|
||||
"workspaces": {
|
||||
"webview-ui": {
|
||||
|
|
|
|||
931
package-lock.json
generated
931
package-lock.json
generated
File diff suppressed because it is too large
Load diff
34
package.json
34
package.json
|
|
@ -276,21 +276,24 @@
|
|||
"scripts": {
|
||||
"build": "npm run build:webview && npm run vsix",
|
||||
"build:webview": "cd webview-ui && npm run build",
|
||||
"changeset": "changeset",
|
||||
"check-types": "tsc --noEmit && cd webview-ui && npm run check-types",
|
||||
"compile": "tsc -p . --outDir out && node esbuild.js",
|
||||
"compile:integration": "tsc -p tsconfig.integration.json",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"knip": "knip --include files",
|
||||
"lint": "eslint src --ext ts && npm run lint --prefix webview-ui",
|
||||
"lint-local": "eslint -c .eslintrc.local.json src --ext ts && npm run lint --prefix webview-ui",
|
||||
"lint-fix": "eslint src --ext ts --fix && npm run lint-fix --prefix webview-ui",
|
||||
"lint-fix-local": "eslint -c .eslintrc.local.json src --ext ts --fix && npm run lint-fix --prefix webview-ui",
|
||||
"install:all": "npm-run-all -p install-*",
|
||||
"install:ci": "npm install npm-run-all && npm run install:all",
|
||||
"install-extension": "npm install",
|
||||
"install-webview-ui": "cd webview-ui && npm install",
|
||||
"install-e2e": "cd e2e && npm install",
|
||||
"lint": "npm-run-all -p lint:*",
|
||||
"lint:extension": "eslint src --ext ts",
|
||||
"lint:webview-ui": "cd webview-ui && npm run lint",
|
||||
"lint:e2e": "cd e2e && npm run lint",
|
||||
"check-types": "npm-run-all -p check-types:*",
|
||||
"check-types:extension": "tsc --noEmit",
|
||||
"check-types:webview-ui": "cd webview-ui && npm run check-types",
|
||||
"check-types:e2e": "cd e2e && npm run check-types",
|
||||
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
||||
"pretest": "npm run compile && npm run compile:integration",
|
||||
"pretest": "npm run compile",
|
||||
"dev": "cd webview-ui && npm run dev",
|
||||
"test": "jest && cd webview-ui && npm run test",
|
||||
"test:integration": "npm run build && npm run compile:integration && npx dotenvx run -f .env.integration -- node ./out-integration/test/runTest.js",
|
||||
"prepare": "husky",
|
||||
"publish:marketplace": "vsce publish && ovsx publish",
|
||||
"publish": "npm run build && changeset publish && npm install --package-lock-only",
|
||||
|
|
@ -300,7 +303,9 @@
|
|||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"watch-tests": "tsc -p . -w --outDir out"
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"changeset": "changeset",
|
||||
"knip": "knip --include files"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||
|
|
@ -343,6 +348,7 @@
|
|||
"sound-play": "^1.1.0",
|
||||
"string-similarity": "^4.0.4",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"js-tiktoken": "^1.0.19",
|
||||
"tmp": "^0.2.3",
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
|
|
@ -358,13 +364,10 @@
|
|||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "20.x",
|
||||
"@types/string-similarity": "^4.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"esbuild": "^0.24.0",
|
||||
"eslint": "^8.57.0",
|
||||
"glob": "^11.0.1",
|
||||
|
|
@ -374,7 +377,6 @@
|
|||
"knip": "^5.44.4",
|
||||
"lint-staged": "^15.2.11",
|
||||
"mkdirp": "^3.0.1",
|
||||
"mocha": "^11.1.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.4.2",
|
||||
"rimraf": "^6.0.1",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
import { ModelInfo } from "../../../shared/api"
|
||||
import { truncateConversation, truncateConversationIfNeeded } from "../index"
|
||||
import { estimateTokenCount, truncateConversation, truncateConversationIfNeeded } from "../index"
|
||||
|
||||
/**
|
||||
* Tests for the truncateConversation function
|
||||
|
|
@ -118,23 +118,26 @@ describe("getMaxTokens", () => {
|
|||
const modelInfo = createModelInfo(100000, 50000)
|
||||
// Max tokens = 100000 - 50000 = 50000
|
||||
|
||||
// Create messages with very small content in the last one to avoid token overflow
|
||||
const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
|
||||
|
||||
// Below max tokens - no truncation
|
||||
const result1 = truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 49999,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
})
|
||||
expect(result1).toEqual(messages)
|
||||
expect(result1).toEqual(messagesWithSmallContent)
|
||||
|
||||
// Above max tokens - truncate
|
||||
const result2 = truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 50001,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
})
|
||||
expect(result2).not.toEqual(messages)
|
||||
expect(result2).not.toEqual(messagesWithSmallContent)
|
||||
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
|
||||
})
|
||||
|
||||
|
|
@ -142,23 +145,26 @@ describe("getMaxTokens", () => {
|
|||
const modelInfo = createModelInfo(100000, undefined)
|
||||
// Max tokens = 100000 - (100000 * 0.2) = 80000
|
||||
|
||||
// Create messages with very small content in the last one to avoid token overflow
|
||||
const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
|
||||
|
||||
// Below max tokens - no truncation
|
||||
const result1 = truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 79999,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
})
|
||||
expect(result1).toEqual(messages)
|
||||
expect(result1).toEqual(messagesWithSmallContent)
|
||||
|
||||
// Above max tokens - truncate
|
||||
const result2 = truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 80001,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
})
|
||||
expect(result2).not.toEqual(messages)
|
||||
expect(result2).not.toEqual(messagesWithSmallContent)
|
||||
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
|
||||
})
|
||||
|
||||
|
|
@ -166,23 +172,26 @@ describe("getMaxTokens", () => {
|
|||
const modelInfo = createModelInfo(50000, 10000)
|
||||
// Max tokens = 50000 - 10000 = 40000
|
||||
|
||||
// Create messages with very small content in the last one to avoid token overflow
|
||||
const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
|
||||
|
||||
// Below max tokens - no truncation
|
||||
const result1 = truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 39999,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
})
|
||||
expect(result1).toEqual(messages)
|
||||
expect(result1).toEqual(messagesWithSmallContent)
|
||||
|
||||
// Above max tokens - truncate
|
||||
const result2 = truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 40001,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
})
|
||||
expect(result2).not.toEqual(messages)
|
||||
expect(result2).not.toEqual(messagesWithSmallContent)
|
||||
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
|
||||
})
|
||||
|
||||
|
|
@ -190,23 +199,26 @@ describe("getMaxTokens", () => {
|
|||
const modelInfo = createModelInfo(200000, 30000)
|
||||
// Max tokens = 200000 - 30000 = 170000
|
||||
|
||||
// Create messages with very small content in the last one to avoid token overflow
|
||||
const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
|
||||
|
||||
// Below max tokens - no truncation
|
||||
const result1 = truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 169999,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
})
|
||||
expect(result1).toEqual(messages)
|
||||
expect(result1).toEqual(messagesWithSmallContent)
|
||||
|
||||
// Above max tokens - truncate
|
||||
const result2 = truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 170001,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
})
|
||||
expect(result2).not.toEqual(messages)
|
||||
expect(result2).not.toEqual(messagesWithSmallContent)
|
||||
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
|
||||
})
|
||||
})
|
||||
|
|
@ -234,13 +246,16 @@ describe("truncateConversationIfNeeded", () => {
|
|||
const maxTokens = 100000 - 30000 // 70000
|
||||
const totalTokens = 69999 // Below threshold
|
||||
|
||||
// Create messages with very small content in the last one to avoid token overflow
|
||||
const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
|
||||
|
||||
const result = truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
})
|
||||
expect(result).toEqual(messages) // No truncation occurs
|
||||
expect(result).toEqual(messagesWithSmallContent) // No truncation occurs
|
||||
})
|
||||
|
||||
it("should truncate if tokens are above max tokens threshold", () => {
|
||||
|
|
@ -248,12 +263,15 @@ describe("truncateConversationIfNeeded", () => {
|
|||
const maxTokens = 100000 - 30000 // 70000
|
||||
const totalTokens = 70001 // Above threshold
|
||||
|
||||
// Create messages with very small content in the last one to avoid token overflow
|
||||
const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
|
||||
|
||||
// When truncating, always uses 0.5 fraction
|
||||
// With 4 messages after the first, 0.5 fraction means remove 2 messages
|
||||
const expectedResult = [messages[0], messages[3], messages[4]]
|
||||
const expectedResult = [messagesWithSmallContent[0], messagesWithSmallContent[3], messagesWithSmallContent[4]]
|
||||
|
||||
const result = truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
|
|
@ -266,18 +284,21 @@ describe("truncateConversationIfNeeded", () => {
|
|||
const modelInfo1 = createModelInfo(100000, true, 30000)
|
||||
const modelInfo2 = createModelInfo(100000, false, 30000)
|
||||
|
||||
// Create messages with very small content in the last one to avoid token overflow
|
||||
const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
|
||||
|
||||
// Test below threshold
|
||||
const belowThreshold = 69999
|
||||
expect(
|
||||
truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: belowThreshold,
|
||||
contextWindow: modelInfo1.contextWindow,
|
||||
maxTokens: modelInfo1.maxTokens,
|
||||
}),
|
||||
).toEqual(
|
||||
truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: belowThreshold,
|
||||
contextWindow: modelInfo2.contextWindow,
|
||||
maxTokens: modelInfo2.maxTokens,
|
||||
|
|
@ -288,18 +309,171 @@ describe("truncateConversationIfNeeded", () => {
|
|||
const aboveThreshold = 70001
|
||||
expect(
|
||||
truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: aboveThreshold,
|
||||
contextWindow: modelInfo1.contextWindow,
|
||||
maxTokens: modelInfo1.maxTokens,
|
||||
}),
|
||||
).toEqual(
|
||||
truncateConversationIfNeeded({
|
||||
messages,
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: aboveThreshold,
|
||||
contextWindow: modelInfo2.contextWindow,
|
||||
maxTokens: modelInfo2.maxTokens,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should consider incoming content when deciding to truncate", () => {
|
||||
const modelInfo = createModelInfo(100000, true, 30000)
|
||||
const maxTokens = 30000
|
||||
const availableTokens = modelInfo.contextWindow - maxTokens
|
||||
|
||||
// Test case 1: Small content that won't push us over the threshold
|
||||
const smallContent = [{ type: "text" as const, text: "Small content" }]
|
||||
const smallContentTokens = estimateTokenCount(smallContent)
|
||||
const messagesWithSmallContent: Anthropic.Messages.MessageParam[] = [
|
||||
...messages.slice(0, -1),
|
||||
{ role: messages[messages.length - 1].role, content: smallContent },
|
||||
]
|
||||
|
||||
// Set base tokens so total is below threshold even with small content added
|
||||
const baseTokensForSmall = availableTokens - smallContentTokens - 10
|
||||
const resultWithSmall = truncateConversationIfNeeded({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: baseTokensForSmall,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens,
|
||||
})
|
||||
expect(resultWithSmall).toEqual(messagesWithSmallContent) // No truncation
|
||||
|
||||
// Test case 2: Large content that will push us over the threshold
|
||||
const largeContent = [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: "A very large incoming message that would consume a significant number of tokens and push us over the threshold",
|
||||
},
|
||||
]
|
||||
const largeContentTokens = estimateTokenCount(largeContent)
|
||||
const messagesWithLargeContent: Anthropic.Messages.MessageParam[] = [
|
||||
...messages.slice(0, -1),
|
||||
{ role: messages[messages.length - 1].role, content: largeContent },
|
||||
]
|
||||
|
||||
// Set base tokens so we're just below threshold without content, but over with content
|
||||
const baseTokensForLarge = availableTokens - Math.floor(largeContentTokens / 2)
|
||||
const resultWithLarge = truncateConversationIfNeeded({
|
||||
messages: messagesWithLargeContent,
|
||||
totalTokens: baseTokensForLarge,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens,
|
||||
})
|
||||
expect(resultWithLarge).not.toEqual(messagesWithLargeContent) // Should truncate
|
||||
|
||||
// Test case 3: Very large content that will definitely exceed threshold
|
||||
const veryLargeContent = [{ type: "text" as const, text: "X".repeat(1000) }]
|
||||
const veryLargeContentTokens = estimateTokenCount(veryLargeContent)
|
||||
const messagesWithVeryLargeContent: Anthropic.Messages.MessageParam[] = [
|
||||
...messages.slice(0, -1),
|
||||
{ role: messages[messages.length - 1].role, content: veryLargeContent },
|
||||
]
|
||||
|
||||
// Set base tokens so we're just below threshold without content
|
||||
const baseTokensForVeryLarge = availableTokens - Math.floor(veryLargeContentTokens / 2)
|
||||
const resultWithVeryLarge = truncateConversationIfNeeded({
|
||||
messages: messagesWithVeryLargeContent,
|
||||
totalTokens: baseTokensForVeryLarge,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens,
|
||||
})
|
||||
expect(resultWithVeryLarge).not.toEqual(messagesWithVeryLargeContent) // Should truncate
|
||||
})
|
||||
})
|
||||
/**
|
||||
* Tests for the estimateTokenCount function
|
||||
*/
|
||||
describe("estimateTokenCount", () => {
|
||||
it("should return 0 for empty or undefined content", () => {
|
||||
expect(estimateTokenCount([])).toBe(0)
|
||||
// @ts-ignore - Testing with undefined
|
||||
expect(estimateTokenCount(undefined)).toBe(0)
|
||||
})
|
||||
|
||||
it("should estimate tokens for text blocks", () => {
|
||||
const content: Array<Anthropic.Messages.ContentBlockParam> = [
|
||||
{ type: "text", text: "This is a text block with 36 characters" },
|
||||
]
|
||||
|
||||
// With tiktoken, the exact token count may differ from character-based estimation
|
||||
// Instead of expecting an exact number, we verify it's a reasonable positive number
|
||||
const result = estimateTokenCount(content)
|
||||
expect(result).toBeGreaterThan(0)
|
||||
|
||||
// We can also verify that longer text results in more tokens
|
||||
const longerContent: Array<Anthropic.Messages.ContentBlockParam> = [
|
||||
{
|
||||
type: "text",
|
||||
text: "This is a longer text block with significantly more characters to encode into tokens",
|
||||
},
|
||||
]
|
||||
const longerResult = estimateTokenCount(longerContent)
|
||||
expect(longerResult).toBeGreaterThan(result)
|
||||
})
|
||||
|
||||
it("should estimate tokens for image blocks based on data size", () => {
|
||||
// Small image
|
||||
const smallImage: Array<Anthropic.Messages.ContentBlockParam> = [
|
||||
{ type: "image", source: { type: "base64", media_type: "image/jpeg", data: "small_dummy_data" } },
|
||||
]
|
||||
// Larger image with more data
|
||||
const largerImage: Array<Anthropic.Messages.ContentBlockParam> = [
|
||||
{ type: "image", source: { type: "base64", media_type: "image/png", data: "X".repeat(1000) } },
|
||||
]
|
||||
|
||||
// Verify the token count scales with the size of the image data
|
||||
const smallImageTokens = estimateTokenCount(smallImage)
|
||||
const largerImageTokens = estimateTokenCount(largerImage)
|
||||
|
||||
// Small image should have some tokens
|
||||
expect(smallImageTokens).toBeGreaterThan(0)
|
||||
|
||||
// Larger image should have proportionally more tokens
|
||||
expect(largerImageTokens).toBeGreaterThan(smallImageTokens)
|
||||
|
||||
// Verify the larger image calculation matches our formula including the 50% fudge factor
|
||||
expect(largerImageTokens).toBe(48)
|
||||
})
|
||||
|
||||
it("should estimate tokens for mixed content blocks", () => {
|
||||
const content: Array<Anthropic.Messages.ContentBlockParam> = [
|
||||
{ type: "text", text: "A text block with 30 characters" },
|
||||
{ type: "image", source: { type: "base64", media_type: "image/jpeg", data: "dummy_data" } },
|
||||
{ type: "text", text: "Another text with 24 chars" },
|
||||
]
|
||||
|
||||
// We know image tokens calculation should be consistent
|
||||
const imageTokens = Math.ceil(Math.sqrt("dummy_data".length)) * 1.5
|
||||
|
||||
// With tiktoken, we can't predict exact text token counts,
|
||||
// but we can verify the total is greater than just the image tokens
|
||||
const result = estimateTokenCount(content)
|
||||
expect(result).toBeGreaterThan(imageTokens)
|
||||
|
||||
// Also test against a version with only the image to verify text adds tokens
|
||||
const imageOnlyContent: Array<Anthropic.Messages.ContentBlockParam> = [
|
||||
{ type: "image", source: { type: "base64", media_type: "image/jpeg", data: "dummy_data" } },
|
||||
]
|
||||
const imageOnlyResult = estimateTokenCount(imageOnlyContent)
|
||||
expect(result).toBeGreaterThan(imageOnlyResult)
|
||||
})
|
||||
|
||||
it("should handle empty text blocks", () => {
|
||||
const content: Array<Anthropic.Messages.ContentBlockParam> = [{ type: "text", text: "" }]
|
||||
expect(estimateTokenCount(content)).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle plain string messages", () => {
|
||||
const content = "This is a plain text message"
|
||||
expect(estimateTokenCount([{ type: "text", text: content }])).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,51 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
import { Tiktoken } from "js-tiktoken/lite"
|
||||
import o200kBase from "js-tiktoken/ranks/o200k_base"
|
||||
|
||||
const TOKEN_FUDGE_FACTOR = 1.5
|
||||
|
||||
/**
|
||||
* Counts tokens for user content using tiktoken for text
|
||||
* and a size-based calculation for images.
|
||||
*
|
||||
* @param {Array<Anthropic.Messages.ContentBlockParam>} content - The content to count tokens for
|
||||
* @returns {number} The token count
|
||||
*/
|
||||
export function estimateTokenCount(content: Array<Anthropic.Messages.ContentBlockParam>): number {
|
||||
if (!content || content.length === 0) return 0
|
||||
|
||||
let totalTokens = 0
|
||||
let encoder = null
|
||||
|
||||
// Create encoder
|
||||
encoder = new Tiktoken(o200kBase)
|
||||
|
||||
// Process each content block
|
||||
for (const block of content) {
|
||||
if (block.type === "text") {
|
||||
// Use tiktoken for text token counting
|
||||
const text = block.text || ""
|
||||
if (text.length > 0) {
|
||||
const tokens = encoder.encode(text)
|
||||
totalTokens += tokens.length
|
||||
}
|
||||
} else if (block.type === "image") {
|
||||
// For images, calculate based on data size
|
||||
const imageSource = block.source
|
||||
if (imageSource && typeof imageSource === "object" && "data" in imageSource) {
|
||||
const base64Data = imageSource.data as string
|
||||
totalTokens += Math.ceil(Math.sqrt(base64Data.length))
|
||||
} else {
|
||||
totalTokens += 300 // Conservative estimate for unknown images
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a fudge factor to account for the fact that tiktoken is not always accurate
|
||||
return Math.ceil(totalTokens * TOKEN_FUDGE_FACTOR)
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates a conversation by removing a fraction of the messages.
|
||||
*
|
||||
|
|
@ -25,10 +71,10 @@ export function truncateConversation(
|
|||
|
||||
/**
|
||||
* Conditionally truncates the conversation messages if the total token count
|
||||
* exceeds the model's limit.
|
||||
* exceeds the model's limit, considering the size of incoming content.
|
||||
*
|
||||
* @param {Anthropic.Messages.MessageParam[]} messages - The conversation messages.
|
||||
* @param {number} totalTokens - The total number of tokens in the conversation.
|
||||
* @param {number} totalTokens - The total number of tokens in the conversation (excluding the last user message).
|
||||
* @param {number} contextWindow - The context window size.
|
||||
* @param {number} maxTokens - The maximum number of tokens allowed.
|
||||
* @returns {Anthropic.Messages.MessageParam[]} The original or truncated conversation messages.
|
||||
|
|
@ -47,6 +93,22 @@ export function truncateConversationIfNeeded({
|
|||
contextWindow,
|
||||
maxTokens,
|
||||
}: TruncateOptions): Anthropic.Messages.MessageParam[] {
|
||||
const allowedTokens = contextWindow - (maxTokens || contextWindow * 0.2)
|
||||
return totalTokens < allowedTokens ? messages : truncateConversation(messages, 0.5)
|
||||
// Calculate the maximum tokens reserved for response
|
||||
const reservedTokens = maxTokens || contextWindow * 0.2
|
||||
|
||||
// Estimate tokens for the last message (which is always a user message)
|
||||
const lastMessage = messages[messages.length - 1]
|
||||
const lastMessageContent = lastMessage.content
|
||||
const lastMessageTokens = Array.isArray(lastMessageContent)
|
||||
? estimateTokenCount(lastMessageContent)
|
||||
: estimateTokenCount([{ type: "text", text: lastMessageContent as string }])
|
||||
|
||||
// Calculate total effective tokens (totalTokens never includes the last message)
|
||||
const effectiveTokens = totalTokens + lastMessageTokens
|
||||
|
||||
// Calculate available tokens for conversation history
|
||||
const allowedTokens = contextWindow - reservedTokens
|
||||
|
||||
// Determine if truncation is needed and apply if necessary
|
||||
return effectiveTokens < allowedTokens ? messages : truncateConversation(messages, 0.5)
|
||||
}
|
||||
|
|
|
|||
93
src/exports/cline.d.ts
vendored
93
src/exports/cline.d.ts
vendored
|
|
@ -40,3 +40,96 @@ export interface ClineAPI {
|
|||
*/
|
||||
sidebarProvider: ClineSidebarProvider
|
||||
}
|
||||
|
||||
export interface ClineProvider {
|
||||
readonly context: vscode.ExtensionContext
|
||||
readonly viewLaunched: boolean
|
||||
readonly messages: ClineMessage[]
|
||||
|
||||
/**
|
||||
* Resolves the webview view for the provider
|
||||
* @param webviewView The webview view or panel to resolve
|
||||
*/
|
||||
resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel): Promise<void>
|
||||
|
||||
/**
|
||||
* Initializes Cline with a task
|
||||
*/
|
||||
initClineWithTask(task?: string, images?: string[]): Promise<void>
|
||||
|
||||
/**
|
||||
* Initializes Cline with a history item
|
||||
*/
|
||||
initClineWithHistoryItem(historyItem: HistoryItem): Promise<void>
|
||||
|
||||
/**
|
||||
* Posts a message to the webview
|
||||
*/
|
||||
postMessageToWebview(message: ExtensionMessage): Promise<void>
|
||||
|
||||
/**
|
||||
* Handles mode switching
|
||||
*/
|
||||
handleModeSwitch(newMode: Mode): Promise<void>
|
||||
|
||||
/**
|
||||
* Updates custom instructions
|
||||
*/
|
||||
updateCustomInstructions(instructions?: string): Promise<void>
|
||||
|
||||
/**
|
||||
* Cancels the current task
|
||||
*/
|
||||
cancelTask(): Promise<void>
|
||||
|
||||
/**
|
||||
* Clears the current task
|
||||
*/
|
||||
clearTask(): Promise<void>
|
||||
|
||||
/**
|
||||
* Gets the current state
|
||||
*/
|
||||
getState(): Promise<any>
|
||||
|
||||
/**
|
||||
* Updates a value in the global state
|
||||
* @param key The key to update
|
||||
* @param value The value to set
|
||||
*/
|
||||
updateGlobalState(key: GlobalStateKey, value: any): Promise<void>
|
||||
|
||||
/**
|
||||
* Gets a value from the global state
|
||||
* @param key The key to get
|
||||
*/
|
||||
getGlobalState(key: GlobalStateKey): Promise<any>
|
||||
|
||||
/**
|
||||
* Stores a secret value in secure storage
|
||||
* @param key The key to store the secret under
|
||||
* @param value The secret value to store, or undefined to remove the secret
|
||||
*/
|
||||
storeSecret(key: SecretKey, value?: string): Promise<void>
|
||||
|
||||
/**
|
||||
* Retrieves a secret value from secure storage
|
||||
* @param key The key of the secret to retrieve
|
||||
*/
|
||||
getSecret(key: SecretKey): Promise<string | undefined>
|
||||
|
||||
/**
|
||||
* Resets the state
|
||||
*/
|
||||
resetState(): Promise<void>
|
||||
|
||||
/**
|
||||
* Logs a message
|
||||
*/
|
||||
log(message: string): void
|
||||
|
||||
/**
|
||||
* Disposes of the provider
|
||||
*/
|
||||
dispose(): Promise<void>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,11 @@ export type CustomModePrompts = {
|
|||
|
||||
// Helper to extract group name regardless of format
|
||||
export function getGroupName(group: GroupEntry): ToolGroup {
|
||||
return Array.isArray(group) ? group[0] : group
|
||||
if (typeof group === "string") {
|
||||
return group
|
||||
}
|
||||
|
||||
return group[0]
|
||||
}
|
||||
|
||||
// Helper to get group options if they exist
|
||||
|
|
|
|||
|
|
@ -880,7 +880,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
const placeholderText = useMemo(() => {
|
||||
const baseText = task ? "Type a message..." : "Type your task here..."
|
||||
const contextText = "(@ to add context, / to switch modes"
|
||||
const imageText = shouldDisableImages ? "hold shift to drag in files" : ", hold shift to drag in files/images"
|
||||
const imageText = shouldDisableImages ? ", hold shift to drag in files" : ", hold shift to drag in files/images"
|
||||
return baseText + `\n${contextText}${imageText})`
|
||||
}, [task, shouldDisableImages])
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue