Compare commits

..

1 Commits

Author SHA1 Message Date
celestial-vault 43b863809a fix: prevent tooltip hovering by using inline pointer-events none
- Add pointer-events: none inline style to HeroTooltip component
- Reduce closeDelay from 500ms to 100ms for faster tooltip dismissal
- Apply pointer-events-none to both base and content classNames
- Fixes annoying tooltip lingering when accidentally hovering over tooltip content
2025-09-25 17:29:15 -07:00
38 changed files with 207 additions and 435 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: automatically retry on rate limit errors with SAP AI Core provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
feat: preserve reasoning traces for cline/openrouter/anthropic providers to maintain conversation integrity
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Empty Pr to bump changeset
+2 -2
View File
@@ -71,5 +71,5 @@ jobs:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: staging
run: npm run publish:marketplace:nightly
CLINE_ENVIRONMENT: production
run: npm run publish:marketplace:nightly
+2 -6
View File
@@ -1,13 +1,9 @@
# Changelog
## [3.32.0]
- Added the new code-supernova-1-million stealth model, available for free and delivering a 1 million token context window
- Changes to inform Cline about commands that are available on your system
## [3.31.1]
- Version bump
- Add installed useful CLI tools to environment details
- Rename MCP tab 'Installed' to 'Configure'
## [3.31.0]
+1 -2
View File
@@ -126,8 +126,7 @@
{
"group": "Customization",
"pages": [
"features/customization/opening-cline-in-sidebar",
"features/customization/disable-terminal-pagers"
"features/customization/opening-cline-in-sidebar"
]
}
]
@@ -1,79 +0,0 @@
---
title: "Disable Terminal Pagers During Cline Sessions"
description: "Make CLI output non-interactive when Cline runs commands by detecting the CLINE_ACTIVE environment variable and disabling pagers like less."
---
Many CLI tools (like Git) use a pager such as `less` for interactive, scrollable output. When Cline runs commands in your terminal, that interactivity gets in the way — the pager can pause on the first page and block progress. You can configure your shell so that when a terminal is spawned by Cline, pagers are disabled and output streams through normally.
## How it works
Cline sets an environment variable for terminals it opens to run commands:
- `CLINE_ACTIVE` — non-empty when the shell is running under Cline
You can detect this variable in your shell startup file and adjust environment variables or aliases only for Cline-run sessions. This keeps your normal interactive terminals unchanged.
## Quick setup (Zsh/Bash)
Add the following to your `~/.zshrc`, `~/.bashrc`, or `~/.bash_profile`:
```bash
# Disable pagers when the terminal is launched by Cline
if [[ -n "$CLINE_ACTIVE" ]]; then
export PAGER=cat
export GIT_PAGER=cat
export SYSTEMD_PAGER=cat
export LESS="-FRX"
fi
```
<Note>
- `PAGER=cat` ensures generic pager-aware tools print directly to stdout
- `GIT_PAGER=cat` prevents Git from invoking `less`
- `SYSTEMD_PAGER=cat` disables paging in systemd tools (if present)
- `LESS="-FRX"` makes `less` behave more like streaming output if a tool still calls it
</Note>
This configuration only applies when `CLINE_ACTIVE` is set, so your normal terminals keep their usual interactive behavior.
## Verify
- Open a task in Cline that runs terminal commands and check:
- `echo "$CLINE_ACTIVE"` prints a non-empty value
- `git log` or other long outputs should stream without pausing
- If changes don't take effect:
- Make sure you updated the correct startup file for your shell
- Restart VS Code/Cursor so integrated terminals reload your shell config
- Confirm your terminal profile sources your `~/.zshrc` or `~/.bashrc`
## Optional tweaks
- Prefer command-line options when you don't want to rely on env vars:
```bash
# One-off usage (no aliases)
git --no-pager log -n 50 --decorate --oneline
systemctl --no-pager status nginx
journalctl --no-pager -u nginx -n 200
less -FRX README.md
```
- You can also override paging via shell aliases scoped to Cline sessions using options rather than env vars:
```bash
if [[ -n "$CLINE_ACTIVE" ]]; then
# Make 'less' non-interactive by default
alias less='less -FRX'
# Disable paging for common tools via CLI flags
alias git='command git --no-pager'
alias systemctl='command systemctl --no-pager'
alias journalctl='command journalctl --no-pager'
fi
```
- If you prefer environment variables, many CLIs also respect a generic or tool-specific pager variable:
- Git: `GIT_PAGER=cat`
- Systemd: `SYSTEMD_PAGER=cat`
- Man pages: `MANPAGER=cat` (not typically needed for Cline-driven commands)
- Aliases affect the current interactive shell, while environment variables propagate to child processes. Choose the approach that best fits your workflow.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.32.0",
"version": "3.30.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.32.0",
"version": "3.30.3",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
+1 -1
View File
@@ -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.32.0",
"version": "3.31.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
+1 -1
View File
@@ -50,7 +50,7 @@ service FileService {
rpc refreshRules(EmptyRequest) returns (RefreshedRules);
// Opens a task's conversation history file on disk
rpc openDiskConversationHistory(StringRequest) returns (Empty);
rpc openTaskHistory(StringRequest) returns (Empty);
// Toggles a workflow on or off
rpc toggleWorkflow(ToggleWorkflowRequest) returns (ClineRulesToggles);
+6 -29
View File
@@ -150,8 +150,6 @@ export class AnthropicHandler implements ApiHandler {
}
}
let thinkingDeltaAccumulator = ""
for await (const chunk of stream) {
switch (chunk?.type) {
case "message_start":
@@ -184,26 +182,14 @@ export class AnthropicHandler implements ApiHandler {
type: "reasoning",
reasoning: chunk.content_block.thinking || "",
}
const thinking = chunk.content_block.thinking
const signature = chunk.content_block.signature
if (thinking && signature) {
yield {
type: "ant_thinking",
thinking,
signature,
}
}
break
case "redacted_thinking":
// Content is encrypted, and we don't to pass placeholder text back to the API
// Handle redacted thinking blocks - we still mark it as reasoning
// but note that the content is encrypted
yield {
type: "reasoning",
reasoning: "[Redacted thinking block]",
}
yield {
type: "ant_redacted_thinking",
data: chunk.content_block.data,
}
break
case "text":
// we may receive multiple text blocks, in which case just insert a line break between them
@@ -223,23 +209,10 @@ export class AnthropicHandler implements ApiHandler {
case "content_block_delta":
switch (chunk.delta.type) {
case "thinking_delta":
// 'reasoning' type just displays in the UI, but ant_thinking will be used to send the thinking traces back to the API
yield {
type: "reasoning",
reasoning: chunk.delta.thinking,
}
thinkingDeltaAccumulator += chunk.delta.thinking
break
case "signature_delta":
// It's used when sending the thinking block back to the API
// API expects this in completed form, not as array of deltas
if (thinkingDeltaAccumulator && chunk.delta.signature) {
yield {
type: "ant_thinking",
thinking: thinkingDeltaAccumulator,
signature: chunk.delta.signature,
}
}
break
case "text_delta":
yield {
@@ -247,6 +220,10 @@ export class AnthropicHandler implements ApiHandler {
text: chunk.delta.text,
}
break
case "signature_delta":
// We don't need to do anything with the signature in the client
// It's used when sending the thinking block back to the API
break
}
break
case "content_block_stop":
+1 -1
View File
@@ -162,7 +162,7 @@ export class ClineHandler implements ApiHandler {
// @ts-ignore-next-line
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
if (this.getModel().id === "cline/code-supernova-1-million") {
if (this.getModel().id === "cline/code-supernova") {
totalCost = 0
}
-15
View File
@@ -122,21 +122,6 @@ export class OpenRouterHandler implements ApiHandler {
}
}
// OpenRouter passes reasoning details that we can pass back unmodified in api requests to preserve reasoning traces for model
// See: https://openrouter.ai/docs/use-cases/reasoning-tokens#preserving-reasoning-blocks
if (
"reasoning_details" in delta &&
delta.reasoning_details &&
// @ts-ignore-next-line
delta.reasoning_details.length && // exists and non-0
!shouldSkipReasoningForModel(this.options.openRouterModelId)
) {
yield {
type: "reasoning_details",
reasoning_details: delta.reasoning_details,
}
}
if (!didOutputUsage && chunk.usage) {
yield {
type: "usage",
-2
View File
@@ -9,7 +9,6 @@ import { ModelInfo, SapAiCoreModelId, sapAiCoreDefaultModelId, sapAiCoreModels }
import axios from "axios"
import OpenAI from "openai"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
@@ -455,7 +454,6 @@ export class SapAiCoreHandler implements ApiHandler {
return this.deployments?.some((d) => d.name.split(":")[0].toLowerCase() === modelId.split(":")[0].toLowerCase()) ?? false
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
if (this.options.sapAiCoreUseOrchestrationMode) {
yield* this.createMessageWithOrchestration(systemPrompt, messages)
-10
View File
@@ -115,15 +115,7 @@ export function convertToOpenAiMessages(
// Process non-tool messages
let content: string | undefined
const reasoningDetails: any[] = []
if (nonToolMessages.length > 0) {
nonToolMessages.forEach((part) => {
// @ts-ignore-next-line
if (part.type === "text" && part.reasoning_details) {
// @ts-ignore-next-line
reasoningDetails.push(part.reasoning_details)
}
})
content = nonToolMessages
.map((part) => {
if (part.type === "image") {
@@ -150,8 +142,6 @@ export function convertToOpenAiMessages(
content,
// Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty
tool_calls: tool_calls.length > 0 ? tool_calls : undefined,
// @ts-ignore-next-line
reasoning_details: reasoningDetails,
})
}
}
+1 -23
View File
@@ -1,11 +1,5 @@
export type ApiStream = AsyncGenerator<ApiStreamChunk>
export type ApiStreamChunk =
| ApiStreamTextChunk
| ApiStreamReasoningChunk
| ApiStreamReasoningDetailsChunk
| ApiStreamAnthropicThinkingChunk
| ApiStreamAnthropicRedactedThinkingChunk
| ApiStreamUsageChunk
export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamReasoningChunk | ApiStreamUsageChunk
export interface ApiStreamTextChunk {
type: "text"
@@ -17,22 +11,6 @@ export interface ApiStreamReasoningChunk {
reasoning: string
}
export interface ApiStreamReasoningDetailsChunk {
type: "reasoning_details"
reasoning_details: any // openrouter has various properties that we can pass back unmodified in api requests to preserve reasoning traces
}
export interface ApiStreamAnthropicThinkingChunk {
type: "ant_thinking"
thinking: string
signature: string
}
export interface ApiStreamAnthropicRedactedThinkingChunk {
type: "ant_redacted_thinking"
data: string
}
export interface ApiStreamUsageChunk {
type: "usage"
inputTokens: number
@@ -9,11 +9,11 @@ import { Controller } from ".."
* @param request The request message containing the file path in the 'value' field
* @returns Empty response
*/
export async function openDiskConversationHistory(_controller: Controller, request: StringRequest): Promise<Empty> {
export async function openTaskHistory(_controller: Controller, request: StringRequest): Promise<Empty> {
const globalStoragePath = HostProvider.get().globalStorageFsPath
const taskConversationHistoryPath = path.join(globalStoragePath, "tasks", request.value, "api_conversation_history.json")
const taskHistoryPath = path.join(globalStoragePath, "tasks", request.value, "api_conversation_history.json")
if (request.value) {
openFileIntegration(taskConversationHistoryPath)
openFileIntegration(taskHistoryPath)
}
return Empty.create()
}
@@ -242,7 +242,7 @@ export async function refreshOpenRouterModels(
* Stealth models are models that are compatible with the OpenRouter API but not listed on the OpenRouter website or API.
*/
const CLINE_STEALTH_MODELS: Record<string, OpenRouterModelInfo> = {
"cline/code-supernova-1-million": OpenRouterModelInfo.create({
"cline/code-supernova": OpenRouterModelInfo.create({
maxTokens: clineCodeSupernovaModelInfo.maxTokens ?? 0,
contextWindow: clineCodeSupernovaModelInfo.contextWindow ?? 0,
supportsImages: clineCodeSupernovaModelInfo.supportsImages ?? false,
+1 -1
View File
@@ -99,7 +99,7 @@ export async function ensureMcpServersDirectoryExists(): Promise<string> {
try {
await fs.mkdir(mcpServersDir, { recursive: true })
} catch (_error) {
return path.join(os.homedir(), "Documents", "Cline", "MCP") // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine since this path is only ever used in the system prompt
return "~/Documents/Cline/MCP" // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine since this path is only ever used in the system prompt
}
return mcpServersDir
}
+3 -5
View File
@@ -1,4 +1,4 @@
import { ANTHROPIC_MIN_THINKING_BUDGET, ApiProvider, fireworksDefaultModelId, type OcaModelInfo } from "@shared/api"
import { ApiProvider, fireworksDefaultModelId, type OcaModelInfo } from "@shared/api"
import { ExtensionContext } from "vscode"
import { Controller } from "@/core/controller"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@/shared/AutoApprovalSettings"
@@ -461,9 +461,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
// Plan mode configurations
planModeApiProvider: planModeApiProvider || apiProvider,
planModeApiModelId,
// undefined means it was never modified, 0 means it was turned off
// (having this on by default ensures that <thinking> text does not pollute the user's chat and is instead rendered as reasoning)
planModeThinkingBudgetTokens: planModeThinkingBudgetTokens ?? ANTHROPIC_MIN_THINKING_BUDGET,
planModeThinkingBudgetTokens,
planModeReasoningEffort,
planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected,
@@ -497,7 +495,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
// Act mode configurations
actModeApiProvider: actModeApiProvider || apiProvider,
actModeApiModelId,
actModeThinkingBudgetTokens: actModeThinkingBudgetTokens ?? ANTHROPIC_MIN_THINKING_BUDGET,
actModeThinkingBudgetTokens,
actModeReasoningEffort,
actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected,
+2 -35
View File
@@ -1218,6 +1218,7 @@ export class Task {
if (userFeedback) {
await this.say("user_feedback", userFeedback.text, userFeedback.images, userFeedback.files)
await this.checkpointManager?.saveCheckpoint()
let fileContentString = ""
if (userFeedback.files && userFeedback.files.length > 0) {
@@ -1998,8 +1999,6 @@ export class Task {
const stream = this.attemptApiRequest(previousApiReqIndex) // yields only if the first chunk is successful, otherwise will allow the user to retry the request (most likely due to rate limit error, which gets thrown on the first chunk)
let assistantMessage = ""
let reasoningMessage = ""
const reasoningDetails = []
const antThinkingContent: (Anthropic.Messages.RedactedThinkingBlock | Anthropic.Messages.ThinkingBlock)[] = []
this.taskState.isStreaming = true
let didReceiveUsageChunk = false
try {
@@ -2024,24 +2023,6 @@ export class Task {
await this.say("reasoning", reasoningMessage, undefined, undefined, true)
}
break
// for cline/openrouter providers
case "reasoning_details":
reasoningDetails.push(chunk.reasoning_details)
break
// for anthropic providers
case "ant_thinking":
antThinkingContent.push({
type: "thinking",
thinking: chunk.thinking,
signature: chunk.signature,
})
break
case "ant_redacted_thinking":
antThinkingContent.push({
type: "redacted_thinking",
data: chunk.data,
})
break
case "text": {
if (reasoningMessage && assistantMessage.length === 0) {
// complete reasoning message
@@ -2171,21 +2152,7 @@ export class Task {
await this.messageStateHandler.addToApiConversationHistory({
role: "assistant",
content: [
// This is critical for maintaining the models reasoning flow and conversation integrity.
// "When providing thinking blocks, the entire sequence of consecutive thinking blocks must match the outputs generated by the model during the original request; you cannot rearrange or modify the sequence of these blocks." The signature_delta is used to verify that the thinking was generated by Claude, and the thinking blocks will be ignored if it's incorrect or missing.
// https://docs.claude.com/en/docs/build-with-claude/extended-thinking#preserving-thinking-blocks
...antThinkingContent,
{
type: "text",
text: assistantMessage,
// reasoning_details only exists for cline/openrouter providers
// @ts-ignore-next-line
reasoning_details: reasoningDetails.length > 0 ? reasoningDetails : undefined,
},
] as Array<
Anthropic.Messages.RedactedThinkingBlock | Anthropic.Messages.ThinkingBlock | Anthropic.Messages.TextBlock
>,
content: [{ type: "text", text: assistantMessage }],
})
// NOTE: this comment is here for future reference - this was a workaround for userMessageContent not getting set to true. It was due to it not recursively calling for partial blocks when didRejectTool, so it would get stuck waiting for a partial block to complete before it could continue.
+1 -1
View File
@@ -1,9 +1,9 @@
import { ApiHandler } from "@core/api"
import { execSync } from "child_process"
import { showSystemNotification } from "@/integrations/notifications"
import { ClineApiReqCancelReason, ClineApiReqInfo } from "@/shared/ExtensionMessage"
import { calculateApiCostAnthropic } from "@/utils/cost"
import { MessageStateHandler } from "./message-state"
import { execSync } from "child_process"
export const showNotificationForApprovalIfAutoApprovalEnabled = (
message: string,
+1 -2
View File
@@ -60,8 +60,7 @@ export abstract class BaseGrpcClient<TClient> {
protected getClient(): TClient {
if (!this.client || !this.channel) {
const channelOptions = { "grpc.enable_http_proxy": 0 }
this.channel = createChannel(this.address, undefined, channelOptions)
this.channel = createChannel(this.address)
this.client = this.createClient(this.channel)
}
return this.client
@@ -50,6 +50,7 @@ export class ServiceRegistry {
}
this.methodMetadata[methodName] = { isStreaming, ...metadata }
console.log(`Registered ${this.serviceName} method: ${methodName}${isStreaming ? " (streaming)" : ""}`)
}
/**
+1 -12
View File
@@ -262,7 +262,6 @@ export const CLAUDE_SONNET_4_1M_TIERS = [
// https://docs.anthropic.com/en/docs/about-claude/models // prices updated 2025-01-02
export type AnthropicModelId = keyof typeof anthropicModels
export const anthropicDefaultModelId: AnthropicModelId = "claude-sonnet-4-20250514"
export const ANTHROPIC_MIN_THINKING_BUDGET = 1_024
export const anthropicModels = {
"claude-sonnet-4-20250514:1m": {
maxTokens: 8192,
@@ -597,7 +596,7 @@ export const openRouterDefaultModelInfo: ModelInfo = {
// Cline custom model - code-supernova
export const clineCodeSupernovaModelInfo: ModelInfo = {
contextWindow: 1000000,
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0,
@@ -2510,16 +2509,6 @@ export const nebiusDefaultModelId = "Qwen/Qwen2.5-32B-Instruct-fast" satisfies N
export type XAIModelId = keyof typeof xaiModels
export const xaiDefaultModelId: XAIModelId = "grok-4"
export const xaiModels = {
"grok-4-fast-reasoning": {
maxTokens: 30000,
contextWindow: 2000000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.2,
cacheReadsPrice: 0.05,
outputPrice: 0.5,
description: "xAI's Grok 4 Fast (free) multimodal model with 2M context.",
},
"grok-4": {
maxTokens: 8192,
contextWindow: 262144,
+1 -2
View File
@@ -32,8 +32,7 @@ function createHealthClient(address: string) {
const healthDef = protoLoader.loadSync(health.protoPath)
const grpcObj = grpc.loadPackageDefinition(healthDef) as unknown as any
const Health = grpcObj.grpc.health.v1.Health
const opts: grpc.ChannelOptions = { "grpc.enable_http_proxy": 0 }
return new Health(address, grpc.credentials.createInsecure(), opts)
return new Health(address, grpc.credentials.createInsecure())
}
async function checkHealthOnce(client: any): Promise<boolean> {
+72 -48
View File
@@ -1,65 +1,89 @@
import { expect } from "@playwright/test"
import { e2e } from "./utils/helpers"
import { E2E_WORKSPACE_TYPES, e2e } from "./utils/helpers"
e2e("Chat - can send messages and switch between modes", async ({ helper, sidebar, page }) => {
// Sign in
await helper.signin(sidebar)
e2e.describe("Chat - can send messages and switch between modes", () => {
E2E_WORKSPACE_TYPES.forEach(({ title, workspaceType }) => {
e2e.extend({
workspaceType,
})(title, async ({ helper, sidebar, page }) => {
// Sign in
await helper.signin(sidebar)
// Submit a message
const inputbox = sidebar.getByTestId("chat-input")
await expect(inputbox).toBeVisible()
await inputbox.fill("Hello, Cline!")
await expect(inputbox).toHaveValue("Hello, Cline!")
await sidebar.getByTestId("send-button").click({ delay: 100 })
await expect(inputbox).toHaveValue("")
// Submit a message
const inputbox = sidebar.getByTestId("chat-input")
await expect(inputbox).toBeVisible()
await inputbox.fill("Hello, Cline!")
await expect(inputbox).toHaveValue("Hello, Cline!")
await sidebar.getByTestId("send-button").click({ delay: 100 })
await expect(inputbox).toHaveValue("")
// Loading State initially
await expect(sidebar.getByText("API Request...")).toBeVisible()
// Loading State initially
await expect(sidebar.getByText("API Request...")).toBeVisible()
// Starting a new task should clear the current chat view and show the recent tasks
await sidebar.getByRole("button", { name: "New Task" }).click()
await expect(sidebar.getByText("Recent Tasks")).toBeVisible()
await expect(sidebar.getByText("Hello, Cline!")).toBeVisible()
// The request should eventually fail
await expect(sidebar.getByText("API Request Failed")).toBeVisible()
// Makes sure the act and plan switches are working correctly
// Aria-checked state should be true for Act and false for Plan
const actButton = sidebar.getByRole("switch", { name: "Act" })
const planButton = sidebar.getByRole("switch", { name: "Plan" })
await expect(inputbox).toBeVisible()
await expect(actButton).toBeChecked()
await expect(planButton).not.toBeChecked()
await expect(sidebar.getByRole("button", { name: "Retry" })).toBeVisible()
await expect(sidebar.getByRole("button", { name: "Start New Task" })).toBeVisible()
await actButton.click()
await expect(actButton).not.toBeChecked()
await expect(planButton).toBeChecked()
// Starting a new task should clear the current chat view and show the recent tasks
await sidebar.getByRole("button", { name: "Start New Task" }).click()
await expect(sidebar.getByText("API Request Failed")).not.toBeVisible()
await expect(sidebar.getByText("Recent Tasks")).toBeVisible()
await expect(sidebar.getByText("Hello, Cline!")).toBeVisible()
// === slash commands preserve following text ===
await expect(inputbox).toHaveValue("")
// Type partial slash command to trigger menu
await inputbox.pressSequentially("/new", { delay: 100 })
// Makes sure the act and plan switches are working correctly
// Aria-checked state should be true for Act and false for Plan
const actButton = sidebar.getByRole("switch", { name: "Act" })
const planButton = sidebar.getByRole("switch", { name: "Plan" })
// Wait for menu to be visible and select first option with Tab
await inputbox.press("Tab")
await expect(inputbox).toHaveValue("/newtask ")
await expect(actButton).toBeChecked()
await expect(planButton).not.toBeChecked()
// Add following text to verify it works correctly
await inputbox.pressSequentially("following text should be preserved")
await expect(inputbox).toHaveValue("/newtask following text should be preserved")
await actButton.click()
await expect(actButton).not.toBeChecked()
await expect(planButton).toBeChecked()
// === @ mentions preserve following text ===
await inputbox.fill("")
await expect(inputbox).toHaveValue("")
await inputbox.fill("Plan mode submission")
await sidebar.getByTestId("send-button").click()
// Type partial @ mention to trigger menu
await inputbox.pressSequentially("@prob")
await expect(sidebar.getByText("API Request Failed")).toBeVisible()
// Wait for menu to be visible and select first option with Tab
await inputbox.press("Tab")
await expect(inputbox).toHaveValue("@problems ")
// === slash commands preserve following text ===
await inputbox.fill("")
await expect(inputbox).toHaveValue("")
await inputbox.focus()
// Add following text to verify it works correctly
await inputbox.pressSequentially("following text should be preserved")
await expect(inputbox).toHaveValue("@problems following text should be preserved")
// Type partial slash command to trigger menu
await inputbox.pressSequentially("/new")
await page.close()
// Wait for menu to be visible and select first option with Tab
await inputbox.press("Tab")
await expect(inputbox).toHaveValue("/newtask ")
// Add following text to verify it works correctly
await inputbox.pressSequentially("following text should be preserved")
await expect(inputbox).toHaveValue("/newtask following text should be preserved")
// === @ mentions preserve following text ===
await inputbox.fill("")
await expect(inputbox).toHaveValue("")
await inputbox.focus()
// Type partial @ mention to trigger menu
await inputbox.pressSequentially("@prob")
// Wait for menu to be visible and select first option with Tab
await inputbox.press("Tab")
await expect(inputbox).toHaveValue("@problems ")
// Add following text to verify it works correctly
await inputbox.pressSequentially("following text should be preserved")
await expect(inputbox).toHaveValue("@problems following text should be preserved")
await page.close()
})
})
})
@@ -73,8 +73,8 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
}
const setCodeSupernova = () => {
const modelId = "cline/code-supernova-1-million"
// set both plan and act modes to use code-supernova-1-million
const modelId = "cline/code-supernova"
// set both plan and act modes to use code-supernova
handleFieldsChange({
planModeOpenRouterModelId: modelId,
actModeOpenRouterModelId: modelId,
@@ -124,20 +124,20 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
</VSCodeButtonLink>
</li>
<li>
<b>Free Models:</b> Try the new code-supernova-1-million stealth model, or grok-code-fast-1 for free!
<b>Continued Free Models:</b> Try grok-code-fast-1 or code-supernova (stealth model 🥷)!
<br />
{user ? (
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap", margin: "5px 0" }}>
{!didClickCodeSupernovaButton && (
<VSCodeButton appearance="primary" onClick={setCodeSupernova}>
Try code-supernova
</VSCodeButton>
)}
{!didClickGrokCodeButton && (
<VSCodeButton appearance="primary" onClick={setGrokCodeFast1}>
Try grok-code-fast-1
</VSCodeButton>
)}
{!didClickCodeSupernovaButton && (
<VSCodeButton appearance="primary" onClick={setCodeSupernova}>
Try code-supernova
</VSCodeButton>
)}
</div>
) : (
<VSCodeButton appearance="primary" onClick={handleShowAccount} style={{ margin: "5px 0" }}>
@@ -381,7 +381,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
scrollToBottomSmooth: scrollBehavior.scrollToBottomSmooth,
disableAutoScrollRef: scrollBehavior.disableAutoScrollRef,
showScrollToBottom: scrollBehavior.showScrollToBottom,
virtuosoRef: scrollBehavior.virtuosoRef,
}}
task={task}
/>
@@ -3,7 +3,6 @@ import type { Mode } from "@shared/storage/types"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import type React from "react"
import { useCallback, useEffect, useMemo, useState } from "react"
import { VirtuosoHandle } from "react-virtuoso"
import { ButtonActionType, getButtonConfig } from "../../shared/buttonConfig"
import type { ChatState, MessageHandlers } from "../../types/chatTypes"
@@ -17,7 +16,6 @@ interface ActionButtonsProps {
scrollToBottomSmooth: () => void
disableAutoScrollRef: React.MutableRefObject<boolean>
showScrollToBottom: boolean
virtuosoRef: React.RefObject<VirtuosoHandle>
}
}
@@ -96,53 +94,41 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({
const { showScrollToBottom, scrollToBottomSmooth, disableAutoScrollRef } = scrollBehavior
const { primaryText, secondaryText, primaryAction, secondaryAction, enableButtons } = buttonConfig
const hasButtons = primaryText || secondaryText
const isStreaming = task.partial === true
const canInteract = enableButtons && !isProcessing
// Early return for scroll button to avoid unnecessary computation
if (showScrollToBottom || !hasButtons) {
if (showScrollToBottom) {
const handleScrollToBottom = () => {
scrollToBottomSmooth()
disableAutoScrollRef.current = false
}
// Show scroll to top button when there are no action buttons
const handleScrollToTop = () => {
scrollBehavior.virtuosoRef.current?.scrollTo({
top: 0,
behavior: "smooth",
})
disableAutoScrollRef.current = true
}
return (
<div className="flex px-[15px]">
<VSCodeButton
appearance="icon"
aria-label={showScrollToBottom ? "Scroll to bottom" : "Scroll to top"}
aria-label="Scroll to bottom"
className="text-lg text-[var(--vscode-primaryButton-foreground)] bg-[color-mix(in_srgb,var(--vscode-toolbar-hoverBackground)_55%,transparent)] rounded-[3px] overflow-hidden cursor-pointer flex justify-center items-center flex-1 h-[25px] hover:bg-[color-mix(in_srgb,var(--vscode-toolbar-hoverBackground)_90%,transparent)] active:bg-[color-mix(in_srgb,var(--vscode-toolbar-hoverBackground)_70%,transparent)] border-0"
onClick={showScrollToBottom ? handleScrollToBottom : handleScrollToTop}
onClick={handleScrollToBottom}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
if (showScrollToBottom) {
handleScrollToBottom()
} else {
handleScrollToTop()
}
handleScrollToBottom()
}
}}>
{showScrollToBottom ? (
<span className="codicon codicon-chevron-down" />
) : (
<span className="codicon codicon-chevron-up" />
)}
<span className="codicon codicon-chevron-down" />
</VSCodeButton>
</div>
)
}
const { primaryText, secondaryText, primaryAction, secondaryAction, enableButtons } = buttonConfig
const hasButtons = primaryText || secondaryText
const isStreaming = task.partial === true
const canInteract = enableButtons && !isProcessing
if (!hasButtons) {
return null
}
const opacity = canInteract || isStreaming ? 1 : 0.5
return (
@@ -10,7 +10,7 @@ import { UiServiceClient } from "@/services/grpc-client"
import CopyTaskButton from "./buttons/CopyTaskButton"
import DeleteTaskButton from "./buttons/DeleteTaskButton"
import NewTaskButton from "./buttons/NewTaskButton"
import OpenDiskConversationHistoryButton from "./buttons/OpenDiskConversationHistoryButton"
import OpenDiskTaskHistoryButton from "./buttons/OpenDiskTaskHistoryButton"
import { CheckpointError } from "./CheckpointError"
import ContextWindow from "./ContextWindow"
import { FocusChain } from "./FocusChain"
@@ -117,9 +117,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
taskSize={currentTaskItem?.size}
/>
{/* Only visible in development mode */}
{IS_DEV && (
<OpenDiskConversationHistoryButton className={BUTTON_CLASS} taskId={currentTaskItem?.id} />
)}
{IS_DEV && <OpenDiskTaskHistoryButton className={BUTTON_CLASS} taskId={currentTaskItem?.id} />}
</div>
)}
</div>
@@ -1,36 +0,0 @@
import { Button, cn } from "@heroui/react"
import { StringRequest } from "@shared/proto/cline/common"
import { ArrowDownToLineIcon } from "lucide-react"
import HeroTooltip from "@/components/common/HeroTooltip"
import { FileServiceClient } from "@/services/grpc-client"
const OpenDiskConversationHistoryButton: React.FC<{
taskId?: string
className?: string
}> = ({ taskId, className }) => {
const handleOpenDiskConversationHistory = () => {
if (!taskId) {
return
}
FileServiceClient.openDiskConversationHistory(StringRequest.create({ value: taskId })).catch((err) => {
console.error(err)
})
}
return (
<HeroTooltip content="Open Conversation History File" placement="right">
<Button
aria-label="Open Disk Conversation History"
className={cn("flex items-center border-0 text-sm font-bold bg-transparent hover:opacity-100", className)}
isIconOnly={true}
onPress={() => handleOpenDiskConversationHistory()}
radius="sm"
size="sm">
<ArrowDownToLineIcon size="14" />
</Button>
</HeroTooltip>
)
}
export default OpenDiskConversationHistoryButton
@@ -0,0 +1,34 @@
import { Button, cn } from "@heroui/react"
import { StringRequest } from "@shared/proto/cline/common"
import { ArrowDownToLineIcon } from "lucide-react"
import { FileServiceClient } from "@/services/grpc-client"
const OpenDiskTaskHistoryButton: React.FC<{
taskId?: string
className?: string
}> = ({ taskId, className }) => {
const handleOpenDiskTaskHistory = () => {
if (!taskId) {
return
}
FileServiceClient.openTaskHistory(StringRequest.create({ value: taskId })).catch((err) => {
console.error(err)
})
}
return (
<Button
aria-label="Open Disk Task History"
className={cn("flex items-center border-0 text-sm font-bold bg-transparent hover:opacity-100", className)}
isIconOnly={true}
onPress={() => handleOpenDiskTaskHistory()}
radius="sm"
size="sm"
title="Export Task">
<ArrowDownToLineIcon size="14" />
</Button>
)
}
export default OpenDiskTaskHistoryButton
@@ -22,7 +22,7 @@ const HeroTooltip: React.FC<HeroTooltipProps> = ({
className,
showArrow = false,
delay = 0,
closeDelay = 500,
closeDelay = 100,
placement = "top",
disabled = false,
}) => {
@@ -46,15 +46,20 @@ const HeroTooltip: React.FC<HeroTooltipProps> = ({
return (
<Tooltip
classNames={{
content: "hero-tooltip-content pointer-events-none", // Prevent hovering over tooltip
base: "pointer-events-none", // Prevent hovering over tooltip container
content: "hero-tooltip-content pointer-events-none", // Prevent hovering over tooltip content
}}
closeDelay={closeDelay}
content={formattedContent} // Immediate close when cursor moves away
content={formattedContent}
delay={delay}
disableAnimation={true}
isDisabled={disabled}
placement={placement} // Disable animation for immediate appearance/disappearance
showArrow={showArrow}>
placement={placement}
showArrow={showArrow}
// Inline style to override any library styles - above classNames aren't applying correctly
style={{
pointerEvents: "none",
}}>
{children}
</Tooltip>
)
+28 -28
View File
@@ -1,41 +1,41 @@
import { Int64Request } from "@shared/proto/cline/common"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useCallback } from "react"
import { PlatformType } from "@/config/platform.config"
import { usePlatform } from "@/context/PlatformContext"
import { StateServiceClient } from "@/services/grpc-client"
export const CURRENT_INFO_BANNER_VERSION = 1
export const InfoBanner: React.FC = () => {
const handleClose = useCallback((e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
StateServiceClient.updateInfoBannerVersion({ value: CURRENT_INFO_BANNER_VERSION }).catch(console.error)
const request = Int64Request.create({
value: CURRENT_INFO_BANNER_VERSION,
})
StateServiceClient.updateInfoBannerVersion(request).catch(console.error)
}, [])
if (usePlatform().type === PlatformType.VSCODE) {
return (
<a
className="bg-banner-background px-3 py-2 flex flex-col gap-1 shrink-0 mb-1 relative text-sm m-4 no-underline transition-colors hover:brightness-120"
href="https://docs.cline.bot/features/customization/opening-cline-in-sidebar"
rel="noopener noreferrer"
style={{ color: "var(--vscode-foreground)" }}
target="_blank">
<h3 className="m-0">💡 Cline in the Right Sidebar</h3>
<p className="m-0">
Keep your files visible when chatting with Cline. Drag the Cline icon to the right sidebar panel for a better
experience. <span className="text-link cursor-pointer">See how </span>
</p>
{/* Close button */}
<VSCodeButton
appearance="icon"
data-testid="info-banner-close-button"
onClick={handleClose}
style={{ position: "absolute", top: "8px", right: "8px" }}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
</a>
)
}
return null
return (
<a
className="bg-banner-background px-3 py-2 flex flex-col gap-1 shrink-0 mb-1 relative text-sm m-4 no-underline transition-colors hover:brightness-120"
href="https://docs.cline.bot/features/customization/opening-cline-in-sidebar"
rel="noopener noreferrer"
style={{ color: "var(--vscode-foreground)" }}
target="_blank">
<h3 className="m-0">💡 Cline in the Right Sidebar</h3>
<p className="m-0">
Keep your files visible when chatting with Cline. Drag the Cline icon to the right sidebar panel for a better
experience. <span className="text-link cursor-pointer">See how </span>
</p>
{/* Close button */}
<VSCodeButton
appearance="icon"
data-testid="info-banner-close-button"
onClick={handleClose}
style={{ position: "absolute", top: "8px", right: "8px" }}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
</a>
)
}
export default InfoBanner
@@ -61,7 +61,7 @@ const featuredModels = [
label: "Free",
},
{
id: "cline/code-supernova-1-million",
id: "cline/code-supernova",
description: "Stealth coding model with image support",
label: "Free",
},
@@ -1,4 +1,4 @@
import { ANTHROPIC_MIN_THINKING_BUDGET, anthropicModels, geminiDefaultModelId, geminiModels } from "@shared/api"
import { anthropicModels, geminiDefaultModelId, geminiModels } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { memo, useCallback, useEffect, useMemo, useState } from "react"
@@ -8,6 +8,7 @@ import { getModeSpecificFields } from "./utils/providerUtils"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
// Constants
const DEFAULT_MIN_VALID_TOKENS = 1024
const MAX_PERCENTAGE = 0.8
const THUMB_SIZE = 16
@@ -142,7 +143,7 @@ const ThinkingBudgetSlider = ({ maxBudget, currentMode }: ThinkingBudgetSliderPr
const handleToggleChange = (event: any) => {
const isChecked = (event.target as HTMLInputElement).checked
const newThinkingBudgetValue = isChecked ? ANTHROPIC_MIN_THINKING_BUDGET : 0
const newThinkingBudgetValue = isChecked ? DEFAULT_MIN_VALID_TOKENS : 0
setIsEnabled(isChecked)
setLocalValue(newThinkingBudgetValue)
@@ -168,16 +169,16 @@ const ThinkingBudgetSlider = ({ maxBudget, currentMode }: ThinkingBudgetSliderPr
</LabelContainer>
<RangeInput
$max={maxSliderValue}
$min={ANTHROPIC_MIN_THINKING_BUDGET}
$min={DEFAULT_MIN_VALID_TOKENS}
$value={localValue}
aria-describedby="thinking-budget-description"
aria-label={`Thinking budget: ${localValue.toLocaleString()} tokens`}
aria-valuemax={maxSliderValue}
aria-valuemin={ANTHROPIC_MIN_THINKING_BUDGET}
aria-valuemin={DEFAULT_MIN_VALID_TOKENS}
aria-valuenow={localValue}
id="thinking-budget-slider"
max={maxSliderValue}
min={ANTHROPIC_MIN_THINKING_BUDGET}
min={DEFAULT_MIN_VALID_TOKENS}
onChange={handleSliderChange}
onMouseUp={handleSliderComplete}
onTouchEnd={handleSliderComplete}
-21
View File
@@ -1,7 +1,6 @@
import platformConfigs from "./platform-configs.json"
export interface PlatformConfig {
type: PlatformType
messageEncoding: MessageEncoding
showNavbar: boolean
postMessage: PostMessageFunction
@@ -11,24 +10,6 @@ export interface PlatformConfig {
supportsTerminalMentions: boolean
}
export enum PlatformType {
VSCODE = 0,
STANDALONE = 1,
}
function stringToPlatformType(name: string): PlatformType {
const mapping: Record<string, PlatformType> = {
vscode: PlatformType.VSCODE,
standalone: PlatformType.STANDALONE,
}
if (name in mapping) {
return mapping[name]
}
console.error("Unknown platform:", name)
// Default to VSCode for unknown types
return PlatformType.VSCODE
}
// Internal type for JSON structure (not exported)
type PlatformConfigJson = {
messageEncoding: "none" | "json"
@@ -95,9 +76,7 @@ const selectedConfig = configs[__PLATFORM__]
console.log("[PLATFORM_CONFIG] Build platform:", __PLATFORM__)
// Build the platform config with injected functions
// Callers should use this in the situations where the react component is not available.
export const PLATFORM_CONFIG: PlatformConfig = {
type: stringToPlatformType(__PLATFORM__),
messageEncoding: selectedConfig.messageEncoding,
showNavbar: selectedConfig.showNavbar,
postMessage: postMessageStrategies[selectedConfig.postMessageHandler],