mirror of
https://github.com/cline/cline.git
synced 2026-09-08 22:13:11 +08:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f463197a2b | ||
|
|
e62fbf6b0c | ||
|
|
64254fc97a | ||
|
|
c312c4aef6 | ||
|
|
fab49e810b | ||
|
|
2a20523e16 | ||
|
|
06585821d1 | ||
|
|
9e802b11da | ||
|
|
afb77c5a8d | ||
|
|
0a4811222f | ||
|
|
b4ce378e4b | ||
|
|
2f60a898af | ||
|
|
8ffd82eda3 | ||
|
|
81276fdf85 | ||
|
|
164e11aae1 | ||
|
|
9792f174b1 | ||
|
|
a590200c64 | ||
|
|
297a45d73a | ||
|
|
e84de0ab3c | ||
|
|
515cb81439 | ||
|
|
550428eabd | ||
|
|
22c22a1cfc | ||
|
|
8202479cec | ||
|
|
bcbaa4518d | ||
|
|
9d799643ba | ||
|
|
6271c5da37 | ||
|
|
89aeb3db3d | ||
|
|
0caeea1b37 | ||
|
|
852a7c9198 | ||
|
|
c4ef472aeb | ||
|
|
e22c457d19 | ||
|
|
c15287ace0 | ||
|
|
d30f54a89c | ||
|
|
56b913d951 | ||
|
|
55a30e0ffa | ||
|
|
a017f3dfd3 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix: do not retry request automatically on auth failure.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add support for sonar and sonar-pro models to SAP AI Core Provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"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 task timeline display height.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Ensure tool arguments are streamed during file operations when native tool calling is enabled.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added thinking level setting for Gemini 3.0 Pro
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Feat: add thought signature support for Gemini SDK
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Feat: Enable native tool calling for Baseten and Kimi K2 models
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed a bug where terminal commands with double quotes are broken when "Terminal Execution Mode" is set to "Background Exec"
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Replaces generic robot icon with Cline logo across VS Code UI
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add Kimi K2 Thinking to Baseten Provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Skip MCP tool with invalid name (e.g. name too long) when native tool calling is enabled.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix Anthropic provider missing signature param when thinking is enabled.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Networking & Proxy Support
|
||||
|
||||
To ensure Cline works correctly in all environments (VSCode, JetBrains, CLI) and with various network configurations (especially corporate proxies), strictly follow these guidelines for all network activity.
|
||||
|
||||
In extension code, do NOT use the global `fetch` or a default `axios` instance. (Note, `shared/net.ts` is exempt from these rules because it sets up the fetch wrappers.) In Webview code, you SHOULD use global `fetch`.
|
||||
|
||||
Global `fetch` and default `axios` do not automatically pick up proxy configurations in all environments (specifically JetBrains and CLI). You MUST use the provided utilities in `@/shared/net` which handle proxy agent configuration. In the webview, the browser/embedder handles proxies.
|
||||
|
||||
## Guidelines
|
||||
|
||||
### 1. Using `fetch`
|
||||
|
||||
Instead of `fetch(...)`, import the proxy-aware wrapper:
|
||||
|
||||
```typescript
|
||||
import { fetch } from '@/shared/net'
|
||||
|
||||
// Usage is identical to global fetch
|
||||
const response = await fetch('https://api.example.com/data')
|
||||
```
|
||||
|
||||
### 2. Using `axios`
|
||||
|
||||
When using `axios`, you must apply the settings from `getAxiosSettings()`:
|
||||
|
||||
```typescript
|
||||
import axios from 'axios'
|
||||
import { getAxiosSettings } from '@/shared/net'
|
||||
|
||||
const response = await axios.get('https://api.example.com/data', {
|
||||
headers: { 'Authorization': '...' },
|
||||
...getAxiosSettings() // <--- CRITICAL: Injects the proxy agent if needed
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Third-Party Clients (OpenAI, Ollama, etc.)
|
||||
|
||||
Most API client libraries allow you to customize the `fetch` implementation. You **MUST** pass the proxy-aware `fetch` to these clients.
|
||||
|
||||
**Example (OpenAI):**
|
||||
```typescript
|
||||
import OpenAI from "openai"
|
||||
import { fetch } from "@/shared/net"
|
||||
|
||||
this.client = new OpenAI({
|
||||
apiKey: '...',
|
||||
fetch, // <--- CRITICAL: Pass our fetch wrapper
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Tests
|
||||
|
||||
Use `mockFetchForTesting` to mock the underlying fetch implementation.
|
||||
|
||||
**Example (callback):**
|
||||
|
||||
```
|
||||
import { mockFetchForTesting } from "@/shared/net"
|
||||
|
||||
...
|
||||
let mockFetch = ...
|
||||
mockFetchForTesting(mockFetch, () => {
|
||||
// This calls mockFetch
|
||||
fetch('https://foo.example').then(...)
|
||||
})
|
||||
// Original fetch is restored immediately when the call returns.
|
||||
```
|
||||
|
||||
**Example (Promise):**
|
||||
|
||||
```
|
||||
import { mockFetchForTesting } from "@/shared/net"
|
||||
|
||||
...
|
||||
let mockFetch = ...
|
||||
await mockFetchForTesting(mockFetch, async () => {
|
||||
await ...
|
||||
// This calls mockFetch
|
||||
await fetch('https://foo.example')
|
||||
...
|
||||
})
|
||||
// Original fetch is restored when the Promise from the callback settles
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
If you are adding a new network call or integration:
|
||||
1. Check `@/shared/net.ts` is imported.
|
||||
2. Ensure `fetch` or `getAxiosSettings` is being used.
|
||||
3. Verify that third-party clients are configured to use the custom fetch.
|
||||
@@ -1,5 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## [3.38.3]
|
||||
|
||||
- Task export feature now opens the task directory, allowing easy access to the full task files
|
||||
- Added Grok 4.1 and Grok Code to XAI provider
|
||||
- Enabled native tool calling for Baseten and Kimi K2 models
|
||||
- Added thinking level to Gemini 3.0 Pro preview
|
||||
- Expanded Hooks functionality
|
||||
- Removed Task Timeline from Task Header
|
||||
- Bug fix for slash commands
|
||||
- Bug fixes for Vertex provider
|
||||
- Bug fixes for thinking/reasoning issues across multiple providers when using native tool calling
|
||||
- Bug fixes for terminal usage on Windows devices
|
||||
|
||||
## [3.38.2]
|
||||
|
||||
- Add Claude Opus 4.5
|
||||
|
||||
## [3.38.1]
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -290,6 +290,12 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
|
||||
return err
|
||||
}
|
||||
settings.ActModeAwsBedrockCustomSelected = boolPtr(val)
|
||||
case "hooks_enabled":
|
||||
val, err := parseBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.HooksEnabled = boolPtr(val)
|
||||
|
||||
// Integer fields
|
||||
case "request_timeout_ms":
|
||||
|
||||
+43
-12
@@ -248,16 +248,10 @@
|
||||
"exploring-clines-tools/remote-browser-support"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Enterprise",
|
||||
"pages": [
|
||||
"enterprise-solutions/overview",
|
||||
"enterprise-solutions/security-concerns"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"troubleshooting/networking-and-proxies",
|
||||
"troubleshooting/terminal-quick-fixes",
|
||||
"troubleshooting/terminal-integration-guide",
|
||||
"more-info/telemetry"
|
||||
@@ -265,15 +259,36 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Enterprise",
|
||||
"icon": "building",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Enterprise Solutions",
|
||||
"pages": [
|
||||
"enterprise-solutions/overview",
|
||||
"enterprise-solutions/onboarding",
|
||||
"enterprise-solutions/members/roles-and-permissions",
|
||||
{
|
||||
"group": "Provider Remote Configuration",
|
||||
"pages": [
|
||||
{
|
||||
"group": "AWS Bedrock",
|
||||
"pages": [
|
||||
"enterprise-solutions/provider-remote-config/aws-bedrock/admin-configuration",
|
||||
"enterprise-solutions/provider-remote-config/aws-bedrock/member-configuration"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Learn",
|
||||
"icon": "graduation-cap",
|
||||
"href": "https://cline.bot/learn"
|
||||
},
|
||||
{
|
||||
"tab": "Blog",
|
||||
"icon": "newspaper",
|
||||
"href": "https://cline.bot/blog"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -335,6 +350,22 @@
|
||||
{
|
||||
"source": "/cline-cli/samples",
|
||||
"destination": "/cline-cli/samples/overview"
|
||||
},
|
||||
{
|
||||
"source": "/enterprise-solutions/configure-AWS-Bedrock-Admin",
|
||||
"destination": "/enterprise-solutions/provider-remote-config/aws-bedrock/admin-configuration"
|
||||
},
|
||||
{
|
||||
"source": "/enterprise-solutions/configure-AWS-Bedrock-Member",
|
||||
"destination": "/enterprise-solutions/provider-remote-config/aws-bedrock/member-configuration"
|
||||
},
|
||||
{
|
||||
"source": "/enterprise-solutions/configure-workOS-authkit",
|
||||
"destination": "/enterprise-solutions/onboarding"
|
||||
},
|
||||
{
|
||||
"source": "/enterprise-solutions/Onboarding your Organization",
|
||||
"destination": "/enterprise-solutions/onboarding"
|
||||
}
|
||||
],
|
||||
"search": {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: "Managing Members"
|
||||
sidebarTitle: "Managing Members"
|
||||
description: "A guide to adding, removing, and editing members in your enterprise organization."
|
||||
---
|
||||
|
||||
This guide covers the practical steps for adding, editing, and removing members from your enterprise dashboard. For a conceptual overview of roles and permissions, see the [Roles and Permissions](/enterprise-solutions/members/roles-and-permissions).
|
||||
|
||||
<Frame caption="The Members Dashboard provides a central place to manage your team.">
|
||||
<img src="https://storage.googleapis.com/cline_public_images/members-dash.png" alt="Members Dashboard" />
|
||||
</Frame>
|
||||
|
||||
## Adding Members
|
||||
|
||||
To invite someone to your organization, you must have an open seat available on your organization.
|
||||
|
||||
1. Navigate to the **Members** tab in your dashboard.
|
||||
2. Click the **Add Members** button.
|
||||
3. Enter one or more email addresses, separated by commas.
|
||||
4. Select a role for the new member(s). It's best practice to start with the "Member" role unless you know they need admin privileges.
|
||||
5. Click **Send Invitation**.
|
||||
|
||||
Invited users will receive an email with a link to join. You can cancel a pending invitation at any time by clicking the trash icon next to the user's email in the 'Pending Invites' section.
|
||||
|
||||
<Tip>
|
||||
**Managing Users at Scale**
|
||||
|
||||
When inviting a large number of users, you can paste a comma-separated list of emails directly into the invitation field. While role changes and removals are performed individually, this bulk invitation feature helps streamline the onboarding process for entire teams.
|
||||
</Tip>
|
||||
|
||||
<Frame caption="Adding members to your organization">
|
||||
<img src="https://storage.googleapis.com/cline_public_images/adding-members.png" alt="Confirm Member Removal" />
|
||||
</Frame>
|
||||
|
||||
## Editing Member Roles
|
||||
|
||||
As your team's needs change, you can adjust member roles directly from the dashboard.
|
||||
|
||||
- Find the member in your list.
|
||||
- Under the "Role" column, click the dropdown menu.
|
||||
- Select their new role. The change takes effect immediately.
|
||||
|
||||
Refer to the [Roles and Permissions](/enterprise-solutions/members/roles-and-permissions) for a detailed breakdown of what each role can do.
|
||||
|
||||
## Removing Members
|
||||
|
||||
Removing a member immediately revokes their access to all organization-specific resources, including shared API keys and configurations.
|
||||
|
||||
1. Go to the **Members Dashboard**.
|
||||
2. Find the member in the list and click the red trash icon (<Icon icon="trash" iconType="solid" />).
|
||||
3. Confirm the removal when prompted.
|
||||
|
||||
<Frame caption="You will be asked to confirm before a member is permanently removed.">
|
||||
<img src="https://storage.googleapis.com/cline_public_images/remove-user.png" alt="Confirm Member Removal" />
|
||||
</Frame>
|
||||
|
||||
## Troubleshooting Invitations
|
||||
|
||||
If an invited user is having trouble joining, check these common issues:
|
||||
|
||||
- **Invitation Not Received**: Ask the user to check their spam or junk mail folder. If it's not there, cancel the pending invitation and try sending it again, verifying the email address is correct.
|
||||
|
||||
- **"Invalid Domain" Error**: The user's email address must belong to a domain that has been verified for your organization. Work with your IT administrator to ensure the necessary domains are configured.
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: "Managing Members"
|
||||
sidebarTitle: "Managing Members"
|
||||
description: "A guide to adding, removing, and editing members in your enterprise organization."
|
||||
---
|
||||
|
||||
This guide covers the practical steps for adding, editing, and removing members from your enterprise dashboard. For a conceptual overview of roles and permissions, see the [Roles and Permissions](/enterprise-solutions/members/roles-and-permissions).
|
||||
|
||||
<Frame caption="The Members Dashboard provides a central place to manage your team.">
|
||||
<img src="https://storage.googleapis.com/cline_public_images/members-dash.png" alt="Members Dashboard" />
|
||||
</Frame>
|
||||
|
||||
## Adding Members
|
||||
|
||||
To invite someone to your organization, you must have an open seat available on your organization.
|
||||
|
||||
1. Navigate to the **Members** tab in your dashboard.
|
||||
2. Click the **Add Members** button.
|
||||
3. Enter one or more email addresses, separated by commas.
|
||||
4. Select a role for the new member(s). It's best practice to start with the "Member" role unless you know they need admin privileges.
|
||||
5. Click **Send Invitation**.
|
||||
|
||||
Invited users will receive an email with a link to join. You can cancel a pending invitation at any time by clicking the trash icon next to the user's email in the 'Pending Invites' section.
|
||||
|
||||
<Tip>
|
||||
**Managing Users at Scale**
|
||||
|
||||
When inviting a large number of users, you can paste a comma-separated list of emails directly into the invitation field. While role changes and removals are performed individually, this bulk invitation feature helps streamline the onboarding process for entire teams.
|
||||
</Tip>
|
||||
|
||||
<Frame caption="Adding members to your organization">
|
||||
<img src="https://storage.googleapis.com/cline_public_images/adding-members.png" alt="Confirm Member Removal" />
|
||||
</Frame>
|
||||
|
||||
## Editing Member Roles
|
||||
|
||||
As your team's needs change, you can adjust member roles directly from the dashboard.
|
||||
|
||||
- Find the member in your list.
|
||||
- Under the "Role" column, click the dropdown menu.
|
||||
- Select their new role. The change takes effect immediately.
|
||||
|
||||
Refer to the [Roles and Permissions](/enterprise-solutions/members/roles-and-permissions) for a detailed breakdown of what each role can do.
|
||||
|
||||
## Removing Members
|
||||
|
||||
Removing a member immediately revokes their access to all organization-specific resources, including shared API keys and configurations.
|
||||
|
||||
1. Go to the **Members Dashboard**.
|
||||
2. Find the member in the list and click the red trash icon (<Icon icon="trash" iconType="solid" />).
|
||||
3. Confirm the removal when prompted.
|
||||
|
||||
<Frame caption="You will be asked to confirm before a member is permanently removed.">
|
||||
<img src="https://storage.googleapis.com/cline_public_images/remove-user.png" alt="Confirm Member Removal" />
|
||||
</Frame>
|
||||
|
||||
## Troubleshooting Invitations
|
||||
|
||||
If an invited user is having trouble joining, check these common issues:
|
||||
|
||||
- **Invitation Not Received**: Ask the user to check their spam or junk mail folder. If it's not there, cancel the pending invitation and try sending it again, verifying the email address is correct.
|
||||
|
||||
- **"Invalid Domain" Error**: The user's email address must belong to a domain that has been verified for your organization. Work with your IT administrator to ensure the necessary domains are configured.
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: "Members Overview"
|
||||
sidebarTitle: "Overview"
|
||||
description: "An overview of member management in your enterprise organization."
|
||||
---
|
||||
|
||||
This section provides a comprehensive guide to managing members in your enterprise organization. Here, you'll find everything you need to know about roles, permissions, and the practical steps for adding, editing, and removing members from your dashboard.
|
||||
|
||||
## Key Topics
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="Roles and Permissions"
|
||||
icon="user-shield"
|
||||
href="/enterprise-solutions/members/roles-and-permissions"
|
||||
>
|
||||
A detailed breakdown of the available roles and their specific permissions.
|
||||
</Card>
|
||||
<Card
|
||||
title="Managing Members"
|
||||
icon="users-gear"
|
||||
href="/enterprise-solutions/members/managing-members"
|
||||
>
|
||||
A practical guide to adding, editing, and removing members from your
|
||||
dashboard.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: "Roles and Permissions"
|
||||
sidebarTitle: "Roles and Permissions"
|
||||
description: "An overview of member roles, permissions, and best practices for your enterprise organization."
|
||||
---
|
||||
|
||||
Choosing the right role for each member is crucial for maintaining security and ensuring your team can work effectively. This guide provides a detailed breakdown of the available roles, their specific permissions, and best practices for managing your organization.
|
||||
|
||||
## Role Definitions
|
||||
|
||||
Here’s a summary of the available roles and their intended use cases.
|
||||
|
||||
<CardGroup cols={1}>
|
||||
<Card title="Owner" icon="user-crown">
|
||||
**Best for:** The primary account holder or a small number of designated leaders.
|
||||
|
||||
Owners have unrestricted access to all settings, including billing, member management, and security configurations. To maintain tight control over the organization, the number of Owners should be kept to a minimum.
|
||||
</Card>
|
||||
<Card title="Admin" icon="user-gear">
|
||||
**Best for:** Team leads or IT administrators who need to manage users and configurations.
|
||||
|
||||
Admins can invite, edit, and remove members, as well as manage provider configurations. They have broad access but cannot manage billing or change the Owner. This is a suitable role for trusted team managers.
|
||||
</Card>
|
||||
<Card title="Member" icon="user">
|
||||
**Best for:** Most developers and individual contributors.
|
||||
|
||||
Members can use Cline with the organization's shared resources but cannot change any settings or view other users' activity. This is the safest default role for new users.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Permissions Matrix
|
||||
|
||||
For a detailed comparison, this matrix outlines the specific capabilities of each role.
|
||||
|
||||
| Permission | Member | Admin | Owner |
|
||||
| --------------------------- | :----: | :----: | :----: |
|
||||
| **General Usage** | | | |
|
||||
| Use Cline | ✅ | ✅ | ✅ |
|
||||
| Access Shared API Providers | ✅ | ✅ | ✅ |
|
||||
| | | | |
|
||||
| **Member Management** | | | |
|
||||
| View Members | ❌ | ✅ | ✅ |
|
||||
| Invite New Members | ❌ | ✅ | ✅ |
|
||||
| Edit Member Roles | ❌ | ✅ | ✅ |
|
||||
| Remove Members | ❌ | ✅ | ✅ |
|
||||
| Remove Admins | ❌ | ❌ | ✅ |
|
||||
| | | | |
|
||||
| **Configuration** | | | |
|
||||
| Configure API Providers | ❌ | ✅ | ✅ |
|
||||
| Manage Security Settings | ❌ | ❌ | ✅ |
|
||||
| | | | |
|
||||
| **Billing & Ownership** | | | |
|
||||
| View Billing Information | ❌ | ❌ | ✅ |
|
||||
| Manage Subscription | ❌ | ❌ | ✅ |
|
||||
| Transfer Ownership | ❌ | ❌ | ✅ |
|
||||
|
||||
## Role Management Best Practices
|
||||
|
||||
Effective role management is fundamental to securing your organization.
|
||||
|
||||
- **Apply the Principle of Least Privilege**: Always assign the role with the minimum necessary permissions. Most users should be **Members**. Grant **Admin** rights only to those who are responsible for user management or technical configuration.
|
||||
|
||||
- **Limit the Number of Owners**: The **Owner** role should be reserved for one or two key individuals who control the account and billing. This centralization of power prevents accidental or malicious changes to critical settings.
|
||||
|
||||
- **Regularly Audit Roles**: Periodically review the list of Admins and Owners to ensure the assigned roles are still appropriate. When a team member's responsibilities change, adjust their role accordingly.
|
||||
|
||||
## Identity Providers and Domain Verification
|
||||
|
||||
For a user to successfully join and sign in to your organization, two conditions must be met:
|
||||
1. Their email must be managed by your organization's verified **Identity Provider (IDP)**, such as Microsoft Entra ID, Okta, or AWS.
|
||||
2. Your organization must have a **verified domain** with a provider like Google or Microsoft.
|
||||
|
||||
This ensures that only authenticated users from your company can access your Cline organization.
|
||||
|
||||
## Seat Management and Invitations
|
||||
|
||||
Each user in your organization, regardless of role, consumes one seat from your license.
|
||||
|
||||
- When an invitation is sent, a seat is considered "pending."
|
||||
- If an invited user does not accept, the invitation can be revoked to free up the seat.
|
||||
- Removing a member from the organization immediately frees up a seat.
|
||||
|
||||
Now that you understand the different roles and how to manage them, you can proceed to [configuring provider remote access](/enterprise-solutions/provider-remote-config/aws-bedrock/admin-configuration) for your organization.
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
title: "Onboarding"
|
||||
|
||||
description: "This guide explains how administrators configure SSO provisioning and user management in Cline Enterprise."
|
||||
---
|
||||
|
||||
## Overview
|
||||
Cline Enterprise integrates with your existing identity provider (IdP) via WorkOS to deliver secure SSO and zero-touch user lifecycle management. In this guide, you'll connect your IdP (Okta, Azure AD, Google Workspace, or any SAML/OIDC provider), enable just-in-time (JIT) provisioning so new users are created automatically on first sign-in, and configure role mapping so permissions stay aligned with your directory—no manual invites or seat reconciliations required.
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Cline Enterprise License](https://cline.bot/enterprise)
|
||||
- Access to your identity provider (IdP) configuration (e.g., Okta, Azure AD, Google Workspace)
|
||||
- Knowledge of your organization's SSO requirements
|
||||
|
||||
## Configuration Steps
|
||||
|
||||
### Step 1: Onboard to Cline Enterprise license
|
||||
|
||||
Your IdP administrator will receive an email with a link to register their organization with WorkOS during onboarding.
|
||||
|
||||
### Step 2: Configure Your Identity Provider
|
||||
|
||||
Connect your identity provider (IdP) to WorkOS:
|
||||
|
||||
1. In the WorkOS dashboard, go to **AuthKit → Connections**
|
||||
2. Click **Add Connection**
|
||||
3. Select your identity provider (e.g., Okta, Azure AD, Google Workspace, Generic SAML/OIDC)
|
||||
4. Follow the provider-specific setup instructions
|
||||
|
||||
Each identity provider (IdP) will have its own setup process and required fields. Be sure to follow the specific instructions in the WorkOS dashboard for your chosen provider.
|
||||
For more explicit instruction on connecting your IdP, refer to the [WorkOS SSO documentation](https://workos.com/docs/authkit/sso)
|
||||
|
||||
### Step 3: Configure User Provisioning
|
||||
|
||||
Cline Enterprise uses **just-in-time provisioning** that works automatically:
|
||||
|
||||
- **Organizations are created automatically**
|
||||
- **Users gain access automatically** on their first SSO sign-in, once their credentials have been configured by the IdP administrator.
|
||||
- **Roles sync automatically** from your IdP (Admin/Owner → Admin, Member → Member)
|
||||
- **No manual user invites or seat management** required
|
||||
|
||||
No additional configuration is needed. Users are provisioned automatically when they sign in through SSO.
|
||||
|
||||
### Step 4: Configure User Attributes Mapping
|
||||
|
||||
User roles are mapped automatically from your IdP:
|
||||
|
||||
- **Admin** in IdP → **Admin** role in Cline (Note: The first Owner of the org is created manually during onboarding)
|
||||
- **Member** in IdP → **Member** role in Cline
|
||||
|
||||
<Info>
|
||||
For what each role can access, see the [Roles and Permissions](./members/roles-and-permissions) page.
|
||||
</Info>
|
||||
|
||||
If needed, you can configure additional user attributes in the Cline Admin console:
|
||||
|
||||
1. Go to **Settings → Authentication → User Attributes**
|
||||
2. Map attributes such as email and name based on your IdP configuration
|
||||
|
||||
For information about available user attributes, see the [WorkOS User Object Documentation](https://workos.com/docs/authkit/user-management).
|
||||
|
||||
### Step 5: Test SSO Connection
|
||||
|
||||
Before allowing users to sign in, test the SSO flow to ensure everything is configured correctly.
|
||||
|
||||
**To test the connection:**
|
||||
|
||||
1. In the WorkOS dashboard (or Cline Admin console if available), locate and click **Test SSO Connection**
|
||||
2. You'll be redirected to your IdP's login page
|
||||
3. Enter valid credentials for a test user
|
||||
4. After successful authentication, you should be redirected back
|
||||
5. Confirm that the user's information (name, email, role) displays correctly
|
||||
|
||||
**Expected outcome:** The test user is authenticated, their account details are visible, and their role matches what's configured in your IdP.
|
||||
|
||||
**If the test fails:** Double-check your IdP configuration (redirect URIs, SAML certificates, attribute mappings). See the [WorkOS SSO documentation](https://workos.com/docs/authkit/sso) for troubleshooting guidance.
|
||||
|
||||
### User Access
|
||||
|
||||
Once SSO is configured, users in your IdP can access Cline automatically without manual invites or account setup.
|
||||
|
||||
**First-time sign-in flow:**
|
||||
|
||||
1. User navigates to Cline and clicks **Sign in with SSO**
|
||||
2. User authenticates via your organization's IdP
|
||||
3. Cline automatically creates their account in your Organization
|
||||
4. Role is assigned based on their IdP role (see [Step 4](#step-4-configure-user-attributes-mapping))
|
||||
5. User is redirected to Cline and can begin working
|
||||
|
||||
**What happens automatically:**
|
||||
- Account creation with correct organization assignment
|
||||
- Role and permission assignment
|
||||
- Basic profile information (name, email) populated from IdP
|
||||
|
||||
**No action required:** Users don't need to request access or wait for approval. Access is granted immediately upon successful IdP authentication.
|
||||
|
||||
### Managing Access
|
||||
|
||||
All access management and revocation of users is currently handled by your IdP:
|
||||
|
||||
- Add users → access granted automatically on first login
|
||||
- Change roles → updated on next login
|
||||
- Remove users → access revoked automatically
|
||||
|
||||
<Info>
|
||||
Role changes sync automatically on the user's next sign-in.
|
||||
</Info>
|
||||
|
||||
### Changing your IdP
|
||||
|
||||
In order to change to a different IdP, please contact support and we will guide you through this process.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
Steps to verify successful configuration:
|
||||
|
||||
1. **Test User Sign-In**: Have a test user sign in through the SSO flow (access is granted automatically on first login)
|
||||
2. **Verify User Provisioning**: Confirm that the user is automatically created and has appropriate role permissions
|
||||
3. **Check User Attributes**: Verify that user information (name, email, organization) is correctly populated
|
||||
4. **Test Role Changes**: Update a user's role in your IdP and verify it syncs on their next login
|
||||
5. **Test User Deprovisioning**: Remove a user from your IdP and verify they lose access to Cline on their next login attempt
|
||||
6. **Review Audit Logs**: Check WorkOS audit logs to ensure authentication events are being recorded
|
||||
|
||||
---
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: "Configure AWS Bedrock Provider (Admin)"
|
||||
sidebarTitle: "Configure AWS Bedrock (Admin)"
|
||||
description: "This guide explains how administrators configure AWS Bedrock as the organization-wide LLM provider for Cline."
|
||||
---
|
||||
|
||||
As an administrator, you can add AWS Bedrock as the organization-wide LLM provider for all Cline users. This centralized approach ensures consistent access to Amazon's AI models while maintaining your organization's security and compliance requirements through VPC endpoints, region controls, and prompt caching optimizations.
|
||||
|
||||
## Before You Begin
|
||||
|
||||
To get started with setting up AWS Bedrock as your organization's LLM provider, you'll need a few items in place.
|
||||
|
||||
**Administrator access to the Cline Admin console**
|
||||
You need admin privileges to enforce provider settings across your organization. If you can navigate to **Settings → Cline Settings** in the admin console at [app.cline.bot](https://app.cline.bot), you have the right access level.
|
||||
|
||||
<Info>
|
||||
**Quick Check**: Try accessing the settings page now. If you can see the provider configuration options, you're good to go.
|
||||
</Info>
|
||||
|
||||
**AWS Bedrock account with the right permissions**
|
||||
Your AWS account needs specific Bedrock permissions to work with Cline.
|
||||
|
||||
<Note>
|
||||
If you don't have direct AWS access, coordinate with your cloud team to get these permissions set up before proceeding.
|
||||
</Note>
|
||||
|
||||
**Your preferred AWS region**
|
||||
Choose your primary AWS region carefully since this will be enforced for all users.
|
||||
|
||||
<Tip>
|
||||
Check which models are available in your region first. Some newer models might not be available in all regions yet.
|
||||
</Tip>
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline-static-assets-prod/assets/AWS%20Remote%20Config.gif"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
## Configuration Steps
|
||||
|
||||
<Steps>
|
||||
<Step title="Access Cline Settings">
|
||||
Navigate to [app.cline.bot](https://app.cline.bot) and sign in with your administrator account. Go to **Settings → Cline Settings**.
|
||||
|
||||
<Info>
|
||||
You should see the provider configuration options if you have the correct admin access level.
|
||||
</Info>
|
||||
</Step>
|
||||
|
||||
<Step title="Enable Remote Provider Configuration">
|
||||
Toggle on **Enable settings** to reveal the remote provider configuration options. This allows you to enforce provider settings across your organization.
|
||||
</Step>
|
||||
|
||||
<Step title="Select AWS Bedrock as the API Provider">
|
||||
Open the **API Provider** dropdown menu and select **Amazon Bedrock**. This will open the Bedrock configuration panel where you'll configure all your organization-wide settings.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure Bedrock Settings">
|
||||
The configuration panel includes several settings that control how Bedrock works for your organization. Configure what you need:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Region (required)">
|
||||
Enter your preferred AWS region like `us-west-2` or `us-east-1`. This region will be enforced for all organization members.
|
||||
|
||||
[View AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
|
||||
|
||||
<Tip>
|
||||
For most organizations, `us-east-1` or `us-west-2` are recommended as they have the best model availability.
|
||||
</Tip>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Custom VPC Endpoint (optional)">
|
||||
If your organization uses a private VPC endpoint for Bedrock, specify it here to ensure all API calls go through your network infrastructure.
|
||||
|
||||
[Learn more about AWS PrivateLink](https://docs.aws.amazon.com/vpc/latest/userguide/endpoint-services-overview.html)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Cross-region Inference (optional)">
|
||||
Enable this to let Bedrock automatically route requests to other regions when your primary region has capacity constraints. Useful for maintaining availability during high-demand periods.
|
||||
|
||||
[Learn more about Inference Profiles](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Global Inference Profile (optional)">
|
||||
Turn this on to use AWS's global inference routing, which automatically directs requests to the optimal region based on availability and latency.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Prompt Caching (optional)">
|
||||
Enable prompt caching to reduce costs and latency. Bedrock caches portions of prompts that remain consistent across requests, making repeated interactions faster and cheaper.
|
||||
|
||||
[Learn more about Prompt Caching](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html)
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</Step>
|
||||
|
||||
<Step title="Save Configuration">
|
||||
After configuring your settings, close the provider configuration panel and click **Save** on the settings page to persist your changes.
|
||||
|
||||
Once saved, all organization members signed into the Cline extension will automatically use AWS Bedrock with your configured settings. They won't be able to select other providers or switch to their personal Cline accounts.
|
||||
|
||||
<Warning>
|
||||
Members can't switch to personal Cline accounts or join other organizations once remote configuration is enabled. This ensures consistent provider usage across your team.
|
||||
</Warning>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verification
|
||||
|
||||
To verify the configuration:
|
||||
|
||||
1. Check that the provider shows as "Amazon Bedrock" in the Enabled provider field
|
||||
2. Confirm the settings persist after refreshing the page
|
||||
3. Test with a member account to ensure they see only Bedrock as a provider
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Members don't see the configured provider**
|
||||
Ensure you clicked Save after closing the configuration panel. Verify the member account belongs to the correct organization.
|
||||
|
||||
**Configuration changes don't persist**
|
||||
Make sure to click the Save button on the main settings page, not just close the configuration panel.
|
||||
|
||||
**Need to change regions later**
|
||||
You can update the region at any time. Members will need to ensure their local AWS credentials have access to the new region. For more information, refer to the [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html).
|
||||
|
||||
For further details, consult the [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) and coordinate with your internal cloud team.
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
---
|
||||
title: "Configure AWS Bedrock in VS Code (Members)"
|
||||
sidebarTitle: "Configure AWS Bedrock (Member)"
|
||||
description: "Guide for engineers configuring AWS Bedrock credentials in VS Code after admin setup"
|
||||
---
|
||||
|
||||
As a team member, you can connect your local development environment to your organization's AWS Bedrock setup. This guide walks you through configuring your AWS credentials in VS Code so you can start using models through your organization's Bedrock infrastructure. Your administrator has already configured the provider settings—you just need to add your credentials to get started.
|
||||
|
||||
## Before You Begin
|
||||
|
||||
To successfully connect to your organization's AWS Bedrock setup, you'll need a few things ready.
|
||||
|
||||
**Cline extension installed and configured**
|
||||
The Cline extension must be installed in VS Code and you need to be signed into your organization account. If you haven't installed Cline yet, follow our [installation guide](/getting-started/installing-cline).
|
||||
|
||||
<Info>
|
||||
**Quick Check**: Open the Cline panel in VS Code. If you see your organization name in the bottom left, you're signed in correctly.
|
||||
</Info>
|
||||
|
||||
**AWS credentials with Bedrock access**
|
||||
You need AWS credentials that have permission to access Bedrock in your organization's configured region.
|
||||
|
||||
<Note>
|
||||
If you don't have AWS credentials yet, reach out to your IT or cloud team to get access keys or AWS CLI profiles configured with the necessary Bedrock permissions.
|
||||
</Note>
|
||||
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline-static-assets-prod/assets/VS%20Code%20Bedrock%20API%20Key.gif"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
## Configuration Steps
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Cline Settings">
|
||||
Open VS Code and access the Cline settings panel using either of these methods:
|
||||
|
||||
- Click the settings icon (⚙️) in the Cline panel
|
||||
- Click on the API Provider dropdown located directly below the chat area (it will display as `bedrock.anthropic.claude-sonnet-4-20250514-v1:0` or similar)
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Select Your Authentication Method">
|
||||
Choose one of the following credential methods to authenticate with AWS Bedrock:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="AWS Bedrock API Key">
|
||||
Use dedicated AWS access keys specifically for Bedrock access.
|
||||
|
||||
[Learn more about AWS Bedrock API Keys](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html)
|
||||
|
||||
1. Select the **API Key** radio button
|
||||
2. Enter your AWS Access Key ID and Secret Access Key
|
||||
3. These credentials are stored locally and used only by the VS Code extension
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="AWS Profile">
|
||||
Use an existing AWS CLI profile configured on your machine.
|
||||
|
||||
[Learn more about AWS CLI Profiles](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html)
|
||||
|
||||
1. Select the **AWS Profile** radio button
|
||||
2. Choose or enter the profile name from your `~/.aws/credentials` file
|
||||
3. Cline will use the credentials associated with that profile
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="AWS Credentials">
|
||||
Use your default AWS credential chain (environment variables, EC2 instance roles, etc.).
|
||||
|
||||
1. Select the **AWS Credentials** radio button
|
||||
2. Cline will automatically detect credentials from your environment using the standard AWS credential provider chain
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Note>
|
||||
The AWS Region is preconfigured by your administrator and does not need to be set in the extension.
|
||||
</Note>
|
||||
</Step>
|
||||
|
||||
<Step title="Verify Configuration">
|
||||
After selecting your authentication method, the extension will display checkmarks for enabled features:
|
||||
|
||||
- ✓ Supports images
|
||||
- ✓ Supports browser use
|
||||
- ✓ Supports prompt caching
|
||||
|
||||
Additional settings like cross-region inference and global inference profile will be locked (shown with a lock icon 🔒) as they're controlled by your administrator.
|
||||
</Step>
|
||||
|
||||
<Step title="Test the Connection">
|
||||
Send a test message in Cline to verify your credentials work correctly with the configured Bedrock region.
|
||||
|
||||
<Tip>
|
||||
**Testing Recommendation**
|
||||
|
||||
It is recommended to test the connection in plan mode to verify everything works correctly before using it for actual tasks.
|
||||
</Tip>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Authentication errors ("Access Denied" or "Invalid Credentials")**
|
||||
Verify your chosen credential method has the necessary IAM permissions to call Bedrock in the configured region. Required permissions include `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream`. For more information, refer to [AWS Bedrock IAM Permissions](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html).
|
||||
|
||||
**Region-related errors or "model not available"**
|
||||
Ask your administrator to confirm which region is configured for your organization. Ensure your AWS credentials have access to Bedrock in that specific region. [View AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
|
||||
|
||||
**Don't see AWS Bedrock as an option**
|
||||
Confirm you're signed into the correct Cline organization. Verify your administrator has saved the Bedrock configuration. Try signing out and back into the extension.
|
||||
|
||||
**AWS Credentials option not finding credentials**
|
||||
Verify AWS CLI is installed and configured with `aws configure` ([AWS CLI Installation Guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). Check that credentials are present in `~/.aws/credentials`. For EC2/ECS environments, ensure IAM roles are properly attached. If using environment variables, set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.
|
||||
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
When configuring your AWS credentials, follow these security guidelines:
|
||||
|
||||
- Use IAM roles with minimum required permissions ([AWS IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html))
|
||||
- Rotate access keys regularly if using the API Key method
|
||||
- Never store credentials in code or version control
|
||||
- Prefer AWS Profile method for better credential management
|
||||
- Consider using AWS SSO/federated roles for enhanced security
|
||||
|
||||
Your organization administrator controls which models are available. The extension will automatically display available models based on your region's Bedrock configuration. For more information about available models, refer to the [AWS Bedrock Model Access documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html).
|
||||
|
||||
For further assistance, consult the [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) and coordinate with your organization's cloud administrator.
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
title: "Security Concerns"
|
||||
---
|
||||
|
||||
## Enterprise Security with Cline
|
||||
|
||||
Cline addresses enterprise security concerns through its unique client-side architecture that prioritizes data privacy, secure cloud integration, and transparent operations. Below is a comprehensive overview of how Cline maintains robust security measures for enterprise environments.
|
||||
|
||||
### Client-Side Architecture
|
||||
|
||||
Cline operates exclusively as a client-side VSCode extension with zero server-side components. This fundamental design choice ensures that your code and data remain within your secure environment at all times. Unlike traditional AI assistants that send data to external servers for processing, Cline connects directly to your chosen cloud provider's AI endpoints, keeping all sensitive information within your infrastructure boundaries.
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-arch.png"
|
||||
alt="Cline's relationship to local and remote assets"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
### Data Privacy Commitment
|
||||
|
||||
Cline implements a strict zero data retention policy, meaning your intellectual property never leaves your secure environment. The extension does not collect, store, or transmit your code to any central servers. This approach significantly reduces potential attack vectors that might otherwise be introduced through data transmission to third-party systems. Telemetry collection is optional and requires explicit consent.
|
||||
|
||||
### Cloud Provider Integration
|
||||
|
||||
Enterprise teams can access cutting-edge AI models through their existing cloud deployments. Cline supports seamless integration with:
|
||||
|
||||
- AWS Bedrock
|
||||
- Google Cloud Vertex AI
|
||||
- Microsoft Azure
|
||||
|
||||
These integrations utilize your organization's existing security credentials, including native IAM role assumption for AWS. This ensures that all AI processing occurs within your corporate cloud environment, maintaining compliance with your established security protocols.
|
||||
|
||||
### Open-Source Transparency
|
||||
|
||||
Cline's codebase is completely open-source, allowing for comprehensive security auditing by your internal teams. This transparency enables security professionals to verify exactly how the extension functions and confirm that it adheres to your organization's security requirements. Organizations can review the code to ensure it aligns with their security policies before deployment.
|
||||
|
||||
### Controlled Modifications
|
||||
|
||||
The extension implements safeguards against unauthorized changes to your codebase. Cline requires explicit user approval for all file modifications and terminal commands, preventing accidental or unwanted alterations. This approval-based workflow maintains the integrity of your projects while still providing AI assistance.
|
||||
|
||||
### Enterprise Deployment Support
|
||||
|
||||
For organizations with strict security review processes, Cline provides comprehensive documentation including detailed deployment diagrams, sequence diagrams illustrating all data flows, and complete security posture documentation. These materials facilitate thorough security reviews and help demonstrate compliance with enterprise data handling standards and regulations.
|
||||
|
||||
### Access Control
|
||||
|
||||
Enterprise editions of Cline (planned for Q2 2025) will include centralized administration features that allow organizations to:
|
||||
|
||||
- Manage user access with customizable permission levels
|
||||
- Provision accounts with corporate credentials
|
||||
- Immediately revoke access when needed
|
||||
- Control which AI providers and LLM endpoints can be used
|
||||
- Deploy standardized settings across the organization
|
||||
- Prevent unauthorized use of personal API keys
|
||||
|
||||
### Compliance and Governance
|
||||
|
||||
Cline's architecture supports compliance with data sovereignty requirements and enterprise data handling regulations. The planned Enterprise Complete edition will further enhance governance with detailed audit logging, compliance reporting, and automated policy enforcement mechanisms.
|
||||
|
||||
By combining client-side processing, direct cloud provider integration, and transparent operations, Cline offers enterprise teams a secure way to leverage AI assistance while maintaining strict control over their sensitive code and data.
|
||||
Generated
+3
-3
@@ -5017,9 +5017,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
|
||||
@@ -17,6 +17,7 @@ description: "Learn how to configure and use Anthropic Claude models with Cline.
|
||||
Cline supports the following Anthropic Claude models:
|
||||
|
||||
- `claude-haiku-4-5-20251001`
|
||||
- `claude-opus-4-5-20251101`
|
||||
- `claude-opus-4-1-20250805`
|
||||
- `claude-opus-4-20250514`
|
||||
- `anthropic/claude-sonnet-4.5` (Recommended)
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
---
|
||||
title: "Networking and Proxies"
|
||||
sidebarTitle: "Networking & Proxies"
|
||||
description: "Configure Cline to work behind firewalls and proxies"
|
||||
---
|
||||
|
||||
If you're working behind a corporate proxy or firewall, you'll need to configure
|
||||
proxy settings for Cline to connect to AI providers. The configuration varies
|
||||
depending on which version of Cline you're using.
|
||||
|
||||
## VSCode Extension
|
||||
|
||||
The VSCode extension automatically uses VSCode's built-in proxy settings. See
|
||||
[Network Connections in Visual Studio Code, Proxy server support](https://code.visualstudio.com/docs/setup/network#_proxy-server-support)
|
||||
for instructions on how to set up proxies in VSCode. No additional configuration
|
||||
is needed for Cline itself.
|
||||
|
||||
## CLI
|
||||
|
||||
The Cline CLI uses standard HTTP proxy environment variables. Configure these before running `cline` commands.
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
**Windows (Command Prompt)**
|
||||
```cmd
|
||||
set https_proxy=http://proxy.company.com:8080
|
||||
set http_proxy=http://proxy.company.com:8080
|
||||
cline start
|
||||
```
|
||||
|
||||
**Windows (PowerShell)**
|
||||
```powershell
|
||||
$env:https_proxy="http://proxy.company.com:8080"
|
||||
$env:http_proxy="http://proxy.company.com:8080"
|
||||
cline start
|
||||
```
|
||||
|
||||
**macOS/Linux**
|
||||
```bash
|
||||
export https_proxy=http://proxy.company.com:8080
|
||||
export http_proxy=http://proxy.company.com:8080
|
||||
cline start
|
||||
```
|
||||
|
||||
### Proxy with Authentication
|
||||
|
||||
If your proxy requires authentication, include credentials in the URL:
|
||||
|
||||
```bash
|
||||
export https_proxy=http://username:password@proxy.company.com:8080
|
||||
export http_proxy=http://username:password@proxy.company.com:8080
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Storing credentials in environment variables can be a security risk.
|
||||
</Warning>
|
||||
|
||||
### Bypass Proxy for Localhost
|
||||
|
||||
To prevent localhost traffic from going through the proxy, set the `no_proxy` environment variable:
|
||||
|
||||
**Windows**
|
||||
```cmd
|
||||
set no_proxy=localhost,127.0.0.1,.local
|
||||
```
|
||||
|
||||
**macOS/Linux**
|
||||
```bash
|
||||
export no_proxy=localhost,127.0.0.1,.local
|
||||
```
|
||||
|
||||
### Custom Certificate Authority
|
||||
|
||||
If your proxy uses a custom CA certificate:
|
||||
|
||||
**Windows**
|
||||
```cmd
|
||||
set NODE_EXTRA_CA_CERTS=C:\path\to\ca-certificate.crt
|
||||
cline start
|
||||
```
|
||||
|
||||
**macOS/Linux**
|
||||
```bash
|
||||
export NODE_EXTRA_CA_CERTS=/path/to/ca-certificate.pem
|
||||
cline start
|
||||
```
|
||||
|
||||
### Permanent Configuration
|
||||
|
||||
To avoid setting these variables every time, add them to your shell profile or system environment variables.
|
||||
|
||||
**macOS/Linux** (add to `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
|
||||
```bash
|
||||
# Proxy configuration
|
||||
export https_proxy=http://proxy.company.com:8080
|
||||
export http_proxy=http://proxy.company.com:8080
|
||||
export no_proxy=localhost,127.0.0.1,.local
|
||||
export NODE_EXTRA_CA_CERTS=/path/to/ca-certificate.pem
|
||||
```
|
||||
|
||||
**Windows** (System Environment Variables):
|
||||
1. Search for "Environment Variables" in Windows Settings
|
||||
2. Add the variables under "User variables" or "System variables"
|
||||
3. Restart your terminal or IDE
|
||||
|
||||
### Known Limitations
|
||||
|
||||
Cline CLI only supports HTTP proxies. It does not support SOCKS proxies,
|
||||
proxy autoconfiguration (PAC) scripts, or HTTP proxies which require
|
||||
authentication beyond a basic username and password.
|
||||
|
||||
## JetBrains IDEs
|
||||
|
||||
The JetBrains plugin uses the IDE's HTTP proxy settings.
|
||||
|
||||
### Configure JetBrains Proxy
|
||||
|
||||
1. Open Settings/Preferences:
|
||||
- **Windows/Linux**: File > Settings
|
||||
- **macOS**: IntelliJ IDEA > Preferences
|
||||
- Or press `Ctrl+Alt+S` (Windows/Linux) or `Cmd+,` (macOS)
|
||||
|
||||
2. Navigate to:
|
||||
```
|
||||
Appearance & Behavior > System Settings > HTTP Proxy
|
||||
```
|
||||
|
||||
3. Select "Manual proxy configuration"
|
||||
|
||||
4. Configure your proxy:
|
||||
- **Host name**: `proxy.company.com`
|
||||
- **Port number**: `8080`
|
||||
- **No proxy for**: `localhost,127.0.0.1`
|
||||
- Check "Proxy authentication" if required
|
||||
- Enter your username and password
|
||||
|
||||
5. Click "Check connection" to verify the settings
|
||||
|
||||
6. Click "OK" to apply
|
||||
|
||||
7. Restart the IDE
|
||||
|
||||
### Test Connection
|
||||
|
||||
After configuring the proxy, test that Cline can connect to your AI provider:
|
||||
|
||||
1. Open the Cline panel
|
||||
2. Try sending a simple message
|
||||
3. If connection fails, check the IDE's Event Log for error messages
|
||||
|
||||
### Custom Certificate Authority
|
||||
|
||||
If your proxy uses a custom CA:
|
||||
|
||||
1. Add the certificate to your system's trust store, or
|
||||
2. Import it into the JetBrains IDE:
|
||||
- Settings > Tools > Server Certificates
|
||||
- Click "+" to add your certificate
|
||||
|
||||
### Known Limitations
|
||||
|
||||
Cline in JetBrains only supports HTTP proxies. It does not support SOCKS
|
||||
proxies, proxy autoconfiguration (PAC) scripts, or HTTP proxies which require
|
||||
authentication beyond a basic username and password.
|
||||
|
||||
Cline does not pick up changed proxy settings dynamically. After changing proxy
|
||||
settings, restart the IDE for Cline to use the new settings.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Timeouts
|
||||
|
||||
If you're experiencing connection timeouts:
|
||||
|
||||
1. Verify your proxy address and port are correct
|
||||
2. Check if the proxy requires authentication
|
||||
3. Ensure the AI provider's API endpoints aren't blocked by your firewall
|
||||
|
||||
### SSL/TLS Certificate Errors
|
||||
|
||||
If you see certificate-related errors:
|
||||
|
||||
1. Check that `NODE_EXTRA_CA_CERTS` points to the correct certificate file
|
||||
2. Ensure the certificate file is in PEM format
|
||||
3. Use curl to verify the certificate works, for example, `curl -x proxy.corp.example:8080 --cacert /path/to/ca-cert.pem -o - -vv https://api.cline.bot/`
|
||||
4. Consider disabling `http.proxyStrictSSL` in VSCode (not recommended for production)
|
||||
|
||||
### Testing Proxy Configuration
|
||||
|
||||
If you encounter problems with Cline networking, first verify your proxy
|
||||
configuration works using curl:
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
export https_proxy=http://proxy.company.com:8080
|
||||
curl -vv https://api.anthropic.com
|
||||
|
||||
# Windows PowerShell
|
||||
$env:https_proxy="http://proxy.company.com:8080"
|
||||
curl.exe -vv https://api.anthropic.com
|
||||
```
|
||||
|
||||
Use `--cacert $NODE_EXTRA_CA_CERTS` to specify a certificate if necessary.
|
||||
|
||||
Next, check ~/.cline/cline-core-service.log (CLI, JetBrains) for log messages
|
||||
confirming your proxy configuration and any network-related errors.
|
||||
|
||||
## Common Proxy Patterns
|
||||
|
||||
### Authenticated HTTPS Proxy
|
||||
|
||||
```bash
|
||||
export https_proxy=http://username:password@proxy.company.com:8080
|
||||
export NODE_EXTRA_CA_CERTS=/path/to/ca-cert.pem
|
||||
```
|
||||
|
||||
### Proxy with No Authentication
|
||||
|
||||
```bash
|
||||
export https_proxy=http://proxy.company.com:8080
|
||||
export http_proxy=http://proxy.company.com:8080
|
||||
```
|
||||
|
||||
### Proxy with Bypass Rules
|
||||
|
||||
```bash
|
||||
export https_proxy=http://proxy.company.com:8080
|
||||
export no_proxy=localhost,127.0.0.1,.company.local,192.168.0.0/16
|
||||
```
|
||||
Generated
+27
-16
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.38.1",
|
||||
"version": "3.38.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.38.1",
|
||||
"version": "3.38.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
@@ -2222,6 +2222,8 @@
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2230,6 +2232,8 @@
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6348,13 +6352,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/glob": {
|
||||
"version": "11.0.3",
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
||||
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minimatch": "^10.1.1",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
@@ -6370,9 +6376,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": {
|
||||
"version": "10.0.3",
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
@@ -10241,7 +10249,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.3",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
@@ -10254,9 +10264,6 @@
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
@@ -15195,13 +15202,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf/node_modules/glob": {
|
||||
"version": "11.0.3",
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
||||
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minimatch": "^10.1.1",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
@@ -15239,9 +15248,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf/node_modules/minimatch": {
|
||||
"version": "10.0.3",
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
|
||||
+1
-1
@@ -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.38.1",
|
||||
"version": "3.38.3",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
|
||||
@@ -225,7 +225,7 @@ message ToggleWorkflowRequest {
|
||||
message HookInfo {
|
||||
string name = 1;
|
||||
bool enabled = 2;
|
||||
string absolutePath = 3;
|
||||
string absolute_path = 3;
|
||||
}
|
||||
|
||||
message WorkspaceHooks {
|
||||
|
||||
@@ -27,8 +27,12 @@ service ModelsService {
|
||||
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Hicap models
|
||||
rpc refreshHicapModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns LiteLLM models
|
||||
rpc refreshLiteLlmModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Subscribe to OpenRouter models updates
|
||||
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
|
||||
// Subscribe to LiteLLM models updates
|
||||
rpc subscribeToLiteLlmModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
|
||||
// Updates API configuration (legacy - uses combined configuration)
|
||||
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
|
||||
// Updates API configuration (new - uses separate options and secrets)
|
||||
@@ -98,6 +102,7 @@ message OpenRouterModelInfo {
|
||||
repeated ModelTier tiers = 12;
|
||||
optional string name = 13;
|
||||
optional double temperature = 14;
|
||||
optional bool supports_reasoning = 15;
|
||||
}
|
||||
|
||||
// Shared response message for model information
|
||||
|
||||
@@ -225,6 +225,7 @@ message Settings {
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 131;
|
||||
optional string act_mode_aihubmix_model_id = 132;
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 133;
|
||||
optional bool hooks_enabled = 134;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
|
||||
@@ -129,6 +129,7 @@ function createHandlerForProvider(
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
geminiApiKey: options.geminiApiKey,
|
||||
geminiBaseUrl: options.geminiBaseUrl,
|
||||
thinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
|
||||
ulid: options.ulid,
|
||||
})
|
||||
case "openai":
|
||||
|
||||
@@ -63,6 +63,12 @@ export class AnthropicHandler implements ApiHandler {
|
||||
|
||||
switch (modelId) {
|
||||
// 'latest' alias does not support cache_control
|
||||
case "claude-haiku-4-5@20251001":
|
||||
case "claude-sonnet-4-5@20250929":
|
||||
case "claude-sonnet-4@20250514":
|
||||
case "claude-opus-4-5@20251101":
|
||||
case "claude-opus-4-1@20250805":
|
||||
case "claude-opus-4@20250514":
|
||||
case "claude-haiku-4-5-20251001":
|
||||
case "claude-sonnet-4-5-20250929:1m":
|
||||
case "claude-sonnet-4-5-20250929":
|
||||
@@ -70,6 +76,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
case "claude-3-5-sonnet-20241022":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
case "claude-opus-4-5-20251101":
|
||||
case "claude-opus-4-20250514":
|
||||
case "claude-opus-4-1-20250805":
|
||||
case "claude-3-opus-20240229":
|
||||
|
||||
@@ -19,6 +19,16 @@ type AskSageRequest = {
|
||||
}[]
|
||||
model: string
|
||||
dataset: "none"
|
||||
usage: boolean
|
||||
}
|
||||
|
||||
type AskSageUsage = {
|
||||
model_tokens: {
|
||||
completion_tokens: number
|
||||
prompt_tokens: number
|
||||
total_tokens: number
|
||||
}
|
||||
asksage_tokens: number
|
||||
}
|
||||
|
||||
type AskSageResponse = {
|
||||
@@ -28,6 +38,18 @@ type AskSageResponse = {
|
||||
response: string
|
||||
// Generated response message
|
||||
message: string
|
||||
// whether embedding & vector systems are down
|
||||
embedding_down: boolean
|
||||
vectors_down: boolean
|
||||
// references if dataset is not none
|
||||
references: string
|
||||
type: string
|
||||
added_obj: any
|
||||
tool_calls: any
|
||||
// usage metrics
|
||||
usage: AskSageUsage | null
|
||||
tool_responses: any[]
|
||||
tool_calls_unified: any[]
|
||||
}
|
||||
|
||||
export class AskSageHandler implements ApiHandler {
|
||||
@@ -50,7 +72,6 @@ export class AskSageHandler implements ApiHandler {
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
|
||||
try {
|
||||
const model = this.getModel()
|
||||
|
||||
// Transform messages into AskSageRequest format
|
||||
const formattedMessages = messages.map((msg) => {
|
||||
const content = Array.isArray(msg.content)
|
||||
@@ -68,6 +89,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
message: formattedMessages,
|
||||
model: model.id,
|
||||
dataset: "none",
|
||||
usage: true,
|
||||
}
|
||||
|
||||
// Make request to AskSage API
|
||||
@@ -91,15 +113,72 @@ export class AskSageHandler implements ApiHandler {
|
||||
throw new Error("No content in AskSage response")
|
||||
}
|
||||
|
||||
// Return entire response as a single chunk since streaming is not supported
|
||||
// Yield tool responses if they exist
|
||||
if (result.tool_responses && result.tool_responses.length > 0) {
|
||||
for (const toolResponse of result.tool_responses) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: `[Tool Response: ${JSON.stringify(toolResponse)}]\n`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Yield the main response text
|
||||
yield {
|
||||
type: "text",
|
||||
text: result.message,
|
||||
}
|
||||
|
||||
// Yield usage information if available
|
||||
if (result.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: result.usage.model_tokens.prompt_tokens,
|
||||
outputTokens: result.usage.model_tokens.completion_tokens,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
totalCost: result.usage.asksage_tokens, // Cost = Consumed AskSage tokens
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`AskSage request failed: ${error.message}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getApiStreamUsage() {
|
||||
if (!this.apiKey) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiUrl}/count-monthly-tokens`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-access-tokens": this.apiKey,
|
||||
},
|
||||
body: JSON.stringify({ app_name: "asksage" }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch AskSage usage", await response.text())
|
||||
return undefined
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const usedTokens = data.response as number
|
||||
|
||||
return {
|
||||
type: "usage" as const,
|
||||
inputTokens: usedTokens,
|
||||
outputTokens: 0,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching AskSage usage:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { isAnthropicModelId } from "@/utils/model-utils"
|
||||
@@ -19,6 +20,14 @@ interface LiteLlmHandlerOptions extends CommonApiHandlerOptions {
|
||||
ulid?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended chat completion parameters that include LiteLLM-specific options
|
||||
* not present in the standard OpenAI SDK types
|
||||
*/
|
||||
interface LiteLlmChatCompletionCreateParams extends OpenAI.Chat.ChatCompletionCreateParamsStreaming {
|
||||
drop_params?: boolean
|
||||
}
|
||||
|
||||
export interface LiteLlmModelInfoResponse {
|
||||
data: Array<{
|
||||
model_name: string
|
||||
@@ -37,6 +46,54 @@ export interface LiteLlmModelInfoResponse {
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* Exported utility function to fetch LiteLLM model info
|
||||
* @param baseUrl The base URL for the LiteLLM API
|
||||
* @param apiKey The API key for authentication
|
||||
* @returns The model info response or undefined if fetch fails
|
||||
*/
|
||||
export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): Promise<LiteLlmModelInfoResponse | undefined> {
|
||||
// Handle base URLs that already include /v1 to avoid double /v1/v1/
|
||||
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`
|
||||
const url = `${normalizedBaseUrl}/model/info`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"x-litellm-api-key": apiKey,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data: LiteLlmModelInfoResponse = await response.json()
|
||||
return data
|
||||
} else {
|
||||
console.error("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
// Try with Authorization header instead
|
||||
const retryResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (retryResponse.ok) {
|
||||
const data: LiteLlmModelInfoResponse = await retryResponse.json()
|
||||
return data
|
||||
} else {
|
||||
console.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
throw new Error(`Failed to fetch LiteLLM model info: ${retryResponse.statusText}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching LiteLLM model info:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export class LiteLlmHandler implements ApiHandler {
|
||||
private options: LiteLlmHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
@@ -84,49 +141,14 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const client = this.ensureClient()
|
||||
// Handle base URLs that already include /v1 to avoid double /v1/v1/
|
||||
const baseUrl = client.baseURL.endsWith("/v1") ? client.baseURL : `${client.baseURL}/v1`
|
||||
const url = `${baseUrl}/model/info`
|
||||
const data = await fetchLiteLlmModelsInfo(client.baseURL, this.options.liteLlmApiKey || "")
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"x-litellm-api-key": this.options.liteLlmApiKey || "",
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data: LiteLlmModelInfoResponse = await response.json()
|
||||
this.modelInfoCache = data
|
||||
this.modelInfoCacheTimestamp = now
|
||||
return data
|
||||
} else {
|
||||
console.warn("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
// Try with Authorization header instead
|
||||
const retryResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
Authorization: `Bearer ${this.options.liteLlmApiKey || ""}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (retryResponse.ok) {
|
||||
const data: LiteLlmModelInfoResponse = await retryResponse.json()
|
||||
this.modelInfoCache = data
|
||||
this.modelInfoCacheTimestamp = now
|
||||
return data
|
||||
} else {
|
||||
console.warn("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error fetching LiteLLM model info:", error)
|
||||
return undefined
|
||||
if (data) {
|
||||
this.modelInfoCache = data
|
||||
this.modelInfoCacheTimestamp = now
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
private async getModelCostInfo(publicModelName: string): Promise<{
|
||||
@@ -186,6 +208,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
|
||||
const formattedMessages = convertToOpenAiMessages(messages)
|
||||
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam | Anthropic.Messages.TextBlockParam = {
|
||||
role: "system",
|
||||
@@ -193,23 +216,23 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
}
|
||||
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
|
||||
const isOminiModel = modelId.includes("o1-mini") || modelId.includes("o3-mini") || modelId.includes("o4-mini")
|
||||
const isCodexModel = modelId.toLowerCase().includes("codex")
|
||||
|
||||
// Configuration for extended thinking
|
||||
const budgetTokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = budgetTokens !== 0
|
||||
const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined
|
||||
|
||||
let temperature: number | undefined = this.options.liteLlmModelInfo?.temperature ?? 0
|
||||
let temperature: number | undefined = this.options.liteLlmModelInfo?.temperature ?? 1
|
||||
|
||||
if ((isOminiModel || isAnthropicModelId(modelId)) && reasoningOn) {
|
||||
temperature = undefined // OAI omni and Anthropic extended thinking mode doesn't support temperature
|
||||
}
|
||||
|
||||
const modelInfo = await this.modelInfo(modelId)
|
||||
// Automatically enable caching if the model supports it
|
||||
const cacheControl =
|
||||
this.options.liteLlmUsePromptCache && Boolean(modelInfo?.model_info.supports_prompt_caching)
|
||||
? { cache_control: { type: "ephemeral" } }
|
||||
: undefined
|
||||
(modelInfo?.model_info.supports_prompt_caching ?? false) ? { cache_control: { type: "ephemeral" } } : undefined
|
||||
|
||||
if (cacheControl) {
|
||||
// Add cache_control to system message if enabled
|
||||
@@ -277,10 +300,11 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
messages: [systemMessage, ...enhancedMessages],
|
||||
temperature,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
drop_params: true,
|
||||
...(!isCodexModel && { stream_options: { include_usage: true } }), // Codex models are only on the responses api, which doesn't take the stream_options parameter. we will need to migrate to the responses api for this to work
|
||||
...(thinkingConfig && { thinking: thinkingConfig }), // Add thinking configuration when applicable
|
||||
...(this.options.ulid && { litellm_session_id: `cline-${this.options.ulid}` }), // Add session ID for LiteLLM tracking
|
||||
})
|
||||
} as LiteLlmChatCompletionCreateParams)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
@@ -344,9 +368,17 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel() {
|
||||
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
|
||||
|
||||
// Try to get model info from StateManager cache first
|
||||
const cachedModelInfo = StateManager.get().getModelInfo("liteLlm", modelId)
|
||||
|
||||
// Fall back to provided model info or defaults if not in cache
|
||||
const modelInfo = cachedModelInfo || liteLlmModelInfoSaneDefaults
|
||||
|
||||
return {
|
||||
id: this.options.liteLlmModelId || liteLlmDefaultModelId,
|
||||
info: this.options.liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
|
||||
id: modelId,
|
||||
info: modelInfo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import { type Config, type Message, Ollama } from "ollama"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOllamaMessages } from "../transform/ollama-format"
|
||||
@@ -30,6 +31,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
try {
|
||||
const clientOptions: Partial<Config> = {
|
||||
host: this.options.ollamaBaseUrl,
|
||||
fetch,
|
||||
}
|
||||
|
||||
// Add API key if provided (for Ollama cloud or authenticated instances)
|
||||
|
||||
@@ -312,13 +312,12 @@ namespace Gemini {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare Gemini request payload with thinking configuration and implicit caching support
|
||||
* Prepare Gemini request payload with implicit caching support
|
||||
*/
|
||||
export function prepareRequestPayload(
|
||||
systemPrompt: string,
|
||||
messages: ClineStorageMessage[],
|
||||
model: { id: SapAiCoreModelId; info: ModelInfo },
|
||||
thinkingBudgetTokens?: number,
|
||||
): any {
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
|
||||
@@ -337,17 +336,15 @@ namespace Gemini {
|
||||
},
|
||||
}
|
||||
|
||||
// Add thinking config if the model supports it and budget is provided
|
||||
const thinkingBudget = thinkingBudgetTokens ?? 0
|
||||
const _maxBudget = model.info.thinkingConfig?.maxBudget ?? 0
|
||||
|
||||
if (thinkingBudget > 0 && model.info.thinkingConfig) {
|
||||
// Add thinking configuration to the payload
|
||||
;(payload as any).thinkingConfig = {
|
||||
thinkingBudget: thinkingBudget,
|
||||
includeThoughts: true,
|
||||
}
|
||||
}
|
||||
// Note: SAP AI Core's Gemini deployment doesn't support thinkingConfig yet
|
||||
// Commenting out until support is added
|
||||
// const thinkingBudget = thinkingBudgetTokens ?? 0
|
||||
// if (thinkingBudget > 0 && model.info.thinkingConfig) {
|
||||
// ;(payload as any).thinkingConfig = {
|
||||
// thinkingBudget: thinkingBudget,
|
||||
// includeThoughts: true,
|
||||
// }
|
||||
// }
|
||||
|
||||
return payload
|
||||
}
|
||||
@@ -363,6 +360,21 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a chunk from the stream to a UTF-8 string
|
||||
* Handles Buffer, string, and byte array formats
|
||||
*/
|
||||
private chunkToString(chunk: any): string {
|
||||
if (Buffer.isBuffer(chunk)) {
|
||||
return chunk.toString("utf-8")
|
||||
} else if (typeof chunk === "string") {
|
||||
return chunk
|
||||
} else {
|
||||
// Handle comma-separated byte values or other array-like formats
|
||||
return Buffer.from(chunk).toString("utf-8")
|
||||
}
|
||||
}
|
||||
|
||||
private validateCredentials(): void {
|
||||
if (
|
||||
!this.options.sapAiCoreClientId ||
|
||||
@@ -598,10 +610,12 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
const formattedMessages = Bedrock.formatMessagesForConverseAPI(messages)
|
||||
|
||||
// Get message indices for caching
|
||||
const userMsgIndices = messages.reduce(
|
||||
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
|
||||
[] as number[],
|
||||
)
|
||||
const userMsgIndices = messages.reduce((acc, msg, index) => {
|
||||
if (msg.role === "user") {
|
||||
acc.push(index)
|
||||
}
|
||||
return acc
|
||||
}, [] as number[])
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
|
||||
@@ -693,7 +707,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
} else if (geminiModels.includes(model.id)) {
|
||||
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/models/${model.id}:streamGenerateContent`
|
||||
payload = Gemini.prepareRequestPayload(systemPrompt, messages, model, this.options.thinkingBudgetTokens)
|
||||
payload = Gemini.prepareRequestPayload(systemPrompt, messages, model)
|
||||
} else {
|
||||
throw new Error(`Unsupported model: ${model.id}`)
|
||||
}
|
||||
@@ -747,18 +761,54 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
} else {
|
||||
yield* this.streamCompletion(response.data, model)
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.error("Error status:", error.response.status)
|
||||
console.error("Error data:", error.response.data)
|
||||
console.error("Error headers:", error.response.headers)
|
||||
|
||||
if (error.response.status === 404) {
|
||||
console.error("404 Error reason:", error.response.data)
|
||||
throw new Error(`404 Not Found: ${error.response.data}`)
|
||||
// Handle error data - need to read stream if responseType was 'stream'
|
||||
let errorMessage = "Unknown error"
|
||||
if (error.response.data) {
|
||||
try {
|
||||
// If it's a stream, read it
|
||||
if (
|
||||
typeof error.response.data.on === "function" ||
|
||||
typeof error.response.data[Symbol.asyncIterator] === "function"
|
||||
) {
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of error.response.data) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
||||
}
|
||||
const fullData = Buffer.concat(chunks).toString("utf-8")
|
||||
errorMessage = fullData
|
||||
try {
|
||||
// Try to parse as JSON for better formatting
|
||||
const jsonError = JSON.parse(fullData)
|
||||
errorMessage = JSON.stringify(jsonError, null, 2)
|
||||
} catch {
|
||||
// Keep as plain text if not JSON
|
||||
}
|
||||
} else if (typeof error.response.data === "string") {
|
||||
errorMessage = error.response.data
|
||||
} else if (typeof error.response.data === "object") {
|
||||
errorMessage = JSON.stringify(error.response.data, null, 2)
|
||||
}
|
||||
console.error("Error data:", errorMessage)
|
||||
} catch (e) {
|
||||
console.error("Failed to read error data:", e)
|
||||
console.error("Raw error data:", error.response.data)
|
||||
}
|
||||
}
|
||||
|
||||
if (error.response.status === 404) {
|
||||
throw new Error(`404 Not Found: ${errorMessage}`)
|
||||
} else if (error.response.status === 400) {
|
||||
throw new Error(`400 Bad Request: ${errorMessage}`)
|
||||
}
|
||||
|
||||
throw new Error(`HTTP ${error.response.status}: ${errorMessage}`)
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
console.error("Error request:", error.request)
|
||||
@@ -768,8 +818,6 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
console.error("Error message:", error.message)
|
||||
throw new Error(`Error setting up request: ${error.message}`)
|
||||
}
|
||||
|
||||
throw new Error("Failed to create message")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -781,7 +829,8 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
const lines = chunk.toString().split("\n").filter(Boolean)
|
||||
const chunkStr = this.chunkToString(chunk)
|
||||
const lines = chunkStr.split("\n").filter(Boolean)
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const jsonData = line.slice(6)
|
||||
@@ -840,7 +889,8 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
try {
|
||||
// Iterate over the stream and process each chunk
|
||||
for await (const chunk of stream) {
|
||||
const lines = chunk.toString().split("\n").filter(Boolean)
|
||||
const chunkStr = this.chunkToString(chunk)
|
||||
const lines = chunkStr.split("\n").filter(Boolean)
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
@@ -913,7 +963,8 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
const lines = chunk.toString().split("\n").filter(Boolean)
|
||||
const chunkStr = this.chunkToString(chunk)
|
||||
const lines = chunkStr.split("\n").filter(Boolean)
|
||||
for (const line of lines) {
|
||||
if (line.trim() === "data: [DONE]") {
|
||||
// End of stream, yield final usage
|
||||
@@ -985,7 +1036,8 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
const lines = chunk.toString().split("\n").filter(Boolean)
|
||||
const chunkStr = this.chunkToString(chunk)
|
||||
const lines = chunkStr.split("\n").filter(Boolean)
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const jsonData = line.slice(6)
|
||||
|
||||
@@ -18,6 +18,7 @@ interface VertexHandlerOptions extends CommonApiHandlerOptions {
|
||||
geminiApiKey?: string
|
||||
geminiBaseUrl?: string
|
||||
ulid?: string
|
||||
thinkingLevel?: string
|
||||
}
|
||||
|
||||
export class VertexHandler implements ApiHandler {
|
||||
@@ -89,12 +90,16 @@ export class VertexHandler implements ApiHandler {
|
||||
modelId.includes("haiku-4-5")) &&
|
||||
budget_tokens !== 0
|
||||
)
|
||||
// Tools are available only when native tools are enabled.
|
||||
const nativeToolsOn = tools?.length ? tools?.length > 0 : false
|
||||
|
||||
let stream
|
||||
|
||||
switch (modelId) {
|
||||
case "claude-haiku-4-5@20251001":
|
||||
case "claude-sonnet-4-5@20250929":
|
||||
case "claude-sonnet-4@20250514":
|
||||
case "claude-opus-4-5@20251101":
|
||||
case "claude-opus-4-1@20250805":
|
||||
case "claude-opus-4@20250514":
|
||||
case "claude-3-7-sonnet@20250219":
|
||||
@@ -103,6 +108,7 @@ export class VertexHandler implements ApiHandler {
|
||||
case "claude-3-5-haiku@20241022":
|
||||
case "claude-3-opus@20240229":
|
||||
case "claude-3-haiku@20240307": {
|
||||
const anthropicMessages = sanitizeAnthropicMessages(messages, true)
|
||||
stream = await clientAnthropic.beta.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
@@ -116,14 +122,15 @@ export class VertexHandler implements ApiHandler {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
messages: sanitizeAnthropicMessages(messages, true),
|
||||
messages: anthropicMessages,
|
||||
stream: true,
|
||||
tools: tools?.length ? (tools as AnthropicTool[]) : undefined,
|
||||
tools: nativeToolsOn ? (tools as AnthropicTool[]) : undefined,
|
||||
// tool_choice options:
|
||||
// - none: disables tool use, even if tools are provided. Claude will not call any tools.
|
||||
// - auto: allows Claude to decide whether to call any provided tools or not. This is the default value when tools are provided.
|
||||
// - any: tells Claude that it must use one of the provided tools, but doesn’t force a particular tool.
|
||||
tool_choice: tools ? { type: "any" } : undefined,
|
||||
// NOTE: Forcing tool use when tools are provided will result in error when thinking is also enabled.
|
||||
tool_choice: nativeToolsOn && !reasoningOn ? { type: "any" } : undefined,
|
||||
},
|
||||
{
|
||||
headers: {},
|
||||
@@ -219,6 +226,13 @@ export class VertexHandler implements ApiHandler {
|
||||
break
|
||||
case "content_block_delta":
|
||||
switch (chunk.delta.type) {
|
||||
case "signature_delta":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: "",
|
||||
signature: chunk.delta.signature,
|
||||
}
|
||||
break
|
||||
case "thinking_delta":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
|
||||
@@ -44,6 +44,7 @@ export async function createOpenRouterStream(
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
case "anthropic/claude-4.5-sonnet": // OpenRouter accidentally included this in model list for a brief moment, and users may be using this model id. And to support prompt caching, we need to add it here.
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.5":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
@@ -107,6 +108,7 @@ export async function createOpenRouterStream(
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
case "anthropic/claude-4.5-sonnet":
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.5":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
@@ -151,6 +153,7 @@ export async function createOpenRouterStream(
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
case "anthropic/claude-4.5-sonnet":
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.5":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
|
||||
@@ -6,7 +6,8 @@ export function checkContextWindowExceededError(error: unknown): boolean {
|
||||
checkIsOpenRouterContextWindowError(error) ||
|
||||
checkIsAnthropicContextWindowError(error) ||
|
||||
checkIsCerebrasContextWindowError(error) ||
|
||||
checkIsBedrockContextWindowError(error)
|
||||
checkIsBedrockContextWindowError(error) ||
|
||||
checkIsVercelContextWindowError(error)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -113,3 +114,58 @@ function checkIsBedrockContextWindowError(error: any): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function checkIsVercelContextWindowError(error: any): boolean {
|
||||
try {
|
||||
const status = error?.status ?? error?.error?.param?.statusCode ?? error?.statusCode
|
||||
|
||||
// Check for explicit context_length_exceeded code (OpenAI streaming errors)
|
||||
const errorCode = error?.error?.error?.code
|
||||
if (errorCode === "context_length_exceeded") {
|
||||
return true
|
||||
}
|
||||
|
||||
const messages: string[] = [
|
||||
error?.message,
|
||||
error?.error?.message,
|
||||
error?.error?.param?.message,
|
||||
error?.error?.param?.error,
|
||||
error?.error?.error?.message,
|
||||
error?.error?.value?.error_message, // Alibaba Qwen validation errors
|
||||
].filter((msg) => msg != null)
|
||||
|
||||
if (messages.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Must be a 400 error OR have 400 embedded in error_message (Alibaba Qwen case)
|
||||
const hasValidStatus = String(status) === "400"
|
||||
const errorMessage = error?.error?.value?.error_message
|
||||
const has400InMessage =
|
||||
errorMessage &&
|
||||
typeof errorMessage === "string" &&
|
||||
(errorMessage.includes('"code":400') || errorMessage.includes('"code": 400'))
|
||||
|
||||
if (!hasValidStatus && !has400InMessage) {
|
||||
return false
|
||||
}
|
||||
|
||||
const CONTEXT_ERROR_PATTERNS = [
|
||||
/input is too long/i,
|
||||
/input token count exceeds.*maximum.*tokens? allowed/i,
|
||||
/input exceeds.*context window/i,
|
||||
/requested input length.*exceeds.*maximum input length/i,
|
||||
/prompt is too long.*tokens?\s*>\s*\d+\s*maximum/i,
|
||||
/\bcontext\s*(?:length|window)\b.*exceed/i,
|
||||
/\bmaximum\s*context\b/i,
|
||||
/\b(?:input\s*)?tokens?\s*exceed/i,
|
||||
/too\s*many\s*tokens/i,
|
||||
] as const
|
||||
|
||||
return messages
|
||||
.map((msg) => String(msg).toLowerCase())
|
||||
.some((message) => CONTEXT_ERROR_PATTERNS.some((pattern) => pattern.test(message)))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,18 @@ export interface ModelMetadataEntry {
|
||||
mode: string
|
||||
}
|
||||
|
||||
export interface EnvironmentMetadataEntry {
|
||||
ts: number
|
||||
os_name: string
|
||||
os_version: string
|
||||
os_arch: string
|
||||
host_name: string
|
||||
host_version: string
|
||||
cline_version: string
|
||||
}
|
||||
|
||||
export interface TaskMetadata {
|
||||
files_in_context: FileMetadataEntry[]
|
||||
model_usage: ModelMetadataEntry[]
|
||||
environment_history: EnvironmentMetadataEntry[]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { collectEnvironmentMetadata, getTaskMetadata, saveTaskMetadata } from "@core/storage/disk"
|
||||
import type { EnvironmentMetadataEntry } from "./ContextTrackerTypes"
|
||||
|
||||
export class EnvironmentContextTracker {
|
||||
readonly taskId: string
|
||||
|
||||
constructor(taskId: string) {
|
||||
this.taskId = taskId
|
||||
}
|
||||
|
||||
async recordEnvironment() {
|
||||
const metadata = await getTaskMetadata(this.taskId)
|
||||
|
||||
if (!metadata.environment_history) {
|
||||
metadata.environment_history = []
|
||||
}
|
||||
|
||||
const currentEnv = await collectEnvironmentMetadata()
|
||||
const currentEnvWithTs: EnvironmentMetadataEntry = {
|
||||
ts: Date.now(),
|
||||
...currentEnv,
|
||||
}
|
||||
|
||||
const lastEntry = metadata.environment_history[metadata.environment_history.length - 1]
|
||||
if (lastEntry && this.isSameEnvironment(lastEntry, currentEnvWithTs)) {
|
||||
return // No change, don't add duplicate
|
||||
}
|
||||
|
||||
metadata.environment_history.push(currentEnvWithTs)
|
||||
await saveTaskMetadata(this.taskId, metadata)
|
||||
}
|
||||
|
||||
private isSameEnvironment(a: EnvironmentMetadataEntry, b: EnvironmentMetadataEntry): boolean {
|
||||
return (
|
||||
a.os_name === b.os_name &&
|
||||
a.os_version === b.os_version &&
|
||||
a.os_arch === b.os_arch &&
|
||||
a.host_name === b.host_name &&
|
||||
a.host_version === b.host_version &&
|
||||
a.cline_version === b.cline_version
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ describe("FileContextTracker", () => {
|
||||
chokidarWatchStub = sandbox.stub(chokidar, "watch").returns(mockFileSystemWatcher as any)
|
||||
|
||||
// Mock disk module functions
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [] }
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [], environment_history: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("ModelContextTracker", () => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock disk module functions
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [] }
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [], environment_history: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
|
||||
|
||||
|
||||
@@ -1,16 +1,64 @@
|
||||
import { StateManager } from "@core/storage/StateManager"
|
||||
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
|
||||
import { Empty, StringRequest } from "@shared/proto/cline/common"
|
||||
import { REMOTE_URI_SCHEME } from "@shared/remote-config/constants"
|
||||
import { writeFile } from "@utils/fs"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Opens a file in the editor
|
||||
* @param controller The controller instance
|
||||
* @param request The request message containing the file path in the 'value' field
|
||||
* @param request The request message containing the file path in the 'value' field.
|
||||
* Supports special URI format for remote rules/workflows:
|
||||
* - remote://rule/{ruleName}
|
||||
* - remote://workflow/{workflowName}
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function openFile(_controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
if (request.value) {
|
||||
openFileIntegration(request.value)
|
||||
// Check for remote:// prefix for remote rules/workflows
|
||||
if (request.value.startsWith(REMOTE_URI_SCHEME)) {
|
||||
await openRemoteFile(request.value)
|
||||
} else {
|
||||
await openFileIntegration(request.value)
|
||||
}
|
||||
}
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a remote rule or workflow file by creating a temp file with its contents
|
||||
* @param uri The remote URI in format: remote://rule/{name} or remote://workflow/{name}
|
||||
*/
|
||||
async function openRemoteFile(uri: string): Promise<void> {
|
||||
// Parse: remote://rule/{name} or remote://workflow/{name}
|
||||
const match = uri.match(/^remote:\/\/(rule|workflow)\/(.+)$/)
|
||||
if (!match) {
|
||||
throw new Error(`Invalid remote file URI: ${uri}`)
|
||||
}
|
||||
|
||||
const [, type, name] = match
|
||||
const remoteConfig = StateManager.get().getRemoteConfigSettings()
|
||||
|
||||
// Look up content based on type
|
||||
const items = type === "rule" ? remoteConfig.remoteGlobalRules : remoteConfig.remoteGlobalWorkflows
|
||||
const item = items?.find((r) => r.name === name)
|
||||
|
||||
if (!item?.contents) {
|
||||
throw new Error(`Remote ${type} not found: ${name}`)
|
||||
}
|
||||
|
||||
// Create temp file with read-only header comment
|
||||
const typeLabel = type === "rule" ? "rule" : "workflow"
|
||||
const header = `# ⚠️ READ-ONLY: This ${typeLabel} is managed by your organization.\n# Changes made here will not be saved.\n\n`
|
||||
const content = header + item.contents
|
||||
|
||||
// Sanitize the name for use in filename (replace invalid characters)
|
||||
const sanitizedName = name.replace(/[<>:"/\\|?*]/g, "_")
|
||||
const tempPath = path.join(os.tmpdir(), `cline-remote-${type}-${sanitizedName}.md`)
|
||||
|
||||
await writeFile(tempPath, content)
|
||||
await openFileIntegration(tempPath)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { detectWorkspaceRoots } from "@core/workspace/detection"
|
||||
import { setupWorkspaceManager } from "@core/workspace/setup"
|
||||
import type { WorkspaceRootManager } from "@core/workspace/WorkspaceRootManager"
|
||||
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
|
||||
import { downloadTask } from "@integrations/misc/export-markdown"
|
||||
import { ClineAccountService } from "@services/account/ClineAccountService"
|
||||
import { McpHub } from "@services/mcp/McpHub"
|
||||
import type { ApiProvider, ModelInfo } from "@shared/api"
|
||||
@@ -20,6 +19,7 @@ import type { UserInfo } from "@shared/UserInfo"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import axios from "axios"
|
||||
import fs from "fs/promises"
|
||||
import open from "open"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import * as path from "path"
|
||||
import type { FolderLockWithRetryResult } from "src/core/locks/types"
|
||||
@@ -251,7 +251,14 @@ export class Controller {
|
||||
historyItem?: HistoryItem,
|
||||
taskSettings?: Partial<Settings>,
|
||||
) {
|
||||
await fetchRemoteConfig(this)
|
||||
// Fire-and-forget: We intentionally don't await fetchRemoteConfig here.
|
||||
// Remote config is already fetched in startRemoteConfigTimer() which runs in the constructor,
|
||||
// so enterprise policies (yoloModeAllowed, allowedMCPServers, etc.) are already applied.
|
||||
// This call just ensures we have the latest state, but we shouldn't block the UI for it.
|
||||
// getGlobalSettingsKey() reads from remoteConfigCache on each call, so any updates
|
||||
// will apply as soon as this fetch completes. The function also calls postStateToWebview()
|
||||
// when done and catches all errors internally.
|
||||
fetchRemoteConfig(this)
|
||||
|
||||
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||
|
||||
@@ -824,8 +831,9 @@ export class Controller {
|
||||
}
|
||||
|
||||
async exportTaskWithId(id: string) {
|
||||
const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
|
||||
await downloadTask(historyItem.ts, apiConversationHistory)
|
||||
const { taskDirPath } = await this.getTaskWithId(id)
|
||||
console.log(`[EXPORT] Opening task directory: ${taskDirPath}`)
|
||||
await open(taskDirPath)
|
||||
}
|
||||
|
||||
async deleteTaskFromState(id: string) {
|
||||
@@ -989,10 +997,7 @@ export class Controller {
|
||||
remoteConfigSettings: this.stateManager.getRemoteConfigSettings(),
|
||||
lastDismissedCliBannerVersion,
|
||||
subagentsEnabled,
|
||||
nativeToolCallSetting: {
|
||||
user: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
|
||||
featureFlag: featureFlagsService.getNativeToolCallEnabled(),
|
||||
},
|
||||
nativeToolCallSetting: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { ModelInfo } from "@shared/api"
|
||||
import { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
|
||||
import { fetchLiteLlmModelsInfo } from "@/core/api/providers/litellm"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { toProtobufModels } from "@/shared/proto-conversions/models/typeConversion"
|
||||
import { sendLiteLlmModelsEvent } from "./subscribeToLiteLlmModels"
|
||||
|
||||
/**
|
||||
* Core function: Refreshes the LiteLLM models and returns application types
|
||||
* @param controller The controller instance
|
||||
* @returns Record of model ID to ModelInfo (application types)
|
||||
*/
|
||||
export async function refreshLiteLlmModels(): Promise<Record<string, ModelInfo>> {
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
|
||||
const stateManager = StateManager.get()
|
||||
|
||||
try {
|
||||
// Get the LiteLLM configuration
|
||||
const apiConfiguration = stateManager.getApiConfiguration()
|
||||
const baseUrl = apiConfiguration.liteLlmBaseUrl || ""
|
||||
const apiKey = apiConfiguration.liteLlmApiKey
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error("LiteLLM API key is not configured or is invalid")
|
||||
}
|
||||
|
||||
// Use the shared utility function to fetch model info
|
||||
const data = await fetchLiteLlmModelsInfo(baseUrl, apiKey)
|
||||
|
||||
if (data?.data) {
|
||||
for (const rawModel of data.data) {
|
||||
const modelInfo: ModelInfo = {
|
||||
name: rawModel.model_name,
|
||||
maxTokens: rawModel.model_info?.max_output_tokens ?? rawModel.model_info?.max_tokens ?? 4096,
|
||||
contextWindow: rawModel.model_info?.max_input_tokens ?? rawModel.model_info?.max_tokens ?? 8192,
|
||||
supportsImages: rawModel.model_info?.supports_vision ?? false,
|
||||
supportsPromptCache: rawModel.model_info?.supports_prompt_caching ?? false,
|
||||
supportsReasoning: rawModel.model_info?.supports_reasoning ?? false,
|
||||
inputPrice: rawModel.model_info?.input_cost_per_token
|
||||
? rawModel.model_info.input_cost_per_token * 1_000_000
|
||||
: 0,
|
||||
outputPrice: rawModel.model_info?.output_cost_per_token
|
||||
? rawModel.model_info.output_cost_per_token * 1_000_000
|
||||
: 0,
|
||||
cacheWritesPrice: rawModel.model_info?.cache_creation_input_token_cost
|
||||
? rawModel.model_info.cache_creation_input_token_cost * 1_000_000
|
||||
: undefined,
|
||||
cacheReadsPrice: rawModel.model_info?.cache_read_input_token_cost
|
||||
? rawModel.model_info.cache_read_input_token_cost * 1_000_000
|
||||
: undefined,
|
||||
description: undefined,
|
||||
}
|
||||
|
||||
models[rawModel.model_name] = modelInfo
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching LiteLLM models:", error)
|
||||
throw error
|
||||
}
|
||||
|
||||
// Store in StateManager's in-memory cache
|
||||
StateManager.get().setModelsCache("liteLlm", models)
|
||||
|
||||
// Send event to subscribers
|
||||
try {
|
||||
await sendLiteLlmModelsEvent(
|
||||
OpenRouterCompatibleModelInfo.create({
|
||||
models: toProtobufModels(models),
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending LiteLLM models event:", error)
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
|
||||
import { toProtobufModels } from "../../../shared/proto-conversions/models/typeConversion"
|
||||
import type { Controller } from "../index"
|
||||
import { refreshLiteLlmModels } from "./refreshLiteLlmModels"
|
||||
|
||||
/**
|
||||
* Refreshes LiteLLM models and returns protobuf types for gRPC
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request (unused but required for gRPC signature)
|
||||
* @returns OpenRouterCompatibleModelInfo with protobuf types
|
||||
*/
|
||||
export async function refreshLiteLlmModelsRpc(
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const models = await refreshLiteLlmModels()
|
||||
return OpenRouterCompatibleModelInfo.create({
|
||||
models: toProtobufModels(models),
|
||||
})
|
||||
}
|
||||
@@ -134,6 +134,11 @@ export async function refreshOpenRouterModels(controller: Controller): Promise<R
|
||||
modelInfo.cacheWritesPrice = 3.75
|
||||
modelInfo.cacheReadsPrice = 0.3
|
||||
break
|
||||
case "anthropic/claude-opus-4.5":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 6.25
|
||||
modelInfo.cacheReadsPrice = 0.5
|
||||
break
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
modelInfo.supportsPromptCache = true
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import { Controller } from "../index"
|
||||
|
||||
// Keep track of active LiteLLM models subscriptions
|
||||
const activeLiteLlmModelsSubscriptions = new Set<StreamingResponseHandler<OpenRouterCompatibleModelInfo>>()
|
||||
|
||||
/**
|
||||
* Subscribe to LiteLLM models events
|
||||
* @param controller The controller instance
|
||||
* @param request The empty request
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request (passed by the gRPC handler)
|
||||
*/
|
||||
export async function subscribeToLiteLlmModels(
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<OpenRouterCompatibleModelInfo>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
// Add this subscription to the active subscriptions
|
||||
activeLiteLlmModelsSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeLiteLlmModelsSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
if (requestId) {
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "liteLlmModels_subscription" }, responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a LiteLLM models event to all active subscribers
|
||||
* @param models The LiteLLM models to send
|
||||
*/
|
||||
export async function sendLiteLlmModelsEvent(models: OpenRouterCompatibleModelInfo): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(activeLiteLlmModelsSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
await responseStream(
|
||||
models,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending LiteLLM models event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
activeLiteLlmModelsSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { sendMcpMarketplaceCatalogEvent } from "../mcp/subscribeToMcpMarketplace
|
||||
import { refreshBasetenModels } from "../models/refreshBasetenModels"
|
||||
import { refreshGroqModels } from "../models/refreshGroqModels"
|
||||
import { refreshHicapModels } from "../models/refreshHicapModels"
|
||||
import { refreshLiteLlmModels } from "../models/refreshLiteLlmModels"
|
||||
import { refreshOpenRouterModels } from "../models/refreshOpenRouterModels"
|
||||
import { sendOpenRouterModelsEvent } from "../models/subscribeToOpenRouterModels"
|
||||
|
||||
@@ -194,6 +195,12 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
|
||||
}
|
||||
})
|
||||
|
||||
const liteLlmBaseUrl = controller.stateManager.getGlobalSettingsKey("liteLlmBaseUrl")
|
||||
const liteLlmApiKey = controller.stateManager.getSecretKey("liteLlmApiKey")
|
||||
if (liteLlmBaseUrl && liteLlmApiKey) {
|
||||
await refreshLiteLlmModels()
|
||||
}
|
||||
|
||||
// GUI relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
|
||||
// We do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
|
||||
// (see normalizeApiConfiguration > openrouter)
|
||||
|
||||
@@ -241,8 +241,13 @@ cline "<prompt>"
|
||||
* Generates the deep-planning slash command response with model-family-aware variant selection
|
||||
* @param focusChainSettings Optional focus chain settings to include in the prompt
|
||||
* @param providerInfo Optional API provider info for model family detection
|
||||
* @param enableNativeToolCalls Optional flag to determine if native tool calling is enabled
|
||||
* @returns The deep-planning prompt string with appropriate variant and focus chain settings applied
|
||||
*/
|
||||
export const deepPlanningToolResponse = (focusChainSettings?: { enabled: boolean }, providerInfo?: ApiProviderInfo) => {
|
||||
return getDeepPlanningPrompt(focusChainSettings, providerInfo)
|
||||
export const deepPlanningToolResponse = (
|
||||
focusChainSettings?: { enabled: boolean },
|
||||
providerInfo?: ApiProviderInfo,
|
||||
enableNativeToolCalls?: boolean,
|
||||
) => {
|
||||
return getDeepPlanningPrompt(focusChainSettings, providerInfo, enableNativeToolCalls)
|
||||
}
|
||||
|
||||
@@ -2,15 +2,23 @@ import type { ApiProviderInfo } from "@/core/api"
|
||||
import type { SystemPromptContext } from "@/core/prompts/system-prompt/types"
|
||||
import { getDeepPlanningRegistry } from "./registry"
|
||||
import { generateGemini3Template } from "./variants/gemini3"
|
||||
import { generateGPT51Template } from "./variants/gpt5"
|
||||
import { generateGPT51Template } from "./variants/gpt51"
|
||||
|
||||
const focusChainIntro: string = `**Task Progress Parameter:**
|
||||
When creating the new task, you must include a task_progress parameter that breaks down the implementation into trackable steps. This parameter should be included inside the tool call, but not located inside of other content/argument blocks. This should follow the standard Markdown checklist format with "- [ ]" for incomplete items.`
|
||||
|
||||
/**
|
||||
* Generates the deep-planning slash command response with model-family-aware variant selection
|
||||
* @param focusChainSettings Optional focus chain settings to include in the prompt
|
||||
* @param providerInfo Optional API provider info for model family detection
|
||||
* @param enableNativeToolCalls Optional flag to determine if native tool calling is enabled
|
||||
* @returns The deep-planning prompt string with appropriate variant and focus chain settings applied
|
||||
*/
|
||||
export function getDeepPlanningPrompt(focusChainSettings?: { enabled: boolean }, providerInfo?: ApiProviderInfo): string {
|
||||
export function getDeepPlanningPrompt(
|
||||
focusChainSettings?: { enabled: boolean },
|
||||
providerInfo?: ApiProviderInfo,
|
||||
enableNativeToolCalls?: boolean,
|
||||
): string {
|
||||
// Create context for variant selection
|
||||
const context: SystemPromptContext = {
|
||||
providerInfo: providerInfo || ({} as ApiProviderInfo),
|
||||
@@ -20,27 +28,61 @@ export function getDeepPlanningPrompt(focusChainSettings?: { enabled: boolean },
|
||||
// Get the appropriate variant from registry
|
||||
const registry = getDeepPlanningRegistry()
|
||||
const variant = registry.get(context)
|
||||
const newTaskInstructions = generateNewTaskInstructions(enableNativeToolCalls ?? false)
|
||||
const focusChainParam = focusChainSettings?.enabled ? focusChainIntro : ""
|
||||
|
||||
// For variants with extensive focus chain prompting, generate template with focus chain flag
|
||||
let template: string
|
||||
if (variant.id === "gpt-5") {
|
||||
template = generateGPT51Template(focusChainSettings?.enabled ?? false)
|
||||
if (variant.id === "gpt-51") {
|
||||
template = generateGPT51Template(focusChainSettings?.enabled ?? false, enableNativeToolCalls ?? false)
|
||||
} else if (variant.id === "gemini-3") {
|
||||
template = generateGemini3Template(focusChainSettings?.enabled ?? false)
|
||||
template = generateGemini3Template(focusChainSettings?.enabled ?? false, enableNativeToolCalls ?? false)
|
||||
} else {
|
||||
template = variant.template
|
||||
template = template.replace("{{FOCUS_CHAIN_PARAM}}", focusChainParam)
|
||||
template = template.replace("{{NEW_TASK_INSTRUCTIONS}}", newTaskInstructions)
|
||||
}
|
||||
|
||||
// For variants with simpler focus chain prompting, Replace the FOCUS_CHAIN_PARAM placeholder with actual content
|
||||
const focusChainParam = focusChainSettings?.enabled
|
||||
? `**Task Progress Parameter:**
|
||||
When creating the new task, you must include a task_progress parameter that breaks down the implementation into trackable steps. This parameter should be included inside the tool call, but not located inside of other content/argument blocks. This should follow the standard Markdown checklist format with "- [ ]" for incomplete items.`
|
||||
: ""
|
||||
|
||||
template = template.replace("{{FOCUS_CHAIN_PARAM}}", focusChainParam)
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the new_task tool instructions based on whether native tool calling is enabled
|
||||
* @param enableNativeToolCalls Whether native tool calling is enabled
|
||||
* @returns The new_task tool instructions string
|
||||
*/
|
||||
function generateNewTaskInstructions(enableNativeToolCalls: boolean): string {
|
||||
if (enableNativeToolCalls) {
|
||||
return `
|
||||
**new_task Tool Definition:**
|
||||
|
||||
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"name": "new_task",
|
||||
"arguments": {
|
||||
"context": "Your detailed context here following the 5-point structure..."
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
The context parameter should include all five sections as described above.`
|
||||
} else {
|
||||
return `
|
||||
**new_task Tool Definition:**
|
||||
|
||||
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
|
||||
|
||||
\`\`\`xml
|
||||
<new_task>
|
||||
<context>Your detailed context here following the 5-point structure...</context>
|
||||
</new_task>
|
||||
\`\`\`
|
||||
|
||||
The context parameter should include all five sections as described above.`
|
||||
}
|
||||
}
|
||||
|
||||
// Export types for external use
|
||||
export type { DeepPlanningRegistry, DeepPlanningVariant } from "./types"
|
||||
|
||||
@@ -254,7 +254,7 @@ Refer to @path/to/file/markdown.md for a complete breakdown of the task requirem
|
||||
|
||||
{{FOCUS_CHAIN_PARAM}}
|
||||
|
||||
|
||||
{{NEW_TASK_INSTRUCTIONS}}
|
||||
|
||||
### Mode Switching
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ Refer to @path/to/file/markdown.md for a complete breakdown of the task requirem
|
||||
|
||||
{{FOCUS_CHAIN_PARAM}}
|
||||
|
||||
|
||||
{{NEW_TASK_INSTRUCTIONS}}
|
||||
|
||||
### Mode Switching
|
||||
|
||||
|
||||
@@ -26,8 +26,9 @@ export function createGemini3Variant(): DeepPlanningVariant {
|
||||
/**
|
||||
* Generates the deep-planning template with shell-specific commands
|
||||
* @param focusChainEnabled Whether focus chain (task_progress) is enabled for this task
|
||||
* @param enableNativeToolCalls Whether native tool calling is enabled
|
||||
*/
|
||||
export function generateGemini3Template(focusChainEnabled: boolean): string {
|
||||
export function generateGemini3Template(focusChainEnabled: boolean, enableNativeToolCalls: boolean): string {
|
||||
const detectedShell = getShell()
|
||||
|
||||
let isPowerShell = false
|
||||
@@ -209,7 +210,34 @@ You also MUST include the path to the markdown file you have created in your new
|
||||
}
|
||||
</IMPORTANT>
|
||||
|
||||
${
|
||||
enableNativeToolCalls
|
||||
? `**new_task Tool Definition:**
|
||||
|
||||
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
|
||||
|
||||
{
|
||||
"name": "new_task",
|
||||
"arguments": {
|
||||
"context": "Your detailed context here following the 5-point structure..."
|
||||
}
|
||||
}
|
||||
|
||||
The context parameter should include all five sections as described above.
|
||||
|
||||
`
|
||||
: `**new_task Tool Definition:**
|
||||
|
||||
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
|
||||
|
||||
<new_task>
|
||||
<context>Your detailed context here following the 5-point structure...</context>
|
||||
</new_task>
|
||||
|
||||
The context parameter should include all five sections as described above.
|
||||
|
||||
`
|
||||
}
|
||||
### Mode Switching
|
||||
|
||||
<IMPORTANT>
|
||||
|
||||
@@ -245,7 +245,7 @@ Refer to @path/to/file/markdown.md for a complete breakdown of the task requirem
|
||||
|
||||
{{FOCUS_CHAIN_PARAM}}
|
||||
|
||||
|
||||
{{NEW_TASK_INSTRUCTIONS}}
|
||||
|
||||
### Mode Switching
|
||||
|
||||
|
||||
+30
-1
@@ -26,8 +26,9 @@ export function createGPT51Variant(): DeepPlanningVariant {
|
||||
/**
|
||||
* Generates the deep-planning template with shell-specific commands
|
||||
* @param focusChainEnabled Whether focus chain (task_progress) is enabled for this task
|
||||
* @param enableNativeToolCalls Whether native tool calling is enabled
|
||||
*/
|
||||
export function generateGPT51Template(focusChainEnabled: boolean): string {
|
||||
export function generateGPT51Template(focusChainEnabled: boolean, enableNativeToolCalls: boolean): string {
|
||||
const detectedShell = getShell()
|
||||
|
||||
let isPowerShell = false
|
||||
@@ -209,6 +210,34 @@ You also MUST include the path to the markdown file you have created in your new
|
||||
}
|
||||
</IMPORTANT>
|
||||
|
||||
${
|
||||
enableNativeToolCalls
|
||||
? `**new_task Tool Definition:**
|
||||
|
||||
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
|
||||
|
||||
{
|
||||
"name": "new_task",
|
||||
"arguments": {
|
||||
"context": "Your detailed context here following the 5-point structure..."
|
||||
}
|
||||
}
|
||||
|
||||
The context parameter should include all five sections as described above.
|
||||
|
||||
`
|
||||
: `**new_task Tool Definition:**
|
||||
|
||||
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
|
||||
|
||||
<new_task>
|
||||
<context>Your detailed context here following the 5-point structure...</context>
|
||||
</new_task>
|
||||
|
||||
The context parameter should include all five sections as described above.
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
### Mode Switching
|
||||
|
||||
@@ -6,4 +6,4 @@ export { createAnthropicVariant } from "./anthropic"
|
||||
export { createGeminiVariant } from "./gemini"
|
||||
export { createGemini3Variant } from "./gemini3"
|
||||
export { createGenericVariant } from "./generic"
|
||||
export { createGPT51Variant } from "./gpt5"
|
||||
export { createGPT51Variant } from "./gpt51"
|
||||
|
||||
@@ -125,11 +125,11 @@ export class VariantValidator {
|
||||
|
||||
// Check component overrides reference valid components
|
||||
if (variant.componentOverrides) {
|
||||
const invalidOverrides = Object.entries(variant.componentOverrides).filter(
|
||||
([key, component]) => component.enabled !== false && !variant.componentOrder.includes(key as SystemPromptSection),
|
||||
const invalidOverrides = Object.keys(variant.componentOverrides).filter(
|
||||
(key) => !variant.componentOrder.includes(key as SystemPromptSection),
|
||||
)
|
||||
if (invalidOverrides.length > 0) {
|
||||
warnings.push(`Component overrides for unused components: ${invalidOverrides.map(([k]) => k).join(", ")}`)
|
||||
warnings.push(`Component overrides for unused components: ${invalidOverrides.join(", ")}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,12 @@ export class VariantValidator {
|
||||
|
||||
private validateBestPractices(variant: PromptVariant, warnings: string[]): void {
|
||||
// Check for recommended components
|
||||
const recommendedComponents = [SystemPromptSection.AGENT_ROLE, SystemPromptSection.RULES, SystemPromptSection.SYSTEM_INFO]
|
||||
const recommendedComponents = [
|
||||
SystemPromptSection.AGENT_ROLE,
|
||||
SystemPromptSection.TOOL_USE,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
]
|
||||
|
||||
const missingRecommended = recommendedComponents.filter((c) => !variant.componentOrder.includes(c))
|
||||
if (missingRecommended.length > 0) {
|
||||
|
||||
@@ -52,7 +52,7 @@ export async function parseSlashCommands(
|
||||
compact: condenseToolResponse(focusChainSettings),
|
||||
newrule: newRuleToolResponse(),
|
||||
reportbug: reportBugToolResponse(),
|
||||
"deep-planning": deepPlanningToolResponse(focusChainSettings, providerInfo),
|
||||
"deep-planning": deepPlanningToolResponse(focusChainSettings, providerInfo, willUseNativeTools),
|
||||
subagent: subagentToolResponse(),
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ export class StateManager {
|
||||
huaweiCloudMaasModels: Record<string, ModelInfo> | null
|
||||
hicapModels: Record<string, ModelInfo> | null
|
||||
aihubmixModels: Record<string, ModelInfo> | null
|
||||
liteLlmModels: Record<string, ModelInfo> | null
|
||||
} = {
|
||||
openRouterModels: null,
|
||||
groqModels: null,
|
||||
@@ -64,6 +65,7 @@ export class StateManager {
|
||||
huaweiCloudMaasModels: null,
|
||||
hicapModels: null,
|
||||
aihubmixModels: null,
|
||||
liteLlmModels: null,
|
||||
}
|
||||
|
||||
// Debounced persistence state
|
||||
@@ -374,7 +376,16 @@ export class StateManager {
|
||||
* Set models cache for a specific provider (in-memory only, not persisted)
|
||||
*/
|
||||
setModelsCache(
|
||||
provider: "openRouter" | "groq" | "baseten" | "huggingFace" | "requesty" | "huaweiCloudMaas" | "hicap" | "aihubmix",
|
||||
provider:
|
||||
| "openRouter"
|
||||
| "groq"
|
||||
| "baseten"
|
||||
| "huggingFace"
|
||||
| "requesty"
|
||||
| "huaweiCloudMaas"
|
||||
| "hicap"
|
||||
| "aihubmix"
|
||||
| "liteLlm",
|
||||
models: Record<string, ModelInfo>,
|
||||
): void {
|
||||
const cacheKey = `${provider}Models` as keyof typeof this.modelInfoCache
|
||||
@@ -385,11 +396,19 @@ export class StateManager {
|
||||
* Get model info by provider and model ID (from in-memory cache)
|
||||
*/
|
||||
getModelInfo(
|
||||
provider: "openRouter" | "groq" | "baseten" | "huggingFace" | "requesty" | "huaweiCloudMaas" | "hicap" | "aihubmix",
|
||||
provider:
|
||||
| "openRouter"
|
||||
| "groq"
|
||||
| "baseten"
|
||||
| "huggingFace"
|
||||
| "requesty"
|
||||
| "huaweiCloudMaas"
|
||||
| "hicap"
|
||||
| "aihubmix"
|
||||
| "liteLlm",
|
||||
modelId: string,
|
||||
): ModelInfo | undefined {
|
||||
const cacheKey = `${provider}Models` as keyof typeof this.modelInfoCache
|
||||
console.log("[OpenRouter] modelInfoCache:", this.modelInfoCache)
|
||||
return this.modelInfoCache[cacheKey]?.[modelId]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { TaskMetadata } from "@core/context/context-tracking/ContextTrackerTypes"
|
||||
import { EnvironmentMetadataEntry, TaskMetadata } from "@core/context/context-tracking/ContextTrackerTypes"
|
||||
import { execa } from "@packages/execa"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
@@ -10,6 +10,7 @@ import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import * as path from "path"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { McpMarketplaceCatalog } from "@/shared/mcp"
|
||||
import { StateManager } from "./StateManager"
|
||||
|
||||
@@ -168,6 +169,37 @@ export async function saveClineMessages(taskId: string, uiMessages: ClineMessage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects environment metadata for the current system and host.
|
||||
* This information is used for debugging and task portability.
|
||||
* Returns metadata without timestamp - timestamp is added by EnvironmentContextTracker.
|
||||
*/
|
||||
export async function collectEnvironmentMetadata(): Promise<Omit<EnvironmentMetadataEntry, "ts">> {
|
||||
try {
|
||||
const hostVersion = await HostProvider.env.getHostVersion({})
|
||||
|
||||
return {
|
||||
os_name: os.platform(),
|
||||
os_version: os.release(),
|
||||
os_arch: os.arch(),
|
||||
host_name: hostVersion.platform || "Unknown",
|
||||
host_version: hostVersion.version || "Unknown",
|
||||
cline_version: ExtensionRegistryInfo.version,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to collect environment metadata:", error)
|
||||
// Return fallback values if collection fails
|
||||
return {
|
||||
os_name: os.platform(),
|
||||
os_version: os.release(),
|
||||
os_arch: os.arch(),
|
||||
host_name: "Unknown",
|
||||
host_version: "Unknown",
|
||||
cline_version: "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTaskMetadata(taskId: string): Promise<TaskMetadata> {
|
||||
const filePath = path.join(await ensureTaskDirectoryExists(taskId), GlobalFileNames.taskMetadata)
|
||||
try {
|
||||
@@ -177,7 +209,7 @@ export async function getTaskMetadata(taskId: string): Promise<TaskMetadata> {
|
||||
} catch (error) {
|
||||
console.error("Failed to read task metadata:", error)
|
||||
}
|
||||
return { files_in_context: [], model_usage: [] }
|
||||
return { files_in_context: [], model_usage: [], environment_history: [] }
|
||||
}
|
||||
|
||||
export async function saveTaskMetadata(taskId: string, metadata: TaskMetadata) {
|
||||
|
||||
+24
-6
@@ -5,6 +5,7 @@ import { AssistantMessageContent, parseAssistantMessageV2, ToolUse } from "@core
|
||||
import { ContextManager } from "@core/context/context-management/ContextManager"
|
||||
import { checkContextWindowExceededError } from "@core/context/context-management/context-error-handling"
|
||||
import { getContextWindowInfo } from "@core/context/context-management/context-window-utils"
|
||||
import { EnvironmentContextTracker } from "@core/context/context-tracking/EnvironmentContextTracker"
|
||||
import { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
|
||||
import { ModelContextTracker } from "@core/context/context-tracking/ModelContextTracker"
|
||||
import {
|
||||
@@ -65,6 +66,7 @@ import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@shared/Languages"
|
||||
import { CLINE_MCP_TOOL_IDENTIFIER } from "@shared/mcp"
|
||||
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"
|
||||
@@ -226,6 +228,7 @@ export class Task {
|
||||
// Metadata tracking
|
||||
private fileContextTracker: FileContextTracker
|
||||
private modelContextTracker: ModelContextTracker
|
||||
private environmentContextTracker: EnvironmentContextTracker
|
||||
|
||||
// Focus Chain
|
||||
private FocusChainManager?: FocusChainManager
|
||||
@@ -362,9 +365,10 @@ export class Task {
|
||||
updateTaskHistory: this.updateTaskHistory,
|
||||
})
|
||||
|
||||
// Initialize file context tracker
|
||||
// Initialize context trackers
|
||||
this.fileContextTracker = new FileContextTracker(controller, this.taskId)
|
||||
this.modelContextTracker = new ModelContextTracker(this.taskId)
|
||||
this.environmentContextTracker = new EnvironmentContextTracker(this.taskId)
|
||||
|
||||
// Initialize focus chain manager only if enabled
|
||||
const focusChainSettings = this.stateManager.getGlobalSettingsKey("focusChainSettings")
|
||||
@@ -1017,6 +1021,13 @@ export class Task {
|
||||
})
|
||||
}
|
||||
|
||||
// Record environment metadata for new task
|
||||
try {
|
||||
await this.environmentContextTracker.recordEnvironment()
|
||||
} catch (error) {
|
||||
console.error("Failed to record environment metadata:", error)
|
||||
}
|
||||
|
||||
await this.initiateTaskLoop(userContent)
|
||||
}
|
||||
|
||||
@@ -1279,6 +1290,13 @@ export class Task {
|
||||
})
|
||||
}
|
||||
|
||||
// Record environment metadata when resuming task (tracks cross-platform migrations)
|
||||
try {
|
||||
await this.environmentContextTracker.recordEnvironment()
|
||||
} catch (error) {
|
||||
console.error("Failed to record environment metadata on resume:", error)
|
||||
}
|
||||
|
||||
await this.messageStateHandler.overwriteApiConversationHistory(modifiedApiConversationHistory)
|
||||
await this.initiateTaskLoop(newUserContent)
|
||||
}
|
||||
@@ -2100,8 +2118,7 @@ export class Task {
|
||||
workspaceRoots,
|
||||
isSubagentsEnabledAndCliInstalled,
|
||||
isCliSubagent,
|
||||
enableNativeToolCalls:
|
||||
featureFlagsService.getNativeToolCallEnabled() && this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
|
||||
enableNativeToolCalls: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
|
||||
}
|
||||
|
||||
const { systemPrompt, tools } = await getSystemPrompt(promptContext)
|
||||
@@ -2670,7 +2687,9 @@ export class Task {
|
||||
content: userContent,
|
||||
})
|
||||
|
||||
const currentMode = this.stateManager.getGlobalSettingsKey("mode")
|
||||
const modeSetting = this.stateManager.getGlobalSettingsKey("mode")
|
||||
const currentMode: Mode = modeSetting === "act" ? "act" : "plan"
|
||||
|
||||
telemetryService.captureConversationTurnEvent(this.ulid, providerId, model.id, "user", currentMode)
|
||||
|
||||
// Capture task initialization timing telemetry for the first API request
|
||||
@@ -2747,7 +2766,6 @@ export class Task {
|
||||
})
|
||||
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
|
||||
|
||||
const currentMode = this.stateManager.getGlobalSettingsKey("mode")
|
||||
telemetryService.captureConversationTurnEvent(
|
||||
this.ulid,
|
||||
providerId,
|
||||
@@ -3210,7 +3228,7 @@ export class Task {
|
||||
// Pre-fetch necessary data to avoid redundant calls within loops
|
||||
const ulid = this.ulid
|
||||
const focusChainSettings = this.stateManager.getGlobalSettingsKey("focusChainSettings")
|
||||
const useNativeToolCalls = this.useNativeToolCalls
|
||||
const useNativeToolCalls = this.stateManager.getGlobalStateKey("nativeToolCallEnabled")
|
||||
const providerInfo = this.getCurrentProviderInfo()
|
||||
const cwd = this.cwd
|
||||
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(this.controller, cwd)
|
||||
|
||||
@@ -1,58 +1,9 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { writeFile } from "@utils/fs"
|
||||
import os from "os"
|
||||
import * as path from "path"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { openFile } from "./open-file"
|
||||
|
||||
export async function downloadTask(dateTs: number, conversationHistory: Anthropic.MessageParam[]) {
|
||||
// File name
|
||||
const date = new Date(dateTs)
|
||||
const month = date.toLocaleString("en-US", { month: "short" }).toLowerCase()
|
||||
const day = date.getDate()
|
||||
const year = date.getFullYear()
|
||||
let hours = date.getHours()
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0")
|
||||
const seconds = date.getSeconds().toString().padStart(2, "0")
|
||||
const ampm = hours >= 12 ? "pm" : "am"
|
||||
hours = hours % 12
|
||||
hours = hours ? hours : 12 // the hour '0' should be '12'
|
||||
const fileName = `cline_task_${month}-${day}-${year}_${hours}-${minutes}-${seconds}-${ampm}.md`
|
||||
|
||||
// Generate markdown
|
||||
const markdownContent = conversationHistory
|
||||
.map((message) => {
|
||||
const role = message.role === "user" ? "**User:**" : "**Assistant:**"
|
||||
const content = Array.isArray(message.content)
|
||||
? message.content.map((block) => formatContentBlockToMarkdown(block)).join("\n")
|
||||
: message.content
|
||||
return `${role}\n\n${content}\n\n`
|
||||
})
|
||||
.join("---\n\n")
|
||||
|
||||
// Prompt user for save location
|
||||
const saveResponse = await HostProvider.window.showSaveDialog({
|
||||
options: {
|
||||
filters: { Markdown: { extensions: ["md"] } },
|
||||
defaultPath: path.join(os.homedir(), "Downloads", fileName),
|
||||
},
|
||||
})
|
||||
|
||||
if (saveResponse.selectedPath) {
|
||||
try {
|
||||
// Write content to the selected location
|
||||
await writeFile(saveResponse.selectedPath, markdownContent)
|
||||
await openFile(saveResponse.selectedPath, false, true)
|
||||
} catch (error) {
|
||||
await HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a content block to markdown for display in API request messages.
|
||||
* Used by Task class to format user content for the api_req_started message.
|
||||
*/
|
||||
export function formatContentBlockToMarkdown(block: Anthropic.ContentBlockParam): string {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from "axios"
|
||||
import ogs from "open-graph-scraper"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { fetch, getAxiosSettings } from "@/shared/net"
|
||||
|
||||
export interface OpenGraphData {
|
||||
title?: string
|
||||
@@ -28,6 +28,7 @@ export async function fetchOpenGraphData(url: string): Promise<OpenGraphData> {
|
||||
fetchOptions: {
|
||||
redirect: "follow", // Follow redirects
|
||||
} as any,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
}
|
||||
|
||||
const { result } = await ogs(options)
|
||||
|
||||
@@ -69,6 +69,7 @@ export class AuthService {
|
||||
protected _activeAuthStatusUpdateHandlers = new Set<StreamingResponseHandler<AuthState>>()
|
||||
protected _handlerToController = new Map<StreamingResponseHandler<AuthState>, Controller>()
|
||||
protected _controller: Controller
|
||||
protected _refreshPromise: Promise<void> | null = null
|
||||
|
||||
/**
|
||||
* Creates an instance of AuthService.
|
||||
@@ -152,28 +153,56 @@ export class AuthService {
|
||||
// Check if token has expired
|
||||
|
||||
if (await provider.shouldRefreshIdToken(clineAccountAuthToken, this._clineAuthInfo.expiresAt)) {
|
||||
try {
|
||||
const updatedAuthInfo = await provider.retrieveClineAuthInfo(this._controller)
|
||||
if (updatedAuthInfo) {
|
||||
this._clineAuthInfo = updatedAuthInfo
|
||||
this._authenticated = true
|
||||
clineAccountAuthToken = updatedAuthInfo.idToken
|
||||
}
|
||||
} catch (error) {
|
||||
// Only log out for permanent auth failures, not network issues
|
||||
if (error instanceof AuthInvalidTokenError) {
|
||||
Logger.error("Token is invalid or expired:", error)
|
||||
this._clineAuthInfo = null
|
||||
this._authenticated = false
|
||||
telemetryService.captureAuthLoggedOut(this._provider?.name, LogoutReason.ERROR_RECOVERY)
|
||||
} else if (error instanceof AuthNetworkError) {
|
||||
Logger.error("Network error refreshing token", error)
|
||||
// Keep existing auth info, will retry on next getAuthToken() call
|
||||
} else {
|
||||
throw error // Re-throw unexpected errors
|
||||
}
|
||||
// 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
|
||||
// After waiting, return the updated token
|
||||
clineAccountAuthToken = this._clineAuthInfo?.idToken
|
||||
return clineAccountAuthToken ? `workos:${clineAccountAuthToken}` : null
|
||||
}
|
||||
await this.sendAuthStatusUpdate()
|
||||
|
||||
// Start a new refresh operation
|
||||
this._refreshPromise = (async () => {
|
||||
let authStatusChanged = false
|
||||
|
||||
try {
|
||||
const updatedAuthInfo = await provider.retrieveClineAuthInfo(this._controller)
|
||||
if (updatedAuthInfo) {
|
||||
this._clineAuthInfo = updatedAuthInfo
|
||||
this._authenticated = true
|
||||
clineAccountAuthToken = updatedAuthInfo.idToken
|
||||
authStatusChanged = true
|
||||
}
|
||||
} catch (error) {
|
||||
// Only log out for permanent auth failures, not network issues
|
||||
if (error instanceof AuthInvalidTokenError) {
|
||||
Logger.error("Token is invalid or expired:", error)
|
||||
this._clineAuthInfo = null
|
||||
this._authenticated = false
|
||||
telemetryService.captureAuthLoggedOut(this._provider?.name, LogoutReason.ERROR_RECOVERY)
|
||||
authStatusChanged = true
|
||||
} else if (error instanceof AuthNetworkError) {
|
||||
Logger.error("Network error refreshing token", error)
|
||||
// Keep existing auth info, will retry on next getAuthToken() call
|
||||
} else {
|
||||
throw error // Re-throw unexpected errors
|
||||
}
|
||||
} finally {
|
||||
this._refreshPromise = null
|
||||
}
|
||||
|
||||
// Defer auth status update to avoid infinite loop
|
||||
if (authStatusChanged) {
|
||||
setImmediate(() => {
|
||||
this.sendAuthStatusUpdate().catch((error) => {
|
||||
Logger.error("Error sending auth status update after token refresh:", error)
|
||||
})
|
||||
})
|
||||
}
|
||||
})()
|
||||
|
||||
await this._refreshPromise
|
||||
}
|
||||
|
||||
return clineAccountAuthToken ? `workos:${clineAccountAuthToken}` : null
|
||||
@@ -393,11 +422,12 @@ export class AuthService {
|
||||
// Identify the user in telemetry if available
|
||||
if (this._clineAuthInfo?.userInfo?.id) {
|
||||
telemetryService.identifyAccount(this._clineAuthInfo.userInfo)
|
||||
// Reset feature flags to ensure they are fetched for the new/logged in user
|
||||
featureFlagsService.reset(this._clineAuthInfo?.userInfo?.id)
|
||||
// Poll feature flags immediately for authenticated users to ensure cache is populated
|
||||
await featureFlagsService.poll(this._clineAuthInfo?.userInfo?.id)
|
||||
} else {
|
||||
// Poll feature flags for unauthenticated state
|
||||
await featureFlagsService.poll(undefined)
|
||||
}
|
||||
// Poll feature flags to ensure they are up to date for all users
|
||||
await featureFlagsService.poll(this._clineAuthInfo?.userInfo?.id)
|
||||
|
||||
// Update state in webviews once per unique controller
|
||||
await Promise.all(Array.from(uniqueControllers).map((c) => c.postStateToWebview()))
|
||||
|
||||
@@ -56,14 +56,13 @@ export class ClineError extends Error {
|
||||
public readonly modelId?: string,
|
||||
public readonly providerId?: string,
|
||||
) {
|
||||
const serialized = serializeError(raw)
|
||||
const error = serialized?.response || serialized
|
||||
const error = serializeError(raw)
|
||||
|
||||
const message = error.message || String(error) || error?.cause?.means
|
||||
const message = error.message || error?.response?.message || String(error) || error?.cause?.means
|
||||
super(message)
|
||||
|
||||
// Extract status from multiple possible locations
|
||||
const status = error.status || error.statusCode
|
||||
const status = error.status || error.statusCode || error.response?.status
|
||||
this.modelId = modelId || error.modelId
|
||||
this.providerId = providerId || error.providerId
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export class ErrorService {
|
||||
|
||||
public logException(error: Error | ClineError, properties?: Record<string, unknown>): void {
|
||||
this.provider.logException(error, properties)
|
||||
console.error("[ErrorService] Logging exception", error)
|
||||
console.error("[ErrorService] Logging exception", JSON.stringify(error))
|
||||
}
|
||||
|
||||
public logMessage(
|
||||
|
||||
@@ -38,13 +38,15 @@ export class FeatureFlagsService {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.cacheInfo = { updateTime: timesNow, userId: userId || null }
|
||||
|
||||
for (const flag of FEATURE_FLAGS) {
|
||||
const payload = await this.getFeatureFlag(flag).catch(() => false)
|
||||
this.cache.set(flag, payload ?? false)
|
||||
}
|
||||
|
||||
// Only update timestamp after successfully populating cache
|
||||
this.cacheInfo = { updateTime: timesNow, userId: userId || null }
|
||||
|
||||
getClineOnboardingModels() // Refresh onboarding models cache if relevant flag changed
|
||||
}
|
||||
|
||||
@@ -93,10 +95,6 @@ export class FeatureFlagsService {
|
||||
return this.getBooleanFlagEnabled(FeatureFlag.HOOKS)
|
||||
}
|
||||
|
||||
public getNativeToolCallEnabled(): boolean {
|
||||
return this.getBooleanFlagEnabled(FeatureFlag.NATIVE_TOOL_CALLS_NEXT_GEN_MODELS)
|
||||
}
|
||||
|
||||
public isResponseApiEnabled(): boolean {
|
||||
return this.getBooleanFlagEnabled(FeatureFlag.OPENAI_NATIVE_RESPONSE_API)
|
||||
}
|
||||
@@ -149,19 +147,6 @@ export class FeatureFlagsService {
|
||||
return this.provider.getSettings()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the feature flags cache
|
||||
* Should run on user auth state changes to ensure flags are up-to-date
|
||||
*/
|
||||
public reset(userId?: string): void {
|
||||
// Skip known user ID to avoid redundant resets
|
||||
if (userId && this.cacheInfo.userId === userId) {
|
||||
return
|
||||
}
|
||||
this.cacheInfo = { updateTime: 0, userId: userId || null }
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* For testing: directly set a feature flag in the cache
|
||||
*/
|
||||
|
||||
@@ -34,6 +34,7 @@ import * as path from "path"
|
||||
import ReconnectingEventSource from "reconnecting-eventsource"
|
||||
import { z } from "zod"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getServerAuthHash } from "@/utils/mcpAuth"
|
||||
import { TelemetryService } from "../telemetry/TelemetryService"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getValidOpenTelemetryConfig } from "@/shared/services/config/otel-config"
|
||||
import { isPostHogConfigValid, posthogConfig } from "@/shared/services/config/posthog-config"
|
||||
import { Logger } from "../logging/Logger"
|
||||
import type { ITelemetryProvider } from "./providers/ITelemetryProvider"
|
||||
import type { ITelemetryProvider, TelemetryProperties, TelemetrySettings } from "./providers/ITelemetryProvider"
|
||||
import { OpenTelemetryClientProvider } from "./providers/opentelemetry/OpenTelemetryClientProvider"
|
||||
import { OpenTelemetryTelemetryProvider } from "./providers/opentelemetry/OpenTelemetryTelemetryProvider"
|
||||
import { PostHogClientProvider } from "./providers/posthog/PostHogClientProvider"
|
||||
@@ -15,9 +15,10 @@ export type TelemetryProviderType = "posthog" | "no-op" | "opentelemetry"
|
||||
/**
|
||||
* Configuration for telemetry providers
|
||||
*/
|
||||
export interface TelemetryProviderConfig {
|
||||
type: TelemetryProviderType
|
||||
}
|
||||
export type TelemetryProviderConfig =
|
||||
| { type: "posthog"; apiKey?: string; host?: string }
|
||||
| { type: "opentelemetry"; enabled?: boolean }
|
||||
| { type: "no-op" }
|
||||
|
||||
/**
|
||||
* Factory class for creating telemetry providers
|
||||
@@ -27,17 +28,25 @@ export class TelemetryProviderFactory {
|
||||
/**
|
||||
* Creates multiple telemetry providers based on configuration
|
||||
* Supports dual tracking during transition period
|
||||
* @returns Array of ITelemetryProvider instances
|
||||
*/
|
||||
public static async createProviders(): Promise<ITelemetryProvider[]> {
|
||||
const configs = TelemetryProviderFactory.getDefaultConfigs()
|
||||
const providers: ITelemetryProvider[] = await Promise.all(configs.map((c) => TelemetryProviderFactory.createProvider(c)))
|
||||
const providers: ITelemetryProvider[] = []
|
||||
|
||||
// Fallback to no-op if no providers available
|
||||
for (const config of configs) {
|
||||
try {
|
||||
const provider = await TelemetryProviderFactory.createProvider(config)
|
||||
providers.push(provider)
|
||||
} catch (error) {
|
||||
Logger.error(`Failed to create telemetry provider: ${config.type}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Always have at least a no-op provider
|
||||
if (providers.length === 0) {
|
||||
providers.push(new NoOpTelemetryProvider())
|
||||
Logger.info("TelemetryProviderFactory: Using NoOp provider (no valid configs)")
|
||||
}
|
||||
|
||||
Logger.info("TelemetryProviderFactory: Created providers - " + providers.map((p) => p.constructor.name).join(", "))
|
||||
return providers
|
||||
}
|
||||
@@ -46,8 +55,6 @@ export class TelemetryProviderFactory {
|
||||
* Creates a single telemetry provider based on the provided configuration
|
||||
* @param config Configuration for the telemetry provider
|
||||
* @returns ITelemetryProvider instance
|
||||
* @deprecated Use createProviders() for multi-provider support
|
||||
* @deprecated Use createProviders() for multi-provider support
|
||||
*/
|
||||
private static async createProvider(config: TelemetryProviderConfig): Promise<ITelemetryProvider> {
|
||||
switch (config.type) {
|
||||
@@ -68,11 +75,9 @@ export class TelemetryProviderFactory {
|
||||
return new NoOpTelemetryProvider()
|
||||
}
|
||||
case "no-op":
|
||||
return new NoOpTelemetryProvider()
|
||||
default:
|
||||
// Always fallback to NoOp provider. Only log error for unsupported types
|
||||
if (config.type !== "no-op") {
|
||||
console.error(`Unsupported telemetry provider type: ${config.type}`)
|
||||
}
|
||||
Logger.error(`Unsupported telemetry provider type: ${(config as { type?: string }).type ?? "unknown"}`)
|
||||
return new NoOpTelemetryProvider()
|
||||
}
|
||||
}
|
||||
@@ -80,17 +85,19 @@ export class TelemetryProviderFactory {
|
||||
/**
|
||||
* Gets the default telemetry provider configuration
|
||||
* @returns Default configuration using available providers
|
||||
* @returns Default configuration using available providers
|
||||
*/
|
||||
public static getDefaultConfigs(): TelemetryProviderConfig[] {
|
||||
const configs: TelemetryProviderConfig[] = []
|
||||
|
||||
if (isPostHogConfigValid(posthogConfig)) {
|
||||
configs.push({ type: "posthog", ...posthogConfig })
|
||||
}
|
||||
|
||||
const otelConfig = getValidOpenTelemetryConfig()
|
||||
if (otelConfig) {
|
||||
configs.push({ type: "opentelemetry", ...otelConfig })
|
||||
}
|
||||
|
||||
return configs.length > 0 ? configs : [{ type: "no-op" }]
|
||||
}
|
||||
}
|
||||
@@ -100,38 +107,59 @@ export class TelemetryProviderFactory {
|
||||
* or for testing purposes
|
||||
*/
|
||||
export class NoOpTelemetryProvider implements ITelemetryProvider {
|
||||
public isOptIn = true
|
||||
private isOptIn = true
|
||||
|
||||
public log(event: string, properties?: Record<string, unknown>): void {
|
||||
Logger.log(`[NoOpTelemetryProvider] ${event}: ${JSON.stringify(properties)}`)
|
||||
log(_event: string, _properties?: TelemetryProperties): void {
|
||||
Logger.log(`[NoOpTelemetryProvider] ${_event}: ${JSON.stringify(_properties)}`)
|
||||
}
|
||||
|
||||
public logRequired(event: string, properties?: Record<string, unknown>): void {
|
||||
Logger.log(`[NoOpTelemetryProvider] REQUIRED ${event}: ${JSON.stringify(properties)}`)
|
||||
logRequired(_event: string, _properties?: TelemetryProperties): void {
|
||||
Logger.log(`[NoOpTelemetryProvider] REQUIRED ${_event}: ${JSON.stringify(_properties)}`)
|
||||
}
|
||||
|
||||
public identifyUser(userInfo: any, properties?: Record<string, unknown>): void {
|
||||
Logger.info(`[NoOpTelemetryProvider] identifyUser - ${JSON.stringify(userInfo)} - ${JSON.stringify(properties)}`)
|
||||
identifyUser(_userInfo: any, _properties?: TelemetryProperties): void {
|
||||
Logger.info(`[NoOpTelemetryProvider] identifyUser - ${JSON.stringify(_userInfo)} - ${JSON.stringify(_properties)}`)
|
||||
}
|
||||
|
||||
public setOptIn(optIn: boolean): void {
|
||||
Logger.info(`[NoOpTelemetryProvider] setOptIn(${optIn})`)
|
||||
this.isOptIn = optIn
|
||||
setOptIn(_optIn: boolean): void {
|
||||
Logger.info(`[NoOpTelemetryProvider] setOptIn(${_optIn})`)
|
||||
this.isOptIn = _optIn
|
||||
}
|
||||
|
||||
public isEnabled(): boolean {
|
||||
isEnabled(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
public getSettings() {
|
||||
getSettings(): TelemetrySettings {
|
||||
return {
|
||||
extensionEnabled: false,
|
||||
hostEnabled: false,
|
||||
level: "off" as const,
|
||||
level: "off",
|
||||
}
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
Logger.info("[NoOpTelemetryProvider] Disposing")
|
||||
recordCounter(
|
||||
_name: string,
|
||||
_value: number,
|
||||
_attributes?: TelemetryProperties,
|
||||
_description?: string,
|
||||
_required = false,
|
||||
): void {
|
||||
// no-op
|
||||
}
|
||||
recordHistogram(
|
||||
_name: string,
|
||||
_value: number,
|
||||
_attributes?: TelemetryProperties,
|
||||
_description?: string,
|
||||
_required = false,
|
||||
): void {
|
||||
// no-op
|
||||
}
|
||||
recordGauge(
|
||||
_name: string,
|
||||
_value: number | null,
|
||||
_attributes?: TelemetryProperties,
|
||||
_description?: string,
|
||||
_required = false,
|
||||
): void {
|
||||
// no-op
|
||||
}
|
||||
async dispose(): Promise<void> {
|
||||
Logger.info(`[NoOpTelemetryProvider] Disposing (optIn=${this.isOptIn})`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
})
|
||||
const MOCK_USER_INFO = {
|
||||
id: "test-user-123",
|
||||
email: "test@example.com",
|
||||
displayName: "Test User",
|
||||
email: "test@example.com",
|
||||
createdAt: new Date().toISOString(),
|
||||
organizations: [],
|
||||
}
|
||||
|
||||
@@ -81,6 +81,42 @@ export class TelemetryService {
|
||||
["subagents", true], // CLI Subagents telemetry enabled
|
||||
])
|
||||
|
||||
private userId?: string
|
||||
private taskTurnCounts = new Map<string, number>()
|
||||
private taskToolCallCounts = new Map<string, number>()
|
||||
private taskErrorCounts = new Map<string, number>()
|
||||
public static readonly METRICS = {
|
||||
TASK: {
|
||||
TURNS_TOTAL: "cline.turns.total",
|
||||
TURNS_PER_TASK: "cline.turns.per_task",
|
||||
TOKENS_INPUT_TOTAL: "cline.tokens.input.total",
|
||||
TOKENS_INPUT_PER_RESPONSE: "cline.tokens.input.per_response",
|
||||
TOKENS_OUTPUT_TOTAL: "cline.tokens.output.total",
|
||||
TOKENS_OUTPUT_PER_RESPONSE: "cline.tokens.output.per_response",
|
||||
COST_TOTAL: "cline.cost.total",
|
||||
COST_PER_EVENT: "cline.cost.per_event",
|
||||
},
|
||||
CACHE: {
|
||||
WRITE_TOTAL: "cline.cache.write.tokens.total",
|
||||
WRITE_PER_EVENT: "cline.cache.write.tokens.per_event",
|
||||
READ_TOTAL: "cline.cache.read.tokens.total",
|
||||
READ_PER_EVENT: "cline.cache.read.tokens.per_event",
|
||||
HITS_TOTAL: "cline.cache.hits.total",
|
||||
},
|
||||
TOOLS: {
|
||||
CALLS_TOTAL: "cline.tool.calls.total",
|
||||
CALLS_PER_TASK: "cline.tool.calls.per_task",
|
||||
},
|
||||
ERRORS: {
|
||||
TOTAL: "cline.errors.total",
|
||||
PER_TASK: "cline.errors.per_task",
|
||||
},
|
||||
API: {
|
||||
TTFT_SECONDS: "cline.api.ttft.seconds",
|
||||
DURATION_SECONDS: "cline.api.duration.seconds",
|
||||
THROUGHPUT_TOKENS_PER_SECOND: "cline.api.throughput.tokens_per_second",
|
||||
},
|
||||
}
|
||||
// Event constants for tracking user interactions and system events
|
||||
private static readonly EVENTS = {
|
||||
// Task-related events for tracking conversation and execution flow
|
||||
@@ -329,6 +365,81 @@ export class TelemetryService {
|
||||
})
|
||||
}
|
||||
|
||||
private getStandardAttributes(extra?: TelemetryProperties): TelemetryProperties {
|
||||
return {
|
||||
...this.telemetryMetadata,
|
||||
...(this.userId ? { userId: this.userId } : {}),
|
||||
...(extra ?? {}),
|
||||
}
|
||||
}
|
||||
|
||||
private recordCounter(
|
||||
name: string,
|
||||
value: number,
|
||||
attributes?: TelemetryProperties,
|
||||
description?: string,
|
||||
required = false,
|
||||
): void {
|
||||
const attrs = this.getStandardAttributes(attributes)
|
||||
this.providers.forEach((provider) => {
|
||||
try {
|
||||
provider.recordCounter(name, value, attrs, description, required)
|
||||
} catch (error) {
|
||||
console.error(`[TelemetryService] recordCounter failed: ${name}`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private recordHistogram(
|
||||
name: string,
|
||||
value: number,
|
||||
attributes?: TelemetryProperties,
|
||||
description?: string,
|
||||
required = false,
|
||||
): void {
|
||||
const attrs = this.getStandardAttributes(attributes)
|
||||
this.providers.forEach((provider) => {
|
||||
try {
|
||||
provider.recordHistogram(name, value, attrs, description, required)
|
||||
} catch (error) {
|
||||
console.error(`[TelemetryService] recordHistogram failed: ${name}`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gauge values require explicit cleanup: callers must pass null with the same attribute set
|
||||
* when the series identified by name+attributes ends to prevent stale metric entries.
|
||||
*/
|
||||
private recordGauge(
|
||||
name: string,
|
||||
value: number | null,
|
||||
attributes?: TelemetryProperties,
|
||||
description?: string,
|
||||
required = false,
|
||||
): void {
|
||||
const attrs = this.getStandardAttributes(attributes)
|
||||
this.providers.forEach((provider) => {
|
||||
try {
|
||||
provider.recordGauge(name, value, attrs, description, required)
|
||||
} catch (error) {
|
||||
console.error(`[TelemetryService] recordGauge failed: ${name}`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private incrementTaskCounter(store: Map<string, number>, ulid: string): number {
|
||||
const nextValue = (store.get(ulid) ?? 0) + 1
|
||||
store.set(ulid, nextValue)
|
||||
return nextValue
|
||||
}
|
||||
|
||||
private resetTaskAggregates(ulid: string): void {
|
||||
this.taskTurnCounts.delete(ulid)
|
||||
this.taskToolCallCounts.delete(ulid)
|
||||
this.taskErrorCounts.delete(ulid)
|
||||
}
|
||||
|
||||
public captureExtensionActivated() {
|
||||
this.captureToProviders(TelemetryService.EVENTS.USER.EXTENSION_ACTIVATED, {}, false)
|
||||
}
|
||||
@@ -396,6 +507,7 @@ export class TelemetryService {
|
||||
...this.telemetryMetadata,
|
||||
}
|
||||
|
||||
this.userId = userInfo.id
|
||||
// Update all providers with error isolation
|
||||
this.providers.forEach((provider) => {
|
||||
try {
|
||||
@@ -539,6 +651,7 @@ export class TelemetryService {
|
||||
* @param openAiCompatibleDomain Optional domain for OpenAI Compatible providers (e.g., "api.example.com")
|
||||
*/
|
||||
public captureTaskCreated(ulid: string, apiProvider?: string, openAiCompatibleDomain?: string) {
|
||||
this.resetTaskAggregates(ulid)
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.CREATED,
|
||||
properties: { ulid, apiProvider, openAiCompatibleDomain },
|
||||
@@ -552,6 +665,7 @@ export class TelemetryService {
|
||||
* @param openAiCompatibleDomain Optional domain for OpenAI Compatible providers (e.g., "api.example.com")
|
||||
*/
|
||||
public captureTaskRestarted(ulid: string, apiProvider?: string, openAiCompatibleDomain?: string) {
|
||||
this.resetTaskAggregates(ulid)
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.RESTARTED,
|
||||
properties: { ulid, apiProvider, openAiCompatibleDomain },
|
||||
@@ -567,6 +681,7 @@ export class TelemetryService {
|
||||
event: TelemetryService.EVENTS.TASK.COMPLETED,
|
||||
properties: { ulid },
|
||||
})
|
||||
this.resetTaskAggregates(ulid)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -612,6 +727,51 @@ export class TelemetryService {
|
||||
isNativeToolCall,
|
||||
},
|
||||
})
|
||||
|
||||
const turnCount = this.incrementTaskCounter(this.taskTurnCounts, ulid)
|
||||
|
||||
const turnAttributes = { ulid, provider, model, source, mode }
|
||||
this.recordCounter(TelemetryService.METRICS.TASK.TURNS_TOTAL, 1, turnAttributes)
|
||||
this.recordHistogram(TelemetryService.METRICS.TASK.TURNS_PER_TASK, turnCount, turnAttributes)
|
||||
|
||||
if (Number.isFinite(tokenUsage.cacheWriteTokens)) {
|
||||
const cacheWriteTokens = tokenUsage.cacheWriteTokens ?? 0
|
||||
this.recordCounter(TelemetryService.METRICS.CACHE.WRITE_TOTAL, cacheWriteTokens, {
|
||||
ulid,
|
||||
provider,
|
||||
model,
|
||||
mode,
|
||||
})
|
||||
this.recordHistogram(TelemetryService.METRICS.CACHE.WRITE_PER_EVENT, cacheWriteTokens, {
|
||||
ulid,
|
||||
provider,
|
||||
model,
|
||||
mode,
|
||||
})
|
||||
}
|
||||
|
||||
if (Number.isFinite(tokenUsage.cacheReadTokens)) {
|
||||
const cacheReadTokens = tokenUsage.cacheReadTokens ?? 0
|
||||
this.recordCounter(TelemetryService.METRICS.CACHE.READ_TOTAL, cacheReadTokens, {
|
||||
ulid,
|
||||
provider,
|
||||
model,
|
||||
mode,
|
||||
})
|
||||
this.recordHistogram(TelemetryService.METRICS.CACHE.READ_PER_EVENT, cacheReadTokens, {
|
||||
ulid,
|
||||
provider,
|
||||
model,
|
||||
mode,
|
||||
})
|
||||
}
|
||||
|
||||
if (Number.isFinite(tokenUsage.totalCost)) {
|
||||
const totalCost = tokenUsage.totalCost ?? 0
|
||||
const costAttributes = { ulid, provider, model, mode, currency: "USD" }
|
||||
this.recordCounter(TelemetryService.METRICS.TASK.COST_TOTAL, totalCost, costAttributes)
|
||||
this.recordHistogram(TelemetryService.METRICS.TASK.COST_PER_EVENT, totalCost, costAttributes)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -631,6 +791,18 @@ export class TelemetryService {
|
||||
model,
|
||||
},
|
||||
})
|
||||
|
||||
if (Number.isFinite(tokensIn)) {
|
||||
const value = tokensIn ?? 0
|
||||
this.recordCounter(TelemetryService.METRICS.TASK.TOKENS_INPUT_TOTAL, value, { ulid, model })
|
||||
this.recordHistogram(TelemetryService.METRICS.TASK.TOKENS_INPUT_PER_RESPONSE, value, { ulid, model })
|
||||
}
|
||||
|
||||
if (Number.isFinite(tokensOut)) {
|
||||
const value = tokensOut ?? 0
|
||||
this.recordCounter(TelemetryService.METRICS.TASK.TOKENS_OUTPUT_TOTAL, value, { ulid, model })
|
||||
this.recordHistogram(TelemetryService.METRICS.TASK.TOKENS_OUTPUT_PER_RESPONSE, value, { ulid, model })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -692,6 +864,7 @@ export class TelemetryService {
|
||||
feedbackType,
|
||||
},
|
||||
})
|
||||
this.resetTaskAggregates(ulid)
|
||||
}
|
||||
|
||||
// Tool events
|
||||
@@ -739,6 +912,17 @@ export class TelemetryService {
|
||||
isNativeToolCall,
|
||||
},
|
||||
})
|
||||
|
||||
const toolAttributes = {
|
||||
ulid,
|
||||
tool,
|
||||
model: modelId,
|
||||
success,
|
||||
autoApproved,
|
||||
}
|
||||
const toolCallCount = this.incrementTaskCounter(this.taskToolCallCounts, ulid)
|
||||
this.recordCounter(TelemetryService.METRICS.TOOLS.CALLS_TOTAL, 1, toolAttributes)
|
||||
this.recordHistogram(TelemetryService.METRICS.TOOLS.CALLS_PER_TASK, toolCallCount, toolAttributes)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -990,6 +1174,34 @@ export class TelemetryService {
|
||||
...data,
|
||||
},
|
||||
})
|
||||
|
||||
if (typeof data.ttftSec === "number") {
|
||||
this.recordHistogram(TelemetryService.METRICS.API.TTFT_SECONDS, data.ttftSec, {
|
||||
ulid,
|
||||
model: modelId,
|
||||
provider: "gemini",
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof data.totalDurationSec === "number") {
|
||||
this.recordHistogram(TelemetryService.METRICS.API.DURATION_SECONDS, data.totalDurationSec, {
|
||||
ulid,
|
||||
model: modelId,
|
||||
provider: "gemini",
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof data.throughputTokensPerSec === "number") {
|
||||
this.recordHistogram(TelemetryService.METRICS.API.THROUGHPUT_TOKENS_PER_SECOND, data.throughputTokensPerSec, {
|
||||
ulid,
|
||||
model: modelId,
|
||||
provider: "gemini",
|
||||
})
|
||||
}
|
||||
|
||||
if (data.cacheHit) {
|
||||
this.recordCounter(TelemetryService.METRICS.CACHE.HITS_TOTAL, 1, { ulid, model: modelId, provider: "gemini" })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1043,6 +1255,21 @@ export class TelemetryService {
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
|
||||
this.recordCounter(TelemetryService.METRICS.ERRORS.TOTAL, 1, {
|
||||
ulid: args.ulid,
|
||||
model: args.model,
|
||||
provider: args.provider,
|
||||
error_status: args.errorStatus,
|
||||
})
|
||||
const errorAttributes = {
|
||||
ulid: args.ulid,
|
||||
model: args.model,
|
||||
provider: args.provider,
|
||||
error_status: args.errorStatus,
|
||||
}
|
||||
const errorCount = this.incrementTaskCounter(this.taskErrorCounts, args.ulid)
|
||||
this.recordHistogram(TelemetryService.METRICS.ERRORS.PER_TASK, errorCount, errorAttributes)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1374,6 +1601,15 @@ export class TelemetryService {
|
||||
feature_flag_enabled: featureFlagEnabled,
|
||||
},
|
||||
})
|
||||
|
||||
const isMultiRoot = rootCount > 1
|
||||
this.recordGauge("cline.workspace.active_roots", rootCount, {
|
||||
is_multi_root: isMultiRoot,
|
||||
})
|
||||
// Retire the previous series to avoid leaking gauge entries when the flag flips.
|
||||
this.recordGauge("cline.workspace.active_roots", null, {
|
||||
is_multi_root: !isMultiRoot,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import * as assert from "assert"
|
||||
import type { ITelemetryProvider, TelemetryProperties, TelemetrySettings } from "../providers/ITelemetryProvider"
|
||||
import { TelemetryService } from "../TelemetryService"
|
||||
|
||||
class FakeProvider implements ITelemetryProvider {
|
||||
public counters: Array<{ name: string; value: number; attributes: TelemetryProperties; description?: string }> = []
|
||||
public histograms: Array<{ name: string; value: number; attributes: TelemetryProperties; description?: string }> = []
|
||||
public gauges = new Map<string, Map<string, { value: number; attributes: TelemetryProperties; description?: string }>>()
|
||||
|
||||
log(): void {}
|
||||
logRequired(): void {}
|
||||
identifyUser(): void {}
|
||||
setOptIn(): void {}
|
||||
isEnabled(): boolean {
|
||||
return true
|
||||
}
|
||||
getSettings(): TelemetrySettings {
|
||||
return { extensionEnabled: true, hostEnabled: true, level: "all" }
|
||||
}
|
||||
recordCounter(name: string, value: number, attributes?: TelemetryProperties, description?: string, _required = false): void {
|
||||
this.counters.push({ name, value, attributes: attributes ?? {}, description })
|
||||
}
|
||||
recordHistogram(
|
||||
name: string,
|
||||
value: number,
|
||||
attributes?: TelemetryProperties,
|
||||
description?: string,
|
||||
_required = false,
|
||||
): void {
|
||||
this.histograms.push({ name, value, attributes: attributes ?? {}, description })
|
||||
}
|
||||
recordGauge(
|
||||
name: string,
|
||||
value: number | null,
|
||||
attributes?: TelemetryProperties,
|
||||
description?: string,
|
||||
_required = false,
|
||||
): void {
|
||||
const attrKey = JSON.stringify(attributes ?? {})
|
||||
const series = this.gauges.get(name)
|
||||
if (value === null) {
|
||||
series?.delete(attrKey)
|
||||
if (series && series.size === 0) {
|
||||
this.gauges.delete(name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let nextSeries = series
|
||||
if (!nextSeries) {
|
||||
nextSeries = new Map()
|
||||
this.gauges.set(name, nextSeries)
|
||||
}
|
||||
|
||||
nextSeries.set(attrKey, { value, attributes: attributes ?? {}, description })
|
||||
}
|
||||
async dispose(): Promise<void> {}
|
||||
}
|
||||
|
||||
function createTelemetryService(provider: FakeProvider): TelemetryService {
|
||||
return new TelemetryService([provider], {
|
||||
extension_version: "test",
|
||||
platform: "test-platform",
|
||||
platform_version: "1.0.0",
|
||||
os_type: "darwin",
|
||||
os_version: "24",
|
||||
is_dev: "true",
|
||||
})
|
||||
}
|
||||
|
||||
describe("TelemetryService metrics", () => {
|
||||
it("captureTokenUsage emits token counters and histograms", () => {
|
||||
const provider = new FakeProvider()
|
||||
const service = createTelemetryService(provider)
|
||||
|
||||
service.captureTokenUsage("task-1", 120, 80, "model-a")
|
||||
|
||||
assert.deepStrictEqual(
|
||||
provider.counters.map((entry) => entry.name),
|
||||
[TelemetryService.METRICS.TASK.TOKENS_INPUT_TOTAL, TelemetryService.METRICS.TASK.TOKENS_OUTPUT_TOTAL],
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
provider.histograms.map((entry) => entry.name),
|
||||
[TelemetryService.METRICS.TASK.TOKENS_INPUT_PER_RESPONSE, TelemetryService.METRICS.TASK.TOKENS_OUTPUT_PER_RESPONSE],
|
||||
)
|
||||
provider.counters.forEach((entry) => {
|
||||
assert.strictEqual(entry.attributes.ulid, "task-1")
|
||||
assert.strictEqual(entry.attributes.model, "model-a")
|
||||
assert.strictEqual(entry.attributes.extension_version, "test")
|
||||
})
|
||||
})
|
||||
|
||||
it("captureConversationTurnEvent emits counters with cache and cost", () => {
|
||||
const provider = new FakeProvider()
|
||||
const service = createTelemetryService(provider)
|
||||
service.identifyAccount({ id: "user-1" } as any)
|
||||
|
||||
service.captureConversationTurnEvent("task-2", "openai", "gpt-4", "assistant", "plan", {
|
||||
tokensIn: 150,
|
||||
tokensOut: 200,
|
||||
cacheWriteTokens: 40,
|
||||
cacheReadTokens: 20,
|
||||
totalCost: 1.23,
|
||||
})
|
||||
|
||||
assert.deepStrictEqual(
|
||||
provider.counters.map((entry) => entry.name),
|
||||
[
|
||||
TelemetryService.METRICS.TASK.TURNS_TOTAL,
|
||||
TelemetryService.METRICS.CACHE.WRITE_TOTAL,
|
||||
TelemetryService.METRICS.CACHE.READ_TOTAL,
|
||||
TelemetryService.METRICS.TASK.COST_TOTAL,
|
||||
],
|
||||
)
|
||||
const costEntry = provider.counters.find((entry) => entry.name === "cline.cost.total")
|
||||
assert.ok(costEntry)
|
||||
assert.strictEqual(costEntry?.attributes.ulid, "task-2")
|
||||
assert.strictEqual(costEntry?.attributes.provider, "openai")
|
||||
assert.strictEqual(costEntry?.attributes.model, "gpt-4")
|
||||
assert.strictEqual(costEntry?.attributes.mode, "plan")
|
||||
assert.strictEqual(costEntry?.attributes.currency, "USD")
|
||||
assert.deepStrictEqual(
|
||||
provider.histograms.map((entry) => entry.name),
|
||||
[
|
||||
TelemetryService.METRICS.TASK.TURNS_PER_TASK,
|
||||
TelemetryService.METRICS.CACHE.WRITE_PER_EVENT,
|
||||
TelemetryService.METRICS.CACHE.READ_PER_EVENT,
|
||||
TelemetryService.METRICS.TASK.COST_PER_EVENT,
|
||||
],
|
||||
)
|
||||
const turnEntry = provider.histograms.find((entry) => entry.name === TelemetryService.METRICS.TASK.TURNS_PER_TASK)
|
||||
assert.ok(turnEntry)
|
||||
assert.strictEqual(turnEntry?.value, 1)
|
||||
assert.strictEqual(turnEntry?.attributes.ulid, "task-2")
|
||||
assert.strictEqual(turnEntry?.attributes.provider, "openai")
|
||||
assert.strictEqual(turnEntry?.attributes.model, "gpt-4")
|
||||
assert.strictEqual(turnEntry?.attributes.source, "assistant")
|
||||
assert.strictEqual(turnEntry?.attributes.mode, "plan")
|
||||
})
|
||||
|
||||
it("captureWorkspaceInitialized emits gauge and retires previous series", () => {
|
||||
const provider = new FakeProvider()
|
||||
const service = createTelemetryService(provider)
|
||||
|
||||
service.captureWorkspaceInitialized(3, ["Git"], 500)
|
||||
const initialSeries = provider.gauges.get("cline.workspace.active_roots")
|
||||
assert.ok(initialSeries)
|
||||
assert.strictEqual(initialSeries.size, 1)
|
||||
const [initialEntry] = Array.from(initialSeries.values())
|
||||
assert.strictEqual(initialEntry.value, 3)
|
||||
assert.strictEqual(initialEntry.attributes.is_multi_root, true)
|
||||
assert.strictEqual(initialEntry.attributes.extension_version, "test")
|
||||
|
||||
service.captureWorkspaceInitialized(1, ["Git"], 200)
|
||||
const updatedSeries = provider.gauges.get("cline.workspace.active_roots")
|
||||
assert.ok(updatedSeries)
|
||||
assert.strictEqual(updatedSeries.size, 1)
|
||||
const [updatedEntry] = Array.from(updatedSeries.values())
|
||||
assert.strictEqual(updatedEntry.value, 1)
|
||||
assert.strictEqual(updatedEntry.attributes.is_multi_root, false)
|
||||
})
|
||||
|
||||
it("captureProviderApiError increments error counter", () => {
|
||||
const provider = new FakeProvider()
|
||||
const service = createTelemetryService(provider)
|
||||
|
||||
service.captureProviderApiError({
|
||||
ulid: "task-3",
|
||||
model: "claude",
|
||||
errorMessage: "boom",
|
||||
provider: "anthropic",
|
||||
errorStatus: 500,
|
||||
})
|
||||
|
||||
assert.strictEqual(provider.counters.length, 1)
|
||||
const entry = provider.counters[0]
|
||||
assert.strictEqual(entry.name, TelemetryService.METRICS.ERRORS.TOTAL)
|
||||
assert.strictEqual(entry.value, 1)
|
||||
assert.strictEqual(entry.attributes.ulid, "task-3")
|
||||
assert.strictEqual(entry.attributes.provider, "anthropic")
|
||||
assert.strictEqual(entry.attributes.model, "claude")
|
||||
assert.strictEqual(entry.attributes.error_status, 500)
|
||||
assert.strictEqual(provider.histograms.length, 1)
|
||||
const errorHistogram = provider.histograms[0]
|
||||
assert.strictEqual(errorHistogram.name, TelemetryService.METRICS.ERRORS.PER_TASK)
|
||||
assert.strictEqual(errorHistogram.value, 1)
|
||||
assert.strictEqual(errorHistogram.attributes.ulid, "task-3")
|
||||
assert.strictEqual(errorHistogram.attributes.provider, "anthropic")
|
||||
assert.strictEqual(errorHistogram.attributes.model, "claude")
|
||||
assert.strictEqual(errorHistogram.attributes.error_status, 500)
|
||||
})
|
||||
})
|
||||
@@ -87,22 +87,37 @@ export interface ITelemetryProvider {
|
||||
getSettings(): TelemetrySettings
|
||||
|
||||
/**
|
||||
* (Optional) Increment a counter metric.
|
||||
* Record a counter metric (cumulative value that only increases)
|
||||
* Providers that don't support metrics may implement this as a no-op.
|
||||
* @param name Metric name
|
||||
* @param name Metric name (e.g., "cline.tokens.input")
|
||||
* @param value Amount to increment by (default 1)
|
||||
* @param attributes Optional metric attributes (JSON-serializable)
|
||||
* @param attributes Optional metric attributes including userId, ulid (JSON-serializable)
|
||||
*/
|
||||
incrementCounter?(name: string, value?: number, attributes?: TelemetryProperties): void
|
||||
recordCounter(name: string, value: number, attributes?: TelemetryProperties, description?: string, required?: boolean): void
|
||||
|
||||
/**
|
||||
* (Optional) Record a value in a histogram metric.
|
||||
* Record a histogram metric (distribution of values for percentile analysis)
|
||||
* Providers that don't support metrics may implement this as a no-op.
|
||||
* @param name Metric name
|
||||
* @param name Metric name (e.g., "cline.api.duration_seconds")
|
||||
* @param value Value to record
|
||||
* @param attributes Optional metric attributes (JSON-serializable)
|
||||
* @param attributes Optional metric attributes including userId, ulid (JSON-serializable)
|
||||
*/
|
||||
recordHistogram?(name: string, value: number, attributes?: TelemetryProperties): void
|
||||
recordHistogram(name: string, value: number, attributes?: TelemetryProperties, description?: string, required?: boolean): void
|
||||
|
||||
/**
|
||||
* Record a gauge metric (point-in-time value that can go up or down)
|
||||
* Providers that don't support metrics may implement this as a no-op.
|
||||
* @param name Metric name (e.g., "cline.workspace.active_roots")
|
||||
* @param value Current value, or null to retire the series identified by name + attributes
|
||||
* @param attributes Optional metric attributes including userId, ulid (JSON-serializable). When retiring a series pass the same attribute set that was used when recording it.
|
||||
*/
|
||||
recordGauge(
|
||||
name: string,
|
||||
value: number | null,
|
||||
attributes?: TelemetryProperties,
|
||||
description?: string,
|
||||
required?: boolean,
|
||||
): void
|
||||
|
||||
/**
|
||||
* Clean up resources when the provider is disposed
|
||||
|
||||
@@ -20,6 +20,8 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
|
||||
// Lazy instrument caches for metrics
|
||||
private counters = new Map<string, ReturnType<Meter["createCounter"]>>()
|
||||
private histograms = new Map<string, ReturnType<Meter["createHistogram"]>>()
|
||||
private gauges = new Map<string, ReturnType<Meter["createObservableGauge"]>>()
|
||||
private gaugeValues = new Map<string, Map<string, { value: number; attributes?: TelemetryProperties }>>()
|
||||
|
||||
constructor() {
|
||||
// Initialize telemetry settings
|
||||
@@ -120,7 +122,6 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
|
||||
// Store user attributes for future events
|
||||
this.userAttributes = {
|
||||
user_id: userInfo.id,
|
||||
user_email: userInfo.email || "",
|
||||
user_name: userInfo.displayName || "",
|
||||
...this.flattenProperties(properties),
|
||||
}
|
||||
@@ -156,17 +157,24 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment a counter metric (lazy creation).
|
||||
* Only creates the counter on first use if meter is available.
|
||||
* Record a counter metric (cumulative value that only increases)
|
||||
* Lazy creation - only creates the counter on first use if meter is available.
|
||||
*/
|
||||
public incrementCounter(name: string, value: number = 1, attributes?: TelemetryProperties): void {
|
||||
if (!this.meter) {
|
||||
public recordCounter(
|
||||
name: string,
|
||||
value: number,
|
||||
attributes?: TelemetryProperties,
|
||||
description?: string,
|
||||
required = false,
|
||||
): void {
|
||||
if (!this.meter || (!required && !this.isEnabled())) {
|
||||
return
|
||||
}
|
||||
|
||||
let counter = this.counters.get(name)
|
||||
if (!counter) {
|
||||
counter = this.meter.createCounter(name)
|
||||
const options = description ? { description } : undefined
|
||||
counter = this.meter.createCounter(name, options)
|
||||
this.counters.set(name, counter)
|
||||
console.log(`[OTEL] Created counter: ${name}`)
|
||||
}
|
||||
@@ -175,17 +183,24 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a histogram metric (lazy creation).
|
||||
* Only creates the histogram on first use if meter is available.
|
||||
* Record a histogram metric (distribution of values for percentile analysis)
|
||||
* Lazy creation - only creates the histogram on first use if meter is available.
|
||||
*/
|
||||
public recordHistogram(name: string, value: number, attributes?: TelemetryProperties): void {
|
||||
if (!this.meter) {
|
||||
public recordHistogram(
|
||||
name: string,
|
||||
value: number,
|
||||
attributes?: TelemetryProperties,
|
||||
description?: string,
|
||||
required = false,
|
||||
): void {
|
||||
if (!this.meter || (!required && !this.isEnabled())) {
|
||||
return
|
||||
}
|
||||
|
||||
let histogram = this.histograms.get(name)
|
||||
if (!histogram) {
|
||||
histogram = this.meter.createHistogram(name)
|
||||
const options = description ? { description } : undefined
|
||||
histogram = this.meter.createHistogram(name, options)
|
||||
this.histograms.set(name, histogram)
|
||||
console.log(`[OTEL] Created histogram: ${name}`)
|
||||
}
|
||||
@@ -193,6 +208,78 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
|
||||
histogram.record(value, this.flattenProperties(attributes))
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a gauge metric (point-in-time value that can go up or down)
|
||||
* Lazy creation - creates an observable gauge that reads from stored values
|
||||
*/
|
||||
public recordGauge(
|
||||
name: string,
|
||||
value: number | null,
|
||||
attributes?: TelemetryProperties,
|
||||
description?: string,
|
||||
required = false,
|
||||
): void {
|
||||
if (!this.meter || (!required && !this.isEnabled())) {
|
||||
return
|
||||
}
|
||||
|
||||
const attrKey = attributes ? JSON.stringify(attributes) : ""
|
||||
|
||||
const existingSeries = this.gaugeValues.get(name)
|
||||
|
||||
if (value === null) {
|
||||
if (existingSeries) {
|
||||
existingSeries.delete(attrKey)
|
||||
if (existingSeries.size === 0) {
|
||||
this.gaugeValues.delete(name)
|
||||
this.gauges.delete(name)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let series = existingSeries
|
||||
if (!series) {
|
||||
series = new Map()
|
||||
this.gaugeValues.set(name, series)
|
||||
}
|
||||
|
||||
if (!this.gauges.has(name)) {
|
||||
const options = description ? { description } : undefined
|
||||
const gauge = this.meter.createObservableGauge(name, options)
|
||||
|
||||
gauge.addCallback((observableResult) => {
|
||||
const snapshot = this.snapshotGaugeSeries(name)
|
||||
if (snapshot.length === 0) {
|
||||
return
|
||||
}
|
||||
for (const data of snapshot) {
|
||||
observableResult.observe(data.value, this.flattenProperties(data.attributes))
|
||||
}
|
||||
})
|
||||
|
||||
this.gauges.set(name, gauge)
|
||||
console.log(`[OTEL] Created gauge: ${name}`)
|
||||
}
|
||||
|
||||
series.set(attrKey, { value, attributes })
|
||||
}
|
||||
|
||||
private snapshotGaugeSeries(name: string): Array<{ value: number; attributes?: TelemetryProperties }> {
|
||||
const series = this.gaugeValues.get(name)
|
||||
if (!series) {
|
||||
return []
|
||||
}
|
||||
const snapshot: Array<{ value: number; attributes?: TelemetryProperties }> = []
|
||||
for (const data of series.values()) {
|
||||
snapshot.push({
|
||||
value: data.value,
|
||||
attributes: data.attributes ? { ...data.attributes } : undefined,
|
||||
})
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
// OpenTelemetry client provider handles shutdown
|
||||
// Individual providers don't need to do anything
|
||||
|
||||
@@ -98,7 +98,6 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
|
||||
distinctId: userInfo.id,
|
||||
properties: {
|
||||
uuid: userInfo.id,
|
||||
email: userInfo.email,
|
||||
name: userInfo.displayName,
|
||||
...properties,
|
||||
alias: distinctId,
|
||||
@@ -129,14 +128,63 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Metrics are not supported in PostHog provider. These are intentional no-ops.
|
||||
* Record a counter metric by converting to equivalent PostHog event
|
||||
* This maintains backward compatibility with existing dashboards
|
||||
*/
|
||||
public incrementCounter(name: string, value: number = 1, attributes?: TelemetryProperties): void {
|
||||
// no-op
|
||||
public recordCounter(
|
||||
name: string,
|
||||
value: number,
|
||||
attributes?: TelemetryProperties,
|
||||
_description?: string,
|
||||
required = false,
|
||||
): void {
|
||||
if (!this.isEnabled() && !required) return
|
||||
|
||||
// Convert metric to event format for PostHog
|
||||
// Most counters don't need individual events - they're aggregated in OpenTelemetry
|
||||
// Only log significant counter events that have dashboard equivalents
|
||||
if (name === "cline.tokens.input.total" || name === "cline.tokens.output.total") {
|
||||
// These will be batched and emitted as a single "task.tokens" event
|
||||
// Implementation will be added when we update captureTokenUsage
|
||||
}
|
||||
}
|
||||
|
||||
public recordHistogram(name: string, value: number, attributes?: TelemetryProperties): void {
|
||||
// no-op
|
||||
/**
|
||||
* Record a histogram metric by converting to equivalent PostHog event
|
||||
* Histograms track distributions, but PostHog events capture individual values
|
||||
*/
|
||||
public recordHistogram(
|
||||
_name: string,
|
||||
_value: number,
|
||||
_attributes?: TelemetryProperties,
|
||||
_description?: string,
|
||||
_required = false,
|
||||
): void {
|
||||
// Histograms are for distribution analysis in OpenTelemetry
|
||||
// PostHog gets the raw values through existing event capture methods
|
||||
// No action needed here - events already capture these values
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a gauge metric by converting to equivalent PostHog event
|
||||
* Gauges track current state, which we can log as state change events
|
||||
*/
|
||||
public recordGauge(
|
||||
name: string,
|
||||
value: number | null,
|
||||
attributes?: TelemetryProperties,
|
||||
_description?: string,
|
||||
required = false,
|
||||
): void {
|
||||
if ((!this.isEnabled() && !required) || value === null) return
|
||||
|
||||
// Convert gauge updates to state change events
|
||||
if (name === "cline.workspace.active_roots") {
|
||||
this.log("workspace.roots_changed", {
|
||||
count: value,
|
||||
...attributes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
|
||||
@@ -103,7 +103,7 @@ export interface ExtensionState {
|
||||
hooksEnabled?: ClineFeatureSetting
|
||||
remoteConfigSettings?: Partial<RemoteConfigFields>
|
||||
subagentsEnabled?: boolean
|
||||
nativeToolCallSetting?: ClineFeatureSetting
|
||||
nativeToolCallSetting?: boolean
|
||||
}
|
||||
|
||||
export interface ClineMessage {
|
||||
|
||||
+105
-10
@@ -236,6 +236,7 @@ export interface ModelInfo {
|
||||
contextWindow?: number
|
||||
supportsImages?: boolean
|
||||
supportsPromptCache: boolean // this value is hardcoded for now
|
||||
supportsReasoning?: boolean // Whether the model supports reasoning/thinking mode
|
||||
inputPrice?: number // Keep for non-tiered input models
|
||||
outputPrice?: number // Keep for non-tiered output models
|
||||
thinkingConfig?: {
|
||||
@@ -362,6 +363,16 @@ export const anthropicModels = {
|
||||
cacheReadsPrice: 0.3,
|
||||
tiers: CLAUDE_SONNET_1M_TIERS,
|
||||
},
|
||||
"claude-opus-4-5-20251101": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 5.0,
|
||||
outputPrice: 25.0,
|
||||
cacheWritesPrice: 6.25,
|
||||
cacheReadsPrice: 0.5,
|
||||
},
|
||||
"claude-opus-4-1-20250805": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
@@ -465,6 +476,11 @@ export const claudeCodeModels = {
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
"claude-opus-4-5-20251101": {
|
||||
...anthropicModels["claude-opus-4-5-20251101"],
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
"claude-opus-4-1-20250805": {
|
||||
...anthropicModels["claude-opus-4-1-20250805"],
|
||||
supportsImages: false,
|
||||
@@ -548,6 +564,17 @@ export const bedrockModels = {
|
||||
cacheReadsPrice: 0.3,
|
||||
tiers: CLAUDE_SONNET_1M_TIERS,
|
||||
},
|
||||
"anthropic.claude-opus-4-5-20251101-v1:0": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsGlobalEndpoint: true,
|
||||
inputPrice: 5.0,
|
||||
outputPrice: 25.0,
|
||||
cacheWritesPrice: 6.25,
|
||||
cacheReadsPrice: 0.5,
|
||||
},
|
||||
"anthropic.claude-opus-4-20250514-v1:0": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
@@ -878,6 +905,16 @@ export const vertexModels = {
|
||||
cacheWritesPrice: 1.25,
|
||||
cacheReadsPrice: 0.1,
|
||||
},
|
||||
"claude-opus-4-5@20251101": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 5.0,
|
||||
outputPrice: 25.0,
|
||||
cacheWritesPrice: 6.25,
|
||||
cacheReadsPrice: 0.5,
|
||||
},
|
||||
"claude-opus-4-1@20250805": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
@@ -2627,6 +2664,46 @@ export const askSageModels = {
|
||||
"google-gemini-2.5-pro": {
|
||||
maxTokens: 65536,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"google-claude-45-sonnet": {
|
||||
maxTokens: 64000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"google-claude-4-opus": {
|
||||
maxTokens: 32000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gpt-5": {
|
||||
maxTokens: 65536,
|
||||
contextWindow: 2_097_152,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gpt-5-mini": {
|
||||
maxTokens: 32768,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gpt-5-nano": {
|
||||
maxTokens: 16384,
|
||||
contextWindow: 262_144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
@@ -2822,6 +2899,33 @@ export const nebiusDefaultModelId = "Qwen/Qwen2.5-32B-Instruct-fast" satisfies N
|
||||
export type XAIModelId = keyof typeof xaiModels
|
||||
export const xaiDefaultModelId: XAIModelId = "grok-4"
|
||||
export const xaiModels = {
|
||||
"grok-4-1-fast-reasoning": {
|
||||
contextWindow: 2_000_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.2,
|
||||
cacheReadsPrice: 0.05,
|
||||
outputPrice: 0.5,
|
||||
description: "xAI's Grok 4.1 Reasoning Fast - multimodal model with 2M context.",
|
||||
},
|
||||
"grok-4-1-fast-non-reasoning": {
|
||||
contextWindow: 2_000_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.2,
|
||||
cacheReadsPrice: 0.05,
|
||||
outputPrice: 0.5,
|
||||
description: "xAI's Grok 4.1 Non-Reasoning Fast - multimodal model with 2M context.",
|
||||
},
|
||||
"grok-code-fast-1": {
|
||||
contextWindow: 256_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.2,
|
||||
cacheReadsPrice: 0.02,
|
||||
outputPrice: 1.5,
|
||||
description: "xAI's Grok Coding model.",
|
||||
},
|
||||
"grok-4-fast-reasoning": {
|
||||
maxTokens: 30000,
|
||||
contextWindow: 2000000,
|
||||
@@ -3110,7 +3214,7 @@ export const cerebrasModels = {
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Intelligent general purpose model with 2,000 tokens/s",
|
||||
description: "Intelligent general purpose model with 1,000 tokens/s",
|
||||
},
|
||||
"gpt-oss-120b": {
|
||||
maxTokens: 65536,
|
||||
@@ -3148,15 +3252,6 @@ export const cerebrasModels = {
|
||||
outputPrice: 0,
|
||||
description: "SOTA coding performance with ~2500 tokens/s",
|
||||
},
|
||||
"qwen-3-235b-a22b-thinking-2507": {
|
||||
maxTokens: 32000,
|
||||
contextWindow: 65000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "SOTA performance with ~1500 tokens/s",
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// Groq
|
||||
|
||||
@@ -48,6 +48,7 @@ export function fromProtobufModelInfo(protoInfo: OpenRouterModelInfo): ModelInfo
|
||||
contextWindow: protoInfo.contextWindow,
|
||||
supportsImages: protoInfo.supportsImages,
|
||||
supportsPromptCache: protoInfo.supportsPromptCache,
|
||||
supportsReasoning: protoInfo.supportsReasoning,
|
||||
inputPrice: protoInfo.inputPrice,
|
||||
outputPrice: protoInfo.outputPrice,
|
||||
cacheWritesPrice: protoInfo.cacheWritesPrice,
|
||||
@@ -68,6 +69,7 @@ export function toProtobufModelInfo(modelInfo: ModelInfo): OpenRouterModelInfo {
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
supportsImages: modelInfo.supportsImages,
|
||||
supportsPromptCache: modelInfo.supportsPromptCache,
|
||||
supportsReasoning: modelInfo.supportsReasoning,
|
||||
inputPrice: modelInfo.inputPrice,
|
||||
outputPrice: modelInfo.outputPrice,
|
||||
cacheWritesPrice: modelInfo.cacheWritesPrice,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* URI scheme for opening remote rules/workflows in the editor.
|
||||
* Used to construct URIs like: remote://rule/{name} or remote://workflow/{name}
|
||||
*/
|
||||
export const REMOTE_URI_SCHEME = "remote://"
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./schema"
|
||||
@@ -6,8 +6,6 @@ export enum FeatureFlag {
|
||||
FOCUS_CHAIN_CHECKLIST = "focus_chain_checklist",
|
||||
DO_NOTHING = "do_nothing",
|
||||
HOOKS = "hooks",
|
||||
// Feature flag for enabling native tool calls for next-gen models
|
||||
NATIVE_TOOL_CALLS_NEXT_GEN_MODELS = "native_tool_calls_next_gen",
|
||||
// Feature flag for showing the new onboarding flow or old welcome view.
|
||||
ONBOARDING_MODELS = "onboarding_models",
|
||||
OPENAI_NATIVE_RESPONSE_API = "openai_native_response_api",
|
||||
@@ -16,7 +14,6 @@ export enum FeatureFlag {
|
||||
export const FeatureFlagDefaultValue: Partial<Record<FeatureFlag, FeatureFlagPayload>> = {
|
||||
[FeatureFlag.DO_NOTHING]: false,
|
||||
[FeatureFlag.HOOKS]: false,
|
||||
[FeatureFlag.NATIVE_TOOL_CALLS_NEXT_GEN_MODELS]: process.env.IS_DEV === "true",
|
||||
[FeatureFlag.ONBOARDING_MODELS]: process.env.E2E_TEST === "true" ? { models: {} } : undefined,
|
||||
[FeatureFlag.OPENAI_NATIVE_RESPONSE_API]: process.env.IS_DEV === "true",
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ class StandaloneTerminalProcess extends EventEmitter {
|
||||
const shellArgs = this.getShellArgs(shell, command)
|
||||
|
||||
try {
|
||||
// Spawn the process
|
||||
this.childProcess = spawn(shell, shellArgs, {
|
||||
// Create shell options
|
||||
const shellOptions = {
|
||||
cwd: cwd,
|
||||
stdio: ["ignore", "pipe", "pipe"], // Disable STDIN to prevent interactivity
|
||||
env: {
|
||||
@@ -45,7 +45,18 @@ class StandaloneTerminalProcess extends EventEmitter {
|
||||
SYSTEMD_PAGER: "", // Disable systemd pager
|
||||
MANPAGER: "cat", // Disable man pager
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Enable the shell option for "cmd.exe" to prevent double quotes from being over escaped
|
||||
if (shell.toLowerCase().includes("cmd")) {
|
||||
shellOptions.shell = true
|
||||
|
||||
// Spawn the process with special handling for "cmd.exe"
|
||||
this.childProcess = spawn("cmd.exe", shellArgs, shellOptions)
|
||||
} else {
|
||||
// Spawn the process
|
||||
this.childProcess = spawn(shell, shellArgs, shellOptions)
|
||||
}
|
||||
|
||||
// Track process state
|
||||
let didEmitEmptyLine = false
|
||||
@@ -200,8 +211,7 @@ class StandaloneTerminalProcess extends EventEmitter {
|
||||
if (shell.toLowerCase().includes("powershell") || shell.toLowerCase().includes("pwsh")) {
|
||||
return ["-Command", command]
|
||||
} else {
|
||||
// Use /s /c with quoted command for proper quote handling in cmd.exe
|
||||
return ["/s", "/c", `"${command}"`]
|
||||
return ["/c", command]
|
||||
}
|
||||
} else {
|
||||
// Use -l for login shell, -c for command
|
||||
|
||||
Generated
+3
-3
@@ -9434,9 +9434,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -53,18 +53,21 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
<XIcon />
|
||||
</Button>
|
||||
<h4 style={h4TitleStyle}>
|
||||
🎉{" "}New in v{minorVersion}
|
||||
🎉{" "}New in v{version}
|
||||
</h4>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<strong>MiniMax-M2</strong> is currently free to use in Cline!
|
||||
</li>
|
||||
<li>
|
||||
<strong>Gemini 3 Pro Preview</strong> is now available with SOTA reasoning and coding capabilities.
|
||||
<strong>Gemini 3 Pro Preview</strong> and <strong>Claude Opus 4.5</strong> are available with SOTA reasoning
|
||||
and coding capabilities.
|
||||
</li>
|
||||
<li>
|
||||
<strong>AquaVoice's Avalon</strong> model scores 97.3% accuracy on AISpeak and now powers voice-to-text
|
||||
dictation.
|
||||
<strong>Bug fixes,</strong> expanded features, and more! Check out the{" "}
|
||||
<VSCodeLink href="https://github.com/cline/cline/blob/main/CHANGELOG.md" style={linkStyle}>
|
||||
changelog
|
||||
</VSCodeLink>
|
||||
</li>
|
||||
</ul>
|
||||
<div style={hrStyle} />
|
||||
|
||||
@@ -1275,7 +1275,7 @@ export const ChatRowContent = memo(
|
||||
style={{
|
||||
opacity: cost != null && cost > 0 ? 1 : 0,
|
||||
}}>
|
||||
${Number(cost || 0)?.toFixed(4)}
|
||||
{cost != null && Number(cost || 0) > 0 ? `$${Number(cost || 0).toFixed(4)}` : ""}
|
||||
</VSCodeBadge>
|
||||
</div>
|
||||
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react"
|
||||
import Announcement from "@/components/chat/Announcement"
|
||||
import CliInstallBanner, { CURRENT_CLI_BANNER_VERSION } from "@/components/common/CliInstallBanner"
|
||||
import InfoBanner, { CURRENT_INFO_BANNER_VERSION } from "@/components/common/InfoBanner"
|
||||
import NewModelBanner, { CURRENT_MODEL_BANNER_VERSION } from "@/components/common/NewModelBanner"
|
||||
import HistoryPreview from "@/components/history/HistoryPreview"
|
||||
import HomeHeader from "@/components/welcome/HomeHeader"
|
||||
import { SuggestedTasks } from "@/components/welcome/SuggestedTasks"
|
||||
@@ -22,10 +23,10 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
|
||||
taskHistory,
|
||||
shouldShowQuickWins,
|
||||
}) => {
|
||||
const { lastDismissedInfoBannerVersion, lastDismissedCliBannerVersion } = useExtensionState()
|
||||
const { lastDismissedInfoBannerVersion, lastDismissedCliBannerVersion, lastDismissedModelBannerVersion } = useExtensionState()
|
||||
|
||||
const shouldShowInfoBanner = lastDismissedInfoBannerVersion < CURRENT_INFO_BANNER_VERSION
|
||||
// const shouldShowNewModelBanner = lastDismissedModelBannerVersion < CURRENT_MODEL_BANNER_VERSION
|
||||
const shouldShowNewModelBanner = lastDismissedModelBannerVersion < CURRENT_MODEL_BANNER_VERSION
|
||||
|
||||
// Show CLI banner if not dismissed and platform is VSCode (not JetBrains/standalone)
|
||||
const shouldShowCliBanner =
|
||||
@@ -38,7 +39,7 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
|
||||
<div className="overflow-y-auto flex flex-col pb-2.5">
|
||||
{shouldShowInfoBanner && <InfoBanner />}
|
||||
{showAnnouncement && <Announcement hideAnnouncement={hideAnnouncement} version={version} />}
|
||||
{/* {shouldShowNewModelBanner && <NewModelBanner />} */}
|
||||
{shouldShowNewModelBanner && <NewModelBanner />}
|
||||
{shouldShowCliBanner && <CliInstallBanner />}
|
||||
<HomeHeader shouldShowQuickWins={shouldShowQuickWins} />
|
||||
{!shouldShowQuickWins && taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"
|
||||
import React, { useCallback, useMemo, useState } from "react"
|
||||
import React, { useCallback, useLayoutEffect, useMemo, useState } from "react"
|
||||
import Thumbnails from "@/components/common/Thumbnails"
|
||||
import { getModeSpecificFields, normalizeApiConfiguration } from "@/components/settings/utils/providerUtils"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
@@ -16,7 +16,6 @@ import { CheckpointError } from "./CheckpointError"
|
||||
import ContextWindow from "./ContextWindow"
|
||||
import { FocusChain } from "./FocusChain"
|
||||
import { highlightText } from "./Highlights"
|
||||
import TaskTimeline from "./TaskTimeline"
|
||||
|
||||
const IS_DEV = process.env.IS_DEV === '"true"'
|
||||
interface TaskHeaderProps {
|
||||
@@ -62,14 +61,19 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
} = useExtensionState()
|
||||
|
||||
const [isHighlightedTextExpanded, setIsHighlightedTextExpanded] = useState(false)
|
||||
const [isTextOverflowing, setIsTextOverflowing] = useState(false)
|
||||
const highlightedTextRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const { highlightedText, displayTextExpandable } = useMemo(() => {
|
||||
const taskTextLines = task.text?.split("\n") || []
|
||||
const highlightedText = highlightText(task.text, false)
|
||||
const highlightedText = useMemo(() => highlightText(task.text, false), [task.text])
|
||||
|
||||
return { highlightedText, displayTextExpandable: taskTextLines.length > 3 }
|
||||
}, [task.text])
|
||||
// Check if text overflows the container (i.e., needs clamping)
|
||||
useLayoutEffect(() => {
|
||||
const el = highlightedTextRef.current
|
||||
if (el && isTaskExpanded && !isHighlightedTextExpanded) {
|
||||
// Check if content height exceeds the max-height
|
||||
setIsTextOverflowing(el.scrollHeight > el.clientHeight)
|
||||
}
|
||||
}, [task.text, isTaskExpanded, isHighlightedTextExpanded])
|
||||
|
||||
// Handle click outside to collapse
|
||||
React.useEffect(() => {
|
||||
@@ -176,16 +180,17 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
<div className="flex flex-col break-words" key={`task-details-${currentTaskItem?.id}`}>
|
||||
<div
|
||||
className={cn(
|
||||
"ph-no-capture whitespace-pre-wrap break-words px-0.5 text-sm cursor-pointer mt-1 relative",
|
||||
"ph-no-capture whitespace-pre-wrap break-words px-0.5 text-sm mt-1 relative",
|
||||
"max-h-[4.5rem] overflow-hidden",
|
||||
{
|
||||
"max-h-[25vh] overflow-y-auto scroll-smooth": isHighlightedTextExpanded,
|
||||
"max-h-[4.5rem] overflow-hidden": !isHighlightedTextExpanded && displayTextExpandable,
|
||||
"cursor-pointer": isTextOverflowing,
|
||||
},
|
||||
)}
|
||||
onClick={() => displayTextExpandable && setIsHighlightedTextExpanded(true)}
|
||||
onClick={() => isTextOverflowing && setIsHighlightedTextExpanded(true)}
|
||||
ref={highlightedTextRef}
|
||||
style={
|
||||
!isHighlightedTextExpanded && displayTextExpandable
|
||||
!isHighlightedTextExpanded && isTextOverflowing
|
||||
? {
|
||||
WebkitMaskImage: "linear-gradient(to bottom, black 60%, transparent 100%)",
|
||||
maskImage: "linear-gradient(to bottom, black 60%, transparent 100%)",
|
||||
@@ -209,8 +214,6 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
tokensOut={tokensOut}
|
||||
useAutoCondense={false} // Disable auto-condense configuration in UI for now
|
||||
/>
|
||||
|
||||
<TaskTimeline messages={clineMessages} onBlockClick={onScrollToMessage} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import { combineApiRequests } from "@shared/combineApiRequests"
|
||||
import { combineCommandSequences } from "@shared/combineCommandSequences"
|
||||
import { combineHookSequences } from "@shared/combineHookSequences"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react"
|
||||
import { Virtuoso } from "react-virtuoso"
|
||||
import { COLOR_GRAY } from "../colors"
|
||||
import TaskTimelineTooltip from "./TaskTimelineTooltip"
|
||||
import { getColor } from "./util"
|
||||
|
||||
// Timeline dimensions and spacing
|
||||
const BLOCK_WIDTH = "10px"
|
||||
const BLOCK_GAP = "4px"
|
||||
|
||||
interface TaskTimelineProps {
|
||||
messages: ClineMessage[]
|
||||
onBlockClick?: (messageIndex: number) => void
|
||||
}
|
||||
|
||||
const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages, onBlockClick }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const scrollableRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { taskTimelinePropsMessages, messageIndexMap } = useMemo(() => {
|
||||
if (messages.length <= 1) {
|
||||
return { taskTimelinePropsMessages: [], messageIndexMap: [] }
|
||||
}
|
||||
|
||||
const processed = combineApiRequests(combineCommandSequences(combineHookSequences(messages.slice(1))))
|
||||
const indexMap: number[] = []
|
||||
|
||||
const filtered = processed.filter((msg, _processedIndex) => {
|
||||
const originalIndex = messages.findIndex((originalMsg, idx) => idx > 0 && originalMsg.ts === msg.ts)
|
||||
|
||||
// Filter out standard "say" events we don't want to show
|
||||
if (
|
||||
msg.type === "say" &&
|
||||
(msg.say === "api_req_started" ||
|
||||
msg.say === "api_req_finished" ||
|
||||
msg.say === "api_req_retried" ||
|
||||
msg.say === "deleted_api_reqs" ||
|
||||
msg.say === "checkpoint_created" ||
|
||||
msg.say === "task_progress" ||
|
||||
msg.say === "text" ||
|
||||
msg.say === "reasoning")
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter out "ask" events we don't want to show, including the duplicate completion_result
|
||||
if (
|
||||
msg.type === "ask" &&
|
||||
(msg.ask === "resume_task" || msg.ask === "resume_completed_task" || msg.ask === "completion_result") // Filter out the duplicate completion_result "ask" message
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (originalIndex !== -1) {
|
||||
indexMap.push(originalIndex)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
return { taskTimelinePropsMessages: filtered, messageIndexMap: indexMap }
|
||||
}, [messages])
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollableRef.current && taskTimelinePropsMessages.length > 0) {
|
||||
scrollableRef.current.scrollLeft = scrollableRef.current.scrollWidth
|
||||
}
|
||||
}, [taskTimelinePropsMessages])
|
||||
|
||||
// Calculate the item size (width of block + gap)
|
||||
const itemWidth = parseInt(BLOCK_WIDTH.replace("px", "")) + parseInt(BLOCK_GAP.replace("px", ""))
|
||||
|
||||
// Virtuoso requires a reference to scroll to the end
|
||||
const virtuosoRef = useRef<any>(null)
|
||||
|
||||
// Render a timeline block
|
||||
const TimelineBlock = useCallback(
|
||||
(index: number) => {
|
||||
// Show placeholder block when no items exist
|
||||
if (taskTimelinePropsMessages.length === 0 || index >= taskTimelinePropsMessages.length) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: BLOCK_WIDTH,
|
||||
height: BLOCK_WIDTH,
|
||||
backgroundColor: "#e5e5e5", // Light gray placeholder
|
||||
flexShrink: 0,
|
||||
marginRight: BLOCK_GAP,
|
||||
opacity: 0.5,
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const message = taskTimelinePropsMessages[index]
|
||||
const originalMessageIndex = messageIndexMap[index]
|
||||
|
||||
const handleClick = () => {
|
||||
if (onBlockClick && originalMessageIndex !== undefined) {
|
||||
onBlockClick(originalMessageIndex)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TaskTimelineTooltip message={message}>
|
||||
<div
|
||||
className="rounded-full hover:brightness-120"
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
width: BLOCK_WIDTH,
|
||||
height: BLOCK_WIDTH,
|
||||
backgroundColor: getColor(message),
|
||||
flexShrink: 0,
|
||||
cursor: "pointer",
|
||||
marginRight: BLOCK_GAP,
|
||||
}}
|
||||
/>
|
||||
</TaskTimelineTooltip>
|
||||
)
|
||||
},
|
||||
[taskTimelinePropsMessages, messageIndexMap, onBlockClick],
|
||||
)
|
||||
|
||||
// Scroll to the end when messages change
|
||||
useEffect(() => {
|
||||
if (virtuosoRef.current && taskTimelinePropsMessages.length > 0) {
|
||||
virtuosoRef.current.scrollToIndex({
|
||||
index: taskTimelinePropsMessages.length - 1,
|
||||
align: "end",
|
||||
})
|
||||
}
|
||||
}, [taskTimelinePropsMessages])
|
||||
|
||||
if (taskTimelinePropsMessages.length === 0) {
|
||||
return (
|
||||
<div className="relative w-full h-full mb-1 overflow-hidden" ref={containerRef}>
|
||||
<div className="w-full h-full flex items-center">
|
||||
<div
|
||||
style={{
|
||||
width: BLOCK_WIDTH,
|
||||
height: BLOCK_WIDTH,
|
||||
backgroundColor: COLOR_GRAY,
|
||||
borderRadius: "50%",
|
||||
opacity: 0.5,
|
||||
flexShrink: 0,
|
||||
marginRight: BLOCK_GAP,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full mb-1" ref={containerRef}>
|
||||
<style>
|
||||
{`
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.timeline-virtuoso::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.timeline-virtuoso {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<Virtuoso
|
||||
className="timeline-virtuoso h-4 w-full"
|
||||
fixedItemHeight={itemWidth}
|
||||
horizontalDirection={true}
|
||||
increaseViewportBy={12}
|
||||
itemContent={TimelineBlock}
|
||||
ref={virtuosoRef}
|
||||
totalCount={Math.max(1, taskTimelinePropsMessages.length)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TaskTimeline
|
||||
@@ -1,217 +0,0 @@
|
||||
import { Tooltip } from "@heroui/react"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import React from "react"
|
||||
import { getColor } from "./util"
|
||||
|
||||
interface TaskTimelineTooltipProps {
|
||||
message: ClineMessage
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const TaskTimelineTooltip = ({ message, children }: TaskTimelineTooltipProps) => {
|
||||
const getMessageDescription = (message: ClineMessage): string => {
|
||||
if (message.type === "say") {
|
||||
switch (message.say) {
|
||||
// TODO: Need to confirm these classifcations with design
|
||||
case "task":
|
||||
return "Task Message"
|
||||
case "user_feedback":
|
||||
return "User Message"
|
||||
case "text":
|
||||
return "Assistant Response"
|
||||
case "tool":
|
||||
if (message.text) {
|
||||
try {
|
||||
const toolData = JSON.parse(message.text)
|
||||
if (
|
||||
toolData.tool === "readFile" ||
|
||||
toolData.tool === "listFilesTopLevel" ||
|
||||
toolData.tool === "listFilesRecursive" ||
|
||||
toolData.tool === "listCodeDefinitionNames" ||
|
||||
toolData.tool === "searchFiles"
|
||||
) {
|
||||
return `File Read: ${toolData.tool}`
|
||||
} else if (toolData.tool === "editedExistingFile") {
|
||||
return `File Edit: ${toolData.path || "Unknown file"}`
|
||||
} else if (toolData.tool === "newFileCreated") {
|
||||
return `New File: ${toolData.path || "Unknown file"}`
|
||||
} else if (toolData.tool === "fileDeleted") {
|
||||
return `Delete File: ${toolData.path || "Unknown file"}`
|
||||
} else if (toolData.tool === "webFetch") {
|
||||
return `Web Fetch: ${toolData.path || "Unknown URL"}`
|
||||
}
|
||||
return `Tool: ${toolData.tool}`
|
||||
} catch (_e) {
|
||||
return "Tool Use"
|
||||
}
|
||||
}
|
||||
return "Tool Use"
|
||||
case "command":
|
||||
return "Terminal Command"
|
||||
case "command_output":
|
||||
return "Terminal Output"
|
||||
case "browser_action":
|
||||
return "Browser Action"
|
||||
case "browser_action_result":
|
||||
return "Browser Result"
|
||||
case "completion_result":
|
||||
return "Task Completed"
|
||||
case "checkpoint_created":
|
||||
return "Checkpoint Created"
|
||||
default:
|
||||
return message.say || "Unknown"
|
||||
}
|
||||
} else if (message.type === "ask") {
|
||||
switch (message.ask) {
|
||||
case "followup":
|
||||
return "Assistant Message"
|
||||
case "plan_mode_respond":
|
||||
return "Planning Response"
|
||||
case "tool":
|
||||
if (message.text) {
|
||||
try {
|
||||
const toolData = JSON.parse(message.text)
|
||||
if (
|
||||
toolData.tool === "readFile" ||
|
||||
toolData.tool === "listFilesTopLevel" ||
|
||||
toolData.tool === "listFilesRecursive" ||
|
||||
toolData.tool === "listCodeDefinitionNames" ||
|
||||
toolData.tool === "searchFiles"
|
||||
) {
|
||||
return `File Read Approval: ${toolData.tool}`
|
||||
} else if (toolData.tool === "editedExistingFile") {
|
||||
return `File Edit Approval: ${toolData.path || "Unknown file"}`
|
||||
} else if (toolData.tool === "newFileCreated") {
|
||||
return `New File Approval: ${toolData.path || "Unknown file"}`
|
||||
} else if (toolData.tool === "fileDeleted") {
|
||||
return `File Deletion Approval: ${toolData.path || "Unknown file"}`
|
||||
} else if (toolData.tool === "webFetch") {
|
||||
return `Web Fetch: ${toolData.path || "Unknown URL"}`
|
||||
}
|
||||
return `Tool Approval: ${toolData.tool}`
|
||||
} catch (_e) {
|
||||
return "Tool Approval"
|
||||
}
|
||||
}
|
||||
return "Tool Approval"
|
||||
case "command":
|
||||
return "Terminal Command Approval"
|
||||
case "browser_action_launch":
|
||||
return "Browser Action Approval"
|
||||
default:
|
||||
return message.ask || "Unknown"
|
||||
}
|
||||
}
|
||||
return "Unknown Message Type"
|
||||
}
|
||||
|
||||
const getMessageContent = (message: ClineMessage): string => {
|
||||
if (message.text) {
|
||||
if (message.type === "ask" && message.ask === "plan_mode_respond" && message.text) {
|
||||
try {
|
||||
const planData = JSON.parse(message.text)
|
||||
return planData.response || message.text
|
||||
} catch (_e) {
|
||||
return message.text
|
||||
}
|
||||
} else if (message.type === "say" && message.say === "tool" && message.text) {
|
||||
try {
|
||||
const toolData = JSON.parse(message.text)
|
||||
return JSON.stringify(toolData, null, 2)
|
||||
} catch (_e) {
|
||||
return message.text
|
||||
}
|
||||
}
|
||||
|
||||
if (message.text.length > 200) {
|
||||
return message.text.substring(0, 200) + "..."
|
||||
}
|
||||
return message.text
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
const getTimestamp = (message: ClineMessage): string => {
|
||||
if (message.ts) {
|
||||
const messageDate = new Date(message.ts)
|
||||
const today = new Date()
|
||||
|
||||
const todayDate = new Date(today.getFullYear(), today.getMonth(), today.getDate())
|
||||
const messageDateOnly = new Date(messageDate.getFullYear(), messageDate.getMonth(), messageDate.getDate())
|
||||
|
||||
const time = messageDate.toLocaleTimeString([], { hour: "numeric", minute: "2-digit", hour12: true })
|
||||
|
||||
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
||||
const monthName = monthNames[messageDate.getMonth()]
|
||||
|
||||
if (messageDateOnly.getTime() === todayDate.getTime()) {
|
||||
return `${time}`
|
||||
} else if (messageDate.getFullYear() === today.getFullYear()) {
|
||||
return `${monthName} ${messageDate.getDate()} ${time}`
|
||||
} else {
|
||||
return `${monthName} ${messageDate.getDate()}, ${messageDate.getFullYear()} ${time}`
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
classNames={{
|
||||
base: "bg-(--vscode-editor-background) text-(--vscode-editor-foreground) border-(--vscode-widget-border) py-1 rounded-[3px] max-w-[calc(100dvw-2rem)] text-xs",
|
||||
}}
|
||||
closeDelay={100}
|
||||
content={
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-wrap items-center font-bold mb-1">
|
||||
<div className="mr-4 mb-0.5">
|
||||
<div
|
||||
style={{
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
minWidth: "10px", // Ensure fixed width
|
||||
minHeight: "10px", // Ensure fixed height
|
||||
borderRadius: 1.5,
|
||||
backgroundColor: getColor(message),
|
||||
marginRight: "8px",
|
||||
display: "inline-block",
|
||||
flexShrink: 0, // Prevent shrinking when space is limited
|
||||
}}
|
||||
/>
|
||||
{getMessageDescription(message)}
|
||||
</div>
|
||||
{getTimestamp(message) && (
|
||||
<span className="font-normal text-tiny" style={{ fontWeight: "normal", fontSize: "10px" }}>
|
||||
{getTimestamp(message)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{getMessageContent(message) && (
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
maxHeight: "150px",
|
||||
overflowY: "auto",
|
||||
fontSize: "11px",
|
||||
fontFamily: "var(--vscode-editor-font-family)",
|
||||
backgroundColor: "var(--vscode-textBlockQuote-background)",
|
||||
padding: "4px",
|
||||
borderRadius: "2px",
|
||||
scrollbarWidth: "none",
|
||||
}}>
|
||||
{getMessageContent(message)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
disableAnimation
|
||||
isKeyboardDismissDisabled={true}
|
||||
placement="bottom"
|
||||
shadow="sm">
|
||||
{children}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default TaskTimelineTooltip
|
||||
@@ -378,7 +378,7 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
|
||||
{isVisible && (
|
||||
<div
|
||||
className="fixed left-[15px] right-[15px] border border-editor-group-border pb-3 px-2 rounded z-1000 overflow-y-auto"
|
||||
className="fixed left-[15px] right-[15px] border border-editor-group-border rounded z-1000 flex flex-col"
|
||||
style={{
|
||||
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
|
||||
background: CODE_BLOCK_BG_COLOR,
|
||||
@@ -394,310 +394,322 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Tabs container */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
{/* Fixed header section - tabs and description */}
|
||||
<div className="flex-shrink-0 px-2 pt-0">
|
||||
{/* Tabs container */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1px",
|
||||
borderBottom: "1px solid var(--vscode-panel-border)",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
<TabButton isActive={currentView === "rules"} onClick={() => setCurrentView("rules")}>
|
||||
Rules
|
||||
</TabButton>
|
||||
<TabButton isActive={currentView === "workflows"} onClick={() => setCurrentView("workflows")}>
|
||||
Workflows
|
||||
</TabButton>
|
||||
{hooksEnabled?.user && (
|
||||
<TabButton isActive={currentView === "hooks"} onClick={() => setCurrentView("hooks")}>
|
||||
Hooks
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1px",
|
||||
borderBottom: "1px solid var(--vscode-panel-border)",
|
||||
}}>
|
||||
<TabButton isActive={currentView === "rules"} onClick={() => setCurrentView("rules")}>
|
||||
Rules
|
||||
</TabButton>
|
||||
)}
|
||||
<TabButton isActive={currentView === "workflows"} onClick={() => setCurrentView("workflows")}>
|
||||
Workflows
|
||||
</TabButton>
|
||||
{hooksEnabled?.user && (
|
||||
<TabButton isActive={currentView === "hooks"} onClick={() => setCurrentView("hooks")}>
|
||||
Hooks
|
||||
</TabButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remote config banner */}
|
||||
{(currentView === "rules" && hasRemoteRules) || (currentView === "workflows" && hasRemoteWorkflows) ? (
|
||||
<div className="flex items-center gap-2 px-5 py-3 mb-4 bg-vscode-textBlockQuote-background border-l-[3px] border-vscode-textLink-foreground">
|
||||
<i className="codicon codicon-lock text-sm" />
|
||||
<span className="text-base">
|
||||
{currentView === "rules"
|
||||
? "Your organization manages some rules"
|
||||
: "Your organization manages some workflows"}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Description text */}
|
||||
<div className="text-xs text-description mb-4">
|
||||
{currentView === "rules" ? (
|
||||
<p>
|
||||
Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way to
|
||||
include context and preferences for your projects or globally for every conversation.{" "}
|
||||
<VSCodeLink
|
||||
className="text-xs"
|
||||
href="https://docs.cline.bot/features/cline-rules"
|
||||
style={{ display: "inline", fontSize: "inherit" }}>
|
||||
Docs
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
) : currentView === "workflows" ? (
|
||||
<p>
|
||||
Workflows allow you to define a series of steps to guide Cline through a repetitive set of tasks,
|
||||
such as deploying a service or submitting a PR. To invoke a workflow, type{" "}
|
||||
<span className="text-foreground font-bold">/workflow-name</span> in the chat.{" "}
|
||||
<VSCodeLink
|
||||
className="text-xs inline"
|
||||
href="https://docs.cline.bot/features/slash-commands/workflows">
|
||||
Docs
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Hooks allow you to execute custom scripts at specific points in Cline's execution lifecycle,
|
||||
enabling automation and integration with external tools.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{currentView === "rules" ? (
|
||||
<>
|
||||
{/* Remote Rules Section */}
|
||||
{hasRemoteRules && (
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Enterprise Rules</div>
|
||||
<div className="flex flex-col gap-0">
|
||||
{remoteGlobalRules.map((rule) => {
|
||||
const enabled = rule.alwaysEnabled || remoteRulesToggles[rule.name] === true
|
||||
return (
|
||||
<RuleRow
|
||||
alwaysEnabled={rule.alwaysEnabled}
|
||||
enabled={enabled}
|
||||
isGlobal={false}
|
||||
isRemote={true}
|
||||
key={rule.name}
|
||||
rulePath={rule.name}
|
||||
ruleType="cline"
|
||||
toggleRule={toggleRemoteRule}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Rules Section */}
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Global Rules</div>
|
||||
|
||||
{/* File-based Global Rules */}
|
||||
<RulesToggleList
|
||||
isGlobal={true}
|
||||
listGap="small"
|
||||
rules={globalRules}
|
||||
ruleType={"cline"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(true, rulePath, enabled)}
|
||||
/>
|
||||
{/* Remote config banner */}
|
||||
{(currentView === "rules" && hasRemoteRules) || (currentView === "workflows" && hasRemoteWorkflows) ? (
|
||||
<div className="flex items-center gap-2 px-5 py-3 mb-4 bg-vscode-textBlockQuote-background border-l-[3px] border-vscode-textLink-foreground">
|
||||
<i className="codicon codicon-lock text-sm" />
|
||||
<span className="text-base">
|
||||
{currentView === "rules"
|
||||
? "Your organization manages some rules"
|
||||
: "Your organization manages some workflows"}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Local Rules Section */}
|
||||
<div style={{ marginBottom: -10 }}>
|
||||
<div className="text-sm font-normal mb-2">Workspace Rules</div>
|
||||
<RulesToggleList
|
||||
isGlobal={false}
|
||||
listGap="small"
|
||||
rules={localRules}
|
||||
ruleType={"cline"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
|
||||
/>
|
||||
|
||||
<RulesToggleList
|
||||
isGlobal={false}
|
||||
listGap="small"
|
||||
rules={cursorRules}
|
||||
ruleType={"cursor"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
toggleRule={toggleCursorRule}
|
||||
/>
|
||||
<RulesToggleList
|
||||
isGlobal={false}
|
||||
listGap="small"
|
||||
rules={windsurfRules}
|
||||
ruleType={"windsurf"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
toggleRule={toggleWindsurfRule}
|
||||
/>
|
||||
<RulesToggleList
|
||||
isGlobal={false}
|
||||
listGap="small"
|
||||
rules={agentsRules}
|
||||
ruleType={"agents"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
toggleRule={toggleAgentsRule}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : currentView === "workflows" ? (
|
||||
<>
|
||||
{/* Remote Workflows Section */}
|
||||
{hasRemoteWorkflows && (
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Enterprise Workflows</div>
|
||||
<div className="flex flex-col gap-0">
|
||||
{remoteGlobalWorkflows.map((workflow) => {
|
||||
const enabled =
|
||||
workflow.alwaysEnabled || remoteWorkflowToggles[workflow.name] === true
|
||||
return (
|
||||
<RuleRow
|
||||
alwaysEnabled={workflow.alwaysEnabled}
|
||||
enabled={enabled}
|
||||
isGlobal={false}
|
||||
isRemote={true}
|
||||
key={workflow.name}
|
||||
rulePath={workflow.name}
|
||||
ruleType="workflow"
|
||||
toggleRule={toggleRemoteWorkflow}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Workflows Section */}
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Global Workflows</div>
|
||||
|
||||
{/* File-based Global Workflows */}
|
||||
<RulesToggleList
|
||||
isGlobal={true}
|
||||
listGap="small"
|
||||
rules={globalWorkflows}
|
||||
ruleType={"workflow"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
toggleRule={(rulePath, enabled) => toggleWorkflow(true, rulePath, enabled)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Local Workflows Section */}
|
||||
<div style={{ marginBottom: -10 }}>
|
||||
<div className="text-sm font-normal mb-2">Workspace Workflows</div>
|
||||
<RulesToggleList
|
||||
isGlobal={false}
|
||||
listGap="small"
|
||||
rules={localWorkflows}
|
||||
ruleType={"workflow"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
toggleRule={(rulePath, enabled) => toggleWorkflow(false, rulePath, enabled)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-xs text-description mb-4">
|
||||
{/* Description text */}
|
||||
<div className="text-xs text-description mb-4">
|
||||
{currentView === "rules" ? (
|
||||
<p>
|
||||
Toggle to enable/disable (chmod +x/-x).{" "}
|
||||
Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way
|
||||
to include context and preferences for your projects or globally for every conversation.{" "}
|
||||
<VSCodeLink
|
||||
className="text-xs"
|
||||
href="https://docs.cline.bot/features/hooks"
|
||||
href="https://docs.cline.bot/features/cline-rules"
|
||||
style={{ display: "inline", fontSize: "inherit" }}>
|
||||
Docs
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
</div>
|
||||
{/* Hooks Tab */}
|
||||
{/* Windows warning banner */}
|
||||
{isWindows && (
|
||||
<div className="flex items-center gap-2 px-5 py-3 mb-4 bg-vscode-inputValidation-warningBackground border-l-[3px] border-vscode-inputValidation-warningBorder">
|
||||
<i className="codicon codicon-warning text-sm" />
|
||||
<span className="text-base">
|
||||
Hook toggling is not supported on Windows. Hooks can be created, edited, and deleted, but
|
||||
cannot be enabled/disabled and will not execute.
|
||||
</span>
|
||||
</div>
|
||||
) : currentView === "workflows" ? (
|
||||
<p>
|
||||
Workflows allow you to define a series of steps to guide Cline through a repetitive set of
|
||||
tasks, such as deploying a service or submitting a PR. To invoke a workflow, type{" "}
|
||||
<span className="text-foreground font-bold">/workflow-name</span> in the chat.{" "}
|
||||
<VSCodeLink
|
||||
className="text-xs inline"
|
||||
href="https://docs.cline.bot/features/slash-commands/workflows">
|
||||
Docs
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Hooks allow you to execute custom scripts at specific points in Cline's execution lifecycle,
|
||||
enabling automation and integration with external tools.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global Hooks */}
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Global Hooks</div>
|
||||
<div className="flex flex-col gap-0">
|
||||
{globalHooks
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((hook) => (
|
||||
<HookRow
|
||||
absolutePath={hook.absolutePath}
|
||||
enabled={hook.enabled}
|
||||
hookName={hook.name}
|
||||
isGlobal={true}
|
||||
isWindows={isWindows}
|
||||
key={hook.name}
|
||||
onDelete={(hooksToggles) => {
|
||||
// Use response data directly, no need to refresh
|
||||
setGlobalHooks(hooksToggles.globalHooks || [])
|
||||
setWorkspaceHooks(hooksToggles.workspaceHooks || [])
|
||||
}}
|
||||
onToggle={(name: string, newEnabled: boolean) =>
|
||||
toggleHook(true, name, newEnabled)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<NewRuleRow existingHooks={globalHooks.map((h) => h.name)} isGlobal={true} ruleType="hook" />
|
||||
{/* Scrollable content area */}
|
||||
<div className="flex-1 overflow-y-auto px-2 pb-3" style={{ minHeight: 0 }}>
|
||||
{currentView === "rules" ? (
|
||||
<>
|
||||
{/* Remote Rules Section */}
|
||||
{hasRemoteRules && (
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Enterprise Rules</div>
|
||||
<div className="flex flex-col gap-0">
|
||||
{remoteGlobalRules.map((rule) => {
|
||||
const enabled = rule.alwaysEnabled || remoteRulesToggles[rule.name] === true
|
||||
return (
|
||||
<RuleRow
|
||||
alwaysEnabled={rule.alwaysEnabled}
|
||||
enabled={enabled}
|
||||
isGlobal={false}
|
||||
isRemote={true}
|
||||
key={rule.name}
|
||||
rulePath={rule.name}
|
||||
ruleType="cline"
|
||||
toggleRule={toggleRemoteRule}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Rules Section */}
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Global Rules</div>
|
||||
|
||||
{/* File-based Global Rules */}
|
||||
<RulesToggleList
|
||||
isGlobal={true}
|
||||
listGap="small"
|
||||
rules={globalRules}
|
||||
ruleType={"cline"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(true, rulePath, enabled)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workspace Hooks - one section per workspace */}
|
||||
{workspaceHooks.map((workspace, index) => (
|
||||
<div
|
||||
key={workspace.workspaceName}
|
||||
style={{ marginBottom: index === workspaceHooks.length - 1 ? -10 : 12 }}>
|
||||
<div className="text-sm font-normal mb-2">{workspace.workspaceName}/.clinerules/hooks/</div>
|
||||
{/* Local Rules Section */}
|
||||
<div style={{ marginBottom: -10 }}>
|
||||
<div className="text-sm font-normal mb-2">Workspace Rules</div>
|
||||
<RulesToggleList
|
||||
isGlobal={false}
|
||||
listGap="small"
|
||||
rules={localRules}
|
||||
ruleType={"cline"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
|
||||
/>
|
||||
|
||||
<RulesToggleList
|
||||
isGlobal={false}
|
||||
listGap="small"
|
||||
rules={cursorRules}
|
||||
ruleType={"cursor"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
toggleRule={toggleCursorRule}
|
||||
/>
|
||||
<RulesToggleList
|
||||
isGlobal={false}
|
||||
listGap="small"
|
||||
rules={windsurfRules}
|
||||
ruleType={"windsurf"}
|
||||
showNewRule={false}
|
||||
showNoRules={false}
|
||||
toggleRule={toggleWindsurfRule}
|
||||
/>
|
||||
<RulesToggleList
|
||||
isGlobal={false}
|
||||
listGap="small"
|
||||
rules={agentsRules}
|
||||
ruleType={"agents"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
toggleRule={toggleAgentsRule}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : currentView === "workflows" ? (
|
||||
<>
|
||||
{/* Remote Workflows Section */}
|
||||
{hasRemoteWorkflows && (
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Enterprise Workflows</div>
|
||||
<div className="flex flex-col gap-0">
|
||||
{remoteGlobalWorkflows.map((workflow) => {
|
||||
const enabled =
|
||||
workflow.alwaysEnabled || remoteWorkflowToggles[workflow.name] === true
|
||||
return (
|
||||
<RuleRow
|
||||
alwaysEnabled={workflow.alwaysEnabled}
|
||||
enabled={enabled}
|
||||
isGlobal={false}
|
||||
isRemote={true}
|
||||
key={workflow.name}
|
||||
rulePath={workflow.name}
|
||||
ruleType="workflow"
|
||||
toggleRule={toggleRemoteWorkflow}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Workflows Section */}
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Global Workflows</div>
|
||||
|
||||
{/* File-based Global Workflows */}
|
||||
<RulesToggleList
|
||||
isGlobal={true}
|
||||
listGap="small"
|
||||
rules={globalWorkflows}
|
||||
ruleType={"workflow"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
toggleRule={(rulePath, enabled) => toggleWorkflow(true, rulePath, enabled)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Local Workflows Section */}
|
||||
<div style={{ marginBottom: -10 }}>
|
||||
<div className="text-sm font-normal mb-2">Workspace Workflows</div>
|
||||
<RulesToggleList
|
||||
isGlobal={false}
|
||||
listGap="small"
|
||||
rules={localWorkflows}
|
||||
ruleType={"workflow"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
toggleRule={(rulePath, enabled) => toggleWorkflow(false, rulePath, enabled)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-xs text-description mb-4">
|
||||
<p>
|
||||
Toggle to enable/disable (chmod +x/-x).{" "}
|
||||
<VSCodeLink
|
||||
className="text-xs"
|
||||
href="https://docs.cline.bot/features/hooks"
|
||||
style={{ display: "inline", fontSize: "inherit" }}>
|
||||
Docs
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
</div>
|
||||
{/* Hooks Tab */}
|
||||
{/* Windows warning banner */}
|
||||
{isWindows && (
|
||||
<div className="flex items-center gap-2 px-5 py-3 mb-4 bg-vscode-inputValidation-warningBackground border-l-[3px] border-vscode-inputValidation-warningBorder">
|
||||
<i className="codicon codicon-warning text-sm" />
|
||||
<span className="text-base">
|
||||
Hook toggling is not supported on Windows. Hooks can be created, edited, and deleted,
|
||||
but cannot be enabled/disabled and will not execute.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Hooks */}
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Global Hooks</div>
|
||||
<div className="flex flex-col gap-0">
|
||||
{workspace.hooks
|
||||
{globalHooks
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((hook) => (
|
||||
<HookRow
|
||||
absolutePath={hook.absolutePath}
|
||||
enabled={hook.enabled}
|
||||
hookName={hook.name}
|
||||
isGlobal={false}
|
||||
isGlobal={true}
|
||||
isWindows={isWindows}
|
||||
key={hook.absolutePath}
|
||||
key={hook.name}
|
||||
onDelete={(hooksToggles) => {
|
||||
// Use response data directly, no need to refresh
|
||||
setGlobalHooks(hooksToggles.globalHooks || [])
|
||||
setWorkspaceHooks(hooksToggles.workspaceHooks || [])
|
||||
}}
|
||||
onToggle={(name: string, newEnabled: boolean) =>
|
||||
toggleHook(false, name, newEnabled, workspace.workspaceName)
|
||||
toggleHook(true, name, newEnabled)
|
||||
}
|
||||
workspaceName={workspace.workspaceName}
|
||||
/>
|
||||
))}
|
||||
<NewRuleRow
|
||||
existingHooks={workspace.hooks.map((h) => h.name)}
|
||||
isGlobal={false}
|
||||
existingHooks={globalHooks.map((h) => h.name)}
|
||||
isGlobal={true}
|
||||
ruleType="hook"
|
||||
workspaceName={workspace.workspaceName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Workspace Hooks - one section per workspace */}
|
||||
{workspaceHooks.map((workspace, index) => (
|
||||
<div
|
||||
key={workspace.workspaceName}
|
||||
style={{ marginBottom: index === workspaceHooks.length - 1 ? -10 : 12 }}>
|
||||
<div className="text-sm font-normal mb-2">
|
||||
{workspace.workspaceName}/.clinerules/hooks/
|
||||
</div>
|
||||
<div className="flex flex-col gap-0">
|
||||
{workspace.hooks
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((hook) => (
|
||||
<HookRow
|
||||
absolutePath={hook.absolutePath}
|
||||
enabled={hook.enabled}
|
||||
hookName={hook.name}
|
||||
isGlobal={false}
|
||||
isWindows={isWindows}
|
||||
key={hook.absolutePath}
|
||||
onDelete={(hooksToggles) => {
|
||||
// Use response data directly, no need to refresh
|
||||
setGlobalHooks(hooksToggles.globalHooks || [])
|
||||
setWorkspaceHooks(hooksToggles.workspaceHooks || [])
|
||||
}}
|
||||
onToggle={(name: string, newEnabled: boolean) =>
|
||||
toggleHook(false, name, newEnabled, workspace.workspaceName)
|
||||
}
|
||||
workspaceName={workspace.workspaceName}
|
||||
/>
|
||||
))}
|
||||
<NewRuleRow
|
||||
existingHooks={workspace.hooks.map((h) => h.name)}
|
||||
isGlobal={false}
|
||||
ruleType="hook"
|
||||
workspaceName={workspace.workspaceName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { RuleFileRequest } from "@shared/proto/index.cline"
|
||||
import { InfoIcon, PenIcon, Trash2Icon } from "lucide-react"
|
||||
import { REMOTE_URI_SCHEME } from "@shared/remote-config/constants"
|
||||
import { EyeIcon, InfoIcon, PenIcon, Trash2Icon } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { FileServiceClient } from "@/services/grpc-client"
|
||||
|
||||
const RuleRow: React.FC<{
|
||||
@@ -81,7 +81,9 @@ const RuleRow: React.FC<{
|
||||
}
|
||||
|
||||
const handleEditClick = () => {
|
||||
FileServiceClient.openFile(StringRequest.create({ value: rulePath })).catch((err) =>
|
||||
// For remote rules, use the special remote:// URI format
|
||||
const filePath = isRemote ? `${REMOTE_URI_SCHEME}${ruleType === "workflow" ? "workflow" : "rule"}/${rulePath}` : rulePath
|
||||
FileServiceClient.openFile(StringRequest.create({ value: filePath })).catch((err) =>
|
||||
console.error("Failed to open file:", err),
|
||||
)
|
||||
}
|
||||
@@ -98,10 +100,7 @@ const RuleRow: React.FC<{
|
||||
|
||||
return (
|
||||
<div className="mb-2.5">
|
||||
<div
|
||||
className={cn("flex items-center px-2 py-4 rounded bg-text-block-background max-h-4", {
|
||||
"opacity-60": isDisabled,
|
||||
})}>
|
||||
<div className="flex items-center px-2 py-4 rounded bg-text-block-background max-h-4">
|
||||
<span className="flex-1 overflow-hidden break-all whitespace-normal flex items-center mr-1" title={rulePath}>
|
||||
{getRuleTypeIcon() && <span className="mr-1.5">{getRuleTypeIcon()}</span>}
|
||||
<span className="ph-no-capture">{finalDisplayName}</span>
|
||||
@@ -128,13 +127,12 @@ const RuleRow: React.FC<{
|
||||
title={isDisabled ? "This rule is required and cannot be disabled" : undefined}
|
||||
/>
|
||||
<Button
|
||||
aria-label="Edit rule file"
|
||||
disabled={isRemote}
|
||||
aria-label={isRemote ? "View rule file" : "Edit rule file"}
|
||||
onClick={handleEditClick}
|
||||
size="xs"
|
||||
title="Edit rule file"
|
||||
title={isRemote ? "View rule file (read-only)" : "Edit rule file"}
|
||||
variant="icon">
|
||||
<PenIcon />
|
||||
{isRemote ? <EyeIcon /> : <PenIcon />}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Delete rule file"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { AccountServiceClient, StateServiceClient } from "@/services/grpc-client
|
||||
import { getAsVar, VSC_INACTIVE_SELECTION_BACKGROUND } from "@/utils/vscStyles"
|
||||
import { useApiConfigurationHandlers } from "../settings/utils/useApiConfigurationHandlers"
|
||||
|
||||
export const CURRENT_MODEL_BANNER_VERSION = 2
|
||||
export const CURRENT_MODEL_BANNER_VERSION = 3
|
||||
|
||||
export const NewModelBanner: React.FC = () => {
|
||||
const { clineUser } = useClineAuth()
|
||||
@@ -31,7 +31,7 @@ export const NewModelBanner: React.FC = () => {
|
||||
}, [])
|
||||
|
||||
const setNewModel = () => {
|
||||
const modelId = "anthropic/claude-haiku-4.5"
|
||||
const modelId = "anthropic/claude-opus-4.5"
|
||||
// set both plan and act modes to use new model
|
||||
handleFieldsChange({
|
||||
planModeOpenRouterModelId: modelId,
|
||||
@@ -74,10 +74,10 @@ export const NewModelBanner: React.FC = () => {
|
||||
}}>
|
||||
<h4 className="m-0 flex items-center gap-2">
|
||||
<Megaphone className="w-4 h-4" />
|
||||
Claude Haiku 4.5
|
||||
Claude Opus 4.5
|
||||
</h4>
|
||||
<p className="m-0">
|
||||
Anthropic's fastest model with frontier-level coding intelligence at a fraction of the cost.{" "}
|
||||
Anthropic's latest model with SOTA performance at 3x lower cost than Opus 4.1.{" "}
|
||||
<span className="text-link cursor-pointer">{user ? "Try new model" : "Try with Cline account"} →</span>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -52,21 +52,15 @@ const featuredModels = [
|
||||
label: "Best",
|
||||
},
|
||||
{
|
||||
id: "google/gemini-3-pro-preview",
|
||||
description: "Google's latest reasoning model with 1M context window",
|
||||
id: "anthropic/claude-opus-4.5",
|
||||
description: "SOTA performance on coding at 3x lower cost than Opus 4.1",
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
id: "openai/gpt-5.1",
|
||||
description: "Latest flagship model from OpenAI with enhanced coding capabilities",
|
||||
id: "google/gemini-3-pro-preview",
|
||||
description: "Google's latest reasoning model with 1M context window",
|
||||
label: "Trending",
|
||||
},
|
||||
{
|
||||
id: "minimax/minimax-m2",
|
||||
description: "Compact, high-efficiency model optimized for coding and agentic workflows",
|
||||
label: "Free",
|
||||
isFree: true,
|
||||
},
|
||||
{
|
||||
id: "x-ai/grok-code-fast-1",
|
||||
description: "Advanced model with 262K context for complex coding",
|
||||
@@ -269,6 +263,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup,
|
||||
selectedModelId?.toLowerCase().includes("claude-sonnet-4") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-opus-4.1") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-opus-4") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-opus-4.5") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-3-7-sonnet") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-3.7-sonnet") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-3.7-sonnet:thinking")
|
||||
|
||||
@@ -15,6 +15,7 @@ export const SUPPORTED_ANTHROPIC_THINKING_MODELS = [
|
||||
"claude-3-7-sonnet-20250219",
|
||||
"claude-sonnet-4-20250514",
|
||||
`claude-sonnet-4-20250514${CLAUDE_SONNET_1M_SUFFIX}`,
|
||||
"claude-opus-4-5-20251101",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-opus-4-1-20250805",
|
||||
"claude-sonnet-4-5-20250929",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { askSageDefaultURL, askSageModels } from "@shared/api"
|
||||
import { askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
import { DebouncedTextField } from "../common/DebouncedTextField"
|
||||
@@ -23,10 +24,49 @@ interface AskSageProviderProps {
|
||||
export const AskSageProvider = ({ showModelOptions, isPopup, currentMode }: AskSageProviderProps) => {
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
|
||||
const [availableModels, setAvailableModels] = useState<Record<string, ModelInfo>>(askSageModels)
|
||||
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchModels = async () => {
|
||||
try {
|
||||
const apiUrl = apiConfiguration?.asksageApiUrl || askSageDefaultURL
|
||||
const response = await fetch(`${apiUrl}/get-models`)
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch AskSage models, falling back to default list.")
|
||||
setAvailableModels(askSageModels)
|
||||
return
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const modelIds = data.response as string[]
|
||||
|
||||
if (Array.isArray(modelIds) && modelIds.length > 0) {
|
||||
const filteredModels = Object.entries(askSageModels)
|
||||
.filter(([id]) => modelIds.includes(id))
|
||||
.reduce(
|
||||
(acc, [id, info]) => {
|
||||
acc[id] = info
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, ModelInfo>,
|
||||
)
|
||||
setAvailableModels(Object.keys(filteredModels).length > 0 ? filteredModels : askSageModels)
|
||||
} else {
|
||||
setAvailableModels(askSageModels)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching AskSage models:", error)
|
||||
setAvailableModels(askSageModels)
|
||||
}
|
||||
}
|
||||
|
||||
fetchModels()
|
||||
}, [apiConfiguration?.asksageApiUrl])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ApiKeyField
|
||||
@@ -49,7 +89,7 @@ export const AskSageProvider = ({ showModelOptions, isPopup, currentMode }: AskS
|
||||
<>
|
||||
<ModelSelector
|
||||
label="Model"
|
||||
models={askSageModels}
|
||||
models={availableModels}
|
||||
onChange={(e) =>
|
||||
handleModeFieldChange(
|
||||
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user