mirror of
https://github.com/cline/cline.git
synced 2026-09-05 05:02:27 +08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85a7b7dbaf | |||
| 852f307268 | |||
| 4e3fe004f4 | |||
| 2b63eed85e | |||
| 2ffdc50ea1 | |||
| 74808431e5 | |||
| 7a523fbaf6 | |||
| c22ea39dc1 | |||
| c9f23076c2 | |||
| a5f6c1d732 | |||
| 3c37a160ac | |||
| 5c3294051f | |||
| 4c2f28f2af | |||
| 6e016298cb | |||
| 0cd7bebfba | |||
| 363aac61fb | |||
| eeb1cc7da8 | |||
| f760f13de5 | |||
| dd52a4a39c | |||
| 639edb5db6 | |||
| 3eac9b04de | |||
| 09692d7d3a | |||
| c81fa0a9d6 | |||
| 326c9c9f99 | |||
| a4518b90c2 | |||
| 79f4d938e6 |
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
claude-dev: patch
|
||||
---
|
||||
|
||||
Fix highlighted text flashing when task header is collapsed.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add X-Cerebras-3rd-Party-Integration header to Cerebras API requests.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add DeepSeek 3.2 to native tool calling allow list
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
claude-dev: patch
|
||||
---
|
||||
|
||||
Added microwave family system prompt configuration
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
claude-dev: patch
|
||||
---
|
||||
|
||||
removing tooltips from auto approve menu
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
claude-dev: patch
|
||||
---
|
||||
|
||||
fix: Standalone, ensure cwd is the install dir to find resources reliably
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"cline-vscode": patch
|
||||
---
|
||||
|
||||
Add Explain Changes feature for reviewing code changes with AI-powered explanations
|
||||
@@ -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"
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Prevent simultaneuos refreshes when restoring auth info
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
claude-dev: patch
|
||||
---
|
||||
|
||||
Add task history recovery documentation with storage paths, recovery command usage, and troubleshooting guide.
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Generated
+54
-101
@@ -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
@@ -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",
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface ApiHandlerModel {
|
||||
export interface ApiProviderInfo {
|
||||
providerId: string
|
||||
model: ApiHandlerModel
|
||||
mode: Mode
|
||||
customPrompt?: string // "compact"
|
||||
autoCondenseThreshold?: number // 0-1 range
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -302,6 +302,7 @@ export class ToolExecutor {
|
||||
ClineDefaultTool.FILE_NEW,
|
||||
ClineDefaultTool.FILE_EDIT,
|
||||
ClineDefaultTool.NEW_RULE,
|
||||
ClineDefaultTool.APPLY_PATCH,
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
+104
-82
@@ -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
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user