Compare commits

..
Author SHA1 Message Date
Claude 676da8982c feat: install gh CLI and add GITHUB_TOKEN support in session hook
- Rename session-start.sh to claude-code-for-web-setup.sh
- Install latest gh CLI from GitHub releases
- Check for GITHUB_TOKEN and inform Claude about gh availability
- Enables using `gh issue`, `gh pr` commands when token is configured
2025-12-22 05:48:24 +00:00
Claude d51da744c0 fix: include node_modules and generated files in worktreeinclude
Copying these to worktrees saves significant setup time:
- node_modules: skips npm install (~1-2 min)
- src/generated/, src/shared/proto/: skips proto generation
2025-12-22 05:34:34 +00:00
Claude 7fca787e70 feat: add .worktreeinclude for Claude Code worktrees
Ensures environment files and local settings are copied to new worktrees:
- .env files
- .clineignore
- Local Claude settings
2025-12-20 18:59:01 +00:00
Claude 9207e8bb30 feat: add SessionStart hook for Claude Code on the web
Adds a session-start hook that runs in remote environments to:
- Install all dependencies (npm run install:all)
- Generate gRPC/protobuf types (npm run protos)

This enables Claude Code web sessions to properly run tests and linters.
2025-12-20 18:56:37 +00:00
136 changed files with 2813 additions and 6498 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Refactor Anthropic handler to use metadata for reasoning support
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix cline auth for bedrock provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Prevent duplicate error messages during streaming for Diff Edit tool when Parallel Tool Calling is not enabled.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Exclude files without extensions (and dotfiles) from getDiffSet results if they are binary
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: preserve file endings and trailing newlines across all edit tools
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix Cerebras rate limiting by using conservative max_tokens (16K) instead of model maximum.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix issues where platform-based content was not displayed correctly.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix banner carousel styling and dismiss functionality
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fixed Auto Compact not triggering when using Claude Code provider. Short model aliases like "sonnet" and "opus" are now correctly recognized as Claude 4+ models.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix Workspace and Favorites history filters to work independently instead of being mutually exclusive
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fixed connection failures with remote MCP servers that return 404 instead of 405 for SSE stream checks. This was causing "Failed to open SSE stream: Not Found" errors after the v3.46.0 SDK upgrade.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Improve the auth state tracking and reduce logouts caused by errors
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: correct typos in gemini system prompt overrides
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
feat(prompts): modify prompts for parallel tool usage in claude and gemini 3 models
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Refactor Bedrock provider to use metadata for reasoning support
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
add multi-root workspace support to cline CLI
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Stop automatically opening Cline sidebar on extension update - only show a notification
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
add supportsReasoning property to Baseten models
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
bump go version
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
show slash command autocompletion in the cli
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add background edit mode with webview diff view.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix sapaicore security issue
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix regression that broke JSON parsing for SAP AI Core provider in native API mode for claude models
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: Fetch remote config values from the cache
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix Baseten model selector issue in model picker modal mode
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Replace current diff edit tools with Apply Patch tool for GPT-5+ models
-173
View File
@@ -1,173 +0,0 @@
name: Claude Issue Triage
on:
issues:
types: [opened]
# Manual trigger for backfilling existing issues. Run from terminal:
# gh workflow run claude-issue-triage.yml -f issue_number=1234
# Or batch process:
# gh issue list --state open --limit 10 --json number --jq '.[].number' | while read num; do
# gh workflow run claude-issue-triage.yml -f issue_number=$num
# sleep 60
# done
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to triage'
required: true
type: string
jobs:
claude-issue-triage:
runs-on: ubuntu-latest
timeout-minutes: 120
# SECURITY: These permissions are intentionally restrictive.
# - contents: read -> Claude can read the codebase but CANNOT write/push any code
# - issues: write -> Claude can comment and add labels (the only write access needed)
# - pull-requests: read -> Claude can view PR context but CANNOT create PRs
# This ensures that even if a malicious user attempts prompt injection via issue content,
# Claude cannot modify repository code, create branches, or open PRs.
permissions:
contents: read
issues: write
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Issue Response & Triage
id: triage
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
# Allow all tools - security is enforced by GitHub permissions above (contents: read, issues: write)
claude_args: --model claude-opus-4-5-20251101 --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch"
prompt: |
You're a GitHub issue first responder for the open source Cline repository.
**Issue:** #${{ github.event.issue.number || inputs.issue_number }}
**Title:** ${{ github.event.issue.title || 'See issue details below' }}
**Author:** @${{ github.event.issue.user.login || 'See issue details below' }}
## Your job
Investigate this issue thoroughly, then post a single helpful comment that helps the user and gives maintainers the context they need.
## Investigation
Start by reading the full issue:
gh issue view ${{ github.event.issue.number || inputs.issue_number }}
### Search for duplicates and related issues
Search thoroughly for existing issues that match this one:
gh issue list --search "<keywords from the issue>" --state all --limit 30
gh issue list --search "<error messages>" --state all --limit 20
gh issue list --search "<affected feature/component>" --state all --limit 20
For each relevant issue you find, read it including its comments:
gh issue view <number> --comments
You're looking for:
- **Duplicates**: Issues describing the same problem. Link to them and explain why you think they're duplicates. If closed, check how they were resolved - the solution might apply here.
- **Related issues**: Similar problems or context that could help. Pull useful information from their comments (workarounds others found, debugging steps that helped, maintainer explanations). Link to them and explain the connection.
If there are closed issues with solutions, surface those solutions prominently - this might immediately solve the user's problem.
### Analyze recent changes (ALWAYS DO THIS)
Many issues are regressions from recent releases. **Always** check what changed recently:
gh release list --limit 10
gh pr list --state merged --limit 50 --json number,title,mergedAt,author,body
Look for PRs merged in the last few weeks that might correlate with the issue. If you find a likely connection:
gh pr view <number>
gh pr diff <number>
git log --since="1 month ago" --oneline -- <relevant paths>
git show <commit>
**Always include your findings in your comment:**
- If you find a regression, call it out explicitly: which PR/commit likely caused it, who authored it, what changed, and suggest a fix direction if you can see one.
- If you don't find anything related, still mention it: "I analyzed recent PRs and releases but didn't find any changes that seem related to this issue."
### Search the codebase
Find the relevant code:
- Use grep/find to locate code related to the issue
- Key areas: `src/api/` (providers/models), `src/core/prompts/` (tools/prompts), platform-specific code for VS Code vs JetBrains
### Find documentation
Cline docs are at **https://docs.cline.bot/** and built with Mintlify from the `docs/` directory.
The URL structure maps directly to the file structure:
- `docs/getting-started/selecting-your-model.mdx` → https://docs.cline.bot/getting-started/selecting-your-model
- `docs/troubleshooting.mdx` → https://docs.cline.bot/troubleshooting
- Headings become anchors: `## Which Model` → `#which-model`
Search the `docs/` directory to find relevant documentation, then construct URLs to link users to:
```bash
ls docs/
grep -r "keyword" docs/ --include="*.mdx" -l
```
### Identify subject matter experts
For issues that clearly need engineering attention:
git log --since="6 months ago" --format="%an" -- <relevant paths> | sort | uniq -c | sort -rn | head -5
Cross-reference with GitHub usernames. Include in your response (@mention, do NOT assign):
| SME | Reason |
|-----|--------|
| @username1 | Authored PR #X which modified this area |
| @username2 | Primary contributor to affected file |
## Weak model detection
Many issues are caused by users running small or non-frontier models that don't tool-call reliably. Signs include:
- Model failing to use tools correctly
- Nonsensical or malformed responses
- User is running a small/local model or older model version
If this looks like a weak model issue, kindly suggest they try reproducing with Claude Sonnet and report back if it persists. Link to https://docs.cline.bot/getting-started/selecting-your-model if helpful. Still label and triage normally.
## Your comment
Write a single comment as a helpful community member. Be conversational, not robotic. Include what's relevant:
- **Helpful response** - Answer their question, suggest a fix, provide a workaround. If you found solutions in related closed issues, surface those prominently.
- **Duplicates and related issues** - Link to any you found and explain why they're duplicates/related. Summarize useful context from their comments.
- **Regression analysis** - If this looks like a regression, explain what change likely caused it, link to the PR/commit, and tag the author.
- **Clarifying questions** - If you need more info, ask specific questions. Don't ask for things already provided.
- **SME table** - Include the table above if this needs engineering attention. Don't tag people for questions with obvious answers or weak-model issues.
- **Context for maintainers** - Relevant code paths, what you found. Keep it concise.
- **Docs links** - If there's relevant documentation, link to it naturally in your response as a recommendation (e.g., "For more details, check out [the Ollama setup guide](url)"). Do NOT add a "Sources" section at the end - integrate doc links into your response where they're helpful.
- **Possible Duplicates section** - ALWAYS include a "Possible Duplicates" section at the end of your comment listing issues that might be duplicates so maintainers can quickly close if appropriate. If none found, say "No obvious duplicates found."
## Labels
First, retrieve all available labels and read their descriptions to understand what each is for:
gh label list --json name,description --limit 100
Then apply the appropriate labels based on your analysis. Only use labels from the list above—do not create new labels.
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "label1,label2"
If your regression analysis found a likely culprit (a recent PR/commit that probably caused this issue), add the "Regression" label:
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "Regression"
IMPORTANT: After posting your comment, add the "Bot Responded" label to indicate this issue has received an automated response:
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "Bot Responded"
## Remember
- **This is a one-time automated response** - you will NOT see their reply or respond again. Never say things like "I can help you", "let me know", "once I have that info", or "I can give you more targeted help" - you won't be there to follow up. If you ask clarifying questions, frame them for the maintainers who will follow up, e.g., "If you can share X, that would help the maintainers diagnose this."
- Don't be formulaic. Respond to what the issue actually needs.
- Surface solutions from past issues - often the fastest path to helping.
- Connecting regressions to specific changes is extremely valuable.
- Link issues with #number so they're clickable.
-272
View File
@@ -1,272 +0,0 @@
name: Claude PR Review
on:
pull_request:
types: [opened, ready_for_review]
# Manual trigger for backfilling existing PRs. Run from terminal:
# gh workflow run claude-pr-review.yml -f pr_number=1234
# Or batch process open PRs:
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
# gh workflow run claude-pr-review.yml -f pr_number=$num
# sleep 60
# done
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to review'
required: true
type: string
jobs:
claude-pr-review:
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
if: |
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 120
# SECURITY: These permissions are intentionally restrictive.
# - contents: read -> Claude can read the codebase but CANNOT write/push any code
# - pull-requests: write -> Claude can post reviews and inline suggestions
# - issues: read -> Claude can search for related issues
# NOTE: Even with pull-requests: write, Claude CANNOT merge PRs because branch protection
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
permissions:
contents: read
pull-requests: write
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get PR number
id: pr
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
else
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
fi
- name: Run PR Review
id: review
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
claude_args: --model claude-opus-4-5-20251101 --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch"
prompt: |
You're a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
PR: #${{ steps.pr.outputs.number }}
## Gather context
```bash
# Get full PR details
gh pr view ${{ steps.pr.outputs.number }} --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
# Get the diff
gh pr diff ${{ steps.pr.outputs.number }}
# Check CI status
gh pr checks ${{ steps.pr.outputs.number }}
# Get existing review comments (to understand context and your previous feedback)
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments --jq '.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'
# Get conversation comments
gh pr view ${{ steps.pr.outputs.number }} --comments
```
If this is a re-review (workflow_dispatch event):
Read your previous comments carefully. Understand what you asked for before.
Check if new commits or comments address your previous feedback.
## Check contributing guidelines
Flag (but don't block) if:
- Missing changeset - For user-facing changes, check if there's a `.changeset/` file:
```bash
gh pr diff ${{ steps.pr.outputs.number }} --name-only | grep '.changeset/' || echo "No changeset found"
```
If missing, ask them to run `npm run changeset`
- Missing tests - New features should have tests
## Find related issues and PRs
Search thoroughly for context that might help with the review:
```bash
# Find related issues for context
gh issue list --search "<keywords from the PR>" --state all --limit 30
gh issue list --search "<error messages or feature names>" --state all --limit 20
# Find similar PRs for reference
gh pr list --search "<keywords>" --state all --limit 30
```
For each relevant issue or PR you find, read it including comments:
```bash
gh issue view <number> --comments
gh pr view <number> --comments
```
Look for:
- Open issues this PR might fix that weren't linked in the description
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
- Context from maintainer discussions that could inform your review
## Find subject matter experts
For files changed in this PR, find who knows the code best:
```bash
# Get files changed
gh pr diff ${{ steps.pr.outputs.number }} --name-only
# For each relevant path, find contributors
git log --since="6 months ago" --format="%an" -- <path> | sort | uniq -c | sort -rn | head -5
```
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
| SME | Reason |
|-----|--------|
| @username1 | Authored PR #X which modified this area |
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
| @username3 | Reviewed similar PR #Y with extensive feedback |
## Deep code review
This is the most important part. Don't just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
Step 1: Understand the intent
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
Step 2: Form your own opinion first
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
Step 3: Compare approaches
Now look at their implementation. How does it compare to what you would have done?
- Is their approach better in some ways? Note what they did well.
- Is their approach missing something? Be specific about what and why.
- Are there edge cases they haven't considered?
- Does it follow the patterns established in similar parts of the codebase?
Step 4: Look at the bigger picture
- What other files or systems does this change interact with?
- Could this break anything else?
- Is there additional work needed beyond this PR to complete the feature?
- Does this fit well with the overall architecture?
Step 5: Find reference implementations
Look for similar changes in the codebase:
```bash
git log --oneline --all --grep="<relevant keywords>" | head -20
git log --oneline -- <similar files> | head -20
```
If this is adding a new API provider, look at how other providers are implemented.
If this is adding a new feature, look at how similar features were added.
Note where their implementation aligns with or diverges from established patterns.
Step 6: Standard code review checks
- DRY: Is there duplicated code that could be extracted?
- Error handling: Are errors handled appropriately?
- Security: Any injection risks, credential exposure, unsafe dependencies?
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
- Types: Is TypeScript used correctly? Any unsafe type assertions?
- Naming: Are variables and functions named clearly?
- Comments: Is complex logic explained? Are there outdated comments?
## Inline code suggestions
For specific code improvements, use GitHub's suggestion syntax via `gh api`.
This creates suggestions the author can commit with one click.
Single-line suggestion:
```bash
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews \
-X POST \
-f commit_id="$(gh pr view ${{ steps.pr.outputs.number }} --json headRefOid -q .headRefOid)" \
-f event="COMMENT" \
-f body="" \
-F comments='[
{
"path": "src/example.ts",
"line": 42,
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
}
]'
```
Multi-line suggestion (replacing lines 40-45):
```bash
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews \
-X POST \
-f commit_id="$(gh pr view ${{ steps.pr.outputs.number }} --json headRefOid -q .headRefOid)" \
-f event="COMMENT" \
-f body="" \
-F comments='[
{
"path": "src/example.ts",
"start_line": 40,
"line": 45,
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
}
]'
```
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
## Post your review
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
Start with a warm thank you for their contribution. Be conversational, not robotic.
Include what's relevant:
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author's intent, why they made the changes, how they implemented it, and what files/systems are affected. Don't just summarize - explain.
- Related issues/PRs you found that provide useful context (link to them)
- Your review findings (issues to address, suggestions, etc.)
- Clear next steps for the author
Include a "For Maintainers" section with:
- Anything else useful to help the maintainer resolve this PR
- Related issues/PRs with context on why they're relevant
- Open issues this PR might fix that weren't linked in the description
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
- SME table - who should review this and why
For the SME table:
| SME | Reason |
|-----|--------|
| @username | Primary contributor to affected files |
## Update labels
Add appropriate labels based on your analysis:
```bash
gh label list --json name,description --limit 100
gh pr edit ${{ steps.pr.outputs.number }} --add-label "label1,label2"
```
When done, add the reviewed label:
```bash
gh pr edit ${{ steps.pr.outputs.number }} --add-label "Bot Reviewed"
```
## Remember
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like "let me know if you have questions", "I can help you with", or "feel free to ask" - you won't be there to follow up. Frame any questions for the maintainers who will follow up.
- Be helpful and welcoming - Many contributors are new to the project
- Be specific - Point to exact lines and suggest fixes, don't give vague feedback
- Think deeply - Don't just surface-level review, understand the intent and evaluate the approach
- Use inline suggestions - Make it easy for authors to accept changes
- You're a first-pass reviewer - A human maintainer will do final approval
-4
View File
@@ -8,14 +8,12 @@ tmp
.DS_Store
.idea
.husky/_/
pnpm-lock.yaml
.clineignore
.venv
.actrc
CLAUDE.local.md
webview-ui/src/**/*.js
webview-ui/src/**/*.js.map
@@ -30,8 +28,6 @@ coverage-unit
*evals.env
.env
.worktrees
## Generated files ##
src/generated/
src/shared/proto/
-1
View File
@@ -1 +0,0 @@
.gitignore
+8
View File
@@ -0,0 +1,8 @@
# Dependencies
node_modules
webview-ui/node_modules
# Generated proto files
src/generated/
src/shared/proto/
webview-ui/src/services/grpc-client.ts
-28
View File
@@ -1,33 +1,5 @@
# Changelog
## [3.46.1]
### Fixed
- Remove GLM 4.6 from free models
## [3.46.0]
### Added
- Added GLM 4.7 model
- Enhanced background terminal execution with command tracking, log file output, zombie process prevention (10-minute timeout), and clickable log paths in UI
- Apply Patch tool for GPT-5+ models (replacing current diff edit tools)
### Fixed
- Duplicate error messages during streaming for Diff Edit tool when Parallel Tool Calling is not enabled
- Banner carousel styling and dismiss functionality
- Typos in Gemini system prompt overrides
- Model picker favorites ordering, star toggle, and keyboard navigation for OpenRouter and Vercel AI Gateway providers
- Fetch remote config values from the cache
### Refactored
- Anthropic handler to use metadata for reasoning support
- Bedrock provider to use metadata for reasoning support
## [3.45.1]
- Fixed MCP settings race condition where toggling auto-approve or changing timeout settings would cause the UI to flash and revert
-4
View File
@@ -14,10 +14,6 @@ This file is the secret sauce for working effectively in this codebase. It captu
**What NOT to add:** Stuff you can figure out from reading a few files, obvious patterns, or standard practices. This file should be high-signal, not comprehensive.
## Miscellaneous
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
- When creating PRs, if the change is user-facing and significant enough to warrant a changelog entry, run `npm run changeset` and create a patch changeset. Never create minor or major version bumps. Skip changesets for trivial fixes, internal refactors, or minor UI tweaks that users wouldn't notice.
## gRPC/Protobuf Communication
The extension and webview communicate via gRPC-like protocol over VS Code message passing.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

+1 -3
View File
@@ -278,7 +278,6 @@
"pages": [
"enterprise-solutions/overview",
"enterprise-solutions/onboarding",
"enterprise-solutions/sso-setup",
"enterprise-solutions/team-management/managing-members",
{
"group": "SaaS Provider Configuration",
@@ -318,8 +317,7 @@
"pages": [
"enterprise-solutions/monitoring/overview",
"enterprise-solutions/monitoring/telemetry",
"enterprise-solutions/monitoring/opentelemetry",
"enterprise-solutions/monitoring/opentelemetry_override"
"enterprise-solutions/monitoring/opentelemetry"
]
}
]
@@ -31,7 +31,7 @@ Check which models are available in your region first. Some newer models might n
<Frame>
<img
src="https://assets.int.cline.bot/assets/AWS%20Remote%20Config.gif"
src="https://storage.googleapis.com/cline-static-assets-prod/assets/AWS%20Remote%20Config.gif"
/>
</Frame>
@@ -27,7 +27,7 @@ If you don't have AWS credentials yet, reach out to your IT or cloud team to get
<Frame>
<img
src="https://assets.int.cline.bot/assets/VS%20Code%20Bedrock%20API%20Key.gif"
src="https://storage.googleapis.com/cline-static-assets-prod/assets/VS%20Code%20Bedrock%20API%20Key.gif"
/>
</Frame>
@@ -42,45 +42,76 @@ Cline supports three OTLP export protocols:
- **HTTP/protobuf**
- **HTTP/JSON**
### Export Destinations
You can export to:
- **Console** (for testing)
- **OTLP endpoint** (your own collector or observability platform)
## Configuration
OpenTelemetry is configured using [Remote Configuration](/enterprise-solutions/configuration/remote-configuration/overview#how-remote-configuration-works) from the [dashboard](https://app.cline.bot/dashboard/organization?tab=settings).
OpenTelemetry is configured using environment variables before launching Cline.
### Basic Setup
Enable OpenTelemetry, configure an OTLP endpoint and select a protocol:
Enable OpenTelemetry and configure an OTLP endpoint:
<Frame>
<img
src="https://assets.int.cline.bot/assets/open_telemetry_main_options.png"
/>
</Frame>
```bash
# Enable OpenTelemetry
export OTEL_TELEMETRY_ENABLED=1
If you're using gRPC, you can opt out of TLS.
# Configure metrics and logs export
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
Once the collector has been configured, you can enable logs and/or metrics collection. At least one of them needs to be enabled.
# Set your OTLP endpoint
export OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4317
You only need to configure it further if you need an advanced configuration.
# Optional: Set protocol (default is grpc)
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
```
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `OTEL_TELEMETRY_ENABLED` | Enable OpenTelemetry (`1` or `true`) | Disabled |
| `OTEL_METRICS_EXPORTER` | Metrics exporter type (`console`, `otlp`, or both) | None |
| `OTEL_LOGS_EXPORTER` | Logs exporter type (`console`, `otlp`, or both) | None |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL | None |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol (`grpc`, `http/protobuf`, `http/json`) | `grpc` |
| `OTEL_EXPORTER_OTLP_INSECURE` | Allow insecure connections | `false` |
| `OTEL_EXPORTER_OTLP_HEADERS` | Custom headers (comma-separated `key=value` pairs) | None |
### Advanced Configuration
You can add custom protocols and endpoints for both, logs and metrics. You can also configure the metrics export interval, and the logs batch size, batch timeout and max queue size.
<Frame>
<img
src="https://assets.int.cline.bot/assets/open_telemetry_metrics_and_logs.png"
/>
</Frame>
**Separate endpoints for metrics and logs:**
```bash
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://metrics-collector:4317
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs-collector:4317
```
**Custom headers for authentication:**
```bash
export OTEL_EXPORTER_OTLP_HEADERS="api-key=your-key,x-custom-header=value"
```
Finally, if your collector needs authentication headers, you can add key value pairs in the headers section.
**Multiple exporters (console + OTLP):**
```bash
export OTEL_METRICS_EXPORTER=console,otlp
export OTEL_LOGS_EXPORTER=console,otlp
```
<Frame>
<img
src="https://assets.int.cline.bot/assets/open_telemetry_headers.png"
/>
</Frame>
**Export intervals:**
```bash
# Metrics export interval in milliseconds (default: 60000)
export OTEL_METRIC_EXPORT_INTERVAL=30000
# Logs batch size and timeout
export OTEL_LOG_BATCH_SIZE=512
export OTEL_LOG_BATCH_TIMEOUT=5000
export OTEL_LOG_MAX_QUEUE_SIZE=2048
```
## Integration Examples
@@ -88,46 +119,73 @@ Finally, if your collector needs authentication headers, you can add key value p
Export to Datadog using their OTLP endpoint:
<Frame>
<img
src="https://assets.int.cline.bot/assets/open_telemetry_datadog_example.png"
/>
</Frame>
```bash
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.datadoghq.com
export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
```
### New Relic
Export to New Relic:
<Frame>
<img
src="https://assets.int.cline.bot/assets/open_telemetry_relic_example.png"
/>
</Frame>
```bash
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4317
export OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
```
### Grafana Cloud
Export to Grafana Cloud:
<Frame>
<img
src="https://assets.int.cline.bot/assets/open_telemetry_grafana_example.png"
/>
</Frame>
```bash
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic YOUR_BASE64_CREDENTIALS"
```
## Testing Configuration
To test your configuration, log in to your account, perform some actions in a task, wait for the export interval, and verify that the data has arrived at your collector.
Test your configuration with console output before sending to a real endpoint:
```bash
# Enable console output to see what data would be exported
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=console
export OTEL_LOGS_EXPORTER=console
```
Then launch Cline and check the console output for metrics and logs.
## Troubleshooting
If you arent getting any data in your collector, the easiest way to verify your integration is to enable the developer tools in your editor.
### No Data Being Exported
To do this, open the [webview developer tools](https://code.visualstudio.com/api/extension-guides/webview#inspecting-and-debugging-webviews).
1. **Verify OpenTelemetry is enabled:**
```bash
echo $OTEL_TELEMETRY_ENABLED
```
Should output `1` or `true`
Once youve done so, if you perform some actions that trigger metrics and/or logs (such as doing a task with Cline),
you will see error logs if any error occurs when sending the data to your collector.
2. **Check exporters are configured:**
```bash
echo $OTEL_METRICS_EXPORTER
echo $OTEL_LOGS_EXPORTER
```
If you don't see any logs, enable [debug mode](#debug-mode).
3. **Test with console exporter first:**
```bash
export OTEL_METRICS_EXPORTER=console
export OTEL_LOGS_EXPORTER=console
```
### Connection Errors
@@ -136,7 +194,10 @@ If you don't see any logs, enable [debug mode](#debug-mode).
curl -v https://your-otlp-endpoint:4317
```
2. **Check if insecure mode is needed** by opting out of TLS
2. **Check if insecure mode is needed:**
```bash
export OTEL_EXPORTER_OTLP_INSECURE=true
```
3. **Verify authentication headers:**
Double-check your API keys and authentication headers are correct
@@ -146,7 +207,7 @@ If you don't see any logs, enable [debug mode](#debug-mode).
Enable debug logging to see detailed OpenTelemetry information:
```bash
TEL_DEBUG_DIAGNOSTICS=true code .
export TEL_DEBUG_DIAGNOSTICS=true
```
This will output detailed information about:
@@ -157,7 +218,7 @@ This will output detailed information about:
## What Gets Exported
When OpenTelemetry is enabled, Cline exports:
When Opentelemetry is enabled, Cline exports:
### Metrics
- Feature usage counts
@@ -177,9 +238,9 @@ Exported data is already anonymous and doesn't include code content, file paths,
## Limitations
Current OpenTelemetry support in Cline:
- ✅ OTLP metrics export (gRPC, HTTP)
- ✅ OTLP logs export (gRPC, HTTP)
- ✅ Basic configuration via [Remote Configuration](/enterprise-solutions/configuration/remote-configuration/overview#how-remote-configuration-works)
- ✅ OTLP metrics export (console, gRPC, HTTP)
- ✅ OTLP logs export (console, gRPC, HTTP)
- ✅ Basic configuration via environment variables
- ❌ Distributed tracing (not yet implemented)
- ❌ Custom instrumentation API (not yet exposed)
- ❌ Sampling configuration (uses defaults)
@@ -1,266 +0,0 @@
---
title: "OpenTelemetry Integration Override"
sidebarTitle: "OpenTelemetry Override"
description: "Export Cline telemetry to your observability platform using OpenTelemetry Protocol (OTLP)"
---
Cline includes opt-in OpenTelemetry support for exporting metrics and logs to your own observability infrastructure using the OpenTelemetry Protocol (OTLP).
<Note>
OpenTelemetry integration is **optional** and intended for advanced users with existing observability infrastructure. Most users won't need this feature.
</Note>
## What is OpenTelemetry?
[OpenTelemetry](https://opentelemetry.io/) is an industry-standard observability framework that provides a unified way to collect and export telemetry data (metrics, logs, and traces).
Cline's OpenTelemetry support allows you to:
- Export telemetry to your own systems
- Integrate with observability platforms like Datadog, New Relic, Grafana Cloud, etc.
- Maintain full control over your monitoring data
- Use your organization's existing monitoring infrastructure
## Supported Features
Cline supports OpenTelemetry's **OTLP (OpenTelemetry Protocol)** export with:
<CardGroup cols={2}>
<Card title="Metrics Export" icon="chart-bar">
Export metrics about Cline usage, performance, and errors
</Card>
<Card title="Logs Export" icon="file-lines">
Export structured logs for debugging and analysis
</Card>
</CardGroup>
### Export Formats
Cline supports three OTLP export protocols:
- **gRPC** (default, recommended)
- **HTTP/protobuf**
- **HTTP/JSON**
### Export Destinations
You can export to:
- **Console** (for testing)
- **OTLP endpoint** (your own collector or observability platform)
## Configuration
OpenTelemetry is configured using environment variables before launching Cline.
### Basic Setup
Enable OpenTelemetry and configure an OTLP endpoint:
```bash
# Enable OpenTelemetry
export OTEL_TELEMETRY_ENABLED=1
# Configure metrics and logs export
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
# Set your OTLP endpoint
export OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4317
# Optional: Set protocol (default is grpc)
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
```
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `OTEL_TELEMETRY_ENABLED` | Enable OpenTelemetry (`1` or `true`) | Disabled |
| `OTEL_METRICS_EXPORTER` | Metrics exporter type (`console`, `otlp`, or both) | None |
| `OTEL_LOGS_EXPORTER` | Logs exporter type (`console`, `otlp`, or both) | None |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL | None |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol (`grpc`, `http/protobuf`, `http/json`) | `grpc` |
| `OTEL_EXPORTER_OTLP_INSECURE` | Allow insecure connections | `false` |
| `OTEL_EXPORTER_OTLP_HEADERS` | Custom headers (comma-separated `key=value` pairs) | None |
### Advanced Configuration
**Separate endpoints for metrics and logs:**
```bash
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://metrics-collector:4317
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs-collector:4317
```
**Custom headers for authentication:**
```bash
export OTEL_EXPORTER_OTLP_HEADERS="api-key=your-key,x-custom-header=value"
```
**Multiple exporters (console + OTLP):**
```bash
export OTEL_METRICS_EXPORTER=console,otlp
export OTEL_LOGS_EXPORTER=console,otlp
```
**Export intervals:**
```bash
# Metrics export interval in milliseconds (default: 60000)
export OTEL_METRIC_EXPORT_INTERVAL=30000
# Logs batch size and timeout
export OTEL_LOG_BATCH_SIZE=512
export OTEL_LOG_BATCH_TIMEOUT=5000
export OTEL_LOG_MAX_QUEUE_SIZE=2048
```
## Integration Examples
### Datadog
Export to Datadog using their OTLP endpoint:
```bash
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.datadoghq.com
export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
```
### New Relic
Export to New Relic:
```bash
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4317
export OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
```
### Grafana Cloud
Export to Grafana Cloud:
```bash
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic YOUR_BASE64_CREDENTIALS"
```
## Testing Configuration
Test your configuration with console output before sending to a real endpoint:
```bash
# Enable console output to see what data would be exported
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=console
export OTEL_LOGS_EXPORTER=console
```
Then launch Cline and check the console output for metrics and logs.
## Troubleshooting
### No Data Being Exported
1. **Verify OpenTelemetry is enabled:**
```bash
echo $OTEL_TELEMETRY_ENABLED
```
Should output `1` or `true`
2. **Check exporters are configured:**
```bash
echo $OTEL_METRICS_EXPORTER
echo $OTEL_LOGS_EXPORTER
```
3. **Test with console exporter first:**
```bash
export OTEL_METRICS_EXPORTER=console
export OTEL_LOGS_EXPORTER=console
```
### Connection Errors
1. **Verify endpoint is accessible:**
```bash
curl -v https://your-otlp-endpoint:4317
```
2. **Check if insecure mode is needed:**
```bash
export OTEL_EXPORTER_OTLP_INSECURE=true
```
3. **Verify authentication headers:**
Double-check your API keys and authentication headers are correct
### Debug Mode
Enable debug logging to see detailed OpenTelemetry information:
```bash
export TEL_DEBUG_DIAGNOSTICS=true
```
This will output detailed information about:
- Configuration being used
- Exporters being created
- Connection attempts
- Export successes/failures
## What Gets Exported
When OpenTelemetry is enabled, Cline exports:
### Metrics
- Feature usage counts
- Task execution metrics
- Error rates and types
- Performance measurements
### Logs
- System events
- Error logs with context
- Operational information
<Warning>
Exported data is already anonymous and doesn't include code content, file paths, or sensitive information. However, you're responsible for securing the data once exported to your systems.
</Warning>
## Limitations
Current OpenTelemetry support in Cline:
- ✅ OTLP metrics export (console, gRPC, HTTP)
- ✅ OTLP logs export (console, gRPC, HTTP)
- ✅ Basic configuration via environment variables
- ❌ Distributed tracing (not yet implemented)
- ❌ Custom instrumentation API (not yet exposed)
- ❌ Sampling configuration (uses defaults)
## Best Practices
1. **Test First**: Always test with console exporter before sending to production
2. **Secure Credentials**: Never hardcode API keys; use secure environment variable management
3. **Monitor Costs**: Be aware of data ingestion costs with your observability platform
4. **Start Simple**: Begin with metrics only, add logs if needed
5. **Use Compression**: OTLP supports compression; check if your endpoint requires it
## Next Steps
<CardGroup cols={2}>
<Card title="Cline Telemetry" icon="chart-simple" href="/enterprise-solutions/monitoring/telemetry">
Configure simple built-in telemetry
</Card>
<Card title="OpenTelemetry Docs" icon="book" href="https://opentelemetry.io/docs/">
Learn more about OpenTelemetry
</Card>
</CardGroup>
@@ -8,20 +8,14 @@ Cline includes optional monitoring capabilities for organizations that want to t
## Monitoring Options
<CardGroup cols={2}>
<Card title="OpenTelemetry" icon="chart-line" href="/enterprise-solutions/monitoring/opentelemetry">
Export metrics and logs to your own observability backends
</Card>
<Card title="OpenTelemetry Override" icon="chart-line" href="/enterprise-solutions/monitoring/opentelemetry_override">
Export to your own observability backends through environment variables (advanced)
</Card>
</CardGroup>
<CardGroup cols={1}>
<CardGroup cols={2}>
<Card title="Cline Telemetry" icon="chart-simple" href="/enterprise-solutions/monitoring/telemetry">
Built-in anonymous usage tracking that helps improve Cline (opt-in)
</Card>
</Card>
<Card title="OpenTelemetry" icon="chart-line" href="/enterprise-solutions/monitoring/opentelemetry">
Export metrics and logs to your own observability backends (advanced)
</Card>
</CardGroup>
## Cline Telemetry
-4
View File
@@ -22,10 +22,6 @@ Your IdP administrator will receive an email with a link to register their organ
### Step 2: Configure Your Identity Provider
<Info>
For a short overview of where SSO configuration lives (Cline dashboard vs WorkOS vs your IdP), see [SSO Setup](/enterprise-solutions/sso-setup).
</Info>
Connect your identity provider (IdP) to WorkOS:
1. In the WorkOS dashboard, go to **AuthKit → Connections**
-61
View File
@@ -1,61 +0,0 @@
---
title: "SSO Setup"
sidebarTitle: "SSO Setup"
description: "Configure Single Sign-On (SSO) for Cline Enterprise via WorkOS AuthKit."
---
## Overview
Cline Enterprise integrates with your identity provider (IdP) via **WorkOS AuthKit** for SSO.
This page describes, at a high level, how SSO is set up for Cline Enterprise using WorkOS AuthKit.
If you havent completed initial onboarding, start with [Onboarding](/enterprise-solutions/onboarding).
## Where setup happens
SSO setup spans two places:
1) **Cline Dashboard (app.cline.bot)**
- Where you sign in and verify SSO works for your organization.
2) **WorkOS dashboard**
- Where your IdP connection is configured (AuthKit → Connections). Your designated admin receives access to this during enterprise onboarding.
## Using the Cline Dashboard
Use the Cline Dashboard at https://app.cline.bot to:
- complete sign-in and onboarding flows
- verify users can authenticate via SSO
## Configure your IdP connection in WorkOS
During enterprise onboarding, your designated admin will receive an invitation email from WorkOS with a link to access your organization's WorkOS dashboard.
<Frame>
<img src="/assets/workos-invite-email.png" alt="WorkOS invitation email example" />
</Frame>
To connect your IdP to Cline Enterprise, configure your identity provider in **WorkOS AuthKit**:
1. In the WorkOS dashboard, go to **AuthKit → Connections**
2. Click **Add Connection**
3. Select your identity provider (e.g., Okta, Microsoft Entra ID/Azure AD, Google Workspace, Generic SAML/OIDC)
4. Follow the provider-specific instructions in WorkOS
WorkOSs UI and required fields vary by provider. For details, follow WorkOS documentation:
- https://workos.com/docs/authkit/sso
## Keycloak note (IdP compatibility)
Cline Enterprises default SSO integration is **via WorkOS**.
If you use **Keycloak** as your IdP, the supported path is to configure Keycloak in WorkOS as a **Generic SAML** or **Generic OIDC** provider (using the settings WorkOS requests for those provider types).
## Verification
After configuring WorkOS:
1) Attempt an SSO sign-in from https://app.cline.bot.
2) Confirm the sign-in completes (you are redirected back successfully).
## Troubleshooting
- **Redirect URI mismatch**: confirm the redirect/callback URL configured in WorkOS matches what was provided during your Cline Enterprise onboarding.
For additional troubleshooting guidance, refer to WorkOS documentation:
- https://workos.com/docs/authkit/sso
@@ -13,6 +13,46 @@ Cline is your AI assistant that can:
- Automate repetitive tasks
- Integrate with external tools
## First Steps
1. **Start a Task**
- Type your request in the chat
- Example: "Create a new React component called Header"
2. **Provide Context**
- Use @ mentions to add files, folders, or URLs
- Example: "@file:src/components/App.tsx"
3. **Review Changes**
- Cline will show diffs before making changes
- You can edit or reject changes
## Key Features
1. **File Editing**
- Create new files
- Modify existing code
- Search and replace across files
2. **Terminal Commands**
- Run npm commands
- Start development servers
- Install dependencies
3. **Code Analysis**
- Find and fix errors
- Refactor code
- Add documentation
4. **Browser Integration**
- Test web pages
- Capture screenshots
- Inspect console logs
## Available Tools
@@ -44,7 +84,6 @@ Cline has access to the following tools for various tasks:
- `ask_followup_question`: Ask user for clarification
- `attempt_completion`: Present final results
Each tool has specific parameters and usage patterns. Here are some examples:
- Create a new file (write_to_file):
@@ -12,19 +12,6 @@ sidebarTitle: "/deep-planning"
/>
</Frame>
## Demo Video
Watch Deep Planning in action as Cline investigates a codebase, asks clarifying questions, and generates a comprehensive implementation plan:
<Frame>
<video
muted
controls
playsInline
src="https://storage.googleapis.com/cline_public_images/docs/assets/Cline-Deep-Planning-Demo.mp4"
/>
</Frame>
When you use `/deep-planning`, Cline follows a four-step process that mirrors how senior developers approach complex features: thorough investigation, discussion & clarification of requirements, detailed planning, and structured task creation with progress tracking.
## The Four-Step Process
+1423 -2311
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -13,7 +13,7 @@
"license": "ISC",
"description": "",
"dependencies": {
"mintlify": "^4.2.249"
"mintlify": "^4.2.23"
},
"overrides": {
"tar-fs": "^3.1.1",
@@ -6,17 +6,12 @@ description: "Complete guide to resolving terminal integration issues in Cline"
This guide helps you resolve terminal integration issues in Cline. Terminal integration is crucial for Cline to execute commands and read their output, enabling it to understand errors, test results, and command responses.
## Try This First: Background Execution Mode
<Tip>
If you're experiencing terminal issues, try switching to a simpler shell like `bash` in the Cline settings, under "Terminal Settings"
The simplest fix for most terminal issues is switching to **Background Execution Mode**:
This resolves most terminal integration problems.
1. Click **Settings** (top right of Cline chat)
2. Go to **Terminal Settings**
3. Set **Terminal Execution Mode** → **Background Exec**
This runs commands in a background process instead of VSCode's terminal, bypassing most integration issues. The guide below is for users who specifically need VSCode's integrated terminal.
---
</Tip>
## Quick Diagnosis Flowchart
+1 -15
View File
@@ -4,21 +4,7 @@ sidebarTitle: "Terminal Quick Fixes"
description: "Quick solutions for common terminal issues"
---
## Try This First: Background Execution Mode
The simplest fix for most terminal issues is switching to **Background Execution Mode**:
1. Click **Settings** (top right of Cline chat)
2. Go to **Terminal Settings**
3. Set **Terminal Execution Mode** → **Background Exec**
This runs commands in a background process instead of VSCode's terminal, bypassing most integration issues.
---
## Other Fixes
If you need VSCode's integrated terminal, try these:
**Here is a list of common fixes, starting with the most applicable:**
- **Switch to bash** (solves most instances)
+1 -1
View File
@@ -1,4 +1,4 @@
streamlit==1.43.2
streamlit>=1.28.0
plotly>=5.17.0
pandas>=2.0.0
numpy>=1.24.0
+69 -176
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.46.1",
"version": "3.45.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.46.1",
"version": "3.45.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -20,7 +20,7 @@
"@grpc/grpc-js": "^1.9.15",
"@grpc/reflection": "^1.0.4",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.25.1",
"@modelcontextprotocol/sdk": "^1.11.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/core": "^2.1.0",
"@opentelemetry/exporter-logs-otlp-grpc": "^0.56.0",
@@ -68,7 +68,6 @@
"image-size": "^2.0.2",
"isbinaryfile": "^5.0.2",
"jschardet": "^3.1.4",
"json5": "^2.2.3",
"mammoth": "^1.11.0",
"nanoid": "^5.1.6",
"nice-grpc": "^2.1.12",
@@ -1187,6 +1186,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2649,6 +2649,7 @@
"node_modules/@grpc/grpc-js": {
"version": "1.9.15",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@grpc/proto-loader": "^0.7.8",
"@types/node": ">=12.12.47"
@@ -2684,18 +2685,6 @@
"@grpc/grpc-js": "^1.8.21"
}
},
"node_modules/@hono/node-server": {
"version": "1.19.7",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz",
"integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
},
"peerDependencies": {
"hono": "^4"
}
},
"node_modules/@inquirer/external-editor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
@@ -3228,12 +3217,12 @@
"license": "BSD-2-Clause"
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.22.0.tgz",
"integrity": "sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@hono/node-server": "^1.19.7",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"content-type": "^1.0.5",
@@ -3243,26 +3232,20 @@
"eventsource-parser": "^3.0.0",
"express": "^5.0.1",
"express-rate-limit": "^7.5.0",
"jose": "^6.1.1",
"json-schema-typed": "^8.0.2",
"pkce-challenge": "^5.0.0",
"raw-body": "^3.0.0",
"zod": "^3.25 || ^4.0",
"zod-to-json-schema": "^3.25.0"
"zod": "^3.23.8",
"zod-to-json-schema": "^3.24.1"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@cfworker/json-schema": "^4.1.1",
"zod": "^3.25 || ^4.0"
"@cfworker/json-schema": "^4.1.1"
},
"peerDependenciesMeta": {
"@cfworker/json-schema": {
"optional": true
},
"zod": {
"optional": false
}
}
},
@@ -3300,6 +3283,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -4915,8 +4899,7 @@
"optional": true,
"os": [
"android"
],
"peer": true
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.4",
@@ -4929,8 +4912,7 @@
"optional": true,
"os": [
"android"
],
"peer": true
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.4",
@@ -4943,8 +4925,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.4",
@@ -4957,8 +4938,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.4",
@@ -4971,8 +4951,7 @@
"optional": true,
"os": [
"freebsd"
],
"peer": true
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.4",
@@ -4985,8 +4964,7 @@
"optional": true,
"os": [
"freebsd"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.4",
@@ -4999,8 +4977,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.4",
@@ -5013,8 +4990,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.4",
@@ -5027,8 +5003,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.4",
@@ -5041,8 +5016,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.4",
@@ -5055,8 +5029,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.4",
@@ -5069,8 +5042,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.4",
@@ -5083,8 +5055,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.4",
@@ -5097,8 +5068,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.4",
@@ -5111,8 +5081,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.4",
@@ -5125,8 +5094,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.4",
@@ -5139,8 +5107,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.4",
@@ -5153,8 +5120,7 @@
"optional": true,
"os": [
"openharmony"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.4",
@@ -5167,8 +5133,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.4",
@@ -5181,8 +5146,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.4",
@@ -5195,8 +5159,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.4",
@@ -5209,8 +5172,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@sap-ai-sdk/ai-api": {
"version": "2.1.0",
@@ -6467,60 +6429,6 @@
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.5.0",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.5.0",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.5",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.5.0",
"@emnapi/runtime": "^1.5.0",
"@tybys/wasm-util": "^0.10.1"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz",
@@ -6763,8 +6671,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/get-folder-size": {
"version": "3.0.4",
@@ -6789,6 +6696,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz",
"integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -7486,6 +7394,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -8252,6 +8161,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -9455,7 +9365,8 @@
},
"node_modules/devtools-protocol": {
"version": "0.0.1342118",
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/diff": {
"version": "5.2.0",
@@ -11440,16 +11351,6 @@
"he": "bin/he"
}
},
"node_modules/hono": {
"version": "4.11.1",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.1.tgz",
"integrity": "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/hosted-git-info": {
"version": "2.8.9",
"dev": true,
@@ -12431,19 +12332,11 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"license": "MIT",
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/jose": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
"integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"dev": true,
@@ -12504,16 +12397,9 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/json-schema-typed": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
"license": "BSD-2-Clause"
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
@@ -12533,12 +12419,10 @@
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"version": "9.0.2",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
@@ -12554,6 +12438,23 @@
"npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/jwa": {
"version": "1.4.2",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jsonwebtoken/node_modules/jws": {
"version": "3.2.2",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"license": "(MIT OR GPL-3.0-or-later)",
@@ -12581,12 +12482,10 @@
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
@@ -12662,6 +12561,7 @@
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
"license": "MPL-2.0",
"peer": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
@@ -15542,7 +15442,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -15563,7 +15462,6 @@
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -16231,7 +16129,6 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -17668,7 +17565,6 @@
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
@@ -17685,7 +17581,6 @@
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -18049,6 +17944,7 @@
"version": "5.5.3",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -18308,7 +18204,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -18383,7 +18278,6 @@
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -19079,17 +18973,16 @@
"node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.25.0",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz",
"integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==",
"version": "3.24.4",
"license": "ISC",
"peerDependencies": {
"zod": "^3.25 || ^4"
"zod": "^3.24.1"
}
}
}
+2 -3
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.46.1",
"version": "3.45.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -463,7 +463,7 @@
"@grpc/grpc-js": "^1.9.15",
"@grpc/reflection": "^1.0.4",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.25.1",
"@modelcontextprotocol/sdk": "^1.11.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/core": "^2.1.0",
"@opentelemetry/exporter-logs-otlp-grpc": "^0.56.0",
@@ -511,7 +511,6 @@
"image-size": "^2.0.2",
"isbinaryfile": "^5.0.2",
"jschardet": "^3.1.4",
"json5": "^2.2.3",
"mammoth": "^1.11.0",
"nanoid": "^5.1.6",
"nice-grpc": "^2.1.12",
-6
View File
@@ -19,8 +19,6 @@ service AccountService {
// Clears API keys and user state.
rpc accountLogoutClicked(EmptyRequest) returns (Empty);
rpc fetchAuth(EmptyRequest) returns (Empty);
// Subscribe to auth status update events (when authentication state changes)
rpc subscribeToAuthStatusUpdate(EmptyRequest) returns (stream AuthState);
@@ -55,10 +53,6 @@ message AuthStateChangedRequest {
message AuthState {
optional UserInfo user = 1;
bool has_session_data = 2;
bool pending = 3;
optional int64 next_retry_at = 4;
optional string error = 5;
}
// User's information
+4 -2
View File
@@ -104,15 +104,17 @@ async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
if (!previousVersion || currentVersion !== previousVersion) {
Logger.log(`Cline version changed: ${previousVersion} -> ${currentVersion}. First run or update detected.`)
// Check if there's a new announcement to show
// Use the same condition as announcements: focus when there's a new announcement to show
const lastShownAnnouncementId = context.globalState.get<string>("lastShownAnnouncementId")
const latestAnnouncementId = getLatestAnnouncementId()
if (lastShownAnnouncementId !== latestAnnouncementId) {
// Show notification when there's a new announcement (major/minor updates or fresh installs)
// Focus Cline when there's a new announcement to show (major/minor updates or fresh installs)
const message = previousVersion
? `Cline has been updated to v${currentVersion}`
: `Welcome to Cline v${currentVersion}`
await HostProvider.workspace.openClineSidebarPanel({})
await new Promise((resolve) => setTimeout(resolve, 200))
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message,
+1 -5
View File
@@ -4,7 +4,6 @@ import { ModelInfo } from "@shared/api"
import OpenAI from "openai"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { sanitizeAnthropicMessages } from "../transform/anthropic-format"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
@@ -145,15 +144,12 @@ export class AIhubmixHandler implements ApiHandler {
const client = this.ensureAnthropicClient()
const modelId = this.options.modelId || "claude-3-5-sonnet-20241022"
// Sanitize messages to remove Cline-specific fields like call_id that are not allowed by Anthropic API
const sanitizedMessages = sanitizeAnthropicMessages(messages, false)
const stream = await client.messages.create({
model: modelId,
temperature: 0,
max_tokens: this.options.modelInfo?.maxTokens || 8192,
system: [{ text: systemPrompt, type: "text" }],
messages: sanitizedMessages,
messages,
stream: true,
})
+1 -7
View File
@@ -11,12 +11,6 @@ interface CerebrasHandlerOptions extends CommonApiHandlerOptions {
apiModelId?: string
}
// Conservative max_tokens for Cerebras to avoid premature rate limiting.
// Cerebras rate limiter estimates token consumption using max_completion_tokens upfront,
// so requesting the model maximum (e.g., 64K) reserves that quota even if actual usage is low.
// 16K is sufficient for most agentic tool use while preserving rate limit headroom.
const CEREBRAS_DEFAULT_MAX_TOKENS = 16_384
export class CerebrasHandler implements ApiHandler {
private options: CerebrasHandlerOptions
private client: Cerebras | undefined
@@ -117,7 +111,7 @@ export class CerebrasHandler implements ApiHandler {
messages: cerebrasMessages,
temperature: 0,
stream: true,
max_tokens: CEREBRAS_DEFAULT_MAX_TOKENS,
max_tokens: this.getModel().info.maxTokens,
})
// Handle streaming response
+1 -3
View File
@@ -7,7 +7,6 @@ import {
import { ChatMessage, OrchestrationClient, OrchestrationModuleConfig } from "@sap-ai-sdk/orchestration"
import { ModelInfo, SapAiCoreModelId, sapAiCoreDefaultModelId, sapAiCoreModels } from "@shared/api"
import axios from "axios"
import JSON5 from "json5"
import OpenAI from "openai"
import { ClineStorageMessage } from "@/shared/messages/content"
import { getAxiosSettings } from "@/shared/net"
@@ -893,8 +892,7 @@ export class SapAiCoreHandler implements ApiHandler {
try {
// Parse the incoming JSON data from the stream
// Using JSON5 to handle relaxed JSON syntax (e.g., single quotes)
const data = JSON5.parse(jsonData)
const data = JSON.parse(jsonData)
// Handle metadata (token usage)
if (data.metadata?.usage) {
+1 -5
View File
@@ -48,11 +48,7 @@ export function convertAnthropicContentToGemini(content: string | ClineStorageMe
},
}
case "thinking":
return {
text: block.thinking,
thought: true,
thoughtSignature: block.signature || GEMINI_DUMMY_THOUGHT_SIGNATURE,
}
return { text: block.thinking, thought: true, thoughtSignature: block.signature }
default:
return undefined
}
+4 -20
View File
@@ -176,18 +176,11 @@ export function convertToOpenAiMessages(
// Process tool use messages
const tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => {
const toolDetails = toolMessage.reasoning_details
if (toolDetails) {
if (toolDetails?.length) {
if (Array.isArray(toolDetails)) {
// For Gemini: reasoning details must be linkable back to the tool call.
// Sometimes OpenRouter/Gemini returns entries without `id`; those poison the next request.
// Keep only entries with an id matching the tool call id.
// See: https://github.com/cline/cline/issues/8214
const validDetails = toolDetails.filter((detail: any) => detail?.id === toolMessage.id)
if (validDetails.length > 0) reasoningDetails.push(...validDetails)
reasoningDetails.push(...toolDetails)
} else {
// Single reasoning detail - only include if it has matching id
const detail = toolDetails as any
if (detail?.id === toolMessage.id) reasoningDetails.push(toolDetails)
reasoningDetails.push(toolDetails)
}
}
@@ -207,17 +200,13 @@ export function convertToOpenAiMessages(
const hasMeaningfulContent = content !== undefined && content.trim() !== ""
const finalContent = hasMeaningfulContent ? content : hasToolCalls ? null : undefined
const consolidatedReasoningDetails =
reasoningDetails.length > 0 ? consolidateReasoningDetails(reasoningDetails as any) : []
openAiMessages.push({
role: "assistant",
content: finalContent,
// Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty
tool_calls: tool_calls?.length > 0 ? tool_calls : undefined,
// Only include reasoning_details when non-empty; sending [] can trigger provider validation issues.
// @ts-ignore-next-line
reasoning_details: consolidatedReasoningDetails.length > 0 ? consolidatedReasoningDetails : undefined,
reasoning_details: reasoningDetails.length > 0 ? consolidateReasoningDetails(reasoningDetails) : undefined,
})
}
}
@@ -256,11 +245,6 @@ function consolidateReasoningDetails(reasoningDetails: ReasoningDetail[]): Reaso
const groupedByIndex = new Map<number, ReasoningDetail[]>()
for (const detail of reasoningDetails) {
// Drop corrupted encrypted reasoning blocks that would otherwise trigger:
// "Invalid input: expected string, received undefined" for reasoning_details.*.data
// See: https://github.com/cline/cline/issues/8214
if (detail.type === "reasoning.encrypted" && !detail.data) continue
const index = detail.index ?? 0
if (!groupedByIndex.has(index)) {
groupedByIndex.set(index, [])
@@ -36,46 +36,6 @@ export async function createOpenRouterStream(
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
}
// Gemini models require thought signatures for tool calls. When switching providers mid-conversation,
// historical tool calls may not include Gemini/OpenRouter reasoning details, which can poison the next request.
// Bandaid: for Gemini only, drop tool_calls that lack reasoning_details and their paired tool messages.
if (model.id.includes("gemini")) {
const droppedToolCallIds = new Set<string>()
const sanitized: OpenAI.Chat.ChatCompletionMessageParam[] = []
for (const msg of openAiMessages) {
if (msg.role === "assistant") {
const anyMsg = msg as any
const toolCalls = anyMsg.tool_calls
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
const reasoningDetails = anyMsg.reasoning_details
const hasReasoningDetails = Array.isArray(reasoningDetails) && reasoningDetails.length > 0
if (!hasReasoningDetails) {
for (const tc of toolCalls) {
if (tc?.id) droppedToolCallIds.add(tc.id)
}
// Keep any textual content, but drop the tool_calls themselves.
if (anyMsg.content) {
sanitized.push({ role: "assistant", content: anyMsg.content } as any)
}
continue
}
}
}
if (msg.role === "tool") {
const anyMsg = msg as any
if (anyMsg.tool_call_id && droppedToolCallIds.has(anyMsg.tool_call_id)) {
continue
}
}
sanitized.push(msg)
}
openAiMessages = sanitized
}
// prompt caching: https://openrouter.ai/docs/prompt-caching
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
// handles direct model.id match logic
@@ -105,9 +65,6 @@ export async function createOpenRouterStream(
case "anthropic/claude-3-haiku:beta":
case "anthropic/claude-3-opus":
case "anthropic/claude-3-opus:beta":
case "minimax/minimax-m2":
case "minimax/minimax-m2.1":
case "minimax/minimax-m2.1-lightning":
openAiMessages[0] = {
role: "system",
content: [
@@ -31,9 +31,8 @@ export async function createVercelAIGatewayStream(
}
const isAnthropicModel = model.id.startsWith("anthropic/")
const isMinimaxModel = model.id.startsWith("minimax/")
if (isAnthropicModel || isMinimaxModel) {
if (isAnthropicModel) {
openAiMessages[0] = {
role: "system",
content: systemPrompt,
-15
View File
@@ -1,15 +0,0 @@
import type { EmptyRequest } from "@shared/proto/cline/common"
import { Empty } from "@shared/proto/cline/common"
import { AuthService } from "@/services/auth/AuthService"
import type { Controller } from "../index"
/**
* Handles triggering restoring the auth data
* @param controller The controller instance
* @param _request The empty request object
* @returns Empty response
*/
export async function fetchAuth(_: Controller, _request: EmptyRequest): Promise<Empty> {
await AuthService.getInstance().restoreRefreshTokenAndRetrieveAuthInfo()
return Empty.create({})
}
@@ -1,10 +1,10 @@
import fs from "node:fs/promises"
import path from "node:path"
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import { ANTHROPIC_MAX_THINKING_BUDGET, ModelInfo } from "@shared/api"
import { ModelInfo } from "@shared/api"
import { fileExistsAtPath } from "@utils/fs"
import { parsePrice } from "@utils/model-utils"
import axios from "axios"
import fs from "fs/promises"
import path from "path"
import { getAxiosSettings } from "@/shared/net"
import { basetenModels } from "../../../shared/api"
import { Controller } from ".."
@@ -22,7 +22,22 @@ export async function refreshBasetenModels(controller: Controller): Promise<Reco
const models: Record<string, Partial<ModelInfo> & { supportedFeatures?: string[] }> = {}
try {
if (basetenApiKey) {
if (!basetenApiKey) {
// Don't throw an error, just use static models, althought this might be slightly out of date
for (const [modelId, modelInfo] of Object.entries(basetenModels)) {
models[modelId] = {
maxTokens: modelInfo.maxTokens,
contextWindow: modelInfo.contextWindow,
supportsImages: modelInfo.supportsImages,
supportsPromptCache: modelInfo.supportsPromptCache,
inputPrice: modelInfo.inputPrice,
outputPrice: modelInfo.outputPrice,
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
description: (modelInfo as any).description || `${modelId} model`,
}
}
} else {
// Ensure the API key is properly formatted
const cleanApiKey = basetenApiKey.trim()
if (!cleanApiKey) {
@@ -39,9 +54,9 @@ export async function refreshBasetenModels(controller: Controller): Promise<Reco
...getAxiosSettings(),
})
const rawModels = response?.data?.data
if (response.data?.data) {
const rawModels = response.data.data
if (rawModels && Array.isArray(rawModels)) {
for (const rawModel of rawModels) {
// Filter out non-chat models and validate model capabilities
if (!isValidChatModel(rawModel)) {
@@ -50,9 +65,6 @@ export async function refreshBasetenModels(controller: Controller): Promise<Reco
// Check if we have static pricing information for this model
const staticModelInfo = basetenModels[rawModel.id as keyof typeof basetenModels]
const supportThinking = rawModel?.supported_features?.some(
(p: string) => p === "reasoning_effort" || p === "reasoning",
)
const modelInfo: Partial<ModelInfo> & { supportedFeatures?: string[] } = {
maxTokens: rawModel.max_completion_tokens || staticModelInfo?.maxTokens,
@@ -65,23 +77,15 @@ export async function refreshBasetenModels(controller: Controller): Promise<Reco
cacheReadsPrice: staticModelInfo?.cacheReadsPrice || 0,
description: generateModelDescription(rawModel, staticModelInfo),
supportedFeatures: rawModel.supported_features || [],
supportsReasoning: supportThinking || false,
// If thinking is supported, set maxBudget with a default value as a placeholder
// to ensure it has a valid thinkingConfig that lets the application know thinking is supported.
thinkingConfig: supportThinking ? { maxBudget: ANTHROPIC_MAX_THINKING_BUDGET } : undefined,
}
models[rawModel.id] = modelInfo
}
} else {
console.error("Invalid response from Baseten API")
}
// Cache the fetched models to disk
await fs.writeFile(basetenModelsFilePath, JSON.stringify(models))
}
// If no API key is set or models is empty, throw an error to trigger fallback
if (Object.keys(models).length === 0) {
throw new Error("No Baseten API key set or no models fetched")
}
} catch (error) {
console.error("Error fetching Baseten models:", error)
@@ -125,8 +129,6 @@ export async function refreshBasetenModels(controller: Controller): Promise<Reco
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
description: (modelInfo as any).description || `${modelId} model`,
supportsReasoning: modelInfo.supportsReasoning || false,
thinkingConfig: modelInfo.supportsReasoning ? { maxBudget: ANTHROPIC_MAX_THINKING_BUDGET } : undefined,
}
}
}
@@ -147,8 +149,6 @@ export async function refreshBasetenModels(controller: Controller): Promise<Reco
cacheReadsPrice: model.cacheReadsPrice ?? 0,
description: model.description ?? "",
tiers: model.tiers,
supportsReasoning: model.supportsReasoning || false,
thinkingConfig: model.supportsReasoning ? { maxBudget: ANTHROPIC_MAX_THINKING_BUDGET } : undefined,
}
}
@@ -1,5 +1,4 @@
import { buildApiHandler } from "@core/api"
import { isBinaryFile } from "isbinaryfile"
import { HostProvider } from "@/hosts/host-provider"
import { formatContentBlockToMarkdown } from "@/integrations/misc/export-markdown"
import { ApiConfiguration } from "@/shared/api"
@@ -444,29 +443,9 @@ const BINARY_EXTENSIONS = new Set([
])
/**
* Check if a file is binary based on its extension or content.
* @param filePath - Absolute path to the file to check
* @returns Promise<boolean> - true if the file is binary, false if text or if detection fails
* Check if a file is binary based on its extension
*/
export async function detectBinaryFile(filePath: string): Promise<boolean> {
const lastDotIndex = filePath.lastIndexOf(".")
const lastSlashIndex = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"))
const ext = lastDotIndex > lastSlashIndex ? filePath.substring(lastDotIndex).toLowerCase() : ""
const isDotfile = lastDotIndex !== -1 && lastDotIndex === lastSlashIndex + 1
// Legacy/fast method: Check known binary extensions
if (ext && BINARY_EXTENSIONS.has(ext)) {
return true
}
// Use actual binary check for dotfiles or files without extensions. Returns true if file is binary.
if (!ext || isDotfile) {
try {
const result = await isBinaryFile(filePath)
return result
} catch {
return false
}
}
return false
export function isBinaryFile(filePath: string): boolean {
const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase()
return BINARY_EXTENSIONS.has(ext)
}
@@ -2,7 +2,7 @@ You are Cline, a highly skilled software engineer with extensive knowledge in ma
TOOL USE
You have access to a set of tools that are executed upon the user's approval. You will receive the results of all tool uses in the user's response.
You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
====
@@ -68,8 +68,7 @@ When user is providing you with feedback on how you could improve, you can let t
RULES
- The current working directory is `/test/project` - this is the directory where all the tools will be executed from. Then if you want to test your work, you might use browser_action to launch the site, wait for the user's response confirming the site was launched along with a screenshot, then perhaps e.g., click a button to test functionality if needed, wait for the user's response confirming the button was clicked along with a screenshot of the new state, before finally closing the browser.
- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.
- The current working directory is `/test/project` - this is the directory where all the tools will be executed from.
====
@@ -2,7 +2,7 @@ You are Cline, a highly skilled software engineer with extensive knowledge in ma
TOOL USE
You have access to a set of tools that are executed upon the user's approval. You will receive the results of all tool uses in the user's response.
You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
====
@@ -67,7 +67,6 @@ When user is providing you with feedback on how you could improve, you can let t
RULES
- The current working directory is `/test/project` - this is the directory where all the tools will be executed from.
- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.
====
@@ -2,7 +2,7 @@ You are Cline, a highly skilled software engineer with extensive knowledge in ma
TOOL USE
You have access to a set of tools that are executed upon the user's approval. You will receive the results of all tool uses in the user's response.
You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
====
@@ -42,8 +42,7 @@ CAPABILITIES
RULES
- The current working directory is `/test/project` - this is the directory where all the tools will be executed from. Then if you want to test your work, you might use browser_action to launch the site, wait for the user's response confirming the site was launched along with a screenshot, then perhaps e.g., click a button to test functionality if needed, wait for the user's response confirming the button was clicked along with a screenshot of the new state, before finally closing the browser.
- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.
- The current working directory is `/test/project` - this is the directory where all the tools will be executed from.
====
@@ -2,7 +2,7 @@ You are Cline, a highly skilled software engineer with extensive knowledge in ma
TOOL USE
You have access to a set of tools that are executed upon the user's approval. You will receive the results of all tool uses in the user's response.
You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
====
@@ -68,7 +68,7 @@ When user is providing you with feedback on how you could improve, you can let t
RULES
- The current working directory is `/test/project` - this is the directory where all the tools will be executed from. Then if you want to test your work, you might use browser_action to launch the site, wait for the user's response confirming the site was launched along with a screenshot, then perhaps e.g., click a button to test functionality if needed, wait for the user's response confirming the button was clicked along with a screenshot of the new state, before finally closing the browser.
- The current working directory is `/test/project` - this is the directory where all the tools will be executed from.
====
@@ -608,6 +608,7 @@ RULES
- When using the replace_in_file tool, if you use multiple SEARCH/REPLACE blocks, list them in the order they appear in the file. For example if you need to make changes to both line 10 and line 50, first include the SEARCH/REPLACE block for line 10, followed by the SEARCH/REPLACE block for line 50.
- When using the replace_in_file tool, Do NOT add extra characters to the markers (e.g., ------- SEARCH> is INVALID). Do NOT forget to use the closing +++++++ REPLACE marker. Do NOT modify the marker format in any way. Malformed XML will cause complete tool failure and break the entire editing process.
- You may use multiple tools in a single response when the operations are independent (e.g., reading several files, creating independent files). For dependent operations where one result informs the next, use tools sequentially and wait for the user's response. Then if you want to test your work, you might use browser_action to launch the site, wait for the user's response confirming the site was launched along with a screenshot, then perhaps e.g., click a button to test functionality if needed, wait for the user's response confirming the button was clicked along with a screenshot of the new state, before finally closing the browser.
- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.
====
@@ -2,7 +2,7 @@ You are Cline, a software engineering AI. Your mission is to execute precisely w
TOOL USE
You have access to a set of tools that are executed upon the user's approval. You should use a single tool at a time and wait for the result before proceeding. You will receive the results of all tool uses in the user's response.
You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
When using tools, proceed directly with tool calls. Save explanations for the attempt_completion summary. Both attempt_completion and plan_mode_respond display to the user as assistant messages, so include your message content within the tool call itself rather than duplicating it outside.
@@ -240,7 +240,7 @@ OBJECTIVE
You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
2. Work through these goals sequentially, utilizing available tools as necessary. Use a single tool at a time and wait for the result before proceeding. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. If one of the values for a required parameter is missing, ask the user to provide the missing parameters using the ask_followup_question tool (use your tools to gather information when possible to avoid unnecessary questions). Focus on required parameters only - proceed with defaults for optional parameters.
4. Once you've completed the user's task, use the attempt_completion tool to present the result. Provide a CLI command to showcase your work when applicable (e.g., `open index.html` for web development). Before calling attempt_completion, verify with the user that the feature works as expected.
5. For non-actionable tasks, use attempt_completion to provide a clear explanation or direct answer.
@@ -2,7 +2,7 @@ You are Cline, a software engineering AI. Your mission is to execute precisely w
TOOL USE
You have access to a set of tools that are executed upon the user's approval. You should use a single tool at a time and wait for the result before proceeding. You will receive the results of all tool uses in the user's response.
You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
When using tools, proceed directly with tool calls. Save explanations for the attempt_completion summary. Both attempt_completion and plan_mode_respond display to the user as assistant messages, so include your message content within the tool call itself rather than duplicating it outside.
@@ -238,7 +238,7 @@ OBJECTIVE
You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
2. Work through these goals sequentially, utilizing available tools as necessary. Use a single tool at a time and wait for the result before proceeding. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. If one of the values for a required parameter is missing, ask the user to provide the missing parameters using the ask_followup_question tool (use your tools to gather information when possible to avoid unnecessary questions). Focus on required parameters only - proceed with defaults for optional parameters.
4. Once you've completed the user's task, use the attempt_completion tool to present the result. Provide a CLI command to showcase your work when applicable (e.g., `open index.html` for web development). Before calling attempt_completion, verify with the user that the feature works as expected.
5. For non-actionable tasks, use attempt_completion to provide a clear explanation or direct answer.
@@ -2,7 +2,7 @@ You are Cline, a software engineering AI. Your mission is to execute precisely w
TOOL USE
You have access to a set of tools that are executed upon the user's approval. You should use a single tool at a time and wait for the result before proceeding. You will receive the results of all tool uses in the user's response.
You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
When using tools, proceed directly with tool calls. Save explanations for the attempt_completion summary. Both attempt_completion and plan_mode_respond display to the user as assistant messages, so include your message content within the tool call itself rather than duplicating it outside.
@@ -218,7 +218,7 @@ OBJECTIVE
You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
2. Work through these goals sequentially, utilizing available tools as necessary. Use a single tool at a time and wait for the result before proceeding. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. If one of the values for a required parameter is missing, ask the user to provide the missing parameters using the ask_followup_question tool (use your tools to gather information when possible to avoid unnecessary questions). Focus on required parameters only - proceed with defaults for optional parameters.
4. Once you've completed the user's task, use the attempt_completion tool to present the result. Provide a CLI command to showcase your work when applicable (e.g., `open index.html` for web development). Before calling attempt_completion, verify with the user that the feature works as expected.
5. For non-actionable tasks, use attempt_completion to provide a clear explanation or direct answer.
@@ -2,7 +2,7 @@ You are Cline, a software engineering AI. Your mission is to execute precisely w
TOOL USE
You have access to a set of tools that are executed upon the user's approval. You should use a single tool at a time and wait for the result before proceeding. You will receive the results of all tool uses in the user's response.
You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
When using tools, proceed directly with tool calls. Save explanations for the attempt_completion summary. Both attempt_completion and plan_mode_respond display to the user as assistant messages, so include your message content within the tool call itself rather than duplicating it outside.
@@ -240,7 +240,7 @@ OBJECTIVE
You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
2. Work through these goals sequentially, utilizing available tools as necessary. Use a single tool at a time and wait for the result before proceeding. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. If one of the values for a required parameter is missing, ask the user to provide the missing parameters using the ask_followup_question tool (use your tools to gather information when possible to avoid unnecessary questions). Focus on required parameters only - proceed with defaults for optional parameters.
4. Once you've completed the user's task, use the attempt_completion tool to present the result. Provide a CLI command to showcase your work when applicable (e.g., `open index.html` for web development). Before calling attempt_completion, verify with the user that the feature works as expected.
5. For non-actionable tasks, use attempt_completion to provide a clear explanation or direct answer.
@@ -3,23 +3,6 @@ import { SystemPromptSection } from "../templates/placeholders"
import { TemplateEngine } from "../templates/TemplateEngine"
import type { PromptVariant, SystemPromptContext } from "../types"
/**
* Checks if there are any enabled MCP servers in the context.
* This is a utility function to standardize MCP server detection across all prompt variants.
*
* @param context - The system prompt context
* @returns true if there are enabled MCP servers, false otherwise
*
* @example
* const hasMcp = hasEnabledMcpServers(context)
* if (hasMcp) {
* // Include MCP-specific instructions
* }
*/
export function hasEnabledMcpServers(context: SystemPromptContext): boolean {
return (context.mcpHub?.getServers() || []).length > 0
}
const MCP_TEMPLATE_TEXT = `MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
@@ -14,24 +14,6 @@ Default Shell: {{shell}}
Home Directory: {{homeDir}}
{{WORKSPACE_TITLE}}: {{workingDir}}`
/**
* Get the shell that will actually be used for command execution.
* When using background exec mode, commands run in the system default shell
* (cmd.exe on Windows, /bin/bash on Unix), not the VS Code configured shell.
*/
function getEffectiveShell(context: SystemPromptContext): string {
if (context.terminalExecutionMode === "backgroundExec") {
// Background exec uses the system default shell, not VS Code config
if (process.platform === "win32") {
return process.env.COMSPEC || "cmd.exe"
} else {
return process.env.SHELL || "/bin/bash"
}
}
// VS Code terminal mode (or undefined) uses the VS Code configured shell
return getShell()
}
export async function getSystemEnv(context: SystemPromptContext, isTesting = false) {
const currentWorkDir = context.cwd || process.cwd()
const workspaces = (await getWorkspacePaths({}))?.paths || [currentWorkDir]
@@ -48,7 +30,7 @@ export async function getSystemEnv(context: SystemPromptContext, isTesting = fal
: {
os: osName(),
ide: context.ide,
shell: getEffectiveShell(context),
shell: getShell(),
homeDir: osModule.homedir(),
workingDir: currentWorkDir,
workspaces: workspaces,
-2
View File
@@ -115,8 +115,6 @@ export interface SystemPromptContext {
readonly isSubagentsEnabledAndCliInstalled?: boolean
readonly isCliSubagent?: boolean
readonly enableNativeToolCalls?: boolean
readonly enableParallelToolCalling?: boolean
readonly terminalExecutionMode?: "vscodeTerminal" | "backgroundExec"
}
/**
@@ -4,9 +4,9 @@ import type { PromptVariant, SystemPromptContext } from "../../types"
const GEMINI_3_AGENT_ROLE_TEMPLATE = (_context: SystemPromptContext) =>
`You are Cline, a software engineering AI. Your mission is to execute precisely what is requested - implement exactly what was asked for, with the simplest solution that fulfills all requirements. Ask clarifying questions to ensure you understand the user's requirements and that they understand your approach before proceeding.`
const GEMINI_3_TOOL_USE_TEMPLATE = (context: SystemPromptContext) => `TOOL USE
const GEMINI_3_TOOL_USE_TEMPLATE = (_context: SystemPromptContext) => `TOOL USE
You have access to a set of tools that are executed upon the user's approval.${context.enableParallelToolCalling ? " You may use multiple tools in a single response when the operations are independent (e.g., reading several files, searching in parallel). For dependent operations where one result informs the next, use tools sequentially." : " You should use a single tool at a time and wait for the result before proceeding."} You will receive the results of all tool uses in the user's response.
You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
When using tools, proceed directly with tool calls. Save explanations for the attempt_completion summary. Both attempt_completion and plan_mode_respond display to the user as assistant messages, so include your message content within the tool call itself rather than duplicating it outside.`
@@ -15,7 +15,7 @@ const GEMINI_3_OBJECTIVE_TEMPLATE = (context: SystemPromptContext) => `OBJECTIVE
You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
2. Work through these goals sequentially, utilizing available tools as necessary. ${context.enableParallelToolCalling ? "You may call multiple independent tools in a single response to work efficiently." : "Use a single tool at a time and wait for the result before proceeding."} Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use.${context.yoloModeToggled !== true ? " If one of the values for a required parameter is missing, ask the user to provide the missing parameters using the ask_followup_question tool (use your tools to gather information when possible to avoid unnecessary questions)." : ""} Focus on required parameters only - proceed with defaults for optional parameters.
4. Once you've completed the user's task, use the attempt_completion tool to present the result. Provide a CLI command to showcase your work when applicable (e.g., \`open index.html\` for web development).${context.yoloModeToggled !== true ? " Before calling attempt_completion, verify with the user that the feature works as expected." : ""}
5. For non-actionable tasks, use attempt_completion to provide a clear explanation or direct answer.
@@ -1,9 +1,8 @@
import { hasEnabledMcpServers } from "../../components/mcp"
import { SystemPromptSection } from "../../templates/placeholders"
import type { SystemPromptContext } from "../../types"
const GLM_TOOL_USE_TEMPLATE = (context: SystemPromptContext) => {
const hasMcpServers = hasEnabledMcpServers(context)
const hasMcpServers = (context.mcpHub?.getServers() || []).length > 0
return `Begin every task by exploring the codebase (e.g., list_files, search_files, read_file) and outlining the required changes. Do not implement until exploration yields enough context to state objectives, approach, affected files, and risks. Briefly summarize the plan, then proceed with implementation.
@@ -1,4 +1,3 @@
import { hasEnabledMcpServers } from "../../components/mcp"
import { SystemPromptSection } from "../../templates/placeholders"
import type { SystemPromptContext } from "../../types"
@@ -78,7 +77,8 @@ const RULES = (context: SystemPromptContext) => `RULES
- When using the replace_in_file tool, you must include complete lines in your SEARCH blocks, not partial lines. The system requires exact line matches and cannot match partial lines. For example, if you want to match a line containing "const x = 5;", your SEARCH block must include the entire line, not just "x = 5" or other fragments.
- When using the replace_in_file tool, if you use multiple SEARCH/REPLACE blocks, list them in the order they appear in the file. For example if you need to make changes to both line 10 and line 50, first include the SEARCH/REPLACE block for line 10, followed by the SEARCH/REPLACE block for line 50.
- When using the replace_in_file tool, Do NOT add extra characters to the markers (e.g., ------- SEARCH> is INVALID). Do NOT forget to use the closing +++++++ REPLACE marker. Do NOT modify the marker format in any way. Malformed XML will cause complete tool failure and break the entire editing process.
- You may use multiple tools in a single response when the operations are independent (e.g., reading several files, creating independent files). For dependent operations where one result informs the next, use tools sequentially and wait for the user's response.{{BROWSER_WAIT_RULES}}${hasEnabledMcpServers(context) ? "\n- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations." : ""}`
- You may use multiple tools in a single response when the operations are independent (e.g., reading several files, creating independent files). For dependent operations where one result informs the next, use tools sequentially and wait for the user's response.{{BROWSER_WAIT_RULES}}
- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.`
export const GPT_5_TEMPLATE_OVERRIDES = {
BASE,
@@ -1,4 +1,3 @@
import { hasEnabledMcpServers } from "../../components/mcp"
import { SystemPromptSection } from "../../templates/placeholders"
import type { SystemPromptContext } from "../../types"
@@ -49,22 +48,13 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}}
{{${SystemPromptSection.USER_INSTRUCTIONS}}}`
const RULES = (context: SystemPromptContext) => {
const hasMcpServers = hasEnabledMcpServers(context)
const RULES = (_context: SystemPromptContext) => `RULES
return `RULES
- The current working directory is \`{{CWD}}\` - this is the directory where all the tools will be executed from.`
- The current working directory is \`{{CWD}}\` - this is the directory where all the tools will be executed from.${
context.enableParallelToolCalling
? `
- You may use multiple tools in a single response when the operations are independent (e.g., reading several files, creating independent files). For dependent operations where one result informs the next, use tools sequentially and wait for the user's response.`
: ""
}{{BROWSER_WAIT_RULES}}${hasMcpServers ? "\n- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations." : ""}`
}
const TOOL_USE = (_context: SystemPromptContext) => `TOOL USE
const TOOL_USE = (context: SystemPromptContext) => `TOOL USE
You have access to a set of tools that are executed upon the user's approval.${context.enableParallelToolCalling ? " You may use multiple tools in a single response when the operations are independent (e.g., reading several files, searching in parallel). For dependent operations where one result informs the next, use tools sequentially." : ""} You will receive the results of all tool uses in the user's response.`
You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.`
const ACT_VS_PLAN = (context: SystemPromptContext) => `ACT MODE V.S. PLAN MODE
@@ -89,7 +79,7 @@ const OBJECTIVE = (context: SystemPromptContext) => `OBJECTIVE
You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
2. Work through these goals sequentially, utilizing available tools ${context.enableParallelToolCalling ? "as necessary. You may call multiple independent tools in a single response to work efficiently." : "one at a time as necessary."} Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params)${context.yoloModeToggled !== true ? " and instead, ask the user to provide the missing parameters using the ask_followup_question tool" : ""}. DO NOT ask for more information on optional parameters if it is not provided.
4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built.
5. If the task is not actionable, you may use the attempt_completion tool to explain to the user why the task cannot be completed, or provide a simple answer if that is what the user is looking for.`
+4 -9
View File
@@ -73,12 +73,8 @@ import type { SystemPromptContext } from "@/core/prompts/system-prompt"
import { getSystemPrompt } from "@/core/prompts/system-prompt"
import { HostProvider } from "@/hosts/host-provider"
import { FileEditProvider } from "@/integrations/editor/FileEditProvider"
import {
CommandExecutor,
CommandExecutorCallbacks,
FullCommandExecutorConfig,
StandaloneTerminalManager,
} from "@/integrations/terminal"
import { CommandExecutorCallbacks, StandaloneTerminalManager } from "@/integrations/terminal"
import { CommandExecutor, FullCommandExecutorConfig } from "@/integrations/terminal/CommandExecutor"
import { ClineError, ClineErrorType, ErrorService } from "@/services/error"
import { telemetryService } from "@/services/telemetry"
import {
@@ -276,6 +272,7 @@ export class Task {
this.cancelTask = cancelTask
this.clineIgnoreController = new ClineIgnoreController(cwd)
this.taskLockAcquired = taskLockAcquired
// Determine terminal execution mode and create appropriate terminal manager
this.terminalExecutionMode = vscodeTerminalExecutionMode || "vscodeTerminal"
@@ -513,7 +510,7 @@ export class Task {
},
updateBackgroundCommandState: (isRunning: boolean) =>
this.controller.updateBackgroundCommandState(isRunning, this.taskId),
updateClineMessage: async (index: number, updates: { commandCompleted?: boolean; text?: string }) => {
updateClineMessage: async (index: number, updates: { commandCompleted?: boolean }) => {
await this.messageStateHandler.updateClineMessage(index, updates)
},
getClineMessages: () => this.messageStateHandler.getClineMessages() as Array<{ ask?: string; say?: string }>,
@@ -1783,8 +1780,6 @@ export class Task {
isSubagentsEnabledAndCliInstalled,
isCliSubagent,
enableNativeToolCalls: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
enableParallelToolCalling: this.stateManager.getGlobalSettingsKey("enableParallelToolCalling"),
terminalExecutionMode: this.terminalExecutionMode,
}
const { systemPrompt, tools } = await getSystemPrompt(promptContext)
@@ -468,7 +468,7 @@ export class ApplyPatchHandler implements IFullyManagedTool {
changes[path] = {
type: PatchActionType.UPDATE,
oldContent: originalFiles[path],
newContent: this.applyChunks(originalFiles[path]!, action.chunks, path),
newContent: this.applyChunks(originalFiles[path]!, action.chunks, path).trimEnd(),
movePath: action.movePath,
}
break
@@ -2,7 +2,7 @@ import type { ToolUse } from "@core/assistant-message"
import {
buildDiffContent,
type ChangedFile,
detectBinaryFile,
isBinaryFile,
openDiffView,
setupCommentController,
streamAIExplanationComments,
@@ -160,7 +160,7 @@ export class GenerateExplanationToolHandler implements IToolHandler, IPartialBlo
const absolutePath = path.join(cwd, filePath)
// Skip binary files - they can't be displayed properly in diff view
if (await detectBinaryFile(absolutePath)) {
if (isBinaryFile(filePath)) {
continue
}
@@ -480,6 +480,8 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
return
}
newContent = newContent.trimEnd() // remove any trailing newlines, since it's automatically inserted by the editor
return { relPath, absolutePath, fileExists, diff, content, newContent, workspaceContext }
}
+2 -2
View File
@@ -228,7 +228,7 @@ function createAuthSucceededHtml(redirectUri?: string): string {
<title>Cline - Authentication Success</title>
${redirect}
<style>
@import url('https://fonts.googleapis.com/css2?family=Azeret:wght@300;400;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Azeret+Mono:wght@300;400;700&display=swap');
* {
margin: 0;
@@ -237,7 +237,7 @@ function createAuthSucceededHtml(redirectUri?: string): string {
}
body {
font-family: 'Azeret', sans-serif;
font-family: 'Azeret Mono', monospace;
background-color: #ffffff;
color: #333333;
height: 100vh;
+3 -20
View File
@@ -96,34 +96,17 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
if (!this.activeDiffEditor || !this.activeDiffEditor.document) {
throw new Error("User closed text editor, unable to edit file...")
}
// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
const beginningOfDocument = new vscode.Position(0, 0)
this.activeDiffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
// Replace the text in the diff editor document.
const document = this.activeDiffEditor.document
const document = this.activeDiffEditor?.document
const edit = new vscode.WorkspaceEdit()
// IMPORTANT: VS Code may treat an out-of-bounds end position as an insertion instead of a
// replacement. Always validate the range against the current document to keep edits
// strictly within the real end-of-file.
const startLine = Math.max(0, Math.min(rangeToReplace.startLine, document.lineCount - 1))
const desiredEndLine = Math.max(rangeToReplace.startLine, rangeToReplace.endLine)
const validatedRange = document.validateRange(
new vscode.Range(new vscode.Position(startLine, 0), new vscode.Position(desiredEndLine, 0)),
)
edit.replace(document.uri, validatedRange, content)
const range = new vscode.Range(rangeToReplace.startLine, 0, rangeToReplace.endLine, 0)
edit.replace(document.uri, range, content)
await vscode.workspace.applyEdit(edit)
// Preserve trailing newline: if content ends with newline, ensure document does too
if (content.endsWith("\n") && !document.getText().endsWith("\n")) {
const fixEdit = new vscode.WorkspaceEdit()
fixEdit.insert(document.uri, document.lineAt(Math.max(0, document.lineCount - 1)).range.end, "\n")
await vscode.workspace.applyEdit(fixEdit)
}
if (currentLine !== undefined) {
// Update decorations for the entire changed section
this.activeLineController?.setActiveLine(currentLine)
@@ -3,16 +3,12 @@ import { EventEmitter } from "events"
import * as vscode from "vscode"
import { stripAnsi } from "@/hosts/vscode/terminal/ansiUtils"
import { getLatestTerminalOutput } from "@/hosts/vscode/terminal/get-latest-output"
import {
isCompilingOutput,
MAX_FULL_OUTPUT_SIZE,
MAX_UNRETRIEVED_LINES,
PROCESS_HOT_TIMEOUT_COMPILING,
PROCESS_HOT_TIMEOUT_NORMAL,
TRUNCATE_KEEP_LINES,
} from "@/integrations/terminal/constants"
import type { ITerminalProcess, TerminalProcessEvents } from "@/integrations/terminal/types"
// how long to wait after a process outputs anything before we consider it "cool" again
const PROCESS_HOT_TIMEOUT_NORMAL = 2_000
const PROCESS_HOT_TIMEOUT_COMPILING = 15_000
/**
* VscodeTerminalProcess - Manages command execution in VSCode's integrated terminal.
*
@@ -160,7 +156,24 @@ export class VscodeTerminalProcess extends EventEmitter<TerminalProcessEvents> i
clearTimeout(this.hotTimer)
}
// these markers indicate the command is some kind of local dev server recompiling the app, which we want to wait for output of before sending request to cline
const isCompiling = isCompilingOutput(data)
const compilingMarkers = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
const markerNullifiers = [
"compiled",
"success",
"finish",
"complete",
"succeed",
"done",
"end",
"stop",
"exit",
"terminate",
"error",
"fail",
]
const isCompiling =
compilingMarkers.some((marker) => data.toLowerCase().includes(marker.toLowerCase())) &&
!markerNullifiers.some((nullifier) => data.toLowerCase().includes(nullifier.toLowerCase()))
this.hotTimer = setTimeout(
() => {
this.isHot = false
@@ -176,15 +189,6 @@ export class VscodeTerminalProcess extends EventEmitter<TerminalProcessEvents> i
}
this.fullOutput += data
// Cap fullOutput at MAX_FULL_OUTPUT_SIZE to prevent memory exhaustion
if (this.fullOutput.length > MAX_FULL_OUTPUT_SIZE) {
// Keep last half of max size
this.fullOutput = this.fullOutput.slice(-MAX_FULL_OUTPUT_SIZE / 2)
// Reset lastRetrievedIndex since we truncated the beginning
this.lastRetrievedIndex = 0
}
if (this.isListening) {
this.emitIfEol(data)
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
@@ -196,18 +200,18 @@ export class VscodeTerminalProcess extends EventEmitter<TerminalProcessEvents> i
// the command process is finished, let's check the output to see if we need to use the terminal capture fallback
if (!this.fullOutput.trim()) {
// No output captured via shell integration, trying fallback
telemetryService.captureTerminalOutputFailure(TerminalOutputFailureReason.TIMEOUT, "vscode")
telemetryService.captureTerminalOutputFailure(TerminalOutputFailureReason.TIMEOUT)
await returnCurrentTerminalContents()
// Check if fallback worked
const terminalSnapshot = await getLatestTerminalOutput()
if (terminalSnapshot && terminalSnapshot.trim()) {
telemetryService.captureTerminalExecution(true, "vscode", "clipboard")
telemetryService.captureTerminalExecution(true, "clipboard")
} else {
telemetryService.captureTerminalExecution(false, "vscode", "none")
telemetryService.captureTerminalExecution(false, "none")
}
} else {
// Shell integration worked
telemetryService.captureTerminalExecution(true, "vscode", "shell_integration")
telemetryService.captureTerminalExecution(true, "shell_integration")
}
// for now we don't want this delaying requests since we don't send diagnostics automatically anymore (previous: "even though the command is finished, we still want to consider it 'hot' in case so that api request stalls to let diagnostics catch up")
@@ -221,7 +225,7 @@ export class VscodeTerminalProcess extends EventEmitter<TerminalProcessEvents> i
this.emit("continue")
} else {
// no shell integration detected, we'll fallback to running the command and capturing the terminal's output after some time
telemetryService.captureTerminalOutputFailure(TerminalOutputFailureReason.NO_SHELL_INTEGRATION, "vscode")
telemetryService.captureTerminalOutputFailure(TerminalOutputFailureReason.NO_SHELL_INTEGRATION)
terminal.sendText(command, true)
// wait 3 seconds for the command to run
@@ -232,9 +236,9 @@ export class VscodeTerminalProcess extends EventEmitter<TerminalProcessEvents> i
// Check if clipboard fallback worked
const terminalSnapshot = await getLatestTerminalOutput()
if (terminalSnapshot && terminalSnapshot.trim()) {
telemetryService.captureTerminalExecution(true, "vscode", "clipboard")
telemetryService.captureTerminalExecution(true, "clipboard")
} else {
telemetryService.captureTerminalExecution(false, "vscode", "none")
telemetryService.captureTerminalExecution(false, "none")
}
// For terminals without shell integration, we can't know when the command completes
// So we'll just emit the continue event after a delay
@@ -281,24 +285,9 @@ export class VscodeTerminalProcess extends EventEmitter<TerminalProcessEvents> i
this.emit("continue")
}
/**
* Get output that hasn't been retrieved yet.
* Truncates if output is too large to prevent context window overflow.
* @returns The unretrieved output (truncated if necessary)
*/
getUnretrievedOutput(): string {
const unretrieved = this.fullOutput.slice(this.lastRetrievedIndex)
this.lastRetrievedIndex = this.fullOutput.length
// Truncate if too many lines to prevent context overflow
const lines = unretrieved.split("\n")
if (lines.length > MAX_UNRETRIEVED_LINES) {
const first = lines.slice(0, TRUNCATE_KEEP_LINES)
const last = lines.slice(-TRUNCATE_KEEP_LINES)
const skipped = lines.length - first.length - last.length
return this.removeLastLineArtifacts([...first, `\n... (${skipped} lines truncated) ...\n`, ...last].join("\n"))
}
return this.removeLastLineArtifacts(unretrieved)
}
@@ -1,6 +1,5 @@
import { sendCheckpointEvent } from "@core/controller/checkpoints/subscribeToCheckpoints"
import fs from "fs/promises"
import { isBinaryFile } from "isbinaryfile"
import * as path from "path"
import simpleGit from "simple-git"
import type { FolderLockWithRetryResult } from "@/core/locks/types"
@@ -424,23 +423,6 @@ class CheckpointTracker {
const filePath = file.file
const absolutePath = path.join(this.cwd, filePath)
// For extensionless files or dotfiles: exclude from diff result if binary
const lastDotIndex = filePath.lastIndexOf(".")
const lastSlashIndex = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"))
const ext = lastDotIndex > lastSlashIndex ? filePath.substring(lastDotIndex).toLowerCase() : ""
const isDotfile = lastDotIndex !== -1 && lastDotIndex === lastSlashIndex + 1
if (!ext || isDotfile) {
try {
const isBinary = await isBinaryFile(absolutePath).catch(() => false)
if (isBinary) {
continue
}
} catch {
continue
}
}
let beforeContent = ""
try {
beforeContent = await git.show([`${this.cleanCommitHash(lhsHash)}:${filePath}`])
+13 -9
View File
@@ -182,12 +182,7 @@ export abstract class DiffViewProvider {
// Replace all content up to the current line with accumulated lines
// This is necessary (as compared to inserting one line at a time) to handle cases where html tags
// on previous lines are auto closed for example
let contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n")
if (!isFinal) {
// During streaming, add trailing newline for cursor positioning
contentToReplace += "\n"
}
const contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
const rangeToReplace = { startLine: 0, endLine: currentLine + 1 }
await this.replaceText(contentToReplace, rangeToReplace, currentLine)
@@ -217,6 +212,15 @@ export abstract class DiffViewProvider {
if (isFinal) {
// Handle any remaining lines if the new content is shorter than the original
await this.truncateDocument(this.streamedLines.length)
// Add empty last line if original content had one
const hasEmptyLastLine = this.originalContent?.endsWith("\n")
if (hasEmptyLastLine) {
const accumulatedLines = accumulatedContent.split("\n")
if (accumulatedLines[accumulatedLines.length - 1] !== "") {
accumulatedContent += "\n"
}
}
}
}
@@ -273,10 +277,10 @@ export abstract class DiffViewProvider {
// If the edited content has different EOL characters, we don't want to show a diff with all the EOL differences.
const newContentEOL = this.newContent.includes("\r\n") ? "\r\n" : "\n"
const normalizedPreSaveContent = preSaveContent.replace(/\r\n|\n/g, newContentEOL)
const normalizedPostSaveContent = postSaveContent.replace(/\r\n|\n/g, newContentEOL) // this is the final content we return to the model to use as the new baseline for future edits
const normalizedPreSaveContent = preSaveContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL // trimEnd to fix issue where editor adds in extra new line automatically
const normalizedPostSaveContent = postSaveContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL // this is the final content we return to the model to use as the new baseline for future edits
// just in case the new content has a mix of varying EOL characters
const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL)
const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL
let userEdits: string | undefined
if (normalizedPreSaveContent !== normalizedNewContent) {
+5 -34
View File
@@ -43,48 +43,19 @@ export class FileEditProvider extends DiffViewProvider {
// Split the document into lines
const lines = this.documentContent.split("\n")
const originalEndsWithNewline = this.documentContent.endsWith("\n")
// If original ends with newline, split creates a trailing empty string that isn't a real line.
// Remove it for line-based operations, we'll add it back at the end if needed.
const realLines = originalEndsWithNewline && lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines
// Replace the specified range with the new content
const newContentLines = content.split("\n")
const contentEndsWithNewline = content.endsWith("\n")
// Determine if we're replacing to the end of the document
const replacingToEnd = rangeToReplace.endLine >= realLines.length
// Handle trailing empty string from split:
// - If content ends with \n, split creates an empty string at the end
// - When replacing to end: this empty string becomes the document's trailing newline - keep it
// - When replacing middle: this empty string would create an extra newline - remove it
// (the join operation will naturally add newlines between lines)
// - If content doesn't end with \n but split created empty string, remove it
if (!contentEndsWithNewline && newContentLines[newContentLines.length - 1] === "") {
newContentLines.pop()
} else if (contentEndsWithNewline && !replacingToEnd && newContentLines[newContentLines.length - 1] === "") {
// Content ends with newline but we're replacing middle section - remove trailing empty string
// Remove trailing empty line if present in newContentLines for proper splicing
if (newContentLines[newContentLines.length - 1] === "") {
newContentLines.pop()
}
// Splice the real lines array to replace the range
realLines.splice(rangeToReplace.startLine, rangeToReplace.endLine - rangeToReplace.startLine, ...newContentLines)
// Splice the lines array to replace the range
lines.splice(rangeToReplace.startLine, rangeToReplace.endLine - rangeToReplace.startLine, ...newContentLines)
// Join the lines back together
let result = realLines.join("\n")
// Preserve trailing newline: add it back if original had one OR if we replaced to end with content that ends with newline
const shouldHaveTrailingNewline = originalEndsWithNewline || (replacingToEnd && contentEndsWithNewline)
if (shouldHaveTrailingNewline && !result.endsWith("\n")) {
result += "\n"
} else if (!shouldHaveTrailingNewline && result.endsWith("\n")) {
// Shouldn't have trailing newline but result has one - remove it
result = result.slice(0, -1)
}
this.documentContent = result
this.documentContent = lines.join("\n")
}
protected async scrollEditorToLine(_line: number): Promise<void> {
+141 -106
View File
@@ -18,18 +18,28 @@
import { isSubagentCommand, transformClineCommand } from "@integrations/cli-subagents/subagent_command"
import { Logger } from "@services/logging/Logger"
import { telemetryService } from "@services/telemetry"
import { findLastIndex } from "@shared/array"
import { ClineToolResponseContent } from "@shared/messages"
import { orchestrateCommandExecution } from "./CommandOrchestrator"
import { StandaloneTerminalManager } from "./standalone/StandaloneTerminalManager"
import type {
import {
ActiveBackgroundCommand,
CommandExecutorCallbacks,
CommandExecutorConfig,
ITerminalManager,
ShellIntegrationWarningTracker,
TerminalProcessResultPromise,
} from "./types"
// Re-export types for convenience
export type { CommandExecutorCallbacks, CommandExecutorConfig, FullCommandExecutorConfig } from "./types"
/**
* Tracker for shell integration warnings to determine when to show background terminal suggestion
*/
interface ShellIntegrationWarningTracker {
timestamps: number[]
lastSuggestionShown?: number
}
/**
* CommandExecutor - Unified command executor for all terminal modes.
*
@@ -45,18 +55,19 @@ export class CommandExecutor {
private standaloneManager: StandaloneTerminalManager
private callbacks: CommandExecutorCallbacks
// Track the currently executing foreground process for cancellation
private currentProcess: TerminalProcessResultPromise | null = null
// Flag to track if the current command was cancelled externally
private wasCancelledExternally = false
// Track shell integration warnings to determine when to show background terminal suggestion
private shellIntegrationWarningTracker: ShellIntegrationWarningTracker = {
timestamps: [],
lastSuggestionShown: undefined,
}
// Track active background command for cancellation (standalone mode only)
private activeBackgroundCommand?: {
process: TerminalProcessResultPromise & { terminate?: () => void }
command: string
outputLines: string[]
}
constructor(config: CommandExecutorConfig, callbacks: CommandExecutorCallbacks) {
this.cwd = config.cwd
this.taskId = config.taskId
@@ -65,27 +76,16 @@ export class CommandExecutor {
this.terminalManager = config.terminalManager
this.callbacks = callbacks
// When in backgroundExec mode, the terminalManager is already a StandaloneTerminalManager
// created by Task. We should reuse it so that Task.getEnvironmentDetails() can see
// the terminals and processes we create (for isHot logic, busy terminals, etc.)
if (config.terminalExecutionMode === "backgroundExec" && config.terminalManager instanceof StandaloneTerminalManager) {
// Reuse the same instance that Task is using
this.standaloneManager = config.terminalManager
Logger.info(`[CommandExecutor] Reusing Task's StandaloneTerminalManager for backgroundExec mode`)
} else {
// Create new StandaloneTerminalManager for subagents (even in VSCode mode)
// This ensures subagents run in hidden terminals, not cluttering the user's VSCode terminal
this.standaloneManager = new StandaloneTerminalManager()
Logger.info(`[CommandExecutor] Created new StandaloneTerminalManager for subagents`)
// Always create StandaloneTerminalManager for subagents (even in VSCode mode)
this.standaloneManager = new StandaloneTerminalManager()
// Copy settings from the provided terminalManager to ensure consistency
if ("shellIntegrationTimeout" in config.terminalManager) {
const tm = config.terminalManager as any
this.standaloneManager.setShellIntegrationTimeout(tm.shellIntegrationTimeout || 4000)
this.standaloneManager.setTerminalReuseEnabled(tm.terminalReuseEnabled ?? true)
this.standaloneManager.setTerminalOutputLineLimit(tm.terminalOutputLineLimit || 500)
this.standaloneManager.setSubagentTerminalOutputLineLimit(tm.subagentTerminalOutputLineLimit || 2000)
}
// Copy settings from the provided terminalManager to ensure consistency
if ("shellIntegrationTimeout" in config.terminalManager) {
const tm = config.terminalManager as any
this.standaloneManager.setShellIntegrationTimeout(tm.shellIntegrationTimeout || 4000)
this.standaloneManager.setTerminalReuseEnabled(tm.terminalReuseEnabled ?? true)
this.standaloneManager.setTerminalOutputLineLimit(tm.terminalOutputLineLimit || 500)
this.standaloneManager.setSubagentTerminalOutputLineLimit(tm.subagentTerminalOutputLineLimit || 2000)
}
}
@@ -120,6 +120,7 @@ export class CommandExecutor {
// Subagents always use standalone manager (hidden terminal)
const useStandalone = isSubagent || this.terminalExecutionMode === "backgroundExec"
const manager = useStandalone ? this.standaloneManager : this.terminalManager
Logger.info(`Executing command in ${useStandalone ? "standalone" : "VSCode"} terminal: ${command}`)
// Get terminal and run command
@@ -127,124 +128,146 @@ export class CommandExecutor {
terminalInfo.terminal.show()
const process = manager.runCommand(terminalInfo, command)
// Reset cancellation flag and track the current process
this.wasCancelledExternally = false
this.currentProcess = process
const clearCurrentProcess = () => {
this.currentProcess = null
// Track background command for standalone mode (enables cancellation)
if (useStandalone) {
this.activeBackgroundCommand = {
process: process as any,
command,
outputLines: [],
}
}
process.once("completed", clearCurrentProcess)
process.once("error", clearCurrentProcess)
// Use shared orchestration logic
// The StandaloneTerminalManager handles background command tracking internally
const result = await orchestrateCommandExecution(process, manager, this.callbacks, {
command,
timeoutSeconds,
// When "Proceed While Running" is triggered, track the command in the manager
// Returns the log file path so the orchestrator can send it to the UI
// existingOutput contains all output lines captured so far
onProceedWhileRunning: useStandalone
? (existingOutput: string[]) => {
const backgroundCmd = this.standaloneManager.trackBackgroundCommand(process, command, existingOutput)
return { logFilePath: backgroundCmd.logFilePath }
onOutputLine: useStandalone
? (line) => {
if (this.activeBackgroundCommand) {
this.activeBackgroundCommand.outputLines.push(line)
}
}
: undefined,
showShellIntegrationSuggestion: this.shouldShowBackgroundTerminalSuggestion(),
terminalType: useStandalone ? "standalone" : "vscode",
})
// Clear background command tracking if completed
if (result.completed && useStandalone) {
this.activeBackgroundCommand = undefined
}
// Capture subagent telemetry
if (isSubagent && subAgentStartTime > 0) {
const durationMs = Math.round(performance.now() - subAgentStartTime)
telemetryService.captureSubagentExecution(this.ulid, durationMs, result.outputLines.length, result.completed)
}
// If the command was cancelled externally (via cancel button), return a clear cancellation message
// This ensures the AI agent knows the command was cancelled by the user
if (this.wasCancelledExternally) {
const outputSoFar =
result.outputLines.length > 0
? `\nOutput captured before cancellation:\n${manager.processOutput(result.outputLines)}`
: ""
return [true, `Command was cancelled by the user.${outputSoFar}`]
}
return [result.userRejected, result.result]
}
/**
* Cancel all running commands (both foreground and background).
* Cancel the currently running background command.
* Only works in standalone/backgroundExec mode.
*
* This method cancels:
* 1. All detached background commands (those that were "proceeded while running")
* 2. The current foreground process (if one is actively running)
*
* @returns true if any commands were cancelled, false otherwise
* @returns true if a command was cancelled, false otherwise
*/
async cancelBackgroundCommand(): Promise<boolean> {
let cancelled = false
if (!this.activeBackgroundCommand) {
return false
}
// 1. Cancel all detached background commands
const runningCommands = this.standaloneManager.getRunningBackgroundCommands()
for (const cmd of runningCommands) {
if (this.standaloneManager.cancelBackgroundCommand(cmd.id)) {
cancelled = true
Logger.info(`Cancelled background command: ${cmd.command}`)
const { process, command, outputLines } = this.activeBackgroundCommand
this.activeBackgroundCommand = undefined
this.callbacks.updateBackgroundCommandState(false)
try {
// Try to terminate the process if the method exists
if (typeof process.terminate === "function") {
try {
await process.terminate()
Logger.info(`Terminated background command: ${command}`)
} catch (error) {
Logger.error(`Error terminating background command: ${command}`, error)
}
}
}
// 2. Cancel the current foreground process (if any)
if (this.currentProcess && typeof (this.currentProcess as any).terminate === "function") {
// Set flag so execute() knows the command was cancelled externally
this.wasCancelledExternally = true
;(this.currentProcess as any).terminate()
this.currentProcess = null
cancelled = true
Logger.info("Cancelled foreground command")
}
// Ensure any pending operations complete
if (typeof process.continue === "function") {
try {
process.continue()
} catch (error) {
Logger.error(`Error continuing background command: ${command}`, error)
}
}
// 3. Update UI state and notify user by modifying existing message
// We modify the previous command_output message instead of sending a new say()
// to avoid interfering with any pending ask() dialogs (which would cause
// "Current ask promise was ignored" errors)
if (cancelled) {
this.callbacks.updateBackgroundCommandState(false)
// Wait for terminal buffers to flush before updating the message
// This prevents the cancellation notice from appearing in the middle of output
await new Promise((resolve) => setTimeout(resolve, 300))
// Find the last command_output message and update it
const messages = this.callbacks.getClineMessages()
const lastCommandOutputIndex = findLastIndex(messages, (m) => m.ask === "command_output")
if (lastCommandOutputIndex !== -1) {
const existingText = messages[lastCommandOutputIndex].text || ""
const cancellationNotice = "\n\nCommand(s) cancelled by user."
await this.callbacks.updateClineMessage(lastCommandOutputIndex, {
text: existingText + cancellationNotice,
// Mark the command message as completed in the UI
const clineMessages = this.callbacks.getClineMessages()
const lastCommandIndex = this.findLastIndex(clineMessages, (m) => m.ask === "command" || m.say === "command")
if (lastCommandIndex !== -1) {
await this.callbacks.updateClineMessage(lastCommandIndex, {
commandCompleted: true,
})
}
}
return cancelled
// Process the captured output to include in the cancellation message
const processedOutput = this.standaloneManager.processOutput(outputLines, undefined, false)
// Add cancellation information to the API conversation history
let cancellationMessage = `Command "${command}" was cancelled by the user.`
if (processedOutput.length > 0) {
cancellationMessage += `\n\nOutput captured before cancellation:\n${processedOutput}`
}
this.callbacks.addToUserMessageContent({
type: "text",
text: cancellationMessage,
})
return true
} catch (error) {
Logger.error("Error in cancelBackgroundCommand", error)
return false
} finally {
try {
await this.callbacks.say("command_output", "Command execution has been cancelled.")
} catch (error) {
Logger.error("Failed to send cancellation notification", error)
}
}
}
/**
* Check if there are any active background commands.
* Delegates to StandaloneTerminalManager.
* Check if there's an active background command
*/
hasActiveBackgroundCommand(): boolean {
return this.standaloneManager.hasActiveBackgroundCommands()
return !!this.activeBackgroundCommand
}
/**
* Get a summary of background commands for environment details.
* Delegates to StandaloneTerminalManager which tracks multiple commands.
* Get the active background command info (for external access)
*/
getActiveBackgroundCommand(): ActiveBackgroundCommand | undefined {
return this.activeBackgroundCommand
}
/**
* Get a summary of background commands for environment details
*/
getBackgroundCommandSummary(): string | undefined {
const summary = this.standaloneManager.getBackgroundCommandsSummary()
return summary || undefined
if (!this.activeBackgroundCommand) {
return undefined
}
const { command, outputLines } = this.activeBackgroundCommand
const recentOutput = outputLines.slice(-10).join("\n")
let summary = "# Background Commands\n"
summary += `## Running: \`${command}\`\n`
if (recentOutput) {
summary += `### Recent Output\n${recentOutput}`
}
return summary
}
/**
@@ -281,4 +304,16 @@ export class CommandExecutor {
return false
}
/**
* Helper to find last index matching a predicate
*/
private findLastIndex<T>(array: T[], predicate: (item: T) => boolean): number {
for (let i = array.length - 1; i >= 0; i--) {
if (predicate(array[i])) {
return i
}
}
return -1
}
}
+36 -263
View File
@@ -19,19 +19,6 @@ import { processFilesIntoText } from "@integrations/misc/extract-text"
import { Logger } from "@services/logging/Logger"
import { TerminalHangStage, TerminalUserInterventionAction, telemetryService } from "@services/telemetry"
import { COMMAND_CANCEL_TOKEN } from "@shared/ExtensionMessage"
import * as fs from "fs"
import * as os from "os"
import * as path from "path"
import {
BUFFER_STUCK_TIMEOUT_MS,
CHUNK_BYTE_SIZE,
CHUNK_DEBOUNCE_MS,
CHUNK_LINE_COUNT,
COMPLETION_TIMEOUT_MS,
MAX_BYTES_BEFORE_FILE,
MAX_LINES_BEFORE_FILE,
SUMMARY_LINES_TO_KEEP,
} from "./constants"
import type {
CommandExecutorCallbacks,
ITerminalManager,
@@ -40,6 +27,16 @@ import type {
TerminalProcessResultPromise,
} from "./types"
// Chunked terminal output buffering constants
export const CHUNK_LINE_COUNT = 20
export const CHUNK_BYTE_SIZE = 2048 // 2KB
export const CHUNK_DEBOUNCE_MS = 100
export const BUFFER_STUCK_TIMEOUT_MS = 6000 // 6 seconds
export const COMPLETION_TIMEOUT_MS = 6000 // 6 seconds
// Re-export types for convenience
export type { OrchestrationOptions, OrchestrationResult } from "./types"
/**
* Orchestrate command execution with shared logic for buffering, user interaction, and result formatting.
*
@@ -55,13 +52,7 @@ export async function orchestrateCommandExecution(
callbacks: CommandExecutorCallbacks,
options: OrchestrationOptions,
): Promise<OrchestrationResult> {
const {
timeoutSeconds,
onOutputLine,
showShellIntegrationSuggestion,
onProceedWhileRunning,
terminalType = "vscode",
} = options
const { command, timeoutSeconds, onOutputLine, showShellIntegrationSuggestion } = options
// Track command execution state
callbacks.updateBackgroundCommandState(true)
@@ -88,7 +79,6 @@ export async function orchestrateCommandExecution(
let userFeedback: { text?: string; images?: string[]; files?: string[] } | undefined
let didContinue = false
let didCancelViaUi = false
let backgroundTrackingResult: OrchestrationResult | null = null // Set when background tracking returns early
// Chunked terminal output buffering
let outputBuffer: string[] = []
@@ -114,7 +104,7 @@ export async function orchestrateCommandExecution(
if (!didContinue) {
// Start timer to detect if buffer gets stuck
bufferStuckTimer = setTimeout(() => {
telemetryService.captureTerminalHang(TerminalHangStage.BUFFER_STUCK, terminalType)
telemetryService.captureTerminalHang(TerminalHangStage.BUFFER_STUCK)
bufferStuckTimer = null
}, BUFFER_STUCK_TIMEOUT_MS)
@@ -125,70 +115,22 @@ export async function orchestrateCommandExecution(
if (response === "yesButtonClicked") {
// Track when user clicks "Proceed While Running"
telemetryService.captureTerminalUserIntervention(
TerminalUserInterventionAction.PROCESS_WHILE_RUNNING,
terminalType,
)
telemetryService.captureTerminalUserIntervention(TerminalUserInterventionAction.PROCESS_WHILE_RUNNING)
// Proceed while running - but still capture user feedback if provided
if (text || (images && images.length > 0) || (files && files.length > 0)) {
userFeedback = { text, images, files }
}
didContinue = true
// Notify caller to start background command tracking
// Pass existing output lines so they can be written to the log file
// and send log file path to UI if tracking was started
if (onProceedWhileRunning) {
const trackingResult = onProceedWhileRunning(outputLines)
// Clear timers first
if (chunkTimer) {
clearTimeout(chunkTimer)
chunkTimer = null
}
if (completionTimer) {
clearTimeout(completionTimer)
completionTimer = null
}
// Set early return result BEFORE resuming the process
// This prevents the orchestrator's listener from processing new lines
const result = terminalManager.processOutput(outputLines)
const logMsg = trackingResult?.logFilePath ? `Log file: ${trackingResult.logFilePath}\n` : ""
const outputMsg = result.length > 0 ? `Output so far:\n${result}` : ""
backgroundTrackingResult = {
userRejected: false,
result: `Command is running in the background. You can proceed with other tasks.\n${logMsg}${outputMsg}`,
completed: false,
outputLines,
}
// Send log file message to UI BEFORE resuming the process
// This ensures the message appears before any new output lines
if (trackingResult?.logFilePath) {
await callbacks.say("command_output", `\n📋 Output is being logged to: ${trackingResult.logFilePath}`)
}
// Now resume the process - any new lines will be handled by the background tracker
process.continue()
return
}
process.continue()
} else if (response === "noButtonClicked" && text === COMMAND_CANCEL_TOKEN) {
telemetryService.captureTerminalUserIntervention(TerminalUserInterventionAction.CANCELLED, terminalType)
// Set flags BEFORE resuming the process to prevent new lines from being processed
telemetryService.captureTerminalUserIntervention(TerminalUserInterventionAction.CANCELLED)
didCancelViaUi = true
userFeedback = undefined
didContinue = true
process.continue()
outputBuffer = []
outputBufferSize = 0
// Send cancellation message BEFORE resuming the process
// This ensures the message appears before any new output lines
await callbacks.say("command_output", "Command cancelled")
// Now resume the process
process.continue()
} else {
userFeedback = { text, images, files }
didContinue = true
@@ -220,134 +162,31 @@ export async function orchestrateCommandExecution(
chunkTimer = setTimeout(async () => await flushBuffer(), CHUNK_DEBOUNCE_MS)
}
// Large output file-based logging state
let isWritingToFile = false
let largeOutputLogPath: string | null = null
let largeOutputLogStream: fs.WriteStream | null = null
let totalOutputBytes = 0
let totalLineCount = 0
let firstLines: string[] = [] // Keep first N lines for summary
let lastLines: string[] = [] // Keep last N lines for summary (circular buffer)
/**
* Switch to file-based logging when output is too large.
* This protects against memory exhaustion from commands with huge output.
*/
const switchToFileBased = async () => {
if (isWritingToFile) return
isWritingToFile = true
// FIRST: Flush any pending buffer to UI so the "writing to file" message appears at the end
if (outputBuffer.length > 0) {
const chunk = outputBuffer.join("\n")
outputBuffer = []
outputBufferSize = 0
if (!didContinue) {
// Use say() instead of ask() since we're transitioning to file mode
await callbacks.say("command_output", chunk)
}
}
// Clear any pending flush timer
if (chunkTimer) {
clearTimeout(chunkTimer)
chunkTimer = null
}
// Set up file logging
largeOutputLogPath = path.join(os.tmpdir(), `cline-large-output-${Date.now()}.log`)
largeOutputLogStream = fs.createWriteStream(largeOutputLogPath, { flags: "a" })
// Write all existing lines to file in a single batch to reduce I/O overhead
if (outputLines.length > 0) {
largeOutputLogStream.write(outputLines.join("\n") + "\n")
}
// Keep first N lines for summary
firstLines = outputLines.slice(0, SUMMARY_LINES_TO_KEEP)
// Keep last N lines for summary (will be updated as more lines come in)
lastLines = outputLines.slice(-SUMMARY_LINES_TO_KEEP)
// FINALLY: Notify user (now this will appear at the end after all buffered output)
await callbacks.say(
"command_output",
`\n📋 Output is large (${outputLines.length} lines, ${Math.round(totalOutputBytes / 1024)}KB). Writing to: ${largeOutputLogPath}`,
)
}
/**
* Clean up file-based logging resources.
*/
const cleanupFileBased = () => {
if (largeOutputLogStream) {
largeOutputLogStream.end()
largeOutputLogStream = null
}
}
const outputLines: string[] = []
process.on("line", async (line: string) => {
if (didCancelViaUi) {
return
}
// If background tracking is active, don't process lines here
// The background tracker's listener will handle them
if (backgroundTrackingResult) {
return
}
const lineBytes = Buffer.byteLength(line, "utf8")
totalOutputBytes += lineBytes
totalLineCount++
// Check if we should switch to file-based logging
if (!isWritingToFile && (outputLines.length >= MAX_LINES_BEFORE_FILE || totalOutputBytes >= MAX_BYTES_BEFORE_FILE)) {
await switchToFileBased()
}
if (isWritingToFile) {
// Write to file instead of keeping in memory
if (largeOutputLogStream) {
largeOutputLogStream.write(line + "\n")
}
// Update last lines circular buffer for summary
lastLines.push(line)
if (lastLines.length > SUMMARY_LINES_TO_KEEP) {
lastLines.shift()
}
} else {
// Normal behavior - keep in memory
outputLines.push(line)
}
outputLines.push(line)
// Notify caller about output line (for background command tracking)
if (onOutputLine) {
onOutputLine(line)
}
// Apply buffered streaming (only if not in file mode or still showing initial output)
// Apply buffered streaming
if (!didContinue) {
if (!isWritingToFile) {
outputBuffer.push(line)
outputBufferSize += lineBytes
// Flush if buffer is large enough
if (outputBuffer.length >= CHUNK_LINE_COUNT || outputBufferSize >= CHUNK_BYTE_SIZE) {
await flushBuffer()
} else {
scheduleFlush()
}
outputBuffer.push(line)
outputBufferSize += Buffer.byteLength(line, "utf8")
// Flush if buffer is large enough
if (outputBuffer.length >= CHUNK_LINE_COUNT || outputBufferSize >= CHUNK_BYTE_SIZE) {
await flushBuffer()
} else {
scheduleFlush()
}
// When in file mode, we've already notified the user, so don't keep buffering
} else {
// After "Proceed While Running" (without background tracking): stream output directly to UI
// But throttle if we're in file mode to avoid flooding UI
if (!isWritingToFile) {
await callbacks.say("command_output", line)
}
// After "Proceed While Running": stream output directly to UI
await callbacks.say("command_output", line)
}
})
@@ -357,7 +196,7 @@ export async function orchestrateCommandExecution(
// Start timer to detect if waiting for completion takes too long
completionTimer = setTimeout(() => {
if (!completed) {
telemetryService.captureTerminalHang(TerminalHangStage.WAITING_FOR_COMPLETION, terminalType)
telemetryService.captureTerminalHang(TerminalHangStage.WAITING_FOR_COMPLETION)
completionTimer = null
}
}, COMPLETION_TIMEOUT_MS)
@@ -402,8 +241,9 @@ export async function orchestrateCommandExecution(
if (error.message === "COMMAND_TIMEOUT") {
// Timeout triggers "Proceed While Running" behavior
didContinue = true
process.continue()
// Clear all our timers first
// Clear all our timers
if (chunkTimer) {
clearTimeout(chunkTimer)
chunkTimer = null
@@ -413,43 +253,6 @@ export async function orchestrateCommandExecution(
completionTimer = null
}
// If background tracking is available (standalone mode only), use it
// This writes output to a log file and detaches the command
if (onProceedWhileRunning) {
const trackingResult = onProceedWhileRunning(outputLines)
// Set early return result BEFORE resuming the process
// This prevents the orchestrator's listener from processing new lines
const result = terminalManager.processOutput(outputLines)
const logMsg = trackingResult?.logFilePath ? `Log file: ${trackingResult.logFilePath}\n` : ""
const outputMsg = result.length > 0 ? `Output so far:\n${result}` : ""
backgroundTrackingResult = {
userRejected: false,
result: `Command timed out after ${timeoutSeconds} seconds. Running in background.\n${logMsg}${outputMsg}`,
completed: false,
outputLines,
}
// Send log file message to UI BEFORE resuming the process
if (trackingResult?.logFilePath) {
await callbacks.say(
"command_output",
`\n⏱️ Command timed out. Output is being logged to: ${trackingResult.logFilePath}`,
)
}
// Now resume the process - any new lines will be handled by the background tracker
process.continue()
// Clean up file-based logging if active before returning
cleanupFileBased()
return backgroundTrackingResult
}
// VSCode terminal mode: no background tracking available
// Just continue the process and return timeout result
process.continue()
// Process any output we captured before timeout
await setTimeoutPromise(50)
const result = terminalManager.processOutput(outputLines)
@@ -471,14 +274,6 @@ export async function orchestrateCommandExecution(
}
}
// Check if we returned early due to background tracking
// This happens when user clicks "Proceed While Running" with background tracking enabled
if (backgroundTrackingResult) {
// Clean up file-based logging if active before returning
cleanupFileBased()
return backgroundTrackingResult
}
// Clear timer if process completes normally
if (completionTimer) {
clearTimeout(completionTimer)
@@ -488,23 +283,7 @@ export async function orchestrateCommandExecution(
// Wait for a short delay to ensure all messages are sent to the webview
await setTimeoutPromise(50)
// Clean up file-based logging if active
cleanupFileBased()
// Build result based on whether we used file-based logging
let result: string
let resultOutputLines: string[]
if (isWritingToFile) {
// Build summary from first and last lines
const skippedLines = totalLineCount - firstLines.length - lastLines.length
const summaryLines = [...firstLines, `\n... (${skippedLines} lines written to ${largeOutputLogPath}) ...\n`, ...lastLines]
result = terminalManager.processOutput(summaryLines)
resultOutputLines = summaryLines
} else {
result = terminalManager.processOutput(outputLines)
resultOutputLines = outputLines
}
const result = terminalManager.processOutput(outputLines)
if (didCancelViaUi) {
return {
@@ -513,8 +292,7 @@ export async function orchestrateCommandExecution(
`Command cancelled. ${result.length > 0 ? `\nOutput captured before cancellation:\n${result}` : ""}`,
),
completed: false,
outputLines: resultOutputLines,
logFilePath: largeOutputLogPath || undefined,
outputLines,
}
}
@@ -536,30 +314,25 @@ export async function orchestrateCommandExecution(
fileContentString,
),
completed: false,
outputLines: resultOutputLines,
logFilePath: largeOutputLogPath || undefined,
outputLines,
}
}
if (completed) {
const logFileMsg = largeOutputLogPath ? `\nFull output saved to: ${largeOutputLogPath}` : ""
return {
userRejected: false,
result: `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}${logFileMsg}`,
result: `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`,
completed: true,
outputLines: resultOutputLines,
logFilePath: largeOutputLogPath || undefined,
outputLines,
}
} else {
const logFileMsg = largeOutputLogPath ? `\nFull output saved to: ${largeOutputLogPath}` : ""
return {
userRejected: false,
result: `Command is still running in the user's terminal.${
result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
}${logFileMsg}\n\nYou will be updated on the terminal status and new output in the future.`,
}\n\nYou will be updated on the terminal status and new output in the future.`,
completed: false,
outputLines: resultOutputLines,
logFilePath: largeOutputLogPath || undefined,
outputLines,
}
}
}
-115
View File
@@ -1,115 +0,0 @@
/**
* Terminal Constants
*
* Central location for all terminal-related constants.
* This makes it easy to understand and tune terminal behavior.
*/
// =============================================================================
// Process "Hot" State Timeouts
// =============================================================================
// How long to wait after output before considering the process "cool"
// This stalls API requests to let terminal output settle
/** Normal timeout after last output (2 seconds) */
export const PROCESS_HOT_TIMEOUT_NORMAL = 2_000
/** Extended timeout for compilation/build commands (15 seconds) */
export const PROCESS_HOT_TIMEOUT_COMPILING = 15_000
// =============================================================================
// Output Buffering (CommandOrchestrator)
// =============================================================================
// Controls how output is chunked and sent to the UI
/** Lines to buffer before flushing to UI */
export const CHUNK_LINE_COUNT = 20
/** Bytes to buffer before flushing to UI */
export const CHUNK_BYTE_SIZE = 2048 // 2KB
/** Debounce time for buffer flush */
export const CHUNK_DEBOUNCE_MS = 100
/** Timeout to detect stuck buffer */
export const BUFFER_STUCK_TIMEOUT_MS = 6000 // 6 seconds
/** Timeout to detect stuck completion */
export const COMPLETION_TIMEOUT_MS = 6000 // 6 seconds
// =============================================================================
// Large Output Protection
// =============================================================================
// Prevents memory exhaustion and context window overflow
/** Switch to file-based logging after this many lines */
export const MAX_LINES_BEFORE_FILE = 1000
/** Switch to file-based logging after this many bytes */
export const MAX_BYTES_BEFORE_FILE = 512 * 1024 // 512KB
/** Lines to keep at start/end for summary when truncating */
export const SUMMARY_LINES_TO_KEEP = 100
/** Maximum size for fullOutput storage (memory protection) */
export const MAX_FULL_OUTPUT_SIZE = 1024 * 1024 // 1MB
/** Maximum lines to return from getUnretrievedOutput */
export const MAX_UNRETRIEVED_LINES = 500
/** Lines to keep at start/end when truncating unretrieved output */
export const TRUNCATE_KEEP_LINES = 100
// =============================================================================
// Output Line Limits (processOutput)
// =============================================================================
// Controls truncation when returning output to AI
/** Default max lines for command output */
export const DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT = 500
/** Max lines for subagent commands (more context needed) */
export const DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT = 2000
// =============================================================================
// Background Command Tracking
// =============================================================================
// Controls background command behavior for "Proceed While Running"
/** Hard timeout for background commands to prevent zombie processes (10 minutes) */
export const BACKGROUND_COMMAND_TIMEOUT_MS = 10 * 60 * 1000
// =============================================================================
// Compilation Detection Markers
// =============================================================================
// Used to detect if a command is compiling/building
/** Markers that indicate compilation is starting */
export const COMPILING_MARKERS = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
/** Markers that indicate compilation is done (nullify extended timeout) */
export const COMPILING_NULLIFIERS = [
"compiled",
"success",
"finish",
"complete",
"succeed",
"done",
"end",
"stop",
"exit",
"terminate",
"error",
"fail",
]
/**
* Check if terminal output indicates compilation/building.
* Matches markers anywhere in the output.
*/
export function isCompilingOutput(data: string): boolean {
const lowerData = data.toLowerCase()
const hasMarker = COMPILING_MARKERS.some((marker) => lowerData.includes(marker.toLowerCase()))
const hasNullifier = COMPILING_NULLIFIERS.some((nullifier) => lowerData.includes(nullifier.toLowerCase()))
return hasMarker && !hasNullifier
}
+11 -1
View File
@@ -23,7 +23,17 @@
export { CommandExecutor } from "./CommandExecutor"
// Export command orchestrator (shared logic)
export { findLastIndex, orchestrateCommandExecution } from "./CommandOrchestrator"
export {
BUFFER_STUCK_TIMEOUT_MS,
CHUNK_BYTE_SIZE,
CHUNK_DEBOUNCE_MS,
CHUNK_LINE_COUNT,
COMPLETION_TIMEOUT_MS,
findLastIndex,
orchestrateCommandExecution,
} from "./CommandOrchestrator"
// Export terminal process interface
// Export standalone terminal implementations
export { StandaloneTerminal } from "./standalone/StandaloneTerminal"
@@ -4,29 +4,12 @@
* This class provides the same interface as VSCode's TerminalManager but works
* in CLI and JetBrains environments by using subprocess management instead of
* VSCode's terminal API.
*
* Also handles background command tracking for "Proceed While Running" functionality:
* - Logs output to temp files for later retrieval
* - Tracks command status (running, completed, error, timed_out)
* - Implements 10-minute hard timeout to prevent zombie processes
* - Provides summary for environment details
*/
import * as fs from "fs"
import * as os from "os"
import * as path from "path"
import {
BACKGROUND_COMMAND_TIMEOUT_MS,
DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT,
DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT,
} from "../constants"
import type { BackgroundCommand, ITerminalManager, TerminalInfo, TerminalProcessResultPromise } from "../types"
import type { ITerminalManager, TerminalInfo, TerminalProcessResultPromise } from "../types"
import { StandaloneTerminalProcess } from "./StandaloneTerminalProcess"
import { StandaloneTerminalRegistry } from "./StandaloneTerminalRegistry"
// Re-export BackgroundCommand for backwards compatibility
export type { BackgroundCommand }
/**
* Helper function to merge a process with a promise for the TerminalProcessResultPromise type.
* This allows the returned object to be both awaitable and have event methods.
@@ -80,27 +63,14 @@ export class StandaloneTerminalManager implements ITerminalManager {
private terminalReuseEnabled: boolean = true
/** Maximum output lines to keep */
private terminalOutputLineLimit: number = DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT
private terminalOutputLineLimit: number = 500
/** Maximum output lines for subagent commands */
private subagentTerminalOutputLineLimit: number = DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT
private subagentTerminalOutputLineLimit: number = 2000
/** Default terminal profile */
private defaultTerminalProfile: string = "default"
// =========================================================================
// Background Command Tracking
// =========================================================================
/** Map of background command ID to command info */
private backgroundCommands: Map<string, BackgroundCommand> = new Map()
/** Map of background command ID to log file write stream */
private logStreams: Map<string, fs.WriteStream> = new Map()
/** Map of background command ID to timeout handle */
private backgroundTimeouts: Map<string, NodeJS.Timeout> = new Map()
/**
* Run a command in the specified terminal.
* @param terminalInfo The terminal to run the command in
@@ -118,8 +88,9 @@ export class StandaloneTerminalManager implements ITerminalManager {
terminalInfo.busy = false
})
process.once("error", (_error: Error) => {
process.once("error", (error: Error) => {
terminalInfo.busy = false
console.error(`[StandaloneTerminalManager] Command error on terminal ${terminalInfo.id}:`, error)
})
// Create promise for the process
@@ -167,6 +138,7 @@ export class StandaloneTerminalManager implements ITerminalManager {
availableTerminal.terminal.shellIntegration.cwd.fsPath = cwd
}
this.terminalIds.add(availableTerminal.id)
console.log(`[StandaloneTerminalManager] Reused terminal ${availableTerminal.id} with cd`)
return availableTerminal
}
}
@@ -186,19 +158,10 @@ export class StandaloneTerminalManager implements ITerminalManager {
* @returns Array of terminal info with id and last command
*/
getTerminals(busy: boolean): { id: number; lastCommand: string }[] {
const allTerminalIds = Array.from(this.terminalIds)
const terminals = allTerminalIds
return Array.from(this.terminalIds)
.map((id) => this.registry.getTerminal(id))
.filter((t): t is TerminalInfo => {
if (t === undefined) {
return false
}
return t.busy === busy
})
.filter((t): t is TerminalInfo => t !== undefined && t.busy === busy)
.map((t) => ({ id: t.id, lastCommand: t.lastCommand }))
return terminals
}
/**
@@ -250,9 +213,6 @@ export class StandaloneTerminalManager implements ITerminalManager {
* Dispose of all terminals and clean up resources.
*/
disposeAll(): void {
// Dispose background commands first
this.disposeBackgroundCommands()
// Terminate all processes
for (const [_terminalId, process] of this.processes) {
if (process && process.terminate) {
@@ -409,212 +369,4 @@ export class StandaloneTerminalManager implements ITerminalManager {
closeAllTerminals(): number {
return this.closeTerminals(() => true, true)
}
// =========================================================================
// Background Command Tracking Methods
// =========================================================================
/**
* Track a command that will continue running in the background.
* Called when user clicks "Proceed While Running".
* Creates a log file and pipes output to it.
* Sets up a 10-minute hard timeout to prevent zombie processes.
*
* @param process The terminal process to track
* @param command The command string being executed
* @param existingOutput Output lines already captured before tracking started
* @returns The background command info with log file path
*/
trackBackgroundCommand(
process: TerminalProcessResultPromise,
command: string,
existingOutput: string[] = [],
): BackgroundCommand {
const id = `background-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
const logFilePath = path.join(os.tmpdir(), `cline-${id}.log`)
const backgroundCommand: BackgroundCommand = {
id,
command,
startTime: Date.now(),
status: "running",
logFilePath,
lineCount: existingOutput.length,
process,
}
// Create write stream for log file
const logStream = fs.createWriteStream(logFilePath, { flags: "a" })
this.logStreams.set(id, logStream)
// Write existing output that was captured before tracking started
if (existingOutput.length > 0) {
logStream.write(existingOutput.join("\n") + "\n")
}
// Pipe future process output to log file
process.on("line", (line: string) => {
backgroundCommand.lineCount++
logStream.write(line + "\n")
})
// Set up 10-minute hard timeout to prevent zombie processes
const timeoutId = setTimeout(() => {
if (backgroundCommand.status === "running") {
backgroundCommand.status = "timed_out"
logStream.write("\n[TIMEOUT] Process killed after 10 minutes\n")
logStream.end()
// Terminate the process if it has a terminate method
if (process && typeof (process as any).terminate === "function") {
;(process as any).terminate()
}
}
}, BACKGROUND_COMMAND_TIMEOUT_MS)
this.backgroundTimeouts.set(id, timeoutId)
// Listen for completion - clear timeout
process.on("completed", () => {
// Guard: Skip if already handled by timeout
if (backgroundCommand.status !== "running") {
return
}
const timeout = this.backgroundTimeouts.get(id)
if (timeout) {
clearTimeout(timeout)
this.backgroundTimeouts.delete(id)
}
backgroundCommand.status = "completed"
logStream.end()
})
// Listen for errors - clear timeout
process.on("error", (error: Error) => {
// Guard: Skip if already handled by timeout
if (backgroundCommand.status !== "running") {
return
}
const timeout = this.backgroundTimeouts.get(id)
if (timeout) {
clearTimeout(timeout)
this.backgroundTimeouts.delete(id)
}
backgroundCommand.status = "error"
// Try to extract exit code from error message if available
const exitCodeMatch = error.message.match(/exit code (\d+)/)
if (exitCodeMatch) {
backgroundCommand.exitCode = parseInt(exitCodeMatch[1], 10)
}
logStream.end()
})
this.backgroundCommands.set(id, backgroundCommand)
return backgroundCommand
}
/**
* Get a specific background command by ID.
*/
getBackgroundCommand(id: string): BackgroundCommand | undefined {
return this.backgroundCommands.get(id)
}
/**
* Get all tracked background commands.
*/
getAllBackgroundCommands(): BackgroundCommand[] {
return Array.from(this.backgroundCommands.values())
}
/**
* Get only running background commands.
*/
getRunningBackgroundCommands(): BackgroundCommand[] {
return this.getAllBackgroundCommands().filter((c) => c.status === "running")
}
/**
* Check if there are any active background commands.
*/
hasActiveBackgroundCommands(): boolean {
return this.getRunningBackgroundCommands().length > 0
}
/**
* Cancel/terminate a specific background command.
* @param id The background command ID to cancel
* @returns true if cancelled, false if not found or already completed
*/
cancelBackgroundCommand(id: string): boolean {
const command = this.backgroundCommands.get(id)
if (!command || command.status !== "running") {
return false
}
// Clear timeout
const timeout = this.backgroundTimeouts.get(id)
if (timeout) {
clearTimeout(timeout)
this.backgroundTimeouts.delete(id)
}
// Close log stream
const logStream = this.logStreams.get(id)
if (logStream) {
logStream.write("\n[CANCELLED] Command cancelled by user\n")
logStream.end()
this.logStreams.delete(id)
}
// Terminate process
if (command.process && typeof (command.process as any).terminate === "function") {
;(command.process as any).terminate()
}
command.status = "error"
return true
}
/**
* Get a summary string for environment details.
* Shows running background commands with duration, line count, and log paths.
*/
getBackgroundCommandsSummary(): string {
const running = this.getRunningBackgroundCommands()
if (running.length === 0) {
return ""
}
const lines = [`# Background Commands (${running.length} running)`]
for (const c of running) {
const duration = Math.round((Date.now() - c.startTime) / 1000 / 60)
lines.push(`- ${c.command} (running ${duration}m, ${c.lineCount} lines, log: ${c.logFilePath})`)
}
return lines.join("\n")
}
/**
* Clean up all background command resources.
* Called when disposing the manager.
*/
disposeBackgroundCommands(): void {
// Clear all timeouts
for (const [_id, timeout] of this.backgroundTimeouts) {
clearTimeout(timeout)
}
this.backgroundTimeouts.clear()
// Close all log streams
for (const [_id, logStream] of this.logStreams) {
try {
logStream.end()
} catch (_error) {
// Ignore errors when closing log streams
}
}
this.logStreams.clear()
// Clear command tracking
this.backgroundCommands.clear()
}
}
@@ -8,19 +8,9 @@
* Implements ITerminalProcess interface for polymorphic usage with CommandExecutor.
*/
import { telemetryService } from "@services/telemetry"
import { ChildProcess, spawn } from "child_process"
import { EventEmitter } from "events"
import { terminateProcessTree } from "@/utils/process-termination"
import {
isCompilingOutput,
MAX_FULL_OUTPUT_SIZE,
MAX_UNRETRIEVED_LINES,
PROCESS_HOT_TIMEOUT_COMPILING,
PROCESS_HOT_TIMEOUT_NORMAL,
TRUNCATE_KEEP_LINES,
} from "../constants"
import type { ITerminal, ITerminalProcess, TerminalProcessEvents } from "../types"
/**
@@ -77,6 +67,8 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
* @param command The command to execute
*/
async run(terminal: ITerminal, command: string): Promise<void> {
console.log(`[StandaloneTerminal] Running command: ${command}`)
// Get shell and working directory from terminal
const shell = (terminal as any)._shellPath || this.getDefaultShell()
const cwd = (terminal as any)._cwd || process.cwd()
@@ -112,12 +104,8 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
// Spawn the process with special handling for "cmd.exe"
this.childProcess = spawn("cmd.exe", shellArgs, shellOptions)
} else {
// Spawn the process with detached: true to create a process group
// This allows us to kill the entire process tree when terminating
this.childProcess = spawn(shell, shellArgs, {
...shellOptions,
detached: true,
})
// Spawn the process
this.childProcess = spawn(shell, shellArgs, shellOptions)
}
// Track process state
@@ -144,7 +132,8 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
})
// Handle process completion
this.childProcess.on("close", (code: number | null, _signal: NodeJS.Signals | null) => {
this.childProcess.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
console.log(`[StandaloneTerminal] Process closed with code ${code}, signal ${signal}`)
this.exitCode = code
this.isCompleted = true
this.emitRemainingBuffer()
@@ -155,18 +144,13 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
this.isHot = false
}
// Track terminal execution telemetry
const success = code === 0 || code === null
telemetryService.captureTerminalExecution(success, "standalone", "child_process")
this.emit("completed")
this.emit("continue")
})
// Handle process errors
this.childProcess.on("error", (error: Error) => {
// Track terminal execution error telemetry
telemetryService.captureTerminalExecution(false, "standalone", "child_process_error")
console.error(`[StandaloneTerminal] Process error:`, error)
this.emit("error", error)
})
@@ -174,6 +158,7 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
;(terminal as any)._process = this.childProcess
;(terminal as any)._processId = this.childProcess.pid
} catch (error) {
console.error(`[StandaloneTerminal] Failed to spawn process:`, error)
this.emit("error", error)
}
}
@@ -191,25 +176,37 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
}
// Check for compilation markers to adjust hot timeout
const isCompiling = isCompilingOutput(data)
const hotTimeout = isCompiling ? PROCESS_HOT_TIMEOUT_COMPILING : PROCESS_HOT_TIMEOUT_NORMAL
const compilingMarkers = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
const markerNullifiers = [
"compiled",
"success",
"finish",
"complete",
"succeed",
"done",
"end",
"stop",
"exit",
"terminate",
"error",
"fail",
]
const isCompiling =
compilingMarkers.some((marker) => data.toLowerCase().includes(marker.toLowerCase())) &&
!markerNullifiers.some((nullifier) => data.toLowerCase().includes(nullifier.toLowerCase()))
const hotTimeout = isCompiling ? 15000 : 2000
this.hotTimer = setTimeout(() => {
this.isHot = false
}, hotTimeout)
// Store full output with size cap to prevent memory exhaustion
// Store full output
this.fullOutput += data
// Cap fullOutput at MAX_FULL_OUTPUT_SIZE to prevent memory exhaustion
if (this.fullOutput.length > MAX_FULL_OUTPUT_SIZE) {
// Keep last half of max size
this.fullOutput = this.fullOutput.slice(-MAX_FULL_OUTPUT_SIZE / 2)
// Reset lastRetrievedIndex since we truncated the beginning
this.lastRetrievedIndex = 0
}
if (this.isListening) {
this.emitLines(data)
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
}
}
@@ -243,37 +240,22 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
/**
* Continue execution without waiting for completion.
* Emits "continue" event but keeps emitting "line" events for background tracking.
*
* Note: We intentionally do NOT call removeAllListeners("line") or set isListening=false
* because background command tracking needs to continue receiving output lines
* after the user clicks "Proceed While Running".
* Stops event emission and resolves the promise.
*/
continue(): void {
this.emitRemainingBuffer()
// Keep isListening = true so we continue emitting "line" events
// This is needed for background command tracking to log output to file
this.isListening = false
this.removeAllListeners("line")
this.emit("continue")
}
/**
* Get output that hasn't been retrieved yet.
* Truncates if output is too large to prevent context window overflow.
* @returns The unretrieved output (truncated if necessary)
* @returns The unretrieved output
*/
getUnretrievedOutput(): string {
const unretrieved = this.fullOutput.slice(this.lastRetrievedIndex)
this.lastRetrievedIndex = this.fullOutput.length
// Truncate if too many lines to prevent context overflow
const lines = unretrieved.split("\n")
if (lines.length > MAX_UNRETRIEVED_LINES) {
const first = lines.slice(0, TRUNCATE_KEEP_LINES)
const last = lines.slice(-TRUNCATE_KEEP_LINES)
const skipped = lines.length - first.length - last.length
return this.removeLastLineArtifacts([...first, `\n... (${skipped} lines truncated) ...\n`, ...last].join("\n"))
}
return this.removeLastLineArtifacts(unretrieved)
}
@@ -323,29 +305,41 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
}
/**
* Terminate the process and all its children.
*
* Uses terminateProcessTree utility which handles:
* - Cross-platform process tree termination via tree-kill
* - Graceful shutdown with SIGTERM
* - SIGKILL fallback after 2 second timeout
* Terminate the process if it's still running.
*/
async terminate(): Promise<void> {
terminate(): void {
if (!this.childProcess || this.isCompleted) {
console.log(`[StandaloneTerminal] Process already completed or doesn't exist, skipping termination`)
return
}
const pid = this.childProcess.pid
if (!pid) {
// Fallback: try to kill the process directly if PID is unavailable
this.childProcess.kill("SIGTERM")
return
}
console.log(`[StandaloneTerminal] Terminating process ${pid} with SIGTERM`)
await terminateProcessTree({
pid,
childProcess: this.childProcess,
isCompleted: () => this.isCompleted,
})
try {
this.childProcess.kill("SIGTERM")
// Force kill after timeout if process doesn't exit gracefully
setTimeout(() => {
if (!this.isCompleted && this.childProcess) {
console.log(`[StandaloneTerminal] Process ${pid} did not exit gracefully, force killing with SIGKILL`)
try {
this.childProcess.kill("SIGKILL")
} catch (killError) {
console.error(`[StandaloneTerminal] Failed to force kill process ${pid}:`, killError)
}
} else {
console.log(`[StandaloneTerminal] Process ${pid} exited gracefully`)
}
}, 5000)
} catch (error) {
console.error(`[StandaloneTerminal] Failed to send SIGTERM to process ${pid}:`, error)
// Try SIGKILL immediately if SIGTERM fails
try {
this.childProcess.kill("SIGKILL")
} catch (killError) {
console.error(`[StandaloneTerminal] Failed to send SIGKILL to process ${pid}:`, killError)
}
}
}
}
+4 -62
View File
@@ -62,10 +62,8 @@ export interface ITerminalProcess extends EventEmitter<TerminalProcessEvents> {
* Terminate the process if it's still running.
* Only available for standalone processes (child_process).
* VSCode terminal processes cannot be terminated via this interface.
*
* May be async to allow for graceful shutdown with SIGKILL fallback.
*/
terminate?(): void | Promise<void>
terminate?(): void
}
// =============================================================================
@@ -245,51 +243,12 @@ export interface StandaloneTerminalOptions {
shellPath?: string
}
// =============================================================================
// Background Command Types
// =============================================================================
/**
* Represents a command running in the background after user clicked "Proceed While Running".
* Used by StandaloneTerminalManager to track background commands.
*/
export interface BackgroundCommand {
/** Unique identifier for the background command */
id: string
/** The command string being executed */
command: string
/** Timestamp when the command started */
startTime: number
/** Current status of the command */
status: "running" | "completed" | "error" | "timed_out"
/** Path to the log file where output is being written */
logFilePath: string
/** Number of lines written to the log file */
lineCount: number
/** Exit code if the command completed or errored */
exitCode?: number
/** The terminal process running the command */
process: TerminalProcessResultPromise
}
// =============================================================================
// Command Executor Types
// =============================================================================
/**
* Tracker for shell integration warnings to determine when to show background terminal suggestion.
* Used internally by CommandExecutor to track warning frequency.
*/
export interface ShellIntegrationWarningTracker {
/** Timestamps of recent shell integration warnings */
timestamps: number[]
/** Timestamp when the suggestion was last shown */
lastSuggestionShown?: number
}
/**
* Represents an active background command that can be cancelled
* @deprecated Use BackgroundCommand instead
*/
export interface ActiveBackgroundCommand {
process: {
@@ -325,13 +284,10 @@ export interface CommandExecutorCallbacks {
ask: (type: string, text?: string, partial?: boolean) => Promise<AskResponse>
/** Update the background command running state in the controller */
updateBackgroundCommandState: (running: boolean) => void
/**
* Update a cline message by index
* Supports updating commandCompleted status and/or text content
*/
updateClineMessage: (index: number, updates: { commandCompleted?: boolean; text?: string }) => Promise<void>
/** Update a cline message by index */
updateClineMessage: (index: number, updates: { commandCompleted?: boolean }) => Promise<void>
/** Get cline messages array */
getClineMessages: () => Array<{ ask?: string; say?: string; text?: string }>
getClineMessages: () => Array<{ ask?: string; say?: string }>
/** Add content to user message for next API request */
addToUserMessageContent: (content: { type: string; text: string }) => void
}
@@ -371,18 +327,6 @@ export interface OrchestrationOptions {
onOutputLine?: (line: string) => void
/** Whether to show shell integration warning with suggestion */
showShellIntegrationSuggestion?: boolean
/**
* Callback invoked when user clicks "Proceed While Running".
* Used to start background command tracking in the terminal manager.
* @param existingOutput The output lines captured so far (to write to log file)
* @returns The log file path if tracking was started, undefined otherwise
*/
onProceedWhileRunning?: (existingOutput: string[]) => { logFilePath: string } | undefined
/**
* The type of terminal being used for telemetry tracking.
* Defaults to "vscode" for backward compatibility.
*/
terminalType?: "vscode" | "standalone"
}
/**
@@ -397,6 +341,4 @@ export interface OrchestrationResult {
completed: boolean
/** All output lines captured */
outputLines: string[]
/** Path to log file if output was too large and written to file */
logFilePath?: string
}
+61 -65
View File
@@ -38,10 +38,6 @@ export interface ClineAuthInfo {
startedAt?: number
}
export interface InternalAuthState extends Omit<AuthState, "user"> {
authInfo?: ClineAuthInfo
}
export interface ClineAccountUserInfo {
createdAt: string
displayName: string
@@ -68,11 +64,9 @@ export interface ClineAccountOrganization {
export class AuthService {
protected static instance: AuthService | null = null
protected provider: IAuthProvider | null = null
protected authState: InternalAuthState = {
hasSessionData: false,
pending: false,
}
protected _authenticated: boolean = false
protected _clineAuthInfo: ClineAuthInfo | null = null
protected _provider: IAuthProvider | null = null
protected _activeAuthStatusUpdateHandlers = new Set<StreamingResponseHandler<AuthState>>()
protected _handlerToController = new Map<StreamingResponseHandler<AuthState>, Controller>()
protected _controller: Controller
@@ -122,11 +116,11 @@ export class AuthService {
* Refreshing it if necessary.
*/
async getAuthToken(): Promise<string | null> {
if (!this.provider) {
if (!this._provider) {
throw new Error("Auth provider is not set")
}
return this.internalGetAuthToken(this.provider)
return this.internalGetAuthToken(this._provider)
}
/**
@@ -134,10 +128,10 @@ export class AuthService {
* @returns The active organization ID, or null if no active organization exists
*/
getActiveOrganizationId(): string | null {
if (!this.authState?.authInfo?.userInfo?.organizations) {
if (!this._clineAuthInfo?.userInfo?.organizations) {
return null
}
const activeOrg = this.authState.authInfo?.userInfo.organizations.find((org) => org.active)
const activeOrg = this._clineAuthInfo.userInfo.organizations.find((org) => org.active)
return activeOrg?.organizationId ?? null
}
@@ -146,26 +140,26 @@ export class AuthService {
* @returns Array of organizations, or undefined if not available
*/
getUserOrganizations(): ClineAccountOrganization[] | undefined {
return this.authState?.authInfo?.userInfo?.organizations
return this._clineAuthInfo?.userInfo?.organizations
}
private async internalGetAuthToken(provider: IAuthProvider): Promise<string | null> {
try {
let clineAccountAuthToken = this.authState?.authInfo?.idToken
if (!this.authState || !clineAccountAuthToken || this.authState?.authInfo?.provider !== provider.name) {
let clineAccountAuthToken = this._clineAuthInfo?.idToken
if (!this._clineAuthInfo || !clineAccountAuthToken || this._clineAuthInfo.provider !== provider.name) {
// Not authenticated
return null
}
// Check if token has expired
if (await provider.shouldRefreshIdToken(clineAccountAuthToken, this.authState.authInfo.expiresAt)) {
if (await provider.shouldRefreshIdToken(clineAccountAuthToken, this._clineAuthInfo.expiresAt)) {
// 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.authState?.authInfo?.idToken
clineAccountAuthToken = this._clineAuthInfo?.idToken
return clineAccountAuthToken ? `workos:${clineAccountAuthToken}` : null
}
@@ -175,18 +169,19 @@ export class AuthService {
try {
const updatedAuthInfo = await provider.retrieveClineAuthInfo(this._controller)
this.authState = updatedAuthInfo
clineAccountAuthToken = updatedAuthInfo.authInfo?.idToken
authStatusChanged = true
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.authState = {
hasSessionData: false,
pending: false,
}
telemetryService.captureAuthLoggedOut(this.provider?.name, LogoutReason.ERROR_RECOVERY)
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)
@@ -220,7 +215,7 @@ export class AuthService {
protected _initProvider(): void {
// Only ClineAuthProvider is supported going forward
this.provider = new ClineAuthProvider()
this._provider = new ClineAuthProvider()
}
/**
@@ -228,15 +223,15 @@ export class AuthService {
* @returns The provider name (e.g., "cline", "firebase"), or null if not authenticated
*/
getProviderName(): string | null {
return this.authState?.authInfo?.provider ?? null
return this._clineAuthInfo?.provider ?? null
}
getInfo(): AuthState {
// TODO: this logic should be cleaner, but this will determine the authentication state for the webview -- if a user object is returned then the webview assumes authenticated, otherwise it assumes logged out (we previously returned a UserInfo object with empty fields, and this represented a broken logged in state)
let user: any = null
if (this.authState.authInfo) {
const userInfo = this.authState.authInfo?.userInfo
this.authState.authInfo.userInfo.appBaseUrl = ClineEnv.config()?.appBaseUrl
if (this._clineAuthInfo && this._authenticated) {
const userInfo = this._clineAuthInfo.userInfo
this._clineAuthInfo.userInfo.appBaseUrl = ClineEnv.config()?.appBaseUrl
user = UserInfo.create({
// TODO: create proto for new user info type
@@ -250,21 +245,17 @@ export class AuthService {
return AuthState.create({
user,
pending: this.authState.pending,
hasSessionData: this.authState.hasSessionData,
error: this.authState.error,
nextRetryAt: this.authState.nextRetryAt,
})
}
async createAuthRequest(strict = false): Promise<String> {
// In strict mode, we do not open a new auth window if already authenticated
if (strict) {
if (strict && this._authenticated) {
this.sendAuthStatusUpdate()
return String.create({ value: "Already authenticated" })
}
if (!this.provider) {
if (!this._provider) {
return String.create({
value: "Authentication provider is not configured",
})
@@ -273,25 +264,23 @@ export class AuthService {
const callbackHost = await HostProvider.get().getCallbackUrl()
const callbackUrl = `${callbackHost}/auth`
const authUrl = await this.provider.getAuthRequest(callbackUrl)
const authUrl = await this._provider.getAuthRequest(callbackUrl)
const authUrlString = authUrl.toString()
await openExternal(authUrlString)
telemetryService.captureAuthStarted(this.provider.name)
telemetryService.captureAuthStarted(this._provider.name)
return String.create({ value: authUrlString })
}
async handleDeauth(reason: LogoutReason = LogoutReason.UNKNOWN): Promise<void> {
if (!this.provider) {
if (!this._provider) {
throw new Error("Auth provider is not set")
}
try {
telemetryService.captureAuthLoggedOut(this.provider.name, reason)
this.authState = {
hasSessionData: false,
pending: false,
}
telemetryService.captureAuthLoggedOut(this._provider.name, reason)
this._clineAuthInfo = null
this._authenticated = false
this.destroyTokens()
this.sendAuthStatusUpdate()
} catch (error) {
@@ -301,17 +290,19 @@ export class AuthService {
}
async handleAuthCallback(authorizationCode: string, provider: string): Promise<void> {
if (!this.provider) {
if (!this._provider) {
throw new Error("Auth provider is not set")
}
try {
this.authState = await this.provider.signIn(this._controller, authorizationCode, provider)
telemetryService.captureAuthSucceeded(this.provider.name)
this._clineAuthInfo = await this._provider.signIn(this._controller, authorizationCode, provider)
this._authenticated = this._clineAuthInfo?.idToken !== undefined
telemetryService.captureAuthSucceeded(this._provider.name)
await setWelcomeViewCompleted(this._controller, { value: true })
} catch (error) {
console.error("Error signing in with custom token:", error)
telemetryService.captureAuthFailed(this.provider.name)
telemetryService.captureAuthFailed(this._provider.name)
throw error
} finally {
await this.sendAuthStatusUpdate()
@@ -332,26 +323,32 @@ export class AuthService {
* This is typically called when the extension is activated.
*/
async restoreRefreshTokenAndRetrieveAuthInfo(): Promise<void> {
if (!this.provider) {
if (!this._provider) {
throw new Error("Auth provider is not set")
}
try {
this.authState = await this.retrieveAuthInfo()
this._clineAuthInfo = await this.retrieveAuthInfo()
if (this._clineAuthInfo) {
this._authenticated = true
await this.sendAuthStatusUpdate()
} else {
console.warn("No user found after restoring auth token")
this._authenticated = false
this._clineAuthInfo = null
telemetryService.captureAuthLoggedOut(this._provider?.name, LogoutReason.ERROR_RECOVERY)
}
} catch (error) {
console.error("Error restoring auth token:", error)
this.authState = {
pending: false,
hasSessionData: false,
error: "Unknown error.",
}
} finally {
this.sendAuthStatusUpdate().catch(console.error)
this._authenticated = false
this._clineAuthInfo = null
telemetryService.captureAuthLoggedOut(this._provider?.name, LogoutReason.ERROR_RECOVERY)
return
}
}
private async retrieveAuthInfo(): Promise<InternalAuthState> {
if (!this.provider) {
private async retrieveAuthInfo(): Promise<ClineAuthInfo | null> {
if (!this._provider) {
throw new Error("Auth provider is not set")
}
@@ -361,7 +358,7 @@ export class AuthService {
await this._refreshPromise
}
return this.provider.retrieveClineAuthInfo(this._controller)
return this._provider.retrieveClineAuthInfo(this._controller)
}
/**
@@ -430,11 +427,10 @@ export class AuthService {
await Promise.all(streamSends)
// Identify the user in telemetry if available
const userInfo = this.authState.authInfo?.userInfo
if (userInfo?.id) {
telemetryService.identifyAccount(userInfo)
if (this._clineAuthInfo?.userInfo?.id) {
telemetryService.identifyAccount(this._clineAuthInfo.userInfo)
// Poll feature flags immediately for authenticated users to ensure cache is populated
await featureFlagsService.poll(userInfo.id)
await featureFlagsService.poll(this._clineAuthInfo?.userInfo?.id)
} else {
// Poll feature flags for unauthenticated state
await featureFlagsService.poll(undefined)
+15 -21
View File
@@ -38,10 +38,10 @@ export class AuthServiceMock extends AuthService {
}
override async getAuthToken(): Promise<string | null> {
if (!this.authState.authInfo) {
if (!this._clineAuthInfo) {
return null
}
return this.authState.authInfo.idToken
return this._clineAuthInfo.idToken
}
override async createAuthRequest(): Promise<String> {
@@ -49,13 +49,11 @@ export class AuthServiceMock extends AuthService {
const authUrl = new URL(ClineEnv.config().apiBaseUrl)
const authUrlString = authUrl.toString()
// Call the parent implementation
if (this.authState.hasSessionData && this.authState.authInfo) {
if (this._authenticated && this._clineAuthInfo) {
console.log("Already authenticated with mock server")
return String.create({ value: authUrlString })
}
this.authState.pending = false
try {
// Use token exchange endpoint like ClineAuthProvider
const tokenExchangeUri = new URL(CLINE_API_ENDPOINT.TOKEN_EXCHANGE, ClineEnv.config().apiBaseUrl)
@@ -86,7 +84,7 @@ export class AuthServiceMock extends AuthService {
const authData = responseData.data
// Convert to ClineAuthInfo format matching ClineAuthProvider
this.authState.authInfo = {
this._clineAuthInfo = {
idToken: authData.accessToken,
refreshToken: authData.refreshToken,
expiresAt: new Date(authData.expiresAt).getTime() / 1000,
@@ -99,22 +97,21 @@ export class AuthServiceMock extends AuthService {
appBaseUrl: ClineEnv.config().appBaseUrl,
subject: authData.userInfo.subject,
},
provider: this.provider?.name || "mock",
provider: this._provider?.name || "mock",
}
this.authState.pending = false
console.log(`Successfully authenticated with mock server as ${authData.userInfo.name} (${authData.userInfo.email})`)
const visibleWebview = WebviewProvider.getVisibleInstance()
// Use appropriate provider name for callback
const providerName = this.provider?.name || "mock"
const providerName = this._provider?.name || "mock"
// Simulate handling the auth callback as if from a real provider
await visibleWebview?.controller.handleAuthCallback(authData.accessToken, providerName)
} catch (error) {
console.error("Error signing in with mock server:", error)
this.authState.hasSessionData = false
this.authState.authInfo = undefined
this._authenticated = false
this._clineAuthInfo = null
throw error
}
@@ -123,8 +120,7 @@ export class AuthServiceMock extends AuthService {
override async handleAuthCallback(_token: string, _provider: string): Promise<void> {
try {
this.authState.hasSessionData = true
this.authState.pending = false
this._authenticated = true
await setWelcomeViewCompleted(this._controller, { value: true })
await this.sendAuthStatusUpdate()
} catch (error) {
@@ -135,20 +131,18 @@ export class AuthServiceMock extends AuthService {
override async restoreRefreshTokenAndRetrieveAuthInfo(): Promise<void> {
try {
this.authState.pending = false
if (this.authState.authInfo) {
this.authState.hasSessionData = true
if (this._clineAuthInfo) {
this._authenticated = true
await this.sendAuthStatusUpdate()
} else {
console.warn("No user found after restoring auth token")
this.authState.hasSessionData = false
this.authState.authInfo = undefined
this._authenticated = false
this._clineAuthInfo = null
}
} catch (error) {
console.error("Error restoring auth token:", error)
this.authState.hasSessionData = false
this.authState.authInfo = undefined
this._authenticated = false
this._clineAuthInfo = null
return
}
}
@@ -8,7 +8,7 @@ import { Logger } from "@/services/logging/Logger"
import { telemetryService } from "@/services/telemetry"
import { CLINE_API_ENDPOINT } from "@/shared/cline/api"
import { fetch, getAxiosSettings } from "@/shared/net"
import { type ClineAccountUserInfo, type ClineAuthInfo, InternalAuthState } from "../AuthService"
import { type ClineAccountUserInfo, type ClineAuthInfo } from "../AuthService"
import { parseJwtPayload } from "../oca/utils/utils"
import { IAuthProvider } from "./IAuthProvider"
@@ -123,10 +123,7 @@ export class ClineAuthProvider implements IAuthProvider {
controller.stateManager.setSecret("cline:clineAccountId", undefined)
this.refreshRetryCount = 0
this.lastRefreshAttempt = 0
return {
hasSessionData: false,
pending: false,
}
return null
}
private logFailedRefreshAttempt(response: Response, storedAuthData?: ClineAuthInfo) {
@@ -161,7 +158,7 @@ export class ClineAuthProvider implements IAuthProvider {
* @param controller - The controller instance to access stored secrets.
* @returns {Promise<ClineAuthInfo | null>} A promise that resolves with the auth info or null.
*/
async retrieveClineAuthInfo(controller: Controller): Promise<InternalAuthState> {
async retrieveClineAuthInfo(controller: Controller): Promise<ClineAuthInfo | null> {
try {
// Get the stored auth data from secure storage
const storedAuthDataString = controller.stateManager.getSecretKey("cline:clineAccountId")
@@ -171,10 +168,7 @@ export class ClineAuthProvider implements IAuthProvider {
// Reset retry count when there's no stored auth
this.refreshRetryCount = 0
this.lastRefreshAttempt = 0
return {
hasSessionData: false,
pending: false,
}
return null
}
// Parse the stored auth data
@@ -201,11 +195,7 @@ export class ClineAuthProvider implements IAuthProvider {
) {
this.refreshRetryCount = 0
this.lastRefreshAttempt = 0
return {
authInfo: storedAuthData,
hasSessionData: true,
pending: false,
}
return storedAuthData
}
// Check if we need to wait before retrying
@@ -215,28 +205,14 @@ export class ClineAuthProvider implements IAuthProvider {
Logger.debug(
`Waiting ${Math.ceil((this.RETRY_DELAY_MS - timeSinceLastAttempt) / 1000)}s before retry attempt ${this.refreshRetryCount + 1}/${this.MAX_REFRESH_RETRIES}`,
)
return {
hasSessionData: true,
pending: true,
nextRetryAt: now + this.RETRY_DELAY_MS - timeSinceLastAttempt,
}
return null
}
// Check if we've exceeded max retries
if (this.refreshRetryCount >= this.MAX_REFRESH_RETRIES) {
const waitTime = this.RETRY_DELAY_MS * 3
setTimeout(() => {
this.refreshRetryCount = 0
this.lastRefreshAttempt = 0
}, waitTime)
return {
hasSessionData: true,
pending: false,
error: "Failed to fetch user information. Waiting before retrying again.",
nextRetryAt: now + waitTime,
}
Logger.error(`Max refresh retries (${this.MAX_REFRESH_RETRIES}) exceeded.`)
// Don't clear session - return stored data and let API request fail later
return storedAuthData
}
// Try to refresh the token using the refresh token
@@ -257,12 +233,7 @@ export class ClineAuthProvider implements IAuthProvider {
this.refreshRetryCount = 0
this.lastRefreshAttempt = 0
Logger.debug("Token refresh successful")
return {
authInfo: authInfo,
hasSessionData: true,
pending: false,
}
return authInfo || null
} catch (refreshError) {
Logger.error(
`Token refresh failed (attempt ${this.refreshRetryCount}/${this.MAX_REFRESH_RETRIES}):`,
@@ -278,12 +249,7 @@ export class ClineAuthProvider implements IAuthProvider {
// For network errors, return stored data - let the API request fail later
// when the user actually tries to use Cline, not at startup
return {
hasSessionData: true,
error: "Unknown network error.",
pending: true,
nextRetryAt: now + this.RETRY_DELAY_MS,
}
return storedAuthData
}
}
@@ -293,11 +259,7 @@ export class ClineAuthProvider implements IAuthProvider {
// Is the token valid?
if (storedAuthData.idToken && storedAuthData.refreshToken && storedAuthData.userInfo.id) {
return {
authInfo: storedAuthData,
hasSessionData: true,
pending: false,
}
return storedAuthData
}
// Verify the token structure
@@ -311,12 +273,7 @@ export class ClineAuthProvider implements IAuthProvider {
if (payload.external_id) {
storedAuthData.userInfo.id = payload.external_id
}
return {
authInfo: storedAuthData,
hasSessionData: true,
pending: false,
}
return storedAuthData
} catch (error) {
Logger.error("Authentication failed with stored credential:", error)
// Reset retry count on unexpected errors
@@ -324,12 +281,7 @@ export class ClineAuthProvider implements IAuthProvider {
this.refreshRetryCount = 0
this.lastRefreshAttempt = 0
}
return {
hasSessionData: true,
pending: false,
error: "Unexpected error.",
nextRetryAt: Date.now() + this.RETRY_DELAY_MS,
}
return null
}
}
@@ -435,7 +387,7 @@ export class ClineAuthProvider implements IAuthProvider {
}
}
async signIn(controller: Controller, authorizationCode: string, provider: string): Promise<InternalAuthState> {
async signIn(controller: Controller, authorizationCode: string, provider: string): Promise<ClineAuthInfo | null> {
try {
// Get the callback URL that was used during the initial auth request
const callbackHost = await HostProvider.get().getCallbackUrl()
@@ -486,11 +438,7 @@ export class ClineAuthProvider implements IAuthProvider {
controller.stateManager.setSecret("cline:clineAccountId", JSON.stringify(clineAuthInfo))
return {
authInfo: clineAuthInfo,
hasSessionData: true,
pending: false,
}
return clineAuthInfo
} catch (error) {
Logger.error("Error handling auth callback:", error)
throw error
+4 -3
View File
@@ -1,12 +1,13 @@
import { EnvironmentConfig } from "@/config"
import { Controller } from "@/core/controller"
import { InternalAuthState } from "../AuthService"
import { ClineAuthInfo } from "../AuthService"
export interface IAuthProvider {
readonly name: string
config: EnvironmentConfig
retrieveClineAuthInfo(controller: Controller): Promise<InternalAuthState>
shouldRefreshIdToken(token: string, expiresAt?: number): Promise<boolean>
retrieveClineAuthInfo(controller: Controller): Promise<ClineAuthInfo | null>
refreshToken(refreshToken: string, storedData: ClineAuthInfo): Promise<Partial<ClineAuthInfo>>
getAuthRequest(callbackUrl: string): Promise<string>
signIn(controller: Controller, authorizationCode: string, provider: string): Promise<InternalAuthState>
signIn(controller: Controller, authorizationCode: string, provider: string): Promise<ClineAuthInfo | null>
}

Some files were not shown because too many files have changed in this diff Show More