mirror of
https://github.com/cline/cline.git
synced 2026-09-01 23:19:18 +08:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cd8832734 | |||
| 5799c911db | |||
| 9a17b9e425 | |||
| 29d5371198 | |||
| 8384ffb26e | |||
| 7507056339 | |||
| cac0cefff0 | |||
| 462fdbb319 | |||
| ff4f450ff6 | |||
| 7eff717322 | |||
| bd3e089962 | |||
| 25f8b36022 | |||
| 0d09b0353b | |||
| 237005536e | |||
| 4877fb16e9 | |||
| 069223d75d | |||
| 24094c31ff | |||
| 67915fd246 |
@@ -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: string = "1.0.0",
|
||||
version = "1.0.0",
|
||||
) {
|
||||
this.version = version
|
||||
}
|
||||
@@ -402,7 +402,7 @@ export class ACPHostBridgeClientProvider implements HostBridgeClientProvider {
|
||||
clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
sessionIdResolver: SessionIdResolver,
|
||||
cwdResolver: CwdResolver,
|
||||
version: string = "1.0.0",
|
||||
version = "1.0.0",
|
||||
) {
|
||||
this.workspaceClient = new ACPWorkspaceServiceClient(clientCapabilities, sessionIdResolver, cwdResolver)
|
||||
this.envClient = new ACPEnvServiceClient(clientCapabilities, sessionIdResolver, version)
|
||||
|
||||
@@ -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: boolean = false): ButtonConfig {
|
||||
export function getButtonConfig(message: ClineMessage | undefined, isStreaming = 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 = parseInt(input, 10)
|
||||
const num = Number.parseInt(input, 10)
|
||||
if (textInput === "" && !Number.isNaN(num) && num >= 1 && num <= parts.options.length) {
|
||||
const selectedOption = parts.options[num - 1]
|
||||
sendResponse("messageResponse", selectedOption)
|
||||
@@ -401,43 +401,42 @@ function getCliMessagePrefixIcon(message: ClineMessage): string {
|
||||
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 " "
|
||||
}
|
||||
}
|
||||
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 " "
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: number = 60) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
const delay = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
// Type for our exit mock function
|
||||
type ExitMockFn = ReturnType<typeof vi.fn> & (() => void)
|
||||
|
||||
@@ -120,7 +120,7 @@ export const CheckpointMenu: React.FC<CheckpointMenuProps> = ({ messages, onSele
|
||||
|
||||
// Quick number selection for checkpoints
|
||||
if (stage === "checkpoint") {
|
||||
const num = parseInt(input, 10)
|
||||
const num = Number.parseInt(input, 10)
|
||||
if (!Number.isNaN(num) && num >= 1 && num <= checkpoints.length) {
|
||||
setSelectedCheckpoint(num - 1)
|
||||
setStage("restoreType")
|
||||
|
||||
@@ -81,7 +81,7 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
* Get the maximum valid index for the featured model picker
|
||||
* (includes "Browse all" option if showBrowseAll is true)
|
||||
*/
|
||||
export function getFeaturedModelMaxIndex(showBrowseAll: boolean = true): number {
|
||||
export function getFeaturedModelMaxIndex(showBrowseAll = true): number {
|
||||
const featuredModels = getAllFeaturedModels()
|
||||
return showBrowseAll ? featuredModels.length : featuredModels.length - 1
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ interface FileMentionMenuProps {
|
||||
/**
|
||||
* Truncate path from the left if too long, keeping the filename visible
|
||||
*/
|
||||
function truncatePath(filePath: string, maxLength: number = 50): string {
|
||||
function truncatePath(filePath: string, maxLength = 50): string {
|
||||
if (filePath.length <= maxLength) {
|
||||
return filePath
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ interface HistoryViewProps {
|
||||
/**
|
||||
* Format separator
|
||||
*/
|
||||
function formatSeparator(char: string = "─", width: number = 80): string {
|
||||
function formatSeparator(char = "─", width = 80): string {
|
||||
return char.repeat(Math.max(width, 10))
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ function formatNumber(num: number): string {
|
||||
/**
|
||||
* Create a progress bar for context window usage
|
||||
*/
|
||||
function createContextBar(used: number, total: number, width: number = 8): string {
|
||||
function createContextBar(used: number, total: number, width = 8): string {
|
||||
const ratio = Math.min(used / total, 1)
|
||||
const filled = Math.round(ratio * width)
|
||||
const empty = width - filled
|
||||
|
||||
@@ -79,7 +79,7 @@ export class CliDiffServiceClient implements DiffServiceClientInterface {
|
||||
* CLI implementation of EnvService - handles environment operations
|
||||
*/
|
||||
export class CliEnvServiceClient implements EnvServiceClientInterface {
|
||||
private clipboardContent: string = ""
|
||||
private clipboardContent = ""
|
||||
|
||||
private getTelemetrySetting(): proto.host.Setting {
|
||||
// Read from StateManager - defaults to ENABLED if not set or "unset"
|
||||
|
||||
+40
-41
@@ -112,49 +112,48 @@ function getMessageIcon(message: ClineMessage): string {
|
||||
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 " "
|
||||
}
|
||||
}
|
||||
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: boolean = false): string {
|
||||
export function formatMessage(message: ClineMessage, verbose = false): string {
|
||||
const icon = getMessageIcon(message)
|
||||
const timestamp = formatTimestamp(message.ts)
|
||||
const lines: string[] = []
|
||||
@@ -289,7 +288,7 @@ function formatSayMessage(message: ClineMessage, prefix: string, verbose: boolea
|
||||
/**
|
||||
* Display a horizontal separator
|
||||
*/
|
||||
export function separator(char: string = "─", width: number = 60): string {
|
||||
export function separator(char = "─", width = 60): string {
|
||||
return style.dim(char.repeat(width))
|
||||
}
|
||||
|
||||
@@ -309,7 +308,7 @@ export function taskHeader(taskId: string, task?: string): string {
|
||||
/**
|
||||
* Format the current state for display
|
||||
*/
|
||||
export function formatState(state: ExtensionState, verbose: boolean = false): string {
|
||||
export function formatState(state: ExtensionState, verbose = false): string {
|
||||
const lines: string[] = []
|
||||
|
||||
if (state.currentTaskItem) {
|
||||
@@ -344,7 +343,7 @@ export class Spinner {
|
||||
private frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
||||
private frameIndex = 0
|
||||
private interval: NodeJS.Timeout | null = null
|
||||
private message: string = ""
|
||||
private message = ""
|
||||
|
||||
start(message: string) {
|
||||
this.message = message
|
||||
@@ -392,7 +391,7 @@ export function clearLine() {
|
||||
/**
|
||||
* Move cursor up n lines
|
||||
*/
|
||||
export function cursorUp(n: number = 1) {
|
||||
export function cursorUp(n = 1) {
|
||||
process.stdout.write(`\x1b[${n}A`)
|
||||
}
|
||||
|
||||
|
||||
@@ -183,9 +183,9 @@ export async function listWorkspaceFiles(workspacePath: string, limit = 5000): P
|
||||
|
||||
function countGaps(positions: Iterable<number>): number {
|
||||
let gaps = 0
|
||||
let prev = -Infinity
|
||||
let prev = Number.NEGATIVE_INFINITY
|
||||
for (const pos of positions) {
|
||||
if (prev !== -Infinity && pos - prev > 1) {
|
||||
if (prev !== Number.NEGATIVE_INFINITY && pos - prev > 1) {
|
||||
gaps++
|
||||
}
|
||||
prev = pos
|
||||
|
||||
@@ -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: number = 5): VisibleWindow<T> {
|
||||
export function getVisibleWindow<T>(items: T[], selectedIndex: number, maxVisible = 5): VisibleWindow<T> {
|
||||
if (items.length <= maxVisible) {
|
||||
return { items, startIndex: 0 }
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
export async function waitFor<T>(
|
||||
condition: () => T | undefined | null,
|
||||
timeoutMs: number,
|
||||
pollIntervalMs: number = 100,
|
||||
pollIntervalMs = 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: parseInt(nightlyMatch[2], 10),
|
||||
timestamp: Number.parseInt(nightlyMatch[2], 10),
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -104,15 +104,13 @@ 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 = 46;
|
||||
optional string openai_codex_oauth_credentials = 48;
|
||||
}
|
||||
|
||||
// NOTE: Add new fields under API_HANDLER_SETTINGS_FIELDS or USER_SETTINGS_FIELDS
|
||||
// in src/shared/storage/state-keys.ts and use the scripts/generate-state-proto.mjs
|
||||
// script to regenerate this list.
|
||||
message Settings {
|
||||
reserved 146; // was openai_reasoning_effort (moved to mode-scoped reasoning effort)
|
||||
|
||||
optional string lite_llm_base_url = 1;
|
||||
optional bool lite_llm_use_prompt_cache = 2;
|
||||
optional string anthropic_base_url = 4;
|
||||
@@ -251,7 +249,6 @@ message Settings {
|
||||
optional string default_terminal_profile = 137;
|
||||
optional int32 terminal_output_line_limit = 138;
|
||||
optional int32 max_consecutive_mistakes = 139;
|
||||
optional int32 subagent_terminal_output_line_limit = 140;
|
||||
optional bool strict_plan_mode_enabled = 141;
|
||||
optional bool yolo_mode_toggled = 142;
|
||||
optional bool use_auto_condense = 143;
|
||||
@@ -282,8 +279,8 @@ message Settings {
|
||||
optional int32 open_telemetry_log_max_queue_size = 171;
|
||||
optional bool worktrees_enabled = 172;
|
||||
optional bool auto_approve_all_toggled = 174;
|
||||
map<string, string> open_ai_headers = 175;
|
||||
optional bool double_check_completion_enabled = 176;
|
||||
map<string, string> open_ai_headers = 177;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
|
||||
@@ -42,6 +42,10 @@ service TaskService {
|
||||
rpc deleteAllTaskHistory(EmptyRequest) returns (DeleteAllTaskHistoryCount);
|
||||
// Explains changes with AI and adds inline comments to the diff view
|
||||
rpc explainChanges(ExplainChangesRequest) returns (Empty);
|
||||
// Switches to a different active task
|
||||
rpc switchTask(StringRequest) returns (BooleanResponse);
|
||||
// Cancels a specific task by ID
|
||||
rpc cancelTaskById(StringRequest) returns (Empty);
|
||||
}
|
||||
|
||||
// Request message for creating a new task
|
||||
|
||||
@@ -254,7 +254,7 @@ function parseProtoMessageFieldNumbers(protoContent, messageName) {
|
||||
|
||||
for (const fieldMatch of matches) {
|
||||
const snakeName = fieldMatch[1]
|
||||
const fieldNum = parseInt(fieldMatch[2], 10)
|
||||
const fieldNum = Number.parseInt(fieldMatch[2], 10)
|
||||
const camelName = snakeToCamel(snakeName)
|
||||
fieldNumbers[camelName] = fieldNum
|
||||
}
|
||||
@@ -354,11 +354,10 @@ function replaceMessage(protoContent, messageName, newMessageContent) {
|
||||
|
||||
if (messageRegex.test(protoContent)) {
|
||||
return protoContent.replace(messageRegex, newMessageContent)
|
||||
} else {
|
||||
// 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
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -21,9 +21,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
|
||||
// Stub os.homedir to return our temp directory
|
||||
originalHomedir = os.homedir
|
||||
sandbox
|
||||
.stub(os, "homedir")
|
||||
.returns(tempDir)
|
||||
sandbox.stub(os, "homedir").returns(tempDir)
|
||||
|
||||
// Reset the singleton state using internal method
|
||||
;(ClineEndpoint as any)._instance = null
|
||||
|
||||
+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: boolean = false
|
||||
private isBundled = false
|
||||
|
||||
private constructor() {
|
||||
// Set environment at module load. Use override if provided.
|
||||
|
||||
@@ -357,7 +357,13 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
const reasoningTokens = usage.output_tokens_details?.reasoning_tokens || 0
|
||||
const totalTokens = usage.total_tokens || 0
|
||||
Logger.log(`Total tokens from Responses API usage: ${totalTokens}`)
|
||||
const totalCost = calculateApiCostOpenAI(model.info, inputTokens, outputTokens + reasoningTokens, cacheWriteTokens, cacheReadTokens)
|
||||
const totalCost = calculateApiCostOpenAI(
|
||||
model.info,
|
||||
inputTokens,
|
||||
outputTokens + reasoningTokens,
|
||||
cacheWriteTokens,
|
||||
cacheReadTokens,
|
||||
)
|
||||
const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens)
|
||||
yield {
|
||||
type: "usage",
|
||||
|
||||
@@ -846,9 +846,8 @@ export class ContextManager {
|
||||
}
|
||||
// otherwise there are still file reads here we can overwrite, so still need to process this text chunk
|
||||
// to do so we need to keep track of which files we've already replaced so we don't replace them again
|
||||
else {
|
||||
thisExistingFileReads = blockUpdates[blockUpdates.length - 1][3][0]
|
||||
}
|
||||
|
||||
thisExistingFileReads = blockUpdates[blockUpdates.length - 1][3][0]
|
||||
}
|
||||
} else {
|
||||
// for all other cases we can assume that we dont need to check this again
|
||||
|
||||
+150
-24
@@ -54,6 +54,7 @@ import { fetchRemoteConfig } from "../storage/remote-config/fetch"
|
||||
import { clearRemoteConfig } from "../storage/remote-config/utils"
|
||||
import { type PersistenceErrorEvent, StateManager } from "../storage/StateManager"
|
||||
import { Task } from "../task"
|
||||
import { getTaskStatus } from "../task/TaskState"
|
||||
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
|
||||
import { getClineOnboardingModels } from "./models/getClineOnboardingModels"
|
||||
import { appendClineStealthModels } from "./models/refreshOpenRouterModels"
|
||||
@@ -68,8 +69,23 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
|
||||
*/
|
||||
|
||||
export class Controller {
|
||||
// The task that is currently being displayed in the UI.
|
||||
task?: Task
|
||||
|
||||
/**
|
||||
* Tracks all tasks that are currently running or awaiting user input in this controller instance.
|
||||
* Tasks are added when initialized via `initTask()` and should be removed when:
|
||||
* - The task is cancelled by the user (`cancelTask()`)
|
||||
* - The task is deleted from history (`deleteTaskFromState()`)
|
||||
* - The task completes naturally (user starts a new task after completion)
|
||||
* - The task is cleared (`clearTask()`)
|
||||
* - The controller is disposed
|
||||
*
|
||||
* This map enables parallel task execution where users can start new tasks without
|
||||
* cancelling existing ones. Tasks remain here until explicitly cleaned up.
|
||||
*/
|
||||
private activeTasks: Map<string, Task> = new Map()
|
||||
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
authService: AuthService
|
||||
@@ -171,8 +187,16 @@ export class Controller {
|
||||
this.remoteConfigTimer = undefined
|
||||
}
|
||||
|
||||
// Abort all active tasks before clearing
|
||||
for (const [, task] of this.activeTasks) {
|
||||
await task.abortTask().catch((error) => Logger.error("Failed to abort task during controller dispose:", error))
|
||||
}
|
||||
|
||||
await this.clearTask()
|
||||
this.mcpHub.dispose()
|
||||
this.activeTasks.clear()
|
||||
|
||||
this.task = undefined
|
||||
|
||||
Logger.error("Controller disposed")
|
||||
}
|
||||
@@ -243,8 +267,6 @@ export class Controller {
|
||||
// when done and catches all errors internally.
|
||||
fetchRemoteConfig(this)
|
||||
|
||||
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||
|
||||
const autoApprovalSettings = this.stateManager.getGlobalSettingsKey("autoApprovalSettings")
|
||||
const shellIntegrationTimeout = this.stateManager.getGlobalSettingsKey("shellIntegrationTimeout")
|
||||
const terminalReuseEnabled = this.stateManager.getGlobalStateKey("terminalReuseEnabled")
|
||||
@@ -326,6 +348,9 @@ export class Controller {
|
||||
taskLockAcquired,
|
||||
})
|
||||
|
||||
// Track the task in the active tasks map
|
||||
this.activeTasks.set(taskId, this.task)
|
||||
|
||||
if (historyItem) {
|
||||
this.task.resumeTaskFromHistory()
|
||||
} else if (task || images || files) {
|
||||
@@ -422,14 +447,22 @@ export class Controller {
|
||||
return false
|
||||
}
|
||||
|
||||
async cancelTask() {
|
||||
async cancelTask(taskId?: string) {
|
||||
// Prevent duplicate cancellations from spam clicking
|
||||
if (this.cancelInProgress) {
|
||||
Logger.log(`[Controller.cancelTask] Cancellation already in progress, ignoring duplicate request`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.task) {
|
||||
// Determine which task to cancel
|
||||
const targetTaskId = taskId || this.task?.taskId
|
||||
|
||||
if (targetTaskId && !this.activeTasks.has(targetTaskId) && this.task) {
|
||||
this.activeTasks.set(targetTaskId, this.task)
|
||||
}
|
||||
|
||||
const targetTask = targetTaskId ? this.activeTasks.get(targetTaskId) : undefined
|
||||
if (!targetTask) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -437,20 +470,23 @@ export class Controller {
|
||||
this.cancelInProgress = true
|
||||
|
||||
try {
|
||||
this.updateBackgroundCommandState(false)
|
||||
// If canceling current task, clear background command state
|
||||
if (targetTask === this.task) {
|
||||
this.updateBackgroundCommandState(false)
|
||||
}
|
||||
|
||||
try {
|
||||
await this.task.abortTask()
|
||||
await targetTask.abortTask()
|
||||
} catch (error) {
|
||||
Logger.error("Failed to abort task", error)
|
||||
}
|
||||
|
||||
await pWaitFor(
|
||||
() =>
|
||||
this.task === undefined ||
|
||||
this.task.taskState.isStreaming === false ||
|
||||
this.task.taskState.didFinishAbortingStream ||
|
||||
this.task.taskState.isWaitingForFirstChunk, // if only first chunk is processed, then there's no need to wait for graceful abort (closes edits, browser, etc)
|
||||
targetTask === undefined ||
|
||||
targetTask.taskState.isStreaming === false ||
|
||||
targetTask.taskState.didFinishAbortingStream ||
|
||||
targetTask.taskState.isWaitingForFirstChunk, // if only first chunk is processed, then there's no need to wait for graceful abort (closes edits, browser, etc)
|
||||
{
|
||||
timeout: 3_000,
|
||||
},
|
||||
@@ -458,18 +494,15 @@ export class Controller {
|
||||
Logger.error("Failed to abort task")
|
||||
})
|
||||
|
||||
if (this.task) {
|
||||
if (targetTask) {
|
||||
// 'abandoned' will prevent this cline instance from affecting future cline instance gui. this may happen if its hanging on a streaming request
|
||||
this.task.taskState.abandoned = true
|
||||
targetTask.taskState.abandoned = true
|
||||
}
|
||||
|
||||
// Small delay to ensure state manager has persisted the history update
|
||||
//await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// NOW try to get history after abort has finished (hook may have saved messages)
|
||||
let historyItem: HistoryItem | undefined
|
||||
try {
|
||||
const result = await this.getTaskWithId(this.task.taskId)
|
||||
const result = await this.getTaskWithId(targetTask.taskId)
|
||||
historyItem = result.historyItem
|
||||
} catch (error) {
|
||||
// Task not in history yet (new task with no messages); catch the
|
||||
@@ -477,11 +510,16 @@ export class Controller {
|
||||
Logger.log(`[Controller.cancelTask] Task not found in history: ${error}`)
|
||||
}
|
||||
|
||||
// Only re-initialize if we found a history item, otherwise just clear
|
||||
if (historyItem) {
|
||||
// Remove from active tasks
|
||||
if (targetTaskId) {
|
||||
this.activeTasks.delete(targetTaskId)
|
||||
}
|
||||
|
||||
// Only re-initialize if we found a history item and this is the current task, otherwise just clear
|
||||
if (historyItem && targetTask === this.task) {
|
||||
// Re-initialize task to keep it visible in UI with resume button
|
||||
await this.initTask(undefined, undefined, undefined, historyItem, undefined)
|
||||
} else {
|
||||
} else if (targetTask === this.task) {
|
||||
await this.clearTask()
|
||||
}
|
||||
|
||||
@@ -830,6 +868,7 @@ export class Controller {
|
||||
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
|
||||
const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
|
||||
this.stateManager.setGlobalState("taskHistory", updatedTaskHistory)
|
||||
this.activeTasks.delete(id)
|
||||
|
||||
// Notify the webview that the task has been deleted
|
||||
await this.postStateToWebview()
|
||||
@@ -843,6 +882,9 @@ export class Controller {
|
||||
}
|
||||
|
||||
async getStateToPostToWebview(): Promise<ExtensionState> {
|
||||
// Clean up any tasks that are no longer active before reporting state
|
||||
this.cleanupInactiveTasks()
|
||||
|
||||
// Get API configuration from cache for immediate access
|
||||
const onboardingModels = getClineOnboardingModels()
|
||||
const apiConfiguration = this.stateManager.getApiConfiguration()
|
||||
@@ -896,10 +938,12 @@ export class Controller {
|
||||
const workflowToggles = this.stateManager.getWorkspaceStateKey("workflowToggles")
|
||||
const autoCondenseThreshold = this.stateManager.getGlobalSettingsKey("autoCondenseThreshold")
|
||||
|
||||
const currentTaskItem = this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined
|
||||
const currentTaskId = this.task?.taskId
|
||||
const currentTaskItem = currentTaskId ? (taskHistory || []).find((item) => item.id === currentTaskId) : undefined
|
||||
const currentTask = currentTaskId ? this.activeTasks.get(currentTaskId) : this.task
|
||||
// Spread to create new array reference - React needs this to detect changes in useEffect dependencies
|
||||
const clineMessages = [...(this.task?.messageStateHandler.getClineMessages() || [])]
|
||||
const checkpointManagerErrorMessage = this.task?.taskState.checkpointManagerErrorMessage
|
||||
const clineMessages = [...(currentTask?.messageStateHandler.getClineMessages() || [])]
|
||||
const checkpointManagerErrorMessage = currentTask?.taskState.checkpointManagerErrorMessage
|
||||
|
||||
const processedTaskHistory = (taskHistory || [])
|
||||
.filter((item) => item.ts && item.task)
|
||||
@@ -930,7 +974,7 @@ export class Controller {
|
||||
apiConfiguration,
|
||||
currentTaskItem,
|
||||
clineMessages,
|
||||
currentFocusChainChecklist: this.task?.taskState.currentFocusChainChecklist || null,
|
||||
currentFocusChainChecklist: currentTask?.taskState.currentFocusChainChecklist || null,
|
||||
checkpointManagerErrorMessage,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
@@ -979,6 +1023,14 @@ export class Controller {
|
||||
autoCondenseThreshold,
|
||||
backgroundCommandRunning: this.backgroundCommandRunning,
|
||||
backgroundCommandTaskId: this.backgroundCommandTaskId,
|
||||
// Multi-task support: include all active tasks with status
|
||||
activeTasks: Array.from(this.activeTasks.entries()).map(([taskId, task]) => {
|
||||
const lastMessage = task?.messageStateHandler?.getClineMessages()?.at(-1)
|
||||
return {
|
||||
taskId,
|
||||
status: getTaskStatus(task?.taskState, lastMessage),
|
||||
}
|
||||
}),
|
||||
// NEW: Add workspace information
|
||||
workspaceRoots: this.workspaceManager?.getRoots() ?? [],
|
||||
primaryRootIndex: this.workspaceManager?.getPrimaryIndex() ?? 0,
|
||||
@@ -1015,11 +1067,85 @@ export class Controller {
|
||||
if (this.task) {
|
||||
// Clear task settings cache when task ends
|
||||
await this.stateManager.clearTaskSettings()
|
||||
|
||||
// Remove from active tasks
|
||||
if (this.task.taskId) {
|
||||
this.activeTasks.delete(this.task.taskId)
|
||||
}
|
||||
}
|
||||
await this.task?.abortTask()
|
||||
this.task = undefined // removes reference to it, so once promises end it will be garbage collected
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches focus to a different active task
|
||||
* @param taskId - The ID of the task to switch to
|
||||
* @returns true if switch was successful, false otherwise
|
||||
*/
|
||||
async switchTask(taskId: string): Promise<boolean> {
|
||||
const targetTask = this.activeTasks.get(taskId)
|
||||
|
||||
if (!targetTask) {
|
||||
Logger.error(`[Controller.switchTask] Task ${taskId} not found in active tasks`)
|
||||
return false
|
||||
}
|
||||
|
||||
// Update current task reference
|
||||
this.task = targetTask
|
||||
|
||||
await this.postStateToWebview()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific active task by ID
|
||||
* @param taskId - The task ID to retrieve
|
||||
* @returns The Task instance or undefined if not found
|
||||
*/
|
||||
getActiveTask(taskId: string): Task | undefined {
|
||||
return this.activeTasks.get(taskId) || this.task
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares for creating a new task without aborting the current one
|
||||
* Keeps the current task active in the background (parallel task support)
|
||||
*/
|
||||
async prepareNewTask() {
|
||||
// Just clear the current task reference without aborting
|
||||
// The task remains in activeTasks and can be switched back to
|
||||
this.task = undefined
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes tasks from activeTasks that are no longer active (aborted, abandoned, or completed).
|
||||
* A task is considered inactive when getTaskStatus returns undefined.
|
||||
* This is called automatically when posting state to webview to keep the map clean.
|
||||
*/
|
||||
private cleanupInactiveTasks(): void {
|
||||
// Store the task IDs that needs to be removed
|
||||
// to avoid modifying the activeTasks map while iterating
|
||||
const tasksToRemove: string[] = []
|
||||
|
||||
for (const [taskId, task] of this.activeTasks) {
|
||||
// Skip the current task - we always want to keep it in the map
|
||||
if (taskId === this.task?.taskId) {
|
||||
continue
|
||||
}
|
||||
|
||||
const lastMessage = task.messageStateHandler?.getClineMessages()?.at(-1)
|
||||
const status = getTaskStatus(task.taskState, lastMessage)
|
||||
|
||||
// If status is undefined, the task is no longer active (aborted, abandoned, or completed)
|
||||
if (status === undefined) {
|
||||
tasksToRemove.push(taskId)
|
||||
}
|
||||
}
|
||||
|
||||
for (const taskId of tasksToRemove) {
|
||||
this.activeTasks.delete(taskId)
|
||||
}
|
||||
}
|
||||
|
||||
// Caching mechanism to keep track of webview messages + API conversation history per provider instance
|
||||
|
||||
/*
|
||||
|
||||
@@ -118,7 +118,7 @@ async function fetchAndCacheModels(controller: Controller): Promise<Record<strin
|
||||
const rawModels = response.data.data
|
||||
const parsePrice = (price: any) => {
|
||||
if (price) {
|
||||
return parseFloat(price) * 1_000_000
|
||||
return Number.parseFloat(price) * 1_000_000
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ async function fetchAndCacheModels(): Promise<Record<string, ModelInfo>> {
|
||||
const rawModels = response.data.data
|
||||
const parsePrice = (price: any) => {
|
||||
if (price) {
|
||||
return parseFloat(price) * 1_000_000
|
||||
return Number.parseFloat(price) * 1_000_000
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ function parseFieldMask(updateMask: string[]): {
|
||||
function getAlternateModeField(fieldName: string): string | null {
|
||||
if (fieldName.startsWith("planMode")) {
|
||||
return fieldName.replace("planMode", "actMode")
|
||||
} else if (fieldName.startsWith("actMode")) {
|
||||
}
|
||||
if (fieldName.startsWith("actMode")) {
|
||||
return fieldName.replace("actMode", "planMode")
|
||||
}
|
||||
return null
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Empty, StringRequest } from "@shared/proto/cline/common"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Cancel a specific task by ID
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the task ID to cancel
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function cancelTaskById(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
const taskId = request.value
|
||||
await controller.cancelTask(taskId)
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -18,9 +18,34 @@ export async function showTaskWithId(controller: Controller, request: StringRequ
|
||||
const taskHistory = controller.stateManager.getGlobalStateKey("taskHistory")
|
||||
const historyItem = taskHistory.find((item) => item.id === id)
|
||||
|
||||
// We need to initialize the task before returning data
|
||||
// Check if this task is already active (running in background)
|
||||
const activeTask = controller.getActiveTask(id)
|
||||
if (activeTask) {
|
||||
// Task is already running - just switch to it without showing resume message
|
||||
await controller.switchTask(id)
|
||||
|
||||
// Send UI update to show the chat view
|
||||
await sendChatButtonClickedEvent()
|
||||
|
||||
// Return task data from history
|
||||
const taskData = historyItem || (await controller.getTaskWithId(id)).historyItem
|
||||
return TaskResponse.create({
|
||||
id,
|
||||
task: taskData.task || "",
|
||||
ts: taskData.ts || 0,
|
||||
isFavorited: taskData.isFavorited || false,
|
||||
size: taskData.size || 0,
|
||||
totalCost: taskData.totalCost || 0,
|
||||
tokensIn: taskData.tokensIn || 0,
|
||||
tokensOut: taskData.tokensOut || 0,
|
||||
cacheWrites: taskData.cacheWrites || 0,
|
||||
cacheReads: taskData.cacheReads || 0,
|
||||
})
|
||||
}
|
||||
|
||||
// Task is not active - load from history (will show resume message)
|
||||
if (historyItem) {
|
||||
// Always initialize the task with the history item
|
||||
// Initialize the task with the history item
|
||||
await controller.initTask(undefined, undefined, undefined, historyItem)
|
||||
|
||||
// Send UI update to show the chat view
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { BooleanResponse, StringRequest } from "@shared/proto/cline/common"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Switch to a different active task
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the task ID to switch to
|
||||
* @returns BooleanResponse indicating success
|
||||
*/
|
||||
export async function switchTask(controller: Controller, request: StringRequest): Promise<BooleanResponse> {
|
||||
const taskId = request.value
|
||||
const success = await controller.switchTask(taskId)
|
||||
return BooleanResponse.create({ value: success })
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export class PromptRegistry {
|
||||
private static instance: PromptRegistry
|
||||
private variants: Map<string, PromptVariant> = new Map()
|
||||
private components: ComponentRegistry = {}
|
||||
private loaded: boolean = false
|
||||
private loaded = false
|
||||
public nativeTools: ClineTool[] | undefined = undefined
|
||||
|
||||
private constructor() {
|
||||
|
||||
@@ -820,9 +820,8 @@ export class StateManager {
|
||||
const value = this.secretsCache[key]
|
||||
if (value) {
|
||||
return this.context.secrets.store(key, value)
|
||||
} else {
|
||||
return this.context.secrets.delete(key)
|
||||
}
|
||||
return this.context.secrets.delete(key)
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { AssistantMessageContent } from "@core/assistant-message"
|
||||
import { ActiveTaskStatus, ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import type { HookExecution } from "./types/HookExecution"
|
||||
|
||||
@@ -71,3 +72,35 @@ export class TaskState {
|
||||
currentlySummarizing = false
|
||||
lastAutoCompactTriggerIndex?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the status of a task based on its state and the last message
|
||||
* @param taskState - The TaskState object containing streaming and abort flags
|
||||
* @param lastMessage - The last ClineMessage in the conversation (optional)
|
||||
* @returns The current ActiveTaskStatus
|
||||
*/
|
||||
export function getTaskStatus(taskState?: TaskState, lastMessage?: ClineMessage): ActiveTaskStatus | undefined {
|
||||
// Check if task was aborted/cancelled
|
||||
if (!taskState || taskState.abort || taskState.abandoned || !lastMessage) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const messageType = lastMessage?.say || lastMessage?.ask
|
||||
|
||||
if (!messageType || lastMessage?.partial === true || messageType === "api_req_started" || messageType === "api_req_retried") {
|
||||
return "active"
|
||||
}
|
||||
|
||||
if (messageType === "api_req_failed" || messageType === "diff_error") {
|
||||
return "error"
|
||||
}
|
||||
|
||||
// Task is waiting for user input/approval (any ask type that requires response)
|
||||
if (lastMessage?.partial === false) {
|
||||
if (messageType === "tool" || messageType === "command" || messageType === "followup") {
|
||||
return "pending"
|
||||
}
|
||||
}
|
||||
|
||||
return "done"
|
||||
}
|
||||
|
||||
@@ -113,18 +113,17 @@ export class AccessMcpResourceHandler implements IFullyManagedTool {
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
} else {
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
|
||||
@@ -69,7 +69,7 @@ export class SummarizeTaskHandler implements IToolHandler, IPartialBlockHandler
|
||||
clearActiveHookExecution: config.callbacks.clearActiveHookExecution,
|
||||
postStateToWebview: config.callbacks.postStateToWebview,
|
||||
taskState: config.taskState,
|
||||
cancelTask: config.callbacks.cancelTask,
|
||||
cancelTask: () => config.callbacks.cancelTask(config.taskId),
|
||||
hooksEnabled,
|
||||
})
|
||||
|
||||
|
||||
@@ -128,18 +128,17 @@ export class UseMcpToolHandler implements IFullyManagedTool {
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
} else {
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
|
||||
@@ -123,7 +123,7 @@ export interface TaskCallbacks {
|
||||
// Additional callbacks for task management
|
||||
postStateToWebview: () => Promise<void>
|
||||
reinitExistingTaskFromId: (taskId: string) => Promise<void>
|
||||
cancelTask: () => Promise<void>
|
||||
cancelTask: (id: string) => Promise<void>
|
||||
updateTaskHistory: (update: any) => Promise<any[]>
|
||||
|
||||
applyLatestBrowserSettings: () => Promise<BrowserSession>
|
||||
|
||||
@@ -95,7 +95,7 @@ export class ToolHookUtils {
|
||||
await config.callbacks.clearActiveHookExecution()
|
||||
|
||||
// Abort the entire task (consistent with PostToolUse and other hook cancellations)
|
||||
await config.callbacks.cancelTask()
|
||||
await config.callbacks.cancelTask(config.taskId)
|
||||
throw new PreToolUseHookCancellationError(preToolResult.errorMessage || "PreToolUse hook requested cancellation")
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -118,8 +118,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(commands.PlusButton, async () => {
|
||||
const sidebarInstance = WebviewProvider.getInstance()
|
||||
await sidebarInstance.controller.clearTask()
|
||||
await sidebarInstance.controller.postStateToWebview()
|
||||
// For parallel task support: prepare for a new task without aborting the current one
|
||||
// The current task will remain in activeTasks map running in background
|
||||
await sidebarInstance.controller.prepareNewTask()
|
||||
await sendChatButtonClickedEvent()
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -41,7 +41,7 @@ type BuildArgs = {
|
||||
// callbacks for single-root TaskCheckpointManager
|
||||
updateTaskHistory: (historyItem: any) => Promise<any[]>
|
||||
say: (...args: any[]) => Promise<number | undefined>
|
||||
cancelTask: () => Promise<void>
|
||||
cancelTask: (id: string) => Promise<void>
|
||||
postStateToWebview: () => Promise<void>
|
||||
|
||||
// initial state for single-root
|
||||
|
||||
@@ -45,7 +45,7 @@ interface CheckpointManagerServices {
|
||||
}
|
||||
interface CheckpointManagerCallbacks {
|
||||
readonly updateTaskHistory: UpdateTaskHistoryFunction
|
||||
readonly cancelTask: () => Promise<void>
|
||||
readonly cancelTask: (id: string) => Promise<void>
|
||||
readonly say: SayFunction
|
||||
readonly postStateToWebview: () => Promise<void>
|
||||
}
|
||||
@@ -743,7 +743,7 @@ export class TaskCheckpointManager implements ICheckpointManager {
|
||||
await this.services.messageStateHandler.saveClineMessagesAndUpdateHistory()
|
||||
|
||||
// Cancel and reinitialize the task to get updated messages
|
||||
await this.callbacks.cancelTask()
|
||||
await this.callbacks.cancelTask(this.task.taskId)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -104,7 +104,7 @@ export class FileEditProvider extends DiffViewProvider {
|
||||
return this.getDocumentText()
|
||||
}
|
||||
|
||||
protected async saveDocument(): Promise<Boolean> {
|
||||
protected async saveDocument(): Promise<boolean> {
|
||||
if (!this.absolutePath || !this.documentContent) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -15,17 +15,17 @@ export async function detectEncoding(fileBuffer: Buffer, fileExtension?: string)
|
||||
const detected = chardet.detect(fileBuffer)
|
||||
if (typeof detected === "string") {
|
||||
return detected
|
||||
} else if (detected && (detected as any).encoding) {
|
||||
return (detected as any).encoding
|
||||
} else {
|
||||
if (fileExtension) {
|
||||
const isBinary = await isBinaryFile(fileBuffer).catch(() => false)
|
||||
if (isBinary) {
|
||||
throw new Error(`Cannot read text for file type: ${fileExtension}`)
|
||||
}
|
||||
}
|
||||
return "utf8"
|
||||
}
|
||||
if (detected && (detected as any).encoding) {
|
||||
return (detected as any).encoding
|
||||
}
|
||||
if (fileExtension) {
|
||||
const isBinary = await isBinaryFile(fileBuffer).catch(() => false)
|
||||
if (isBinary) {
|
||||
throw new Error(`Cannot read text for file type: ${fileExtension}`)
|
||||
}
|
||||
}
|
||||
return "utf8"
|
||||
}
|
||||
|
||||
export async function extractTextFromFile(filePath: string): Promise<string> {
|
||||
@@ -131,9 +131,8 @@ function formatCellValue(cell: ExcelJS.Cell): string {
|
||||
if (typeof value === "object" && "formula" in value) {
|
||||
if ("result" in value && value.result !== undefined && value.result !== null) {
|
||||
return value.result.toString()
|
||||
} else {
|
||||
return `[Formula: ${value.formula}]`
|
||||
}
|
||||
return `[Formula: ${value.formula}]`
|
||||
}
|
||||
|
||||
return value.toString()
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Logger } from "@/shared/services/Logger"
|
||||
/*
|
||||
* Unique identifier for the current installation.
|
||||
*/
|
||||
let _distinctId: string = ""
|
||||
let _distinctId = ""
|
||||
|
||||
/**
|
||||
* Some environments don't return a value for the machine ID. For these situations we generated
|
||||
|
||||
@@ -36,6 +36,15 @@ export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sun
|
||||
|
||||
export const DEFAULT_PLATFORM = "unknown"
|
||||
|
||||
/**
|
||||
* Task status types representing the current state of a task
|
||||
* - "active": Task is actively streaming/processing
|
||||
* - "done": Task completed successfully
|
||||
* - "error": Task encountered an error
|
||||
* - "pending": Task is waiting for user response/approval
|
||||
*/
|
||||
export type ActiveTaskStatus = "active" | "done" | "error" | "pending"
|
||||
|
||||
export const COMMAND_CANCEL_TOKEN = "__cline_command_cancel__"
|
||||
export interface ExtensionState {
|
||||
isNewUser: boolean
|
||||
@@ -70,6 +79,8 @@ export interface ExtensionState {
|
||||
backgroundCommandTaskId?: string
|
||||
lastCompletedCommandTs?: number
|
||||
userInfo?: UserInfo
|
||||
// Multi-task support
|
||||
activeTasks?: Array<{ taskId: string; status?: ActiveTaskStatus }>
|
||||
version: string
|
||||
distinctId: string
|
||||
globalClineRulesToggles: ClineRulesToggles
|
||||
|
||||
@@ -43,8 +43,8 @@ export class Session {
|
||||
private sessionId: string
|
||||
private sessionStartTime: number
|
||||
private toolCalls: ToolCallRecord[] = []
|
||||
private apiTimeMs: number = 0
|
||||
private toolTimeMs: number = 0
|
||||
private apiTimeMs = 0
|
||||
private toolTimeMs = 0
|
||||
|
||||
// Track in-flight operations
|
||||
private currentApiCallStart: number | null = null
|
||||
@@ -52,7 +52,7 @@ export class Session {
|
||||
|
||||
// Resource tracking
|
||||
private initialCpuUsage: NodeJS.CpuUsage
|
||||
private peakMemoryBytes: number = 0
|
||||
private peakMemoryBytes = 0
|
||||
|
||||
private constructor() {
|
||||
this.sessionId = nanoid(10)
|
||||
|
||||
@@ -128,7 +128,7 @@ function getOtelConfig(): OpenTelemetryClientConfig {
|
||||
otlpProtocol: BUILD_CONSTANTS.OTEL_EXPORTER_OTLP_PROTOCOL,
|
||||
otlpEndpoint: BUILD_CONSTANTS.OTEL_EXPORTER_OTLP_ENDPOINT,
|
||||
metricExportInterval: BUILD_CONSTANTS.OTEL_METRIC_EXPORT_INTERVAL
|
||||
? parseInt(BUILD_CONSTANTS.OTEL_METRIC_EXPORT_INTERVAL, 10)
|
||||
? Number.parseInt(BUILD_CONSTANTS.OTEL_METRIC_EXPORT_INTERVAL, 10)
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
@@ -174,17 +174,17 @@ function getRuntimeOtelConfig(): OpenTelemetryClientConfig {
|
||||
otlpLogsProtocol: process.env.CLINE_OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
|
||||
otlpLogsEndpoint: process.env.CLINE_OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
|
||||
metricExportInterval: process.env.CLINE_OTEL_METRIC_EXPORT_INTERVAL
|
||||
? parseInt(process.env.CLINE_OTEL_METRIC_EXPORT_INTERVAL, 10)
|
||||
? Number.parseInt(process.env.CLINE_OTEL_METRIC_EXPORT_INTERVAL, 10)
|
||||
: undefined,
|
||||
otlpInsecure: process.env.CLINE_OTEL_EXPORTER_OTLP_INSECURE === "true",
|
||||
logBatchSize: process.env.CLINE_OTEL_LOG_BATCH_SIZE
|
||||
? Math.max(1, parseInt(process.env.CLINE_OTEL_LOG_BATCH_SIZE, 10))
|
||||
? Math.max(1, Number.parseInt(process.env.CLINE_OTEL_LOG_BATCH_SIZE, 10))
|
||||
: undefined,
|
||||
logBatchTimeout: process.env.CLINE_OTEL_LOG_BATCH_TIMEOUT
|
||||
? Math.max(1, parseInt(process.env.CLINE_OTEL_LOG_BATCH_TIMEOUT, 10))
|
||||
? Math.max(1, Number.parseInt(process.env.CLINE_OTEL_LOG_BATCH_TIMEOUT, 10))
|
||||
: undefined,
|
||||
logMaxQueueSize: process.env.CLINE_OTEL_LOG_MAX_QUEUE_SIZE
|
||||
? Math.max(1, parseInt(process.env.CLINE_OTEL_LOG_MAX_QUEUE_SIZE, 10))
|
||||
? Math.max(1, Number.parseInt(process.env.CLINE_OTEL_LOG_MAX_QUEUE_SIZE, 10))
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,12 +96,12 @@ export function getStorageAdapter(settings: BlobStoreSettings): StorageAdapter |
|
||||
const adapterType = settings.adapterType
|
||||
if (adapterType === "r2") {
|
||||
return createR2Adapter(settings)
|
||||
} else if (adapterType === "s3") {
|
||||
return createS3Adapter(settings)
|
||||
} else {
|
||||
Logger.error(`[StorageAdapter] Invalid adapterType: ${adapterType}. Must be "s3" or "r2".`)
|
||||
return undefined
|
||||
}
|
||||
if (adapterType === "s3") {
|
||||
return createS3Adapter(settings)
|
||||
}
|
||||
Logger.error(`[StorageAdapter] Invalid adapterType: ${adapterType}. Must be "s3" or "r2".`)
|
||||
return undefined
|
||||
} catch (error) {
|
||||
Logger.error("[StorageAdapter] Unexpected error creating adapter:", error)
|
||||
return undefined
|
||||
|
||||
@@ -23,7 +23,7 @@ export const AccountWelcomeView = () => {
|
||||
Sign up with Cline
|
||||
{isLoginLoading && (
|
||||
<span className="ml-1 animate-spin">
|
||||
<span className="codicon codicon-refresh"></span>
|
||||
<span className="codicon codicon-refresh" />
|
||||
</span>
|
||||
)}
|
||||
</VSCodeButton>
|
||||
|
||||
@@ -33,7 +33,7 @@ const ViewHeader = ({ title, onDone, showEnvironmentSuffix, environment }: ViewH
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button size="header" onClick={onDone}>
|
||||
<Button onClick={onDone} size="header">
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { memo } from "react"
|
||||
import { memo, useMemo } from "react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { TaskServiceClient } from "@/services/grpc-client"
|
||||
import { Button } from "../ui/button"
|
||||
|
||||
type HistoryPreviewProps = {
|
||||
showHistoryView: () => void
|
||||
}
|
||||
|
||||
const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
const { taskHistory } = useExtensionState()
|
||||
const { activeTasks, taskHistory } = useExtensionState()
|
||||
const handleHistorySelect = (id: string) => {
|
||||
TaskServiceClient.showTaskWithId(StringRequest.create({ value: id })).catch((error) =>
|
||||
console.error("Error showing task:", error),
|
||||
@@ -23,6 +25,45 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Get the top 3 history items, but preserve the order of active tasks
|
||||
// Active tasks should maintain their position from activeTasks array to prevent reordering while displayed
|
||||
const displayItems = useMemo(() => {
|
||||
const validItems = taskHistory.filter((item) => item.ts && item.task).slice(0, 3)
|
||||
|
||||
if (!activeTasks?.length) {
|
||||
return validItems
|
||||
}
|
||||
|
||||
// Reverse active tasks to maintain their order when sorting
|
||||
const reversedActiveTasks = [...activeTasks].reverse()
|
||||
// Create a map of taskId to its index in activeTasks for quick lookup
|
||||
const activeTaskIndexMap = new Map(reversedActiveTasks.map((t, i) => [t?.taskId, i]))
|
||||
|
||||
// Sort items: active tasks maintain their relative order from activeTasks,
|
||||
// non-active tasks come after in their original order
|
||||
return [...validItems].sort((a, b) => {
|
||||
const aActiveIndex = activeTaskIndexMap.get(a.id)
|
||||
const bActiveIndex = activeTaskIndexMap.get(b.id)
|
||||
|
||||
const aIsActive = aActiveIndex !== undefined
|
||||
const bIsActive = bActiveIndex !== undefined
|
||||
|
||||
if (aIsActive && bIsActive) {
|
||||
// Both are active: preserve activeTasks order
|
||||
return aActiveIndex - bActiveIndex
|
||||
} else if (aIsActive) {
|
||||
// Only a is active: a comes first
|
||||
return -1
|
||||
} else if (bIsActive) {
|
||||
// Only b is active: b comes first
|
||||
return 1
|
||||
} else {
|
||||
// Neither is active: preserve original taskHistory order
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}, [taskHistory, activeTasks])
|
||||
|
||||
return (
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<style>
|
||||
@@ -81,82 +122,35 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.history-view-all-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px 0 4px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.history-view-all-btn .codicon {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.history-view-all-btn:hover {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div
|
||||
className="history-header"
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: "10px 16px 10px 16px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<span
|
||||
className="codicon codicon-comment-discussion"
|
||||
style={{
|
||||
marginRight: "4px",
|
||||
transform: "scale(0.9)",
|
||||
}}></span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontSize: "0.85em",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
Recent
|
||||
</span>
|
||||
</div>
|
||||
{taskHistory.filter((item) => item.ts && item.task).length > 0 && (
|
||||
<button
|
||||
aria-label="View all history"
|
||||
className="history-view-all-btn"
|
||||
onClick={() => showHistoryView()}
|
||||
type="button">
|
||||
View All
|
||||
<span className="codicon codicon-chevron-right" />
|
||||
</button>
|
||||
)}
|
||||
<div className="history-header text-description my-2.5 mx-4 flex items-center">
|
||||
<span className="codicon codicon-comment-discussion mr-1 scale-90"></span>
|
||||
<span className="font-medium text-sm uppercase">Recent Tasks</span>
|
||||
</div>
|
||||
|
||||
{
|
||||
<div className="px-4">
|
||||
{taskHistory.filter((item) => item.ts && item.task).length > 0 ? (
|
||||
taskHistory
|
||||
.filter((item) => item.ts && item.task)
|
||||
.slice(0, 3)
|
||||
.map((item) => (
|
||||
{displayItems.length > 0 ? (
|
||||
<>
|
||||
{displayItems.map((item) => (
|
||||
<div className="history-preview-item" key={item.id} onClick={() => handleHistorySelect(item.id)}>
|
||||
<div className="history-task-content">
|
||||
<div
|
||||
className={cn("w-0 h-0 rounded-full self-center", {
|
||||
"w-2 h-2 bg-success":
|
||||
activeTasks?.find((task) => task.taskId === item.id)?.status === "active",
|
||||
"w-2 h-2 bg-warning":
|
||||
activeTasks?.find((task) => task.taskId === item.id)?.status === "pending",
|
||||
"w-2 h-2 bg-error":
|
||||
activeTasks?.find((task) => task.taskId === item.id)?.status === "error",
|
||||
})}
|
||||
/>
|
||||
{item.isFavorited && (
|
||||
<span
|
||||
aria-label="Favorited"
|
||||
className="codicon codicon-star-full"
|
||||
style={{
|
||||
color: "var(--vscode-button-background)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
className="codicon codicon-star-full shrink-0 bg-button-background"
|
||||
/>
|
||||
)}
|
||||
<div className="history-task-description ph-no-capture">{item.task}</div>
|
||||
@@ -168,17 +162,21 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
))}
|
||||
<div className="flex items-center justify-center">
|
||||
<Button
|
||||
aria-label="View all history"
|
||||
onClick={() => showHistoryView()}
|
||||
style={{
|
||||
opacity: 0.9,
|
||||
}}
|
||||
variant="ghost">
|
||||
<div className="text-base text-description">View All</div>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
padding: "10px 0",
|
||||
}}>
|
||||
No recent tasks
|
||||
</div>
|
||||
<div className="text-center text-description font-base py-2.5">No recent tasks</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ const HISTORY_FILTERS = {
|
||||
|
||||
const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
const extensionStateContext = useExtensionState()
|
||||
const { taskHistory, onRelinquishControl, environment } = extensionStateContext
|
||||
const { activeTasks, taskHistory, onRelinquishControl, environment } = extensionStateContext
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [sortOption, setSortOption] = useState<SortOption>("newest")
|
||||
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
|
||||
@@ -157,9 +157,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
setSelectedItems((prev) => {
|
||||
if (checked) {
|
||||
return [...prev, itemId]
|
||||
} else {
|
||||
return prev.filter((id) => id !== itemId)
|
||||
}
|
||||
return prev.filter((id) => id !== itemId)
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -197,14 +196,34 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
}, [tasks])
|
||||
|
||||
const taskHistorySearchResults = useMemo(() => {
|
||||
const results = searchQuery
|
||||
? fuse
|
||||
.search(searchQuery)
|
||||
?.filter(({ matches }) => matches && matches.length)
|
||||
.map(({ item }) => item)
|
||||
: tasks
|
||||
const results = searchQuery ? highlight(fuse.search(searchQuery)) : tasks
|
||||
|
||||
// Create a map of taskId to its index in activeTasks for quick lookup
|
||||
const reversedActiveTasks = activeTasks ? [...activeTasks].reverse() : []
|
||||
const activeTaskIndexMap = new Map(reversedActiveTasks?.map((task, index) => [task.taskId, index]) || [])
|
||||
|
||||
results.sort((a, b) => {
|
||||
// to prevent reordering while active
|
||||
const aActiveIndex = activeTaskIndexMap.get(a.id)
|
||||
const bActiveIndex = activeTaskIndexMap.get(b.id)
|
||||
|
||||
const aIsActive = aActiveIndex !== undefined
|
||||
const bIsActive = bActiveIndex !== undefined
|
||||
|
||||
// If both are active, preserve their activeTasks order
|
||||
if (aIsActive && bIsActive) {
|
||||
return aActiveIndex - bActiveIndex
|
||||
}
|
||||
|
||||
// If only one is active, active tasks come first
|
||||
if (aIsActive) {
|
||||
return -1
|
||||
}
|
||||
if (bIsActive) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Neither is active: apply the selected sort option
|
||||
switch (sortOption) {
|
||||
case "oldest":
|
||||
return a.ts - b.ts
|
||||
@@ -228,7 +247,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
})
|
||||
|
||||
return results
|
||||
}, [tasks, searchQuery, fuse, sortOption])
|
||||
}, [tasks, searchQuery, fuse, sortOption, activeTasks])
|
||||
|
||||
// Group tasks into "Today" and "Older" (only for date-based sorts)
|
||||
const { groupedTasks, groupCounts, groupLabels } = useMemo(() => {
|
||||
@@ -394,19 +413,21 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
<div className="flex-grow overflow-y-auto m-0 w-full py-2">
|
||||
<GroupedVirtuoso
|
||||
className="flex-grow overflow-y-scroll"
|
||||
context={{ activeTasks }}
|
||||
groupContent={(index) => (
|
||||
<div className="px-4 py-2 text-xs font-bold uppercase tracking-wide sticky top-0 z-10 text-description bg-sidebar-background border-b-border-panel">
|
||||
{groupLabels[index]}
|
||||
</div>
|
||||
)}
|
||||
groupCounts={groupCounts}
|
||||
itemContent={(index) => {
|
||||
itemContent={(index, _groupIndex, _data, context) => {
|
||||
const item = groupedTasks[index]
|
||||
return (
|
||||
<HistoryViewItem
|
||||
handleDeleteHistoryItem={handleDeleteHistoryItem}
|
||||
handleHistorySelect={handleHistorySelect}
|
||||
index={index}
|
||||
isActive={context.activeTasks?.find((task) => task.taskId === item.id)?.status === "active"}
|
||||
item={item}
|
||||
pendingFavoriteToggles={pendingFavoriteToggles}
|
||||
selectedItems={selectedItems}
|
||||
@@ -460,7 +481,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
}
|
||||
|
||||
// https://gist.github.com/evenfrost/1ba123656ded32fb7a0cd4651efd4db0
|
||||
export const highlight = (fuseSearchResult: FuseResult<any>[], highlightClassName: string = "history-item-highlight") => {
|
||||
export const highlight = (fuseSearchResult: FuseResult<any>[], highlightClassName = "history-item-highlight") => {
|
||||
const set = (obj: Record<string, any>, path: string, value: any) => {
|
||||
const pathValue = path.split(".")
|
||||
let i: number
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ChevronsDownUpIcon,
|
||||
ChevronsUpDownIcon,
|
||||
DownloadIcon,
|
||||
LoaderIcon,
|
||||
StarIcon,
|
||||
TrashIcon,
|
||||
} from "lucide-react"
|
||||
@@ -26,6 +27,7 @@ type HistoryViewItemProps = {
|
||||
handleDeleteHistoryItem: (id: string) => void
|
||||
toggleFavorite: (id: string, isCurrentlyFavorited: boolean) => void
|
||||
handleHistorySelect: (itemId: string, checked: boolean) => void
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
const HistoryViewItem = ({
|
||||
@@ -34,6 +36,7 @@ const HistoryViewItem = ({
|
||||
handleDeleteHistoryItem,
|
||||
toggleFavorite,
|
||||
handleHistorySelect,
|
||||
isActive,
|
||||
selectedItems,
|
||||
}: HistoryViewItemProps) => {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
@@ -96,7 +99,10 @@ const HistoryViewItem = ({
|
||||
}}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="line-clamp-1 overflow-hidden break-words whitespace-pre-wrap flex-1 min-w-0">
|
||||
<span className="ph-no-capture">{item.task}</span>
|
||||
<span className="ph-no-capture flex items-center">
|
||||
{isActive && <LoaderIcon className="animate-spin size-2 mr-1" />}
|
||||
{item.task}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2 flex-shrink-0">
|
||||
<Button
|
||||
|
||||
@@ -67,6 +67,6 @@ export const parsePrice = (value: string, defaultValue: number): number => {
|
||||
if (value === "" || value === ".") {
|
||||
return defaultValue
|
||||
}
|
||||
const num = parseFloat(value)
|
||||
const num = Number.parseFloat(value)
|
||||
return isNaN(num) ? defaultValue : num
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ const buttonVariants = cva(
|
||||
"bg-success/10 text-success border-[#176f2c] text-white hover:bg-[#197f31] hover:border-[#197f31] active:bg-[#156528] active:border-[#156528] hover:text-white",
|
||||
danger: "bg-[#c42b2b] border-[#c42b2b]! text-white! hover:bg-[#a82424]! hover:border-[#a82424]! active:bg-[#8f1f1f]! active:border-[#8f1f1f]!",
|
||||
},
|
||||
size: {
|
||||
default: "py-1.5 px-4 [&_svg]:size-3",
|
||||
sm: "py-1 px-3 text-sm [&_svg]:size-2",
|
||||
xs: "p-1 text-xs [&_svg]:size-2",
|
||||
lg: "py-4 px-8 [&_svg]:size-4 font-medium",
|
||||
icon: "px-0.5 m-0 [&_svg]:size-2",
|
||||
header: "py-1 px-4 [&_svg]:size-2.5",
|
||||
},
|
||||
size: {
|
||||
default: "py-1.5 px-4 [&_svg]:size-3",
|
||||
sm: "py-1 px-3 text-sm [&_svg]:size-2",
|
||||
xs: "p-1 text-xs [&_svg]:size-2",
|
||||
lg: "py-4 px-8 [&_svg]:size-4 font-medium",
|
||||
icon: "px-0.5 m-0 [&_svg]:size-2",
|
||||
header: "py-1 px-4 [&_svg]:size-2.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
|
||||
Reference in New Issue
Block a user