Compare commits

..

26 Commits

Author SHA1 Message Date
abeatrix 85a7b7dbaf test: add comprehensive test suite for applyFileReadContextHistoryUpdates
Add extensive test coverage for the applyFileReadContextHistoryUpdates method in ContextManager. Tests cover various scenarios including:
- Early return when fileReadIndices is empty
- Handling single file occurrences
- Updating duplicate file reads (keeping only last occurrence)
- FILE_MENTION type with multiple files
- Text block replacements in API messages
- Edge cases and error conditions

This ensures the file read deduplication logic works correctly across different message types and file configurations.
2025-12-04 13:10:17 -08:00
Saoud Rizwan 852f307268 Revert "feat(prompt): add command output limiting guidance to capabilities (#…" (#7909)
This reverts commit 7a523fbaf6.
2025-12-04 11:38:10 -08:00
Bee 4e3fe004f4 feat: enable native tool calling for deepseek 3.2 [AI-27] (#7877)
* feat: enable native tool calling for deepseek 3.2

Add isDeepSeek32ModelFamily() function to identify DeepSeek 3.2 models and integrate it into the isNextGenModelFamily() check. This classifies DeepSeek 3.2 as a next-generation model family, enabling native tool calling support.

* typo
2025-12-04 09:59:27 -08:00
Zhongying Qiao 2b63eed85e feat: remove mcp enable setting for individual users (#7879) 2025-12-04 09:36:44 -08:00
Tomás Barreiro 2ffdc50ea1 Prevent simultaneous refreshes when restoring auth info (#7835)
* Prevent multiple simultaneos refreshes when retrieving auth info

* Add changeset

* refactor
2025-12-04 14:45:10 +01:00
celestial-vault 74808431e5 add litellm provider to remote config in the extension (#7775) 2025-12-04 03:01:05 -08:00
Saoud Rizwan 7a523fbaf6 feat(prompt): add command output limiting guidance to capabilities (#7884)
* feat(prompt): add command output limiting guidance to capabilities

Add guidance in the system prompt instructing the model to proactively
limit command output when anticipating large results. Includes examples
like piping to grep/head/tail or using more specific arguments.

Idea by @AraTheBoss

* chore: add changeset

* refactor: move command output limiting guidance to execute_command tool

Move the guidance from capabilities.ts to execute_command.ts where it
belongs. Extract into a shared COMMAND_BEST_PRACTICES constant to avoid
duplication across model variants (GENERIC, NATIVE_GPT_5, NATIVE_NEXT_GEN,
GEMINI_3).
2025-12-03 21:06:07 -08:00
github-actions[bot] c22ea39dc1 v3.40.0 Release Notes (#7865)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md to reflect recent changes including fixes for highlighted text flashing, terminal command issues, and enhancements for slash command usage and message padding.

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-12-03 19:24:18 -08:00
Saoud Rizwan c9f23076c2 fix: consolidate successive error retry messages in chat UI (#7880)
When API requests fail and auto-retry is enabled, multiple error_retry
messages were shown (e.g., "Attempt 1 of 3", "Attempt 2 of 3", etc.).
This change consolidates them to only show the latest retry message,
reducing visual clutter during retry sequences.
2025-12-03 19:18:17 -08:00
Bee a5f6c1d732 feat: add auto-recovery for corrupted task history state (#7875)
* feat: add auto-recovery for corrupted task history state

Add automatic reconstruction of task history when JSON parsing fails.

Changes:
- Modified `reconstructTaskHistory()` to return reconstruction result or null
- Enhanced `readTaskHistoryFromState()` with automatic corruption recovery
- Added recursive reconstruction attempt with loop prevention flag
- Wrapped JSON parsing in try-catch to handle corruption gracefully

When task history state file is corrupted, the system now automatically
attempts to reconstruct history from existing task folders, providing
better resilience against file corruption issues.

* feat: Add telemetry tracking for extension storage errors

Replace console.error logging with structured telemetry capture for extension storage operations. This change:

- Adds a new EXTENSION_STORAGE_ERROR telemetry event type to track storage-related failures
- Implements captureExtensionStorageError method with error message truncation to prevent excessive data
- Replaces three console.error calls in readTaskHistoryFromState with telemetry events

This improves error monitoring and provides better insights into extension storage failures while maintaining data efficiency through message truncation.

* fix: improve type safety and error handling in task history

Add explicit return type to reconstructTaskHistory() function and refactor error handling in readTaskHistoryFromState() with nested try-catch blocks to better distinguish between file read errors and JSON parse errors. This improves error recovery and makes error tracking more precise through separate telemetry calls.

* add param to reconstructTaskHistory for manually called action

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-12-03 17:49:28 -08:00
Toshii 3c37a160ac support multi-index search over inner messages to find file mentions (#7850) 2025-12-03 15:27:08 -08:00
Saoud Rizwan 5c3294051f Revert "fix: don't return empty array on parse failure (#7773)" (#7874)
This reverts commit 14ccf33d25.
2025-12-03 14:30:58 -08:00
Tony Loehr 4c2f28f2af docs: remove Advanced Patterns and Testing & Debugging from Hooks documentation (#7869)
- Removed advanced-patterns.mdx and testing-and-debugging.mdx files
- Updated docs.json to remove these pages from navigation
- Updated hooks/index.mdx to remove corresponding Card components
- Simplified Hooks documentation to focus on core concepts: Overview, Hook Reference, and Samples
2025-12-03 12:11:43 -08:00
Ara 6e016298cb chore: bump version to 3.39.2 and update dependencies (#7851)
- Update package version from 3.39.1 to 3.39.2
- Upgrade @changesets/* packages to latest versions
- Update @inquirer/external-editor to 1.0.2
- Upgrade js-yaml from v3 to v4 in @changesets/parse
2025-12-03 11:23:29 -08:00
pashpashpash 0cd7bebfba markdown styling fix (#7840)
* markdown styling fix

* nested ul
2025-12-03 01:23:02 -08:00
Bee 363aac61fb fix: OpenAI Response API message format (#7842)
Fixed the message structure to match the OpenAI Responses API format.

Updated Message ID placement: The message id is stored and set at the message level, not inside the content array.

This fixes an error occuring in the current code when reasoning item is followed by a message text block: 400 Item 'rs_...' of type 'reasoning' was provided without its required following item."
2025-12-02 17:41:19 -08:00
Tony Loehr eeb1cc7da8 added subpages and content to hooks (#7797)
* added subpages and content to hooks

* Update docs/features/hooks/advanced-patterns.mdx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Add complete hook type coverage with examples for TaskCancel, TaskComplete, TaskResume, PreCompact, UserPromptSubmit

* Fix hook documentation API mismatches and add TaskComplete

- Add missing TaskComplete hook to reference documentation
- Fix TaskCancel/TaskResume field paths to match protobuf API
- Improve security practices in hook examples
- Add proper error handling and validation

* Update hooks documentation: rename samples, remove PreCompact, improve structure

- Rename 'Real World Examples' to 'Samples' with skill-based organization
- Remove PreCompact references (feature not yet available)
- Update navigation structure in docs.json
- Add multiworkspace mention to Overview
- Create 9 comprehensive examples (beginner/intermediate/advanced)
- Clean up duplicate content and fix cross-references

* Update hooks documentation: Add Windows support

- Remove incorrect warning that hooks don't work on Windows
- Add positive cross-platform support note (Windows, macOS, Linux)
- Clarify that bash examples work with standard shells including Git Bash/WSL on Windows

* fixed hooks overview redirect

* Add UI screenshots to hooks documentation

* hooks in action

* fixed hooks overview and examples

* fixed terminology

* fixed hooks examples

* hooks groupings

* fixed appearance of hook names

* refactor hook docs

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-12-02 17:35:07 -08:00
Sarah Fortune f760f13de5 Don't log otel events to the console because they are really spammy (#7841) 2025-12-02 17:20:52 -08:00
canvrno dd52a4a39c feat: apply_patch auto approve (#7777)
* Added apply_patch to auto approve, strict mode, and minor prompting adjustment

* changeset
2025-12-02 15:36:01 -08:00
Toshii 639edb5db6 correctly handle new and old tool call formats for context rewriting (#7809)
* correctly handle new and old tool call formats

* spelling change
2025-12-02 15:12:05 -08:00
Jack Reinhardt 3eac9b04de fix(bedrock): add sts userAgentAppId (#7719) 2025-12-02 14:40:39 -08:00
Bee 09692d7d3a feat: add mode and token metrics info to storage messages [CLIENTS-26] (#7795)
* feat(storage): add mode and token metrics to storage messages

Add mode (plan/act) tracking to ApiProviderInfo and ClineMessageModelInfo interfaces, ensuring each storage message contains the operational mode used during API requests.

Refactor token metrics tracking by consolidating cache write/read tokens, input/output tokens, and total cost into a centralized taskMetrics object. This enables better tracking and storage of token usage and costs throughout the task lifecycle, including for partial/cancelled streams.

Updated api_req_started and api_req_finished messages to include comprehensive token metrics, allowing for accurate cost reporting even when streams are cancelled or fail mid-execution.

* update unit tests with mode

* store task metrics per assistant turn
2025-12-02 13:52:44 -08:00
Andrei Eternal c81fa0a9d6 set the cli's 'ide version' to just the cli version rather than being blank, to make environment_history work for CLI (#7712)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-12-02 13:21:58 -08:00
Bee 326c9c9f99 feat: set default thinking level for Gemini 3 Pro models (#7831)
- Reorder thinking level checks to prioritize high over low
- Auto-set thinking level to LOW for Gemini 3 Pro models when not specified
- Add clarifying comment for thinking budget usage
- Ensure thinking level is always defined for Gemini 3 models to prevent errors

This change ensures Gemini 3 Pro models always have a thinking level set (required by the API) and removes the thinking budget when a level is specified, as they are mutually exclusive parameters.
2025-12-02 12:54:13 -08:00
celestial-vault a4518b90c2 add atomic file write (#7754)
* add atomic write file using write to temp file + rename to avoid situations where invalid data is written to files due to process interrupt

* adjust concurrency test for windows to expect error

* Add JSON ending to temporary file and don't await unlink
2025-12-02 13:30:53 -06:00
celestial-vault 79f4d938e6 remove unused sentry dependency (#7823) 2025-12-02 13:20:30 -06:00
66 changed files with 2757 additions and 849 deletions
-5
View File
@@ -1,5 +0,0 @@
---
claude-dev: minor
---
This minor change adds new models and image support for rleated models, adds fetching of model info from API, updates tool handling, and adds retrieval usage stats for individual messages and a user's monthly token usage.
-5
View File
@@ -1,5 +0,0 @@
---
claude-dev: patch
---
Fix highlighted text flashing when task header is collapsed.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add X-Cerebras-3rd-Party-Integration header to Cerebras API requests.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add DeepSeek 3.2 to native tool calling allow list
-5
View File
@@ -1,5 +0,0 @@
---
claude-dev: patch
---
Added microwave family system prompt configuration
-5
View File
@@ -1,5 +0,0 @@
---
claude-dev: patch
---
removing tooltips from auto approve menu
-5
View File
@@ -1,5 +0,0 @@
---
claude-dev: patch
---
fix: Standalone, ensure cwd is the install dir to find resources reliably
-5
View File
@@ -1,5 +0,0 @@
---
"cline-vscode": patch
---
Add Explain Changes feature for reviewing code changes with AI-powered explanations
-5
View File
@@ -1,5 +0,0 @@
---
claude-dev: patch
---
Fixed a bug where terminal commands with double quotes are broken when "Terminal Execution Mode" is set to "Background Exec"
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Prevent simultaneuos refreshes when restoring auth info
-5
View File
@@ -1,5 +0,0 @@
---
claude-dev: patch
---
Slash commands can now be typed anywhere in a message, not just at the beginning. This matches the behavior of @ mentions for a more flexible input experience.
-5
View File
@@ -1,5 +0,0 @@
---
claude-dev: patch
---
Adds bottom padding to the last message to fix last response text getting cut off by auto approve settings bar.
-5
View File
@@ -1,5 +0,0 @@
---
claude-dev: patch
---
Add task history recovery documentation with storage paths, recovery command usage, and troubleshooting guide.
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
cache: "npm"
- name: Install Dependencies
run: npm install changeset
run: npm ci
# Check if there are any new changesets to process
- name: Check for changesets
+2 -2
View File
@@ -74,8 +74,8 @@ jobs:
CLINE_ENVIRONMENT: production
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: console,otlp
OTEL_METRICS_EXPORTER: console,otlp
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
+2 -2
View File
@@ -99,8 +99,8 @@ jobs:
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: console,otlp
OTEL_METRICS_EXPORTER: console,otlp
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
+20 -3
View File
@@ -1,16 +1,31 @@
# Changelog
## [3.40.0]
- Fix highlighted text flashing when task header is collapsed
- Add X-Cerebras-3rd-Party-Integration header to Cerebras API requests
- Add microwave family system prompt configuration
- Remove tooltips from auto approve menu
- Fix Standalone, ensure cwd is the install dir to find resources reliably
- Fix a bug where terminal commands with double quotes are broken when "Terminal Execution Mode" is set to "Background Exec"
- Add support for slash commands anywhere in a message, not just at the beginning. This matches the behavior of @ mentions for a more flexible input experience.
- Add bottom padding to the last message to fix last response text getting cut off by auto approve settings bar.
- Add default thinking level for Gemini 3 Pro models in Gemini provider
## [3.39.2]
- Fix for microwave model and thinking settings
## [3.39.1]
- Fix Openrouter and Cline Provider model info
## [3.39.0]
- Add Explain Changes feature
- Add microwave Stealth model
- Add Tabbed Model Picker with Recommended and Free tabs
- Add support to View remote rules and workflows in the editor
- Add Tabbed Model Picker with Recommended and Free tabs
- Add support to View remote rules and workflows in the editor
- Enable NTC (Native Tool Calling) by default
- Bug fixes and improvements for LiteLLM provider
@@ -34,19 +49,21 @@
## [3.38.1]
### Fixed
- Fixed handling of 'signature' field in sanitizeAnthropicContentBlock to properly preserve it when thinking is enabled, as required by Anthropic's API.
## [3.38.0]
### Added
- Gemini 3 Pro Preview model
- AquaVoice Avalon model for voice-to-text dictation
### Fixed
- Automatic context truncation when AWS Bedrock token usage rate limits are exceeded
- Removed new_task tool from system prompts, updated slash command prompts, and added helper function for native tool calling validation
## [3.37.1]
- Comprehensive changes to better support GPT 5.1 - System prompt, tools, deep-planning, focus chain, etc.
+1 -1
View File
@@ -77,7 +77,7 @@ func (s *EnvService) GetHostVersion(ctx context.Context, req *cline.EmptyRequest
return &host.GetHostVersionResponse{
Platform: proto.String("Cline CLI"),
Version: proto.String(""),
Version: proto.String(global.CliVersion),
ClineType: proto.String("CLI"),
ClineVersion: proto.String(global.CliVersion),
}, nil
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

@@ -9,7 +9,7 @@ Automate GitHub issue analysis with AI. Mention `@cline` in any issue comment to
<Note>
**New to Cline CLI?** This sample assumes you understand Cline CLI basics and have completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation). If you're new to Cline CLI, we recommend starting with the [GitHub RCA sample](../github-issue-rca) first, as it's simpler and will help you understand the fundamentals before setting up GitHub Actions.
**New to Cline CLI?** This sample assumes you understand Cline CLI basics and have completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation). If you're new to Cline CLI, we recommend starting with the [GitHub RCA sample](./github-issue-rca) first, as it's simpler and will help you understand the fundamentals before setting up GitHub Actions.
</Note>
## The Workflow
+1 -1
View File
@@ -152,7 +152,7 @@ For in-depth commands and flags, check out the [CLI reference](/cline-cli/cli-re
Understand how YOLO mode works and when to use full automation versus manual approval.
</Card>
<Card title="Task management" icon="clipboard-check" href="/getting-started/task-management">
<Card title="Task management" icon="clipboard-check" href="/features/tasks/task-management">
Learn how Cline tracks and manages tasks, including saving and restoring state from checkpoints.
</Card>
</Columns>
+12 -1
View File
@@ -139,7 +139,14 @@
"features/editing-messages",
"features/explain-changes",
"features/focus-chain",
"features/hooks",
{
"group": "Hooks",
"pages": [
"features/hooks/index",
"features/hooks/hook-reference",
"features/hooks/samples"
]
},
"features/multiroot-workspace",
"features/plan-and-act",
{
@@ -354,6 +361,10 @@
"source": "/cline-cli/samples",
"destination": "/cline-cli/samples/overview"
},
{
"source": "/features/hooks/real-world-examples",
"destination": "/features/hooks/samples"
},
{
"source": "/enterprise-solutions/configure-AWS-Bedrock-Admin",
"destination": "/enterprise-solutions/provider-remote-config/aws-bedrock/admin-configuration"
-419
View File
@@ -1,419 +0,0 @@
---
title: "Hooks"
sidebarTitle: "Hooks"
description: "Inject custom logic into Cline's workflow to validate operations, monitor tool usage, and shape AI decisions"
---
Hooks let you inject custom logic into Cline's workflow at key moments. Think of them as automated checkpoints where you can validate operations before they execute, monitor tool usage as it happens, and shape how Cline makes decisions.
Hooks run automatically when specific events happen during development. They receive detailed information about each operation, can block problematic actions before they cause issues, and can inject context that guides future AI decisions.
The real power comes from combining these capabilities. You can:
- Stop operations before they cause problems (like creating `.js` files in a TypeScript project)
- Learn from what's happening and build up project knowledge over time
- Monitor performance and catch issues as they emerge
- Track everything for analytics or compliance
- Trigger external tools or services at the right moments
<Warning>
Hooks are currently supported on macOS and Linux only. Windows support is not available.
</Warning>
## Getting Started
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/hooks.gif" alt="Hooks in action" />
</Frame>
Enabling hooks in Cline is straightforward. Here's what you need to do:
<Steps>
<Step title="Enable Hooks in Settings">
Open Cline settings and check the **"Enable Hooks"** checkbox.
You can find this setting by:
1. Opening Cline
2. Click the "Settings" button on the top right corner
3. Click the "Feature" section in the left side navigation menu.
4. Scroll down until you see the "Enable Hooks" checkbox and check it.
</Step>
<Step title="Choose Your Hook Location">
Decide where to place your hooks:
**For personal or organization-wide hooks:**
- Create hooks in `~/Documents/Cline/Rules/Hooks/`
- These apply to all workspaces automatically
**For project-specific hooks:**
- Create hooks in `.clinerules/hooks/` in your project root
- These only apply to the specific workspace
- Commit them to version control so your team can use them too
</Step>
<Step title="Create Your First Hook">
Hook files must have exact names with no file extensions. For example, to create a TaskStart hook:
```bash
# Create the hook file
vim .clinerules/hooks/TaskStart
```
Add your script (must start with shebang)
``` bash
#!/usr/bin/env bash
# Store piped input into a variable
input=$(cat)
# Dump the entire JSON payload
echo "$input" | jq .
# Get the type of a field
echo "$input" | jq -r '.timestamp | type'
```
This example script demonstrates the key mechanics of hook input/output: reading the JSON payload from stdin with `input=$(cat)`, and using `jq` to inspect the data structure and field types that your hook receives. This helps you understand what data is available before building more complex hook logic.
**Make it executable**
```bash
chmod +x .clinerules/hooks/TaskStart
```
</Step>
<Step title="Test Your Hook">
Start a task in Cline and verify your hook executes.
</Step>
</Steps>
<Tip>
Start with a simple hook that just logs information before building complex validation logic. This helps you understand the data structure and timing.
</Tip>
## What You Can Build
Once you understand the basics, hooks open up creative possibilities:
<CardGroup cols={2}>
<Card title="Intelligent Code Review" icon="code-branch">
Run linters or custom validators before files get saved. Block commits that don't pass checks. Track code quality metrics over time.
</Card>
<Card title="Security Enforcement" icon="shield-halved">
Prevent operations that violate security policies. Detect when sensitive data might be exposed. Audit all file access for compliance.
</Card>
<Card title="Development Analytics" icon="chart-line">
Measure how long different operations take. Identify patterns in how the AI works. Generate productivity reports from hook data.
</Card>
<Card title="Integration Hub" icon="plug">
Connect to issue trackers when certain keywords appear. Update project management tools. Sync with external APIs at the right moments.
</Card>
</CardGroup>
The key is combining hooks with external tools. A hook can be the glue between Cline's workflow and the rest of your development ecosystem.
## Hook Types
Cline provides multiple hook types that let you tap into different stages of the AI workflow. They're organized into categories based on their trigger points and use cases.
<Note>
The hook names below are the exact file names you need to create. For example, to use the TaskStart hook, create a file named `TaskStart` (no file extension) in your hooks directory.
</Note>
Each hook receives base fields in addition to its specific data: `clineVersion`, `hookName`, `timestamp`, `taskId`, `workspaceRoots`, `userId`.
### Tool Execution
These hooks intercept and validate tool operations before and after they execute. Use them to enforce policies, track changes, and learn from operations.
#### PreToolUse
Runs before any tool executes. Use it to block invalid operations, validate parameters, and enforce project policies before changes happen.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "PreToolUse",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"preToolUse": {
"toolName": "string",
"parameters": {}
}
}
```
#### PostToolUse
Runs after a tool completes. Use it to learn from results, track performance metrics, and build project knowledge based on operations performed.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "PostToolUse",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"postToolUse": {
"toolName": "string",
"parameters": {},
"result": "string",
"success": boolean,
"executionTimeMs": number
}
}
```
### User Interaction
These hooks monitor and enhance user communication with Cline. Use them to validate input, inject context, and track interaction patterns.
#### UserPromptSubmit
Runs when a user sends a message to Cline. Use it to validate input, inject context based on the prompt, and track interaction patterns.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "UserPromptSubmit",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"userPromptSubmit": {
"prompt": "string",
"attachments": ["string"]
}
}
```
### Task Lifecycle
These hooks monitor and respond to task state changes from start to finish. Use them to track progress, restore state, and trigger workflows.
#### TaskStart
Runs when a new task begins. Use it to detect project type, initialize tracking, and inject initial context that shapes how Cline approaches the work.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskStart",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskStart": {
"taskMetadata": {
"taskId": "string",
"ulid": "string",
"initialTask": "string"
}
}
}
```
#### TaskResume
Runs when a task resumes after interruption. Use it to restore state, refresh context, and log resumption for analytics or external system notifications.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskResume",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskResume": {
"taskMetadata": {
"taskId": "string",
"ulid": "string"
},
"previousState": {
"lastMessageTs": "string",
"messageCount": "string",
"conversationHistoryDeleted": "string"
}
}
}
```
#### TaskCancel
Runs when a task is cancelled. Use it to cleanup resources, log cancellation details, and notify external systems about interrupted work.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskCancel",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskCancel": {
"taskMetadata": {
"taskId": "string",
"ulid": "string",
"completionStatus": "string"
}
}
}
```
{/*
#### TaskComplete
Runs when a task finishes successfully. Use it for final cleanup, tracking metrics, generating reports, and triggering post-task workflows.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskComplete",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskComplete": {
"taskMetadata": {
"taskId": "string",
"ulid": "string"
}
}
}
```
*/}
### System Events
These hooks monitor internal Cline operations and system-level events. Use them to track context usage, log system behavior, and analyze performance patterns.
{/*
#### PreCompact
Runs before conversation context is truncated to fit token limits. Use it to monitor compaction frequency, log events, and track context usage patterns.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "PreCompact",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"preCompact": {
"contextSize": number,
"messagesToCompact": number,
"compactionStrategy": "string"
}
}
```
*/}
### JSON Communication
Hooks receive JSON via stdin and return JSON via stdout.
**Output structure:**
```json
{
"cancel": false,
"contextModification": "WORKSPACE_RULES: Use TypeScript",
"errorMessage": "Error details if blocking"
}
```
Your hook script can output logging or diagnostic information to stdout during execution, as long as the JSON response is the last thing written. Cline will parse only the final JSON object from stdout.
For example:
```bash
#!/usr/bin/env bash
echo "Processing hook..." # This is fine
echo "Tool: $tool_name" # This is also fine
# The JSON must be last:
echo '{"cancel": false}'
```
The `cancel` field controls whether execution continues. Set it to `true` to block an action, `false` to allow it.
The `contextModification` field injects text into the conversation. This affects future AI decisions, not the current one. Use prefixes like `WORKSPACE_RULES:` or `PERFORMANCE:` to help categorize the context.
### Understanding Context Timing
Context injection affects future decisions, not current ones. When a hook runs:
1. The AI has already decided what to do
2. The hook can block or allow it
3. Any context gets added to the conversation
4. The next AI request sees that context
This means PreToolUse hooks are for blocking bad actions, while PostToolUse hooks are for learning from completed ones.
## Troubleshooting
### Hook Not Running
- Ensure the "Enable Hooks" setting is checked
- Verify the hook file is executable (`chmod +x hookname`)
- Check the hook file has no syntax errors
- Look for errors in VSCode's Output panel (Cline channel)
### Hook Timing Out
- Reduce complexity of the hook script
- Avoid expensive operations (network calls, heavy computations)
- Consider moving complex logic to a background process
### Context Not Affecting Behavior
Remember that context modifications affect future AI decisions, not the current operation. The AI's current behavior is based on the previous "API Request..." block, and your `contextModification` gets injected into the next "API Request..." block. This means if you need immediate effect, you should use PreToolUse hooks for validation and return `cancel: true` in your hook's JSON response to block Cline from continuing.
When adding context, ensure your modifications are clear and actionable so the AI can understand and apply them effectively. Also check that your context isn't being truncated due to the 50KB limit, as this could prevent important information from reaching the AI.
### Handling Strings with Quotes in JSON Payloads
When your hook needs to include strings containing unescaped quote characters (`"`) in JSON output, use jq's `--arg` flag for proper escaping:
```bash
#!/usr/bin/env bash
# When $output contains unescaped quote characters (")...
output='{"foo":"bar"}'
# Use the --arg flag for automatic string escaping
jq -n --arg ctx "$output" '{cancel: false, contextModification: $ctx}'
# This will result in:
# {
# "cancel": false,
# "contextModification": "{\"foo\":\"bar\"}"
# }
```
The `--arg` flag automatically escapes special characters, preventing JSON parsing errors when your context modification includes complex strings or nested JSON structures.
<Warning>
Hooks run with the same permissions as VS Code. They can access all workspace files and environment variables. Review hooks from untrusted sources before enabling them.
</Warning>
## Related Features
Hooks complement other Cline features:
- [Cline Rules](/features/cline-rules) define high-level guidance that hooks can enforce
- [Checkpoints](/features/checkpoints) let you roll back changes if a hook didn't catch an issue
- [Auto-Approve](/features/auto-approve) works well with hooks as safety nets for automated operations
+437
View File
@@ -0,0 +1,437 @@
---
title: "Hook Reference"
sidebarTitle: "Hook Reference"
description: "Complete API reference for all Cline hook types, JSON schemas, and field documentation"
---
This reference provides complete technical documentation for all hook types, their JSON schemas, input/output formats, and communication protocols.
## Hook Types
Cline provides multiple hook types that let you tap into different stages of the AI workflow. They're organized into categories based on their trigger points and use cases.
<Note>
The hook names below are the exact file names you need to create. For example, to use the TaskStart hook, create a file named `TaskStart` (no file extension) in your hooks directory.
</Note>
Each hook receives base fields in addition to its specific data: `clineVersion`, `hookName`, `timestamp`, `taskId`, `workspaceRoots`, `userId`.
### Tool Execution Hooks
These hooks intercept and validate tool operations before and after they execute. Use them to enforce policies, track changes, and learn from operations.
#### `PreToolUse`
Triggered immediately before Cline uses any tool (see the [Cline Tools Reference Guide](/cline-tools) for all available tools). Use it to block invalid operations, validate parameters, and enforce project policies before changes happen.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "PreToolUse",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"preToolUse": {
"toolName": "string",
"parameters": {}
}
}
```
**Example Usage:**
```bash
#!/usr/bin/env bash
input=$(cat)
# Block creating .js files in TypeScript projects
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
if [[ "$tool_name" == "write_to_file" ]]; then
file_path=$(echo "$input" | jq -r '.preToolUse.parameters.path')
if [[ "$file_path" == *.js ]] && [[ -f "tsconfig.json" ]]; then
echo '{"cancel": true, "errorMessage": "JavaScript files not allowed in TypeScript project"}'
exit 0
fi
fi
echo '{"cancel": false}'
```
#### `PostToolUse`
Triggered immediately after Cline uses any tool (see the [Cline Tools Reference Guide](/cline-tools) for all available tools). Use it to learn from results, track performance metrics, and build project knowledge based on operations performed.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "PostToolUse",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"postToolUse": {
"toolName": "string",
"parameters": {},
"result": "string",
"success": boolean,
"executionTimeMs": number
}
}
```
**Example Usage:**
```bash
#!/usr/bin/env bash
input=$(cat)
# Log slow operations for performance monitoring
execution_time=$(echo "$input" | jq -r '.postToolUse.executionTimeMs')
tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
if (( execution_time > 5000 )); then
context="PERFORMANCE: Slow operation detected - $tool_name took ${execution_time}ms"
echo "{\"cancel\": false, \"contextModification\": \"$context\"}"
else
echo '{"cancel": false}'
fi
```
### User Interaction Hooks
These hooks monitor and enhance user communication with Cline. Use them to validate input, inject context, and track interaction patterns.
#### `UserPromptSubmit`
Triggered when the user enters text into the prompt box and presses enter to start a new task, continue a completed task, or resume a cancelled task. Use it to validate input, inject context based on the prompt, and track interaction patterns.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "UserPromptSubmit",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"userPromptSubmit": {
"prompt": "string",
"attachments": ["string"]
}
}
```
**Example Usage:**
```bash
#!/usr/bin/env bash
input=$(cat)
# Inject coding standards context for certain keywords
prompt=$(echo "$input" | jq -r '.userPromptSubmit.prompt')
context=""
if echo "$prompt" | grep -qi "component\|react"; then
context="CODING_STANDARDS: Follow React functional component patterns with proper TypeScript types"
elif echo "$prompt" | grep -qi "api\|endpoint"; then
context="CODING_STANDARDS: Use consistent REST API patterns with proper error handling"
fi
if [[ -n "$context" ]]; then
jq -n --arg ctx "$context" '{"cancel": false, "contextModification": $ctx}'
else
echo '{"cancel": false}'
fi
```
### Task Lifecycle Hooks
These hooks monitor and respond to task state changes from start to finish. Use them to track progress, restore state, and trigger workflows.
#### `TaskStart`
Triggered once at the beginning of a new task. Use it to detect project type, initialize tracking, and inject initial context that shapes how Cline approaches the work.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskStart",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskStart": {
"taskMetadata": {
"taskId": "string",
"ulid": "string",
"initialTask": "string"
}
}
}
```
**Example Usage:**
```bash
#!/usr/bin/env bash
input=$(cat)
# Detect project type and inject relevant context
context=""
if [[ -f "package.json" ]]; then
if grep -q "react" package.json; then
context="PROJECT_TYPE: React application detected. Follow component-based architecture."
elif grep -q "express" package.json; then
context="PROJECT_TYPE: Express.js API detected. Follow RESTful patterns."
else
context="PROJECT_TYPE: Node.js project detected."
fi
elif [[ -f "requirements.txt" ]] || [[ -f "pyproject.toml" ]]; then
context="PROJECT_TYPE: Python project detected. Follow PEP 8 standards."
elif [[ -f "Cargo.toml" ]]; then
context="PROJECT_TYPE: Rust project detected. Follow Rust conventions."
fi
if [[ -n "$context" ]]; then
jq -n --arg ctx "$context" '{"cancel": false, "contextModification": $ctx}'
else
echo '{"cancel": false}'
fi
```
#### `TaskResume`
Triggered when the user resumes a task that has been cancelled or aborted. Use it to restore state, refresh context, and log resumption for analytics or external system notifications.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskResume",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskResume": {
"taskMetadata": {
"taskId": "string",
"ulid": "string"
},
"previousState": {
"lastMessageTs": "string",
"messageCount": "string",
"conversationHistoryDeleted": "string"
}
}
}
```
#### `TaskCancel`
Triggered when the user cancels a task or aborts a hook execution. Use it to cleanup resources, log cancellation details, and notify external systems about interrupted work.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskCancel",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskCancel": {
"taskMetadata": {
"taskId": "string",
"ulid": "string"
}
}
}
```
#### `TaskComplete`
Triggered when Cline finishes its work and successfully executes the `attempt_completion` tool to finalize the task output. Use it to track completion metrics, generate reports, log task outcomes, and trigger completion workflows.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskComplete",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskComplete": {
"taskMetadata": {
"taskId": "string",
"ulid": "string"
}
}
}
```
**Example Usage:**
```bash
#!/usr/bin/env bash
input=$(cat)
# Extract task metadata
task_id=$(echo "$input" | jq -r '.taskComplete.taskMetadata.taskId // "unknown"')
ulid=$(echo "$input" | jq -r '.taskComplete.taskMetadata.ulid // "unknown"')
# Log completion
completion_log="$HOME/.cline_completions/$(date +%Y-%m-%d).log"
mkdir -p "$(dirname "$completion_log")"
echo "$(date -Iseconds): Task $task_id completed (ULID: $ulid)" >> "$completion_log"
# Provide context about completion
context="TASK_COMPLETED: Task $task_id finished successfully. Completion logged."
jq -n --arg ctx "$context" '{"cancel": false, "contextModification": $ctx}'
```
### System Events Hooks
These hooks monitor internal Cline operations and system-level events. Use them to track context usage, log system behavior, and analyze performance patterns.
## JSON Communication Protocol
Hooks receive JSON via stdin and return JSON via stdout.
### Input Format
All hooks receive a JSON object through stdin with this base structure:
```json
{
"clineVersion": "string",
"hookName": "string",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"[hookSpecificField]": {
// Hook-specific data structure
}
}
```
### Output Format
Your hook script must output a JSON response as the final stdout content:
```json
{
"cancel": false,
"contextModification": "WORKSPACE_RULES: Use TypeScript",
"errorMessage": "Error details if blocking"
}
```
**Field Descriptions:**
- **`cancel`** (required): Boolean controlling whether execution continues
- `true`: Block the current action
- `false`: Allow the action to proceed
- **`contextModification`** (optional): String that gets injected into the conversation
- Affects future AI decisions, not the current one
- Use clear prefixes like `WORKSPACE_RULES:`, `PERFORMANCE:`, `SECURITY:` for categorization
- Maximum length: 50KB
- **`errorMessage`** (optional): String shown to user when `cancel` is `true`
- Only displayed when blocking an action
- Should explain why the action was blocked
### Logging During Execution
Your hook script can output logging or diagnostic information to stdout during execution, as long as the JSON response is the last thing written:
```bash
#!/usr/bin/env bash
echo "Processing hook..." # This is fine
echo "Tool: $tool_name" # This is also fine
# The JSON must be last:
echo '{"cancel": false}'
```
Cline will parse only the final JSON object from stdout.
### Error Handling
Hook execution errors don't prevent task execution - only returning `"cancel": true` can halt a task. All other errors are treated as hook failures, not reasons to abort the task.
**Hook Status Display:**
- **Completed** (grey): Hook executed successfully, regardless of whether it returned `"cancel": false` or no JSON output
- **Failed** (red): Hook exited with non-zero status, output invalid JSON, or timed out. The UI displays the error details (e.g., exit code number)
- **Aborted** (red): Hook returned `"cancel": true`, halting the task. User must manually resume the task to continue
**Important:** Even when a hook fails (non-zero exit, invalid JSON, timeout), Cline continues with the task. Only `"cancel": true` stops execution.
### Context Modification Timing
Context injection affects future decisions, not current ones. When a hook runs:
1. The AI has already decided what to do
2. The hook can block or allow it
3. Any context gets added to the conversation
4. The next AI request sees that context
This means:
- **PreToolUse hooks**: Use for blocking bad actions + injecting context for next decision
- **PostToolUse hooks**: Use for learning from completed actions
### Helpful Tip: String Escaping in JSON
When your hook needs to include strings containing unescaped quote characters (`"`) in JSON output, use jq's `--arg` flag for proper escaping:
```bash
#!/usr/bin/env bash
# When $output contains unescaped quote characters (")...
output='{"foo":"bar"}'
# Use the --arg flag for automatic string escaping
jq -n --arg ctx "$output" '{cancel: false, contextModification: $ctx}'
# This will result in:
# {
# "cancel": false,
# "contextModification": "{\"foo\":\"bar\"}"
# }
```
The `--arg` flag automatically escapes special characters, preventing JSON parsing errors when your context modification includes complex strings or nested JSON structures.
## Hook Execution Environment
### Execution Context
Hooks are executable scripts that run with the same permissions as VS Code. They have unrestricted access to:
- The entire filesystem (any file the user can access)
- All environment variables
- System commands and tools
- Network resources
Hooks can perform any operation the user could perform in a terminal, including reading and writing files outside the workspace, making network requests, and executing system commands.
### Security Considerations
<Warning>
Hooks run with the same permissions as VS Code. They can access all workspace files and environment variables. Review hooks from untrusted sources before enabling them.
</Warning>
### Performance Guidelines
Hooks have a 30 second timeout. As long as your hook completes within this time, it can perform any operations needed, including network calls or heavy computations.
### Hook Discovery
Cline searches for hooks in this order:
1. Project-specific: `.clinerules/hooks/` in workspace root
2. User-global: `~/Documents/Cline/Rules/Hooks/`
Project-specific hooks override global hooks with the same name.
+146
View File
@@ -0,0 +1,146 @@
---
title: "Hooks Overview"
sidebarTitle: "Overview"
description: "Inject custom logic into Cline's workflow to validate operations, monitor tool usage, and shape AI decisions"
---
Hooks let you inject custom logic into Cline's workflow at key moments. Think of them as automated checkpoints where you can validate operations before they execute, monitor tool usage as it happens, and shape how Cline makes decisions.
Hooks run automatically when specific events happen during development. They receive detailed information about each operation, can block problematic actions before they cause issues, and can inject context that guides future AI decisions.
The real power comes from combining these capabilities. You can:
- Stop operations before they cause problems (like creating `.js` files in a TypeScript project)
- Learn from what's happening and build up project knowledge over time
- Monitor performance and catch issues as they emerge
- Track everything for analytics or compliance
- Trigger external tools or services at the right moments
## Getting Started
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/hooks.gif" alt="Hooks in action" />
</Frame>
<Note>
Hooks work across all platforms: Windows, macOS, and Linux. The bash examples in this documentation work with standard shells on all platforms (including Git Bash or WSL on Windows).
</Note>
Setting up hooks in Cline is user-friendly with the built-in hooks management interface. Here's how to get started:
<Steps>
<Step title="Access the Hooks Interface">
Navigate to the Hooks management interface:
<Frame>
<img src="/assets/hooks/hooks-interface-with-dropdown.png" alt="Hooks management interface showing Global Hooks and project-specific hooks with dropdown menu" />
</Frame>
1. Open Cline (ensure hooks are enabled in settings)
2. Look for the **Hooks** tab at the top (alongside Rules and Workflows)
3. Click on **Hooks** to open the hooks management panel
The interface shows you all available hook types and existing hooks organized by workspace.
</Step>
<Step title="Understand Hook Locations">
Hooks are automatically organized by location in the interface:
**Global Hooks** - Apply to all workspaces:
- Stored in `~/Documents/Cline/Rules/Hooks/`
- Perfect for personal coding standards and universal rules
**Project-Specific Hooks** - Apply only to current project:
- Stored in `.clinerules/hooks/` within your repo
- Great for project-specific validation and team workflows
- Can be committed to version control for team sharing
Multi-root workspaces run hooks from all of the repos in your open workspace, making it easy to manage and run hooks across different repos within the same workspace.
</Step>
<Step title="Create Your First Hook">
Use the intuitive interface to create hooks:
<Frame>
<img src="/assets/hooks/hooks-empty-state.png" alt="Empty hooks interface showing New hook... dropdowns for both Global Hooks and project-specific hooks before any hooks are created" />
</Frame>
1. **Choose your location**: Decide between Global Hooks or project-specific hooks
2. **Select hook type**: Click the **"New hook..."** dropdown in your chosen location
3. **Pick a hook type**: The dropdown shows all available hook types that haven't been created yet in this location. Only one of each hook type is allowed per hooks directory, so the dropdown automatically filters to show only the remaining available types.
<Frame>
<img src="/assets/hooks/new-hook-dropdown.png" alt="Creating a new hook with the dropdown menu showing UserPromptSubmit selected with description" />
</Frame>
4. **Review and edit the hook**: Click the pencil icon to review the hook's code and add your custom logic
5. **Enable the hook**: Once you understand and approve of the hook's behavior, toggle the switch to activate it
<Frame>
<img src="/assets/hooks/hook-controls.png" alt="Hook management controls showing toggle, edit, and delete buttons for each hook" />
</Frame>
<Warning>
Always review a hook's code before enabling it. Hooks execute automatically during your workflow, so it's important to understand what they do before activation.
</Warning>
</Step>
<Step title="Test Your Hook">
To develop and refine your hook, you'll need to trigger it multiple times during testing. Each hook type is triggered by different events in Cline's workflow. For example:
- **TaskStart** hooks trigger when you start a new task
- **PreToolUse** hooks trigger before Cline executes tools like file editing
- **PostToolUse** hooks trigger after tool execution completes
- **UserPromptSubmit** hooks trigger when you submit a message to Cline
For complete details on when each hook type is triggered and how to test them effectively, see the [Hook Reference](/features/hooks/hook-reference) documentation. This includes the specific conditions that trigger each hook and examples of how to invoke them during development.
</Step>
</Steps>
<Tip>
Start with a simple hook that just logs information before building complex validation logic. This helps you understand the data structure and timing.
</Tip>
## What You Can Build
Once you understand the basics, hooks open up creative possibilities:
<CardGroup cols={2}>
<Card title="Intelligent Code Review" icon="code-branch">
Run linters or custom validators before files get saved. Block commits that don't pass checks. Track code quality metrics over time.
</Card>
<Card title="Security Enforcement" icon="shield-halved">
Prevent operations that violate security policies. Detect when sensitive data might be exposed. Audit all file access for compliance.
</Card>
<Card title="Development Analytics" icon="chart-line">
Measure how long different operations take. Identify patterns in how the AI works. Generate productivity reports from hook data.
</Card>
<Card title="Integration Hub" icon="plug">
Connect to issue trackers when certain keywords appear. Update project management tools. Sync with external APIs at the right moments.
</Card>
</CardGroup>
The key is combining hooks with external tools. A hook can be the glue between Cline's workflow and the rest of your development ecosystem.
## Explore the Documentation
<CardGroup cols={2}>
<Card title="Hook Reference" icon="book" href="/features/hooks/hook-reference">
Complete API reference for all hook types, JSON schemas, and field documentation.
</Card>
<Card title="Samples" icon="code" href="/features/hooks/samples">
Practical examples and complete working scripts for common use cases.
</Card>
</CardGroup>
## Related Features
Hooks complement other Cline features:
- [Cline Rules](/features/cline-rules) define high-level guidance that hooks can enforce
- [Checkpoints](/features/checkpoints) let you roll back changes if a hook didn't catch an issue
- [Auto-Approve](/features/auto-approve) works well with hooks as safety nets for automated operations
+755
View File
@@ -0,0 +1,755 @@
---
title: "Samples"
sidebarTitle: "Samples"
description: "Practical hook examples organized by complexity level - from beginner to advanced patterns"
---
This page provides complete, production-ready hook examples organized by skill level. Each example includes full working code, detailed explanations, and guidance on when to use each pattern.
## How to Use These Samples
Each sample is designed to be:
- **Copy-and-paste ready**: Use them directly or as starting points
- **Educational**: Learn hook concepts through progressive complexity
- **Practical**: Solve real development workflow challenges
Choose samples based on your experience level and gradually work up to more advanced patterns.
---
## Beginner Examples
Perfect for getting started with hooks. These examples demonstrate core concepts with straightforward logic.
### 1. Project Type Detection
**Hook:** `TaskStart`
```bash
#!/usr/bin/env bash
# Project Type Detection Hook
#
# Overview: Automatically detects project type at task start and injects relevant
# coding standards and best practices into the AI context. This helps Cline understand
# your project structure and apply appropriate conventions from the beginning.
#
# Demonstrates: Basic hook input/output, file system checks, conditional logic,
# and context injection to guide AI behavior.
input=$(cat)
# Read basic JSON structure and detect project type
context=""
# Check for different project indicators
if [[ -f "package.json" ]]; then
if grep -q "react" package.json; then
context="PROJECT_TYPE: React application detected. Follow component-based architecture and use functional components."
elif grep -q "express" package.json; then
context="PROJECT_TYPE: Express.js API detected. Follow RESTful patterns and proper middleware structure."
else
context="PROJECT_TYPE: Node.js project detected. Use proper npm scripts and dependency management."
fi
elif [[ -f "requirements.txt" ]] || [[ -f "pyproject.toml" ]]; then
context="PROJECT_TYPE: Python project detected. Follow PEP 8 standards and use virtual environments."
elif [[ -f "Cargo.toml" ]]; then
context="PROJECT_TYPE: Rust project detected. Follow Rust conventions and use proper error handling."
elif [[ -f "go.mod" ]]; then
context="PROJECT_TYPE: Go project detected. Follow Go conventions and use proper package structure."
fi
# Return the context to guide Cline's behavior
if [[ -n "$context" ]]; then
jq -n --arg ctx "$context" '{"cancel": false, "contextModification": $ctx}'
else
echo '{"cancel": false}'
fi
```
**Key Concepts:**
- Reading hook input with `input=$(cat)`
- Using file system checks to detect project type
- Returning context to influence AI behavior
- Basic JSON output with `jq`
### 2. File Extension Validator
**Hook:** `PreToolUse`
```bash
#!/usr/bin/env bash
# File Extension Validator Hook
#
# Overview: Enforces TypeScript file extensions in TypeScript projects by blocking
# creation of .js and .jsx files. This prevents common mistakes where developers
# accidentally create JavaScript files when they should be using TypeScript.
#
# Demonstrates: PreToolUse blocking, parameter extraction, conditional validation,
# and providing clear error messages to guide users toward correct file extensions.
input=$(cat)
# Extract tool information
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
# Only process file creation tools
if [[ "$tool_name" != "write_to_file" ]]; then
echo '{"cancel": false}'
exit 0
fi
# Check if this is a TypeScript project
if [[ ! -f "tsconfig.json" ]]; then
echo '{"cancel": false}'
exit 0
fi
# Get the file path from tool parameters
file_path=$(echo "$input" | jq -r '.preToolUse.parameters.path // empty')
if [[ -z "$file_path" ]]; then
echo '{"cancel": false}'
exit 0
fi
# Block .js files in TypeScript projects
if [[ "$file_path" == *.js ]]; then
echo '{"cancel": true, "errorMessage": "JavaScript files (.js) are not allowed in TypeScript projects. Use .ts extension instead."}'
exit 0
fi
# Block .jsx files, suggest .tsx
if [[ "$file_path" == *.jsx ]]; then
echo '{"cancel": true, "errorMessage": "JSX files (.jsx) are not allowed in TypeScript projects. Use .tsx extension instead."}'
exit 0
fi
# Everything is OK
echo '{"cancel": false}'
```
**Key Concepts:**
- Extracting tool name and parameters
- Conditional logic based on project state
- Blocking operations with `"cancel": true`
- Providing helpful error messages
### 3. Basic Performance Monitor
**Hook:** `PostToolUse`
```bash
#!/usr/bin/env bash
# Basic Performance Monitor Hook
#
# Overview: Monitors tool execution times and logs operations that exceed a 3-second
# threshold. This helps identify performance bottlenecks and provides feedback to
# users about system resource issues that may be slowing down Cline's operations.
#
# Demonstrates: PostToolUse hook usage, arithmetic operations in bash, simple file
# logging, and conditional context injection based on performance metrics.
input=$(cat)
# Extract performance information
tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
execution_time=$(echo "$input" | jq -r '.postToolUse.executionTimeMs // 0')
success=$(echo "$input" | jq -r '.postToolUse.success')
# Log slow operations (threshold: 3 seconds)
if (( execution_time > 3000 )); then
# Create simple log directory
mkdir -p "$HOME/.cline_logs"
# Log the slow operation
echo "$(date -Iseconds): SLOW OPERATION - $tool_name took ${execution_time}ms" >> "$HOME/.cline_logs/performance.log"
# Provide feedback to user
context="PERFORMANCE: Operation $tool_name took ${execution_time}ms. Consider checking system resources if this happens frequently."
jq -n --arg ctx "$context" '{"cancel": false, "contextModification": $ctx}'
else
echo '{"cancel": false}'
fi
```
**Key Concepts:**
- Processing results after tool execution
- Basic arithmetic operations in bash
- Simple file logging
- Conditional context injection
## Intermediate Examples
These examples demonstrate more advanced concepts including external tool integration, pattern matching, and structured logging.
### 4. Code Quality with Linting
**Hook:** `PreToolUse`
```bash
#!/usr/bin/env bash
# Code Quality Linting Hook
#
# Overview: Integrates ESLint and Flake8 to enforce code quality standards before
# files are written. Blocks file creation if linting errors are detected, ensuring
# all code meets quality standards. Supports TypeScript, JavaScript, and Python files.
#
# Demonstrates: External tool integration, temporary file handling, regex pattern
# matching, and comprehensive error reporting with actionable feedback.
input=$(cat)
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
# Only lint file write operations
if [[ "$tool_name" != "write_to_file" ]]; then
echo '{"cancel": false}'
exit 0
fi
file_path=$(echo "$input" | jq -r '.preToolUse.parameters.path // empty')
# Skip non-code files
if [[ ! "$file_path" =~ \.(ts|tsx|js|jsx|py|rs)$ ]]; then
echo '{"cancel": false}'
exit 0
fi
# Get file content from the tool parameters
content=$(echo "$input" | jq -r '.preToolUse.parameters.content // empty')
if [[ -z "$content" ]]; then
echo '{"cancel": false}'
exit 0
fi
# Create temporary file for linting
temp_file=$(mktemp)
echo "$content" > "$temp_file"
# Run appropriate linter based on file extension
lint_errors=""
if [[ "$file_path" =~ \.(ts|tsx)$ ]] && command -v eslint > /dev/null; then
lint_output=$(eslint "$temp_file" --format=json 2>/dev/null || true)
if [[ "$lint_output" != "[]" ]] && [[ -n "$lint_output" ]]; then
error_count=$(echo "$lint_output" | jq '.[0].errorCount // 0')
if (( error_count > 0 )); then
messages=$(echo "$lint_output" | jq -r '.[0].messages[] | "\(.line):\(.column) \(.message)"')
lint_errors="ESLint errors found:\n$messages"
fi
fi
elif [[ "$file_path" =~ \.py$ ]] && command -v flake8 > /dev/null; then
lint_output=$(flake8 "$temp_file" 2>/dev/null || true)
if [[ -n "$lint_output" ]]; then
lint_errors="Flake8 errors found:\n$lint_output"
fi
fi
# Cleanup
rm -f "$temp_file"
# Block if linting errors found
if [[ -n "$lint_errors" ]]; then
error_message="Code quality check failed. Please fix these issues:\n\n$lint_errors"
jq -n --arg msg "$error_message" '{"cancel": true, "errorMessage": $msg}'
else
echo '{"cancel": false}'
fi
```
**Key Concepts:**
- Temporary file creation and cleanup
- External tool integration (eslint, flake8)
- Complex pattern matching with regex
- Structured error reporting
### 5. Security Scanner
**Hook:** `PreToolUse`
```bash
#!/usr/bin/env bash
# Security Scanner Hook
#
# Overview: Scans file content for hardcoded secrets (API keys, tokens, passwords)
# before files are written. Blocks creation of files containing secrets except in
# safe locations like .env.example files or documentation, preventing credential leaks.
#
# Demonstrates: Pattern matching with regex arrays, file path exception handling,
# security-focused validation, and clear user guidance in error messages.
input=$(cat)
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
# Only check file operations
if [[ "$tool_name" != "write_to_file" ]]; then
echo '{"cancel": false}'
exit 0
fi
content=$(echo "$input" | jq -r '.preToolUse.parameters.content // empty')
file_path=$(echo "$input" | jq -r '.preToolUse.parameters.path // empty')
# Skip if no content
if [[ -z "$content" ]]; then
echo '{"cancel": false}'
exit 0
fi
# Define secret patterns (simplified for readability)
secrets_found=""
# Check for API keys
if echo "$content" | grep -qi "api[_-]*key.*[=:].*['\"][a-z0-9_-]{10,}['\"]"; then
secrets_found+="- API key pattern detected\n"
fi
# Check for tokens
if echo "$content" | grep -qi "token.*[=:].*['\"][a-z0-9_-]{10,}['\"]"; then
secrets_found+="- Token pattern detected\n"
fi
# Check for passwords
if echo "$content" | grep -qi "password.*[=:].*['\"][^'\"]{8,}['\"]"; then
secrets_found+="- Password pattern detected\n"
fi
# Allow secrets in safe files
safe_patterns=("\.env\.example$" "\.env\.template$" "/docs/" "\.md$")
is_safe_file=false
for safe_pattern in "${safe_patterns[@]}"; do
if [[ "$file_path" =~ $safe_pattern ]]; then
is_safe_file=true
break
fi
done
if [[ -n "$secrets_found" ]] && [[ "$is_safe_file" == false ]]; then
error_message="🔒 SECURITY ALERT: Potential secrets detected in $file_path
$secrets_found
Please use environment variables or a secrets management service instead."
jq -n --arg msg "$error_message" '{"cancel": true, "errorMessage": $msg}'
else
echo '{"cancel": false}'
fi
```
**Key Concepts:**
- Pattern arrays and iteration
- File path exception handling
- Security-focused validation
- Clear user guidance in error messages
### 6. Git Workflow Assistant
**Hook:** `PostToolUse`
```bash
#!/usr/bin/env bash
# Git Workflow Assistant Hook
#
# Overview: Analyzes file modifications and provides intelligent git workflow suggestions
# based on file types and current branch. Encourages best practices like feature branches
# for components and test branches for test files, with actionable git commands.
#
# Demonstrates: Git integration, branch analysis, file path pattern matching, and
# contextual suggestions to guide users toward better git practices.
input=$(cat)
tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
success=$(echo "$input" | jq -r '.postToolUse.success')
# Only process successful file modifications
if [[ "$success" != "true" ]] || [[ "$tool_name" != "write_to_file" && "$tool_name" != "replace_in_file" ]]; then
echo '{"cancel": false}'
exit 0
fi
# Check if we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo '{"cancel": false}'
exit 0
fi
file_path=$(echo "$input" | jq -r '.postToolUse.parameters.path // empty')
current_branch=$(git branch --show-current 2>/dev/null || echo "main")
# Analyze file type and suggest appropriate branch naming
context=""
if [[ "$file_path" == *"component"* ]] && [[ "$current_branch" == "main" || "$current_branch" == "master" ]]; then
component_name=$(basename "$file_path" .tsx .ts .jsx .js)
context="GIT_WORKFLOW: Consider creating a feature branch: git checkout -b feature/add-${component_name,,}-component"
elif [[ "$file_path" == *"test"* ]] || [[ "$file_path" == *"spec"* ]]; then
if [[ "$current_branch" == "main" || "$current_branch" == "master" ]]; then
context="GIT_WORKFLOW: Consider creating a test branch: git checkout -b test/add-tests-$(basename "$(dirname "$file_path")")"
fi
fi
# Add staging guidance
if [[ -n "$context" ]]; then
context="$context After completing changes, use 'git add $file_path' to stage for commit."
else
context="GIT_WORKFLOW: File modified: $file_path. Use 'git add $file_path' when ready to commit."
fi
jq -n --arg ctx "$context" '{"cancel": false, "contextModification": $ctx}'
```
**Key Concepts:**
- Git repository detection
- Branch analysis and suggestions
- File path analysis for context
- Actionable user guidance
## Advanced Examples
These examples showcase sophisticated patterns including external integrations, asynchronous processing, and complex state management.
### 7. Comprehensive Task Lifecycle Manager
**Hook:** `TaskComplete`
```bash
#!/usr/bin/env bash
# Comprehensive Task Lifecycle Manager Hook
#
# Overview: Tracks task completions by generating detailed markdown reports with
# workspace information and git state, and optionally sends webhook notifications
# to external systems. Perfect for enterprise environments requiring audit trails.
#
# Demonstrates: Complex data extraction, structured report generation, markdown
# heredocs, asynchronous webhook notifications, and robust error handling.
input=$(cat)
# Extract task metadata using proper API field paths
task_id=$(echo "$input" | jq -r '.taskId')
ulid=$(echo "$input" | jq -r '.taskComplete.taskMetadata.ulid // "unknown"')
completion_time=$(echo "$input" | jq -r '.timestamp')
# Create completion report directory with error handling
reports_dir="$HOME/.cline_reports"
if [[ ! -d "$(dirname "$reports_dir")" ]]; then
echo '{"cancel": false, "errorMessage": "Cannot access home directory"}'
exit 0
fi
mkdir -p "$reports_dir" || exit 0
# Generate safe, unique report filename
safe_task_id=$(echo "$task_id" | tr -cd '[:alnum:]_-' | head -c 50)
report_file="$reports_dir/completion_$(date +%Y%m%d_%H%M%S)_${safe_task_id}.md"
# Collect comprehensive workspace information
git_branch=$(git branch --show-current 2>/dev/null || echo "No git repository")
git_status_count=$(git status --porcelain 2>/dev/null | wc -l || echo "0")
project_name=$(basename "$PWD")
# Generate detailed completion report
cat > "$report_file" << EOF
# Cline Task Completion Report
**Task ID:** $task_id
**ULID:** $ulid
**Completed:** $(date -Iseconds)
**Completion Time:** $completion_time
## Workspace Information
- **Project:** $project_name
- **Git Branch:** $git_branch
- **Modified Files:** $git_status_count
## Completion Status
✅ Task completed successfully
## Next Steps
- Review changes made during this task
- Consider committing changes if appropriate
- Run tests to verify functionality
EOF
# Send webhook notification if configured
webhook_url="${COMPLETION_WEBHOOK_URL:-}"
if [[ -n "$webhook_url" ]]; then
payload=$(jq -n \
--arg task_id "$task_id" \
--arg ulid "$ulid" \
--arg workspace "$project_name" \
--arg timestamp "$completion_time" \
'{
event: "task_completed",
task_id: $task_id,
ulid: $ulid,
workspace: $workspace,
timestamp: $timestamp
}')
# Send notification in background with timeout
(curl -X POST \
-H "Content-Type: application/json" \
-d "$payload" \
"$webhook_url" \
--max-time 5 \
--silent > /dev/null 2>&1) &
fi
context="TASK_COMPLETED: ✅ Task $task_id finished successfully. Report saved to: $(basename "$report_file")"
jq -n --arg ctx "$context" '{"cancel": false, "contextModification": $ctx}'
```
**Key Concepts:**
- Complex data extraction and validation
- Structured report generation
- Asynchronous webhook notifications
- Error handling and resource management
### 8. Intelligent User Input Enhancer
**Hook:** `UserPromptSubmit`
```bash
#!/usr/bin/env bash
# Intelligent User Input Enhancer Hook
#
# Overview: Analyzes user prompts to detect potentially harmful commands, logs user
# activity for analytics, and intelligently injects project and git context based on
# prompt keywords. Provides safety guards while enhancing AI responses with relevant context.
#
# Demonstrates: UserPromptSubmit hook usage, multi-pattern safety validation, intelligent
# context detection from prompts, structured JSON logging, and dynamic suggestion generation.
input=$(cat)
user_prompt=$(echo "$input" | jq -r '.userPromptSubmit.prompt')
task_id=$(echo "$input" | jq -r '.taskId')
user_id=$(echo "$input" | jq -r '.userId')
# Log user activity for analytics
activity_log="$HOME/.cline_user_activity/$(date +%Y-%m-%d).log"
mkdir -p "$(dirname "$activity_log")"
activity_entry=$(jq -n \
--arg timestamp "$(date -Iseconds)" \
--arg task_id "$task_id" \
--arg user_id "$user_id" \
--arg prompt_length "${#user_prompt}" \
'{
timestamp: $timestamp,
task_id: $task_id,
user_id: $user_id,
prompt_length: ($prompt_length | tonumber),
workspace: env.PWD
}')
echo "$activity_entry" >> "$activity_log"
context_modifications=""
cancel_request=false
# Safety validation
harmful_patterns=("rm -rf" "delete.*all" "format.*drive" "sudo.*passwd")
for pattern in "${harmful_patterns[@]}"; do
if echo "$user_prompt" | grep -qi "$pattern"; then
cancel_request=true
error_message="🚨 SAFETY ALERT: Potentially harmful command detected. Please review your request."
break
fi
done
# Intelligent context enhancement
if [[ "$cancel_request" == false ]]; then
# Detect project context
if echo "$user_prompt" | grep -qi "file\|directory\|folder"; then
if [[ -f "package.json" ]]; then
project_name=$(jq -r '.name // "unknown"' package.json 2>/dev/null)
context_modifications+="PROJECT_CONTEXT: Working in Node.js project '$project_name'. "
elif [[ -f "requirements.txt" ]]; then
context_modifications+="PROJECT_CONTEXT: Working in Python project. "
fi
fi
# Git context enhancement
if echo "$user_prompt" | grep -qi "git\|commit\|branch" && git rev-parse --git-dir > /dev/null 2>&1; then
current_branch=$(git branch --show-current 2>/dev/null)
uncommitted=$(git status --porcelain | wc -l)
context_modifications+="GIT_CONTEXT: On branch '$current_branch' with $uncommitted uncommitted changes. "
fi
# Tool suggestions
if echo "$user_prompt" | grep -qi "search.*code\|find.*function"; then
context_modifications+="SUGGESTION: Consider using search_files tool for code exploration. "
fi
fi
# Return response
if [[ "$cancel_request" == true ]]; then
jq -n --arg msg "$error_message" '{"cancel": true, "errorMessage": $msg}'
else
if [[ -n "$context_modifications" ]]; then
jq -n --arg ctx "$context_modifications" '{"cancel": false, "contextModification": $ctx}'
else
echo '{"cancel": false}'
fi
fi
```
**Key Concepts:**
- User interaction analysis and logging
- Multi-pattern safety validation
- Intelligent context detection
- Dynamic suggestion generation
### 9. Multi-Service Integration Hub
**Hook:** `PostToolUse`
```bash
#!/usr/bin/env bash
# Multi-Service Integration Hub Hook
#
# Overview: Detects file modifications by type (dependencies, CI/CD, frontend, backend, tests)
# and sends asynchronous webhook notifications to multiple external services like Slack and
# CI/CD systems. Enables seamless integration of Cline operations into enterprise workflows.
#
# Demonstrates: Advanced pattern matching with associative arrays, multi-service webhook
# orchestration, asynchronous background processing, and enterprise notification patterns.
input=$(cat)
tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
success=$(echo "$input" | jq -r '.postToolUse.success')
file_path=$(echo "$input" | jq -r '.postToolUse.parameters.path // empty')
# Only process successful file operations
if [[ "$success" != "true" ]] || [[ "$tool_name" != "write_to_file" && "$tool_name" != "replace_in_file" ]]; then
echo '{"cancel": false}'
exit 0
fi
# Define workflow triggers
declare -A triggers=(
["package\\.json|yarn\\.lock"]="dependencies"
["\\.github/workflows/"]="ci_cd"
["src/.*component"]="frontend"
["api/.*\\.(ts|js)"]="backend"
[".*\\.(test|spec)\\."]="testing"
)
# Determine triggered workflows
triggered_workflows=""
for pattern in "${!triggers[@]}"; do
if [[ "$file_path" =~ $pattern ]]; then
workflow_type="${triggers[$pattern]}"
triggered_workflows+="$workflow_type "
fi
done
context="WORKFLOW: File modified: $file_path"
if [[ -n "$triggered_workflows" ]]; then
# Slack notification (async)
slack_webhook="${SLACK_WEBHOOK_URL:-}"
if [[ -n "$slack_webhook" ]]; then
slack_payload=$(jq -n \
--arg file "$file_path" \
--arg workflows "$triggered_workflows" \
--arg workspace "$(basename "$PWD")" \
'{
text: ("🔧 Cline modified `" + $file + "` in " + $workspace),
color: "good",
fields: [{
title: "Triggered Workflows",
value: $workflows,
short: true
}]
}')
(curl -X POST -H "Content-Type: application/json" -d "$slack_payload" "$slack_webhook" --max-time 5 --silent > /dev/null 2>&1) &
fi
# CI/CD webhook (async)
ci_webhook="${CI_WEBHOOK_URL:-}"
if [[ -n "$ci_webhook" ]]; then
ci_payload=$(jq -n \
--arg file "$file_path" \
--arg workflows "$triggered_workflows" \
'{
event: "file_modified",
file_path: $file,
workflows: ($workflows | split(" "))
}')
(curl -X POST -H "Content-Type: application/json" -d "$ci_payload" "$ci_webhook" --max-time 5 --silent > /dev/null 2>&1) &
fi
context+=" Triggered workflows: $triggered_workflows. Notifications sent to configured services."
fi
jq -n --arg ctx "$context" '{"cancel": false, "contextModification": $ctx}'
```
**Key Concepts:**
- Multi-service integration patterns
- Asynchronous webhook orchestration
- Complex workflow detection
- Enterprise notification systems
## Usage Tips
### Running Multiple Hooks
You can use multiple hooks together by creating separate files for each hook type:
```bash
# Create hooks directory
mkdir -p .clinerules/hooks
# Create multiple hooks
touch .clinerules/hooks/PreToolUse
touch .clinerules/hooks/PostToolUse
touch .clinerules/hooks/TaskStart
# Make them executable
chmod +x .clinerules/hooks/*
```
### Environment Configuration
Set up environment variables for external integrations:
```bash
# Add to your .bashrc or .zshrc
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
export JIRA_URL="https://yourcompany.atlassian.net"
export JIRA_USER="your-email@company.com"
export JIRA_TOKEN="your-api-token"
export CI_WEBHOOK_URL="https://your-ci-system.com/hooks/cline"
```
### Testing Your Hooks
Test hooks manually by simulating their input:
```bash
# Test a PreToolUse hook
echo '{
"clineVersion": "1.0.0",
"hookName": "PreToolUse",
"timestamp": "2024-01-01T12:00:00Z",
"taskId": "test",
"workspaceRoots": ["/path/to/workspace"],
"userId": "test-user",
"preToolUse": {
"toolName": "write_to_file",
"parameters": {
"path": "test.js",
"content": "console.log(\"test\");"
}
}
}' | .clinerules/hooks/PreToolUse
```
These examples provide a solid foundation for implementing hooks in your development workflow. Customize them based on your specific needs, tools, and integrations.
+5 -6
View File
@@ -361,10 +361,9 @@ description: "Get Cline up and running in your favorite IDE with these simple in
<Info>
You'll be redirected to [app.cline.bot](https://app.cline.bot) to authenticate. After signing in, you'll automatically return to your editor.
</Info>
<Frame>
<img src="/assets/installation/login.png" alt="Cline sign up screen"
/>
</Frame>
<Info>
You'll be redirected to the Cline authentication page to sign in with your account.
</Info>
</Step>
<Step title="You're All Set!">
@@ -403,7 +402,7 @@ description: "Get Cline up and running in your favorite IDE with these simple in
Connect with our team and community for support, tips, and discussions.
</Card>
<Card title="Read the Docs" icon="book-open" href="/getting-started/for-new-coders">
Explore guides for new coders, model selection, and advanced features.
<Card title="Read the Docs" icon="book-open" href="/getting-started/selecting-your-model">
Explore model selection guides and advanced features to get the most out of Cline.
</Card>
</CardGroup>
+1 -1
View File
@@ -14,7 +14,7 @@ SAP AI Core, and Generative AI Hub, are offerings from SAP BTP. You need an acti
### Getting a Service Binding
1. **Access:** Go to your subaccount via [BTP Cloud Cockpit](cockpit.btp.cloud.sap/cockpit)
1. **Access:** Go to your subaccount via [BTP Cloud Cockpit](https://cockpit.btp.cloud.sap/cockpit)
2. **Create a Service Binding:** Go to "Instances and Subscriptions", select your SAP AI Core service instance and click on Service Bindings > Create.
3. **Copy the Service Binding:** Copy the service binding values.
+54 -101
View File
@@ -6,7 +6,7 @@
"packages": {
"": {
"name": "claude-dev",
"version": "3.39.1",
"version": "3.39.2",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -43,7 +43,6 @@
"@playwright/test": "^1.55.1",
"@sap-ai-sdk/ai-api": "^2.1.0",
"@sap-ai-sdk/orchestration": "^2.1.0",
"@sentry/browser": "^9.12.0",
"@streamparser/json": "^0.0.22",
"@tailwindcss/vite": "^4.1.14",
"@types/uuid": "^10.0.0",
@@ -1677,13 +1676,13 @@
}
},
"node_modules/@changesets/apply-release-plan": {
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.12.tgz",
"integrity": "sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==",
"version": "7.0.14",
"resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.14.tgz",
"integrity": "sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@changesets/config": "^3.1.1",
"@changesets/config": "^3.1.2",
"@changesets/get-version-range-type": "^0.4.0",
"@changesets/git": "^3.0.4",
"@changesets/should-skip-package": "^0.1.2",
@@ -1724,27 +1723,27 @@
}
},
"node_modules/@changesets/cli": {
"version": "2.29.6",
"resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.29.6.tgz",
"integrity": "sha512-6qCcVsIG1KQLhpQ5zE8N0PckIx4+9QlHK3z6/lwKnw7Tir71Bjw8BeOZaxA/4Jt00pcgCnCSWZnyuZf5Il05QQ==",
"version": "2.29.8",
"resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.29.8.tgz",
"integrity": "sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@changesets/apply-release-plan": "^7.0.12",
"@changesets/apply-release-plan": "^7.0.14",
"@changesets/assemble-release-plan": "^6.0.9",
"@changesets/changelog-git": "^0.2.1",
"@changesets/config": "^3.1.1",
"@changesets/config": "^3.1.2",
"@changesets/errors": "^0.2.0",
"@changesets/get-dependents-graph": "^2.1.3",
"@changesets/get-release-plan": "^4.0.13",
"@changesets/get-release-plan": "^4.0.14",
"@changesets/git": "^3.0.4",
"@changesets/logger": "^0.1.1",
"@changesets/pre": "^2.0.2",
"@changesets/read": "^0.6.5",
"@changesets/read": "^0.6.6",
"@changesets/should-skip-package": "^0.1.2",
"@changesets/types": "^6.1.0",
"@changesets/write": "^0.4.0",
"@inquirer/external-editor": "^1.0.0",
"@inquirer/external-editor": "^1.0.2",
"@manypkg/get-packages": "^1.1.3",
"ansi-colors": "^4.1.3",
"ci-info": "^3.7.0",
@@ -1778,9 +1777,9 @@
}
},
"node_modules/@changesets/config": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.1.tgz",
"integrity": "sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.2.tgz",
"integrity": "sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1817,16 +1816,16 @@
}
},
"node_modules/@changesets/get-release-plan": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.13.tgz",
"integrity": "sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.14.tgz",
"integrity": "sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@changesets/assemble-release-plan": "^6.0.9",
"@changesets/config": "^3.1.1",
"@changesets/config": "^3.1.2",
"@changesets/pre": "^2.0.2",
"@changesets/read": "^0.6.5",
"@changesets/read": "^0.6.6",
"@changesets/types": "^6.1.0",
"@manypkg/get-packages": "^1.1.3"
}
@@ -1863,14 +1862,14 @@
}
},
"node_modules/@changesets/parse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.1.tgz",
"integrity": "sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==",
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.2.tgz",
"integrity": "sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@changesets/types": "^6.1.0",
"js-yaml": "^3.13.1"
"js-yaml": "^4.1.1"
}
},
"node_modules/@changesets/pre": {
@@ -1887,15 +1886,15 @@
}
},
"node_modules/@changesets/read": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.5.tgz",
"integrity": "sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==",
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.6.tgz",
"integrity": "sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@changesets/git": "^3.0.4",
"@changesets/logger": "^0.1.1",
"@changesets/parse": "^0.4.1",
"@changesets/parse": "^0.4.2",
"@changesets/types": "^6.1.0",
"fs-extra": "^7.0.1",
"p-filter": "^2.1.0",
@@ -2199,14 +2198,14 @@
}
},
"node_modules/@inquirer/external-editor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.1.tgz",
"integrity": "sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
"integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chardet": "^2.1.0",
"iconv-lite": "^0.6.3"
"chardet": "^2.1.1",
"iconv-lite": "^0.7.0"
},
"engines": {
"node": ">=18"
@@ -2220,6 +2219,23 @@
}
}
},
"node_modules/@inquirer/external-editor/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
@@ -4718,69 +4734,6 @@
"node": ">=20.0.0"
}
},
"node_modules/@sentry-internal/browser-utils": {
"version": "9.12.0",
"license": "MIT",
"dependencies": {
"@sentry/core": "9.12.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "9.12.0",
"license": "MIT",
"dependencies": {
"@sentry/core": "9.12.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "9.12.0",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "9.12.0",
"@sentry/core": "9.12.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "9.12.0",
"license": "MIT",
"dependencies": {
"@sentry-internal/replay": "9.12.0",
"@sentry/core": "9.12.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/browser": {
"version": "9.12.0",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "9.12.0",
"@sentry-internal/feedback": "9.12.0",
"@sentry-internal/replay": "9.12.0",
"@sentry-internal/replay-canvas": "9.12.0",
"@sentry/core": "9.12.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/core": {
"version": "9.12.0",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@sindresorhus/merge-streams": {
"version": "4.0.0",
"license": "MIT",
@@ -7598,9 +7551,9 @@
}
},
"node_modules/chardet": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz",
"integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
"integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
"license": "MIT"
},
"node_modules/check-error": {
+1 -2
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.39.2",
"version": "3.40.0",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -486,7 +486,6 @@
"@playwright/test": "^1.55.1",
"@sap-ai-sdk/ai-api": "^2.1.0",
"@sap-ai-sdk/orchestration": "^2.1.0",
"@sentry/browser": "^9.12.0",
"@streamparser/json": "^0.0.22",
"@tailwindcss/vite": "^4.1.14",
"@types/uuid": "^10.0.0",
+1
View File
@@ -61,6 +61,7 @@ export interface ApiHandlerModel {
export interface ApiProviderInfo {
providerId: string
model: ApiHandlerModel
mode: Mode
customPrompt?: string // "compact"
autoCondenseThreshold?: number // 0-1 range
}
+7 -1
View File
@@ -104,6 +104,7 @@ interface CachePointContentBlock {
// Define provider options type based on AWS SDK patterns
interface ProviderChainOptions {
clientConfig?: { userAgentAppId?: string }
ignoreCache?: boolean
profile?: string
}
@@ -211,7 +212,12 @@ export class AwsBedrockHandler implements ApiHandler {
sessionToken?: string
}> {
// Configure provider options
const providerOptions: ProviderChainOptions = {}
const providerOptions: ProviderChainOptions = {
clientConfig: {
// set the inner sts client userAgentAppId
userAgentAppId: `cline#${ExtensionRegistryInfo.version}`,
},
}
const useProfile =
(this.options.awsAuthentication === undefined && this.options.awsUseProfile) ||
this.options.awsAuthentication === "profile"
+6 -4
View File
@@ -123,10 +123,12 @@ export class GeminiHandler implements ApiHandler {
// When ThinkingLevel is defineded, thinking budget cannot be zero
// and only level is used to control thinking behavior.
let thinkingLevel: ThinkingLevel | undefined
if (this.options.thinkingLevel === "low") {
thinkingLevel = ThinkingLevel.LOW
} else if (this.options.thinkingLevel === "high") {
if (this.options.thinkingLevel === "high") {
thinkingLevel = ThinkingLevel.HIGH
} else if (this.options.thinkingLevel === "low" || modelId.includes("gemini-3-pro")) {
// Thinking level is required for Gemini 3 Pro models.
// Set it to LOW by default if not specified but is required.
thinkingLevel = ThinkingLevel.LOW
}
// Set up base generation config
@@ -146,7 +148,7 @@ export class GeminiHandler implements ApiHandler {
// Turn on dynamic thinking:
// thinkingBudget: -1
// Turn on fixed thinking budget:
thinkingBudget: thinkingLevel ? undefined : thinkingBudget,
thinkingBudget: thinkingLevel ? undefined : thinkingBudget, // Use budget only if thinkingLevel is not set
thinkingLevel,
includeThoughts: thinkingBudget > 0 || !!thinkingLevel,
}
@@ -90,22 +90,31 @@ export function convertToOpenAIResponsesInput(messages: ClineStorageMessage[]):
for (const part of m.content) {
switch (part.type) {
case "thinking":
// Include reasoning item if it has a call_id, even if thinking is empty
// This is required because the API expects reasoning items to be paired with
// their corresponding function_calls, and will error if a function_call
// references a reasoning item that wasn't sent
if (part.call_id && part.call_id.length > 0) {
// Only include reasoning item if it has actual content (thinking text or summary)
// Empty reasoning items cause API errors: "Item 'rs_...' of type 'reasoning' was provided without its required following item"
const hasThinkingContent = part.thinking && part.thinking.trim().length > 0
const hasSummaryContent = part.summary && Array.isArray(part.summary) && part.summary.length > 0
if (part.call_id && part.call_id.length > 0 && (hasThinkingContent || hasSummaryContent)) {
// Use summary if available, otherwise use thinking text
let summary: any[] = []
if (hasSummaryContent) {
// part.summary is already in the correct format from OpenAI Responses API
summary = part.summary as any[]
} else if (hasThinkingContent) {
// Convert thinking text to summary format
summary = [
{
type: "summary_text",
text: part.thinking,
},
]
}
assistantItems.push({
id: part.call_id,
type: "reasoning",
summary: part.thinking
? [
{
type: "summary_text",
text: part.thinking,
},
]
: [],
summary,
} as ResponseReasoningItem)
}
break
@@ -126,20 +135,34 @@ export function convertToOpenAIResponsesInput(messages: ClineStorageMessage[]):
}
break
case "text":
assistantItems.push({
// Message ID goes at the message level, not in the content
// The reasoning item and message can have different IDs - they just need to be adjacent
const messageItem: any = {
type: "message",
role: "assistant",
content: [{ type: "output_text", text: part.text }],
})
}
// Set message-level id if available
if (part.call_id) {
messageItem.id = part.call_id
}
assistantItems.push(messageItem)
break
case "image":
assistantItems.push({
// Message ID goes at the message level, not in the content
const imageItem: any = {
type: "message",
role: "assistant",
content: [{ type: "output_text", text: `[image:${part.source.media_type}]` }],
})
}
// Set message-level id if available (though images typically don't have call_id)
if (part.call_id) {
imageItem.id = part.call_id
}
assistantItems.push(imageItem)
break
case "tool_use": {
// Function calls use call_id, not related to reasoning item ID
const call_id = part.call_id || part.id
if (part.call_id) {
toolUseIdToCallId.set(part.id, part.call_id)
@@ -156,22 +179,6 @@ export function convertToOpenAIResponsesInput(messages: ClineStorageMessage[]):
}
}
// Ensure every reasoning item is followed by a message or function_call
for (let i = 0; i < assistantItems.length; i++) {
const item = assistantItems[i]
if (item.type === "reasoning") {
const nextItem = assistantItems[i + 1]
if (!nextItem || (nextItem.type !== "message" && nextItem.type !== "function_call")) {
// Insert a placeholder message immediately after this reasoning item
assistantItems.splice(i + 1, 0, {
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "" }],
})
}
}
}
allItems.push(...assistantItems)
} else {
// User messages - collect all content
+33 -22
View File
@@ -16,8 +16,10 @@ interface TaskReconstructionResult {
/**
* Reconstructs task history from existing task folders
* @param isManuallyCalled Whether the function was called manually by the user through command palette
* @returns Reconstruction result or null if cancelled
*/
export async function reconstructTaskHistory(): Promise<void> {
export async function reconstructTaskHistory(isManuallyCalled = true): Promise<TaskReconstructionResult | null> {
try {
// Show confirmation dialog using HostProvider
const proceed = await HostProvider.window.showMessage({
@@ -30,37 +32,46 @@ export async function reconstructTaskHistory(): Promise<void> {
})
if (proceed?.selectedOption !== "Yes, Reconstruct") {
return
return null
}
// Show initial progress message
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: "Reconstructing task history...",
})
if (isManuallyCalled) {
// Show initial progress message
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: "Reconstructing task history...",
})
}
const result = await performTaskHistoryReconstruction()
// Show results
if (result.errors.length > 0) {
const errorMessage = `Reconstruction completed with warnings:\n- Reconstructed: ${result.reconstructedTasks} tasks\n- Skipped: ${result.skippedTasks} tasks\n- Errors: ${result.errors.length}\n\nFirst few errors:\n${result.errors.slice(0, 3).join("\n")}`
if (isManuallyCalled) {
if (result.errors.length > 0) {
const errorMessage = `Reconstruction completed with warnings:\n- Reconstructed: ${result.reconstructedTasks} tasks\n- Skipped: ${result.skippedTasks} tasks\n- Errors: ${result.errors.length}\n\nFirst few errors:\n${result.errors.slice(0, 3).join("\n")}`
HostProvider.window.showMessage({
type: ShowMessageType.WARNING,
message: errorMessage,
})
} else {
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: `Task history successfully reconstructed! Found and restored ${result.reconstructedTasks} tasks.`,
})
HostProvider.window.showMessage({
type: ShowMessageType.WARNING,
message: errorMessage,
})
} else {
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: `Task history successfully reconstructed! Found and restored ${result.reconstructedTasks} tasks.`,
})
}
}
return result
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to reconstruct task history: ${errorMessage}`,
})
if (isManuallyCalled) {
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to reconstruct task history: ${errorMessage}`,
})
}
return null
}
}
@@ -731,15 +731,17 @@ export class ContextManager {
private getPossibleDuplicateFileReads(
apiMessages: Anthropic.Messages.MessageParam[],
startFromIndex: number,
): [Map<string, [number, number, string, string][]>, Map<number, string[]>] {
// fileReadIndices: { fileName => [outerIndex, EditType, searchText, replaceText] }
): [Map<string, [number, number, string, string, number][]>, Map<number, string[]>] {
// fileReadIndices: { fileName => [outerIndex, EditType, searchText, replaceText, innerIndex] }
// messageFilePaths: { outerIndex => [fileRead1, fileRead2, ..] }
// searchText in fileReadIndices is only required for file mention file-reads since there can be more than one file in the text
// searchText will be the empty string "" in the case that it's not required, for non-file mentions
// messageFilePaths is only used for file mentions as there can be multiple files read in the same text chunk
// for all text blocks per file, has info for updating the block
const fileReadIndices = new Map<string, [number, number, string, string][]>()
// originally our messages were formatted where the innerIndex was consistently at index=1, but that is no longer the case
// which is why we now need to support both an outerIndex and innerIndex in this mapping
const fileReadIndices = new Map<string, [number, number, string, string, number][]>()
// for file mention text blocks, track all the unique files read
const messageFilePaths = new Map<number, string[]>()
@@ -757,8 +759,8 @@ export class ContextManager {
if (editType === EditType.FILE_MENTION) {
const innerMap = innerTuple[1]
const blockIndex = 1 // file mention blocks assumed to be at index 1
const blockUpdates = innerMap.get(blockIndex)
// Get the first entry from the innerMap since we only process one inner block index for FILE_MENTION
const blockUpdates = innerMap.values().next().value
// if we have updated this text previously, we want to check whether the lists of files in the metadata are the same
if (blockUpdates && blockUpdates.length > 0) {
@@ -787,36 +789,61 @@ export class ContextManager {
if (message.role === "user" && Array.isArray(message.content) && message.content.length > 0) {
const firstBlock = message.content[0]
if (firstBlock.type === "text") {
const matchTup = this.parsePotentialToolCall(firstBlock.text)
const result = this.parseToolCallWithFormat(firstBlock.text)
let foundNormalFileRead = false
if (matchTup) {
if (matchTup[0] === "read_file") {
this.handleReadFileToolCall(i, matchTup[1], fileReadIndices)
if (result) {
const [toolName, filePath, contentBlockIndex, headerText] = result
if (toolName === "read_file") {
this.handleReadFileToolCall(i, filePath, fileReadIndices, contentBlockIndex, headerText)
foundNormalFileRead = true
} else if (matchTup[0] === "replace_in_file" || matchTup[0] === "write_to_file") {
if (message.content.length > 1) {
} else if (toolName === "replace_in_file" || toolName === "write_to_file") {
// old format has the file contents in index=1 whereas the new format has it in index=0
// in either case we need to extract the correct contents
let blockText: string | undefined
if (contentBlockIndex == 0) {
blockText = firstBlock.text
} else if (contentBlockIndex == 1 && message.content.length > 1) {
const secondBlock = message.content[1]
if (secondBlock.type === "text") {
this.handlePotentialFileChangeToolCalls(i, matchTup[1], secondBlock.text, fileReadIndices)
foundNormalFileRead = true
blockText = secondBlock.text
}
}
if (blockText) {
this.handlePotentialFileChangeToolCalls(
i,
filePath,
blockText,
fileReadIndices,
contentBlockIndex,
)
foundNormalFileRead = true
}
}
}
// file mentions can happen in most other user message blocks
if (!foundNormalFileRead) {
if (message.content.length > 1) {
const secondBlock = message.content[1]
if (secondBlock.type === "text") {
// Search over indices up to 0-2 for file mentions
// Only search index N if there's at least one more element after it
for (const candidateIndex of [0, 1, 2]) {
if (message.content.length <= candidateIndex + 1) {
break
}
const block = message.content[candidateIndex]
if (block.type === "text") {
const [hasFileRead, filePaths] = this.handlePotentialFileMentionCalls(
i,
secondBlock.text,
block.text,
fileReadIndices,
thisExistingFileReads, // file reads we've already replaced in this text in the latest version of this updated text
candidateIndex,
)
if (hasFileRead) {
messageFilePaths.set(i, filePaths) // all file paths in this string
break // at most one file mentions block per outer index
}
}
}
@@ -834,16 +861,17 @@ export class ContextManager {
*/
private handlePotentialFileMentionCalls(
i: number,
secondBlockText: string,
fileReadIndices: Map<string, [number, number, string, string][]>,
blockText: string,
fileReadIndices: Map<string, [number, number, string, string, number][]>,
thisExistingFileReads: string[],
innerIndex: number,
): [boolean, string[]] {
const pattern = /<file_content path="([^"]*)">([\s\S]*?)<\/file_content>/g
let foundMatch = false
const filePaths: string[] = []
for (const match of secondBlockText.matchAll(pattern)) {
for (const match of blockText.matchAll(pattern)) {
foundMatch = true
const filePath = match[1]
@@ -859,7 +887,8 @@ export class ContextManager {
const replacementText = `<file_content path="${filePath}">${formatResponse.duplicateFileReadNotice()}</file_content>`
const indices = fileReadIndices.get(filePath) || []
indices.push([i, EditType.FILE_MENTION, entireMatch, replacementText])
// use the actual inner index where file mentions were found
indices.push([i, EditType.FILE_MENTION, entireMatch, replacementText, innerIndex])
fileReadIndices.set(filePath, indices)
}
}
@@ -868,16 +897,26 @@ export class ContextManager {
}
/**
* parses specific tool call formats, returns null if no acceptable format is found
* Parses tool call formats and returns null if no acceptable format is found
* Supports older version (content in separate block), and newer (content in same block)
* Returns [toolName, filePath, contentBlockIndex, headerText]
*/
private parsePotentialToolCall(text: string): [string, string] | null {
const match = text.match(/^\[([^\s]+) for '([^']+)'\] Result:$/)
private parseToolCallWithFormat(text: string): [string, string, number, string] | null {
const match = text.match(/^\[([^\s]+) for '([^']+)'\] Result:/)
if (!match) {
return null
}
return [match[1], match[2]]
const headerLength = match[0].length
let contentBlockIndex = 1
if (text.length > headerLength) {
// newer format: content follows header in this block (index 0)
// in the older format the content is in the following block (index 1)
contentBlockIndex = 0
}
return [match[1], match[2], contentBlockIndex, match[0]]
}
/**
@@ -886,10 +925,28 @@ export class ContextManager {
private handleReadFileToolCall(
i: number,
filePath: string,
fileReadIndices: Map<string, [number, number, string, string][]>,
fileReadIndices: Map<string, [number, number, string, string, number][]>,
contentBlockIndex: number,
headerText: string,
) {
const indices = fileReadIndices.get(filePath) || []
indices.push([i, EditType.READ_FILE_TOOL, "", formatResponse.duplicateFileReadNotice()])
if (contentBlockIndex == 1) {
// the original tool call format
indices.push([i, EditType.READ_FILE_TOOL, "", formatResponse.duplicateFileReadNotice(), contentBlockIndex])
} else {
// the new tool call format (index=0)
// in the new format the tool call output for read_file is appended to the tool call header with a newline separator
// this means we need to extract just the header and append the duplicateFileReadNotice to it with the separator
indices.push([
i,
EditType.READ_FILE_TOOL,
"",
headerText + "\n" + formatResponse.duplicateFileReadNotice(),
contentBlockIndex,
])
}
fileReadIndices.set(filePath, indices)
}
@@ -899,16 +956,17 @@ export class ContextManager {
private handlePotentialFileChangeToolCalls(
i: number,
filePath: string,
secondBlockText: string,
fileReadIndices: Map<string, [number, number, string, string][]>,
blockText: string,
fileReadIndices: Map<string, [number, number, string, string, number][]>,
contentBlockIndex: number,
) {
const pattern = /(<final_file_content path="[^"]*">)[\s\S]*?(<\/final_file_content>)/
// check if this exists in the text, it won't exist if the user rejects the file change for example
if (pattern.test(secondBlockText)) {
const replacementText = secondBlockText.replace(pattern, `$1 ${formatResponse.duplicateFileReadNotice()} $2`)
if (pattern.test(blockText)) {
const replacementText = blockText.replace(pattern, `$1 ${formatResponse.duplicateFileReadNotice()} $2`)
const indices = fileReadIndices.get(filePath) || []
indices.push([i, EditType.ALTER_FILE_TOOL, "", replacementText])
indices.push([i, EditType.ALTER_FILE_TOOL, "", replacementText, contentBlockIndex])
fileReadIndices.set(filePath, indices)
}
}
@@ -918,14 +976,14 @@ export class ContextManager {
* returns the outer index of messages we alter, to count number of changes
*/
private applyFileReadContextHistoryUpdates(
fileReadIndices: Map<string, [number, number, string, string][]>,
fileReadIndices: Map<string, [number, number, string, string, number][]>,
messageFilePaths: Map<number, string[]>,
apiMessages: Anthropic.Messages.MessageParam[],
timestamp: number,
): [boolean, Set<number>] {
let didUpdate = false
const updatedMessageIndices = new Set<number>() // track which messages we update on this round
const fileMentionUpdates = new Map<number, [string, string[]]>()
const fileMentionUpdates = new Map<number, [string, string[], number]>() // [baseText, prevFilesReplaced, innerIndex]
for (const [filePath, indices] of fileReadIndices.entries()) {
// Only process if there are multiple reads of the same file, else we will want to keep the latest read of the file
@@ -936,6 +994,7 @@ export class ContextManager {
const messageType = indices[i][1] // EditType value
const searchText = indices[i][2] // search text (for file mentions, else empty string)
const messageString = indices[i][3] // what we will replace the string with
const innerIndex = indices[i][4] // inner block index where we are making the change
didUpdate = true
updatedMessageIndices.add(messageIndex)
@@ -950,7 +1009,7 @@ export class ContextManager {
const innerTuple = this.contextHistoryUpdates.get(messageIndex)
if (innerTuple) {
const blockUpdates = innerTuple[1].get(1) // assumed index=1 for file mention filereads
const blockUpdates = innerTuple[1].get(innerIndex)
if (blockUpdates && blockUpdates.length > 0) {
baseText = blockUpdates[blockUpdates.length - 1][2][0] // index 0 of MessageContent
prevFilesReplaced = blockUpdates[blockUpdates.length - 1][3][0] // previously overwritten file reads in this text
@@ -959,20 +1018,20 @@ export class ContextManager {
// can assume that this content will exist, otherwise it would not have been in fileReadIndices
const messageContent = apiMessages[messageIndex]?.content
if (!baseText && Array.isArray(messageContent) && messageContent.length > 1) {
const contentBlock = messageContent[1] // assume index=1 for all text to replace for file mention filereads
if (!baseText && Array.isArray(messageContent) && messageContent.length > innerIndex) {
const contentBlock = messageContent[innerIndex]
if (contentBlock.type === "text") {
baseText = contentBlock.text
}
}
// prevFilesReplaced keeps track of the previous file reads we've replace in this string, empty array if none
fileMentionUpdates.set(messageIndex, [baseText, prevFilesReplaced])
fileMentionUpdates.set(messageIndex, [baseText, prevFilesReplaced, innerIndex])
}
// Replace searchText with messageString for all file reads we need to replace in this text
if (searchText) {
const currentTuple = fileMentionUpdates.get(messageIndex) || ["", []]
const currentTuple = fileMentionUpdates.get(messageIndex) || ["", [], 0]
if (currentTuple[0]) {
// safety check
// replace this text chunk
@@ -982,7 +1041,7 @@ export class ContextManager {
const updatedFileReads = currentTuple[1]
updatedFileReads.push(filePath)
fileMentionUpdates.set(messageIndex, [updatedText, updatedFileReads])
fileMentionUpdates.set(messageIndex, [updatedText, updatedFileReads, currentTuple[2]])
}
}
} else {
@@ -996,8 +1055,7 @@ export class ContextManager {
innerMap = innerTuple[1]
}
// block index for file reads from read_file, write_to_file, replace_in_file tools is 1
const blockIndex = 1
const blockIndex = innerIndex
const updates = innerMap.get(blockIndex) || []
@@ -1012,7 +1070,7 @@ export class ContextManager {
// apply file mention updates to contextHistoryUpdates
// in fileMentionUpdates, filePathsUpdated includes all the file paths which are updated in the latest version of this altered text
for (const [messageIndex, [updatedText, filePathsUpdated]] of fileMentionUpdates.entries()) {
for (const [messageIndex, [updatedText, filePathsUpdated, blockIndex]] of fileMentionUpdates.entries()) {
const innerTuple = this.contextHistoryUpdates.get(messageIndex)
let innerMap: Map<number, ContextUpdate[]>
@@ -1023,14 +1081,12 @@ export class ContextManager {
innerMap = innerTuple[1]
}
const blockIndex = 1 // we only consider the block index of 1 for file mentions
const updates = innerMap.get(blockIndex) || []
// filePathsUpdated includes changes done previously to this timestamp, and right now
if (messageFilePaths.has(messageIndex)) {
const allFileReads = messageFilePaths.get(messageIndex)
if (allFileReads) {
// safety check
// we gather all the file reads possible in this text from messageFilePaths
// filePathsUpdated from fileMentionUpdates stores all the files reads we have replaced now & previously
updates.push([timestamp, "text", [updatedText], [filePathsUpdated, allFileReads]])
@@ -197,4 +197,371 @@ describe("ContextManager", () => {
expect((content[0] as Anthropic.Messages.TextBlockParam).text).to.equal("Additional user text")
})
})
describe("applyFileReadContextHistoryUpdates", () => {
let contextManager: ContextManager
beforeEach(() => {
contextManager = new ContextManager()
})
it("should return early when fileReadIndices is empty", () => {
const fileReadIndices = new Map<string, [number, number, string, string, number][]>()
const messageFilePaths = new Map<number, string[]>()
const apiMessages: Anthropic.Messages.MessageParam[] = []
const timestamp = Date.now()
const [didUpdate, updatedIndices] = (contextManager as any).applyFileReadContextHistoryUpdates(
fileReadIndices,
messageFilePaths,
apiMessages,
timestamp,
)
expect(didUpdate).to.be.false
expect(updatedIndices.size).to.equal(0)
})
it("should not update when file has only one occurrence", () => {
const fileReadIndices = new Map<string, [number, number, string, string, number][]>()
fileReadIndices.set("test.ts", [[3, 2, "", "replacement text", 0]])
const messageFilePaths = new Map<number, string[]>()
const apiMessages: Anthropic.Messages.MessageParam[] = []
const timestamp = Date.now()
const [didUpdate, updatedIndices] = (contextManager as any).applyFileReadContextHistoryUpdates(
fileReadIndices,
messageFilePaths,
apiMessages,
timestamp,
)
expect(didUpdate).to.be.false
expect(updatedIndices.size).to.equal(0)
})
it("should update all but the last occurrence of duplicate file reads", () => {
const fileReadIndices = new Map<string, [number, number, string, string, number][]>()
// messageIndex, messageType (READ_FILE_TOOL=2), searchText, replaceText, innerIndex
fileReadIndices.set("test.ts", [
[3, 2, "", "[read_file for 'test.ts'] Result:\nDuplicate file read...", 0],
[5, 2, "", "[read_file for 'test.ts'] Result:\nDuplicate file read...", 0],
[7, 2, "", "[read_file for 'test.ts'] Result:\nKeep this one", 0],
])
const messageFilePaths = new Map<number, string[]>()
const apiMessages: Anthropic.Messages.MessageParam[] = []
const timestamp = Date.now()
const [didUpdate, updatedIndices] = (contextManager as any).applyFileReadContextHistoryUpdates(
fileReadIndices,
messageFilePaths,
apiMessages,
timestamp,
)
expect(didUpdate).to.be.true
expect(updatedIndices.size).to.equal(2)
expect(updatedIndices.has(3)).to.be.true
expect(updatedIndices.has(5)).to.be.true
expect(updatedIndices.has(7)).to.be.false // Last occurrence should not be updated
})
it("should handle FILE_MENTION type correctly with multiple files in same text", () => {
const fileReadIndices = new Map<string, [number, number, string, string, number][]>()
// FILE_MENTION = 4
fileReadIndices.set("file1.ts", [
[
3,
4,
'<file_content path="file1.ts">content1</file_content>',
'<file_content path="file1.ts">Duplicate file read...</file_content>',
0,
],
[
5,
4,
'<file_content path="file1.ts">content2</file_content>',
'<file_content path="file1.ts">Keep this</file_content>',
0,
],
])
fileReadIndices.set("file2.ts", [
[
3,
4,
'<file_content path="file2.ts">content3</file_content>',
'<file_content path="file2.ts">Duplicate file read...</file_content>',
0,
],
[
6,
4,
'<file_content path="file2.ts">content4</file_content>',
'<file_content path="file2.ts">Keep this</file_content>',
0,
],
])
const messageFilePaths = new Map<number, string[]>()
messageFilePaths.set(3, ["file1.ts", "file2.ts"])
const apiMessages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Initial" },
{ role: "assistant", content: "Response" },
{ role: "user", content: "Message" },
{
role: "user",
content: [
{
type: "text",
text: '<file_content path="file1.ts">content1</file_content>\n<file_content path="file2.ts">content3</file_content>',
},
],
},
]
const timestamp = Date.now()
const [didUpdate, updatedIndices] = (contextManager as any).applyFileReadContextHistoryUpdates(
fileReadIndices,
messageFilePaths,
apiMessages,
timestamp,
)
expect(didUpdate).to.be.true
expect(updatedIndices.size).to.equal(1)
expect(updatedIndices.has(3)).to.be.true
})
it("should handle ALTER_FILE_TOOL type correctly", () => {
const fileReadIndices = new Map<string, [number, number, string, string, number][]>()
// ALTER_FILE_TOOL = 3
fileReadIndices.set("test.ts", [
[3, 3, "", "replacement text 1", 0],
[5, 3, "", "replacement text 2", 0],
])
const messageFilePaths = new Map<number, string[]>()
const apiMessages: Anthropic.Messages.MessageParam[] = []
const timestamp = Date.now()
const [didUpdate, updatedIndices] = (contextManager as any).applyFileReadContextHistoryUpdates(
fileReadIndices,
messageFilePaths,
apiMessages,
timestamp,
)
expect(didUpdate).to.be.true
expect(updatedIndices.size).to.equal(1)
expect(updatedIndices.has(3)).to.be.true
expect(updatedIndices.has(5)).to.be.false
})
it("should handle native tool calling format (tool_result blocks)", () => {
const fileReadIndices = new Map<string, [number, number, string, string, number][]>()
fileReadIndices.set("test.ts", [
[3, 2, "", "[read_file for 'test.ts'] Result:\nDuplicate...", 0],
[5, 2, "", "[read_file for 'test.ts'] Result:\nKeep this", 0],
])
const messageFilePaths = new Map<number, string[]>()
const apiMessages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Initial" },
{ role: "assistant", content: "Response" },
{ role: "user", content: "Message" },
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool_123",
content: [{ type: "text", text: "[read_file for 'test.ts'] Result:\noriginal content" }],
},
],
},
]
const timestamp = Date.now()
const [didUpdate, updatedIndices] = (contextManager as any).applyFileReadContextHistoryUpdates(
fileReadIndices,
messageFilePaths,
apiMessages,
timestamp,
)
expect(didUpdate).to.be.true
expect(updatedIndices.size).to.equal(1)
expect(updatedIndices.has(3)).to.be.true
})
})
describe("helper methods for applyFileReadContextHistoryUpdates", () => {
let contextManager: ContextManager
beforeEach(() => {
contextManager = new ContextManager()
})
it("getBaseTextForFileMention should get text from existing updates", () => {
const messageIndex = 3
const innerIndex = 0
const apiMessages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Initial" },
{ role: "assistant", content: "Response" },
{ role: "user", content: "Message" },
{ role: "user", content: [{ type: "text", text: "original text" }] },
]
// Manually set up context history updates
const timestamp = Date.now()
const innerMap = new Map<number, any[]>()
innerMap.set(innerIndex, [[timestamp, "text", ["updated text"], []]])
;(contextManager as any).contextHistoryUpdates.set(messageIndex, [4, innerMap])
const result = (contextManager as any).getBaseTextForFileMention(messageIndex, innerIndex, apiMessages)
expect(result).to.equal("updated text")
})
it("getBaseTextForFileMention should fallback to original message content", () => {
const messageIndex = 3
const innerIndex = 0
const apiMessages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Initial" },
{ role: "assistant", content: "Response" },
{ role: "user", content: "Message" },
{ role: "user", content: [{ type: "text", text: "original text" }] },
]
const result = (contextManager as any).getBaseTextForFileMention(messageIndex, innerIndex, apiMessages)
expect(result).to.equal("original text")
})
it("getBaseTextForFileMention should handle tool_result blocks", () => {
const messageIndex = 3
const innerIndex = 0
const apiMessages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Initial" },
{ role: "assistant", content: "Response" },
{ role: "user", content: "Message" },
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool_123",
content: [{ type: "text", text: "tool result text" }],
},
],
},
]
const result = (contextManager as any).getBaseTextForFileMention(messageIndex, innerIndex, apiMessages)
expect(result).to.equal("tool result text")
})
it("getPreviouslyReplacedFiles should return empty array when no updates exist", () => {
const messageIndex = 3
const innerIndex = 0
const result = (contextManager as any).getPreviouslyReplacedFiles(messageIndex, innerIndex)
expect(result).to.deep.equal([])
})
it("getPreviouslyReplacedFiles should return previously replaced files", () => {
const messageIndex = 3
const innerIndex = 0
const timestamp = Date.now()
// Manually set up context history updates with metadata
const innerMap = new Map<number, any[]>()
innerMap.set(innerIndex, [
[
timestamp,
"text",
["updated text"],
[
["file1.ts", "file2.ts"],
["file1.ts", "file2.ts", "file3.ts"],
],
],
])
;(contextManager as any).contextHistoryUpdates.set(messageIndex, [4, innerMap])
const result = (contextManager as any).getPreviouslyReplacedFiles(messageIndex, innerIndex)
expect(result).to.deep.equal(["file1.ts", "file2.ts"])
})
it("addContextUpdate should create new entry when none exists", () => {
const messageIndex = 3
const messageType = 2 // READ_FILE_TOOL
const innerIndex = 0
const timestamp = Date.now()
const messageString = "replacement text"
;(contextManager as any).addContextUpdate(messageIndex, messageType, innerIndex, timestamp, messageString)
const contextHistory = (contextManager as any).contextHistoryUpdates
expect(contextHistory.has(messageIndex)).to.be.true
const [storedType, innerMap] = contextHistory.get(messageIndex)
expect(storedType).to.equal(messageType)
expect(innerMap.has(innerIndex)).to.be.true
const updates = innerMap.get(innerIndex)
expect(updates).to.have.lengthOf(1)
expect(updates[0]).to.deep.equal([timestamp, "text", [messageString], []])
})
it("addContextUpdate should append to existing updates", () => {
const messageIndex = 3
const messageType = 2
const innerIndex = 0
const timestamp1 = Date.now()
const timestamp2 = timestamp1 + 1000
;(contextManager as any).addContextUpdate(messageIndex, messageType, innerIndex, timestamp1, "first update")
;(contextManager as any).addContextUpdate(messageIndex, messageType, innerIndex, timestamp2, "second update")
const contextHistory = (contextManager as any).contextHistoryUpdates
const [, innerMap] = contextHistory.get(messageIndex)
const updates = innerMap.get(innerIndex)
expect(updates).to.have.lengthOf(2)
expect(updates[1]).to.deep.equal([timestamp2, "text", ["second update"], []])
})
it("getOrCreateInnerMap should return existing map", () => {
const messageIndex = 3
const messageType = 2
const innerMap = new Map<number, any[]>()
;(contextManager as any).contextHistoryUpdates.set(messageIndex, [messageType, innerMap])
const result = (contextManager as any).getOrCreateInnerMap(messageIndex, messageType)
expect(result).to.equal(innerMap)
})
it("getOrCreateInnerMap should create new map when none exists", () => {
const messageIndex = 3
const messageType = 2
const result = (contextManager as any).getOrCreateInnerMap(messageIndex, messageType)
expect(result).to.be.instanceOf(Map)
const contextHistory = (contextManager as any).contextHistoryUpdates
expect(contextHistory.has(messageIndex)).to.be.true
const [storedType, storedMap] = contextHistory.get(messageIndex)
expect(storedType).to.equal(messageType)
expect(storedMap).to.equal(result)
})
})
})
@@ -74,11 +74,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
controller.stateManager.setGlobalState("enableCheckpointsSetting", request.enableCheckpointsSetting)
}
// Update MCP marketplace setting
if (request.mcpMarketplaceEnabled !== undefined) {
controller.stateManager.setGlobalState("mcpMarketplaceEnabled", request.mcpMarketplaceEnabled)
}
// Update MCP responses collapsed setting
if (request.mcpResponsesCollapsed !== undefined) {
controller.stateManager.setGlobalState("mcpResponsesCollapsed", request.mcpResponsesCollapsed)
@@ -116,6 +116,7 @@ export const mockProviderInfo = {
supportsPromptCache: false,
},
},
mode: "act" as const,
}
const makeMockProviderInfo = (modelId: string, providerId: string = "test") => ({
@@ -15,6 +15,7 @@ const mockProviderInfos: { name: string; providerInfo: ApiProviderInfo; expected
providerInfo: {
providerId: "openai",
model: { id: "gpt-5", info: {} as any },
mode: "act" as const,
},
expectedFamily: ModelFamily.GPT_5,
},
@@ -23,6 +24,7 @@ const mockProviderInfos: { name: string; providerInfo: ApiProviderInfo; expected
providerInfo: {
providerId: "anthropic",
model: { id: "claude-3.5-sonnet-20241022", info: {} as any },
mode: "act" as const,
},
expectedFamily: ModelFamily.NEXT_GEN,
},
@@ -31,6 +33,7 @@ const mockProviderInfos: { name: string; providerInfo: ApiProviderInfo; expected
providerInfo: {
providerId: "ollama",
model: { id: "llama-3.2-1b", info: {} as any },
mode: "act" as const,
customPrompt: "compact",
},
expectedFamily: ModelFamily.XS,
@@ -40,6 +43,7 @@ const mockProviderInfos: { name: string; providerInfo: ApiProviderInfo; expected
providerInfo: {
providerId: "openai",
model: { id: "gpt-3.5-turbo", info: {} as any },
mode: "act" as const,
},
expectedFamily: ModelFamily.GENERIC,
},
@@ -30,7 +30,9 @@ EOF
Where [YOUR_PATCH] is the actual content of your patch, specified in the following V4A diff format.
*** [ACTION] File: [path/to/file] -> ACTION can be one of Add, Update, or Delete.
For each snippet of code that needs to be changed, repeat the following:
In a Add File section, every line of the new file (including blank/empty lines) MUST start with a \`+\` prefix. Do not include any unprefixed lines inside an Add section
In a Update/Delete section, repeat the following for each snippet of code that needs to be changed:
[context_before] -> See below for further instructions on context.
- [old_code] -> Precede the old code with a minus sign.
+ [new_code] -> Precede the new, replacement code with a plus sign.
+421 -1
View File
@@ -1,11 +1,20 @@
import { afterEach, beforeEach, describe, it } from "mocha"
import "should"
import { HistoryItem } from "@shared/HistoryItem"
import * as fsUtils from "@utils/fs"
import fs from "fs/promises"
import os from "os"
import path from "path"
import sinon from "sinon"
import { getWorkspaceHooksDirs } from "../disk"
import { HostProvider } from "@/hosts/host-provider"
import { setVscodeHostProviderMock } from "@/test/host-provider-test-utils"
import {
ensureStateDirectoryExists,
getTaskHistoryStateFilePath,
getWorkspaceHooksDirs,
readTaskHistoryFromState,
writeTaskHistoryToState,
} from "../disk"
import { StateManager } from "../StateManager"
describe("disk - hooks functionality", () => {
@@ -191,3 +200,414 @@ describe("disk - hooks functionality", () => {
})
})
})
describe("disk - atomic writes", () => {
let sandbox: sinon.SinonSandbox
let testGlobalStorageDir: string
// Setup HostProvider for tests with real temp directory
before(async () => {
// Create a real temp directory for the tests
testGlobalStorageDir = path.join(os.tmpdir(), `cline-test-storage-${Date.now()}-${Math.random().toString(36).slice(2)}`)
await fs.mkdir(testGlobalStorageDir, { recursive: true })
// Initialize HostProvider with the real temp directory
setVscodeHostProviderMock({
globalStorageFsPath: testGlobalStorageDir,
})
})
after(async () => {
HostProvider.reset()
// Clean up temp directory
try {
await fs.rm(testGlobalStorageDir, { recursive: true, force: true })
} catch {
// Ignore cleanup errors
}
})
/**
* Helper to create test history items
*/
const createTestHistoryItem = (id: string, task: string): HistoryItem => {
return {
id,
ts: Date.now(),
task,
tokensIn: 100,
tokensOut: 200,
totalCost: 0.01,
}
}
/**
* Helper to check for orphaned temp files
*/
const getTempFileCount = async (): Promise<number> => {
const stateDir = await ensureStateDirectoryExists()
const files = await fs.readdir(stateDir)
return files.filter((f) => f.startsWith("taskHistory.json.tmp.")).length
}
beforeEach(async () => {
sandbox = sinon.createSandbox()
})
afterEach(async () => {
sandbox.restore()
})
describe("writeTaskHistoryToState and readTaskHistoryFromState", () => {
it("should write and read task history correctly", async () => {
const items = [createTestHistoryItem("test-1", "Build a todo app"), createTestHistoryItem("test-2", "Fix a bug")]
await writeTaskHistoryToState(items)
const result = await readTaskHistoryFromState()
result.should.be.an.Array()
result.should.have.length(2)
result[0].id.should.equal("test-1")
result[0].task.should.equal("Build a todo app")
result[1].id.should.equal("test-2")
result[1].task.should.equal("Fix a bug")
})
it("should write valid JSON that can be parsed", async () => {
const items = [
createTestHistoryItem("test-json-1", "Test with special chars: 你好 🎉"),
createTestHistoryItem("test-json-2", "Test with quotes: \"hello\" and 'world'"),
]
await writeTaskHistoryToState(items)
// Read the raw file and verify it's valid JSON
const filePath = await getTaskHistoryStateFilePath()
const rawContent = await fs.readFile(filePath, "utf8")
const parsed = JSON.parse(rawContent) // Should not throw
parsed.should.be.an.Array()
parsed.should.have.length(2)
})
it("should not leave temp files after successful write", async () => {
const items = [createTestHistoryItem("cleanup-test", "Test cleanup")]
const tempCountBefore = await getTempFileCount()
await writeTaskHistoryToState(items)
const tempCountAfter = await getTempFileCount()
tempCountAfter.should.equal(tempCountBefore)
})
it("should handle empty array writes", async () => {
await writeTaskHistoryToState([])
const result = await readTaskHistoryFromState()
result.should.be.an.Array()
result.should.have.length(0)
})
it("should handle large task history arrays", async function () {
this.timeout(30000) // 30 second timeout for large file operations
// Create large task content by repeating a pattern (each task ~50 KB)
const baseContent = "X".repeat(50 * 1024) // 50 KB of X's per task
// Create 1,000 history items (resulting in ~50 MB file)
const items = Array.from({ length: 1000 }, (_, i) =>
createTestHistoryItem(`stress-test-${i}`, `Task ${i}: ${baseContent}`),
)
await writeTaskHistoryToState(items)
const result = await readTaskHistoryFromState()
// Verify array length and data integrity
result.should.have.length(1000)
result[0].id.should.equal("stress-test-0")
result[0].task.should.startWith("Task 0: X")
result[500].id.should.equal("stress-test-500")
result[999].id.should.equal("stress-test-999")
})
it("should handle concurrent writes without corruption", async function () {
this.timeout(30000)
// Perform many concurrent writes to stress test atomicity
const writePromises = Array.from({ length: 100 }, (_, i) => {
const items = [createTestHistoryItem(`concurrent-${i}`, `Task ${i}`)]
return writeTaskHistoryToState(items).catch((error) => {
// On Windows, concurrent renames may fail with EPERM - this is expected
if (process.platform === "win32" && error.code === "EPERM") {
return // Expected Windows behavior
}
throw error // Unexpected error, rethrow
})
})
// Wait for all writes to complete (some may fail on Windows with EPERM)
await Promise.all(writePromises)
// Final read should return valid JSON (not corrupted)
const result = await readTaskHistoryFromState()
result.should.be.an.Array()
// Should have data from one of the concurrent writes that succeeded
result.length.should.be.greaterThan(0)
// Verify the data is valid (not corrupted)
result[0].should.have.property("id")
result[0].should.have.property("task")
})
it("should preserve data integrity with special characters", async () => {
const items = [
createTestHistoryItem("special-1", "Test\nwith\nnewlines"),
createTestHistoryItem("special-2", "Test\twith\ttabs"),
createTestHistoryItem("special-3", "Test with unicode: 日本語 中文 한국어"),
createTestHistoryItem("special-4", "Test with emojis: 😀🎉🚀"),
]
await writeTaskHistoryToState(items)
const result = await readTaskHistoryFromState()
result.should.have.length(4)
result[0].task.should.equal("Test\nwith\nnewlines")
result[1].task.should.equal("Test\twith\ttabs")
result[2].task.should.equal("Test with unicode: 日本語 中文 한국어")
result[3].task.should.equal("Test with emojis: 😀🎉🚀")
})
it("should overwrite existing task history", async () => {
// Write initial data
const initialItems = [createTestHistoryItem("initial-1", "Initial task")]
await writeTaskHistoryToState(initialItems)
// Verify initial data
let result = await readTaskHistoryFromState()
result.should.have.length(1)
result[0].id.should.equal("initial-1")
// Overwrite with new data
const newItems = [createTestHistoryItem("new-1", "New task 1"), createTestHistoryItem("new-2", "New task 2")]
await writeTaskHistoryToState(newItems)
// Verify new data replaced old data
result = await readTaskHistoryFromState()
result.should.have.length(2)
result[0].id.should.equal("new-1")
result[1].id.should.equal("new-2")
})
it("should handle rapid successive writes", async function () {
this.timeout(5000)
// Perform rapid successive writes (not concurrent)
for (let i = 0; i < 20; i++) {
const items = [createTestHistoryItem(`rapid-${i}`, `Task ${i}`)]
await writeTaskHistoryToState(items)
}
// Should have no temp files left
const tempCount = await getTempFileCount()
tempCount.should.equal(0)
// Final read should be valid
const result = await readTaskHistoryFromState()
result.should.be.an.Array()
result.should.have.length(1)
result[0].id.should.equal("rapid-19")
})
it("should preserve all HistoryItem fields", async () => {
const items = [
{
id: "full-test",
ts: 1234567890,
task: "Complete task",
tokensIn: 500,
tokensOut: 1000,
totalCost: 0.15,
cacheWrites: 100,
cacheReads: 200,
},
]
await writeTaskHistoryToState(items)
const result = await readTaskHistoryFromState()
result.should.have.length(1)
result[0].id.should.equal("full-test")
result[0].ts.should.equal(1234567890)
result[0].task.should.equal("Complete task")
result[0].tokensIn.should.equal(500)
result[0].tokensOut.should.equal(1000)
result[0].totalCost.should.equal(0.15)
result[0].cacheWrites!.should.equal(100)
result[0].cacheReads!.should.equal(200)
})
})
describe("atomic write failure scenarios", () => {
it("should leave original file intact if temp file write fails", async () => {
// Write initial data
const initialItems = [createTestHistoryItem("original-1", "Original task")]
await writeTaskHistoryToState(initialItems)
// Verify initial data exists
let result = await readTaskHistoryFromState()
result.should.have.length(1)
result[0].id.should.equal("original-1")
// Stub fs.writeFile to fail during temp file creation
const writeFileStub = sandbox.stub(fs, "writeFile")
writeFileStub.rejects(new Error("Simulated write failure"))
// Attempt to write new data (should fail)
const newItems = [createTestHistoryItem("new-1", "New task")]
try {
await writeTaskHistoryToState(newItems)
throw new Error("Should have thrown")
} catch (error: any) {
error.message.should.equal("Simulated write failure")
}
// Original file should still be intact
result = await readTaskHistoryFromState()
result.should.have.length(1)
result[0].id.should.equal("original-1")
// No temp files should remain
const tempCount = await getTempFileCount()
tempCount.should.equal(0)
})
it("should leave original file intact if rename fails", async () => {
// Write initial data
const initialItems = [createTestHistoryItem("original-2", "Original task 2")]
await writeTaskHistoryToState(initialItems)
// Verify initial data exists
let result = await readTaskHistoryFromState()
result.should.have.length(1)
result[0].id.should.equal("original-2")
// Stub fs.rename to fail
const renameStub = sandbox.stub(fs, "rename")
renameStub.rejects(new Error("Simulated rename failure"))
// Attempt to write new data (should fail)
const newItems = [createTestHistoryItem("new-2", "New task 2")]
try {
await writeTaskHistoryToState(newItems)
throw new Error("Should have thrown")
} catch (error: any) {
error.message.should.equal("Simulated rename failure")
}
// Original file should still be intact
result = await readTaskHistoryFromState()
result.should.have.length(1)
result[0].id.should.equal("original-2")
// Temp file cleanup may or may not succeed, but original file is safe
// (The atomicWriteFile function attempts cleanup but doesn't throw if it fails)
})
it("should ignore temp files during read operations", async () => {
// Write valid data
const items = [createTestHistoryItem("valid-1", "Valid task")]
await writeTaskHistoryToState(items)
// Create a corrupt temp file manually
const stateDir = await ensureStateDirectoryExists()
const corruptTempPath = path.join(stateDir, "taskHistory.json.tmp.12345.corrupt")
await fs.writeFile(corruptTempPath, "INVALID JSON{", "utf8")
// Read should succeed and ignore the temp file
const result = await readTaskHistoryFromState()
result.should.have.length(1)
result[0].id.should.equal("valid-1")
// Cleanup temp file
await fs.unlink(corruptTempPath)
})
it("should handle concurrent read during write without corruption", async () => {
// Write initial data
const initialItems = [createTestHistoryItem("concurrent-read-1", "Initial task")]
await writeTaskHistoryToState(initialItems)
// Create a slow rename by stubbing fs.rename to delay
// This simulates the critical window where temp file is written but rename hasn't occurred
let renameResolve: () => void
const renamePromise = new Promise<void>((resolve) => {
renameResolve = resolve
})
const originalRename = fs.rename
const renameStub = sandbox.stub(fs, "rename")
renameStub.callsFake(async (oldPath, newPath) => {
// Delay the rename operation
await renamePromise // Wait for our signal
return originalRename(oldPath, newPath)
})
// Start a write operation (rename will be delayed)
const newItems = [createTestHistoryItem("concurrent-read-2", "New task")]
const writeOperation = writeTaskHistoryToState(newItems)
// Give temp file time to be written, but before rename completes
await new Promise((resolve) => setTimeout(resolve, 50))
// Perform a read during the critical window (temp file exists, but rename hasn't happened)
const readResult = await readTaskHistoryFromState()
// Should get old data (since rename hasn't completed yet)
readResult.should.have.length(1)
readResult[0].id.should.equal("concurrent-read-1")
// Now allow rename to complete
renameResolve!()
await writeOperation
// Subsequent read should get new data
const finalResult = await readTaskHistoryFromState()
finalResult.should.have.length(1)
finalResult[0].id.should.equal("concurrent-read-2")
})
it("should handle partial temp file from interrupted process", async () => {
// Write initial valid data
const initialItems = [createTestHistoryItem("partial-test-1", "Initial task")]
await writeTaskHistoryToState(initialItems)
// Simulate an interrupted write by creating a partial temp file
const stateDir = await ensureStateDirectoryExists()
const partialTempPath = path.join(stateDir, "taskHistory.json.tmp.99999.partial")
// Write only part of a valid JSON array
await fs.writeFile(partialTempPath, '[{"id":"partial","ts":123456789', "utf8")
// Read should succeed with original data
const result = await readTaskHistoryFromState()
result.should.have.length(1)
result[0].id.should.equal("partial-test-1")
// Write new data should succeed and clean up
const newItems = [createTestHistoryItem("partial-test-2", "New task")]
await writeTaskHistoryToState(newItems)
// Verify new data
const finalResult = await readTaskHistoryFromState()
finalResult.should.have.length(1)
finalResult[0].id.should.equal("partial-test-2")
// Cleanup our partial temp file if it still exists
try {
await fs.unlink(partialTempPath)
} catch {
// May already be cleaned up
}
})
})
})
+49 -9
View File
@@ -11,9 +11,34 @@ import os from "os"
import * as path from "path"
import { HostProvider } from "@/hosts/host-provider"
import { ExtensionRegistryInfo } from "@/registry"
import { telemetryService } from "@/services/telemetry"
import { McpMarketplaceCatalog } from "@/shared/mcp"
import { reconstructTaskHistory } from "../commands/reconstructTaskHistory"
import { StateManager } from "./StateManager"
/**
* Atomically write data to a file using temp file + rename pattern.
* This prevents readers from seeing partial/incomplete data by writing to a temporary
* file first, then renaming it to the target location. The rename operation is atomic
* in most cases on modern systems, though behavior may vary across platforms and filesystems.
*
* @param filePath - The target file path
* @param data - The data to write
*/
async function atomicWriteFile(filePath: string, data: string): Promise<void> {
const tmpPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).substring(7)}.json`
try {
// Write to temporary file first
await fs.writeFile(tmpPath, data, "utf8")
// Rename temp file to target (atomic in most cases)
await fs.rename(tmpPath, filePath)
} catch (error) {
// Clean up temp file if it exists
fs.unlink(tmpPath).catch(() => {})
throw error
}
}
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
contextHistory: "context_history.json",
@@ -136,7 +161,7 @@ export async function getSavedApiConversationHistory(taskId: string): Promise<An
export async function saveApiConversationHistory(taskId: string, apiConversationHistory: Anthropic.MessageParam[]) {
try {
const filePath = path.join(await ensureTaskDirectoryExists(taskId), GlobalFileNames.apiConversationHistory)
await fs.writeFile(filePath, JSON.stringify(apiConversationHistory))
await atomicWriteFile(filePath, JSON.stringify(apiConversationHistory))
} catch (error) {
// in the off chance this fails, we don't want to stop the task
console.error("Failed to save API conversation history:", error)
@@ -163,7 +188,7 @@ export async function saveClineMessages(taskId: string, uiMessages: ClineMessage
try {
const taskDir = await ensureTaskDirectoryExists(taskId)
const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
await fs.writeFile(filePath, JSON.stringify(uiMessages))
await atomicWriteFile(filePath, JSON.stringify(uiMessages))
} catch (error) {
console.error("Failed to save ui messages:", error)
}
@@ -269,17 +294,33 @@ export async function taskHistoryStateFileExists(): Promise<boolean> {
return fileExistsAtPath(filePath)
}
export async function readTaskHistoryFromState(): Promise<HistoryItem[]> {
export async function readTaskHistoryFromState(attemptedReconstruction = false): Promise<HistoryItem[]> {
try {
const filePath = await getTaskHistoryStateFilePath()
if (await fileExistsAtPath(filePath)) {
const contents = await fs.readFile(filePath, "utf8")
return JSON.parse(contents)
try {
const contents = await fs.readFile(filePath, "utf8")
try {
return JSON.parse(contents)
} catch (parseError) {
// Only reconstruction on parse errors
if (attemptedReconstruction) {
telemetryService.captureExtensionStorageError(parseError, "attemptedReconstruction failed")
throw new Error("Failed to parse task history JSON after reconstruction attempt...")
}
telemetryService.captureExtensionStorageError(parseError, "readTaskHistoryFromState parse error")
await reconstructTaskHistory(false)
return await readTaskHistoryFromState(true)
}
} catch (readError) {
// Handle file system errors separately
telemetryService.captureExtensionStorageError(readError, "readTaskHistoryFromState readFile error")
throw readError
}
}
return []
} catch (error) {
console.error("[Disk] Failed to read task history:", error)
telemetryService.captureExtensionStorageError(error, "readTaskHistoryFromState")
throw error
}
}
@@ -287,8 +328,7 @@ export async function readTaskHistoryFromState(): Promise<HistoryItem[]> {
export async function writeTaskHistoryToState(items: HistoryItem[]): Promise<void> {
try {
const filePath = await getTaskHistoryStateFilePath()
// Always create the file; if items is empty, write [] to ensure presence on first startup
await fs.writeFile(filePath, JSON.stringify(items))
await atomicWriteFile(filePath, JSON.stringify(items))
} catch (error) {
console.error("[Disk] Failed to write task history:", error)
throw error
+12
View File
@@ -125,6 +125,18 @@ export function transformRemoteConfigToStateShape(remoteConfig: RemoteConfig): P
providers.push("cline")
}
// Map LiteLLM provider settings
const liteLlmSettings = remoteConfig.providerSettings?.LiteLLM
if (liteLlmSettings) {
transformed.planModeApiProvider = "litellm"
transformed.actModeApiProvider = "litellm"
providers.push("litellm")
if (liteLlmSettings.baseUrl !== undefined) {
transformed.liteLlmBaseUrl = liteLlmSettings.baseUrl
}
}
// This line needs to stay here, it is order dependent on the above code checking the configured providers
if (providers.length > 0) {
transformed.remoteConfiguredProviders = providers
+4
View File
@@ -311,6 +311,10 @@ class ReasoningHandler {
return null
}
if (!this.pendingReasoning.summary.length && !this.pendingReasoning.content) {
return null
}
// Ensure signature is set if it's hidden in the summary / reasoning details
// to ensure it's always accessible at the top level by each provider.
if (!this.pendingReasoning.signature && this.pendingReasoning.summary.length) {
+1
View File
@@ -302,6 +302,7 @@ export class ToolExecutor {
ClineDefaultTool.FILE_NEW,
ClineDefaultTool.FILE_EDIT,
ClineDefaultTool.NEW_RULE,
ClineDefaultTool.APPLY_PATCH,
]
/**
+104 -82
View File
@@ -67,7 +67,6 @@ import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@sha
import { CLINE_MCP_TOOL_IDENTIFIER } from "@shared/mcp"
import { USER_CONTENT_TAGS } from "@shared/messages/constants"
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
import type { Mode } from "@shared/storage/types"
import { ClineDefaultTool } from "@shared/tools"
import { ClineAskResponse } from "@shared/WebviewMessage"
import { isClaude4PlusModelFamily, isGPT5ModelFamily, isLocalModel, isNextGenModelFamily } from "@utils/model-utils"
@@ -94,7 +93,7 @@ import {
ClineTextContentBlock,
ClineToolResponseContent,
ClineUserContent,
} from "@/shared/messages/content"
} from "@/shared/messages"
import { ShowMessageType } from "@/shared/proto/index.host"
import { isClineCliInstalled, isCliSubagentContext } from "@/utils/cli-detector"
import { isInTestMode } from "../../services/test/TestMode"
@@ -715,6 +714,7 @@ export class Task {
const modelInfo: ClineMessageModelInfo = {
providerId: providerInfo.providerId,
modelId: providerInfo.model.id,
mode: providerInfo.mode,
}
if (partial !== undefined) {
@@ -1992,7 +1992,7 @@ export class Task {
const mode = this.stateManager.getGlobalSettingsKey("mode")
const providerId = (mode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider) as string
const customPrompt = this.stateManager.getGlobalSettingsKey("customPrompt")
return { model, providerId, customPrompt }
return { model, providerId, customPrompt, mode }
}
private getApiRequestIdSafe(): string | undefined {
@@ -2446,20 +2446,17 @@ export class Task {
this.taskState.apiRequestsSinceLastTodoUpdate++
// Used to know what models were used in the task if user wants to export metadata for error reporting purposes
const { model, providerId, customPrompt } = this.getCurrentProviderInfo()
const { model, providerId, customPrompt, mode } = this.getCurrentProviderInfo()
if (providerId && model.id) {
try {
await this.modelContextTracker.recordModelUsage(
providerId,
model.id,
this.stateManager.getGlobalSettingsKey("mode"),
)
await this.modelContextTracker.recordModelUsage(providerId, model.id, mode)
} catch {}
}
const modelInfo = {
const modelInfo: ClineMessageModelInfo = {
modelId: model.id,
providerId: providerId,
mode: mode,
}
if (this.taskState.consecutiveMistakeCount >= this.stateManager.getGlobalSettingsKey("maxConsecutiveMistakes")) {
@@ -2696,10 +2693,7 @@ export class Task {
content: userContent,
})
const modeSetting = this.stateManager.getGlobalSettingsKey("mode")
const currentMode: Mode = modeSetting === "act" ? "act" : "plan"
telemetryService.captureConversationTurnEvent(this.ulid, providerId, model.id, "user", currentMode)
telemetryService.captureConversationTurnEvent(this.ulid, providerId, model.id, "user", modelInfo.mode)
// Capture task initialization timing telemetry for the first API request
if (isFirstRequest) {
@@ -2722,11 +2716,7 @@ export class Task {
await this.postStateToWebview()
try {
let cacheWriteTokens = 0
let cacheReadTokens = 0
let inputTokens = 0
let outputTokens = 0
let totalCost: number | undefined
const taskMetrics = { cacheWriteTokens: 0, cacheReadTokens: 0, inputTokens: 0, outputTokens: 0, totalCost: 0 }
const abortStream = async (cancelReason: ClineApiReqCancelReason, streamingFailedMessage?: string) => {
if (this.diffViewProvider.isEditing) {
@@ -2742,6 +2732,20 @@ export class Task {
console.log("updating partial message", lastMessage)
// await this.saveClineMessagesAndUpdateHistory()
}
// update api_req_started to have cancelled and cost, so that we can display the cost of the partial stream
await updateApiReqMsg({
messageStateHandler: this.messageStateHandler,
lastApiReqIndex,
inputTokens: taskMetrics.inputTokens,
outputTokens: taskMetrics.outputTokens,
cacheWriteTokens: taskMetrics.cacheWriteTokens,
cacheReadTokens: taskMetrics.cacheReadTokens,
totalCost: taskMetrics.totalCost,
api: this.api,
cancelReason,
streamingFailedMessage,
})
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
// Let assistant know their response was interrupted for when task is resumed
await this.messageStateHandler.addToApiConversationHistory({
@@ -2758,35 +2762,29 @@ export class Task {
}]`,
},
],
modelInfo,
metrics: {
tokens: {
prompt: taskMetrics.inputTokens,
completion: taskMetrics.outputTokens,
cached: (taskMetrics.cacheWriteTokens ?? 0) + (taskMetrics.cacheReadTokens ?? 0),
},
cost: taskMetrics.totalCost,
},
})
// update api_req_started to have cancelled and cost, so that we can display the cost of the partial stream
await updateApiReqMsg({
messageStateHandler: this.messageStateHandler,
lastApiReqIndex,
inputTokens,
outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
api: this.api,
cancelReason,
streamingFailedMessage,
})
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
telemetryService.captureConversationTurnEvent(
this.ulid,
providerId,
modelInfo.modelId,
"assistant",
currentMode,
modelInfo.mode,
{
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
tokensIn: taskMetrics.inputTokens,
tokensOut: taskMetrics.outputTokens,
cacheWriteTokens: taskMetrics.cacheWriteTokens,
cacheReadTokens: taskMetrics.cacheReadTokens,
totalCost: taskMetrics.totalCost,
},
this.useNativeToolCalls, // For assistant turn only.
)
@@ -2813,6 +2811,7 @@ export class Task {
const { toolUseHandler, reasonsHandler } = this.streamHandler.getHandlers()
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 assistantMessageId = ""
let assistantMessage = "" // For UI display (includes XML)
let assistantTextOnly = "" // For API history (text only, no tool XML)
let assistantTextSignature: string | undefined
@@ -2826,11 +2825,11 @@ export class Task {
case "usage":
this.streamHandler.setRequestId(chunk.id)
didReceiveUsageChunk = true
inputTokens += chunk.inputTokens
outputTokens += chunk.outputTokens
cacheWriteTokens += chunk.cacheWriteTokens ?? 0
cacheReadTokens += chunk.cacheReadTokens ?? 0
totalCost = chunk.totalCost
taskMetrics.inputTokens += chunk.inputTokens
taskMetrics.outputTokens += chunk.outputTokens
taskMetrics.cacheWriteTokens += chunk.cacheWriteTokens ?? 0
taskMetrics.cacheReadTokens += chunk.cacheReadTokens ?? 0
taskMetrics.totalCost = chunk.totalCost ?? taskMetrics.totalCost
break
case "reasoning": {
// Process the reasoning delta through the handler
@@ -2891,6 +2890,9 @@ export class Task {
if (chunk.signature) {
assistantTextSignature = chunk.signature
}
if (chunk.id) {
assistantMessageId = chunk.id
}
assistantMessage += chunk.text
assistantTextOnly += chunk.text // Accumulate text separately
// parse raw assistant message into content blocks
@@ -2996,11 +2998,11 @@ export class Task {
if (!didReceiveUsageChunk) {
this.api.getApiStreamUsage?.().then(async (apiStreamUsage) => {
if (apiStreamUsage) {
inputTokens += apiStreamUsage.inputTokens
outputTokens += apiStreamUsage.outputTokens
cacheWriteTokens += apiStreamUsage.cacheWriteTokens ?? 0
cacheReadTokens += apiStreamUsage.cacheReadTokens ?? 0
totalCost = apiStreamUsage.totalCost
taskMetrics.inputTokens += apiStreamUsage.inputTokens
taskMetrics.outputTokens += apiStreamUsage.outputTokens
taskMetrics.cacheWriteTokens += apiStreamUsage.cacheWriteTokens ?? 0
taskMetrics.cacheReadTokens += apiStreamUsage.cacheReadTokens ?? 0
taskMetrics.totalCost = apiStreamUsage.totalCost ?? taskMetrics.totalCost
}
})
}
@@ -3009,12 +3011,12 @@ export class Task {
await updateApiReqMsg({
messageStateHandler: this.messageStateHandler,
lastApiReqIndex,
inputTokens,
outputTokens,
cacheWriteTokens,
cacheReadTokens,
inputTokens: taskMetrics.inputTokens,
outputTokens: taskMetrics.outputTokens,
cacheWriteTokens: taskMetrics.cacheWriteTokens,
cacheReadTokens: taskMetrics.cacheReadTokens,
api: this.api,
totalCost,
totalCost: taskMetrics.totalCost,
})
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
await this.postStateToWebview()
@@ -3024,39 +3026,21 @@ export class Task {
throw new Error("Cline instance aborted")
}
this.taskState.didCompleteReadingStream = true
// set any blocks to be complete to allow presentAssistantMessage to finish and set userMessageContentReady to true
// (could be a text block that had no subsequent tool uses, or a text block at the very end, or an invalid tool use, etc. whatever the case, presentAssistantMessage relies on these blocks either to be completed or the user to reject a block in order to proceed and eventually set userMessageContentReady to true)
const partialBlocks = this.taskState.assistantMessageContent.filter((block) => block.partial)
partialBlocks.forEach((block) => {
block.partial = false
})
// in case there are native tool calls pending
const partialToolBlocks = toolUseHandler.getPartialToolUsesAsContent()?.map((block) => ({ ...block, partial: false }))
this.processNativeToolCalls(assistantTextOnly, partialToolBlocks)
if (partialBlocks.length > 0) {
await this.presentAssistantMessage() // if there is content to update then it will complete and update this.userMessageContentReady to true, which we pwaitfor before making the next request. all this is really doing is presenting the last partial message that we just set to complete
}
// now add to apiconversationhistory
// need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
let didEndLoop = false
if (assistantMessage.length > 0 || this.useNativeToolCalls) {
const currentMode = this.stateManager.getGlobalSettingsKey("mode")
// Stored the assistant API response immediately after the stream finishes in the same turn
const assistantHasContent = assistantMessage.length > 0 || this.useNativeToolCalls
if (assistantHasContent) {
telemetryService.captureConversationTurnEvent(
this.ulid,
providerId,
model.id,
"assistant",
currentMode,
modelInfo.mode,
{
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
tokensIn: taskMetrics.inputTokens,
tokensOut: taskMetrics.outputTokens,
cacheWriteTokens: taskMetrics.cacheWriteTokens,
cacheReadTokens: taskMetrics.cacheReadTokens,
totalCost: taskMetrics.totalCost,
},
this.useNativeToolCalls,
)
@@ -3088,6 +3072,7 @@ export class Task {
// reasoning_details only exists for cline/openrouter providers
reasoning_details: thinkingBlock?.summary as any[],
signature: assistantTextSignature,
call_id: assistantMessageId,
})
}
@@ -3109,9 +3094,38 @@ export class Task {
content: assistantContent,
modelInfo,
id: requestId,
metrics: {
tokens: {
prompt: taskMetrics.inputTokens,
completion: taskMetrics.outputTokens,
cached: (taskMetrics.cacheWriteTokens ?? 0) + (taskMetrics.cacheReadTokens ?? 0),
},
cost: taskMetrics.totalCost,
},
})
}
}
this.taskState.didCompleteReadingStream = true
// set any blocks to be complete to allow presentAssistantMessage to finish and set userMessageContentReady to true
// (could be a text block that had no subsequent tool uses, or a text block at the very end, or an invalid tool use, etc. whatever the case, presentAssistantMessage relies on these blocks either to be completed or the user to reject a block in order to proceed and eventually set userMessageContentReady to true)
const partialBlocks = this.taskState.assistantMessageContent.filter((block) => block.partial)
partialBlocks.forEach((block) => {
block.partial = false
})
// in case there are native tool calls pending
const partialToolBlocks = toolUseHandler.getPartialToolUsesAsContent()?.map((block) => ({ ...block, partial: false }))
this.processNativeToolCalls(assistantTextOnly, partialToolBlocks)
if (partialBlocks.length > 0) {
await this.presentAssistantMessage() // if there is content to update then it will complete and update this.userMessageContentReady to true, which we pwaitfor before making the next request. all this is really doing is presenting the last partial message that we just set to complete
}
// now add to apiconversationhistory
// need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
let didEndLoop = false
if (assistantHasContent) {
// 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.
// in case the content blocks finished
// it may be the api stream finished after the last parsed content block was executed, so we are able to detect out of bounds and set userMessageContentReady to true (note you should not call presentAssistantMessage since if the last block is completed it will be presented again)
@@ -3169,6 +3183,14 @@ export class Task {
],
modelInfo,
id: this.streamHandler.requestId,
metrics: {
tokens: {
prompt: taskMetrics.inputTokens,
completion: taskMetrics.outputTokens,
cached: (taskMetrics.cacheWriteTokens ?? 0) + (taskMetrics.cacheReadTokens ?? 0),
},
cost: taskMetrics.totalCost,
},
})
let response: ClineAskResponse
+2
View File
@@ -49,6 +49,7 @@ export class AutoApprove {
case ClineDefaultTool.NEW_RULE:
case ClineDefaultTool.FILE_NEW:
case ClineDefaultTool.FILE_EDIT:
case ClineDefaultTool.APPLY_PATCH:
case ClineDefaultTool.BASH:
return [true, true]
@@ -71,6 +72,7 @@ export class AutoApprove {
case ClineDefaultTool.NEW_RULE:
case ClineDefaultTool.FILE_NEW:
case ClineDefaultTool.FILE_EDIT:
case ClineDefaultTool.APPLY_PATCH:
return [autoApprovalSettings.actions.editFiles, autoApprovalSettings.actions.editFilesExternally ?? false]
case ClineDefaultTool.BASH:
return [
+6
View File
@@ -351,6 +351,12 @@ export class AuthService {
throw new Error("Auth provider is not set")
}
// If a refresh is already in progress, wait for it to complete
if (this._refreshPromise) {
Logger.info("Token refresh already in progress, waiting for completion")
await this._refreshPromise
}
return this._provider.retrieveClineAuthInfo(this._controller)
}
@@ -125,6 +125,7 @@ export class TelemetryService {
OPT_OUT: "user.opt_out",
TELEMETRY_ENABLED: "user.telemetry_enabled",
EXTENSION_ACTIVATED: "user.extension_activated",
EXTENSION_STORAGE_ERROR: "user.extension_storage_error",
AUTH_STARTED: "user.auth_started",
AUTH_SUCCEEDED: "user.auth_succeeded",
AUTH_FAILED: "user.auth_failed",
@@ -444,6 +445,20 @@ export class TelemetryService {
this.captureToProviders(TelemetryService.EVENTS.USER.EXTENSION_ACTIVATED, {}, false)
}
public captureExtensionStorageError(errorMessage: string, eventName: string) {
// Truncate error message to prevent excessive data
this.capture({
event: TelemetryService.EVENTS.USER.EXTENSION_STORAGE_ERROR,
properties: {
error:
errorMessage.length > MAX_ERROR_MESSAGE_LENGTH
? errorMessage.substring(0, MAX_ERROR_MESSAGE_LENGTH) + "..."
: errorMessage,
eventName,
},
})
}
/**
* Records when authentication flow is started
* @param provider The authentication provider being used
+1 -1
View File
@@ -12,7 +12,7 @@ import { DictationSettings } from "./DictationSettings"
import { FocusChainSettings } from "./FocusChainSettings"
import { HistoryItem } from "./HistoryItem"
import { McpDisplayMode } from "./McpDisplayMode"
import { ClineMessageModelInfo } from "./messages/content"
import { ClineMessageModelInfo } from "./messages"
import { OnboardingModelGroup } from "./proto/cline/state"
import { Mode, OpenaiReasoningEffort } from "./storage/types"
import { TelemetrySetting } from "./TelemetrySetting"
+46
View File
@@ -0,0 +1,46 @@
import { ClineMessage } from "./ExtensionMessage"
/**
* Consolidates error_retry messages in a retry sequence, keeping only the latest one.
*
* When an API request fails and auto-retry is enabled, multiple error_retry messages are created
* (e.g., "Attempt 1 of 3", "Attempt 2 of 3", "Attempt 3 of 3"), interleaved with api_req_retried
* messages. This function filters out earlier retry messages, showing only the most recent one.
*
* @param messages - An array of ClineMessage objects to process.
* @returns A new array of ClineMessage objects with error_retry sequences consolidated.
*
* @example
* const messages: ClineMessage[] = [
* { type: 'say', say: 'error_retry', text: '{"attempt":1,"maxAttempts":3}', ts: 1000 },
* { type: 'say', say: 'api_req_retried', ts: 1001 },
* { type: 'say', say: 'error_retry', text: '{"attempt":2,"maxAttempts":3}', ts: 1002 },
* { type: 'say', say: 'api_req_retried', ts: 1003 },
* { type: 'say', say: 'error_retry', text: '{"attempt":3,"maxAttempts":3}', ts: 1004 },
* ];
* const result = combineErrorRetryMessages(messages);
* // Result: [{ type: 'say', say: 'error_retry', text: '{"attempt":3,"maxAttempts":3}', ts: 1004 }]
*/
export function combineErrorRetryMessages(messages: ClineMessage[]): ClineMessage[] {
const result: ClineMessage[] = []
for (let i = 0; i < messages.length; i++) {
const message = messages[i]
if (message.say === "error_retry") {
// Look ahead to see if the next non-api_req_retried message is also an error_retry
let nextMessage = messages[i + 1]
if (nextMessage?.say === "api_req_retried") {
nextMessage = messages[i + 2]
}
if (nextMessage?.say === "error_retry") {
// Skip this message, we'll show the next one (or a later one in the sequence)
continue
}
}
result.push(message)
}
return result
}
+9 -8
View File
@@ -1,13 +1,9 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ClineMessageMetricsInfo, ClineMessageModelInfo } from "./metrics"
type ClinePromptInputContent = string
export type ClinePromptInputContent = string
type ClineMessageRole = "user" | "assistant"
export interface ClineMessageModelInfo {
modelId: string
providerId: string
}
export type ClineMessageRole = "user" | "assistant"
export interface ClineReasoningDetailParam {
type: "reasoning.text" | string
@@ -29,7 +25,7 @@ export const REASONING_DETAILS_PROVIDERS = ["cline", "openrouter"]
* This ensures backward compatibility where the messages were stored in Anthropic format with additional
* fields unknown to Anthropic SDK.
*/
export interface ClineTextContentBlock extends Anthropic.TextBlockParam {
export interface ClineTextContentBlock extends Anthropic.TextBlockParam, ClineSharedMessageParam {
// reasoning_details only exists for providers listed in REASONING_DETAILS_PROVIDERS
reasoning_details?: ClineReasoningDetailParam[]
// Thought Signature associates with Gemini
@@ -97,6 +93,11 @@ export interface ClineStorageMessage extends Anthropic.MessageParam {
* MUST be removed before sending message to any LLM provider.
*/
modelInfo?: ClineMessageModelInfo
/**
* LLM operational and performance metrics for this message
* Includes token counts, costs.
*/
metrics?: ClineMessageMetricsInfo
}
/**
+20
View File
@@ -0,0 +1,20 @@
// Core content types
export type {
ClineAssistantContent,
ClineAssistantRedactedThinkingBlock,
ClineAssistantThinkingBlock,
ClineAssistantToolUseBlock,
ClineContent,
ClineDocumentContentBlock,
ClineImageContentBlock,
ClineMessageRole,
ClinePromptInputContent,
ClineReasoningDetailParam,
ClineStorageMessage,
ClineTextContentBlock,
ClineToolResponseContent,
ClineUserContent,
ClineUserToolResultContentBlock,
} from "./content"
export { cleanContentBlock, convertClineStorageToAnthropicMessage, REASONING_DETAILS_PROVIDERS } from "./content"
export type { ClineMessageMetricsInfo, ClineMessageModelInfo } from "./metrics"
+18
View File
@@ -0,0 +1,18 @@
import { Mode } from "../storage/types"
export interface ClineMessageModelInfo {
modelId: string
providerId: string
mode: Mode
}
interface ClineTokensInfo {
prompt: number // Total input tokens (includes cached + non-cached)
completion: number // Total output tokens
cached: number // Subset of prompt_tokens that were cache hits
}
export interface ClineMessageMetricsInfo {
tokens?: ClineTokensInfo
cost?: number // Monetary cost for this turn
}
+4
View File
@@ -32,6 +32,10 @@ export const window = {
showErrorMessage: (_message: string) => Promise.resolve(),
showWarningMessage: (_message: string) => Promise.resolve(),
showInformationMessage: (_message: string) => Promise.resolve(),
createTextEditorDecorationType: (_options: any) => ({
key: "mock-decoration-type",
dispose: () => {},
}),
}
export const commands = {
+7 -1
View File
@@ -121,6 +121,11 @@ export function isGemini3ModelFamily(id: string): boolean {
return modelId.includes("gemini3") || modelId.includes("gemini-3")
}
function isDeepSeek32ModelFamily(id: string): boolean {
const modelId = normalize(id)
return modelId.includes("deepseek") && modelId.includes("3.2")
}
export function isNextGenModelFamily(id: string): boolean {
const modelId = normalize(id)
return (
@@ -130,7 +135,8 @@ export function isNextGenModelFamily(id: string): boolean {
isGPT5ModelFamily(modelId) ||
isMinimaxModelFamily(modelId) ||
isGemini3ModelFamily(modelId) ||
isNextGenOpenSourceModelFamily(modelId)
isNextGenOpenSourceModelFamily(modelId) ||
isDeepSeek32ModelFamily(modelId)
)
}
+2 -1
View File
@@ -1,6 +1,7 @@
import { findLast } from "@shared/array"
import { combineApiRequests } from "@shared/combineApiRequests"
import { combineCommandSequences } from "@shared/combineCommandSequences"
import { combineErrorRetryMessages } from "@shared/combineErrorRetryMessages"
import { combineHookSequences } from "@shared/combineHookSequences"
import type { ClineApiReqInfo, ClineMessage } from "@shared/ExtensionMessage"
import { getApiMetrics } from "@shared/getApiMetrics"
@@ -64,7 +65,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
// Only combine hook sequences if hooks are enabled (both user setting and feature flag)
const areHooksEnabled = hooksEnabled?.user
const withHooks = areHooksEnabled ? combineHookSequences(slicedMessages) : slicedMessages
return combineApiRequests(combineCommandSequences(withHooks))
return combineErrorRetryMessages(combineApiRequests(combineCommandSequences(withHooks)))
}, [messages, hooksEnabled])
// has to be after api_req_finished are all reduced into api_req_started messages
const apiMetrics = useMemo(() => getApiMetrics(modifiedMessages), [modifiedMessages])
@@ -295,6 +295,15 @@ const StyledMarkdown = styled.div<{ compact?: boolean }>`
text-decoration: underline;
}
}
hr, ul {
margin: 13px 0;
}
li > ul {
margin: 4px 0; /* or 0 if you want them very tight */
}
`
const PreWithCopyButton = ({ children, ...preProps }: React.HTMLAttributes<HTMLPreElement>) => {
@@ -18,25 +18,27 @@ type McpViewProps = {
}
const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
const { mcpMarketplaceEnabled, setMcpServers, environment } = useExtensionState()
const [activeTab, setActiveTab] = useState<McpViewTab>(initialTab || (mcpMarketplaceEnabled ? "marketplace" : "configure"))
const { remoteConfigSettings, setMcpServers, environment } = useExtensionState()
// Show marketplace by default unless remote config explicitly disables it
const showMarketplace = remoteConfigSettings?.mcpMarketplaceEnabled !== false
const [activeTab, setActiveTab] = useState<McpViewTab>(initialTab || (showMarketplace ? "marketplace" : "configure"))
const handleTabChange = (tab: McpViewTab) => {
setActiveTab(tab)
}
useEffect(() => {
if (!mcpMarketplaceEnabled && activeTab === "marketplace") {
// If marketplace is disabled and we're on marketplace tab, switch to configure
if (!showMarketplace && activeTab === "marketplace") {
// If marketplace is disabled by remote config and we're on marketplace tab, switch to configure
setActiveTab("configure")
}
}, [mcpMarketplaceEnabled, activeTab])
}, [showMarketplace, activeTab])
// Get setter for MCP marketplace catalog from context
const { setMcpMarketplaceCatalog } = useExtensionState()
useEffect(() => {
if (mcpMarketplaceEnabled) {
if (showMarketplace) {
McpServiceClient.refreshMcpMarketplace(EmptyRequest.create({}))
.then((response) => {
setMcpMarketplaceCatalog(response)
@@ -56,7 +58,7 @@ const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
console.error("Failed to fetch MCP servers:", error)
})
}
}, [mcpMarketplaceEnabled])
}, [showMarketplace])
return (
<div
@@ -95,7 +97,7 @@ const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
padding: "0 20px 0 20px",
borderBottom: "1px solid var(--vscode-panel-border)",
}}>
{mcpMarketplaceEnabled && (
{showMarketplace && (
<TabButton isActive={activeTab === "marketplace"} onClick={() => handleTabChange("marketplace")}>
Marketplace
</TabButton>
@@ -110,7 +112,7 @@ const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
{/* Content container */}
<div style={{ width: "100%" }}>
{mcpMarketplaceEnabled && activeTab === "marketplace" && <McpMarketplaceView />}
{showMarketplace && activeTab === "marketplace" && <McpMarketplaceView />}
{activeTab === "addRemote" && <AddRemoteServerForm onServerAdded={() => handleTabChange("configure")} />}
{activeTab === "configure" && <ConfigureServersView />}
</div>
@@ -15,8 +15,9 @@ import McpMarketplaceCard from "./McpMarketplaceCard"
import McpSubmitCard from "./McpSubmitCard"
const McpMarketplaceView = () => {
const { mcpServers, mcpMarketplaceCatalog, setMcpMarketplaceCatalog, mcpMarketplaceEnabled, remoteConfigSettings } =
useExtensionState()
const { mcpServers, mcpMarketplaceCatalog, setMcpMarketplaceCatalog, remoteConfigSettings } = useExtensionState()
const showMarketplace = remoteConfigSettings?.mcpMarketplaceEnabled !== false
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isRefreshing, setIsRefreshing] = useState(false)
@@ -80,7 +81,7 @@ const McpMarketplaceView = () => {
}
setError(null)
if (mcpMarketplaceEnabled) {
if (showMarketplace) {
McpServiceClient.refreshMcpMarketplace(EmptyRequest.create({}))
.then((response) => {
setMcpMarketplaceCatalog(response)
@@ -180,33 +180,6 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
may not work well with large workspaces.
</p>
</div>
<div style={{ marginTop: 10 }}>
<Tooltip>
<TooltipTrigger>
<div className="flex items-center gap-2">
<VSCodeCheckbox
checked={mcpMarketplaceEnabled}
disabled={remoteConfigSettings?.mcpMarketplaceEnabled !== undefined}
onChange={(e: any) => {
const checked = e.target.checked === true
updateSetting("mcpMarketplaceEnabled", checked)
}}>
Enable MCP Marketplace
</VSCodeCheckbox>
{remoteConfigSettings?.mcpMarketplaceEnabled !== undefined && (
<i className="codicon codicon-lock text-description text-sm" />
)}
</div>
</TooltipTrigger>
<TooltipContent hidden={remoteConfigSettings?.mcpMarketplaceEnabled === undefined}>
This setting is managed by your organization's remote configuration
</TooltipContent>
</Tooltip>
<p className="text-xs text-description">
Enables the MCP Marketplace tab for discovering and installing MCP servers.
</p>
</div>
<div style={{ marginTop: 10 }}>
<label
className="block text-sm font-medium text-(--vscode-foreground) mb-1"