mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Continuing work on support for OpenRouter compression (#43)
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
# Roo Cline Changelog
|
# Roo Cline Changelog
|
||||||
|
|
||||||
|
## [2.1.11]
|
||||||
|
|
||||||
|
- Incorporate lloydchang's [PR](https://github.com/RooVetGit/Roo-Cline/pull/42) to add support for OpenRouter compression
|
||||||
|
|
||||||
## [2.1.10]
|
## [2.1.10]
|
||||||
|
|
||||||
- Incorporate HeavenOSK's [PR](https://github.com/cline/cline/pull/818) to add sound effects to Cline
|
- Incorporate HeavenOSK's [PR](https://github.com/cline/cline/pull/818) to add sound effects to Cline
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ After installation, Roo Cline will appear in your VSCode-compatible editor's ins
|
|||||||
<a href="https://discord.gg/cline" target="_blank"><strong>Join the Discord</strong></a>
|
<a href="https://discord.gg/cline" target="_blank"><strong>Join the Discord</strong></a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/cline/cline/wiki" target="_blank"><strong>Docs</strong></a>
|
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
|
<a href="https://cline.bot/join-us" target="_blank"><strong>We're Hiring!</strong></a>
|
||||||
</td>
|
</td>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -112,7 +112,7 @@ Try asking Cline to "test the app", and watch as he runs a command like `npm run
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
To contribute to the project, start by exploring [open issues](https://github.com/cline/cline/issues) or checking our [feature request board](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop). We'd also love to have you join our [Discord](https://discord.gg/cline) to share ideas and connect with other contributors.
|
To contribute to the project, start by exploring [open issues](https://github.com/cline/cline/issues) or checking our [feature request board](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop). We'd also love to have you join our [Discord](https://discord.gg/cline) to share ideas and connect with other contributors. If you're interested in joining the team, check out our [careers page](https://cline.bot/join-us)!
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Local Development Instructions</summary>
|
<summary>Local Development Instructions</summary>
|
||||||
|
|||||||
Binary file not shown.
1960
package-lock.json
generated
1960
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
|||||||
"displayName": "Roo Cline",
|
"displayName": "Roo Cline",
|
||||||
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
|
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
|
||||||
"publisher": "RooVeterinaryInc",
|
"publisher": "RooVeterinaryInc",
|
||||||
"version": "2.1.10",
|
"version": "2.1.11",
|
||||||
"icon": "assets/icons/rocket.png",
|
"icon": "assets/icons/rocket.png",
|
||||||
"galleryBanner": {
|
"galleryBanner": {
|
||||||
"color": "#617A91",
|
"color": "#617A91",
|
||||||
@@ -136,7 +136,6 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"vscode:prepublish": "npm run package",
|
"vscode:prepublish": "npm run package",
|
||||||
"vsix": "vsce package --out bin",
|
|
||||||
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||||
"watch": "npm-run-all -p watch:*",
|
"watch": "npm-run-all -p watch:*",
|
||||||
"watch:esbuild": "node esbuild.js --watch",
|
"watch:esbuild": "node esbuild.js --watch",
|
||||||
|
|||||||
121
src/api/providers/__tests__/openrouter.test.ts
Normal file
121
src/api/providers/__tests__/openrouter.test.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { OpenRouterHandler } from '../openrouter'
|
||||||
|
import { ApiHandlerOptions, ModelInfo } from '../../../shared/api'
|
||||||
|
import OpenAI from 'openai'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Anthropic } from '@anthropic-ai/sdk'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('openai')
|
||||||
|
jest.mock('axios')
|
||||||
|
jest.mock('delay', () => jest.fn(() => Promise.resolve()))
|
||||||
|
|
||||||
|
describe('OpenRouterHandler', () => {
|
||||||
|
const mockOptions: ApiHandlerOptions = {
|
||||||
|
openRouterApiKey: 'test-key',
|
||||||
|
openRouterModelId: 'test-model',
|
||||||
|
openRouterModelInfo: {
|
||||||
|
name: 'Test Model',
|
||||||
|
description: 'Test Description',
|
||||||
|
maxTokens: 1000,
|
||||||
|
contextWindow: 2000,
|
||||||
|
supportsPromptCache: true,
|
||||||
|
inputPrice: 0.01,
|
||||||
|
outputPrice: 0.02
|
||||||
|
} as ModelInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('constructor initializes with correct options', () => {
|
||||||
|
const handler = new OpenRouterHandler(mockOptions)
|
||||||
|
expect(handler).toBeInstanceOf(OpenRouterHandler)
|
||||||
|
expect(OpenAI).toHaveBeenCalledWith({
|
||||||
|
baseURL: 'https://openrouter.ai/api/v1',
|
||||||
|
apiKey: mockOptions.openRouterApiKey,
|
||||||
|
defaultHeaders: {
|
||||||
|
'HTTP-Referer': 'https://cline.bot',
|
||||||
|
'X-Title': 'Cline',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getModel returns correct model info when options are provided', () => {
|
||||||
|
const handler = new OpenRouterHandler(mockOptions)
|
||||||
|
const result = handler.getModel()
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: mockOptions.openRouterModelId,
|
||||||
|
info: mockOptions.openRouterModelInfo
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('createMessage generates correct stream chunks', async () => {
|
||||||
|
const handler = new OpenRouterHandler(mockOptions)
|
||||||
|
const mockStream = {
|
||||||
|
async *[Symbol.asyncIterator]() {
|
||||||
|
yield {
|
||||||
|
id: 'test-id',
|
||||||
|
choices: [{
|
||||||
|
delta: {
|
||||||
|
content: 'test response'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock OpenAI chat.completions.create
|
||||||
|
const mockCreate = jest.fn().mockResolvedValue(mockStream)
|
||||||
|
;(OpenAI as jest.MockedClass<typeof OpenAI>).prototype.chat = {
|
||||||
|
completions: { create: mockCreate }
|
||||||
|
} as any
|
||||||
|
|
||||||
|
// Mock axios.get for generation details
|
||||||
|
;(axios.get as jest.Mock).mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
native_tokens_prompt: 10,
|
||||||
|
native_tokens_completion: 20,
|
||||||
|
total_cost: 0.001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const systemPrompt = 'test system prompt'
|
||||||
|
const messages: Anthropic.Messages.MessageParam[] = [{ role: 'user' as const, content: 'test message' }]
|
||||||
|
|
||||||
|
const generator = handler.createMessage(systemPrompt, messages)
|
||||||
|
const chunks = []
|
||||||
|
|
||||||
|
for await (const chunk of generator) {
|
||||||
|
chunks.push(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify stream chunks
|
||||||
|
expect(chunks).toHaveLength(2) // One text chunk and one usage chunk
|
||||||
|
expect(chunks[0]).toEqual({
|
||||||
|
type: 'text',
|
||||||
|
text: 'test response'
|
||||||
|
})
|
||||||
|
expect(chunks[1]).toEqual({
|
||||||
|
type: 'usage',
|
||||||
|
inputTokens: 10,
|
||||||
|
outputTokens: 20,
|
||||||
|
totalCost: 0.001,
|
||||||
|
fullResponseText: 'test response'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify OpenAI client was called with correct parameters
|
||||||
|
expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
model: mockOptions.openRouterModelId,
|
||||||
|
temperature: 0,
|
||||||
|
messages: expect.arrayContaining([
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: 'test message' }
|
||||||
|
]),
|
||||||
|
stream: true
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -4,9 +4,19 @@ import OpenAI from "openai"
|
|||||||
import { ApiHandler } from "../"
|
import { ApiHandler } from "../"
|
||||||
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
|
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
|
||||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||||
import { ApiStream } from "../transform/stream"
|
import { ApiStreamChunk, ApiStreamUsageChunk } from "../transform/stream"
|
||||||
import delay from "delay"
|
import delay from "delay"
|
||||||
|
|
||||||
|
// Add custom interface for OpenRouter params
|
||||||
|
interface OpenRouterChatCompletionParams extends OpenAI.Chat.ChatCompletionCreateParamsStreaming {
|
||||||
|
transforms?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom interface for OpenRouter usage chunk
|
||||||
|
interface OpenRouterApiStreamUsageChunk extends ApiStreamUsageChunk {
|
||||||
|
fullResponseText: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class OpenRouterHandler implements ApiHandler {
|
export class OpenRouterHandler implements ApiHandler {
|
||||||
private options: ApiHandlerOptions
|
private options: ApiHandlerOptions
|
||||||
private client: OpenAI
|
private client: OpenAI
|
||||||
@@ -23,7 +33,7 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): AsyncGenerator<ApiStreamChunk> {
|
||||||
// Convert Anthropic messages to OpenAI format
|
// Convert Anthropic messages to OpenAI format
|
||||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||||
{ role: "system", content: systemPrompt },
|
{ role: "system", content: systemPrompt },
|
||||||
@@ -95,17 +105,21 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
maxTokens = 8_192
|
maxTokens = 8_192
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
// https://openrouter.ai/docs/transforms
|
||||||
|
let fullResponseText = "";
|
||||||
const stream = await this.client.chat.completions.create({
|
const stream = await this.client.chat.completions.create({
|
||||||
model: this.getModel().id,
|
model: this.getModel().id,
|
||||||
max_tokens: maxTokens,
|
max_tokens: maxTokens,
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
messages: openAiMessages,
|
messages: openAiMessages,
|
||||||
stream: true,
|
stream: true,
|
||||||
})
|
// This way, the transforms field will only be included in the parameters when openRouterUseMiddleOutTransform is true.
|
||||||
|
...(this.options.openRouterUseMiddleOutTransform && { transforms: ["middle-out"] })
|
||||||
|
} as OpenRouterChatCompletionParams);
|
||||||
|
|
||||||
let genId: string | undefined
|
let genId: string | undefined
|
||||||
|
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream as unknown as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>) {
|
||||||
// openrouter returns an error object instead of the openai sdk throwing an error
|
// openrouter returns an error object instead of the openai sdk throwing an error
|
||||||
if ("error" in chunk) {
|
if ("error" in chunk) {
|
||||||
const error = chunk.error as { message?: string; code?: number }
|
const error = chunk.error as { message?: string; code?: number }
|
||||||
@@ -119,10 +133,11 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
|
|
||||||
const delta = chunk.choices[0]?.delta
|
const delta = chunk.choices[0]?.delta
|
||||||
if (delta?.content) {
|
if (delta?.content) {
|
||||||
|
fullResponseText += delta.content;
|
||||||
yield {
|
yield {
|
||||||
type: "text",
|
type: "text",
|
||||||
text: delta.content,
|
text: delta.content,
|
||||||
}
|
} as ApiStreamChunk;
|
||||||
}
|
}
|
||||||
// if (chunk.usage) {
|
// if (chunk.usage) {
|
||||||
// yield {
|
// yield {
|
||||||
@@ -153,13 +168,14 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
inputTokens: generation?.native_tokens_prompt || 0,
|
inputTokens: generation?.native_tokens_prompt || 0,
|
||||||
outputTokens: generation?.native_tokens_completion || 0,
|
outputTokens: generation?.native_tokens_completion || 0,
|
||||||
totalCost: generation?.total_cost || 0,
|
totalCost: generation?.total_cost || 0,
|
||||||
}
|
fullResponseText
|
||||||
|
} as OpenRouterApiStreamUsageChunk;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ignore if fails
|
// ignore if fails
|
||||||
console.error("Error fetching OpenRouter generation details:", error)
|
console.error("Error fetching OpenRouter generation details:", error)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
getModel(): { id: string; info: ModelInfo } {
|
getModel(): { id: string; info: ModelInfo } {
|
||||||
const modelId = this.options.openRouterModelId
|
const modelId = this.options.openRouterModelId
|
||||||
const modelInfo = this.options.openRouterModelInfo
|
const modelInfo = this.options.openRouterModelInfo
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ type GlobalStateKey =
|
|||||||
| "azureApiVersion"
|
| "azureApiVersion"
|
||||||
| "openRouterModelId"
|
| "openRouterModelId"
|
||||||
| "openRouterModelInfo"
|
| "openRouterModelInfo"
|
||||||
|
| "openRouterUseMiddleOutTransform"
|
||||||
| "allowedCommands"
|
| "allowedCommands"
|
||||||
| "soundEnabled"
|
| "soundEnabled"
|
||||||
|
|
||||||
@@ -391,6 +392,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
azureApiVersion,
|
azureApiVersion,
|
||||||
openRouterModelId,
|
openRouterModelId,
|
||||||
openRouterModelInfo,
|
openRouterModelInfo,
|
||||||
|
openRouterUseMiddleOutTransform,
|
||||||
} = message.apiConfiguration
|
} = message.apiConfiguration
|
||||||
await this.updateGlobalState("apiProvider", apiProvider)
|
await this.updateGlobalState("apiProvider", apiProvider)
|
||||||
await this.updateGlobalState("apiModelId", apiModelId)
|
await this.updateGlobalState("apiModelId", apiModelId)
|
||||||
@@ -416,6 +418,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
await this.updateGlobalState("azureApiVersion", azureApiVersion)
|
await this.updateGlobalState("azureApiVersion", azureApiVersion)
|
||||||
await this.updateGlobalState("openRouterModelId", openRouterModelId)
|
await this.updateGlobalState("openRouterModelId", openRouterModelId)
|
||||||
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
|
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
|
||||||
|
await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
|
||||||
if (this.cline) {
|
if (this.cline) {
|
||||||
this.cline.api = buildApiHandler(message.apiConfiguration)
|
this.cline.api = buildApiHandler(message.apiConfiguration)
|
||||||
}
|
}
|
||||||
@@ -943,6 +946,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
azureApiVersion,
|
azureApiVersion,
|
||||||
openRouterModelId,
|
openRouterModelId,
|
||||||
openRouterModelInfo,
|
openRouterModelInfo,
|
||||||
|
openRouterUseMiddleOutTransform,
|
||||||
lastShownAnnouncementId,
|
lastShownAnnouncementId,
|
||||||
customInstructions,
|
customInstructions,
|
||||||
alwaysAllowReadOnly,
|
alwaysAllowReadOnly,
|
||||||
@@ -977,6 +981,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
|
this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
|
||||||
this.getGlobalState("openRouterModelId") as Promise<string | undefined>,
|
this.getGlobalState("openRouterModelId") as Promise<string | undefined>,
|
||||||
this.getGlobalState("openRouterModelInfo") as Promise<ModelInfo | undefined>,
|
this.getGlobalState("openRouterModelInfo") as Promise<ModelInfo | undefined>,
|
||||||
|
this.getGlobalState("openRouterUseMiddleOutTransform") as Promise<boolean | undefined>,
|
||||||
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
|
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
|
||||||
this.getGlobalState("customInstructions") as Promise<string | undefined>,
|
this.getGlobalState("customInstructions") as Promise<string | undefined>,
|
||||||
this.getGlobalState("alwaysAllowReadOnly") as Promise<boolean | undefined>,
|
this.getGlobalState("alwaysAllowReadOnly") as Promise<boolean | undefined>,
|
||||||
@@ -1028,6 +1033,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
azureApiVersion,
|
azureApiVersion,
|
||||||
openRouterModelId,
|
openRouterModelId,
|
||||||
openRouterModelInfo,
|
openRouterModelInfo,
|
||||||
|
openRouterUseMiddleOutTransform,
|
||||||
},
|
},
|
||||||
lastShownAnnouncementId,
|
lastShownAnnouncementId,
|
||||||
customInstructions,
|
customInstructions,
|
||||||
|
|||||||
234
src/core/webview/__tests__/ClineProvider.test.ts
Normal file
234
src/core/webview/__tests__/ClineProvider.test.ts
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import { ClineProvider } from '../ClineProvider'
|
||||||
|
import * as vscode from 'vscode'
|
||||||
|
import { ExtensionMessage, ExtensionState } from '../../../shared/ExtensionMessage'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('vscode', () => ({
|
||||||
|
ExtensionContext: jest.fn(),
|
||||||
|
OutputChannel: jest.fn(),
|
||||||
|
WebviewView: jest.fn(),
|
||||||
|
Uri: {
|
||||||
|
joinPath: jest.fn(),
|
||||||
|
file: jest.fn()
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
getConfiguration: jest.fn().mockReturnValue({
|
||||||
|
get: jest.fn().mockReturnValue([]),
|
||||||
|
update: jest.fn()
|
||||||
|
}),
|
||||||
|
onDidChangeConfiguration: jest.fn().mockImplementation((callback) => ({
|
||||||
|
dispose: jest.fn()
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
uriScheme: 'vscode'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock ESM modules
|
||||||
|
jest.mock('p-wait-for', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn().mockResolvedValue(undefined)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock fs/promises
|
||||||
|
jest.mock('fs/promises', () => ({
|
||||||
|
mkdir: jest.fn(),
|
||||||
|
writeFile: jest.fn(),
|
||||||
|
readFile: jest.fn(),
|
||||||
|
unlink: jest.fn(),
|
||||||
|
rmdir: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock axios
|
||||||
|
jest.mock('axios', () => ({
|
||||||
|
get: jest.fn().mockResolvedValue({ data: { data: [] } }),
|
||||||
|
post: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock buildApiHandler
|
||||||
|
jest.mock('../../../api', () => ({
|
||||||
|
buildApiHandler: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock WorkspaceTracker
|
||||||
|
jest.mock('../../../integrations/workspace/WorkspaceTracker', () => {
|
||||||
|
return jest.fn().mockImplementation(() => ({
|
||||||
|
initializeFilePaths: jest.fn(),
|
||||||
|
dispose: jest.fn()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock Cline
|
||||||
|
jest.mock('../../Cline', () => {
|
||||||
|
return {
|
||||||
|
Cline: jest.fn().mockImplementation(() => ({
|
||||||
|
abortTask: jest.fn(),
|
||||||
|
handleWebviewAskResponse: jest.fn(),
|
||||||
|
clineMessages: []
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Spy on console.error and console.log to suppress expected messages
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
|
jest.spyOn(console, 'log').mockImplementation(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ClineProvider', () => {
|
||||||
|
let provider: ClineProvider
|
||||||
|
let mockContext: vscode.ExtensionContext
|
||||||
|
let mockOutputChannel: vscode.OutputChannel
|
||||||
|
let mockWebviewView: vscode.WebviewView
|
||||||
|
let mockPostMessage: jest.Mock
|
||||||
|
let visibilityChangeCallback: (e?: unknown) => void
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset mocks
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
// Mock context
|
||||||
|
mockContext = {
|
||||||
|
extensionPath: '/test/path',
|
||||||
|
extensionUri: {} as vscode.Uri,
|
||||||
|
globalState: {
|
||||||
|
get: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
keys: jest.fn().mockReturnValue([]),
|
||||||
|
},
|
||||||
|
secrets: {
|
||||||
|
get: jest.fn(),
|
||||||
|
store: jest.fn(),
|
||||||
|
delete: jest.fn()
|
||||||
|
},
|
||||||
|
subscriptions: [],
|
||||||
|
extension: {
|
||||||
|
packageJSON: { version: '1.0.0' }
|
||||||
|
},
|
||||||
|
globalStorageUri: {
|
||||||
|
fsPath: '/test/storage/path'
|
||||||
|
}
|
||||||
|
} as unknown as vscode.ExtensionContext
|
||||||
|
|
||||||
|
// Mock output channel
|
||||||
|
mockOutputChannel = {
|
||||||
|
appendLine: jest.fn(),
|
||||||
|
clear: jest.fn(),
|
||||||
|
dispose: jest.fn()
|
||||||
|
} as unknown as vscode.OutputChannel
|
||||||
|
|
||||||
|
// Mock webview
|
||||||
|
mockPostMessage = jest.fn()
|
||||||
|
mockWebviewView = {
|
||||||
|
webview: {
|
||||||
|
postMessage: mockPostMessage,
|
||||||
|
html: '',
|
||||||
|
options: {},
|
||||||
|
onDidReceiveMessage: jest.fn(),
|
||||||
|
asWebviewUri: jest.fn()
|
||||||
|
},
|
||||||
|
visible: true,
|
||||||
|
onDidDispose: jest.fn().mockImplementation((callback) => {
|
||||||
|
callback()
|
||||||
|
return { dispose: jest.fn() }
|
||||||
|
}),
|
||||||
|
onDidChangeVisibility: jest.fn().mockImplementation((callback) => {
|
||||||
|
visibilityChangeCallback = callback
|
||||||
|
return { dispose: jest.fn() }
|
||||||
|
})
|
||||||
|
} as unknown as vscode.WebviewView
|
||||||
|
|
||||||
|
provider = new ClineProvider(mockContext, mockOutputChannel)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('constructor initializes correctly', () => {
|
||||||
|
expect(provider).toBeInstanceOf(ClineProvider)
|
||||||
|
// Since getVisibleInstance returns the last instance where view.visible is true
|
||||||
|
// @ts-ignore - accessing private property for testing
|
||||||
|
provider.view = mockWebviewView
|
||||||
|
expect(ClineProvider.getVisibleInstance()).toBe(provider)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('resolveWebviewView sets up webview correctly', () => {
|
||||||
|
provider.resolveWebviewView(mockWebviewView)
|
||||||
|
|
||||||
|
expect(mockWebviewView.webview.options).toEqual({
|
||||||
|
enableScripts: true,
|
||||||
|
localResourceRoots: [mockContext.extensionUri]
|
||||||
|
})
|
||||||
|
expect(mockWebviewView.webview.html).toContain('<!DOCTYPE html>')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('postMessageToWebview sends message to webview', async () => {
|
||||||
|
provider.resolveWebviewView(mockWebviewView)
|
||||||
|
|
||||||
|
const mockState: ExtensionState = {
|
||||||
|
version: '1.0.0',
|
||||||
|
clineMessages: [],
|
||||||
|
taskHistory: [],
|
||||||
|
shouldShowAnnouncement: false,
|
||||||
|
apiConfiguration: {
|
||||||
|
apiProvider: 'openrouter'
|
||||||
|
},
|
||||||
|
customInstructions: undefined,
|
||||||
|
alwaysAllowReadOnly: false,
|
||||||
|
alwaysAllowWrite: false,
|
||||||
|
alwaysAllowExecute: false,
|
||||||
|
alwaysAllowBrowser: false,
|
||||||
|
uriScheme: 'vscode',
|
||||||
|
soundEnabled: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const message: ExtensionMessage = {
|
||||||
|
type: 'state',
|
||||||
|
state: mockState
|
||||||
|
}
|
||||||
|
await provider.postMessageToWebview(message)
|
||||||
|
|
||||||
|
expect(mockPostMessage).toHaveBeenCalledWith(message)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles webviewDidLaunch message', async () => {
|
||||||
|
provider.resolveWebviewView(mockWebviewView)
|
||||||
|
|
||||||
|
// Get the message handler from onDidReceiveMessage
|
||||||
|
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||||
|
|
||||||
|
// Simulate webviewDidLaunch message
|
||||||
|
await messageHandler({ type: 'webviewDidLaunch' })
|
||||||
|
|
||||||
|
// Should post state and theme to webview
|
||||||
|
expect(mockPostMessage).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('clearTask aborts current task', async () => {
|
||||||
|
const mockAbortTask = jest.fn()
|
||||||
|
// @ts-ignore - accessing private property for testing
|
||||||
|
provider.cline = { abortTask: mockAbortTask }
|
||||||
|
|
||||||
|
await provider.clearTask()
|
||||||
|
|
||||||
|
expect(mockAbortTask).toHaveBeenCalled()
|
||||||
|
// @ts-ignore - accessing private property for testing
|
||||||
|
expect(provider.cline).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getState returns correct initial state', async () => {
|
||||||
|
const state = await provider.getState()
|
||||||
|
|
||||||
|
expect(state).toHaveProperty('apiConfiguration')
|
||||||
|
expect(state.apiConfiguration).toHaveProperty('apiProvider')
|
||||||
|
expect(state).toHaveProperty('customInstructions')
|
||||||
|
expect(state).toHaveProperty('alwaysAllowReadOnly')
|
||||||
|
expect(state).toHaveProperty('alwaysAllowWrite')
|
||||||
|
expect(state).toHaveProperty('alwaysAllowExecute')
|
||||||
|
expect(state).toHaveProperty('alwaysAllowBrowser')
|
||||||
|
expect(state).toHaveProperty('taskHistory')
|
||||||
|
expect(state).toHaveProperty('soundEnabled')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -33,6 +33,7 @@ export interface ApiHandlerOptions {
|
|||||||
geminiApiKey?: string
|
geminiApiKey?: string
|
||||||
openAiNativeApiKey?: string
|
openAiNativeApiKey?: string
|
||||||
azureApiVersion?: string
|
azureApiVersion?: string
|
||||||
|
openRouterUseMiddleOutTransform?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiConfiguration = ApiHandlerOptions & {
|
export type ApiConfiguration = ApiHandlerOptions & {
|
||||||
|
|||||||
8004
webview-ui/package-lock.json
generated
8004
webview-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -249,6 +249,15 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
|||||||
</span>
|
</span>
|
||||||
)} */}
|
)} */}
|
||||||
</p>
|
</p>
|
||||||
|
<VSCodeCheckbox
|
||||||
|
checked={apiConfiguration?.openRouterUseMiddleOutTransform || false}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
const isChecked = e.target.checked === true
|
||||||
|
setApiConfiguration({ ...apiConfiguration, openRouterUseMiddleOutTransform: isChecked })
|
||||||
|
}}>
|
||||||
|
Compress prompts and message chains to the context size (<a href="https://openrouter.ai/docs/transforms">OpenRouter Transforms</a>)
|
||||||
|
</VSCodeCheckbox>
|
||||||
|
<br/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user