mirror of
https://github.com/cline/cline.git
synced 2026-09-04 11:44:01 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d879f2cbfc | |||
| bdb3bf46d5 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"cline": minor
|
||||
---
|
||||
|
||||
Adds Messages API support to Oracle Code Assist, adding functionality for Claude models
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Add /q command to quit CLI
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix a streaming crash when a chunk has usage data but no `delta` by guarding reasoning field checks in provider handlers. Add regression tests for OpenRouter, Cline, Vercel AI Gateway, and Fireworks handlers to cover usage-only chunks.
|
||||
@@ -1,4 +0,0 @@
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add missing smoke evaluation npm scripts so documented commands like `npm run eval:smoke:run` work from the repository root.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
fix acp auth check so acp mode can be used with more providers
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
VSCode uses shared files for global, workspace and secret state.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Update SambaNova Provider models list and add temperature for models
|
||||
@@ -36,9 +36,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
# Keep publish environment aligned with test workflow/tooling lockfile expectations.
|
||||
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
|
||||
node-version: 22
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci --include=optional
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm install --include=optional
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [3.66.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Gemini-3.1 Pro Preview
|
||||
|
||||
|
||||
## [3.65.0]
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
# cline
|
||||
|
||||
## 2.4.2
|
||||
|
||||
### Added
|
||||
|
||||
- Gemini-3.1 Pro Preview
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- VSCode uses shared files for global, workspace and secret state.
|
||||
|
||||
## [2.4.1]
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "2.4.2",
|
||||
"version": "2.4.1",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "dist/cli.mjs",
|
||||
"bin": {
|
||||
|
||||
@@ -211,7 +211,7 @@ export class ACPDiffViewProvider extends FileEditProvider {
|
||||
* content via the ACP connection. Otherwise, it falls back to the
|
||||
* FileEditProvider's local fs implementation.
|
||||
*/
|
||||
protected override async saveDocument(): Promise<boolean> {
|
||||
protected override async saveDocument(): Promise<Boolean> {
|
||||
// If we can't write files via ACP, fall back to FileEditProvider
|
||||
if (!this.canWriteFile()) {
|
||||
Logger.debug("[ACPDiffViewProvider] Client does not support fs.writeTextFile, falling back to local fs")
|
||||
|
||||
@@ -111,7 +111,7 @@ class ACPEnvServiceClient implements EnvServiceClientInterface {
|
||||
constructor(
|
||||
_clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
_sessionIdResolver: SessionIdResolver,
|
||||
version = "1.0.0",
|
||||
version: string = "1.0.0",
|
||||
) {
|
||||
this.version = version
|
||||
}
|
||||
@@ -191,6 +191,8 @@ class ACPEnvServiceClient implements EnvServiceClientInterface {
|
||||
* Most operations are stubs that will be implemented using ACP extension methods.
|
||||
*/
|
||||
class ACPWindowServiceClient implements WindowServiceClientInterface {
|
||||
constructor(_clientCapabilities: acp.ClientCapabilities | undefined, _sessionIdResolver: SessionIdResolver) {}
|
||||
|
||||
async showTextDocument(request: proto.host.ShowTextDocumentRequest): Promise<proto.host.TextEditorInfo> {
|
||||
// Next phase: Send ACP extension request to open document in the editor.
|
||||
// This would tell the ACP client to open the specified file.
|
||||
@@ -400,11 +402,11 @@ export class ACPHostBridgeClientProvider implements HostBridgeClientProvider {
|
||||
clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
sessionIdResolver: SessionIdResolver,
|
||||
cwdResolver: CwdResolver,
|
||||
version = "1.0.0",
|
||||
version: string = "1.0.0",
|
||||
) {
|
||||
this.workspaceClient = new ACPWorkspaceServiceClient(clientCapabilities, sessionIdResolver, cwdResolver)
|
||||
this.envClient = new ACPEnvServiceClient(clientCapabilities, sessionIdResolver, version)
|
||||
this.windowClient = new ACPWindowServiceClient()
|
||||
this.windowClient = new ACPWindowServiceClient(clientCapabilities, sessionIdResolver)
|
||||
this.diffClient = new ACPDiffServiceClient()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
} from "@shared/api"
|
||||
import type { ClineAsk, ClineMessage as ClineMessageType } from "@shared/ExtensionMessage"
|
||||
import { CLI_ONLY_COMMANDS, VSCODE_ONLY_COMMANDS } from "@shared/slashCommands"
|
||||
import { ProviderToApiKeyMap } from "@shared/storage"
|
||||
import { getProviderModelIdKey } from "@shared/storage/provider-keys"
|
||||
import { ClineEndpoint } from "@/config.js"
|
||||
import { Controller } from "@/core/controller"
|
||||
@@ -57,7 +58,6 @@ import { openExternal } from "@/utils/env"
|
||||
import { ACPDiffViewProvider } from "../acp/ACPDiffViewProvider.js"
|
||||
import { ACPHostBridgeClientProvider } from "../acp/ACPHostBridgeClientProvider.js"
|
||||
import { AcpTerminalManager } from "../acp/AcpTerminalManager.js"
|
||||
import { isAuthConfigured } from "../index.js"
|
||||
import { fetchOpenRouterModels, usesOpenRouterModels } from "../utils/openrouter-models"
|
||||
import { CliContextResult, initializeCliContext } from "../vscode-context.js"
|
||||
import { ClineSessionEmitter } from "./ClineSessionEmitter.js"
|
||||
@@ -265,7 +265,7 @@ export class ClineAgent implements acp.Agent {
|
||||
*/
|
||||
async newSession(params: acp.NewSessionRequest): Promise<acp.NewSessionResponse> {
|
||||
// Check if authentication is required
|
||||
const isAuthenticated = await isAuthConfigured()
|
||||
const isAuthenticated = await this.isAuthConfigured()
|
||||
if (!isAuthenticated) {
|
||||
throw RequestError.authRequired()
|
||||
}
|
||||
@@ -829,7 +829,7 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
await this.emitSessionUpdate(sessionId, {
|
||||
sessionUpdate,
|
||||
content: { type: "text", text: needsNewline ? `\n${textDelta}` : textDelta },
|
||||
content: { type: "text", text: needsNewline ? "\n" + textDelta : textDelta },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1082,7 +1082,7 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
// Use the permission handler callback pattern
|
||||
return new Promise<acp.RequestPermissionResponse>((resolve) => {
|
||||
this.permissionHandler?.({ toolCall, options }, resolve)
|
||||
this.permissionHandler!({ toolCall, options }, resolve)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1146,6 +1146,39 @@ export class ClineAgent implements acp.Agent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has authentication configured.
|
||||
* Returns true if they have either:
|
||||
* - Cline provider with stored auth data
|
||||
* - OpenAI Codex provider with OAuth credentials
|
||||
* - BYO provider with an API key configured
|
||||
*/
|
||||
private async isAuthConfigured(): Promise<boolean> {
|
||||
const stateManager = StateManager.get()
|
||||
const mode = stateManager.getGlobalSettingsKey("mode") as string
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const currentProvider = (stateManager.getGlobalSettingsKey(providerKey) as string) || "cline"
|
||||
|
||||
if (currentProvider === "cline") {
|
||||
// For Cline provider, check if we have stored auth data
|
||||
return Boolean(stateManager.getSecretKey("clineApiKey") || stateManager.getSecretKey("clineAccountId"))
|
||||
}
|
||||
|
||||
// For OpenAI Codex provider, check OAuth credentials
|
||||
if (currentProvider === "openai-codex") {
|
||||
return await openAiCodexOAuthManager.isAuthenticated()
|
||||
}
|
||||
|
||||
// For BYO providers, check if the API key is configured
|
||||
const keyField = ProviderToApiKeyMap[currentProvider as keyof typeof ProviderToApiKeyMap]
|
||||
if (!keyField) {
|
||||
return false
|
||||
}
|
||||
|
||||
const fields = Array.isArray(keyField) ? keyField : [keyField]
|
||||
return fields.some((key) => stateManager.getSecretKey(key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle OpenAI Codex OAuth authentication flow.
|
||||
*
|
||||
|
||||
@@ -983,13 +983,13 @@ describe("translateMessage - ask messages", () => {
|
||||
// Should require permission
|
||||
expect(result.requiresPermission).toBe(true)
|
||||
expect(result.permissionRequest).toBeDefined()
|
||||
expect(result.permissionRequest?.toolCall.toolCallId).toBe(call.toolCallId)
|
||||
expect(result.permissionRequest!.toolCall.toolCallId).toBe(call.toolCallId)
|
||||
|
||||
// Should have standard permission options
|
||||
expect(result.permissionRequest?.options).toHaveLength(3)
|
||||
expect(result.permissionRequest?.options.map((o) => o.kind)).toContain("allow_once")
|
||||
expect(result.permissionRequest?.options.map((o) => o.kind)).toContain("allow_always")
|
||||
expect(result.permissionRequest?.options.map((o) => o.kind)).toContain("reject_once")
|
||||
expect(result.permissionRequest!.options).toHaveLength(3)
|
||||
expect(result.permissionRequest!.options.map((o) => o.kind)).toContain("allow_once")
|
||||
expect(result.permissionRequest!.options.map((o) => o.kind)).toContain("allow_always")
|
||||
expect(result.permissionRequest!.options.map((o) => o.kind)).toContain("reject_once")
|
||||
|
||||
// Should track pending tool call
|
||||
expect(sessionState.pendingToolCalls.has(call.toolCallId)).toBe(true)
|
||||
@@ -1087,8 +1087,8 @@ describe("translateMessage - ask messages", () => {
|
||||
expect(result.requiresPermission).toBe(true)
|
||||
|
||||
// Browser actions have restricted options (no "always allow")
|
||||
expect(result.permissionRequest?.options).toHaveLength(2)
|
||||
expect(result.permissionRequest?.options.map((o) => o.kind)).not.toContain("allow_always")
|
||||
expect(result.permissionRequest!.options).toHaveLength(2)
|
||||
expect(result.permissionRequest!.options.map((o) => o.kind)).not.toContain("allow_always")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ function translateSayMessage(
|
||||
if (message.text) {
|
||||
updates.push({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: `\n${message.text}` },
|
||||
content: { type: "text", text: "\n" + message.text },
|
||||
})
|
||||
}
|
||||
break
|
||||
@@ -622,7 +622,7 @@ function translateAskMessage(
|
||||
if (message.text) {
|
||||
updates.push({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: `\n${message.text}` },
|
||||
content: { type: "text", text: "\n" + message.text },
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
@@ -116,7 +116,7 @@ export function getPermissionOptionsForAskType(askType: ClineAsk): acp.Permissio
|
||||
* @param askType - The original ClineAsk type that triggered the permission request
|
||||
* @returns The translated result for Cline's handleWebviewAskResponse
|
||||
*/
|
||||
export function handlePermissionResponse(response: acp.RequestPermissionResponse, _askType: ClineAsk): PermissionHandlerResult {
|
||||
export function handlePermissionResponse(response: acp.RequestPermissionResponse, askType: ClineAsk): PermissionHandlerResult {
|
||||
// Check if cancelled
|
||||
if (response.outcome.outcome === "cancelled") {
|
||||
return {
|
||||
|
||||
@@ -194,7 +194,7 @@ const errorTypes = ["api_req_failed", "mistake_limit_reached"]
|
||||
/**
|
||||
* Get button configuration based on message type and state
|
||||
*/
|
||||
export function getButtonConfig(message: ClineMessage | undefined, isStreaming = false): ButtonConfig {
|
||||
export function getButtonConfig(message: ClineMessage | undefined, isStreaming: boolean = false): ButtonConfig {
|
||||
if (!message) {
|
||||
return BUTTON_CONFIGS.default
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
|
||||
setTextInput((prev) => prev.slice(0, -1))
|
||||
} else if (input && !key.ctrl && !key.meta) {
|
||||
// Check if it's a number for option selection (only when no text typed yet)
|
||||
const num = Number.parseInt(input, 10)
|
||||
const num = parseInt(input, 10)
|
||||
if (textInput === "" && !Number.isNaN(num) && num >= 1 && num <= parts.options.length) {
|
||||
const selectedOption = parts.options[num - 1]
|
||||
sendResponse("messageResponse", selectedOption)
|
||||
@@ -401,42 +401,43 @@ function getCliMessagePrefixIcon(message: ClineMessage): string {
|
||||
default:
|
||||
return "❔"
|
||||
}
|
||||
}
|
||||
switch (message.say) {
|
||||
case "task":
|
||||
return "📋"
|
||||
case "error":
|
||||
return "❌"
|
||||
case "text":
|
||||
return "💬"
|
||||
case "reasoning":
|
||||
return "🧠"
|
||||
case "completion_result":
|
||||
return "✅"
|
||||
case "user_feedback":
|
||||
return "👤"
|
||||
case "command":
|
||||
case "command_output":
|
||||
return "⚙️"
|
||||
case "tool":
|
||||
return "🔧"
|
||||
case "browser_action":
|
||||
case "browser_action_launch":
|
||||
case "browser_action_result":
|
||||
return "🌐"
|
||||
case "mcp_server_request_started":
|
||||
case "mcp_server_response":
|
||||
return "🔌"
|
||||
case "api_req_started":
|
||||
case "api_req_finished":
|
||||
return "🔄"
|
||||
case "checkpoint_created":
|
||||
return "💾"
|
||||
case "info":
|
||||
return "ℹ️"
|
||||
case "generate_explanation":
|
||||
return "📝"
|
||||
default:
|
||||
return " "
|
||||
} else {
|
||||
switch (message.say) {
|
||||
case "task":
|
||||
return "📋"
|
||||
case "error":
|
||||
return "❌"
|
||||
case "text":
|
||||
return "💬"
|
||||
case "reasoning":
|
||||
return "🧠"
|
||||
case "completion_result":
|
||||
return "✅"
|
||||
case "user_feedback":
|
||||
return "👤"
|
||||
case "command":
|
||||
case "command_output":
|
||||
return "⚙️"
|
||||
case "tool":
|
||||
return "🔧"
|
||||
case "browser_action":
|
||||
case "browser_action_launch":
|
||||
case "browser_action_result":
|
||||
return "🌐"
|
||||
case "mcp_server_request_started":
|
||||
case "mcp_server_response":
|
||||
return "🔌"
|
||||
case "api_req_started":
|
||||
case "api_req_finished":
|
||||
return "🔄"
|
||||
case "checkpoint_created":
|
||||
return "💾"
|
||||
case "info":
|
||||
return "ℹ️"
|
||||
case "generate_explanation":
|
||||
return "📝"
|
||||
default:
|
||||
return " "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +323,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
setErrorMessage(error instanceof Error ? error.message : String(error))
|
||||
setStep("error")
|
||||
}
|
||||
}, [controller])
|
||||
}, [])
|
||||
|
||||
// Start Cline auth flow
|
||||
const startClineAuth = useCallback(async () => {
|
||||
@@ -379,7 +379,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
setStep("apikey")
|
||||
}
|
||||
},
|
||||
[startOpenAiCodexAuth],
|
||||
[startOcaAuth, startOpenAiCodexAuth],
|
||||
)
|
||||
|
||||
const handleApiKeySubmit = useCallback(
|
||||
|
||||
@@ -177,7 +177,7 @@ function getToolMainArg(_toolName: string, args: Record<string, unknown>): strin
|
||||
|
||||
// Command - truncate long commands
|
||||
if (typeof args.command === "string") {
|
||||
return args.command.length > 120 ? `${args.command.substring(0, 117)}...` : args.command
|
||||
return args.command.length > 120 ? args.command.substring(0, 117) + "..." : args.command
|
||||
}
|
||||
|
||||
// URL
|
||||
@@ -221,7 +221,7 @@ const ToolCallText: React.FC<{
|
||||
*/
|
||||
function truncate(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text
|
||||
return `${text.substring(0, maxLength - 3)}...`
|
||||
return text.substring(0, maxLength - 3) + "..."
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,7 +245,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
// User messages (task, user_feedback)
|
||||
// If multi-line, extend background to full width for consistent appearance
|
||||
if (say === "task" || say === "user_feedback") {
|
||||
const content = `> ${text || ""}`
|
||||
const content = "> " + (text || "")
|
||||
const isMultiLine = content.includes("\n") || content.length > terminalWidth
|
||||
|
||||
if (isMultiLine) {
|
||||
@@ -412,7 +412,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
: "tool: unknown"
|
||||
|
||||
let argsLines: string[] = []
|
||||
if (parsed?.arguments?.trim() && parsed.arguments !== "{}") {
|
||||
if (parsed?.arguments && parsed.arguments.trim() && parsed.arguments !== "{}") {
|
||||
let formattedArgs = parsed.arguments
|
||||
try {
|
||||
formattedArgs = JSON.stringify(JSON.parse(parsed.arguments), null, 2)
|
||||
|
||||
@@ -12,7 +12,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { ChatView } from "./ChatView"
|
||||
|
||||
// Helper to wait for async state updates
|
||||
const delay = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
const delay = (ms: number = 60) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
// Type for our exit mock function
|
||||
type ExitMockFn = ReturnType<typeof vi.fn> & (() => void)
|
||||
|
||||
@@ -486,7 +486,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
if (taskState.mode && taskState.mode !== mode) {
|
||||
setMode(taskState.mode as Mode)
|
||||
}
|
||||
}, [taskState.mode, mode])
|
||||
}, [taskState.mode])
|
||||
|
||||
const toggleAutoApproveAll = useCallback(() => {
|
||||
const newValue = !autoApproveAll
|
||||
@@ -499,7 +499,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
const stateManager = StateManager.get()
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
return (stateManager.getGlobalSettingsKey(providerKey) as string) || ""
|
||||
}, [mode])
|
||||
}, [mode, activePanel])
|
||||
|
||||
// Get model ID based on current mode and provider
|
||||
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
@@ -510,7 +510,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
const stateManager = StateManager.get()
|
||||
const modelKey = getProviderModelIdKey(provider as ApiProvider, mode)
|
||||
return (stateManager.getGlobalSettingsKey(modelKey) as string) || getProviderDefaultModelId(provider as ApiProvider) || ""
|
||||
}, [mode, provider])
|
||||
}, [mode, provider, activePanel])
|
||||
|
||||
const toggleMode = useCallback(async () => {
|
||||
const newMode: Mode = mode === "act" ? "plan" : "act"
|
||||
@@ -549,7 +549,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
if (ctrl) {
|
||||
ctrl.postStateToWebview()
|
||||
}
|
||||
}, [ctrl, clearState, storageKey, setCursorPos, setTextInput])
|
||||
}, [ctrl, clearState, storageKey])
|
||||
|
||||
const refs = useRef({
|
||||
searchTimeout: null as NodeJS.Timeout | null,
|
||||
@@ -644,10 +644,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
const messages = taskState.clineMessages || []
|
||||
|
||||
// Refresh git diff stats when messages change (after file edits)
|
||||
const _lastMsg = messages[messages.length - 1]
|
||||
const lastMsg = messages[messages.length - 1]
|
||||
useEffect(() => {
|
||||
setGitDiffStats(getGitDiffStats(workspacePath))
|
||||
}, [workspacePath])
|
||||
}, [messages.length, lastMsg?.partial, lastMsg?.ts, workspacePath])
|
||||
|
||||
// Filter messages we want to display
|
||||
const displayMessages = useMemo(() => {
|
||||
@@ -819,7 +819,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
// Controller may be disposed
|
||||
}
|
||||
},
|
||||
[ctrl, pendingAsk, pastedTexts, storageKey, setCursorPos, setTextInput],
|
||||
[ctrl, pendingAsk, pastedTexts, storageKey],
|
||||
)
|
||||
|
||||
// Handle cancel/interrupt
|
||||
@@ -895,7 +895,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
break
|
||||
}
|
||||
},
|
||||
[sendAskResponse, pendingAsk, handleExit, handleCancel, clearViewAndResetTask, ctrl, setCursorPos, setTextInput],
|
||||
[controller, taskController, sendAskResponse, pendingAsk, handleExit, handleCancel, clearViewAndResetTask],
|
||||
)
|
||||
|
||||
// Handle task submission (new task)
|
||||
@@ -939,7 +939,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
onError?.()
|
||||
}
|
||||
},
|
||||
[ctrl, onError, pastedTexts, storageKey, setCursorPos, setTextInput],
|
||||
[ctrl, onError, pastedTexts, storageKey],
|
||||
)
|
||||
|
||||
// Auto-submit initial prompt if provided
|
||||
@@ -998,7 +998,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
|
||||
autoSubmit()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [controller, initialImages, initialPrompt, onError, taskController, taskId]) // Only run once on mount
|
||||
}, []) // Only run once on mount
|
||||
|
||||
// Search for files when in mention mode
|
||||
useEffect(() => {
|
||||
@@ -1051,7 +1051,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
clearTimeout(r.searchTimeout)
|
||||
}
|
||||
}
|
||||
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath, mentionInfo])
|
||||
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath])
|
||||
|
||||
// Handle keyboard input
|
||||
//
|
||||
@@ -1172,7 +1172,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
setSlashMenuDismissed(true)
|
||||
return
|
||||
}
|
||||
if (cmd.name === "exit" || cmd.name === "q") {
|
||||
if (cmd.name === "exit") {
|
||||
handleExit()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ export const CheckpointMenu: React.FC<CheckpointMenuProps> = ({ messages, onSele
|
||||
|
||||
// Quick number selection for checkpoints
|
||||
if (stage === "checkpoint") {
|
||||
const num = Number.parseInt(input, 10)
|
||||
const num = parseInt(input, 10)
|
||||
if (!Number.isNaN(num) && num >= 1 && num <= checkpoints.length) {
|
||||
setSelectedCheckpoint(num - 1)
|
||||
setStage("restoreType")
|
||||
|
||||
@@ -309,7 +309,7 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
const currentTabIndex = availableTabs.findIndex((t) => t.key === currentTab)
|
||||
const targetIdx =
|
||||
input >= "1" && input <= "5"
|
||||
? Number.parseInt(input, 10) - 1
|
||||
? Number.parseInt(input) - 1
|
||||
: key.leftArrow
|
||||
? (currentTabIndex - 1 + availableTabs.length) % availableTabs.length
|
||||
: (currentTabIndex + 1) % availableTabs.length
|
||||
|
||||
@@ -127,10 +127,10 @@ export function formatValue(value: unknown, maxLen = 50): string {
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
const json = JSON.stringify(value)
|
||||
return json.length > maxLen ? `${json.slice(0, maxLen - 3)}...` : json
|
||||
return json.length > maxLen ? json.slice(0, maxLen - 3) + "..." : json
|
||||
}
|
||||
const str = String(value)
|
||||
return str.length > maxLen ? `${str.slice(0, maxLen - 3)}...` : str
|
||||
return str.length > maxLen ? str.slice(0, maxLen - 3) + "..." : str
|
||||
}
|
||||
|
||||
export function parseValue(input: string, type: ValueType): unknown {
|
||||
@@ -337,7 +337,7 @@ export const SkillRow: React.FC<{
|
||||
{skill.description && (
|
||||
<Box marginLeft={4}>
|
||||
<Text color="gray">
|
||||
{skill.description.length > 60 ? `${skill.description.slice(0, 57)}...` : skill.description}
|
||||
{skill.description.length > 60 ? skill.description.slice(0, 57) + "..." : skill.description}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -20,11 +20,11 @@ interface FileMentionMenuProps {
|
||||
/**
|
||||
* Truncate path from the left if too long, keeping the filename visible
|
||||
*/
|
||||
function truncatePath(filePath: string, maxLength = 50): string {
|
||||
function truncatePath(filePath: string, maxLength: number = 50): string {
|
||||
if (filePath.length <= maxLength) {
|
||||
return filePath
|
||||
}
|
||||
return `...${filePath.slice(-(maxLength - 3))}`
|
||||
return "..." + filePath.slice(-(maxLength - 3))
|
||||
}
|
||||
|
||||
export const FileMentionMenu: React.FC<FileMentionMenuProps> = ({
|
||||
|
||||
@@ -115,7 +115,7 @@ const Header: React.FC<{
|
||||
const isCompleted = completedCount === totalCount
|
||||
|
||||
const displayText = isCompleted ? "All tasks completed!" : currentTodo?.text || "To-Do list"
|
||||
const truncatedText = displayText.length > 50 ? `${displayText.substring(0, 47)}...` : displayText
|
||||
const truncatedText = displayText.length > 50 ? displayText.substring(0, 47) + "..." : displayText
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" gap={1}>
|
||||
|
||||
@@ -88,10 +88,6 @@ export const HelpPanelContent: React.FC<HelpPanelContentProps> = ({ onClose }) =
|
||||
{" "}
|
||||
<Text color="white">/clear</Text> - Start a fresh task
|
||||
</Text>
|
||||
<Text>
|
||||
{" "}
|
||||
<Text color="white">/q</Text> - Quit Cline
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Text>
|
||||
|
||||
@@ -98,7 +98,7 @@ export const HistoryPanelContent: React.FC<HistoryPanelContentProps> = ({ onClos
|
||||
// Reset selection when search changes
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0)
|
||||
}, [])
|
||||
}, [searchQuery])
|
||||
|
||||
const handleSelect = useCallback(
|
||||
async (item: TaskHistoryItem) => {
|
||||
|
||||
@@ -40,7 +40,7 @@ interface HistoryViewProps {
|
||||
/**
|
||||
* Format separator
|
||||
*/
|
||||
function formatSeparator(char = "─", width = 80): string {
|
||||
function formatSeparator(char: string = "─", width: number = 80): string {
|
||||
return char.repeat(Math.max(width, 10))
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ export const HistoryView: React.FC<HistoryViewProps> = ({
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color="white">
|
||||
{`📜 Task History (${totalCount} total)`}
|
||||
{"📜 Task History (" + totalCount + " total)"}
|
||||
</Text>
|
||||
<Text color="gray">Use ↑↓/j/k to navigate, Enter to select</Text>
|
||||
|
||||
@@ -161,7 +161,7 @@ export const HistoryView: React.FC<HistoryViewProps> = ({
|
||||
<Text>No task history available.</Text>
|
||||
) : (
|
||||
<Box flexDirection="column">
|
||||
{showUpIndicator && <Text color="gray">{` ↑ ${startIndex} more above`}</Text>}
|
||||
{showUpIndicator && <Text color="gray">{" ↑ " + startIndex + " more above"}</Text>}
|
||||
{visibleTasks.map((task, index) => {
|
||||
const actualIndex = startIndex + index
|
||||
const isSelected = actualIndex === selectedIndex
|
||||
@@ -197,7 +197,7 @@ export const HistoryView: React.FC<HistoryViewProps> = ({
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
{showDownIndicator && <Text color="gray">{` ↓ ${pageItems.length - endIndex} more below`}</Text>}
|
||||
{showDownIndicator && <Text color="gray">{" ↓ " + (pageItems.length - endIndex) + " more below"}</Text>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export const ImportView: React.FC<ImportViewProps> = ({ source, onComplete, onCa
|
||||
}, [keys, selectedIndex, onComplete])
|
||||
|
||||
useInput(
|
||||
(_input, key) => {
|
||||
(input, key) => {
|
||||
if (key.escape) {
|
||||
if (step === "confirm" && keys.length > 1) {
|
||||
setStep("select")
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { render } from "ink-testing-library"
|
||||
// biome-ignore lint/correctness/noUnusedImports: React must be in scope for JSX in this test file.
|
||||
import React from "react"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
// Mock ink's useApp
|
||||
const mockExit = vi.fn()
|
||||
vi.mock("ink", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("ink")>()
|
||||
return {
|
||||
...actual,
|
||||
useApp: () => ({ exit: mockExit }),
|
||||
}
|
||||
})
|
||||
|
||||
// Mock child_process
|
||||
vi.mock("child_process", () => ({
|
||||
execSync: vi.fn().mockReturnValue(""),
|
||||
exec: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/core/controller/slash/getAvailableSlashCommands", () => ({
|
||||
getAvailableSlashCommands: vi.fn().mockResolvedValue({ commands: [] }),
|
||||
}))
|
||||
|
||||
vi.mock("@/core/storage/StateManager", () => ({
|
||||
StateManager: {
|
||||
get: () => ({
|
||||
getGlobalSettingsKey: vi.fn().mockReturnValue("act"),
|
||||
getGlobalStateKey: vi.fn().mockReturnValue([]),
|
||||
getApiConfiguration: vi.fn().mockReturnValue({}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@/services/telemetry", () => ({
|
||||
telemetryService: {
|
||||
captureHostEvent: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@shared/services/Session", () => ({
|
||||
Session: {
|
||||
get: () => ({
|
||||
getStats: vi.fn().mockReturnValue({}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("../context/TaskContext", () => ({
|
||||
useTaskContext: () => ({
|
||||
controller: {},
|
||||
clearState: vi.fn(),
|
||||
}),
|
||||
useTaskState: () => ({
|
||||
clineMessages: [],
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock("../hooks/useStateSubscriber", () => ({
|
||||
useIsSpinnerActive: () => ({ isActive: false, startTime: 0 }),
|
||||
}))
|
||||
|
||||
import { ChatView } from "./ChatView"
|
||||
|
||||
// Helper to wait for async state updates
|
||||
const delay = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
describe("Quit Command (/q and /exit)", () => {
|
||||
const mockOnExit = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should exit the application when /q is selected from slash menu", async () => {
|
||||
const { stdin } = render(<ChatView onExit={mockOnExit} />)
|
||||
await delay()
|
||||
|
||||
// Type /q
|
||||
stdin.write("/q")
|
||||
await delay()
|
||||
|
||||
// Press Enter
|
||||
stdin.write("\r")
|
||||
|
||||
// handleExit has a 150ms timeout
|
||||
await delay(200)
|
||||
|
||||
expect(mockExit).toHaveBeenCalled()
|
||||
expect(mockOnExit).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should exit the application when /exit is selected from slash menu", async () => {
|
||||
const { stdin } = render(<ChatView onExit={mockOnExit} />)
|
||||
await delay()
|
||||
|
||||
// Type /exit
|
||||
stdin.write("/exit")
|
||||
await delay()
|
||||
|
||||
// Press Enter
|
||||
stdin.write("\r")
|
||||
|
||||
// handleExit has a 150ms timeout
|
||||
await delay(200)
|
||||
|
||||
expect(mockExit).toHaveBeenCalled()
|
||||
expect(mockOnExit).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -63,7 +63,7 @@ export function SearchableList<T extends SearchableListItem>({
|
||||
// Reset index when search changes
|
||||
useEffect(() => {
|
||||
setIndex(0)
|
||||
}, [])
|
||||
}, [search])
|
||||
|
||||
useInput(
|
||||
(input, key) => {
|
||||
|
||||
@@ -237,7 +237,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
"not configured",
|
||||
)
|
||||
// Refresh trigger to force re-reading model IDs from state
|
||||
const [_modelRefreshKey, setModelRefreshKey] = useState(0)
|
||||
const [modelRefreshKey, setModelRefreshKey] = useState(0)
|
||||
const refreshModelIds = useCallback(() => setModelRefreshKey((k) => k + 1), [])
|
||||
|
||||
// OCA auth hook
|
||||
@@ -273,7 +273,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
actModelId: actKey ? (stateManager.getGlobalSettingsKey(actKey) as string) || "" : "",
|
||||
planModelId: planKey ? (stateManager.getGlobalSettingsKey(planKey) as string) || "" : "",
|
||||
}
|
||||
}, [stateManager])
|
||||
}, [modelRefreshKey, stateManager])
|
||||
|
||||
// Toggle a feature setting
|
||||
const toggleFeature = useCallback(
|
||||
@@ -433,7 +433,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [isWaitingForClineAuth, controller, fetchAccountInfo, refreshModelIds])
|
||||
}, [isWaitingForClineAuth, controller, fetchAccountInfo])
|
||||
|
||||
// Build items list based on current tab
|
||||
const items: ListItem[] = useMemo(() => {
|
||||
@@ -945,10 +945,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
actReasoningEffort,
|
||||
planReasoningEffort,
|
||||
rebuildTaskApi,
|
||||
setReasoningEffortForMode, // Update telemetry providers to respect the new setting
|
||||
controller,
|
||||
handleTabChange,
|
||||
provider,
|
||||
setReasoningEffortForMode,
|
||||
])
|
||||
|
||||
// Handle completion of the Bedrock custom ARN flow (ARN + base model selected)
|
||||
@@ -1099,7 +1096,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
setCodexAuthError(error instanceof Error ? error.message : String(error))
|
||||
setIsWaitingForCodexAuth(false)
|
||||
}
|
||||
}, [controller, refreshModelIds])
|
||||
}, [controller])
|
||||
|
||||
const handleProviderSelect = useCallback(
|
||||
async (providerId: string) => {
|
||||
@@ -1171,7 +1168,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
setIsPickingProvider(false)
|
||||
}
|
||||
},
|
||||
[stateManager, startCodexAuth, handleClineLogin, isOcaAuthenticated, controller, refreshModelIds],
|
||||
[stateManager, startCodexAuth, handleClineLogin, startOcaAuth, isOcaAuthenticated, controller, refreshModelIds],
|
||||
)
|
||||
|
||||
// Handle API key submission after provider selection
|
||||
|
||||
@@ -248,7 +248,7 @@ const SkillRow: React.FC<{ skill: SkillInfo; isSelected: boolean }> = ({ skill,
|
||||
{skill.description && (
|
||||
<Box marginLeft={4}>
|
||||
<Text color="gray">
|
||||
{skill.description.length > 60 ? `${skill.description.slice(0, 57)}...` : skill.description}
|
||||
{skill.description.length > 60 ? skill.description.slice(0, 57) + "..." : skill.description}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -24,7 +24,7 @@ export const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ commands, se
|
||||
if (maxLength <= 0) return ""
|
||||
if (text.length <= maxLength) return text
|
||||
if (maxLength <= 3) return text.slice(0, maxLength)
|
||||
return `${text.slice(0, maxLength - 3)}...`
|
||||
return text.slice(0, maxLength - 3) + "..."
|
||||
}
|
||||
|
||||
if (commands.length === 0) {
|
||||
|
||||
@@ -50,7 +50,7 @@ function formatNumber(num: number): string {
|
||||
/**
|
||||
* Create a progress bar for context window usage
|
||||
*/
|
||||
function createContextBar(used: number, total: number, width = 8): string {
|
||||
function createContextBar(used: number, total: number, width: number = 8): string {
|
||||
const ratio = Math.min(used / total, 1)
|
||||
const filled = Math.round(ratio * width)
|
||||
const empty = width - filled
|
||||
@@ -76,7 +76,7 @@ export const StatusBar: React.FC<StatusBarProps> = ({
|
||||
const contextBar = createContextBar(totalTokens, contextWindowSize)
|
||||
|
||||
// Format model ID for display (shorten if needed)
|
||||
const displayModel = modelId.length > 20 ? `${modelId.substring(0, 17)}...` : modelId
|
||||
const displayModel = modelId.length > 20 ? modelId.substring(0, 17) + "..." : modelId
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
|
||||
@@ -92,7 +92,7 @@ export const TaskJsonView: React.FC<TaskJsonViewProps> = ({ taskId: _taskId, ver
|
||||
|
||||
outputtedMessages.current.add(message.ts)
|
||||
}
|
||||
}, [state.clineMessages, verbose, getRole])
|
||||
}, [state.clineMessages, verbose])
|
||||
|
||||
// Handle task completion
|
||||
useEffect(() => {
|
||||
|
||||
@@ -72,7 +72,7 @@ export const WelcomeView: React.FC<WelcomeViewProps> = ({ onSubmit, onExit, cont
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const currentProvider = stateManager.getGlobalSettingsKey(providerKey) as string
|
||||
return currentProvider || "cline"
|
||||
}, [])
|
||||
}, [controller])
|
||||
|
||||
// Get model ID based on current mode and provider
|
||||
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
@@ -165,7 +165,7 @@ export const WelcomeView: React.FC<WelcomeViewProps> = ({ onSubmit, onExit, cont
|
||||
clearTimeout(r.searchTimeout)
|
||||
}
|
||||
}
|
||||
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath, mentionInfo])
|
||||
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath])
|
||||
|
||||
useInput(
|
||||
(input, key) => {
|
||||
|
||||
@@ -13,22 +13,16 @@ export interface FeaturedModel {
|
||||
export const FEATURED_MODELS: { recommended: FeaturedModel[]; free: FeaturedModel[] } = {
|
||||
recommended: [
|
||||
{
|
||||
id: "google/gemini-3.1-pro-preview",
|
||||
name: "Gemini 3.1 Pro Preview",
|
||||
description: "Latest Gemini release with 1m ctx window and strong coding performance",
|
||||
labels: ["NEW"],
|
||||
},
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4.6",
|
||||
name: "Claude Sonnet 4.6",
|
||||
description: "Latest Sonnet release with strong coding and agent performance",
|
||||
labels: ["NEW"],
|
||||
id: "anthropic/claude-sonnet-4.5",
|
||||
name: "Claude Sonnet 4.5",
|
||||
description: "Best balance of speed, cost, and quality",
|
||||
labels: ["BEST"],
|
||||
},
|
||||
{
|
||||
id: "anthropic/claude-opus-4.6",
|
||||
name: "Claude Opus 4.6",
|
||||
description: "Most intelligent model for agents and coding",
|
||||
labels: ["BEST"],
|
||||
description: "State-of-the-art for complex coding",
|
||||
labels: ["NEW"],
|
||||
},
|
||||
{
|
||||
id: "openai/gpt-5.2-codex",
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
* Instead of rendering to a webview, this outputs to the terminal
|
||||
*/
|
||||
|
||||
import type * as vscode from "vscode"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
|
||||
export class CliWebviewProvider extends WebviewProvider {
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
super(context)
|
||||
}
|
||||
|
||||
override getWebviewUrl(path: string): string {
|
||||
// CLI doesn't have webview URLs
|
||||
return `file://${path}`
|
||||
|
||||
@@ -79,7 +79,7 @@ export class CliDiffServiceClient implements DiffServiceClientInterface {
|
||||
* CLI implementation of EnvService - handles environment operations
|
||||
*/
|
||||
export class CliEnvServiceClient implements EnvServiceClientInterface {
|
||||
private clipboardContent = ""
|
||||
private clipboardContent: string = ""
|
||||
|
||||
private getTelemetrySetting(): proto.host.Setting {
|
||||
// Read from StateManager - defaults to ENABLED if not set or "unset"
|
||||
@@ -182,6 +182,7 @@ export class CliWindowServiceClient implements WindowServiceClientInterface {
|
||||
case proto.host.ShowMessageType.WARNING:
|
||||
printWarning(message)
|
||||
break
|
||||
case proto.host.ShowMessageType.INFORMATION:
|
||||
default:
|
||||
printInfo(message)
|
||||
break
|
||||
|
||||
@@ -58,7 +58,7 @@ export const useCompletedAskMessages = () => {
|
||||
*/
|
||||
export const useLastCompletedAskMessage = () => {
|
||||
const { state } = useTaskContext()
|
||||
const _processed = useProcessedMessages()
|
||||
const processed = useProcessedMessages()
|
||||
|
||||
const getLastCompletedAskMessage = useCallback((): ClineMessage | null => {
|
||||
if (!state.clineMessages) {
|
||||
|
||||
+31
-5
@@ -73,7 +73,24 @@ async function disposeTelemetryServices(): Promise<void> {
|
||||
await Promise.allSettled([telemetryService.dispose(), PostHogClientProvider.getInstance().dispose()])
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore yoloModeToggled to its original value from before this CLI session.
|
||||
* This ensures the --yolo flag is session-only and doesn't leak into future runs.
|
||||
* Must be called before flushPendingState so the restored value gets persisted.
|
||||
*/
|
||||
function restoreYoloState(): void {
|
||||
if (savedYoloModeToggled !== null) {
|
||||
try {
|
||||
StateManager.get().setGlobalState("yoloModeToggled", savedYoloModeToggled)
|
||||
savedYoloModeToggled = null
|
||||
} catch {
|
||||
// StateManager may not be initialized (e.g., early exit before init)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function disposeCliContext(ctx: CliContext): Promise<void> {
|
||||
restoreYoloState()
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
@@ -186,10 +203,12 @@ function applyTaskOptions(options: TaskOptions): void {
|
||||
telemetryService.captureHostEvent("max_consecutive_mistakes_flag", String(maxConsecutiveMistakes))
|
||||
}
|
||||
|
||||
// Set yolo mode as a session-scoped override so AutoApprove picks it up,
|
||||
// but it is never persisted to disk (setSessionOverride never touches pendingGlobalState).
|
||||
// Override yolo mode only if --yolo flag is explicitly passed.
|
||||
// The original value is saved in initializeCli and restored on exit.
|
||||
if (options.yolo) {
|
||||
StateManager.get().setSessionOverride("yoloModeToggled", true)
|
||||
const state = StateManager.get()
|
||||
savedYoloModeToggled = state.getGlobalSettingsKey("yoloModeToggled") ?? false
|
||||
state.setGlobalState("yoloModeToggled", true)
|
||||
telemetryService.captureHostEvent("yolo_flag", "true")
|
||||
}
|
||||
|
||||
@@ -294,6 +313,9 @@ let activeContext: CliContext | null = null
|
||||
let isShuttingDown = false
|
||||
// Track if we're in plain text mode (no Ink UI) - set by runTask when piped stdin detected
|
||||
let isPlainTextMode = false
|
||||
// Track the original yoloModeToggled value from before this CLI session so we can restore it on exit.
|
||||
// The --yolo flag should only affect the current invocation, not persist across runs.
|
||||
let savedYoloModeToggled: boolean | null = null
|
||||
|
||||
/**
|
||||
* Wait for stdout to fully drain before exiting.
|
||||
@@ -335,6 +357,10 @@ function setupSignalHandlers() {
|
||||
printWarning(`${signal} received, shutting down...`)
|
||||
|
||||
try {
|
||||
// Restore yolo state before any cleanup - this is idempotent and safe
|
||||
// even if disposeCliContext also calls it (restoreYoloState checks savedYoloModeToggled !== null)
|
||||
restoreYoloState()
|
||||
|
||||
if (activeContext) {
|
||||
const task = activeContext.controller.task
|
||||
if (task) {
|
||||
@@ -797,7 +823,7 @@ devCommand
|
||||
* If `welcomeViewCompleted` is undefined (first run), checks if ANY provider has credentials
|
||||
* and sets the flag accordingly.
|
||||
*/
|
||||
export async function isAuthConfigured(): Promise<boolean> {
|
||||
async function isAuthConfigured(): Promise<boolean> {
|
||||
const stateManager = StateManager.get()
|
||||
|
||||
// Check welcomeViewCompleted first - this is the single source of truth
|
||||
@@ -826,7 +852,7 @@ async function checkAnyProviderConfigured(): Promise<boolean> {
|
||||
const config = stateManager.getApiConfiguration() as Record<string, unknown>
|
||||
|
||||
// Check Cline account (stored as "cline:clineAccountId" in secrets, loaded into config)
|
||||
if (config.clineApiKey || config["cline:clineAccountId"]) return true
|
||||
if (config["clineApiKey"] || config["cline:clineAccountId"]) return true
|
||||
|
||||
// Check OpenAI Codex OAuth (stored in SECRETS_KEYS, loaded into config)
|
||||
if (config["openai-codex-oauth-credentials"]) return true
|
||||
|
||||
+46
-45
@@ -112,48 +112,49 @@ function getMessageIcon(message: ClineMessage): string {
|
||||
default:
|
||||
return "❔"
|
||||
}
|
||||
}
|
||||
switch (message.say) {
|
||||
case "task":
|
||||
return "📋"
|
||||
case "error":
|
||||
return "❌"
|
||||
case "text":
|
||||
return "💬"
|
||||
case "reasoning":
|
||||
return "🧠"
|
||||
case "completion_result":
|
||||
return "✅"
|
||||
case "user_feedback":
|
||||
return "👤"
|
||||
case "command":
|
||||
case "command_output":
|
||||
return "⚙️ "
|
||||
case "tool":
|
||||
return "🔧"
|
||||
case "browser_action":
|
||||
case "browser_action_launch":
|
||||
case "browser_action_result":
|
||||
return "🌐"
|
||||
case "mcp_server_request_started":
|
||||
case "mcp_server_response":
|
||||
return "🔌"
|
||||
case "api_req_started":
|
||||
case "api_req_finished":
|
||||
return "🔄"
|
||||
case "checkpoint_created":
|
||||
return "💾"
|
||||
case "info":
|
||||
return "ℹ️ "
|
||||
default:
|
||||
return " "
|
||||
} else {
|
||||
switch (message.say) {
|
||||
case "task":
|
||||
return "📋"
|
||||
case "error":
|
||||
return "❌"
|
||||
case "text":
|
||||
return "💬"
|
||||
case "reasoning":
|
||||
return "🧠"
|
||||
case "completion_result":
|
||||
return "✅"
|
||||
case "user_feedback":
|
||||
return "👤"
|
||||
case "command":
|
||||
case "command_output":
|
||||
return "⚙️ "
|
||||
case "tool":
|
||||
return "🔧"
|
||||
case "browser_action":
|
||||
case "browser_action_launch":
|
||||
case "browser_action_result":
|
||||
return "🌐"
|
||||
case "mcp_server_request_started":
|
||||
case "mcp_server_response":
|
||||
return "🔌"
|
||||
case "api_req_started":
|
||||
case "api_req_finished":
|
||||
return "🔄"
|
||||
case "checkpoint_created":
|
||||
return "💾"
|
||||
case "info":
|
||||
return "ℹ️ "
|
||||
default:
|
||||
return " "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a ClineMessage for terminal display
|
||||
*/
|
||||
export function formatMessage(message: ClineMessage, verbose = false): string {
|
||||
export function formatMessage(message: ClineMessage, verbose: boolean = false): string {
|
||||
const icon = getMessageIcon(message)
|
||||
const timestamp = formatTimestamp(message.ts)
|
||||
const lines: string[] = []
|
||||
@@ -243,7 +244,7 @@ function formatSayMessage(message: ClineMessage, prefix: string, verbose: boolea
|
||||
|
||||
case "command_output":
|
||||
const output = message.text || ""
|
||||
const truncated = output.length > 500 ? `${output.substring(0, 500)}...` : output
|
||||
const truncated = output.length > 500 ? output.substring(0, 500) + "..." : output
|
||||
return `${prefix} ${style.dim("Output:")} ${truncated}`
|
||||
|
||||
case "tool":
|
||||
@@ -288,7 +289,7 @@ function formatSayMessage(message: ClineMessage, prefix: string, verbose: boolea
|
||||
/**
|
||||
* Display a horizontal separator
|
||||
*/
|
||||
export function separator(char = "─", width = 60): string {
|
||||
export function separator(char: string = "─", width: number = 60): string {
|
||||
return style.dim(char.repeat(width))
|
||||
}
|
||||
|
||||
@@ -308,7 +309,7 @@ export function taskHeader(taskId: string, task?: string): string {
|
||||
/**
|
||||
* Format the current state for display
|
||||
*/
|
||||
export function formatState(state: ExtensionState, verbose = false): string {
|
||||
export function formatState(state: ExtensionState, verbose: boolean = false): string {
|
||||
const lines: string[] = []
|
||||
|
||||
if (state.currentTaskItem) {
|
||||
@@ -319,7 +320,7 @@ export function formatState(state: ExtensionState, verbose = false): string {
|
||||
if (state.clineMessages && state.clineMessages.length > 0) {
|
||||
const messagesToShow = verbose
|
||||
? state.clineMessages
|
||||
: state.clineMessages.filter((_m) => {
|
||||
: state.clineMessages.filter((m) => {
|
||||
// Filter out noisy messages in non-verbose mode
|
||||
// if (m.say === "api_req_started" || m.say === "api_req_finished") return false
|
||||
return true
|
||||
@@ -343,7 +344,7 @@ export class Spinner {
|
||||
private frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
||||
private frameIndex = 0
|
||||
private interval: NodeJS.Timeout | null = null
|
||||
private message = ""
|
||||
private message: string = ""
|
||||
|
||||
start(message: string) {
|
||||
this.message = message
|
||||
@@ -366,7 +367,7 @@ export class Spinner {
|
||||
if (finalMessage) {
|
||||
process.stdout.write(`\r${style.success("✓")} ${finalMessage}\n`)
|
||||
} else {
|
||||
process.stdout.write(`\r${" ".repeat(this.message.length + 4)}\r`)
|
||||
process.stdout.write("\r" + " ".repeat(this.message.length + 4) + "\r")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +392,7 @@ export function clearLine() {
|
||||
/**
|
||||
* Move cursor up n lines
|
||||
*/
|
||||
export function cursorUp(n = 1) {
|
||||
export function cursorUp(n: number = 1) {
|
||||
process.stdout.write(`\x1b[${n}A`)
|
||||
}
|
||||
|
||||
@@ -443,7 +444,7 @@ export async function promptUser(question: string): Promise<string> {
|
||||
})
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rl.question(`${style.info(question)} `, (answer: string) => {
|
||||
rl.question(style.info(question) + " ", (answer: string) => {
|
||||
rl.close()
|
||||
resolve(answer.trim())
|
||||
})
|
||||
@@ -465,7 +466,7 @@ export async function promptConfirmation(question: string): Promise<boolean> {
|
||||
export function setTerminalTitle(title: string): void {
|
||||
if (process.stdout.isTTY) {
|
||||
const maxLength = 80
|
||||
const truncated = title.length > maxLength ? `${title.slice(0, maxLength)}...` : title
|
||||
const truncated = title.length > maxLength ? title.slice(0, maxLength) + "..." : title
|
||||
process.stdout.write(`\x1b]0;${truncated}\x07`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,9 +183,9 @@ export async function listWorkspaceFiles(workspacePath: string, limit = 5000): P
|
||||
|
||||
function countGaps(positions: Iterable<number>): number {
|
||||
let gaps = 0
|
||||
let prev = Number.NEGATIVE_INFINITY
|
||||
let prev = -Infinity
|
||||
for (const pos of positions) {
|
||||
if (prev !== Number.NEGATIVE_INFINITY && pos - prev > 1) {
|
||||
if (prev !== -Infinity && pos - prev > 1) {
|
||||
gaps++
|
||||
}
|
||||
prev = pos
|
||||
@@ -255,5 +255,5 @@ export function insertMention(text: string, atIndex: number, filePath: string):
|
||||
// Ensure path starts with / for proper mention format
|
||||
const normalizedPath = filePath.startsWith("/") ? filePath : `/${filePath}`
|
||||
const mention = normalizedPath.includes(" ") ? `@"${normalizedPath}"` : `@${normalizedPath}`
|
||||
return `${text.slice(0, atIndex) + mention} ${text.slice(end).trimStart()}`
|
||||
return text.slice(0, atIndex) + mention + " " + text.slice(end).trimStart()
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
|
||||
|
||||
// JSON mode: stream all messages to stdout (existing behavior)
|
||||
if (jsonOutput) {
|
||||
process.stdout.write(`${JSON.stringify(message)}\n`)
|
||||
process.stdout.write(JSON.stringify(message) + "\n")
|
||||
} else {
|
||||
handleMessageForPipeMode(message, verbose || false)
|
||||
}
|
||||
@@ -119,7 +119,7 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
|
||||
} catch (error) {
|
||||
if (jsonOutput) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ type: "error", message: error instanceof Error ? error.message : String(error) })}\n`,
|
||||
JSON.stringify({ type: "error", message: error instanceof Error ? error.message : String(error) }) + "\n",
|
||||
)
|
||||
} else {
|
||||
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`)
|
||||
@@ -164,7 +164,7 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error)
|
||||
if (jsonOutput) {
|
||||
process.stdout.write(`${JSON.stringify({ type: "error", message: errMsg })}\n`)
|
||||
process.stdout.write(JSON.stringify({ type: "error", message: errMsg }) + "\n")
|
||||
} else {
|
||||
process.stderr.write(`Error: ${errMsg}\n`)
|
||||
}
|
||||
@@ -180,7 +180,7 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
|
||||
.sort(([aTs], [bTs]) => aTs - bTs)
|
||||
.map(([_, msg]) => msg)
|
||||
.at(-1)
|
||||
process.stdout.write(`${msg}\n`)
|
||||
process.stdout.write(msg + "\n")
|
||||
}
|
||||
|
||||
return !hasError
|
||||
|
||||
@@ -76,30 +76,30 @@ export function printSessionSummary(): void {
|
||||
"├─────────────────────────────────────────────────────────┤",
|
||||
`│ ${GRAY}Session ID:${RESET} ${stats.sessionId.padEnd(42)}│`,
|
||||
`│ ${GRAY}Session Time:${RESET} ${sessionTimeStr.padEnd(42)}│`,
|
||||
`${`│ ${GRAY}Tool Calls:${RESET} ${stats.totalToolCalls} ( ${GREEN}✓ ${stats.successfulToolCalls}${RESET} ${RED}✗ ${stats.failedToolCalls}${RESET} )`.padEnd(
|
||||
`│ ${GRAY}Tool Calls:${RESET} ${stats.totalToolCalls} ( ${GREEN}✓ ${stats.successfulToolCalls}${RESET} ${RED}✗ ${stats.failedToolCalls}${RESET} )`.padEnd(
|
||||
70,
|
||||
)}│`,
|
||||
`${`│ ${GRAY}Success Rate:${RESET} ${session.getSuccessRate().toFixed(1)}%`.padEnd(60)}│`,
|
||||
) + "│",
|
||||
`│ ${GRAY}Success Rate:${RESET} ${session.getSuccessRate().toFixed(1)}%`.padEnd(60) + "│",
|
||||
"├─────────────────────────────────────────────────────────┤",
|
||||
`│ ${BOLD}Performance${RESET} │`,
|
||||
`│ ${GRAY}Wall Time:${RESET} ${formatDuration(wallTimeMs).padEnd(42)}│`,
|
||||
`│ ${GRAY}Agent Active:${RESET} ${formatDuration(agentActiveMs).padEnd(42)}│`,
|
||||
`${`│ ${GRAY} » API Time:${RESET} ${formatDuration(stats.apiTimeMs)} ${GRAY}(${formatPercent(stats.apiTimeMs, agentActiveMs)})${RESET}`.padEnd(
|
||||
`│ ${GRAY} » API Time:${RESET} ${formatDuration(stats.apiTimeMs)} ${GRAY}(${formatPercent(stats.apiTimeMs, agentActiveMs)})${RESET}`.padEnd(
|
||||
60,
|
||||
)}│`,
|
||||
`${`│ ${GRAY} » Tool Time:${RESET} ${formatDuration(stats.toolTimeMs)} ${GRAY}(${formatPercent(stats.toolTimeMs, agentActiveMs)})${RESET}`.padEnd(
|
||||
) + "│",
|
||||
`│ ${GRAY} » Tool Time:${RESET} ${formatDuration(stats.toolTimeMs)} ${GRAY}(${formatPercent(stats.toolTimeMs, agentActiveMs)})${RESET}`.padEnd(
|
||||
60,
|
||||
)}│`,
|
||||
) + "│",
|
||||
"├─────────────────────────────────────────────────────────┤",
|
||||
`│ ${BOLD}Resources${RESET} │`,
|
||||
`│ ${GRAY}Memory (RSS):${RESET} ${formatBytes(stats.resources.rss).padEnd(42)}│`,
|
||||
`│ ${GRAY}Peak Memory:${RESET} ${formatBytes(stats.peakMemoryBytes).padEnd(42)}│`,
|
||||
`${`│ ${GRAY}Heap Used:${RESET} ${formatBytes(stats.resources.heapUsed)} ${GRAY}/ ${formatBytes(stats.resources.heapTotal)}${RESET}`.padEnd(
|
||||
`│ ${GRAY}Heap Used:${RESET} ${formatBytes(stats.resources.heapUsed)} ${GRAY}/ ${formatBytes(stats.resources.heapTotal)}${RESET}`.padEnd(
|
||||
60,
|
||||
)}│`,
|
||||
`${`│ ${GRAY}CPU Time:${RESET} ${formatDuration(stats.resources.userCpuMs + stats.resources.systemCpuMs)} ${GRAY}(user: ${formatDuration(stats.resources.userCpuMs)}, sys: ${formatDuration(stats.resources.systemCpuMs)})${RESET}`.padEnd(
|
||||
) + "│",
|
||||
`│ ${GRAY}CPU Time:${RESET} ${formatDuration(stats.resources.userCpuMs + stats.resources.systemCpuMs)} ${GRAY}(user: ${formatDuration(stats.resources.userCpuMs)}, sys: ${formatDuration(stats.resources.systemCpuMs)})${RESET}`.padEnd(
|
||||
60,
|
||||
)}│`,
|
||||
) + "│",
|
||||
"└─────────────────────────────────────────────────────────┘",
|
||||
"",
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface VisibleWindow<T> {
|
||||
* Centers the selected item in the visible window when possible.
|
||||
* Returns the visible items and the start index for selection tracking.
|
||||
*/
|
||||
export function getVisibleWindow<T>(items: T[], selectedIndex: number, maxVisible = 5): VisibleWindow<T> {
|
||||
export function getVisibleWindow<T>(items: T[], selectedIndex: number, maxVisible: number = 5): VisibleWindow<T> {
|
||||
if (items.length <= maxVisible) {
|
||||
return { items, startIndex: 0 }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export function emitTaskStartedMessage(taskId: string, jsonOutput: boolean): void {
|
||||
if (jsonOutput) {
|
||||
process.stdout.write(`${JSON.stringify({ type: "task_started", taskId })}\n`)
|
||||
process.stdout.write(JSON.stringify({ type: "task_started", taskId }) + "\n")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
export async function waitFor<T>(
|
||||
condition: () => T | undefined | null,
|
||||
timeoutMs: number,
|
||||
pollIntervalMs = 100,
|
||||
pollIntervalMs: number = 100,
|
||||
): Promise<T | undefined> {
|
||||
// Check immediately first
|
||||
const immediate = condition()
|
||||
|
||||
@@ -259,7 +259,7 @@ function parseVersion(version: string): ParsedVersion {
|
||||
return {
|
||||
base: nightlyMatch[1].split(".").map(Number),
|
||||
isNightly: true,
|
||||
timestamp: Number.parseInt(nightlyMatch[2], 10),
|
||||
timestamp: parseInt(nightlyMatch[2], 10),
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -73,8 +73,6 @@ Cline stores configuration in `~/.cline/data/`:
|
||||
├── data/ # Configuration directory
|
||||
│ ├── globalState.json # Global settings
|
||||
│ ├── secrets.json # API keys (encrypted)
|
||||
│ ├── settings/ # Settings files
|
||||
│ │ └── cline_mcp_settings.json # MCP server configuration
|
||||
│ ├── workspace/ # Workspace-specific state
|
||||
│ └── tasks/ # Task history and data
|
||||
└── log/ # Log files
|
||||
@@ -174,46 +172,6 @@ cline --config ~/.cline-work "review this PR"
|
||||
cline --config ~/.cline-personal "help me with this side project"
|
||||
```
|
||||
|
||||
## MCP Server Configuration
|
||||
|
||||
Cline CLI supports [MCP (Model Context Protocol)](/mcp/mcp-overview) servers, giving you access to external tools and data sources directly from the terminal. The CLI uses the same MCP configuration format as the VS Code extension.
|
||||
|
||||
### Setting Up MCP Servers
|
||||
|
||||
To configure MCP servers for the CLI, create or edit the settings file at:
|
||||
|
||||
```
|
||||
~/.cline/data/settings/cline_mcp_settings.json
|
||||
```
|
||||
|
||||
The file uses the same JSON format as the VS Code extension:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-server": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/server.js"],
|
||||
"env": {
|
||||
"API_KEY": "your_api_key"
|
||||
},
|
||||
"alwaysAllow": ["tool1", "tool2"],
|
||||
"disabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For the full configuration reference including STDIO and SSE transport types, see [Adding and Configuring MCP Servers](/mcp/adding-and-configuring-servers).
|
||||
|
||||
<Note>
|
||||
The CLI does not yet have a `/mcp` slash command for managing MCP servers interactively. For now, you'll need to edit the `cline_mcp_settings.json` file directly.
|
||||
</Note>
|
||||
|
||||
### Custom Config Directory
|
||||
|
||||
If you use the `CLINE_DIR` environment variable or `--config` flag, the MCP settings file will be located at `<your-config-dir>/data/settings/cline_mcp_settings.json` instead.
|
||||
|
||||
## Configuration for Local Providers
|
||||
|
||||
### Ollama
|
||||
|
||||
@@ -207,15 +207,6 @@ Chains multiple Cline invocations together for creative multi-step workflows.
|
||||
| Session summary | ✓ | - |
|
||||
| JSON output | - | `--json` |
|
||||
| Piped input | - | ✓ |
|
||||
| [MCP servers](/cline-cli/configuration#mcp-server-configuration) | ✓ | ✓ |
|
||||
|
||||
## MCP Server Support
|
||||
|
||||
Cline CLI supports [MCP (Model Context Protocol)](/mcp/mcp-overview) servers, the same extensibility system available in the VS Code extension. MCP servers give Cline access to external tools and data sources, from databases and APIs to browser automation and project management.
|
||||
|
||||
To use MCP servers with the CLI, add your server configuration to `~/.cline/data/settings/cline_mcp_settings.json`. The format is identical to the VS Code extension.
|
||||
|
||||
[Configure MCP servers for the CLI →](/cline-cli/configuration#mcp-server-configuration)
|
||||
|
||||
## Learn More
|
||||
|
||||
|
||||
@@ -70,8 +70,6 @@ Cline does not come with any pre-installed MCP servers. You'll need to find and
|
||||
|
||||
## Integration with Cline
|
||||
|
||||
MCP servers work with both the **Cline VS Code extension** and the **[Cline CLI](/cline-cli/overview)**. If you use the CLI, see [MCP Server Configuration for the CLI](/cline-cli/configuration#mcp-server-configuration) to get set up.
|
||||
|
||||
Cline simplifies the building and use of MCP servers through its AI capabilities.
|
||||
|
||||
### Building MCP Servers
|
||||
|
||||
@@ -19,7 +19,7 @@ Google Gemini is Google's family of multimodal AI models, offering some of the l
|
||||
Cline supports the following Google Gemini models:
|
||||
|
||||
#### Gemini 3 Series (Latest)
|
||||
- `gemini-3.1-pro-preview` (Default) - Latest pro model with 1M context, thinking support, and tiered pricing ($2.00-$4.00/M input)
|
||||
- `gemini-3-pro-preview` (Default) - Latest pro model with 1M context, thinking support, and tiered pricing ($2.00-$4.00/M input)
|
||||
- `gemini-3-flash-preview` - Fast model with 1M context and thinking level support ($0.30-$0.50/M input)
|
||||
|
||||
#### Gemini 2.5 Series
|
||||
|
||||
@@ -14,6 +14,29 @@ SambaNova provides fast AI inference on custom-built hardware, hosting popular o
|
||||
3. **Create a Key:** Generate a new API key.
|
||||
4. **Copy the Key:** Copy the API key immediately and store it securely.
|
||||
|
||||
### Supported Models
|
||||
|
||||
Cline supports the following SambaNova models:
|
||||
|
||||
#### Meta Llama Models
|
||||
- `Llama-4-Maverick-17B-128E-Instruct` - Llama 4 Maverick with vision support ($0.63/$1.80 per 1M tokens)
|
||||
- `Llama-4-Scout-17B-16E-Instruct` - Llama 4 Scout ($0.40/$0.70 per 1M tokens)
|
||||
- `Meta-Llama-3.3-70B-Instruct` (Default) - Versatile 70B model with 128K context ($0.60/$1.20 per 1M tokens)
|
||||
- `Meta-Llama-3.1-405B-Instruct` - Largest Llama model ($5.00/$10.00 per 1M tokens)
|
||||
- `Meta-Llama-3.1-8B-Instruct` - Compact 8B model ($0.10/$0.20 per 1M tokens)
|
||||
- `Meta-Llama-3.2-1B-Instruct` - Ultra-compact 1B model ($0.04/$0.08 per 1M tokens)
|
||||
- `Meta-Llama-3.2-3B-Instruct` - Small 3B model ($0.08/$0.16 per 1M tokens)
|
||||
|
||||
#### DeepSeek Models
|
||||
- `DeepSeek-R1` - Reasoning model ($5.00/$7.00 per 1M tokens)
|
||||
- `DeepSeek-R1-Distill-Llama-70B` - Distilled reasoning model ($0.70/$1.40 per 1M tokens)
|
||||
- `DeepSeek-V3-0324` - General-purpose model ($3.00/$4.50 per 1M tokens)
|
||||
- `DeepSeek-V3.1` - Latest DeepSeek with hybrid reasoning ($3.00/$4.50 per 1M tokens)
|
||||
|
||||
#### Qwen Models
|
||||
- `Qwen3-32B` - Dense 32B model ($0.40/$0.80 per 1M tokens)
|
||||
- `QwQ-32B` - Reasoning-focused Qwen model ($0.50/$1.00 per 1M tokens)
|
||||
|
||||
### Configuration in Cline
|
||||
|
||||
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
|
||||
|
||||
+9
-7
@@ -14,7 +14,8 @@ td,
|
||||
th,
|
||||
span:not(code *),
|
||||
div:not(code *):not(pre *) {
|
||||
font-family: "Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
font-family:
|
||||
"Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif !important;
|
||||
}
|
||||
|
||||
/* Ensure code blocks use Geist Mono */
|
||||
@@ -24,12 +25,12 @@ pre,
|
||||
pre code,
|
||||
code *,
|
||||
pre * {
|
||||
font-family: "Geist Mono", "Monaco", "Courier New", monospace;
|
||||
font-family: "Geist Mono", "Monaco", "Courier New", monospace !important;
|
||||
}
|
||||
|
||||
/* Make h1 titles lighter in font weight */
|
||||
h1 {
|
||||
font-weight: 600;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* Keep headings and images at full opacity */
|
||||
@@ -40,8 +41,9 @@ h4,
|
||||
h5,
|
||||
h6,
|
||||
img {
|
||||
opacity: 1;
|
||||
font-family: "Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
opacity: 1 !important;
|
||||
font-family:
|
||||
"Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif !important;
|
||||
}
|
||||
|
||||
/* Also apply to any h1 elements within content areas */
|
||||
@@ -49,7 +51,7 @@ img {
|
||||
.markdown h1,
|
||||
article h1,
|
||||
main h1 {
|
||||
font-weight: 500;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
/* JetBrains logo visibility fix for dark mode */
|
||||
@@ -87,5 +89,5 @@ img[alt="JetBrains logo"]:hover {
|
||||
|
||||
/* Reduce list margin-top */
|
||||
.steps {
|
||||
margin-top: 5px;
|
||||
margin-top: 5px !important;
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import * as esbuild from "esbuild"
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const production = process.argv.includes("--production") || process.env.IS_DEBUG_BUILD === "false"
|
||||
const production = process.argv.includes("--production") || process.env["IS_DEBUG_BUILD"] === "false"
|
||||
const watch = process.argv.includes("--watch")
|
||||
const standalone = process.argv.includes("--standalone")
|
||||
const e2eBuild = process.argv.includes("--e2e-build")
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.66.0",
|
||||
"version": "3.65.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.66.0",
|
||||
"version": "3.65.0",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
".",
|
||||
|
||||
+3
-8
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.66.0",
|
||||
"version": "3.65.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"workspaces": [
|
||||
".",
|
||||
@@ -414,9 +414,8 @@
|
||||
"lint:proto": "bash ./scripts/proto-lint.sh",
|
||||
"format": "biome format --changed --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error",
|
||||
"format:fix": "biome check --changed --no-errors-on-unmatched --files-ignore-unknown=true --write",
|
||||
"format:all": "biome check --files-ignore-unknown=true --diagnostic-level=error",
|
||||
"fix:all": "biome check --no-errors-on-unmatched --files-ignore-unknown=true --write --diagnostic-level=error --unsafe",
|
||||
"ci:check-all": "npx npm-run-all -p check-types lint format:all",
|
||||
"ci:check-all": "npx npm-run-all -p check-types lint format",
|
||||
"ci:build": "npm run protos && npm run build:webview && node esbuild.mjs && npm run compile-tests",
|
||||
"pretest": "npm run compile && npm run compile-tests && npm run compile-standalone && npm run lint",
|
||||
"test": "npx npm-run-all test:unit test:integration",
|
||||
@@ -445,11 +444,7 @@
|
||||
"docs:rename-file": "cd docs && npm run rename",
|
||||
"report-issue": "node scripts/report-issue.js",
|
||||
"storybook": "cd webview-ui && npm run storybook",
|
||||
"cli:unlink": "cd cli && npm run unlink",
|
||||
"eval:smoke:build": "npm run cli:build && npm run cli:link",
|
||||
"eval:smoke:run": "npx tsx evals/smoke-tests/run-smoke-tests.ts",
|
||||
"eval:smoke": "npm run eval:smoke:build && npm run eval:smoke:run",
|
||||
"eval:smoke:ci": "npm run eval:smoke:build && npm run eval:smoke:run -- --trials 1 --parallel"
|
||||
"cli:unlink": "cd cli && npm run unlink"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/shared/storage/state-keys.ts": [
|
||||
|
||||
@@ -19,8 +19,6 @@ service ModelsService {
|
||||
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
|
||||
// Refreshes and returns OpenRouter models
|
||||
rpc refreshOpenRouterModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns recommended and free Cline models
|
||||
rpc refreshClineRecommendedModelsRpc(EmptyRequest) returns (ClineRecommendedModelsResponse);
|
||||
// Refreshes and returns Hugging Face models
|
||||
rpc refreshHuggingFaceModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns OpenAI models
|
||||
@@ -115,18 +113,6 @@ message OpenRouterCompatibleModelInfo {
|
||||
map<string, OpenRouterModelInfo> models = 1;
|
||||
}
|
||||
|
||||
message ClineRecommendedModel {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string description = 3;
|
||||
repeated string tags = 4;
|
||||
}
|
||||
|
||||
message ClineRecommendedModelsResponse {
|
||||
repeated ClineRecommendedModel recommended = 1;
|
||||
repeated ClineRecommendedModel free = 2;
|
||||
}
|
||||
|
||||
// Request for fetching OpenAI models
|
||||
message OpenAiModelsRequest {
|
||||
Metadata metadata = 1;
|
||||
@@ -462,7 +448,6 @@ enum ApiFormat {
|
||||
OPENAI_CHAT = 2;
|
||||
R1_CHAT = 3;
|
||||
OPENAI_RESPONSES = 4;
|
||||
OPENAI_RESPONSES_WEBSOCKET_MODE = 5;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
|
||||
@@ -104,7 +104,7 @@ message Secrets {
|
||||
optional string oca_refresh_token = 42;
|
||||
optional string mcp_o_auth_secrets = 43;
|
||||
optional string cline_api_key = 44;
|
||||
optional string openai_codex_oauth_credentials = 48;
|
||||
optional string openai_codex_oauth_credentials = 47;
|
||||
}
|
||||
|
||||
// NOTE: Add new fields under API_HANDLER_SETTINGS_FIELDS or USER_SETTINGS_FIELDS
|
||||
|
||||
@@ -241,7 +241,7 @@ function normalizeProviderName(providerPart) {
|
||||
function applySpecialCaseMappings(providerApiKeyMap, apiSecretsFields, assignedFields) {
|
||||
// Special case 1: Bedrock needs AWS fields (if not already assigned)
|
||||
const awsFields = ["awsAccessKey", "awsSecretKey", "awsRegion"]
|
||||
const bedrockFields = providerApiKeyMap.bedrock || []
|
||||
const bedrockFields = providerApiKeyMap["bedrock"] || []
|
||||
|
||||
for (const field of awsFields) {
|
||||
if (apiSecretsFields.fieldNames.includes(field) && !bedrockFields.includes(field)) {
|
||||
@@ -257,19 +257,19 @@ function applySpecialCaseMappings(providerApiKeyMap, apiSecretsFields, assignedF
|
||||
}
|
||||
|
||||
if (bedrockFields.length > 0) {
|
||||
providerApiKeyMap.bedrock = bedrockFields
|
||||
providerApiKeyMap["bedrock"] = bedrockFields
|
||||
}
|
||||
|
||||
// Special case 2: Vertex needs project ID and region
|
||||
if (providerApiKeyMap.vertex) {
|
||||
if (providerApiKeyMap["vertex"]) {
|
||||
// Vertex typically uses application default credentials,
|
||||
// but requires project ID and region configuration
|
||||
// These are already captured if they exist in ApiHandlerSecrets
|
||||
}
|
||||
|
||||
// Special case 3: SAP AI Core multi-key authentication
|
||||
if (providerApiKeyMap.sapaicore) {
|
||||
const sapFields = providerApiKeyMap.sapaicore
|
||||
if (providerApiKeyMap["sapaicore"]) {
|
||||
const sapFields = providerApiKeyMap["sapaicore"]
|
||||
const requiredSapFields = ["sapAiCoreClientId", "sapAiCoreClientSecret"]
|
||||
|
||||
for (const field of requiredSapFields) {
|
||||
|
||||
@@ -106,7 +106,7 @@ async function parseServicesWithFiles(protoDir, protoFiles) {
|
||||
return services
|
||||
}
|
||||
|
||||
function _upperFirst(s) {
|
||||
function upperFirst(s) {
|
||||
return s.length ? s[0].toUpperCase() + s.slice(1) : s
|
||||
}
|
||||
|
||||
@@ -273,8 +273,8 @@ async function generateServiceClientsPy(outDir, services) {
|
||||
:return: iterator of ${aliasPb2}.${respTypeName}
|
||||
"""
|
||||
return self._stub.${m.name}(req)`
|
||||
}
|
||||
return `
|
||||
} else {
|
||||
return `
|
||||
def ${m.name}(self, req):
|
||||
"""
|
||||
Unary RPC.
|
||||
@@ -282,6 +282,7 @@ async function generateServiceClientsPy(outDir, services) {
|
||||
:return: ${aliasPb2}.${respTypeName}
|
||||
"""
|
||||
return self._stub.${m.name}(req)`
|
||||
}
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ async function main() {
|
||||
}
|
||||
|
||||
// Print summary
|
||||
console.log(`\n${"=".repeat(50)}`)
|
||||
console.log("\n" + "=".repeat(50))
|
||||
console.log("📊 Summary:")
|
||||
console.log("=".repeat(50))
|
||||
|
||||
|
||||
@@ -125,9 +125,9 @@ function generateExternalClientSetup(serviceName, serviceDefinition) {
|
||||
return ` ${methodName}(request: ${requestType}): Promise<${responseType}> {
|
||||
return this.makeRequest((client) => client.${methodName}(request))
|
||||
}`
|
||||
}
|
||||
// Generate streaming method
|
||||
return ` ${methodName}(
|
||||
} else {
|
||||
// Generate streaming method
|
||||
return ` ${methodName}(
|
||||
request: ${requestType},
|
||||
callbacks: StreamingCallbacks<${responseType}>,
|
||||
): () => void {
|
||||
@@ -150,6 +150,7 @@ function generateExternalClientSetup(serviceName, serviceDefinition) {
|
||||
abortController.abort()
|
||||
}
|
||||
}\n`
|
||||
}
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
@@ -221,8 +222,9 @@ function generateVscodeClientImplementation(serviceName, serviceDefinition) {
|
||||
const isStreamingResponse = methodDef.responseStream
|
||||
if (!isStreamingResponse) {
|
||||
return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName})`
|
||||
} else {
|
||||
return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName}, { isStreaming: true })`
|
||||
}
|
||||
return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName}, { isStreaming: true })`
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
|
||||
@@ -157,8 +157,8 @@ async function generateStandaloneProtobusServiceSetup(protobusServices) {
|
||||
handlerSetup.push(` server.addService(cline.${name}Service, {`)
|
||||
for (const [rpcName, rpc] of Object.entries(def.service)) {
|
||||
imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${rpcName}"`)
|
||||
const requestType = `cline.${rpc.requestType.type.name}`
|
||||
const responseType = `cline.${rpc.responseType.type.name}`
|
||||
const requestType = "cline." + rpc.requestType.type.name
|
||||
const responseType = "cline." + rpc.responseType.type.name
|
||||
if (rpc.requestStream) {
|
||||
throw new Error("Request streaming is not supported")
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ function replaceMessage(protoContent, messageName, newMessageContent) {
|
||||
}
|
||||
// Message doesn't exist, append before the first message or at end
|
||||
console.warn(`Warning: ${messageName} message not found in proto file, appending`)
|
||||
return `${protoContent}\n\n${newMessageContent}`
|
||||
return protoContent + "\n\n" + newMessageContent
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -56,7 +56,6 @@ const log = {
|
||||
const config = {
|
||||
// The name and display name for the nightly version
|
||||
nightlyName: "cline-nightly",
|
||||
originalName: "claude-dev",
|
||||
nightlyDisplayName: "Cline (Nightly)",
|
||||
projectRoot: path.join(__dirname, ".."),
|
||||
get packageJsonPath() {
|
||||
@@ -71,15 +70,6 @@ const config = {
|
||||
get vsixPath() {
|
||||
return path.join(this.distDir, "cline-nightly.vsix")
|
||||
},
|
||||
get nodeModulesPath() {
|
||||
return path.join(this.projectRoot, "node_modules")
|
||||
},
|
||||
get originalWorkspaceLinkPath() {
|
||||
return path.join(this.nodeModulesPath, this.originalName)
|
||||
},
|
||||
get nightlyWorkspaceLinkPath() {
|
||||
return path.join(this.nodeModulesPath, this.nightlyName)
|
||||
},
|
||||
}
|
||||
|
||||
// Utility class for managing the publish process
|
||||
@@ -87,31 +77,6 @@ class NightlyPublisher {
|
||||
constructor() {
|
||||
this.originalPackageJson = null
|
||||
this.hasBackup = false
|
||||
this.didRenameWorkspaceLink = false
|
||||
this.didCreateNightlyWorkspaceLink = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve symlink target to an absolute path.
|
||||
*/
|
||||
resolveSymlinkTarget(linkPath) {
|
||||
const target = fs.readlinkSync(linkPath)
|
||||
return path.resolve(path.dirname(linkPath), target)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a path is the expected workspace self-link to project root.
|
||||
*/
|
||||
isExpectedWorkspaceSelfLink(linkPath) {
|
||||
try {
|
||||
if (!fs.lstatSync(linkPath).isSymbolicLink()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.resolveSymlinkTarget(linkPath) === path.resolve(config.projectRoot)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,98 +145,6 @@ class NightlyPublisher {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep workspace self-link consistent with package name during nightly packaging.
|
||||
*
|
||||
* The repo root is a workspace package ("."). When npm installs dependencies,
|
||||
* it creates a self-link at node_modules/<package-name>. Nightly packaging
|
||||
* changes package.json name from "claude-dev" to "cline-nightly". If we don't
|
||||
* align this link, vsce's dependency detection (`npm list --production`) fails
|
||||
* with ELSPROBLEMS (missing cline-nightly + extraneous claude-dev).
|
||||
*/
|
||||
reconcileWorkspaceSelfLinkForNightly() {
|
||||
const originalPath = config.originalWorkspaceLinkPath
|
||||
const nightlyPath = config.nightlyWorkspaceLinkPath
|
||||
|
||||
if (!fs.existsSync(config.nodeModulesPath)) {
|
||||
log.warn("node_modules not found, skipping workspace self-link reconciliation")
|
||||
return
|
||||
}
|
||||
|
||||
if (fs.existsSync(nightlyPath)) {
|
||||
if (!this.isExpectedWorkspaceSelfLink(nightlyPath)) {
|
||||
throw new Error(
|
||||
`Refusing to continue: unexpected path at ${nightlyPath}. Expected a workspace symlink to ${config.projectRoot}`,
|
||||
)
|
||||
}
|
||||
|
||||
log.info("Nightly workspace self-link already exists")
|
||||
return
|
||||
}
|
||||
|
||||
if (fs.existsSync(originalPath)) {
|
||||
if (!this.isExpectedWorkspaceSelfLink(originalPath)) {
|
||||
throw new Error(
|
||||
`Refusing to continue: unexpected path at ${originalPath}. Expected a workspace symlink to ${config.projectRoot}`,
|
||||
)
|
||||
}
|
||||
|
||||
log.info(`Renaming workspace self-link: ${config.originalName} -> ${config.nightlyName}`)
|
||||
fs.renameSync(originalPath, nightlyPath)
|
||||
this.didRenameWorkspaceLink = true
|
||||
return
|
||||
}
|
||||
|
||||
// In some environments npm may not have created the workspace self-link yet.
|
||||
// Create it explicitly so `npm list --production` can resolve the renamed
|
||||
// package name during vsce dependency detection.
|
||||
log.warn("Original workspace self-link not found, creating nightly workspace self-link")
|
||||
fs.symlinkSync(config.projectRoot, nightlyPath, "dir")
|
||||
|
||||
if (!this.isExpectedWorkspaceSelfLink(nightlyPath)) {
|
||||
throw new Error(`Failed to create expected workspace symlink at ${nightlyPath}`)
|
||||
}
|
||||
|
||||
this.didCreateNightlyWorkspaceLink = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore workspace self-link after packaging.
|
||||
*/
|
||||
restoreWorkspaceSelfLink() {
|
||||
if (!this.didRenameWorkspaceLink && !this.didCreateNightlyWorkspaceLink) {
|
||||
return
|
||||
}
|
||||
|
||||
const originalPath = config.originalWorkspaceLinkPath
|
||||
const nightlyPath = config.nightlyWorkspaceLinkPath
|
||||
|
||||
if (fs.existsSync(nightlyPath) && !fs.existsSync(originalPath)) {
|
||||
if (this.didRenameWorkspaceLink) {
|
||||
if (!this.isExpectedWorkspaceSelfLink(nightlyPath)) {
|
||||
throw new Error(
|
||||
`Refusing to restore: unexpected path at ${nightlyPath}. Expected a workspace symlink to ${config.projectRoot}`,
|
||||
)
|
||||
}
|
||||
|
||||
log.info(`Restoring workspace self-link: ${config.nightlyName} -> ${config.originalName}`)
|
||||
fs.renameSync(nightlyPath, originalPath)
|
||||
} else if (this.didCreateNightlyWorkspaceLink) {
|
||||
if (!this.isExpectedWorkspaceSelfLink(nightlyPath)) {
|
||||
throw new Error(
|
||||
`Refusing to remove: unexpected path at ${nightlyPath}. Expected a workspace symlink to ${config.projectRoot}`,
|
||||
)
|
||||
}
|
||||
|
||||
log.info(`Removing temporary workspace self-link: ${config.nightlyName}`)
|
||||
fs.unlinkSync(nightlyPath)
|
||||
}
|
||||
}
|
||||
|
||||
this.didRenameWorkspaceLink = false
|
||||
this.didCreateNightlyWorkspaceLink = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate new version with timestamp
|
||||
* Format: major.minor.timestamp
|
||||
@@ -427,9 +300,6 @@ class NightlyPublisher {
|
||||
// Step 3: Update package.json
|
||||
const newVersion = this.updatePackageJson()
|
||||
|
||||
// Step 3.5: Keep npm workspace self-link aligned with nightly package name
|
||||
this.reconcileWorkspaceSelfLinkForNightly()
|
||||
|
||||
// Step 4: Package extension
|
||||
this.packageExtension()
|
||||
|
||||
@@ -456,9 +326,6 @@ class NightlyPublisher {
|
||||
log.error(`Publish failed: ${error.message}`)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
// Always restore workspace link first
|
||||
this.restoreWorkspaceSelfLink()
|
||||
|
||||
// Always restore package.json
|
||||
this.restorePackageJson()
|
||||
}
|
||||
@@ -469,20 +336,17 @@ class NightlyPublisher {
|
||||
const publisher = new NightlyPublisher()
|
||||
|
||||
process.on("exit", () => {
|
||||
publisher.restoreWorkspaceSelfLink()
|
||||
publisher.restorePackageJson()
|
||||
})
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
log.info("\nInterrupted, cleaning up...")
|
||||
publisher.restoreWorkspaceSelfLink()
|
||||
publisher.restorePackageJson()
|
||||
process.exit(130)
|
||||
})
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
log.info("\nTerminated, cleaning up...")
|
||||
publisher.restoreWorkspaceSelfLink()
|
||||
publisher.restorePackageJson()
|
||||
process.exit(143)
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@ const collectSystemInfo = () => {
|
||||
if (process.platform === "darwin") {
|
||||
cpuInfo = execSync("sysctl -n machdep.cpu.brand_string").toString().trim()
|
||||
memoryInfo = execSync("sysctl -n hw.memsize").toString().trim()
|
||||
memoryInfo = `${Math.round(Number.parseInt(memoryInfo, 10) / 1e9)} GB RAM`
|
||||
memoryInfo = `${Math.round(parseInt(memoryInfo) / 1e9)} GB RAM`
|
||||
} else {
|
||||
// Linux specific commands
|
||||
cpuInfo = execSync("lscpu").toString().split("\n").slice(0, 5).join("\n")
|
||||
|
||||
@@ -70,7 +70,7 @@ function createMockService<T extends grpc.UntypedServiceImplementation>(serviceN
|
||||
|
||||
case "getMachineId":
|
||||
callback(null, {
|
||||
value: `fake-machine-id-${os.hostname()}`,
|
||||
value: "fake-machine-id-" + os.hostname(),
|
||||
})
|
||||
return
|
||||
|
||||
@@ -103,7 +103,7 @@ function createMockService<T extends grpc.UntypedServiceImplementation>(serviceN
|
||||
|
||||
case "openDiff":
|
||||
callback(null, {
|
||||
diff_id: `fake-diff-${Date.now()}`,
|
||||
diff_id: "fake-diff-" + Date.now(),
|
||||
})
|
||||
return
|
||||
|
||||
|
||||
@@ -204,8 +204,8 @@ async function main() {
|
||||
const inputPath = args._[0]
|
||||
const count = Number(args.count)
|
||||
showServerLogs = Boolean(args["server-logs"])
|
||||
fix = Boolean(args.fix)
|
||||
coverage = Boolean(args.coverage)
|
||||
fix = Boolean(args["fix"])
|
||||
coverage = Boolean(args["coverage"])
|
||||
|
||||
if (!inputPath) {
|
||||
console.error(
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ClineConfigurationError, ClineEndpoint, ClineEnv, Environment } from ".
|
||||
describe("ClineEndpoint configuration", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let tempDir: string
|
||||
let _originalHomedir: typeof os.homedir
|
||||
let originalHomedir: typeof os.homedir
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox()
|
||||
@@ -20,8 +20,10 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.mkdir(path.join(tempDir, ".cline"), { recursive: true })
|
||||
|
||||
// Stub os.homedir to return our temp directory
|
||||
_originalHomedir = os.homedir
|
||||
sandbox.stub(os, "homedir").returns(tempDir)
|
||||
originalHomedir = os.homedir
|
||||
sandbox
|
||||
.stub(os, "homedir")
|
||||
.returns(tempDir)
|
||||
|
||||
// Reset the singleton state using internal method
|
||||
;(ClineEndpoint as any)._instance = null
|
||||
@@ -541,7 +543,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
|
||||
describe("bundled endpoints.json behavior", () => {
|
||||
let bundledDir: string
|
||||
let _setVscodeHostProviderMock: (mock: { extensionFsPath: string; globalStorageFsPath: string }) => void
|
||||
let setVscodeHostProviderMock: (mock: { extensionFsPath: string; globalStorageFsPath: string }) => void
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a separate directory for bundled config
|
||||
@@ -550,7 +552,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
|
||||
// Import HostProvider utilities
|
||||
const hostProviderModule = await import("../test/host-provider-test-utils")
|
||||
_setVscodeHostProviderMock = hostProviderModule.setVscodeHostProviderMock
|
||||
setVscodeHostProviderMock = hostProviderModule.setVscodeHostProviderMock
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type * as vscode from "vscode"
|
||||
import { WebviewProvider } from "./core/webview"
|
||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
|
||||
@@ -24,6 +25,8 @@ import { getBlobStoreSettingsFromEnv } from "./shared/services/worker/worker"
|
||||
import { getLatestAnnouncementId } from "./utils/announcements"
|
||||
import { arePathsEqual } from "./utils/path"
|
||||
|
||||
type SlimExtensionContext = Omit<vscode.ExtensionContext, "globalState" | "secrets" | "workspaceState">
|
||||
|
||||
/**
|
||||
* Performs intialization for Cline that is common to all platforms.
|
||||
*
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ class ClineEndpoint {
|
||||
private onPremiseConfig: EndpointsFileSchema | null = null
|
||||
private environment: Environment = Environment.production
|
||||
// Track if config came from bundled file (enterprise distribution)
|
||||
private isBundled = false
|
||||
private isBundled: boolean = false
|
||||
|
||||
private constructor() {
|
||||
// Set environment at module load. Use override if provided.
|
||||
|
||||
@@ -585,8 +585,9 @@ function reconstructWriteToFileResult(block: any, originalToolName: string, orig
|
||||
// If no final_file_content found, create a simple success message
|
||||
if (originalToolName === "write_to_file") {
|
||||
return `[apply_patch for '${filePath}'] Result:\nThe content was successfully saved to ${filePath}.\n\nThe file has been created/updated with the new content.`
|
||||
} else {
|
||||
return `[apply_patch for '${filePath}'] Result:\nThe content was successfully updated in ${filePath}.\n\nThe file has been modified.`
|
||||
}
|
||||
return `[apply_patch for '${filePath}'] Result:\nThe content was successfully updated in ${filePath}.\n\nThe file has been modified.`
|
||||
}
|
||||
|
||||
const finalContent = finalContentMatch[2]
|
||||
|
||||
@@ -1083,7 +1083,8 @@ describe("AwsBedrockHandler", () => {
|
||||
|
||||
// Capture the command passed to executeConverseStream
|
||||
let capturedCommand: any = null
|
||||
handler["executeConverseStream"] = async function* (command: any, _modelInfo: any) {
|
||||
const originalExecuteConverseStream = handler["executeConverseStream"].bind(handler)
|
||||
handler["executeConverseStream"] = async function* (command: any, modelInfo: any) {
|
||||
capturedCommand = command
|
||||
// Yield nothing — we just want to capture the command
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import "should"
|
||||
import { openRouterDefaultModelInfo } from "@shared/api"
|
||||
import sinon from "sinon"
|
||||
import { ClineHandler } from "../cline"
|
||||
|
||||
describe("ClineHandler", () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
const createAsyncIterable = (data: any[] = []) => ({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield* data
|
||||
},
|
||||
})
|
||||
|
||||
it("should handle usage-only chunks when delta is missing", async () => {
|
||||
const handler = Object.create(ClineHandler.prototype) as ClineHandler
|
||||
;(handler as any).options = {}
|
||||
const fakeClient = {
|
||||
chat: {
|
||||
completions: {
|
||||
create: sinon.stub().resolves(
|
||||
createAsyncIterable([
|
||||
{
|
||||
choices: [{}],
|
||||
usage: {
|
||||
prompt_tokens: 17,
|
||||
completion_tokens: 9,
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
sinon.stub(handler as any, "ensureClient").resolves(fakeClient as any)
|
||||
sinon.stub(handler, "getModel").returns({
|
||||
id: "openai/gpt-4o-mini",
|
||||
info: openRouterDefaultModelInfo,
|
||||
})
|
||||
|
||||
const chunks: any[] = []
|
||||
for await (const chunk of handler.createMessage("system", [{ role: "user", content: "hi" }])) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
chunks.should.deepEqual([
|
||||
{
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
inputTokens: 17,
|
||||
outputTokens: 9,
|
||||
totalCost: 0,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,55 +0,0 @@
|
||||
import "should"
|
||||
import sinon from "sinon"
|
||||
import { FireworksHandler } from "../fireworks"
|
||||
|
||||
describe("FireworksHandler", () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
const createAsyncIterable = (data: any[] = []) => ({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield* data
|
||||
},
|
||||
})
|
||||
|
||||
it("should handle usage-only chunks when delta is missing", async () => {
|
||||
const handler = new FireworksHandler({
|
||||
fireworksApiKey: "test-api-key",
|
||||
fireworksModelId: "accounts/fireworks/models/llama-v3p1-8b-instruct",
|
||||
})
|
||||
const fakeClient = {
|
||||
chat: {
|
||||
completions: {
|
||||
create: sinon.stub().resolves(
|
||||
createAsyncIterable([
|
||||
{
|
||||
choices: [{}],
|
||||
usage: {
|
||||
prompt_tokens: 19,
|
||||
completion_tokens: 4,
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
sinon.stub(handler as any, "ensureClient").returns(fakeClient as any)
|
||||
|
||||
const chunks: any[] = []
|
||||
for await (const chunk of handler.createMessage("system", [{ role: "user", content: "hi" }])) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
chunks.should.deepEqual([
|
||||
{
|
||||
type: "usage",
|
||||
inputTokens: 19,
|
||||
outputTokens: 4,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -46,8 +46,6 @@ describe("LiteLlmHandler", () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fakeClient.chat.completions.create.resetHistory()
|
||||
|
||||
mockFetchForTesting(mockFetch, () => {
|
||||
return new Promise((resolve) => {
|
||||
doneMockingFetch = resolve
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import "should"
|
||||
import { openRouterDefaultModelInfo } from "@shared/api"
|
||||
import sinon from "sinon"
|
||||
import { OpenRouterHandler } from "../openrouter"
|
||||
|
||||
describe("OpenRouterHandler", () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
const createAsyncIterable = (data: any[] = []) => ({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield* data
|
||||
},
|
||||
})
|
||||
|
||||
it("should handle usage-only chunks when delta is missing", async () => {
|
||||
const handler = new OpenRouterHandler({
|
||||
openRouterApiKey: "test-api-key",
|
||||
})
|
||||
const fakeClient = {
|
||||
chat: {
|
||||
completions: {
|
||||
create: sinon.stub().resolves(
|
||||
createAsyncIterable([
|
||||
{
|
||||
choices: [{}],
|
||||
usage: {
|
||||
prompt_tokens: 13,
|
||||
completion_tokens: 5,
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
sinon.stub(handler as any, "ensureClient").returns(fakeClient as any)
|
||||
sinon.stub(handler, "getModel").returns({
|
||||
id: "openai/gpt-4o-mini",
|
||||
info: openRouterDefaultModelInfo,
|
||||
})
|
||||
|
||||
const chunks: any[] = []
|
||||
for await (const chunk of handler.createMessage("system", [{ role: "user", content: "hi" }])) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
chunks.should.deepEqual([
|
||||
{
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
inputTokens: 13,
|
||||
outputTokens: 5,
|
||||
totalCost: 0,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,19 +1,8 @@
|
||||
import "should"
|
||||
import { openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
|
||||
import sinon from "sinon"
|
||||
import { VercelAIGatewayHandler } from "../vercel-ai-gateway"
|
||||
|
||||
describe("VercelAIGatewayHandler", () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
const createAsyncIterable = (data: any[] = []) => ({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield* data
|
||||
},
|
||||
})
|
||||
|
||||
describe("getModel", () => {
|
||||
it("should return configured model and info when both are provided", () => {
|
||||
const customModelInfo = {
|
||||
@@ -22,22 +11,22 @@ describe("VercelAIGatewayHandler", () => {
|
||||
}
|
||||
|
||||
const handler = new VercelAIGatewayHandler({
|
||||
openRouterModelId: "google/gemini-3.1-pro-preview",
|
||||
openRouterModelId: "google/gemini-3-pro-preview",
|
||||
openRouterModelInfo: customModelInfo,
|
||||
})
|
||||
|
||||
const result = handler.getModel()
|
||||
result.id.should.equal("google/gemini-3.1-pro-preview")
|
||||
result.id.should.equal("google/gemini-3-pro-preview")
|
||||
result.info.should.deepEqual(customModelInfo)
|
||||
})
|
||||
|
||||
it("should preserve configured model ID when model info is missing", () => {
|
||||
const handler = new VercelAIGatewayHandler({
|
||||
openRouterModelId: "google/gemini-3.1-pro-preview",
|
||||
openRouterModelId: "google/gemini-3-pro-preview",
|
||||
})
|
||||
|
||||
const result = handler.getModel()
|
||||
result.id.should.equal("google/gemini-3.1-pro-preview")
|
||||
result.id.should.equal("google/gemini-3-pro-preview")
|
||||
result.info.should.deepEqual(openRouterDefaultModelInfo)
|
||||
})
|
||||
|
||||
@@ -49,46 +38,4 @@ describe("VercelAIGatewayHandler", () => {
|
||||
result.info.should.deepEqual(openRouterDefaultModelInfo)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createMessage", () => {
|
||||
it("should handle usage-only chunks when delta is missing", async () => {
|
||||
const handler = new VercelAIGatewayHandler({
|
||||
vercelAiGatewayApiKey: "test-api-key",
|
||||
})
|
||||
const fakeClient = {
|
||||
chat: {
|
||||
completions: {
|
||||
create: sinon.stub().resolves(
|
||||
createAsyncIterable([
|
||||
{
|
||||
choices: [{}],
|
||||
usage: {
|
||||
prompt_tokens: 11,
|
||||
completion_tokens: 7,
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
sinon.stub(handler as any, "ensureClient").returns(fakeClient as any)
|
||||
|
||||
const chunks: any[] = []
|
||||
for await (const chunk of handler.createMessage("system", [{ role: "user", content: "hi" }])) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
chunks.should.deepEqual([
|
||||
{
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
inputTokens: 11,
|
||||
outputTokens: 7,
|
||||
totalCost: 0,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -97,8 +97,9 @@ export class AnthropicHandler implements ApiHandler {
|
||||
"anthropic-beta": "context-1m-2025-08-07",
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
return undefined
|
||||
})(),
|
||||
)
|
||||
} else {
|
||||
|
||||
+147
-143
@@ -264,7 +264,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
if (useProfile) {
|
||||
AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile)
|
||||
} else {
|
||||
delete process.env.AWS_PROFILE
|
||||
delete process.env["AWS_PROFILE"]
|
||||
AwsBedrockHandler.setEnv("AWS_ACCESS_KEY_ID", this.options.awsAccessKey)
|
||||
AwsBedrockHandler.setEnv("AWS_SECRET_ACCESS_KEY", this.options.awsSecretKey)
|
||||
AwsBedrockHandler.setEnv("AWS_SESSION_TOKEN", this.options.awsSessionToken)
|
||||
@@ -537,7 +537,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
combinedContent += message.role === "user" ? `User: ${content}\n` : `Assistant: ${content}\n`
|
||||
combinedContent += message.role === "user" ? "User: " + content + "\n" : "Assistant: " + content + "\n"
|
||||
}
|
||||
|
||||
// Format according to DeepSeek R1's expected prompt format
|
||||
@@ -604,178 +604,182 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
* Common implementation for both Anthropic and Nova models
|
||||
*/
|
||||
private async *executeConverseStream(command: ConverseStreamCommand, modelInfo: ModelInfo): ApiStream {
|
||||
const client = await this.getBedrockClient()
|
||||
const response = await client.send(command)
|
||||
try {
|
||||
const client = await this.getBedrockClient()
|
||||
const response = await client.send(command)
|
||||
|
||||
if (response.stream) {
|
||||
// Buffer content by contentBlockIndex to handle multi-block responses correctly
|
||||
const contentBuffers: Record<number, string> = {}
|
||||
const blockTypes = new Map<number, "reasoning" | "text">()
|
||||
const activeToolCalls: Map<number, { toolUseId: string; name: string }> = new Map()
|
||||
if (response.stream) {
|
||||
// Buffer content by contentBlockIndex to handle multi-block responses correctly
|
||||
const contentBuffers: Record<number, string> = {}
|
||||
const blockTypes = new Map<number, "reasoning" | "text">()
|
||||
const activeToolCalls: Map<number, { toolUseId: string; name: string }> = new Map()
|
||||
|
||||
for await (const chunk of response.stream) {
|
||||
// Debug logging to see actual response structure
|
||||
// Logger.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
|
||||
for await (const chunk of response.stream) {
|
||||
// Debug logging to see actual response structure
|
||||
// Logger.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
|
||||
|
||||
// Handle thinking response in additionalModelResponseFields (LangChain format)
|
||||
const metadata = chunk.metadata as ExtendedMetadata | undefined
|
||||
if (metadata?.additionalModelResponseFields?.thinkingResponse) {
|
||||
const thinkingResponse = metadata.additionalModelResponseFields.thinkingResponse
|
||||
if (thinkingResponse.reasoning && Array.isArray(thinkingResponse.reasoning)) {
|
||||
for (const reasoningBlock of thinkingResponse.reasoning) {
|
||||
if (reasoningBlock.type === "text" && reasoningBlock.text) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: reasoningBlock.text,
|
||||
// Handle thinking response in additionalModelResponseFields (LangChain format)
|
||||
const metadata = chunk.metadata as ExtendedMetadata | undefined
|
||||
if (metadata?.additionalModelResponseFields?.thinkingResponse) {
|
||||
const thinkingResponse = metadata.additionalModelResponseFields.thinkingResponse
|
||||
if (thinkingResponse.reasoning && Array.isArray(thinkingResponse.reasoning)) {
|
||||
for (const reasoningBlock of thinkingResponse.reasoning) {
|
||||
if (reasoningBlock.type === "text" && reasoningBlock.text) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: reasoningBlock.text,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle metadata events with token usage information
|
||||
if (chunk.metadata?.usage) {
|
||||
const inputTokens = chunk.metadata.usage.inputTokens || 0
|
||||
const outputTokens = chunk.metadata.usage.outputTokens || 0
|
||||
const cacheReadInputTokens = chunk.metadata.usage.cacheReadInputTokens || 0
|
||||
const cacheWriteInputTokens = chunk.metadata.usage.cacheWriteInputTokens || 0
|
||||
// Handle metadata events with token usage information
|
||||
if (chunk.metadata?.usage) {
|
||||
const inputTokens = chunk.metadata.usage.inputTokens || 0
|
||||
const outputTokens = chunk.metadata.usage.outputTokens || 0
|
||||
const cacheReadInputTokens = chunk.metadata.usage.cacheReadInputTokens || 0
|
||||
const cacheWriteInputTokens = chunk.metadata.usage.cacheWriteInputTokens || 0
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens: cacheReadInputTokens,
|
||||
cacheWriteTokens: cacheWriteInputTokens,
|
||||
totalCost: calculateApiCostOpenAI(
|
||||
modelInfo,
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheWriteInputTokens,
|
||||
cacheReadInputTokens,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Handle content block start - check if Bedrock uses Anthropic SDK format
|
||||
if (chunk.contentBlockStart) {
|
||||
const blockStart = chunk.contentBlockStart as ContentBlockStart
|
||||
const blockIndex = chunk.contentBlockStart.contentBlockIndex
|
||||
|
||||
if (blockStart.start?.toolUse) {
|
||||
const toolUse = blockStart.start.toolUse
|
||||
if (toolUse.toolUseId && toolUse.name && blockIndex !== undefined) {
|
||||
activeToolCalls.set(blockIndex, {
|
||||
toolUseId: toolUse.toolUseId,
|
||||
name: toolUse.name,
|
||||
})
|
||||
cacheReadTokens: cacheReadInputTokens,
|
||||
cacheWriteTokens: cacheWriteInputTokens,
|
||||
totalCost: calculateApiCostOpenAI(
|
||||
modelInfo,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheWriteInputTokens,
|
||||
cacheReadInputTokens,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Check for thinking block in various possible formats
|
||||
if (
|
||||
blockStart.start?.type === "thinking" ||
|
||||
blockStart.contentBlock?.type === "thinking" ||
|
||||
blockStart.type === "thinking"
|
||||
) {
|
||||
// Handle content block start - check if Bedrock uses Anthropic SDK format
|
||||
if (chunk.contentBlockStart) {
|
||||
const blockStart = chunk.contentBlockStart as ContentBlockStart
|
||||
const blockIndex = chunk.contentBlockStart.contentBlockIndex
|
||||
|
||||
if (blockStart.start?.toolUse) {
|
||||
const toolUse = blockStart.start.toolUse
|
||||
if (toolUse.toolUseId && toolUse.name && blockIndex !== undefined) {
|
||||
activeToolCalls.set(blockIndex, {
|
||||
toolUseId: toolUse.toolUseId,
|
||||
name: toolUse.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for thinking block in various possible formats
|
||||
if (
|
||||
blockStart.start?.type === "thinking" ||
|
||||
blockStart.contentBlock?.type === "thinking" ||
|
||||
blockStart.type === "thinking"
|
||||
) {
|
||||
if (blockIndex !== undefined) {
|
||||
blockTypes.set(blockIndex, "reasoning")
|
||||
// Initialize content if provided
|
||||
const initialContent =
|
||||
blockStart.start?.thinking || blockStart.contentBlock?.thinking || blockStart.thinking || ""
|
||||
if (initialContent) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: initialContent,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle content block delta - accumulate content by block index
|
||||
if (chunk.contentBlockDelta) {
|
||||
const blockIndex = chunk.contentBlockDelta.contentBlockIndex
|
||||
|
||||
if (blockIndex !== undefined) {
|
||||
blockTypes.set(blockIndex, "reasoning")
|
||||
// Initialize content if provided
|
||||
const initialContent =
|
||||
blockStart.start?.thinking || blockStart.contentBlock?.thinking || blockStart.thinking || ""
|
||||
if (initialContent) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: initialContent,
|
||||
}
|
||||
// Initialize buffer for this block if it doesn't exist
|
||||
if (!(blockIndex in contentBuffers)) {
|
||||
contentBuffers[blockIndex] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle content block delta - accumulate content by block index
|
||||
if (chunk.contentBlockDelta) {
|
||||
const blockIndex = chunk.contentBlockDelta.contentBlockIndex
|
||||
// Check if this is a thinking block
|
||||
const blockType = blockTypes.get(blockIndex)
|
||||
const delta = chunk.contentBlockDelta.delta as ContentBlockDelta["delta"]
|
||||
|
||||
if (blockIndex !== undefined) {
|
||||
// Initialize buffer for this block if it doesn't exist
|
||||
if (!(blockIndex in contentBuffers)) {
|
||||
contentBuffers[blockIndex] = ""
|
||||
}
|
||||
|
||||
// Check if this is a thinking block
|
||||
const blockType = blockTypes.get(blockIndex)
|
||||
const delta = chunk.contentBlockDelta.delta as ContentBlockDelta["delta"]
|
||||
|
||||
// Handle thinking delta (Anthropic SDK format)
|
||||
if (delta?.type === "thinking_delta" || delta?.thinking) {
|
||||
const thinkingContent = delta.thinking || delta.text || ""
|
||||
if (thinkingContent) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: thinkingContent,
|
||||
// Handle thinking delta (Anthropic SDK format)
|
||||
if (delta?.type === "thinking_delta" || delta?.thinking) {
|
||||
const thinkingContent = delta.thinking || delta.text || ""
|
||||
if (thinkingContent) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: thinkingContent,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (delta?.reasoningContent?.text) {
|
||||
// Handle reasoning content (Bedrock format)
|
||||
const reasoningText = delta.reasoningContent.text
|
||||
if (reasoningText) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: reasoningText,
|
||||
} else if (delta?.reasoningContent?.text) {
|
||||
// Handle reasoning content (Bedrock format)
|
||||
const reasoningText = delta.reasoningContent.text
|
||||
if (reasoningText) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: reasoningText,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (delta?.toolUse?.input !== undefined) {
|
||||
const toolCall = activeToolCalls.get(blockIndex)
|
||||
const toolInput = delta.toolUse.input
|
||||
if (toolCall && typeof toolInput === "string") {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
call_id: toolCall.toolUseId,
|
||||
function: {
|
||||
id: toolCall.toolUseId,
|
||||
name: toolCall.name,
|
||||
arguments: toolInput,
|
||||
} else if (delta?.toolUse?.input !== undefined) {
|
||||
const toolCall = activeToolCalls.get(blockIndex)
|
||||
const toolInput = delta.toolUse.input
|
||||
if (toolCall && typeof toolInput === "string") {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
call_id: toolCall.toolUseId,
|
||||
function: {
|
||||
id: toolCall.toolUseId,
|
||||
name: toolCall.name,
|
||||
arguments: toolInput,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (chunk.contentBlockDelta.delta?.text) {
|
||||
// Handle regular text content
|
||||
const textContent = chunk.contentBlockDelta.delta.text
|
||||
contentBuffers[blockIndex] += textContent
|
||||
} else if (chunk.contentBlockDelta.delta?.text) {
|
||||
// Handle regular text content
|
||||
const textContent = chunk.contentBlockDelta.delta.text
|
||||
contentBuffers[blockIndex] += textContent
|
||||
|
||||
// Stream based on block type
|
||||
if (blockType === "reasoning") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: textContent,
|
||||
}
|
||||
} else {
|
||||
yield {
|
||||
type: "text",
|
||||
text: textContent,
|
||||
// Stream based on block type
|
||||
if (blockType === "reasoning") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: textContent,
|
||||
}
|
||||
} else {
|
||||
yield {
|
||||
type: "text",
|
||||
text: textContent,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle content block stop - clean up buffers
|
||||
if (chunk.contentBlockStop) {
|
||||
const blockIndex = chunk.contentBlockStop.contentBlockIndex
|
||||
// Handle content block stop - clean up buffers
|
||||
if (chunk.contentBlockStop) {
|
||||
const blockIndex = chunk.contentBlockStop.contentBlockIndex
|
||||
|
||||
if (blockIndex !== undefined) {
|
||||
// Clean up buffers and tracking for this block
|
||||
delete contentBuffers[blockIndex]
|
||||
blockTypes.delete(blockIndex)
|
||||
activeToolCalls.delete(blockIndex)
|
||||
if (blockIndex !== undefined) {
|
||||
// Clean up buffers and tracking for this block
|
||||
delete contentBuffers[blockIndex]
|
||||
blockTypes.delete(blockIndex)
|
||||
activeToolCalls.delete(blockIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle errors with unified error handling
|
||||
yield* this.handleBedrockStreamError(chunk)
|
||||
// Handle errors with unified error handling
|
||||
yield* this.handleBedrockStreamError(chunk)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1014,7 +1018,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
if (item.source.media_type) {
|
||||
// Extract format from media_type (e.g., "image/jpeg" -> "jpeg")
|
||||
const formatMatch = item.source.media_type.match(/image\/(\w+)/)
|
||||
if (formatMatch?.[1]) {
|
||||
if (formatMatch && formatMatch[1]) {
|
||||
const extractedFormat = formatMatch[1]
|
||||
// Ensure format is one of the allowed values
|
||||
if (["png", "jpeg", "gif", "webp"].includes(extractedFormat)) {
|
||||
|
||||
@@ -84,8 +84,7 @@ export class CerebrasHandler implements ApiHandler {
|
||||
.map((block) => {
|
||||
if (block.type === "text") {
|
||||
return block.text
|
||||
}
|
||||
if (block.type === "image") {
|
||||
} else if (block.type === "image") {
|
||||
return "[Image content not supported in Cerebras]"
|
||||
}
|
||||
return ""
|
||||
@@ -196,18 +195,14 @@ export class CerebrasHandler implements ApiHandler {
|
||||
// Rate limit error - will be handled by retry decorator with patient backoff
|
||||
const _limits = this.getRateLimits()
|
||||
throw new Error(`Cerebras API rate limit exceeded.`)
|
||||
}
|
||||
if (error?.status === 401) {
|
||||
} else if (error?.status === 401) {
|
||||
throw new Error("Cerebras API authentication failed. Please check your API key.")
|
||||
}
|
||||
if (error?.status === 403) {
|
||||
} else if (error?.status === 403) {
|
||||
throw new Error("Cerebras API access denied. Please check your API key permissions.")
|
||||
}
|
||||
if (error?.status >= 500) {
|
||||
} else if (error?.status >= 500) {
|
||||
// Server errors - retryable
|
||||
throw new Error(`Cerebras API server error (${error.status}): ${error.message || "Unknown server error"}`)
|
||||
}
|
||||
if (error?.status === 400) {
|
||||
} else if (error?.status === 400) {
|
||||
// Client errors - not retryable
|
||||
throw new Error(`Cerebras API bad request: ${error.message || "Invalid request parameters"}`)
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ export class ClaudeCodeHandler implements ApiHandler {
|
||||
if (message.stop_reason !== null) {
|
||||
const content = "text" in message.content[0] ? message.content[0] : undefined
|
||||
|
||||
const isError = content?.text.startsWith(`API Error`)
|
||||
if (content && isError) {
|
||||
const isError = content && content.text.startsWith(`API Error`)
|
||||
if (isError) {
|
||||
// Error messages are formatted as: `API Error: <<status code>> <<json>>`
|
||||
const errorMessageStart = content.text.indexOf("{")
|
||||
const errorMessage = content.text.slice(errorMessageStart)
|
||||
|
||||
@@ -126,7 +126,7 @@ export class ClineHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
Logger.debug(`ClineHandler chunk:${JSON.stringify(chunk)}`)
|
||||
Logger.debug("ClineHandler chunk:" + JSON.stringify(chunk))
|
||||
// openrouter returns an error object instead of the openai sdk throwing an error
|
||||
if ("error" in chunk) {
|
||||
const error = chunk.error as OpenRouterErrorResponse["error"]
|
||||
@@ -168,12 +168,7 @@ export class ClineHandler implements ApiHandler {
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if (
|
||||
delta &&
|
||||
"reasoning" in delta &&
|
||||
delta.reasoning &&
|
||||
!shouldSkipReasoningForModel(this.options.openRouterModelId)
|
||||
) {
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: typeof delta.reasoning === "string" ? delta.reasoning : JSON.stringify(delta.reasoning),
|
||||
@@ -188,7 +183,6 @@ export class ClineHandler implements ApiHandler {
|
||||
See: https://openrouter.ai/docs/use-cases/reasoning-tokens#preserving-reasoning-blocks
|
||||
*/
|
||||
if (
|
||||
delta &&
|
||||
"reasoning_details" in delta &&
|
||||
delta.reasoning_details &&
|
||||
// @ts-expect-error-next-line
|
||||
|
||||
@@ -78,11 +78,13 @@ export class DifyHandler implements ApiHandler {
|
||||
private baseUrl: string
|
||||
private apiKey: string
|
||||
private conversationId: string | null = null
|
||||
private currentTaskId: string | null = null
|
||||
private abortController: AbortController | null = null
|
||||
|
||||
constructor(options: DifyHandlerOptions) {
|
||||
this.options = options
|
||||
this.apiKey = this.options.difyApiKey || ""
|
||||
this.baseUrl = this.options.difyBaseUrl || ""
|
||||
this.apiKey = options.difyApiKey || ""
|
||||
this.baseUrl = options.difyBaseUrl || ""
|
||||
|
||||
Logger.log("[DIFY DEBUG] Constructor called with:", {
|
||||
hasApiKey: !!this.apiKey,
|
||||
@@ -339,7 +341,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
hasYieldedContent = true
|
||||
}
|
||||
} catch (_e) {
|
||||
} catch (e) {
|
||||
// Not JSON, continue
|
||||
Logger.log("[DIFY DEBUG] Line is not direct JSON, continuing")
|
||||
}
|
||||
@@ -427,7 +429,7 @@ export class DifyHandler implements ApiHandler {
|
||||
* @param user User identifier (defaults to "cline-user")
|
||||
* @returns Promise with file upload response
|
||||
*/
|
||||
async uploadFile(file: Buffer, filename: string, user = "cline-user"): Promise<DifyFileResponse> {
|
||||
async uploadFile(file: Buffer, filename: string, user: string = "cline-user"): Promise<DifyFileResponse> {
|
||||
const formData = new FormData()
|
||||
formData.append("file", new Blob([new Uint8Array(file)]), filename)
|
||||
formData.append("user", user)
|
||||
@@ -452,7 +454,7 @@ export class DifyHandler implements ApiHandler {
|
||||
* @param user User identifier (defaults to "cline-user")
|
||||
* @returns Promise that resolves when generation is stopped
|
||||
*/
|
||||
async stopGeneration(taskId: string, user = "cline-user"): Promise<void> {
|
||||
async stopGeneration(taskId: string, user: string = "cline-user"): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/chat-messages/${taskId}/stop`, {
|
||||
method: "POST",
|
||||
headers: this.jsonHeaders(),
|
||||
@@ -475,9 +477,9 @@ export class DifyHandler implements ApiHandler {
|
||||
*/
|
||||
async getConversationHistory(
|
||||
conversationId: string,
|
||||
user = "cline-user",
|
||||
user: string = "cline-user",
|
||||
firstId?: string,
|
||||
limit = 20,
|
||||
limit: number = 20,
|
||||
): Promise<DifyHistoryResponse> {
|
||||
const params = new URLSearchParams({ user, limit: limit.toString() })
|
||||
if (firstId) {
|
||||
@@ -505,10 +507,10 @@ export class DifyHandler implements ApiHandler {
|
||||
* @returns Promise with conversations list
|
||||
*/
|
||||
async getConversations(
|
||||
user = "cline-user",
|
||||
user: string = "cline-user",
|
||||
lastId?: string,
|
||||
limit = 20,
|
||||
sortBy = "-updated_at",
|
||||
limit: number = 20,
|
||||
sortBy: string = "-updated_at",
|
||||
): Promise<DifyConversationsResponse> {
|
||||
const params = new URLSearchParams({
|
||||
user,
|
||||
@@ -537,7 +539,7 @@ export class DifyHandler implements ApiHandler {
|
||||
* @param user User identifier (defaults to "cline-user")
|
||||
* @returns Promise that resolves when conversation is deleted
|
||||
*/
|
||||
async deleteConversation(conversationId: string, user = "cline-user"): Promise<void> {
|
||||
async deleteConversation(conversationId: string, user: string = "cline-user"): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}`, {
|
||||
method: "DELETE",
|
||||
headers: this.jsonHeaders(),
|
||||
@@ -560,9 +562,9 @@ export class DifyHandler implements ApiHandler {
|
||||
*/
|
||||
async renameConversation(
|
||||
conversationId: string,
|
||||
user = "cline-user",
|
||||
user: string = "cline-user",
|
||||
name?: string,
|
||||
autoGenerate = false,
|
||||
autoGenerate: boolean = false,
|
||||
): Promise<DifyConversationResponse> {
|
||||
const body: any = { user, auto_generate: autoGenerate }
|
||||
if (name) {
|
||||
@@ -595,7 +597,7 @@ export class DifyHandler implements ApiHandler {
|
||||
messageId: string,
|
||||
rating: "like" | "dislike",
|
||||
content?: string,
|
||||
user = "cline-user",
|
||||
user: string = "cline-user",
|
||||
): Promise<void> {
|
||||
const body: any = { rating, user }
|
||||
if (content) {
|
||||
@@ -635,6 +637,7 @@ export class DifyHandler implements ApiHandler {
|
||||
*/
|
||||
resetConversation(): void {
|
||||
this.conversationId = null
|
||||
this.currentTaskId = null
|
||||
}
|
||||
|
||||
private jsonHeaders() {
|
||||
|
||||
@@ -71,7 +71,7 @@ export class FireworksHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (reasoning || (delta && "reasoning_content" in delta && delta.reasoning_content)) {
|
||||
if (reasoning || ("reasoning_content" in delta && delta.reasoning_content)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: delta.content || ((delta as any).reasoning_content as string | undefined) || "",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// Mock for @google/genai module to avoid ESM compatibility issues in tests
|
||||
|
||||
export class GoogleGenAI {
|
||||
constructor(_options: any) {
|
||||
// Mock constructor
|
||||
}
|
||||
|
||||
models = {
|
||||
generateContentStream: async (_params: any) => {
|
||||
// Mock implementation that returns an async iterator
|
||||
|
||||
@@ -157,7 +157,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
httpOptions: this.options.geminiBaseUrl ? { baseUrl: this.options.geminiBaseUrl } : undefined,
|
||||
systemInstruction: systemPrompt,
|
||||
// Set temperature (default to 0)
|
||||
// Gemini 3 recommends 1.0
|
||||
// Gemini 3.0 recommends 1.0
|
||||
temperature: info.temperature ?? 1,
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
// https://github.com/googleapis/js-genai/blob/v1.11.0/src/_api_client.ts#L758
|
||||
const response = this.attemptParse(error.message)
|
||||
|
||||
if (response?.error) {
|
||||
if (response && response.error) {
|
||||
const responseBody = this.attemptParse(response.error.message)
|
||||
|
||||
if (responseBody.error) {
|
||||
|
||||
@@ -79,7 +79,7 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
let reasoning: string | null = null
|
||||
let didOutputUsage = false
|
||||
let didOutputUsage: boolean = false
|
||||
let finalUsage: any = null
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
@@ -66,49 +66,53 @@ export class HuggingFaceHandler implements ApiHandler {
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
try {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const requestParams = {
|
||||
model: model.id,
|
||||
max_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
...getOpenAIToolParams(tools),
|
||||
}
|
||||
const requestParams = {
|
||||
model: model.id,
|
||||
max_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
...getOpenAIToolParams(tools),
|
||||
}
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
const stream = (await client.chat.completions.create(requestParams)) as any
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
const stream = (await client.chat.completions.create(requestParams)) as any
|
||||
|
||||
let _chunkCount = 0
|
||||
let _totalContent = ""
|
||||
let _chunkCount = 0
|
||||
let _totalContent = ""
|
||||
|
||||
for await (const chunk of stream) {
|
||||
_chunkCount++
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
_totalContent += delta.content
|
||||
for await (const chunk of stream) {
|
||||
_chunkCount++
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
_totalContent += delta.content
|
||||
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,24 +72,26 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
if (response.ok) {
|
||||
const data: LiteLlmModelInfoResponse = await response.json()
|
||||
return data
|
||||
}
|
||||
Logger.error("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
// Try with Authorization header instead
|
||||
const retryResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
Logger.error("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
// Try with Authorization header instead
|
||||
const retryResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
|
||||
if (retryResponse.ok) {
|
||||
const data: LiteLlmModelInfoResponse = await retryResponse.json()
|
||||
return data
|
||||
if (retryResponse.ok) {
|
||||
const data: LiteLlmModelInfoResponse = await retryResponse.json()
|
||||
return data
|
||||
} else {
|
||||
Logger.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
throw new Error(`Failed to fetch LiteLLM model info: ${retryResponse.statusText}`)
|
||||
}
|
||||
}
|
||||
Logger.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
throw new Error(`Failed to fetch LiteLLM model info: ${retryResponse.statusText}`)
|
||||
} catch (error) {
|
||||
Logger.error("Error fetching LiteLLM model info:", error)
|
||||
throw error
|
||||
@@ -100,7 +102,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
private options: LiteLlmHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
private modelInfoCache: LiteLlmModelInfoResponse | undefined
|
||||
private modelInfoCacheTimestamp = 0
|
||||
private modelInfoCacheTimestamp: number = 0
|
||||
private readonly modelInfoCacheTTL = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
constructor(options: LiteLlmHandlerOptions) {
|
||||
@@ -272,8 +274,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
},
|
||||
] as any,
|
||||
}
|
||||
}
|
||||
if (Array.isArray(message.content)) {
|
||||
} else if (Array.isArray(message.content)) {
|
||||
// Apply cache control to the last content item in the array
|
||||
return {
|
||||
...message,
|
||||
|
||||
@@ -118,7 +118,7 @@ export class MistralHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} else if (delta?.content) {
|
||||
let content = ""
|
||||
let content: string = ""
|
||||
if (typeof delta.content === "string") {
|
||||
content = delta.content
|
||||
} else if (Array.isArray(delta.content)) {
|
||||
|
||||
+18
-132
@@ -1,6 +1,5 @@
|
||||
import { Anthropic, APIError as AnthropicAPIError } from "@anthropic-ai/sdk"
|
||||
import { liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults, ModelInfo } from "@shared/api"
|
||||
import OpenAI, { APIError as OpenAIAPIError, OpenAIError } from "openai"
|
||||
import OpenAI, { APIError, OpenAIError } from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
import {
|
||||
@@ -17,12 +16,10 @@ import { ApiFormat } from "@/shared/proto/index.cline"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, type CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { sanitizeAnthropicMessages } from "../transform/anthropic-format"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToOpenAIResponsesInput } from "../transform/openai-response-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
import { convertOpenAIToolsToAnthropicTools, handleAnthropicMessagesApiStreamResponse } from "../utils/messages_api_support"
|
||||
import { handleResponsesApiStreamResponse } from "../utils/responses_api_support"
|
||||
|
||||
export interface OcaHandlerOptions extends CommonApiHandlerOptions {
|
||||
@@ -38,15 +35,14 @@ export interface OcaHandlerOptions extends CommonApiHandlerOptions {
|
||||
|
||||
export class OcaHandler implements ApiHandler {
|
||||
protected options: OcaHandlerOptions
|
||||
protected openAIClient: OpenAI | undefined
|
||||
protected anthropicClient: Anthropic | undefined
|
||||
protected client: OpenAI | undefined
|
||||
protected externalHeaders: Record<string, string> = {}
|
||||
|
||||
constructor(options: OcaHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
protected initializeOpenAIClient(options: OcaHandlerOptions): OpenAI {
|
||||
protected initializeClient(options: OcaHandlerOptions): OpenAI {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
return new (class OCIOpenAI extends OpenAI {
|
||||
protected override async prepareOptions(opts: any): Promise<void> {
|
||||
@@ -67,7 +63,7 @@ export class OcaHandler implements ApiHandler {
|
||||
error: Object | undefined,
|
||||
message: string | undefined,
|
||||
headers: any | undefined,
|
||||
): OpenAIAPIError {
|
||||
): APIError {
|
||||
interface OciError {
|
||||
code?: string
|
||||
message?: string
|
||||
@@ -98,89 +94,23 @@ export class OcaHandler implements ApiHandler {
|
||||
})
|
||||
}
|
||||
|
||||
protected initializeAnthropicClient(options: OcaHandlerOptions): Anthropic {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
return new (class OCIAnthropic extends Anthropic {
|
||||
protected override async prepareOptions(opts: any): Promise<void> {
|
||||
const token = await OcaAuthService.getInstance().getAuthToken()
|
||||
if (!token) {
|
||||
throw new OpenAIError("Unable to handle auth, Oracle Code Assist (OCA) access token is not available")
|
||||
}
|
||||
opts.headers ??= {}
|
||||
// OCA Headers
|
||||
const ociHeaders = await createOcaHeaders(token, options.taskId!)
|
||||
opts.headers = { ...opts.headers, ...externalHeaders, ...ociHeaders }
|
||||
Logger.log(`Making request with customer opc-request-id: ${opts.headers?.["opc-request-id"]}`)
|
||||
return super.prepareOptions(opts)
|
||||
}
|
||||
|
||||
protected override makeStatusError(
|
||||
status: number | undefined,
|
||||
error: Object | undefined,
|
||||
message: string | undefined,
|
||||
headers: any | undefined,
|
||||
): AnthropicAPIError {
|
||||
interface OciError {
|
||||
code?: string
|
||||
message?: string
|
||||
}
|
||||
let ociErrorMessage = message
|
||||
if (typeof error === "object" && error !== null) {
|
||||
try {
|
||||
ociErrorMessage = JSON.stringify(error)
|
||||
const ociErr = error as OciError
|
||||
if (ociErr.code !== undefined && ociErr.message !== undefined) {
|
||||
ociErrorMessage = `${ociErr.code}: ${ociErr.message}`
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
const opcRequestId = headers?.[OCI_HEADER_OPC_REQUEST_ID]
|
||||
if (opcRequestId) {
|
||||
ociErrorMessage += `\n(${OCI_HEADER_OPC_REQUEST_ID}: ${opcRequestId})`
|
||||
}
|
||||
const statusCode = typeof status === "number" ? status : 500
|
||||
return super.makeStatusError(statusCode, error ?? {}, ociErrorMessage, headers)
|
||||
}
|
||||
})({
|
||||
baseURL:
|
||||
options.ocaBaseUrl ||
|
||||
(options.ocaMode === "internal" ? DEFAULT_INTERNAL_OCA_BASE_URL : DEFAULT_EXTERNAL_OCA_BASE_URL),
|
||||
apiKey: "noop",
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
}
|
||||
|
||||
protected ensureOpenAIClient(): OpenAI {
|
||||
if (!this.openAIClient) {
|
||||
protected ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.ocaModelId) {
|
||||
throw new Error("Oracle Code Assist (OCA) model is not selected")
|
||||
}
|
||||
try {
|
||||
this.openAIClient = this.initializeOpenAIClient(this.options)
|
||||
this.client = this.initializeClient(this.options)
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Oracle Code Assist (OCA) client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.openAIClient
|
||||
}
|
||||
|
||||
protected ensureAnthropicClient(): Anthropic {
|
||||
if (!this.anthropicClient) {
|
||||
if (!this.options.ocaModelId) {
|
||||
throw new Error("Oracle Code Assist (OCA) model is not selected")
|
||||
}
|
||||
try {
|
||||
this.anthropicClient = this.initializeAnthropicClient(this.options)
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Oracle Code Assist (OCA) client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.anthropicClient
|
||||
return this.client
|
||||
}
|
||||
|
||||
async getApiCosts(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
|
||||
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
|
||||
const client = this.ensureOpenAIClient()
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.options.ocaModelId || liteLlmDefaultModelId
|
||||
const token = await OcaAuthService.getInstance().getAuthToken()
|
||||
if (!token) {
|
||||
@@ -217,7 +147,7 @@ export class OcaHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
async calculateCost(
|
||||
_modelInfo: ModelInfo,
|
||||
modelInfo: ModelInfo,
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
_cacheWriteTokens?: number,
|
||||
@@ -231,17 +161,15 @@ export class OcaHandler implements ApiHandler {
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
|
||||
if (this.options.ocaModelInfo?.apiFormat === ApiFormat.OPENAI_RESPONSES) {
|
||||
if (this.options.ocaModelInfo?.apiFormat == ApiFormat.OPENAI_RESPONSES) {
|
||||
yield* this.createMessageResponsesApi(systemPrompt, messages, tools)
|
||||
} else if (this.options.ocaModelInfo?.apiFormat == ApiFormat.ANTHROPIC_CHAT) {
|
||||
yield* this.createMessageMessagesApi(systemPrompt, messages, tools)
|
||||
} else {
|
||||
yield* this.createMessageChatApi(systemPrompt, messages, tools)
|
||||
}
|
||||
}
|
||||
|
||||
async *createMessageChatApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureOpenAIClient()
|
||||
const client = this.ensureClient()
|
||||
const formattedMessages = convertToOpenAiMessages(messages)
|
||||
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
||||
role: "system",
|
||||
@@ -310,7 +238,7 @@ export class OcaHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
if (this.options.ocaModelInfo?.supportsReasoningEffort) {
|
||||
chatCompletionsParams.reasoning_effort = this.options.ocaReasoningEffort || ("medium" as any)
|
||||
chatCompletionsParams["reasoning_effort"] = this.options.ocaReasoningEffort || ("medium" as any)
|
||||
}
|
||||
|
||||
const stream = await client.chat.completions.create(chatCompletionsParams)
|
||||
@@ -378,8 +306,8 @@ export class OcaHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
async *createMessageResponsesApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureOpenAIClient()
|
||||
const inputMessages = convertToOpenAIResponsesInput(messages, { usePreviousResponseId: false }).input
|
||||
const client = this.ensureClient()
|
||||
const inputMessages = convertToOpenAIResponsesInput(messages).input
|
||||
// Convert messages to Responses API input format
|
||||
const input: OpenAI.Responses.ResponseInputItem[] = [{ role: "system", content: systemPrompt }, ...inputMessages]
|
||||
|
||||
@@ -401,56 +329,14 @@ export class OcaHandler implements ApiHandler {
|
||||
tools: responseTools,
|
||||
}
|
||||
|
||||
const ocaModelInfo = this.options.ocaModelInfo
|
||||
if (!ocaModelInfo) {
|
||||
throw new Error("Oracle Code Assist (OCA) model info is required for Responses API")
|
||||
}
|
||||
if (ocaModelInfo.supportsReasoning) {
|
||||
responsesParams.reasoning = { effort: this.options.ocaReasoningEffort as any, summary: "auto" }
|
||||
if (this.options.ocaModelInfo && this.options.ocaModelInfo.supportsReasoning) {
|
||||
responsesParams["reasoning"] = { effort: this.options.ocaReasoningEffort as any, summary: "auto" }
|
||||
}
|
||||
|
||||
// Create the response using Responses API
|
||||
const stream = await client.responses.create(responsesParams)
|
||||
|
||||
yield* handleResponsesApiStreamResponse(stream, ocaModelInfo, this.calculateCost.bind(this))
|
||||
}
|
||||
|
||||
async *createMessageMessagesApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureAnthropicClient()
|
||||
|
||||
const modelId = this.options.ocaModelId || liteLlmDefaultModelId
|
||||
|
||||
const budgetTokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = this.options.ocaModelInfo?.supportsReasoning && budgetTokens !== 0
|
||||
|
||||
let temperature: number | undefined = this.options.ocaModelInfo?.temperature ?? 0
|
||||
const maxTokens: number | undefined = this.options.ocaModelInfo?.maxTokens || 8192
|
||||
|
||||
if (reasoningOn) {
|
||||
temperature = 0
|
||||
}
|
||||
|
||||
const anthropicTools = convertOpenAIToolsToAnthropicTools(tools)
|
||||
const anthropicMessages = sanitizeAnthropicMessages(messages, this.options.ocaUsePromptCache ?? false)
|
||||
|
||||
const stream = await client.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: maxTokens,
|
||||
temperature: reasoningOn ? undefined : temperature,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
cache_control: this.options.ocaUsePromptCache ? { type: "ephemeral" } : undefined,
|
||||
},
|
||||
],
|
||||
messages: anthropicMessages,
|
||||
stream: true,
|
||||
tools: anthropicTools,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined,
|
||||
})
|
||||
|
||||
yield* handleAnthropicMessagesApiStreamResponse(stream)
|
||||
yield* handleResponsesApiStreamResponse(stream, this.options.ocaModelInfo!, this.calculateCost.bind(this))
|
||||
}
|
||||
|
||||
getModel() {
|
||||
|
||||
@@ -86,7 +86,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
Logger.debug(`[OllamaHandler] Message Chunk${JSON.stringify(chunk)}`)
|
||||
Logger.debug("[OllamaHandler] Message Chunk" + JSON.stringify(chunk))
|
||||
|
||||
const delta = chunk.message
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user