mirror of
https://github.com/cline/cline.git
synced 2026-09-02 07:42:19 +08:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61c769b995 | |||
| a24ab0c6b8 | |||
| 3bc6cc6a92 | |||
| bd7f2a29d6 | |||
| b1c5f0b811 | |||
| ac30e49e0b | |||
| 95411e54c1 | |||
| b756761a3f | |||
| d86f5ed4c9 | |||
| 336d31f95f | |||
| fc9c413058 | |||
| e0282826fc | |||
| bf83b816e2 | |||
| 24033613cd | |||
| f76cfbce48 | |||
| 2a63545224 | |||
| 5308dedc81 | |||
| 4c699da00b | |||
| 6cff60b53b | |||
| 24dcd9ea7c | |||
| 3cded47baf | |||
| 7000bb1894 | |||
| 0de65457c1 | |||
| c83b404764 | |||
| 96b48182c6 | |||
| 3b6e42f0ce | |||
| adb3759738 | |||
| 215fc36d17 | |||
| 53bd0ecd8d | |||
| 9e851a8f7c | |||
| 5d94bbc6fd | |||
| ea66c6c584 | |||
| be7f349693 | |||
| 9c03dfa717 | |||
| c5601d8d7d | |||
| a281aea0e7 | |||
| e8bc6b9794 | |||
| e018199fef | |||
| 35ecf3e551 | |||
| ee028033c7 | |||
| 7adfcabfa0 | |||
| 51201b00be | |||
| 0d43d014eb | |||
| df7d5062bc | |||
| 98aec6c9d3 | |||
| 5a65b12a43 | |||
| 741bea5af8 | |||
| ad1e35a425 | |||
| 2e2239b138 | |||
| 03aac0cd80 | |||
| 22cc73f614 | |||
| 06b05ddfe9 | |||
| 2670a4a171 | |||
| 1699c9a63a | |||
| 808dd42ae9 | |||
| 71af56f493 | |||
| 1167b4f3a6 | |||
| e8d6370b0c | |||
| e243376a39 | |||
| 4f1be9d512 | |||
| 94d36ce719 | |||
| 5df7498f03 | |||
| c2b87252ac | |||
| be353bb3da | |||
| 0a7791de1f | |||
| 7cca102e14 | |||
| 4f591de6a9 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix decimal input crash in OpenAI Compatible price fields (#8129)
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add endpoint configuration file support for on-premise deployments
|
||||
|
||||
Enterprise customers can now configure custom API endpoints by creating a `~/.cline/endpoints.json` file with custom URLs for `appBaseUrl`, `apiBaseUrl`, and `mcpBaseUrl`. When this file is present, Cline runs in on-premise mode with the custom endpoints.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: prevent infinite retry loops when replace_in_file fails repeatedly
|
||||
|
||||
Add safeguards to prevent the LLM from getting stuck in infinite retry loops when `replace_in_file` operations fail repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: skip diff error UI handling during streaming to prevent flickering
|
||||
|
||||
Suppress diff view error notifications while content is actively streaming to prevent visual flickering and improve user experience. Error handling is deferred until streaming completes.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix(extract-text): strip notebook outputs to reduce context size
|
||||
|
||||
Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing the amount of context sent to the LLM while preserving the essential code and markdown content.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: throttle diff view updates during streaming
|
||||
|
||||
Add throttling to diff view updates during content streaming to reduce UI flickering and improve performance. Updates are now batched at reasonable intervals instead of firing on every token received.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Always include the latest working directory path in system prompt.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Disable PostHog telemetry, error tracking, and feature flags in self-hosted mode
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: build complete handlers when upadting the api config
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Updating script documentation and removing unnecessary continue on error
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove deprecated zai-glm-4.6 model from Cerebras provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Make Sonnet 4.5 the default Amazon Bedrock model id
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed missing provider from list
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat(skills): Make skills always enabled and remove feature toggle setting
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed Favorite Icon / Star from getting clipped in the task history view
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Disable PostHog and build-time OpenTelemetry telemetry in self-hosted/on-premise mode. Enterprise customers running self-hosted deployments will no longer send any telemetry to Cline's collectors. Runtime environment OTEL and remote config OTEL remain available for enterprises to configure their own telemetry collection.
|
||||
@@ -16,13 +16,6 @@
|
||||
TELEMETRY_SERVICE_API_KEY=your-posthog-telemetry-api-key
|
||||
ERROR_SERVICE_API_KEY=your-posthog-error-tracking-api-key
|
||||
|
||||
# ============================================================================
|
||||
# TELEMETRY PROVIDER CONTROL
|
||||
# ============================================================================
|
||||
# Control which telemetry providers are active
|
||||
POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: true)
|
||||
# Set to false to disable Telemetry completely
|
||||
|
||||
# ============================================================================
|
||||
# OPENTELEMETRY (Optional - for advanced telemetry)
|
||||
# ============================================================================
|
||||
|
||||
@@ -44,6 +44,12 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Print HEAD commit
|
||||
run: |
|
||||
echo "HEAD is at: $(git rev-parse HEAD)"
|
||||
echo "Short: $(git rev-parse --short HEAD)"
|
||||
git log -1 --format="Commit: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s"
|
||||
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
run: |
|
||||
@@ -229,7 +235,13 @@ jobs:
|
||||
|
||||
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.
|
||||
Start by noting the commit hash you reviewed:
|
||||
```bash
|
||||
git rev-parse --short HEAD
|
||||
```
|
||||
Include this at the top of your comment: "Reviewed at commit: <short hash>"
|
||||
|
||||
Then thank them 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.
|
||||
|
||||
@@ -48,6 +48,12 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Print HEAD commit
|
||||
run: |
|
||||
echo "HEAD is at: $(git rev-parse HEAD)"
|
||||
echo "Short: $(git rev-parse --short HEAD)"
|
||||
git log -1 --format="Commit: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -269,7 +275,13 @@ jobs:
|
||||
|
||||
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.
|
||||
Start by noting the commit hash you reviewed:
|
||||
```bash
|
||||
git rev-parse --short HEAD
|
||||
```
|
||||
Include this at the top of your comment: "Reviewed at commit: <short hash>"
|
||||
|
||||
Then thank them 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.
|
||||
|
||||
@@ -80,13 +80,16 @@ jobs:
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Install vsce
|
||||
run: npm install -g @vscode/vsce
|
||||
|
||||
- name: Install xvfb on Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: sudo apt-get update && sudo apt-get install -y xvfb
|
||||
|
||||
@@ -10,8 +10,8 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -30,10 +30,10 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: "20.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# Cache root dependencies
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
@@ -41,21 +41,9 @@ jobs:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache cli dependencies
|
||||
- name: Cache cli dependencies
|
||||
uses: actions/cache@v4
|
||||
id: cli-cache
|
||||
with:
|
||||
path: cli/node_modules
|
||||
key: ${{ runner.os }}-npm-cli-${{ hashFiles('cli/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install cli dependencies
|
||||
if: steps.cli-cache.outputs.cache-hit != 'true'
|
||||
run: cd cli && npm ci
|
||||
- name: Install root dependencies and CLI dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
|
||||
|
||||
- name: Generate Protos
|
||||
run: npm run protos
|
||||
@@ -68,10 +56,7 @@ jobs:
|
||||
echo "Release version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Clean previous builds
|
||||
run: rm -rf dist-standalone
|
||||
|
||||
- name: Build and package CLI
|
||||
- name: Build standalone NPM package
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
@@ -82,7 +67,6 @@ jobs:
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: node scripts/package-npm.mjs
|
||||
|
||||
- name: Verify build output
|
||||
@@ -90,11 +74,11 @@ jobs:
|
||||
echo "Checking dist-standalone directory..."
|
||||
ls -la dist-standalone/
|
||||
|
||||
echo "Checking dist-standalone/dist..."
|
||||
ls -la dist-standalone/dist/
|
||||
echo "Verifying CLI binaries..."
|
||||
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
|
||||
|
||||
echo "Checking package.json in dist-standalone..."
|
||||
cat dist-standalone/package.json
|
||||
cat dist-standalone/package.json | grep version
|
||||
|
||||
- name: Publish to NPM with latest tag
|
||||
env:
|
||||
|
||||
@@ -2,13 +2,13 @@ name: Publish NPM Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
|
||||
- cron: "0 12 * * *" # 4 AM PST (UTC-8) = 12 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -39,10 +39,10 @@ jobs:
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: "20.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# Cache root dependencies
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/cache@v4
|
||||
@@ -51,22 +51,9 @@ jobs:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache cli dependencies
|
||||
- name: Cache cli dependencies
|
||||
- name: Install root dependencies and CLI dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/cache@v4
|
||||
id: cli-cache
|
||||
with:
|
||||
path: cli/node_modules
|
||||
key: ${{ runner.os }}-npm-cli-${{ hashFiles('cli/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true' && steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install cli dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true' && steps.cli-cache.outputs.cache-hit != 'true'
|
||||
run: cd cli && npm ci
|
||||
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
|
||||
|
||||
- name: Generate Protos
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
@@ -82,7 +69,7 @@ jobs:
|
||||
# Generate timestamp (Unix epoch seconds)
|
||||
TIMESTAMP=$(date +%s)
|
||||
|
||||
# Create unique nightly version: 2.0.0-nightly.1736365200
|
||||
# Create unique nightly version: 1.0.9-nightly.1736365200
|
||||
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
|
||||
|
||||
echo "Base version: $BASE_VERSION"
|
||||
@@ -103,10 +90,6 @@ jobs:
|
||||
echo "Using version ${{ steps.version.outputs.version }} for build"
|
||||
cat cli/package.json | grep '"version"'
|
||||
|
||||
- name: Clean previous builds
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: rm -rf dist-standalone
|
||||
|
||||
- name: Build and package CLI
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
@@ -119,7 +102,6 @@ jobs:
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: node scripts/package-npm.mjs
|
||||
|
||||
- name: Verify build output
|
||||
@@ -128,11 +110,11 @@ jobs:
|
||||
echo "Checking dist-standalone directory..."
|
||||
ls -la dist-standalone/
|
||||
|
||||
echo "Checking dist-standalone/dist..."
|
||||
ls -la dist-standalone/dist/
|
||||
echo "Verifying CLI binaries..."
|
||||
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
|
||||
|
||||
echo "Checking package.json in dist-standalone..."
|
||||
cat dist-standalone/package.json
|
||||
cat dist-standalone/package.json | grep version
|
||||
|
||||
- name: Publish to NPM with nightly tag
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
|
||||
@@ -44,11 +44,11 @@ jobs:
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Run Quality Checks (Parallel)
|
||||
@@ -89,11 +89,11 @@ jobs:
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Set up NPM on Windows
|
||||
@@ -180,11 +180,11 @@ jobs:
|
||||
key: ${{ runner.os }}-npm-testing-platform-${{ hashFiles('testing-platform/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
@@ -194,17 +194,11 @@ jobs:
|
||||
run: npm run compile-standalone
|
||||
|
||||
- name: Install testing platform dependencies
|
||||
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
|
||||
run: cd testing-platform && npm ci
|
||||
|
||||
- name: Running testing platform integration spec tests
|
||||
continue-on-error: true
|
||||
timeout-minutes: 7
|
||||
# Temporarily wrapping the test command to always return a neutral exit code.
|
||||
# This prevents the job from showing as failed and avoids distracting developers
|
||||
# until the integration tests are ready to be enforced.
|
||||
run: |
|
||||
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
|
||||
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -38,6 +38,7 @@ coverage-unit
|
||||
src/generated/
|
||||
src/shared/proto/
|
||||
webview-ui/src/services/grpc-client.ts
|
||||
*.tsbuildinfo
|
||||
|
||||
# E2E Tests
|
||||
test-results
|
||||
|
||||
+51
-2
@@ -1,14 +1,63 @@
|
||||
# Changelog
|
||||
|
||||
## [3.56.0]
|
||||
|
||||
### Added
|
||||
|
||||
- __CLI authentication:__ Added Vercel AI Gateway and Cline API key provider support for headless CI/automation workflows
|
||||
- __New model:__ Added Kimi-K2.5 model to Moonshot provider (262K context, image support, prompt caching)
|
||||
- __Prompt variant:__ Added Trinity Large prompt variant for improved tool-calling support
|
||||
- __OpenTelemetry:__ Added support for custom headers on metrics and logs endpoints
|
||||
- __Social links:__ Added community icons (X, Discord, GitHub, Reddit, LinkedIn) to the What's New modal
|
||||
|
||||
### Fixed
|
||||
|
||||
- __LiteLLM:__ Fixed thinking configuration not appearing for reasoning-capable models
|
||||
- __OpenTelemetry:__ Fixed endpoint path handling (no longer incorrectly appends `/v1/logs` or `/v1/metrics`) and ensured logs are sent regardless of VSCode telemetry settings
|
||||
- __CLI auth:__ Fixed `cline auth` displaying incorrect provider information after configuration
|
||||
|
||||
### Changed
|
||||
|
||||
- __Hooks:__ Hook scripts now run from the workspace repository root instead of filesystem root
|
||||
- __Default settings:__ Enabled multi-root workspaces, parallel tool calling, and skills by default; disabled strict plan mode by default
|
||||
- __Settings UI:__ Refreshed feature settings section with collapsible design
|
||||
|
||||
## [3.55.0]
|
||||
|
||||
- Add new model: Arcee Trinity Large Preview
|
||||
- Add new model: Moonshot Kimi K2.5
|
||||
- Add MCP prompts support - prompts from connected MCP servers now appear in slash command autocomplete as `/mcp:<server>:<prompt>`
|
||||
|
||||
## [3.54.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Native tool calls support for Ollama provider
|
||||
- Sonnet 4.5 is now the default Amazon Bedrock model id
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent infinite retry loops when replace_in_file fails repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
|
||||
- Skip diff error UI handling during streaming to prevent flickering. Error handling is deferred until streaming completes.
|
||||
- Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing context size sent to the LLM.
|
||||
- Throttle diff view updates during streaming to reduce UI flickering and improve performance.
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed Mistral's Devstral-2512 free from the free models list
|
||||
- Removed deprecated zai-glm-4.6 model from Cerebras provider
|
||||
|
||||
## [3.53.1]
|
||||
|
||||
### Fixed
|
||||
- Bug in responses API
|
||||
|
||||
- Bug in responses API
|
||||
|
||||
## [3.53.0]
|
||||
|
||||
### Fixed
|
||||
- Removed grok model from free tier
|
||||
|
||||
- Removed grok model from free tier
|
||||
|
||||
## [3.52.0]
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.317 4.15557C18.7873 3.45369 17.147 2.93658 15.4319 2.6404C15.4007 2.63469 15.3695 2.64897 15.3534 2.67754C15.1424 3.05276 14.9087 3.54225 14.7451 3.927C12.9004 3.65083 11.0652 3.65083 9.25832 3.927C9.09465 3.5337 8.85248 3.05276 8.64057 2.67754C8.62449 2.64992 8.59328 2.63564 8.56205 2.6404C6.84791 2.93563 5.20756 3.45275 3.67693 4.15557C3.66368 4.16129 3.65233 4.17082 3.64479 4.18319C0.533392 8.83155 -0.31895 13.3657 0.0991801 17.8436C0.101072 17.8655 0.11337 17.8864 0.130398 17.8997C2.18321 19.4073 4.17171 20.3225 6.12328 20.9291C6.15451 20.9386 6.18761 20.9272 6.20748 20.9015C6.66913 20.2711 7.08064 19.6063 7.43348 18.9073C7.4543 18.8664 7.43442 18.8178 7.39186 18.8016C6.73913 18.554 6.1176 18.2521 5.51973 17.9093C5.47244 17.8816 5.46865 17.814 5.51216 17.7816C5.63797 17.6873 5.76382 17.5893 5.88396 17.4902C5.90569 17.4721 5.93598 17.4683 5.96153 17.4797C9.88928 19.273 14.1415 19.273 18.023 17.4797C18.0485 17.4674 18.0788 17.4712 18.1015 17.4893C18.2216 17.5883 18.3475 17.6873 18.4742 17.7816C18.5177 17.814 18.5149 17.8816 18.4676 17.9093C17.8697 18.2588 17.2482 18.554 16.5945 18.8006C16.552 18.8168 16.533 18.8664 16.5538 18.9073C16.9143 19.6054 17.3258 20.2701 17.7789 20.9005C17.7978 20.9272 17.8319 20.9386 17.8631 20.9291C19.8241 20.3225 21.8126 19.4073 23.8654 17.8997C23.8834 17.8864 23.8948 17.8664 23.8967 17.8445C24.3971 12.6676 23.0585 8.17064 20.3482 4.18414C20.3416 4.17082 20.3303 4.16129 20.317 4.15557ZM8.02002 15.117C6.8375 15.117 5.86313 14.0313 5.86313 12.6981C5.86313 11.3648 6.8186 10.2791 8.02002 10.2791C9.23087 10.2791 10.1958 11.3743 10.1769 12.6981C10.1769 14.0313 9.22141 15.117 8.02002 15.117ZM15.9947 15.117C14.8123 15.117 13.8379 14.0313 13.8379 12.6981C13.8379 11.3648 14.7933 10.2791 15.9947 10.2791C17.2056 10.2791 18.1705 11.3743 18.1516 12.6981C18.1516 14.0313 17.2056 15.117 15.9947 15.117Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="black" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2C6.477 2 2 6.477 2 12C2 16.418 4.865 20.166 8.84 21.49C9.34 21.58 9.52 21.27 9.52 21C9.52 20.77 9.51 20.14 9.51 19.31C6.73 19.91 6.14 17.97 6.14 17.97C5.68 16.81 5.03 16.5 5.03 16.5C4.12 15.88 5.1 15.9 5.1 15.9C6.1 15.97 6.63 16.93 6.63 16.93C7.5 18.45 8.97 18 9.54 17.76C9.63 17.11 9.89 16.67 10.17 16.42C7.95 16.17 5.62 15.31 5.62 11.5C5.62 10.39 6 9.5 6.65 8.79C6.55 8.54 6.2 7.5 6.75 6.15C6.75 6.15 7.59 5.88 9.5 7.17C10.29 6.95 11.15 6.84 12 6.84C12.85 6.84 13.71 6.95 14.5 7.17C16.41 5.88 17.25 6.15 17.25 6.15C17.8 7.5 17.45 8.54 17.35 8.79C18 9.5 18.38 10.39 18.38 11.5C18.38 15.32 16.04 16.16 13.81 16.41C14.17 16.72 14.5 17.33 14.5 18.26C14.5 19.6 14.49 20.68 14.49 21C14.49 21.27 14.67 21.59 15.17 21.49C19.14 20.16 22 16.42 22 12C22 6.477 17.523 2 12 2Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 902 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2001_1428)">
|
||||
<path d="M22.2234 0H1.77187C0.792187 0 0 0.773438 0 1.72969V22.2656C0 23.2219 0.792187 24 1.77187 24H22.2234C23.2031 24 24 23.2219 24 22.2703V1.72969C24 0.773438 23.2031 0 22.2234 0ZM7.12031 20.4516H3.55781V8.99531H7.12031V20.4516ZM5.33906 7.43438C4.19531 7.43438 3.27188 6.51094 3.27188 5.37187C3.27188 4.23281 4.19531 3.30937 5.33906 3.30937C6.47813 3.30937 7.40156 4.23281 7.40156 5.37187C7.40156 6.50625 6.47813 7.43438 5.33906 7.43438ZM20.4516 20.4516H16.8937V14.8828C16.8937 13.5562 16.8703 11.8453 15.0422 11.8453C13.1906 11.8453 12.9094 13.2937 12.9094 14.7891V20.4516H9.35625V8.99531H12.7687V10.5609H12.8156C13.2891 9.66094 14.4516 8.70938 16.1813 8.70938C19.7859 8.70938 20.4516 11.0813 20.4516 14.1656V20.4516Z" fill="#FAFAFA"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2001_1428">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 989 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.0512 4.07466C15.3113 5.17727 16.301 5.99866 17.4829 5.99866C18.8627 5.99866 19.9813 4.87965 19.9813 3.49933C19.9813 2.11902 18.8627 1 17.4829 1C16.2764 1 15.2703 1.85537 15.036 2.99314C13.0155 3.20991 11.4378 4.92417 11.4378 7.00167C11.4378 7.00636 11.4378 7.00988 11.4378 7.01456C9.24041 7.10713 7.23397 7.73284 5.641 8.72062C5.04949 8.26247 4.30688 7.98945 3.50102 7.98945C1.5672 7.98945 0 9.55725 0 11.4918C0 12.8955 0.824597 14.1048 2.01581 14.6637C2.13177 18.7297 6.56047 22 12.0082 22C17.4559 22 21.8905 18.7261 22.0006 14.6567C23.1824 14.0942 24 12.8885 24 11.493C24 9.55842 22.4328 7.99063 20.499 7.99063C19.6966 7.99063 18.9575 8.2613 18.3672 8.71594C16.7602 7.72113 14.7315 7.09541 12.5119 7.01222C12.5119 7.0087 12.5119 7.00636 12.5119 7.00285C12.5119 5.51473 13.6176 4.27971 15.0512 4.077V4.07466ZM5.50044 13.7146C5.559 12.4444 6.40234 11.4695 7.38272 11.4695C8.3631 11.4695 9.11274 12.4995 9.05417 13.7697C8.99561 15.0398 8.26354 15.5015 7.28199 15.5015C6.30044 15.5015 5.44187 14.9848 5.50044 13.7146ZM16.6348 11.4695C17.6164 11.4695 18.4597 12.4444 18.5171 13.7146C18.5757 14.9848 17.716 15.5015 16.7356 15.5015C15.7552 15.5015 15.022 15.041 14.9634 13.7697C14.9048 12.4995 15.6533 11.4695 16.6348 11.4695ZM15.4682 16.6533C15.6521 16.6721 15.7693 16.8631 15.6978 17.0341C15.0946 18.4766 13.6703 19.4901 12.0082 19.4901C10.3461 19.4901 8.92299 18.4766 8.31859 17.0341C8.24714 16.8631 8.36427 16.6721 8.54817 16.6533C9.62577 16.5444 10.7912 16.4846 12.0082 16.4846C13.2252 16.4846 14.3895 16.5444 15.4682 16.6533Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.3263 1.90393H21.6998L14.3297 10.3274L23 21.7899H16.2112L10.894 14.838L4.80995 21.7899H1.43443L9.31743 12.78L1 1.90393H7.96111L12.7674 8.25826L18.3263 1.90393ZM17.1423 19.7707H19.0116L6.94539 3.81706H4.93946L17.1423 19.7707Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 358 B |
@@ -159,6 +159,7 @@
|
||||
"!**/webview-ui/**",
|
||||
"!**/evals/**",
|
||||
"!**/standalone/**",
|
||||
"!**/cli/**",
|
||||
"!**/e2e/**",
|
||||
"!**/test/**",
|
||||
"!**/__tests__/**",
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
# Cline CLI
|
||||
|
||||
The official CLI for Cline. Run Cline tasks directly from the terminal with the same underlying functionality as the VS Code extension.
|
||||
|
||||
## Features
|
||||
|
||||
- **Reuses Core Codebase**: Shares the same Controller, Task, and API handling as the VS Code extension
|
||||
- **Terminal Output**: Displays Cline messages directly in your terminal with colored output
|
||||
- **Task History**: Access your task history from the command line
|
||||
- **Configurable**: Use custom configuration directories and working directories
|
||||
- **Image Support**: Attach images to your prompts using file paths or inline references
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 20.x or later
|
||||
- npm or yarn
|
||||
- The parent Cline project dependencies installed
|
||||
|
||||
## Installation
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
# Install all dependencies first
|
||||
npm run install:all
|
||||
|
||||
# Ensure protos are generated
|
||||
npm run protos
|
||||
|
||||
# Build and link the CLI globally
|
||||
npm run cli:link
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Interactive Mode (Default)
|
||||
|
||||
When you run `cline` without any command, it launches an interactive welcome prompt:
|
||||
|
||||
```bash
|
||||
# Launch interactive mode
|
||||
cline
|
||||
|
||||
# Or run a task directly
|
||||
cline "Create a hello world function in Python"
|
||||
|
||||
# With options
|
||||
cline -v --thinking "Analyze this codebase"
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
#### `task` (alias: `t`)
|
||||
|
||||
Run a new task with a prompt.
|
||||
|
||||
```bash
|
||||
cline task "Create a hello world function in Python"
|
||||
cline t "Create a hello world function"
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-a, --act` | Run in act mode |
|
||||
| `-p, --plan` | Run in plan mode |
|
||||
| `-y, --yolo` | Enable yolo mode (auto-approve actions) |
|
||||
| `-m, --model <model>` | Model to use for the task |
|
||||
| `-i, --images <paths...>` | Image file paths to include with the task |
|
||||
| `-v, --verbose` | Show verbose output including reasoning |
|
||||
| `-c, --cwd <path>` | Working directory for the task |
|
||||
| `--config <path>` | Path to Cline configuration directory |
|
||||
| `-t, --thinking` | Enable extended thinking (1024 token budget) |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Run in plan mode with verbose output
|
||||
cline task -p -v "Design a REST API"
|
||||
|
||||
# Use a specific model with yolo mode
|
||||
cline task -m claude-sonnet-4-5-20250929 -y "Refactor this function"
|
||||
|
||||
# Include images with your prompt
|
||||
cline task -i screenshot.png diagram.jpg "Fix the UI based on these images"
|
||||
|
||||
# Or use inline image references in the prompt
|
||||
cline task "Fix the layout shown in @./screenshot.png"
|
||||
|
||||
# Enable extended thinking for complex tasks
|
||||
cline task -t "Architect a microservices system"
|
||||
|
||||
# Specify working directory
|
||||
cline task -c /path/to/project "Add unit tests"
|
||||
```
|
||||
|
||||
#### `history` (alias: `h`)
|
||||
|
||||
List task history with pagination support.
|
||||
|
||||
```bash
|
||||
cline history
|
||||
cline h
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-n, --limit <number>` | Number of tasks to show (default: 10) |
|
||||
| `-p, --page <number>` | Page number, 1-based (default: 1) |
|
||||
| `--config <path>` | Path to Cline configuration directory |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Show last 10 tasks (default)
|
||||
cline history
|
||||
|
||||
# Show 20 tasks
|
||||
cline history -n 20
|
||||
|
||||
# Show page 2 with 5 tasks per page
|
||||
cline history -n 5 -p 2
|
||||
```
|
||||
|
||||
#### `config`
|
||||
|
||||
Show current configuration including global and workspace state.
|
||||
|
||||
```bash
|
||||
cline config
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--config <path>` | Path to Cline configuration directory |
|
||||
|
||||
#### `auth`
|
||||
|
||||
Authenticate a provider and configure what model is used.
|
||||
|
||||
```bash
|
||||
cline auth
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-p, --provider <id>` | Provider ID for quick setup (e.g., openai-native, anthropic) |
|
||||
| `-k, --apikey <key>` | API key for the provider |
|
||||
| `-m, --modelid <id>` | Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929) |
|
||||
| `-b, --baseurl <url>` | Base URL (optional, only for openai provider) |
|
||||
| `-v, --verbose` | Show verbose output |
|
||||
| `-c, --cwd <path>` | Working directory for the task |
|
||||
| `--config <path>` | Path to Cline configuration directory |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Interactive authentication
|
||||
cline auth
|
||||
|
||||
# Quick setup with provider and API key
|
||||
cline auth -p anthropic -k sk-ant-xxxxx
|
||||
|
||||
# Full quick setup with model
|
||||
cline auth -p openai-native -k sk-xxxxx -m gpt-4o
|
||||
|
||||
# OpenAI-compatible provider with custom base URL
|
||||
cline auth -p openai -k your-api-key -b https://api.example.com/v1
|
||||
```
|
||||
|
||||
### Global Options
|
||||
|
||||
These options are available for the default command (running a task directly):
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-v, --verbose` | Show verbose output |
|
||||
| `-c, --cwd <path>` | Working directory |
|
||||
| `--config <path>` | Configuration directory |
|
||||
| `--thinking` | Enable extended thinking (1024 token budget) |
|
||||
|
||||
## Development
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Install all dependencies (root, webview-ui, cli)
|
||||
npm run install:all
|
||||
|
||||
# 2. Build and link globally so you can run `cline` from anywhere
|
||||
npm run cli:link
|
||||
|
||||
# 3. Test it
|
||||
cline --help
|
||||
```
|
||||
|
||||
### Scripts
|
||||
|
||||
Run these from the repository root:
|
||||
|
||||
| Script | Description |
|
||||
|--------|-------------|
|
||||
| `npm run install:all` | Install deps for root, webview-ui, and cli |
|
||||
| `npm run cli:build` | Generate protos and build CLI |
|
||||
| `npm run cli:build:production` | Production build (minified) |
|
||||
| `npm run cli:link` | Build and `npm link` so you can run `cline` from anywhere |
|
||||
| `npm run cli:unlink` | Remove the global `cline` symlink |
|
||||
| `npm run cli:dev` | Link + watch mode for development |
|
||||
| `npm run cli:watch` | Watch mode only (no initial build) |
|
||||
| `npm run cli:test` | Run CLI tests |
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. Run `npm run cli:dev` - this links the CLI globally and starts watch mode
|
||||
2. Make changes to files in `cli/src/`
|
||||
3. The build automatically rebuilds on save
|
||||
4. Test your changes by running `cline` in another terminal
|
||||
5. When done, run `npm run cli:unlink` to clean up
|
||||
|
||||
### Proto Generation
|
||||
|
||||
The CLI uses proto-generated types for message passing (same as the VS Code extension). If you modify any `.proto` files, run:
|
||||
|
||||
```bash
|
||||
npm run protos
|
||||
```
|
||||
|
||||
This generates TypeScript types in `src/generated/` that both the CLI and extension use.
|
||||
|
||||
## Publish
|
||||
|
||||
#### 1. Publish to npm
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
#### 2. Update the Homebrew formula
|
||||
```bash
|
||||
npm run update-brew-formula
|
||||
```
|
||||
|
||||
#### 3. Test the formula locally
|
||||
```bash
|
||||
# Create a local tap
|
||||
brew tap-new cline/local
|
||||
cp ./cli/cline.rb "$(brew --repository)/Library/Taps/cline/homebrew-local/Formula/cline.rb"
|
||||
|
||||
# Build from Source
|
||||
brew install --build-from-source cline/local/cline
|
||||
|
||||
# Install from your local tap
|
||||
brew install cline/local/cline
|
||||
|
||||
# Clean up when done
|
||||
brew untap cline/local
|
||||
```
|
||||
|
||||
#### 4. If using a tap, commit and push
|
||||
```bash
|
||||
git add cline.rb
|
||||
git commit -m "Update cline to v2.0.0"
|
||||
git push
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### How It Works
|
||||
|
||||
The CLI directly imports and reuses the core Cline TypeScript codebase (the same code that powers the VS Code extension). This means feature parity is easy to maintain - when core gets updated, the CLI automatically benefits.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ CLI (cli/) │
|
||||
│ - React Ink terminal UI │
|
||||
│ - Command parsing (commander) │
|
||||
│ - Terminal-specific adapters │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ direct imports
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Core (src/core/) │
|
||||
│ - Controller: task lifecycle, state management │
|
||||
│ - Task: AI API calls, tool execution │
|
||||
│ - StateManager: persistent storage │
|
||||
│ - Proto types: message definitions │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Unlike a client-server architecture, the CLI runs everything in a single Node.js process. The "host bridge" pattern provides terminal-appropriate implementations for things the VS Code extension would handle differently (clipboard, file dialogs, etc.).
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/index.ts` | Entry point, command definitions |
|
||||
| `src/components/App.tsx` | Main React Ink app |
|
||||
| `src/components/ChatView.tsx` | Task conversation UI |
|
||||
| `src/controllers/CliWebviewProvider.ts` | Bridges core messages to terminal output |
|
||||
| `src/vscode-context.ts` | Mock VS Code extension context for core compatibility |
|
||||
| `src/vscode-shim.ts` | Shims for VS Code APIs that core depends on |
|
||||
| `src/constants/colors.ts` | Terminal color definitions |
|
||||
|
||||
### React Ink
|
||||
|
||||
The CLI uses [React Ink](https://github.com/vadimdemedes/ink) for its terminal UI. This lets us build the interface with React components that render to the terminal. Key patterns:
|
||||
|
||||
- Components in `src/components/` render terminal UI
|
||||
- Hooks in `src/hooks/` manage terminal-specific state (size, scrolling)
|
||||
- The `useStateSubscriber` hook subscribes to core state changes
|
||||
|
||||
## Configuration
|
||||
|
||||
The CLI stores its data in `~/.cline/data/` by default:
|
||||
|
||||
- `globalState.json`: Global settings and state
|
||||
- `secrets.json`: API keys and secrets
|
||||
- `workspace/`: Workspace-specific state
|
||||
- `tasks/`: Task history and conversation data
|
||||
|
||||
Override with the `--config` option or `CLINE_DIR` environment variable.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Errors
|
||||
|
||||
If you encounter build errors:
|
||||
|
||||
```bash
|
||||
# Make sure all deps are installed
|
||||
npm run install:all
|
||||
|
||||
# Regenerate proto types
|
||||
npm run protos
|
||||
|
||||
# Then rebuild
|
||||
npm run cli:build
|
||||
```
|
||||
|
||||
### "command not found: cline"
|
||||
|
||||
The CLI isn't linked globally. Run:
|
||||
|
||||
```bash
|
||||
npm run cli:link
|
||||
```
|
||||
|
||||
### Changes Not Reflected
|
||||
|
||||
If your code changes aren't showing up:
|
||||
|
||||
1. Make sure watch mode is running (`npm run cli:dev`)
|
||||
2. Check for TypeScript errors in the watch output
|
||||
3. Try unlinking and relinking: `npm run cli:unlink && npm run cli:link`
|
||||
|
||||
### Import Errors from Core
|
||||
|
||||
The CLI imports from `@core/`, `@shared/`, etc. These paths are defined in the root `tsconfig.json`. If you see import errors, make sure you're building from the repo root, not from inside `cli/`.
|
||||
+50
-333
@@ -1,365 +1,82 @@
|
||||
# Cline CLI
|
||||
# Cline
|
||||
|
||||
The official CLI for Cline. Run Cline tasks directly from the terminal with the same underlying functionality as the VS Code extension.
|
||||
<p align="center">
|
||||
<img src="https://github.com/user-attachments/assets/7123f9d1-afeb-48d5-93fa-e750dec0ebba" width="70%" />
|
||||
</p>
|
||||
|
||||
## Features
|
||||
<div align="center">
|
||||
<table>
|
||||
<tbody>
|
||||
<td align="center">
|
||||
<a href="https://www.npmjs.com/package/cline" target="_blank"><strong>NPM</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>Getting Started</strong></a>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
- **Reuses Core Codebase**: Shares the same Controller, Task, and API handling as the VS Code extension
|
||||
- **Terminal Output**: Displays Cline messages directly in your terminal with colored output
|
||||
- **Task History**: Access your task history from the command line
|
||||
- **Configurable**: Use custom configuration directories and working directories
|
||||
- **Image Support**: Attach images to your prompts using file paths or inline references
|
||||
Meet Cline, an AI assistant that lives in your terminal.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 20.x or later
|
||||
- npm or yarn
|
||||
- The parent Cline project dependencies installed
|
||||
|
||||
## Installation
|
||||
|
||||
From the repository root:
|
||||
Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support.
|
||||
|
||||
```bash
|
||||
# Install all dependencies first
|
||||
npm run install:all
|
||||
npm i -g cline
|
||||
|
||||
# Ensure protos are generated
|
||||
npm run protos
|
||||
|
||||
# Build and link the CLI globally
|
||||
npm run cli:link
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Interactive Mode (Default)
|
||||
|
||||
When you run `cline` without any command, it launches an interactive welcome prompt:
|
||||
|
||||
```bash
|
||||
# Launch interactive mode
|
||||
# cd into your project and run:
|
||||
cline
|
||||
|
||||
# Or run a task directly
|
||||
cline "Create a hello world function in Python"
|
||||
|
||||
# With options
|
||||
cline -v --thinking "Analyze this codebase"
|
||||
```
|
||||
|
||||
### Commands
|
||||
> Move your mouse around under the Cline icon for a surprise!
|
||||
|
||||
#### `task` (alias: `t`)
|
||||
---
|
||||
|
||||
Run a new task with a prompt.
|
||||
<img align="right" width="340" src="https://github.com/user-attachments/assets/ceb74224-08aa-4b8b-a3e7-b438ac3d160a">
|
||||
|
||||
```bash
|
||||
cline task "Create a hello world function in Python"
|
||||
cline t "Create a hello world function"
|
||||
```
|
||||
### Use any API and Model
|
||||
|
||||
**Options:**
|
||||
Cline supports API providers like ChatGPT, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using a Cline Account, you'll always have access to the newest models as soon as they're available.
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-a, --act` | Run in act mode |
|
||||
| `-p, --plan` | Run in plan mode |
|
||||
| `-y, --yolo` | Enable yolo mode (auto-approve actions) |
|
||||
| `-m, --model <model>` | Model to use for the task |
|
||||
| `-i, --images <paths...>` | Image file paths to include with the task |
|
||||
| `-v, --verbose` | Show verbose output including reasoning |
|
||||
| `-c, --cwd <path>` | Working directory for the task |
|
||||
| `--config <path>` | Path to Cline configuration directory |
|
||||
| `-t, --thinking` | Enable extended thinking (1024 token budget) |
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
**Examples:**
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
```bash
|
||||
# Run in plan mode with verbose output
|
||||
cline task -p -v "Design a REST API"
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/cad091f6-6c0f-4e4b-97ea-a1ff67e39b9b">
|
||||
|
||||
# Use a specific model with yolo mode
|
||||
cline task -m claude-sonnet-4-5-20250929 -y "Refactor this function"
|
||||
### Stay in Control with Human-in-the-Loop
|
||||
|
||||
# Include images with your prompt
|
||||
cline task -i screenshot.png diagram.jpg "Fix the UI based on these images"
|
||||
Cline asks for your approval before running commands, editing files, or taking any action. Review each step and approve or reject as you go—or enable auto-approve to let Cline work autonomously to completion.
|
||||
|
||||
# Or use inline image references in the prompt
|
||||
cline task "Fix the layout shown in @./screenshot.png"
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
# Enable extended thinking for complex tasks
|
||||
cline task -t "Architect a microservices system"
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/cad091f6-6c0f-4e4b-97ea-a1ff67e39b9b"><br>
|
||||
|
||||
# Specify working directory
|
||||
cline task -c /path/to/project "Add unit tests"
|
||||
```
|
||||
<img align="right" width="400" src="https://github.com/user-attachments/assets/4f264a0c-3802-49a7-8e5e-13d97beb659e">
|
||||
|
||||
#### `history` (alias: `h`)
|
||||
### Plan & Act Modes
|
||||
|
||||
List task history with pagination support.
|
||||
Toggle to Plan Mode to discuss implementation and architecture with Cline. He'll ask clarifying questions, explore your codebase, and present a plan for you to align on. Once you're satisfied, switch to Act Mode and let Cline execute the plan.
|
||||
|
||||
```bash
|
||||
cline history
|
||||
cline h
|
||||
```
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
**Options:**
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/4f264a0c-3802-49a7-8e5e-13d97beb659e"><br>
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-n, --limit <number>` | Number of tasks to show (default: 10) |
|
||||
| `-p, --page <number>` | Page number, 1-based (default: 1) |
|
||||
| `--config <path>` | Path to Cline configuration directory |
|
||||
|
||||
**Examples:**
|
||||
## Enterprise
|
||||
|
||||
```bash
|
||||
# Show last 10 tasks (default)
|
||||
cline history
|
||||
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
|
||||
|
||||
# Show 20 tasks
|
||||
cline history -n 20
|
||||
## License
|
||||
|
||||
# Show page 2 with 5 tasks per page
|
||||
cline history -n 5 -p 2
|
||||
```
|
||||
[Apache 2.0 © 2026 Cline Bot Inc.](./LICENSE)
|
||||
|
||||
#### `config`
|
||||
|
||||
Show current configuration including global and workspace state.
|
||||
|
||||
```bash
|
||||
cline config
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--config <path>` | Path to Cline configuration directory |
|
||||
|
||||
#### `auth`
|
||||
|
||||
Authenticate a provider and configure what model is used.
|
||||
|
||||
```bash
|
||||
cline auth
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-p, --provider <id>` | Provider ID for quick setup (e.g., openai-native, anthropic) |
|
||||
| `-k, --apikey <key>` | API key for the provider |
|
||||
| `-m, --modelid <id>` | Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929) |
|
||||
| `-b, --baseurl <url>` | Base URL (optional, only for openai provider) |
|
||||
| `-v, --verbose` | Show verbose output |
|
||||
| `-c, --cwd <path>` | Working directory for the task |
|
||||
| `--config <path>` | Path to Cline configuration directory |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Interactive authentication
|
||||
cline auth
|
||||
|
||||
# Quick setup with provider and API key
|
||||
cline auth -p anthropic -k sk-ant-xxxxx
|
||||
|
||||
# Full quick setup with model
|
||||
cline auth -p openai-native -k sk-xxxxx -m gpt-4o
|
||||
|
||||
# OpenAI-compatible provider with custom base URL
|
||||
cline auth -p openai -k your-api-key -b https://api.example.com/v1
|
||||
```
|
||||
|
||||
### Global Options
|
||||
|
||||
These options are available for the default command (running a task directly):
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-v, --verbose` | Show verbose output |
|
||||
| `-c, --cwd <path>` | Working directory |
|
||||
| `--config <path>` | Configuration directory |
|
||||
| `--thinking` | Enable extended thinking (1024 token budget) |
|
||||
|
||||
## Development
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Install all dependencies (root, webview-ui, cli)
|
||||
npm run install:all
|
||||
|
||||
# 2. Build and link globally so you can run `cline` from anywhere
|
||||
npm run cli:link
|
||||
|
||||
# 3. Test it
|
||||
cline --help
|
||||
```
|
||||
|
||||
### Scripts
|
||||
|
||||
Run these from the repository root:
|
||||
|
||||
| Script | Description |
|
||||
|--------|-------------|
|
||||
| `npm run install:all` | Install deps for root, webview-ui, and cli |
|
||||
| `npm run cli:build` | Generate protos and build CLI |
|
||||
| `npm run cli:build:production` | Production build (minified) |
|
||||
| `npm run cli:link` | Build and `npm link` so you can run `cline` from anywhere |
|
||||
| `npm run cli:unlink` | Remove the global `cline` symlink |
|
||||
| `npm run cli:dev` | Link + watch mode for development |
|
||||
| `npm run cli:watch` | Watch mode only (no initial build) |
|
||||
| `npm run cli:test` | Run CLI tests |
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. Run `npm run cli:dev` - this links the CLI globally and starts watch mode
|
||||
2. Make changes to files in `cli/src/`
|
||||
3. The build automatically rebuilds on save
|
||||
4. Test your changes by running `cline` in another terminal
|
||||
5. When done, run `npm run cli:unlink` to clean up
|
||||
|
||||
### Proto Generation
|
||||
|
||||
The CLI uses proto-generated types for message passing (same as the VS Code extension). If you modify any `.proto` files, run:
|
||||
|
||||
```bash
|
||||
npm run protos
|
||||
```
|
||||
|
||||
This generates TypeScript types in `src/generated/` that both the CLI and extension use.
|
||||
|
||||
## Publish
|
||||
|
||||
#### 1. Publish to npm
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
#### 2. Update the Homebrew formula
|
||||
```bash
|
||||
npm run update-brew-formula
|
||||
```
|
||||
|
||||
#### 3. Test the formula locally
|
||||
```bash
|
||||
# Create a local tap
|
||||
brew tap-new cline/local
|
||||
cp ./cli/cline.rb "$(brew --repository)/Library/Taps/cline/homebrew-local/Formula/cline.rb"
|
||||
|
||||
# Build from Source
|
||||
brew install --build-from-source cline/local/cline
|
||||
|
||||
# Install from your local tap
|
||||
brew install cline/local/cline
|
||||
|
||||
# Clean up when done
|
||||
brew untap cline/local
|
||||
```
|
||||
|
||||
#### 4. If using a tap, commit and push
|
||||
```bash
|
||||
git add cline.rb
|
||||
git commit -m "Update cline to v2.0.0"
|
||||
git push
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### How It Works
|
||||
|
||||
The CLI directly imports and reuses the core Cline TypeScript codebase (the same code that powers the VS Code extension). This means feature parity is easy to maintain - when core gets updated, the CLI automatically benefits.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ CLI (cli/) │
|
||||
│ - React Ink terminal UI │
|
||||
│ - Command parsing (commander) │
|
||||
│ - Terminal-specific adapters │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ direct imports
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Core (src/core/) │
|
||||
│ - Controller: task lifecycle, state management │
|
||||
│ - Task: AI API calls, tool execution │
|
||||
│ - StateManager: persistent storage │
|
||||
│ - Proto types: message definitions │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Unlike a client-server architecture, the CLI runs everything in a single Node.js process. The "host bridge" pattern provides terminal-appropriate implementations for things the VS Code extension would handle differently (clipboard, file dialogs, etc.).
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/index.ts` | Entry point, command definitions |
|
||||
| `src/components/App.tsx` | Main React Ink app |
|
||||
| `src/components/ChatView.tsx` | Task conversation UI |
|
||||
| `src/controllers/CliWebviewProvider.ts` | Bridges core messages to terminal output |
|
||||
| `src/vscode-context.ts` | Mock VS Code extension context for core compatibility |
|
||||
| `src/vscode-shim.ts` | Shims for VS Code APIs that core depends on |
|
||||
| `src/constants/colors.ts` | Terminal color definitions |
|
||||
|
||||
### React Ink
|
||||
|
||||
The CLI uses [React Ink](https://github.com/vadimdemedes/ink) for its terminal UI. This lets us build the interface with React components that render to the terminal. Key patterns:
|
||||
|
||||
- Components in `src/components/` render terminal UI
|
||||
- Hooks in `src/hooks/` manage terminal-specific state (size, scrolling)
|
||||
- The `useStateSubscriber` hook subscribes to core state changes
|
||||
|
||||
## Configuration
|
||||
|
||||
The CLI stores its data in `~/.cline/data/` by default:
|
||||
|
||||
- `globalState.json`: Global settings and state
|
||||
- `secrets.json`: API keys and secrets
|
||||
- `workspace/`: Workspace-specific state
|
||||
- `tasks/`: Task history and conversation data
|
||||
|
||||
Override with the `--config` option or `CLINE_DIR` environment variable.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Errors
|
||||
|
||||
If you encounter build errors:
|
||||
|
||||
```bash
|
||||
# Make sure all deps are installed
|
||||
npm run install:all
|
||||
|
||||
# Regenerate proto types
|
||||
npm run protos
|
||||
|
||||
# Then rebuild
|
||||
npm run cli:build
|
||||
```
|
||||
|
||||
### "command not found: cline"
|
||||
|
||||
The CLI isn't linked globally. Run:
|
||||
|
||||
```bash
|
||||
npm run cli:link
|
||||
```
|
||||
|
||||
### Changes Not Reflected
|
||||
|
||||
If your code changes aren't showing up:
|
||||
|
||||
1. Make sure watch mode is running (`npm run cli:dev`)
|
||||
2. Check for TypeScript errors in the watch output
|
||||
3. Try unlinking and relinking: `npm run cli:unlink && npm run cli:link`
|
||||
|
||||
### Import Errors from Core
|
||||
|
||||
The CLI imports from `@core/`, `@shared/`, etc. These paths are defined in the root `tsconfig.json`. If you see import errors, make sure you're building from the repo root, not from inside `cli/`.
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "dist/cli.mjs",
|
||||
"bin": {
|
||||
@@ -12,11 +12,11 @@
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "npm run build:production",
|
||||
"package:brew": "node ./scripts/update-brew-formula.mts",
|
||||
"package:brew": "npx tsx ./scripts/update-brew-formula.mts",
|
||||
"package": "npm pack --pack-destination ./dist",
|
||||
"build": "node esbuild.mts",
|
||||
"build:production": "node esbuild.mts --production",
|
||||
"watch": "node esbuild.mts --watch",
|
||||
"build": "npx tsx esbuild.mts",
|
||||
"build:production": "npx tsx esbuild.mts --production",
|
||||
"watch": "npx tsx esbuild.mts --watch",
|
||||
"dev": "IS_DEV=true && npm run link && npm run watch ; npm run unlink",
|
||||
"clean": "rimraf dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
||||
+14
-10
@@ -333,14 +333,17 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
// Get current provider and model for the mode
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const modelKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
|
||||
|
||||
const currentProvider = stateManager.getGlobalSettingsKey(providerKey) as ApiProvider | undefined
|
||||
const currentModelId = stateManager.getGlobalSettingsKey(modelKey) as string | undefined
|
||||
|
||||
// Use provider-specific model ID key (e.g., cline uses actModeOpenRouterModelId)
|
||||
const modelKey = currentProvider ? getProviderModelIdKey(currentProvider, mode) : null
|
||||
const currentModelId = modelKey
|
||||
? (stateManager.getGlobalSettingsKey(modelKey as string) as string | undefined)
|
||||
: undefined
|
||||
|
||||
// Build the current model ID in provider/model format
|
||||
const currentFullModelId =
|
||||
currentProvider && currentModelId ? `${currentProvider}/${currentModelId}` : currentProvider || "anthropic"
|
||||
currentProvider && currentModelId ? `${currentProvider}/${currentModelId}` : currentProvider || ""
|
||||
|
||||
// Get available models based on provider
|
||||
let modelIds: string[] = []
|
||||
@@ -398,13 +401,11 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
const stateManager = StateManager.get()
|
||||
|
||||
// Update model for both plan and act modes (use the same model for both)
|
||||
// Update provider for both modes
|
||||
stateManager.setGlobalState("actModeApiProvider", provider)
|
||||
stateManager.setGlobalState("actModeApiModelId", modelId)
|
||||
stateManager.setGlobalState("planModeApiProvider", provider)
|
||||
stateManager.setGlobalState("planModeApiModelId", modelId)
|
||||
|
||||
// Also update the provider-specific model ID keys for both modes
|
||||
// Update model ID using provider-specific keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
const actProviderModelKey = getProviderModelIdKey(provider, "act")
|
||||
if (actProviderModelKey) {
|
||||
stateManager.setGlobalState(actProviderModelKey, modelId)
|
||||
@@ -1219,8 +1220,11 @@ export class ClineAgent implements acp.Agent {
|
||||
const stateManager = StateManager.get()
|
||||
stateManager.setGlobalState("actModeApiProvider", "openai-codex")
|
||||
stateManager.setGlobalState("planModeApiProvider", "openai-codex")
|
||||
stateManager.setGlobalState("actModeApiModelId", openAiCodexDefaultModelId)
|
||||
stateManager.setGlobalState("planModeApiModelId", openAiCodexDefaultModelId)
|
||||
// Use provider-specific model ID keys for consistency
|
||||
const actModelKey = getProviderModelIdKey("openai-codex", "act")
|
||||
const planModelKey = getProviderModelIdKey("openai-codex", "plan")
|
||||
if (actModelKey) stateManager.setGlobalState(actModelKey, openAiCodexDefaultModelId)
|
||||
if (planModelKey) stateManager.setGlobalState(planModelKey, openAiCodexDefaultModelId)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
return {}
|
||||
|
||||
@@ -82,9 +82,8 @@ describe("App", () => {
|
||||
})
|
||||
|
||||
it("should render AuthView when view is auth", () => {
|
||||
const { lastFrame } = render(<App authQuickSetup={{ provider: "openai" }} controller={mockController} view="auth" />)
|
||||
const { lastFrame } = render(<App controller={mockController} view="auth" />)
|
||||
expect(lastFrame()).toContain("AuthView")
|
||||
expect(lastFrame()).toContain("openai")
|
||||
})
|
||||
|
||||
it("should render ChatView when view is welcome", () => {
|
||||
|
||||
@@ -81,13 +81,6 @@ interface AppProps {
|
||||
globalSkills?: SkillInfo[]
|
||||
localSkills?: SkillInfo[]
|
||||
onToggleSkill?: (isGlobal: boolean, skillPath: string, enabled: boolean) => void
|
||||
// For auth view
|
||||
authQuickSetup?: {
|
||||
provider?: string
|
||||
apikey?: string
|
||||
modelid?: string
|
||||
baseurl?: string
|
||||
}
|
||||
// For welcome view
|
||||
onWelcomeSubmit?: (prompt: string, imagePaths: string[]) => void
|
||||
onWelcomeExit?: () => void
|
||||
@@ -95,8 +88,6 @@ interface AppProps {
|
||||
initialImages?: string[]
|
||||
// Stdin support
|
||||
isRawModeSupported?: boolean
|
||||
// Robot position (calculated before Ink mounts)
|
||||
robotTopRow?: number
|
||||
}
|
||||
|
||||
export const App: React.FC<AppProps> = ({
|
||||
@@ -135,13 +126,11 @@ export const App: React.FC<AppProps> = ({
|
||||
globalSkills,
|
||||
localSkills,
|
||||
onToggleSkill,
|
||||
authQuickSetup,
|
||||
onWelcomeSubmit,
|
||||
onWelcomeExit,
|
||||
initialPrompt,
|
||||
initialImages,
|
||||
isRawModeSupported = true,
|
||||
robotTopRow,
|
||||
}) => {
|
||||
const { resizeKey } = useTerminalSize()
|
||||
const [currentView, setCurrentView] = useState<ViewType>(initialView)
|
||||
@@ -240,7 +229,6 @@ export const App: React.FC<AppProps> = ({
|
||||
onComplete={onComplete}
|
||||
onError={onError}
|
||||
onNavigateToWelcome={handleNavigateToWelcome}
|
||||
quickSetup={authQuickSetup}
|
||||
/>
|
||||
)
|
||||
break
|
||||
@@ -259,7 +247,6 @@ export const App: React.FC<AppProps> = ({
|
||||
onComplete={onComplete}
|
||||
onError={onError}
|
||||
onExit={onWelcomeExit}
|
||||
robotTopRow={robotTopRow}
|
||||
taskId={selectedTaskId}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -34,7 +34,6 @@ type AsciiMotionCliProps = {
|
||||
autoPlay?: boolean;
|
||||
loop?: boolean;
|
||||
onReady?: (api: PlaybackAPI) => void;
|
||||
robotTopRow?: number; // Screen row where robot is rendered (calculated before Ink mounts)
|
||||
onScroll?: () => void; // Called when user scrolls (scroll wheel)
|
||||
};
|
||||
|
||||
@@ -333365,12 +333364,12 @@ const FRAME_BOTTOM_RIGHT = 128;
|
||||
|
||||
export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
hasDarkBackground = true,
|
||||
robotTopRow,
|
||||
onScroll,
|
||||
}) => {
|
||||
const [frameIndex, setFrameIndex] = useState(0);
|
||||
const [targetFrame, setTargetFrame] = useState(0);
|
||||
const [cursor, setCursor] = useState({ x: 0, y: 0 });
|
||||
const lastCursorUpdateRef = useRef(0);
|
||||
const { stdout } = useStdout();
|
||||
const { stdin, setRawMode } = useStdin();
|
||||
|
||||
@@ -333379,8 +333378,9 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
const faceX = Math.floor(terminalWidth / 2);
|
||||
// Robot height after cropping is ~12 rows, eyes are roughly halfway down
|
||||
const robotHeight = 12;
|
||||
// faceY is the robot's eye level - calculated from robotTopRow prop (set before Ink mounts)
|
||||
const faceY = robotTopRow !== undefined ? robotTopRow + Math.floor(robotHeight / 2) : null;
|
||||
// Robot is always rendered at the top of the terminal in welcome state.
|
||||
// faceY is the robot's eye level (row 2 + half robot height)
|
||||
const faceY = 2 + Math.floor(robotHeight / 2);
|
||||
|
||||
// Select color theme based on background
|
||||
const theme = useMemo(() => hasDarkBackground ? THEME_DARK : THEME_LIGHT, [hasDarkBackground]);
|
||||
@@ -333425,7 +333425,12 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
if (button === 64 || button === 65) {
|
||||
onScroll?.();
|
||||
}
|
||||
setCursor({ x: parseInt(mouseMatch[2], 10), y: parseInt(mouseMatch[3], 10) });
|
||||
// Throttle cursor updates to ~20fps to reduce re-renders
|
||||
const now = Date.now();
|
||||
if (now - lastCursorUpdateRef.current >= 50) {
|
||||
lastCursorUpdateRef.current = now;
|
||||
setCursor({ x: parseInt(mouseMatch[2], 10), y: parseInt(mouseMatch[3], 10) });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -333444,9 +333449,6 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
|
||||
// Map cursor position to target frame (continuous interpolation)
|
||||
useEffect(() => {
|
||||
// Wait until we know where the robot is on screen
|
||||
if (faceY === null) return;
|
||||
|
||||
const dx = cursor.x - faceX;
|
||||
const dy = cursor.y - faceY;
|
||||
|
||||
|
||||
+54
-111
@@ -9,8 +9,9 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { API_PROVIDERS_LIST, openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
|
||||
import { ProviderToApiKeyMap } from "@/shared/storage"
|
||||
import type { ApiProvider } from "@/shared/api"
|
||||
import { openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
|
||||
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@/shared/storage"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { getAllFeaturedModels } from "../constants/featured-models"
|
||||
@@ -29,7 +30,7 @@ import {
|
||||
} from "./FeaturedModelPicker"
|
||||
import { ImportView } from "./ImportView"
|
||||
import { getDefaultModelId, hasModelPicker, ModelPicker } from "./ModelPicker"
|
||||
import { getProviderLabel, getProviderOrder } from "./ProviderPicker"
|
||||
import { CLI_EXCLUDED_PROVIDERS, getProviderLabel, getProviderOrder } from "./ProviderPicker"
|
||||
|
||||
type AuthStep =
|
||||
| "menu"
|
||||
@@ -54,13 +55,6 @@ interface AuthViewProps {
|
||||
onComplete?: () => void
|
||||
onError?: () => void
|
||||
onNavigateToWelcome?: () => void
|
||||
// Quick setup options
|
||||
quickSetup?: {
|
||||
provider?: string
|
||||
apikey?: string
|
||||
modelid?: string
|
||||
baseurl?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface SelectItem {
|
||||
@@ -152,9 +146,9 @@ const TextInput: React.FC<{
|
||||
)
|
||||
}
|
||||
|
||||
export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onError, onNavigateToWelcome, quickSetup }) => {
|
||||
export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onError, onNavigateToWelcome }) => {
|
||||
const { exit } = useApp()
|
||||
const [step, setStep] = useState<AuthStep>(quickSetup ? "saving" : "menu")
|
||||
const [step, setStep] = useState<AuthStep>("menu")
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>(
|
||||
StateManager.get().getApiConfiguration().actModeApiProvider ||
|
||||
StateManager.get().getApiConfiguration().planModeApiProvider ||
|
||||
@@ -172,15 +166,14 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const [importSource, setImportSource] = useState<ImportSource | null>(null)
|
||||
const [bedrockConfig, setBedrockConfig] = useState<BedrockConfig | null>(null)
|
||||
|
||||
// Use providers.json order, filtered to only available providers
|
||||
// Use providers.json order, filtered to exclude CLI-incompatible providers
|
||||
const sortedProviders = useMemo(() => {
|
||||
const availableProviders = new Set(API_PROVIDERS_LIST)
|
||||
return getProviderOrder().filter((p) => availableProviders.has(p))
|
||||
return getProviderOrder().filter((p) => !CLI_EXCLUDED_PROVIDERS.has(p))
|
||||
}, [])
|
||||
|
||||
// Main menu items - conditionally include import options
|
||||
const mainMenuItems: SelectItem[] = useMemo(() => {
|
||||
const items: SelectItem[] = [{ label: "Sign in with Cline account", value: "cline_auth" }]
|
||||
const items: SelectItem[] = [{ label: "Sign in with Cline", value: "cline_auth" }]
|
||||
|
||||
// Add OpenAI Codex option for ChatGPT subscribers
|
||||
items.push({ label: "Sign in with ChatGPT Subscription", value: "openai_codex_auth" })
|
||||
@@ -243,13 +236,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
}
|
||||
}, [step, selectedProvider])
|
||||
|
||||
// Handle quick setup
|
||||
useEffect(() => {
|
||||
if (quickSetup && step === "saving") {
|
||||
handleQuickSetup()
|
||||
}
|
||||
}, [quickSetup, step])
|
||||
|
||||
// Subscribe to auth status updates when in cline_auth step
|
||||
useEffect(() => {
|
||||
if (step !== "cline_auth") {
|
||||
@@ -269,17 +255,20 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const stateManager = StateManager.get()
|
||||
const mode = stateManager.getGlobalSettingsKey("mode") || "act"
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const modelIdKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
|
||||
// Use provider-specific model ID key (cline uses OpenRouterModelId)
|
||||
const modelIdKey = getProviderModelIdKey("cline" as ApiProvider, mode as "act" | "plan")
|
||||
const config: Record<string, string> = {
|
||||
actModeApiProvider: "cline",
|
||||
[providerKey]: "cline",
|
||||
[modelIdKey]: openRouterDefaultModelId,
|
||||
}
|
||||
if (modelIdKey) {
|
||||
config[modelIdKey] = openRouterDefaultModelId
|
||||
}
|
||||
stateManager.setApiConfiguration(config)
|
||||
stateManager.flushPendingState()
|
||||
|
||||
setSelectedProvider("cline")
|
||||
setModelId(config[modelIdKey])
|
||||
setModelId(openRouterDefaultModelId)
|
||||
setStep("cline_model")
|
||||
}
|
||||
}
|
||||
@@ -293,80 +282,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
}
|
||||
}, [step, controller])
|
||||
|
||||
const handleQuickSetup = async () => {
|
||||
if (!quickSetup) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { provider, apikey, modelid, baseurl } = quickSetup
|
||||
|
||||
// Validate required parameters
|
||||
if (!provider || !apikey || !modelid) {
|
||||
setErrorMessage("Quick setup requires --provider, --apikey, and --modelid flags")
|
||||
setStep("error")
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedProvider = provider.toLowerCase().trim()
|
||||
|
||||
if (!sortedProviders.includes(normalizedProvider)) {
|
||||
setErrorMessage(`Invalid provider '${provider}'. Supported providers: ${sortedProviders.join(", ")}`)
|
||||
setStep("error")
|
||||
return
|
||||
}
|
||||
|
||||
if (normalizedProvider === "bedrock") {
|
||||
setErrorMessage(
|
||||
"Bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup.",
|
||||
)
|
||||
setStep("error")
|
||||
return
|
||||
}
|
||||
|
||||
if (baseurl && !["openai", "openai-native"].includes(normalizedProvider)) {
|
||||
setErrorMessage("Base URL is only supported for OpenAI and OpenAI-compatible providers")
|
||||
setStep("error")
|
||||
return
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
const stateManager = StateManager.get()
|
||||
const config: Record<string, string> = {
|
||||
actModeApiProvider: normalizedProvider,
|
||||
planModeApiProvider: normalizedProvider,
|
||||
actModeApiModelId: modelid,
|
||||
planModeApiModelId: modelid,
|
||||
}
|
||||
|
||||
// Use provider-specific API key field
|
||||
const keyField = ProviderToApiKeyMap[normalizedProvider]
|
||||
if (keyField) {
|
||||
const fields = Array.isArray(keyField) ? keyField : [keyField]
|
||||
// Set the first key field for the provider
|
||||
config[fields[0]] = apikey
|
||||
} else {
|
||||
// Fallback to generic apiKey
|
||||
config.apiKey = apikey
|
||||
}
|
||||
|
||||
if (baseurl) {
|
||||
config.openAiBaseUrl = baseurl
|
||||
}
|
||||
|
||||
stateManager.setApiConfiguration(config)
|
||||
|
||||
await stateManager.flushPendingState()
|
||||
setSelectedProvider(normalizedProvider)
|
||||
setModelId(modelid)
|
||||
setBaseUrl(baseurl || "")
|
||||
setStep("success")
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : String(error))
|
||||
setStep("error")
|
||||
}
|
||||
}
|
||||
|
||||
// Start OpenAI Codex OAuth flow
|
||||
const startOpenAiCodexAuth = useCallback(async () => {
|
||||
try {
|
||||
@@ -383,14 +298,18 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const stateManager = StateManager.get()
|
||||
const mode = stateManager.getGlobalSettingsKey("mode") || "act"
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const modelIdKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
|
||||
// Use provider-specific model ID key (openai-codex uses generic apiModelId)
|
||||
const modelIdKey = getProviderModelIdKey("openai-codex" as ApiProvider, mode as "act" | "plan")
|
||||
const config: Record<string, string> = {
|
||||
actModeApiProvider: "openai-codex",
|
||||
planModeApiProvider: "openai-codex",
|
||||
[providerKey]: "openai-codex",
|
||||
[modelIdKey]: openAiCodexDefaultModelId,
|
||||
}
|
||||
if (modelIdKey) {
|
||||
config[modelIdKey] = openAiCodexDefaultModelId
|
||||
}
|
||||
stateManager.setApiConfiguration(config)
|
||||
stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
setSelectedProvider("openai-codex")
|
||||
@@ -473,13 +392,26 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
async (model: string, base: string) => {
|
||||
try {
|
||||
const stateManager = StateManager.get()
|
||||
// Use provider-specific model ID keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
const actModelKey = getProviderModelIdKey(selectedProvider as ApiProvider, "act")
|
||||
const planModelKey = getProviderModelIdKey(selectedProvider as ApiProvider, "plan")
|
||||
const config: Record<string, string> = {
|
||||
actModeApiProvider: selectedProvider,
|
||||
planModeApiProvider: selectedProvider,
|
||||
actModeApiModelId: model,
|
||||
planModeApiModelId: model,
|
||||
apiProvider: selectedProvider,
|
||||
}
|
||||
if (actModelKey) config[actModelKey] = model
|
||||
if (planModelKey) config[planModelKey] = model
|
||||
|
||||
// For cline/openrouter, also set model info (required for getModel() to return correct model)
|
||||
if (selectedProvider === "cline" || selectedProvider === "openrouter") {
|
||||
const openRouterModels = await controller?.readOpenRouterModels()
|
||||
const modelInfo = openRouterModels?.[model]
|
||||
if (modelInfo) {
|
||||
stateManager.setGlobalState("actModeOpenRouterModelInfo", modelInfo)
|
||||
stateManager.setGlobalState("planModeOpenRouterModelInfo", modelInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// Add API key or Bedrock-specific config
|
||||
if (selectedProvider === "bedrock" && bedrockConfig) {
|
||||
@@ -505,6 +437,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
config.openAiBaseUrl = base
|
||||
}
|
||||
stateManager.setApiConfiguration(config)
|
||||
stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
setStep("success")
|
||||
@@ -513,7 +446,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
setStep("error")
|
||||
}
|
||||
},
|
||||
[selectedProvider, apiKey, bedrockConfig],
|
||||
[selectedProvider, apiKey, bedrockConfig, controller],
|
||||
)
|
||||
|
||||
const handleModelIdSubmit = useCallback(
|
||||
@@ -565,11 +498,18 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
}, [])
|
||||
|
||||
// Auto-navigate to welcome after success (immediate)
|
||||
// For quick setup mode (no onNavigateToWelcome), exit the Ink app
|
||||
useEffect(() => {
|
||||
if (step === "success" && onNavigateToWelcome) {
|
||||
onNavigateToWelcome()
|
||||
if (step === "success") {
|
||||
if (onNavigateToWelcome) {
|
||||
onNavigateToWelcome()
|
||||
} else {
|
||||
// Quick setup mode - exit Ink app after successful configuration
|
||||
// The cleanup handler in runInkApp will handle process exit
|
||||
exit()
|
||||
}
|
||||
}
|
||||
}, [step, onNavigateToWelcome])
|
||||
}, [step, onNavigateToWelcome, exit])
|
||||
|
||||
// Error screen menu items
|
||||
const errorMenuItems: SelectItem[] = useMemo(() => {
|
||||
@@ -940,9 +880,12 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
<Text> </Text>
|
||||
{mainMenuItems.map((item, index) => (
|
||||
<Box key={item.value}>
|
||||
<Text color={index === menuIndex ? COLORS.primaryBlue : undefined}>
|
||||
{index === menuIndex ? "❯ " : " "}
|
||||
{item.label}
|
||||
<Text>
|
||||
<Text color={index === menuIndex ? COLORS.primaryBlue : undefined}>
|
||||
{index === menuIndex ? "❯ " : " "}
|
||||
{item.label}
|
||||
</Text>
|
||||
{item.value === "cline_auth" && <Text color="yellow"> (try Kimi K2.5 free!)</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
@@ -17,8 +17,43 @@ import { jsonParseSafe } from "../utils/parser"
|
||||
import { getToolDescription, isFileEditTool, parseToolFromMessage } from "../utils/tools"
|
||||
import { DiffView } from "./DiffView"
|
||||
|
||||
/**
|
||||
* Add "(Tab)" hint after "Act mode" mentions.
|
||||
* Case-insensitive, avoids double-adding if already present.
|
||||
* Matches just "Act mode" without requiring "to " prefix because markdown
|
||||
* processing may split "toggle to **Act mode**" into separate text chunks.
|
||||
*/
|
||||
function addActModeHint(text: string): React.ReactNode[] {
|
||||
// Match "Act mode" in various capitalizations, but not if already followed by (Tab)
|
||||
const actModeRegex = /\bact\s+mode\b(?!\s*\(tab\))/gi
|
||||
const parts = text.split(actModeRegex)
|
||||
const matches = text.match(actModeRegex)
|
||||
|
||||
if (!matches || parts.length <= 1) {
|
||||
return [text]
|
||||
}
|
||||
|
||||
const nodes: React.ReactNode[] = []
|
||||
parts.forEach((part, i) => {
|
||||
if (part) {
|
||||
nodes.push(part)
|
||||
}
|
||||
if (matches[i]) {
|
||||
nodes.push(
|
||||
<React.Fragment key={`act-mode-${i}`}>
|
||||
{matches[i]}
|
||||
<Text color="gray"> (Tab)</Text>
|
||||
</React.Fragment>,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Render inline markdown: **bold**, *italic*, `code`
|
||||
* Also adds "(Tab)" hints after "Act mode" mentions.
|
||||
* Returns array of React nodes with appropriate styling
|
||||
*/
|
||||
function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
@@ -29,19 +64,22 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
// Add text before match
|
||||
// Add text before match (with Act Mode hint processing)
|
||||
if (match.index > lastIndex) {
|
||||
nodes.push(text.slice(lastIndex, match.index))
|
||||
const beforeText = text.slice(lastIndex, match.index)
|
||||
nodes.push(...addActModeHint(beforeText))
|
||||
}
|
||||
|
||||
const fullMatch = match[0]
|
||||
const key = `md-${match.index}`
|
||||
|
||||
if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
|
||||
// Bold
|
||||
// Bold - also process for Act Mode hints inside bold text
|
||||
const boldContent = fullMatch.slice(2, -2)
|
||||
const hintedContent = addActModeHint(boldContent)
|
||||
nodes.push(
|
||||
<Text bold key={key}>
|
||||
{fullMatch.slice(2, -2)}
|
||||
{hintedContent}
|
||||
</Text>,
|
||||
)
|
||||
} else if (fullMatch.startsWith("*") && fullMatch.endsWith("*")) {
|
||||
@@ -59,12 +97,12 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
lastIndex = regex.lastIndex
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
// Add remaining text (with Act Mode hint processing)
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push(text.slice(lastIndex))
|
||||
nodes.push(...addActModeHint(text.slice(lastIndex)))
|
||||
}
|
||||
|
||||
return nodes.length > 0 ? nodes : [text]
|
||||
return nodes.length > 0 ? nodes : addActModeHint(text)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,7 +262,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow>
|
||||
<Text>{text}</Text>
|
||||
<MarkdownText>{text}</MarkdownText>
|
||||
</DotRow>
|
||||
</Box>
|
||||
)
|
||||
|
||||
+104
-70
@@ -101,13 +101,14 @@
|
||||
* - log-update: node_modules/ink/build/log-update.js (eraseLines logic)
|
||||
*/
|
||||
|
||||
import type { ModelInfo } from "@shared/api"
|
||||
import type { ApiProvider, ModelInfo } from "@shared/api"
|
||||
import { combineCommandSequences } from "@shared/combineCommandSequences"
|
||||
import type { ClineAsk, ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { getApiMetrics, getLastApiReqTotalTokens } from "@shared/getApiMetrics"
|
||||
import { EmptyRequest, StringRequest } from "@shared/proto/cline/common"
|
||||
import type { SlashCommandInfo } from "@shared/proto/cline/slash"
|
||||
import { CLI_ONLY_COMMANDS } from "@shared/slashCommands"
|
||||
import { getProviderModelIdKey } from "@shared/storage"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import { execSync } from "child_process"
|
||||
import { Box, Static, Text, useApp, useInput } from "ink"
|
||||
@@ -120,7 +121,9 @@ import { Logger } from "@/shared/services/Logger"
|
||||
import { Session } from "@/shared/services/Session"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useTaskContext, useTaskState } from "../context/TaskContext"
|
||||
import { useHomeEndKeys } from "../hooks/useHomeEndKeys"
|
||||
import { useIsSpinnerActive } from "../hooks/useStateSubscriber"
|
||||
import { findWordEnd, findWordStart, useTextInput } from "../hooks/useTextInput"
|
||||
import { moveCursorDown, moveCursorUp } from "../utils/cursor"
|
||||
import { setTerminalTitle } from "../utils/display"
|
||||
import {
|
||||
@@ -152,7 +155,6 @@ interface ChatViewProps {
|
||||
onExit?: () => void
|
||||
onComplete?: () => void
|
||||
onError?: () => void
|
||||
robotTopRow?: number
|
||||
initialPrompt?: string
|
||||
initialImages?: string[]
|
||||
taskId?: string
|
||||
@@ -319,7 +321,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
onExit,
|
||||
onComplete: _onComplete,
|
||||
onError,
|
||||
robotTopRow,
|
||||
initialPrompt,
|
||||
initialImages,
|
||||
taskId,
|
||||
@@ -335,9 +336,22 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
// Prefer prop controller over context controller (memoized for stable reference in callbacks)
|
||||
const ctrl = useMemo(() => controller || taskController, [controller, taskController])
|
||||
|
||||
// Input state
|
||||
const [textInput, setTextInput] = useState("")
|
||||
const [cursorPos, setCursorPos] = useState(0)
|
||||
// Input state - using hook for text editing with keyboard shortcuts
|
||||
const {
|
||||
text: textInput,
|
||||
cursorPos,
|
||||
setText: setTextInput,
|
||||
setCursorPos,
|
||||
handleKeyboardSequence,
|
||||
handleCtrlShortcut,
|
||||
deleteCharBefore,
|
||||
insertText: insertTextAtCursor,
|
||||
} = useTextInput()
|
||||
|
||||
// Ref for text input (used by useHomeEndKeys)
|
||||
const textInputRef = useRef(textInput)
|
||||
textInputRef.current = textInput
|
||||
|
||||
const [fileResults, setFileResults] = useState<FileSearchResult[]>([])
|
||||
const [selectedIndex, setSelectedIndex] = useState(0) // For file menu
|
||||
const [historyIndex, setHistoryIndex] = useState(-1) // -1 = not browsing history, 0+ = history item index
|
||||
@@ -367,9 +381,19 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
|
||||
// Panel state
|
||||
const [activePanel, setActivePanel] = useState<
|
||||
{ type: "settings"; initialMode?: "model-picker" | "featured-models" } | { type: "history" } | { type: "help" } | null
|
||||
| { type: "settings"; initialMode?: "model-picker" | "featured-models"; initialModelKey?: "actModelId" | "planModelId" }
|
||||
| { type: "history" }
|
||||
| { type: "help" }
|
||||
| null
|
||||
>(null)
|
||||
|
||||
// Handle Home/End keys from raw stdin (Ink doesn't expose these in useInput)
|
||||
useHomeEndKeys({
|
||||
onHome: useCallback(() => setCursorPos(0), [setCursorPos]),
|
||||
onEnd: useCallback(() => setCursorPos(textInputRef.current.length), [setCursorPos]),
|
||||
isActive: !activePanel, // Only active when no panel is open
|
||||
})
|
||||
|
||||
// Track when we're exiting to hide UI elements before exit
|
||||
const [isExiting, setIsExiting] = useState(false)
|
||||
|
||||
@@ -417,21 +441,23 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
StateManager.get().setGlobalState("autoApproveAllToggled", newValue)
|
||||
}, [autoApproveAll])
|
||||
|
||||
// Get model ID based on current mode
|
||||
// Re-read when activePanel changes (settings panel closes) to pick up changes
|
||||
const modelId = useMemo(() => {
|
||||
const stateManager = StateManager.get()
|
||||
const modelKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
|
||||
return (stateManager.getGlobalSettingsKey(modelKey) as string) || "claude-sonnet-4-20250514"
|
||||
}, [mode, activePanel])
|
||||
|
||||
// Get provider based on current mode
|
||||
// Get provider based on current mode (computed first since modelId depends on it)
|
||||
const provider = useMemo(() => {
|
||||
const stateManager = StateManager.get()
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
return (stateManager.getGlobalSettingsKey(providerKey) as string) || "anthropic"
|
||||
return (stateManager.getGlobalSettingsKey(providerKey) as string) || ""
|
||||
}, [mode, activePanel])
|
||||
|
||||
// Get model ID based on current mode and provider
|
||||
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
// Re-read when activePanel changes (settings panel closes) to pick up changes
|
||||
const modelId = useMemo(() => {
|
||||
if (!provider) return ""
|
||||
const stateManager = StateManager.get()
|
||||
const modelKey = getProviderModelIdKey(provider as ApiProvider, mode)
|
||||
return (stateManager.getGlobalSettingsKey(modelKey as string) as string) || ""
|
||||
}, [mode, provider, activePanel])
|
||||
|
||||
const toggleMode = useCallback(async () => {
|
||||
const newMode: Mode = mode === "act" ? "plan" : "act"
|
||||
setMode(newMode)
|
||||
@@ -574,6 +600,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
if (m.say === "checkpoint_created") return false
|
||||
if (m.say === "api_req_started") return false
|
||||
if (m.say === "api_req_retried") return false // Redundant with error_retry messages
|
||||
if (m.say === "reasoning") return false // Hide thinking traces - they clutter the UI
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -939,13 +966,48 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath])
|
||||
|
||||
// Handle keyboard input
|
||||
//
|
||||
// KEYBOARD PRIORITY ORDER (first match wins):
|
||||
// 1. Mouse escape sequences -> filtered out (from AsciiMotionCli tracking)
|
||||
// 2. Option+arrow escape sequences -> word navigation (handleKeyboardSequence)
|
||||
// 3. Option+arrow via key.meta -> word navigation (backup for when Ink parses it)
|
||||
// 4. Panel open -> bail (let panel handle its own input)
|
||||
// 5. Slash menu open -> menu navigation (up/down/tab/return/escape)
|
||||
// 6. File menu open -> menu navigation (up/down/tab/return/escape)
|
||||
// 7. History navigation -> up/down when input empty or matches history item
|
||||
// 8. Button actions -> "1"/"2" keys when buttons shown and no text typed
|
||||
// 9. Ask responses -> return to send, numbers for option selection
|
||||
// 10. Ctrl shortcuts -> Ctrl+A/E/W/U (handleCtrlShortcut)
|
||||
// 11. Large paste detection -> collapse into placeholder
|
||||
// 12. Normal input -> tab (mode toggle), return (submit), backspace, arrows, text
|
||||
//
|
||||
// Note: Home/End keys are handled separately by useHomeEndKeys hook because
|
||||
// Ink doesn't expose them in useInput (it sets input='' for these keys).
|
||||
//
|
||||
useInput((input, key) => {
|
||||
// Filter out mouse escape sequences from AsciiMotionCli's mouse tracking
|
||||
// 1. Filter out mouse escape sequences from AsciiMotionCli's mouse tracking
|
||||
if (isMouseEscapeSequence(input)) {
|
||||
return
|
||||
}
|
||||
|
||||
// When a panel is open, let the panel handle its own input
|
||||
// 2. Handle Option+arrow escape sequences for word navigation
|
||||
if (handleKeyboardSequence(input)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Handle Option+arrow via key.meta (backup - Ink sometimes parses these instead of passing raw sequence)
|
||||
if (key.meta) {
|
||||
if (key.leftArrow) {
|
||||
setCursorPos(findWordStart(textInput, cursorPos))
|
||||
return
|
||||
}
|
||||
if (key.rightArrow) {
|
||||
setCursorPos(findWordEnd(textInput, cursorPos))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 4. When a panel is open, let the panel handle its own input
|
||||
if (activePanel) {
|
||||
return
|
||||
}
|
||||
@@ -953,7 +1015,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
const inSlashMenu = slashInfo.inSlashMode && filteredCommands.length > 0 && !slashMenuDismissed
|
||||
const inFileMenu = mentionInfo.inMentionMode && fileResults.length > 0 && !inSlashMenu
|
||||
|
||||
// Slash command menu navigation (takes priority over file menu)
|
||||
// 5. Slash command menu navigation (takes priority over file menu)
|
||||
if (inSlashMenu) {
|
||||
if (key.upArrow) {
|
||||
setSelectedSlashIndex((i) => Math.max(0, i - 1))
|
||||
@@ -984,13 +1046,16 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
return
|
||||
}
|
||||
if (cmd.name === "models") {
|
||||
// If separate models for plan/act is enabled, just open settings (user picks which mode)
|
||||
const hasSeparateModels = StateManager.get().getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
const apiConfig = StateManager.get().getApiConfiguration()
|
||||
const provider = apiConfig.actModeApiProvider || apiConfig.planModeApiProvider
|
||||
const initialMode =
|
||||
hasSeparateModels || !provider ? undefined : provider === "cline" ? "featured-models" : "model-picker"
|
||||
setActivePanel({ type: "settings", initialMode })
|
||||
// Use current mode's provider to determine picker type
|
||||
const provider =
|
||||
mode === "act"
|
||||
? apiConfig.actModeApiProvider || apiConfig.planModeApiProvider
|
||||
: apiConfig.planModeApiProvider || apiConfig.actModeApiProvider
|
||||
const initialMode = !provider ? undefined : provider === "cline" ? "featured-models" : "model-picker"
|
||||
// Set model for current mode (plan or act)
|
||||
const initialModelKey = mode === "act" ? "actModelId" : "planModelId"
|
||||
setActivePanel({ type: "settings", initialMode, initialModelKey })
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
setSelectedSlashIndex(0)
|
||||
@@ -1030,7 +1095,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// File mention menu navigation
|
||||
// 6. File mention menu navigation
|
||||
if (inFileMenu) {
|
||||
if (key.upArrow) {
|
||||
setSelectedIndex((i) => Math.max(0, i - 1))
|
||||
@@ -1058,7 +1123,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// History navigation with up/down arrows
|
||||
// 7. History navigation with up/down arrows
|
||||
// Only works when: input is empty, or input matches the currently selected history item
|
||||
if (key.upArrow && !inSlashMenu && !inFileMenu) {
|
||||
const historyItems = getHistoryItems()
|
||||
@@ -1108,7 +1173,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Handle button actions (1 for primary, 2 for secondary)
|
||||
// 8. Handle button actions (1 for primary, 2 for secondary)
|
||||
// Only when buttons are enabled, not streaming, and no text has been typed
|
||||
if (
|
||||
buttonConfig.enableButtons &&
|
||||
@@ -1135,7 +1200,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ask responses for options and text input
|
||||
// 9. Handle ask responses for options and text input
|
||||
if (pendingAsk && !isYoloSuppressed(yolo, pendingAsk.ask as ClineAsk | undefined)) {
|
||||
// Allow sending text message for any ask type where sending is enabled
|
||||
if (key.return && textInput.trim() && !buttonConfig.sendingDisabled) {
|
||||
@@ -1153,40 +1218,12 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Ctrl+ shortcuts
|
||||
const keydown = input?.toLowerCase()
|
||||
if (key.ctrl && cursorPos && keydown) {
|
||||
switch (keydown) {
|
||||
case "u": // Ctrl+U, clear line before cursor
|
||||
if (cursorPos > 0) {
|
||||
setTextInput((prev) => prev.slice(cursorPos))
|
||||
setCursorPos(0)
|
||||
}
|
||||
return
|
||||
case "e": // Ctrl+E, move cursor to end
|
||||
setCursorPos(textInput.length)
|
||||
return
|
||||
case "b": // Ctrl+B, move cursor left
|
||||
setCursorPos((pos) => Math.max(0, pos - 1))
|
||||
return
|
||||
case "f": // Ctrl+F, move cursor right
|
||||
setCursorPos((pos) => Math.min(textInput.length, pos + 1))
|
||||
return
|
||||
case "d": // Ctrl+D, delete character after cursor
|
||||
if (cursorPos < textInput.length) {
|
||||
setTextInput((prev) => prev.slice(0, cursorPos) + prev.slice(cursorPos + 1))
|
||||
}
|
||||
return
|
||||
case "h": // Ctrl+H, delete character before cursor (like backspace)
|
||||
if (cursorPos > 0) {
|
||||
setTextInput((prev) => prev.slice(0, cursorPos - 1) + prev.slice(cursorPos))
|
||||
setCursorPos((pos) => pos - 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
// 10. Handle Ctrl+ shortcuts (Ctrl+A, Ctrl+E, Ctrl+W, etc.)
|
||||
if (key.ctrl && input && handleCtrlShortcut(input)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Detect paste by checking if input length exceeds threshold
|
||||
// 11. Detect paste by checking if input length exceeds threshold
|
||||
// Large pastes mess up the terminal UI, so we collapse them into a placeholder
|
||||
// Terminal sends large pastes in multiple chunks, so we combine chunks that arrive rapidly
|
||||
if (input && input.length > PASTE_COLLAPSE_THRESHOLD) {
|
||||
@@ -1247,7 +1284,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
return // Exit early - don't also add the raw input via normal handling below
|
||||
}
|
||||
|
||||
// Normal input handling
|
||||
// 12. Normal input handling
|
||||
if (key.shift && key.tab) {
|
||||
toggleAutoApproveAll()
|
||||
return
|
||||
@@ -1263,10 +1300,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
return
|
||||
}
|
||||
if (key.backspace || key.delete) {
|
||||
if (cursorPos > 0) {
|
||||
setTextInput((prev) => prev.slice(0, cursorPos - 1) + prev.slice(cursorPos))
|
||||
setCursorPos((pos) => pos - 1)
|
||||
}
|
||||
deleteCharBefore()
|
||||
return
|
||||
}
|
||||
// Cursor movement (when not in a menu)
|
||||
@@ -1288,8 +1322,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
// Normal input (single char or short paste)
|
||||
if (input && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow && !key.tab) {
|
||||
setTextInput((prev) => prev.slice(0, cursorPos) + input + prev.slice(cursorPos))
|
||||
setCursorPos((pos) => pos + input.length)
|
||||
insertTextAtCursor(input)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1353,7 +1386,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
{/* Animated robot and welcome text - only shown before messages start and user hasn't scrolled */}
|
||||
{isWelcomeState && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<AsciiMotionCli onScroll={() => setUserScrolled(true)} robotTopRow={robotTopRow} />
|
||||
<AsciiMotionCli onScroll={() => setUserScrolled(true)} />
|
||||
<Text> </Text>
|
||||
<Text bold color="white">
|
||||
{centerText("What can I do for you?")}
|
||||
@@ -1404,6 +1437,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
<SettingsPanelContent
|
||||
controller={ctrl}
|
||||
initialMode={activePanel.initialMode}
|
||||
initialModelKey={activePanel.initialModelKey}
|
||||
onClose={() => setActivePanel(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -35,30 +35,32 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{featuredModels.map((model, i) => (
|
||||
<Box flexDirection="column" key={model.id} marginBottom={1}>
|
||||
<Box>
|
||||
<Text color={i === selectedIndex ? COLORS.primaryBlue : undefined}>
|
||||
{i === selectedIndex ? "❯ " : " "}
|
||||
</Text>
|
||||
<Text bold color={i === selectedIndex ? COLORS.primaryBlue : "white"}>
|
||||
{model.name}
|
||||
</Text>
|
||||
{model.label && (
|
||||
<>
|
||||
<Text> </Text>
|
||||
<Text backgroundColor={model.label === "FREE" ? "gray" : COLORS.primaryBlue} color="black">
|
||||
{" "}
|
||||
{model.label}{" "}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{featuredModels.map((model, i) => {
|
||||
const isSelected = i === selectedIndex
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" key={model.id} marginBottom={1}>
|
||||
<Box>
|
||||
<Text color={isSelected ? COLORS.primaryBlue : undefined}>{isSelected ? "❯ " : " "}</Text>
|
||||
<Text bold color={isSelected ? COLORS.primaryBlue : "white"}>
|
||||
{model.name}
|
||||
</Text>
|
||||
{model.label && (
|
||||
<>
|
||||
<Text> </Text>
|
||||
<Text backgroundColor={model.label === "FREE" ? "gray" : COLORS.primaryBlue} color="black">
|
||||
{" "}
|
||||
{model.label}{" "}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box paddingLeft={2}>
|
||||
<Text color="gray">{model.description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box paddingLeft={2}>
|
||||
<Text color="gray">{model.description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
|
||||
{showBrowseAll && (
|
||||
<Box>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import { Box, Text, useInput } from "ink"
|
||||
import React, { useCallback, useEffect, useState } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import type { ApiProvider } from "@/shared/api"
|
||||
import { getProviderModelIdKey } from "@/shared/storage"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import {
|
||||
@@ -69,13 +71,16 @@ export const ImportView: React.FC<ImportViewProps> = ({ source, onComplete, onCa
|
||||
// Set API key
|
||||
config[selectedKey.keyField] = selectedKey.key
|
||||
|
||||
// Set model ID if available
|
||||
// Set model ID if available (use provider-specific keys)
|
||||
if (selectedKey.modelId) {
|
||||
config.actModeApiModelId = selectedKey.modelId
|
||||
config.planModeApiModelId = selectedKey.modelId
|
||||
const actModelKey = getProviderModelIdKey(selectedKey.provider as ApiProvider, "act")
|
||||
const planModelKey = getProviderModelIdKey(selectedKey.provider as ApiProvider, "plan")
|
||||
if (actModelKey) config[actModelKey] = selectedKey.modelId
|
||||
if (planModelKey) config[planModelKey] = selectedKey.modelId
|
||||
}
|
||||
|
||||
stateManager.setApiConfiguration(config)
|
||||
stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
onComplete()
|
||||
|
||||
@@ -4,31 +4,12 @@
|
||||
|
||||
import React, { useMemo } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { API_PROVIDERS_LIST, ApiConfiguration } from "@/shared/api"
|
||||
import providersData from "@/shared/providers/providers.json"
|
||||
import type { ApiConfiguration } from "@/shared/api"
|
||||
import { CLI_EXCLUDED_PROVIDERS, getProviderLabel, getProviderOrder } from "../utils/providers"
|
||||
import { SearchableList, SearchableListItem } from "./SearchableList"
|
||||
|
||||
// Create a lookup map from provider value to display label
|
||||
const providerLabels: Record<string, string> = Object.fromEntries(
|
||||
providersData.list.map((p: { value: string; label: string }) => [p.value, p.label]),
|
||||
)
|
||||
|
||||
// Get provider order from providers.json (same order as webview)
|
||||
const providerOrder: string[] = providersData.list.map((p: { value: string }) => p.value)
|
||||
|
||||
/**
|
||||
* Providers that are not supported in CLI.
|
||||
* - vscode-lm: Requires VS Code's Language Model API (see ENG-1490 for OAuth-based support)
|
||||
*/
|
||||
const CLI_EXCLUDED_PROVIDERS = new Set<string>(["vscode-lm"])
|
||||
|
||||
export function getProviderLabel(providerId: string): string {
|
||||
return providerLabels[providerId] || providerId
|
||||
}
|
||||
|
||||
export function getProviderOrder(): string[] {
|
||||
return providerOrder
|
||||
}
|
||||
// Re-export for backwards compatibility
|
||||
export { CLI_EXCLUDED_PROVIDERS, getProviderLabel, getProviderOrder }
|
||||
|
||||
/**
|
||||
* Check if a provider is configured (has required credentials/settings)
|
||||
@@ -37,7 +18,8 @@ export function getProviderOrder(): string[] {
|
||||
function isProviderConfigured(providerId: string, config: ApiConfiguration): boolean {
|
||||
switch (providerId) {
|
||||
case "cline":
|
||||
return true // Always available
|
||||
// Check if user has Cline account auth data stored
|
||||
return !!(config as Record<string, unknown>)["cline:clineAccountId"]
|
||||
case "anthropic":
|
||||
return !!config.apiKey
|
||||
case "openrouter":
|
||||
@@ -51,7 +33,8 @@ function isProviderConfigured(providerId: string, config: ApiConfiguration): boo
|
||||
case "openai-native":
|
||||
return !!config.openAiNativeApiKey
|
||||
case "openai-codex":
|
||||
return !!config.openAiCodexRefreshToken
|
||||
// OpenAI Codex uses OAuth with credentials stored as JSON blob
|
||||
return !!(config as Record<string, unknown>)["openai-codex-oauth-credentials"]
|
||||
case "deepseek":
|
||||
return !!config.deepSeekApiKey
|
||||
case "xai":
|
||||
@@ -143,12 +126,11 @@ export const ProviderPicker: React.FC<ProviderPickerProps> = ({ onSelect, isActi
|
||||
// Get API configuration to check which providers are configured
|
||||
const apiConfig = StateManager.get().getApiConfiguration()
|
||||
|
||||
// Use providers.json order, filtered to available providers (excluding CLI-incompatible ones)
|
||||
// Use providers.json order, filtered to exclude CLI-incompatible providers
|
||||
const items: SearchableListItem[] = useMemo(() => {
|
||||
const availableProviders = new Set(API_PROVIDERS_LIST)
|
||||
const sorted = providerOrder.filter((p) => availableProviders.has(p) && !CLI_EXCLUDED_PROVIDERS.has(p))
|
||||
const sorted = getProviderOrder().filter((p: string) => !CLI_EXCLUDED_PROVIDERS.has(p))
|
||||
|
||||
return sorted.map((providerId) => ({
|
||||
return sorted.map((providerId: string) => ({
|
||||
id: providerId,
|
||||
label: getProviderLabel(providerId),
|
||||
suffix: isProviderConfigured(providerId, apiConfig) ? "(Configured)" : undefined,
|
||||
|
||||
@@ -5,17 +5,20 @@
|
||||
|
||||
import type { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
|
||||
import { ProviderToApiKeyMap } from "@shared/storage"
|
||||
import type { ApiProvider } from "@shared/api"
|
||||
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@shared/storage"
|
||||
import type { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { Box, Text, useInput } from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { buildApiHandler } from "@/core/api"
|
||||
import type { Controller } from "@/core/controller"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { AuthService, ClineAccountOrganization } from "@/services/auth/AuthService"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { version as CLI_VERSION } from "../../package.json"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
@@ -39,6 +42,7 @@ interface SettingsPanelContentProps {
|
||||
onClose: () => void
|
||||
controller?: Controller
|
||||
initialMode?: "model-picker" | "featured-models"
|
||||
initialModelKey?: "actModelId" | "planModelId"
|
||||
}
|
||||
|
||||
type SettingsTab = "api" | "auto-approve" | "features" | "other" | "account"
|
||||
@@ -113,7 +117,12 @@ function formatBalance(balance: number | null): string {
|
||||
return `$${(balance / 1000000).toFixed(2)}`
|
||||
}
|
||||
|
||||
export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onClose, controller, initialMode }) => {
|
||||
export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
onClose,
|
||||
controller,
|
||||
initialMode,
|
||||
initialModelKey,
|
||||
}) => {
|
||||
const { isRawModeSupported } = useStdinContext()
|
||||
const stateManager = StateManager.get()
|
||||
|
||||
@@ -122,7 +131,9 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [isPickingModel, setIsPickingModel] = useState(initialMode === "model-picker")
|
||||
const [pickingModelKey, setPickingModelKey] = useState<"actModelId" | "planModelId" | null>(initialMode ? "actModelId" : null)
|
||||
const [pickingModelKey, setPickingModelKey] = useState<"actModelId" | "planModelId" | null>(
|
||||
initialMode ? (initialModelKey ?? "actModelId") : null,
|
||||
)
|
||||
const [isPickingFeaturedModel, setIsPickingFeaturedModel] = useState(initialMode === "featured-models")
|
||||
const [featuredModelIndex, setFeaturedModelIndex] = useState(0)
|
||||
const [isPickingProvider, setIsPickingProvider] = useState(false)
|
||||
@@ -180,12 +191,31 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
const [accountChecked, setAccountChecked] = useState(false) // Tracks if we've already checked auth
|
||||
|
||||
// Get current provider and model info
|
||||
const apiConfig = stateManager.getApiConfiguration()
|
||||
const [provider, setProvider] = useState<string>(
|
||||
() => apiConfig.actModeApiProvider || apiConfig.planModeApiProvider || "not configured",
|
||||
() =>
|
||||
stateManager.getApiConfiguration().actModeApiProvider ||
|
||||
stateManager.getApiConfiguration().planModeApiProvider ||
|
||||
"not configured",
|
||||
)
|
||||
const actModelId = (stateManager.getGlobalSettingsKey("actModeApiModelId") as string) || ""
|
||||
const planModelId = (stateManager.getGlobalSettingsKey("planModeApiModelId") as string) || ""
|
||||
// Refresh trigger to force re-reading model IDs from state
|
||||
const [modelRefreshKey, setModelRefreshKey] = useState(0)
|
||||
const refreshModelIds = useCallback(() => setModelRefreshKey((k) => k + 1), [])
|
||||
|
||||
// Read model IDs from state (re-reads when refreshKey changes)
|
||||
const { actModelId, planModelId } = useMemo(() => {
|
||||
const apiConfig = stateManager.getApiConfiguration()
|
||||
const actProvider = apiConfig.actModeApiProvider
|
||||
const planProvider = apiConfig.planModeApiProvider || actProvider
|
||||
if (!actProvider && !planProvider) {
|
||||
return { actModelId: "", planModelId: "" }
|
||||
}
|
||||
const actKey = actProvider ? getProviderModelIdKey(actProvider as ApiProvider, "act") : null
|
||||
const planKey = planProvider ? getProviderModelIdKey(planProvider as ApiProvider, "plan") : null
|
||||
return {
|
||||
actModelId: actKey ? (stateManager.getGlobalSettingsKey(actKey as string) as string) || "" : "",
|
||||
planModelId: planKey ? (stateManager.getGlobalSettingsKey(planKey as string) as string) || "" : "",
|
||||
}
|
||||
}, [modelRefreshKey, stateManager])
|
||||
|
||||
// Toggle a feature setting
|
||||
const toggleFeature = useCallback(
|
||||
@@ -227,19 +257,21 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
return
|
||||
}
|
||||
|
||||
// Get organization info
|
||||
const organizations = authService.getUserOrganizations()
|
||||
const accountService = ClineAccountService.getInstance()
|
||||
|
||||
// Fetch fresh organization info from server (like webview's getUserOrganizations RPC)
|
||||
// Don't use authService.getUserOrganizations() as it returns cached data
|
||||
const organizations = await accountService.fetchUserOrganizationsRPC()
|
||||
let activeOrgId: string | undefined
|
||||
if (organizations) {
|
||||
setAccountOrganizations(organizations)
|
||||
const activeOrg = organizations.find((org) => org.active)
|
||||
setAccountOrganization(activeOrg || null)
|
||||
activeOrgId = activeOrg?.organizationId
|
||||
}
|
||||
|
||||
// Fetch credit balance
|
||||
try {
|
||||
const accountService = ClineAccountService.getInstance()
|
||||
const activeOrgId = authService.getActiveOrganizationId()
|
||||
|
||||
if (activeOrgId) {
|
||||
const orgBalance = await accountService.fetchOrganizationCreditsRPC(activeOrgId)
|
||||
if (orgBalance?.balance !== undefined) {
|
||||
@@ -299,9 +331,8 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
setIsPickingOrganization(false)
|
||||
try {
|
||||
await ClineAccountService.getInstance().switchAccount(orgId || undefined)
|
||||
// Refetch to get updated auth info with new active org
|
||||
await AuthService.getInstance(controller).restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
fetchAccountInfo()
|
||||
// Refetch fresh org data from server
|
||||
await fetchAccountInfo()
|
||||
} catch {
|
||||
// Error switching organization
|
||||
}
|
||||
@@ -334,6 +365,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
setAccountChecked(false) // Reset so fetchAccountInfo can run
|
||||
await applyProviderConfig({ providerId: "cline", controller })
|
||||
setProvider("cline")
|
||||
refreshModelIds()
|
||||
fetchAccountInfo()
|
||||
}
|
||||
}
|
||||
@@ -529,9 +561,11 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
key: "telemetry",
|
||||
label: "Error/usage reporting",
|
||||
type: "checkbox",
|
||||
value: telemetry === "enabled",
|
||||
value: telemetry !== "disabled",
|
||||
description: "Help improve Cline by sending anonymous usage data",
|
||||
},
|
||||
{ key: "separator", label: "", type: "separator", value: "" },
|
||||
{ key: "version", label: "", type: "readonly", value: `Cline v${CLI_VERSION}` },
|
||||
]
|
||||
|
||||
case "account":
|
||||
@@ -683,8 +717,15 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
stateManager.setGlobalState("planActSeparateModelsSetting", newValue)
|
||||
// When disabling separate models, sync plan model to act model
|
||||
if (!newValue) {
|
||||
const actModel = stateManager.getGlobalSettingsKey("actModeApiModelId")
|
||||
stateManager.setGlobalState("planModeApiModelId", actModel)
|
||||
const apiConfig = stateManager.getApiConfiguration()
|
||||
const actProvider = apiConfig.actModeApiProvider
|
||||
const planProvider = apiConfig.planModeApiProvider || actProvider
|
||||
if (actProvider) {
|
||||
const actKey = getProviderModelIdKey(actProvider as ApiProvider, "act")
|
||||
const planKey = planProvider ? getProviderModelIdKey(planProvider as ApiProvider, "plan") : null
|
||||
const actModel = stateManager.getGlobalSettingsKey(actKey as string)
|
||||
if (planKey) stateManager.setGlobalState(planKey, actModel)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -718,8 +759,11 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
const newTelemetry: TelemetrySetting = newValue ? "enabled" : "disabled"
|
||||
setTelemetry(newTelemetry)
|
||||
stateManager.setGlobalState("telemetrySetting", newTelemetry)
|
||||
// Update telemetry providers to respect the new setting
|
||||
controller?.updateTelemetrySetting(newTelemetry)
|
||||
// Flush synchronously before continuing - must complete before app can exit
|
||||
void stateManager.flushPendingState().then(() => {
|
||||
// Update telemetry providers to respect the new setting
|
||||
controller?.updateTelemetrySetting(newTelemetry)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -767,21 +811,69 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
|
||||
// Handle model selection from picker
|
||||
const handleModelSelect = useCallback(
|
||||
(modelId: string) => {
|
||||
async (modelId: string) => {
|
||||
if (!pickingModelKey) return
|
||||
const apiConfig = stateManager.getApiConfiguration()
|
||||
const actProvider = apiConfig.actModeApiProvider
|
||||
const planProvider = apiConfig.planModeApiProvider || actProvider
|
||||
const providerForSelection = separateModels
|
||||
? pickingModelKey === "actModelId"
|
||||
? actProvider
|
||||
: planProvider
|
||||
: actProvider || planProvider
|
||||
if (!providerForSelection) return
|
||||
// Use provider-specific model ID keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
const actKey = actProvider ? getProviderModelIdKey(actProvider as ApiProvider, "act") : null
|
||||
const planKey = planProvider ? getProviderModelIdKey(planProvider as ApiProvider, "plan") : null
|
||||
|
||||
// For cline/openrouter providers, also set model info (like webview does)
|
||||
let modelInfo
|
||||
if (providerForSelection === "cline" || providerForSelection === "openrouter") {
|
||||
const openRouterModels = await controller?.readOpenRouterModels()
|
||||
modelInfo = openRouterModels?.[modelId]
|
||||
}
|
||||
|
||||
if (separateModels) {
|
||||
// Only update the selected mode's model
|
||||
const stateKey = pickingModelKey === "actModelId" ? "actModeApiModelId" : "planModeApiModelId"
|
||||
stateManager.setGlobalState(stateKey, modelId)
|
||||
const stateKey = pickingModelKey === "actModelId" ? actKey : planKey
|
||||
if (stateKey) stateManager.setGlobalState(stateKey, modelId)
|
||||
// Set model info for the selected mode
|
||||
if (modelInfo) {
|
||||
const infoKey =
|
||||
pickingModelKey === "actModelId" ? "actModeOpenRouterModelInfo" : "planModeOpenRouterModelInfo"
|
||||
stateManager.setGlobalState(infoKey, modelInfo)
|
||||
}
|
||||
} else {
|
||||
// Update both modes to keep them in sync
|
||||
stateManager.setGlobalState("actModeApiModelId", modelId)
|
||||
stateManager.setGlobalState("planModeApiModelId", modelId)
|
||||
if (actKey) stateManager.setGlobalState(actKey, modelId)
|
||||
if (planKey) stateManager.setGlobalState(planKey, modelId)
|
||||
// Set model info for both modes
|
||||
if (modelInfo) {
|
||||
stateManager.setGlobalState("actModeOpenRouterModelInfo", modelInfo)
|
||||
stateManager.setGlobalState("planModeOpenRouterModelInfo", modelInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// Flush pending state to ensure model ID is persisted
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
// Rebuild API handler if there's an active task
|
||||
if (controller?.task) {
|
||||
const currentMode = stateManager.getGlobalSettingsKey("mode")
|
||||
const freshApiConfig = stateManager.getApiConfiguration()
|
||||
controller.task.api = buildApiHandler({ ...freshApiConfig, ulid: controller.task.ulid }, currentMode)
|
||||
}
|
||||
|
||||
refreshModelIds()
|
||||
setIsPickingModel(false)
|
||||
setPickingModelKey(null)
|
||||
|
||||
// If opened from /models command, close the entire settings panel
|
||||
if (initialMode) {
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
[pickingModelKey, separateModels, stateManager],
|
||||
[pickingModelKey, separateModels, stateManager, controller, refreshModelIds, initialMode, onClose],
|
||||
)
|
||||
|
||||
// Handle language selection from picker
|
||||
@@ -812,6 +904,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
// Success - apply provider config
|
||||
await applyProviderConfig({ providerId: "openai-codex", controller })
|
||||
setProvider("openai-codex")
|
||||
refreshModelIds()
|
||||
setIsWaitingForCodexAuth(false)
|
||||
} catch (error) {
|
||||
openAiCodexOAuthManager.cancelAuthorizationFlow()
|
||||
@@ -820,7 +913,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
}
|
||||
}, [controller])
|
||||
|
||||
// Handle provider selection from picker
|
||||
const handleProviderSelect = useCallback(
|
||||
(providerId: string) => {
|
||||
// Special handling for Cline - uses OAuth (but skip if already logged in)
|
||||
@@ -832,6 +924,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
// Already logged in - just set the provider
|
||||
applyProviderConfig({ providerId: "cline", controller })
|
||||
setProvider("cline")
|
||||
refreshModelIds()
|
||||
} else {
|
||||
// Not logged in - trigger OAuth
|
||||
handleClineLogin()
|
||||
@@ -870,10 +963,11 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
// Provider doesn't need an API key (rare) - just set it
|
||||
applyProviderConfig({ providerId, controller })
|
||||
setProvider(providerId)
|
||||
refreshModelIds()
|
||||
setIsPickingProvider(false)
|
||||
}
|
||||
},
|
||||
[stateManager, startCodexAuth, handleClineLogin, controller],
|
||||
[stateManager, startCodexAuth, handleClineLogin, controller, refreshModelIds],
|
||||
)
|
||||
|
||||
// Handle API key submission after provider selection
|
||||
@@ -885,11 +979,12 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
|
||||
await applyProviderConfig({ providerId: pendingProvider, apiKey: submittedValue.trim(), controller })
|
||||
setProvider(pendingProvider)
|
||||
refreshModelIds()
|
||||
setIsEnteringApiKey(false)
|
||||
setPendingProvider(null)
|
||||
setApiKeyValue("")
|
||||
},
|
||||
[pendingProvider, controller],
|
||||
[pendingProvider, controller, refreshModelIds],
|
||||
)
|
||||
|
||||
// Handle Bedrock configuration complete
|
||||
@@ -906,8 +1001,11 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
|
||||
const defaultModelId = getDefaultModelId("bedrock")
|
||||
if (defaultModelId) {
|
||||
config.actModeApiModelId = defaultModelId
|
||||
config.planModeApiModelId = defaultModelId
|
||||
// Use provider-specific model ID keys
|
||||
const actModelKey = getProviderModelIdKey("bedrock" as ApiProvider, "act")
|
||||
const planModelKey = getProviderModelIdKey("bedrock" as ApiProvider, "plan")
|
||||
if (actModelKey) config[actModelKey] = defaultModelId
|
||||
if (planModelKey) config[planModelKey] = defaultModelId
|
||||
}
|
||||
|
||||
if (bedrockConfig.awsProfile !== undefined) config.awsProfile = bedrockConfig.awsProfile
|
||||
@@ -919,6 +1017,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
|
||||
// Close Bedrock config first, then flush state async
|
||||
setProvider("bedrock")
|
||||
refreshModelIds()
|
||||
setIsConfiguringBedrock(false)
|
||||
setPendingProvider(null)
|
||||
|
||||
@@ -941,17 +1040,26 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
|
||||
switch (item.key) {
|
||||
case "actModelId":
|
||||
case "planModelId":
|
||||
case "planModelId": {
|
||||
// Use provider-specific model ID keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
const apiConfig = stateManager.getApiConfiguration()
|
||||
const actProvider = apiConfig.actModeApiProvider
|
||||
const planProvider = apiConfig.planModeApiProvider || actProvider
|
||||
if (!actProvider && !planProvider) break
|
||||
const actKey = actProvider ? getProviderModelIdKey(actProvider as ApiProvider, "act") : null
|
||||
const planKey = planProvider ? getProviderModelIdKey(planProvider as ApiProvider, "plan") : null
|
||||
|
||||
if (separateModels) {
|
||||
// Only update the selected mode's model
|
||||
const stateKey = item.key === "actModelId" ? "actModeApiModelId" : "planModeApiModelId"
|
||||
stateManager.setGlobalState(stateKey, editValue || undefined)
|
||||
const stateKey = item.key === "actModelId" ? actKey : planKey
|
||||
if (stateKey) stateManager.setGlobalState(stateKey, editValue || undefined)
|
||||
} else {
|
||||
// Update both modes to keep them in sync
|
||||
stateManager.setGlobalState("actModeApiModelId", editValue || undefined)
|
||||
stateManager.setGlobalState("planModeApiModelId", editValue || undefined)
|
||||
if (actKey) stateManager.setGlobalState(actKey, editValue || undefined)
|
||||
if (planKey) stateManager.setGlobalState(planKey, editValue || undefined)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "language":
|
||||
setPreferredLanguage(editValue)
|
||||
stateManager.setGlobalState("preferredLanguage", editValue)
|
||||
@@ -1019,6 +1127,10 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
if (key.escape) {
|
||||
setIsPickingFeaturedModel(false)
|
||||
setPickingModelKey(null)
|
||||
// If opened from /models command, close the entire settings panel
|
||||
if (initialMode) {
|
||||
onClose()
|
||||
}
|
||||
} else if (key.upArrow) {
|
||||
setFeaturedModelIndex((prev) => (prev > 0 ? prev - 1 : maxIndex))
|
||||
} else if (key.downArrow) {
|
||||
@@ -1045,6 +1157,10 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
if (key.escape) {
|
||||
setIsPickingModel(false)
|
||||
setPickingModelKey(null)
|
||||
// If opened from /models command, close the entire settings panel
|
||||
if (initialMode) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1431,7 +1547,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
|
||||
<Text bold color={isSelected ? COLORS.primaryBlue : undefined}>
|
||||
{isSelected ? "❯" : " "}{" "}
|
||||
</Text>
|
||||
<Text color={isSelected ? COLORS.primaryBlue : "white"}>{item.label}: </Text>
|
||||
{item.label && <Text color={isSelected ? COLORS.primaryBlue : "white"}>{item.label}: </Text>}
|
||||
<Text color={item.type === "readonly" ? "gray" : COLORS.primaryBlue}>
|
||||
{typeof item.value === "string" ? item.value : String(item.value)}
|
||||
</Text>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
import { Box, Text, useInput } from "ink"
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { getProviderDefaultModelId, Mode } from "@/shared/storage"
|
||||
import type { ApiProvider } from "@/shared/api"
|
||||
import { getProviderDefaultModelId, getProviderModelIdKey, Mode } from "@/shared/storage"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import {
|
||||
checkAndWarnRipgrepMissing,
|
||||
@@ -73,11 +74,12 @@ export const WelcomeView: React.FC<WelcomeViewProps> = ({ onSubmit, onExit, cont
|
||||
return currentProvider || "cline"
|
||||
}, [controller])
|
||||
|
||||
// Get model ID based on current mode
|
||||
// Get model ID based on current mode and provider
|
||||
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
const modelId = useMemo(() => {
|
||||
const stateManager = StateManager.get()
|
||||
const modelKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
|
||||
return (stateManager.getGlobalSettingsKey(modelKey) as string) || getProviderDefaultModelId(provider)
|
||||
const modelKey = getProviderModelIdKey(provider as ApiProvider, mode)
|
||||
return (stateManager.getGlobalSettingsKey(modelKey as string) as string) || getProviderDefaultModelId(provider)
|
||||
}, [mode, provider])
|
||||
|
||||
const toggleMode = useCallback(() => {
|
||||
|
||||
@@ -34,8 +34,8 @@ export const FEATURED_MODELS = {
|
||||
free: [
|
||||
{
|
||||
id: "moonshotai/kimi-k2.5",
|
||||
name: "MoonshotAI Kimi K2.5",
|
||||
description: "Topping benchmarks leading open source",
|
||||
name: "Kimi K2.5",
|
||||
description: "State-of-the-art model topping benchmarks",
|
||||
label: "FREE",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Keyboard escape sequences for terminal input handling.
|
||||
*
|
||||
* Different terminals send different escape sequences for the same keys.
|
||||
* This file consolidates all known sequences to ensure broad compatibility.
|
||||
*
|
||||
* Note on Ink's useInput limitations:
|
||||
* - Ink parses Home/End keys but doesn't expose them in the key object
|
||||
* - Ink sets input='' for Home/End (they're in nonAlphanumericKeys)
|
||||
* - We use useHomeEndKeys hook to intercept these from raw stdin
|
||||
* - Option+arrow sometimes comes as escape sequence, sometimes as key.meta + arrow
|
||||
*/
|
||||
|
||||
// Home key escape sequences from various terminals
|
||||
export const HOME_SEQUENCES = new Set([
|
||||
"\x1b[H", // CSI H - most common (xterm, Terminal.app)
|
||||
"\x1b[1~", // CSI 1 ~ - Linux console, some xterms
|
||||
"\x1bOH", // SS3 H - xterm application mode
|
||||
"\x1b[7~", // rxvt
|
||||
])
|
||||
|
||||
// End key escape sequences from various terminals
|
||||
export const END_SEQUENCES = new Set([
|
||||
"\x1b[F", // CSI F - most common (xterm, Terminal.app)
|
||||
"\x1b[4~", // CSI 4 ~ - Linux console, some xterms
|
||||
"\x1bOF", // SS3 F - xterm application mode
|
||||
"\x1b[8~", // rxvt
|
||||
])
|
||||
|
||||
// Option+Left (move word left) escape sequences
|
||||
export const OPTION_LEFT_SEQUENCES = new Set([
|
||||
"\x1bb", // Meta+b - emacs style
|
||||
"\x1b[1;3D", // CSI 1;3 D - xterm with modifiers
|
||||
])
|
||||
|
||||
// Option+Right (move word right) escape sequences
|
||||
export const OPTION_RIGHT_SEQUENCES = new Set([
|
||||
"\x1bf", // Meta+f - emacs style
|
||||
"\x1b[1;3C", // CSI 1;3 C - xterm with modifiers
|
||||
])
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Hook to detect Home/End keys from raw stdin.
|
||||
*
|
||||
* Ink's useInput hook parses Home/End keys but doesn't expose them in the key object,
|
||||
* and sets input to '' for these keys (because they're in nonAlphanumericKeys).
|
||||
* This hook subscribes to raw stdin events to detect Home/End before Ink processes them.
|
||||
*/
|
||||
|
||||
import { useStdin } from "ink"
|
||||
import { useCallback, useEffect, useRef } from "react"
|
||||
|
||||
import { END_SEQUENCES, HOME_SEQUENCES } from "../constants/keyboard"
|
||||
|
||||
interface UseHomeEndKeysOptions {
|
||||
onHome: () => void
|
||||
onEnd: () => void
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to raw stdin to detect Home/End keys.
|
||||
* These keys are parsed by Ink but not exposed in useInput's key object.
|
||||
*/
|
||||
export function useHomeEndKeys({ onHome, onEnd, isActive = true }: UseHomeEndKeysOptions): void {
|
||||
// Use refs to avoid stale closure issues
|
||||
const onHomeRef = useRef(onHome)
|
||||
const onEndRef = useRef(onEnd)
|
||||
onHomeRef.current = onHome
|
||||
onEndRef.current = onEnd
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { internal_eventEmitter } = useStdin() as any
|
||||
|
||||
const handleInput = useCallback((data: Buffer | string) => {
|
||||
const s = typeof data === "string" ? data : data.toString()
|
||||
|
||||
if (HOME_SEQUENCES.has(s)) {
|
||||
onHomeRef.current()
|
||||
} else if (END_SEQUENCES.has(s)) {
|
||||
onEndRef.current()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive || !internal_eventEmitter) {
|
||||
return
|
||||
}
|
||||
|
||||
internal_eventEmitter.on("input", handleInput)
|
||||
return () => {
|
||||
internal_eventEmitter.removeListener("input", handleInput)
|
||||
}
|
||||
}, [isActive, internal_eventEmitter, handleInput])
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useStdout } from "ink"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
|
||||
/**
|
||||
@@ -36,7 +35,6 @@ import { useCallback, useEffect, useRef, useState } from "react"
|
||||
* content wrapper to force full remount.
|
||||
*/
|
||||
export function useTerminalSize() {
|
||||
const { stdout } = useStdout()
|
||||
const [size, setSize] = useState({
|
||||
columns: process.stdout.columns || 80,
|
||||
rows: process.stdout.rows || 24,
|
||||
@@ -47,10 +45,14 @@ export function useTerminalSize() {
|
||||
const refreshAfterResize = useCallback(() => {
|
||||
// Clear terminal + scrollback to wipe stale content from old width
|
||||
// \x1b[2J clears visible screen, \x1b[3J clears scrollback, \x1b[H moves cursor home
|
||||
stdout?.write("\x1b[2J\x1b[3J\x1b[H")
|
||||
// Increment key to force React remount
|
||||
setResizeKey((prev) => prev + 1)
|
||||
}, [stdout])
|
||||
// Use process.stdout directly with callback to ensure clear completes before React re-renders.
|
||||
// Without the callback, the state update can trigger a re-render that interleaves with
|
||||
// the buffered escape sequences, causing visual artifacts in the scrollback.
|
||||
process.stdout.write("\x1b[2J\x1b[3J\x1b[H", () => {
|
||||
// Increment key to force React remount only after clear is flushed
|
||||
setResizeKey((prev) => prev + 1)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
function updateSize() {
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Text input hook with cursor management and keyboard shortcut handling.
|
||||
*
|
||||
* Supports essential terminal shortcuts:
|
||||
* - Option+Left/Right: move by word (via escape sequences)
|
||||
* - Ctrl+A/E: start/end of line
|
||||
* - Ctrl+W: delete word backwards
|
||||
* - Ctrl+U: delete to start of line
|
||||
*
|
||||
* Note: Home/End keys are handled by useHomeEndKeys hook because Ink doesn't
|
||||
* expose them in useInput (it sets input='' for these keys).
|
||||
*/
|
||||
|
||||
import { useCallback, useRef, useState } from "react"
|
||||
|
||||
import { OPTION_LEFT_SEQUENCES, OPTION_RIGHT_SEQUENCES } from "../constants/keyboard"
|
||||
|
||||
/**
|
||||
* Keyboard escape sequence types for special key combinations.
|
||||
* Only includes sequences that Ink passes through in the input string.
|
||||
*/
|
||||
type KeyboardSequence =
|
||||
| "option-left" // Move word left
|
||||
| "option-right" // Move word right
|
||||
| null
|
||||
|
||||
/**
|
||||
* Parse keyboard escape sequences for special key combinations.
|
||||
* Only handles Option+arrow - Home/End are handled by useHomeEndKeys.
|
||||
*/
|
||||
function parseKeyboardSequence(input: string): KeyboardSequence {
|
||||
if (OPTION_LEFT_SEQUENCES.has(input)) {
|
||||
return "option-left"
|
||||
}
|
||||
if (OPTION_RIGHT_SEQUENCES.has(input)) {
|
||||
return "option-right"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the start of the previous word from cursor position.
|
||||
*/
|
||||
export function findWordStart(text: string, cursorPos: number): number {
|
||||
let pos = cursorPos
|
||||
// Skip whitespace before cursor
|
||||
while (pos > 0 && /\s/.test(text[pos - 1])) {
|
||||
pos--
|
||||
}
|
||||
// Skip word characters
|
||||
while (pos > 0 && !/\s/.test(text[pos - 1])) {
|
||||
pos--
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the end of the next word from cursor position.
|
||||
*/
|
||||
export function findWordEnd(text: string, cursorPos: number): number {
|
||||
let pos = cursorPos
|
||||
// Skip word characters
|
||||
while (pos < text.length && !/\s/.test(text[pos])) {
|
||||
pos++
|
||||
}
|
||||
// Skip whitespace
|
||||
while (pos < text.length && /\s/.test(text[pos])) {
|
||||
pos++
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
export interface UseTextInputReturn {
|
||||
// State
|
||||
text: string
|
||||
cursorPos: number
|
||||
|
||||
// Text manipulation
|
||||
setText: (text: string) => void
|
||||
insertText: (text: string) => void
|
||||
setCursorPos: (pos: number | ((prev: number) => number)) => void
|
||||
|
||||
// Deletion
|
||||
deleteCharBefore: () => void
|
||||
|
||||
// Keyboard shortcut handlers
|
||||
handleKeyboardSequence: (input: string) => boolean
|
||||
handleCtrlShortcut: (key: string) => boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing text input with cursor and keyboard shortcuts.
|
||||
*/
|
||||
export function useTextInput(): UseTextInputReturn {
|
||||
const [text, setTextState] = useState("")
|
||||
const [cursorPos, setCursorPosState] = useState(0)
|
||||
|
||||
// Use refs to get current values in callbacks without stale closures
|
||||
const textRef = useRef(text)
|
||||
const cursorRef = useRef(cursorPos)
|
||||
textRef.current = text
|
||||
cursorRef.current = cursorPos
|
||||
|
||||
// Text manipulation
|
||||
const setText = useCallback((newText: string) => {
|
||||
setTextState(newText)
|
||||
setCursorPosState(newText.length)
|
||||
}, [])
|
||||
|
||||
const insertText = useCallback((insertedText: string) => {
|
||||
const pos = cursorRef.current
|
||||
setTextState((prev) => prev.slice(0, pos) + insertedText + prev.slice(pos))
|
||||
setCursorPosState(pos + insertedText.length)
|
||||
}, [])
|
||||
|
||||
const setCursorPos = useCallback((pos: number | ((prev: number) => number)) => {
|
||||
setCursorPosState((prev) => {
|
||||
const newPos = typeof pos === "function" ? pos(prev) : pos
|
||||
return Math.max(0, Math.min(textRef.current.length, newPos))
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Deletion
|
||||
const deleteCharBefore = useCallback(() => {
|
||||
const pos = cursorRef.current
|
||||
if (pos > 0) {
|
||||
setTextState((prev) => prev.slice(0, pos - 1) + prev.slice(pos))
|
||||
setCursorPosState(pos - 1)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const deleteWordBefore = useCallback(() => {
|
||||
const pos = cursorRef.current
|
||||
const wordStart = findWordStart(textRef.current, pos)
|
||||
if (wordStart < pos) {
|
||||
setTextState((prev) => prev.slice(0, wordStart) + prev.slice(pos))
|
||||
setCursorPosState(wordStart)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const deleteToStart = useCallback(() => {
|
||||
const pos = cursorRef.current
|
||||
if (pos > 0) {
|
||||
setTextState((prev) => prev.slice(pos))
|
||||
setCursorPosState(0)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Cursor movement (internal, used by handlers)
|
||||
const moveToStart = useCallback(() => setCursorPosState(0), [])
|
||||
const moveToEnd = useCallback(() => setCursorPosState(textRef.current.length), [])
|
||||
const moveWordLeft = useCallback(() => setCursorPosState((pos) => findWordStart(textRef.current, pos)), [])
|
||||
const moveWordRight = useCallback(() => setCursorPosState((pos) => findWordEnd(textRef.current, pos)), [])
|
||||
|
||||
// Keyboard shortcut handlers
|
||||
const handleKeyboardSequence = useCallback(
|
||||
(input: string): boolean => {
|
||||
const seq = parseKeyboardSequence(input)
|
||||
if (!seq) return false
|
||||
|
||||
switch (seq) {
|
||||
case "option-left":
|
||||
moveWordLeft()
|
||||
return true
|
||||
case "option-right":
|
||||
moveWordRight()
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
},
|
||||
[moveWordLeft, moveWordRight],
|
||||
)
|
||||
|
||||
const handleCtrlShortcut = useCallback(
|
||||
(key: string): boolean => {
|
||||
switch (key.toLowerCase()) {
|
||||
case "a": // Ctrl+A - start of line
|
||||
moveToStart()
|
||||
return true
|
||||
case "e": // Ctrl+E - end of line
|
||||
moveToEnd()
|
||||
return true
|
||||
case "u": // Ctrl+U - delete to start
|
||||
deleteToStart()
|
||||
return true
|
||||
case "w": // Ctrl+W - delete word backwards
|
||||
deleteWordBefore()
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
},
|
||||
[moveToStart, moveToEnd, deleteToStart, deleteWordBefore],
|
||||
)
|
||||
|
||||
return {
|
||||
text,
|
||||
cursorPos,
|
||||
setText,
|
||||
insertText,
|
||||
setCursorPos,
|
||||
deleteCharBefore,
|
||||
handleKeyboardSequence,
|
||||
handleCtrlShortcut,
|
||||
}
|
||||
}
|
||||
+134
-56
@@ -22,7 +22,6 @@ import { telemetryService } from "@/services/telemetry"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Session } from "@/shared/services/Session"
|
||||
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@/shared/storage"
|
||||
import { secretStorage } from "@/shared/storage/ClineSecretStorage"
|
||||
import { version as CLI_VERSION } from "../package.json"
|
||||
import { runAcpMode } from "./acp/index.js"
|
||||
import { App } from "./components/App"
|
||||
@@ -31,13 +30,14 @@ import { createCliHostBridgeProvider } from "./controllers"
|
||||
import { CliCommentReviewController } from "./controllers/CliCommentReviewController"
|
||||
import { CliWebviewProvider } from "./controllers/CliWebviewProvider"
|
||||
import { restoreConsole } from "./utils/console"
|
||||
import { calculateRobotTopRow, queryCursorPos } from "./utils/cursor-position"
|
||||
import { printInfo, printWarning } from "./utils/display"
|
||||
import { parseImagesFromInput, processImagePaths } from "./utils/parser"
|
||||
import { CLINE_CLI_DIR, getCliBinaryPath } from "./utils/path"
|
||||
import { readStdinIfPiped } from "./utils/piped"
|
||||
import { runPlainTextTask } from "./utils/plain-text-task"
|
||||
import { checkForUpdates } from "./utils/update"
|
||||
import { applyProviderConfig } from "./utils/provider-config"
|
||||
import { getValidCliProviders, isValidCliProvider } from "./utils/providers"
|
||||
import { autoUpdateOnStartup, checkForUpdates } from "./utils/update"
|
||||
import { initializeCliContext } from "./vscode-context"
|
||||
import { CLI_LOG_FILE, shutdownEvent, window } from "./vscode-shim"
|
||||
|
||||
@@ -203,6 +203,9 @@ async function initializeCli(options: InitOptions): Promise<CliContext> {
|
||||
* Run an Ink app with proper cleanup handling
|
||||
*/
|
||||
async function runInkApp(element: React.ReactElement, cleanup: () => Promise<void>): Promise<void> {
|
||||
// Clear terminal for clean UI - robot will render at row 1
|
||||
process.stdout.write("\x1b[2J\x1b[3J\x1b[H")
|
||||
|
||||
// Note: incrementalRendering is disabled because it causes UI glitches on terminal resize.
|
||||
// Ink's incremental rendering tries to erase N lines based on previous output height,
|
||||
// but when the terminal shrinks, this leaves artifacts. Gemini CLI only enables
|
||||
@@ -274,14 +277,10 @@ async function runTask(
|
||||
const providerKey = selectedMode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const currentProvider = StateManager.get().getGlobalSettingsKey(providerKey) as ApiProvider
|
||||
|
||||
// Update the generic model ID for the current mode
|
||||
const modelKey = selectedMode === "act" ? "actModeApiModelId" : "planModeApiModelId"
|
||||
StateManager.get().setGlobalState(modelKey, options.model)
|
||||
|
||||
// Also update the provider-specific model ID key if applicable
|
||||
const providerModelKey = getProviderModelIdKey(currentProvider, selectedMode)
|
||||
if (providerModelKey) {
|
||||
StateManager.get().setGlobalState(providerModelKey, options.model)
|
||||
// Update model ID using provider-specific key (e.g., cline uses actModeOpenRouterModelId)
|
||||
const modelKey = getProviderModelIdKey(currentProvider, selectedMode)
|
||||
if (modelKey) {
|
||||
StateManager.get().setGlobalState(modelKey, options.model)
|
||||
}
|
||||
telemetryService.captureHostEvent("model_flag", options.model)
|
||||
}
|
||||
@@ -353,12 +352,6 @@ async function runTask(
|
||||
exit(success ? 0 : 1)
|
||||
}
|
||||
|
||||
// Use welcome view for consistent rendering (same as interactive mode)
|
||||
// Query cursor position BEFORE Ink mounts to know where robot will render
|
||||
const cursorPos = await queryCursorPos(process.stdin, process.stdout)
|
||||
const terminalRows = process.stdout.rows ?? 24
|
||||
const robotTopRow = calculateRobotTopRow(cursorPos, terminalRows)
|
||||
|
||||
let taskError = false
|
||||
|
||||
// Render the welcome view with optional initial prompt/images
|
||||
@@ -370,7 +363,6 @@ async function runTask(
|
||||
verbose: options.verbose,
|
||||
controller: ctx.controller,
|
||||
isRawModeSupported: checkRawModeSupport(),
|
||||
robotTopRow,
|
||||
initialPrompt: taskPrompt || undefined,
|
||||
initialImages: imageDataUrls.length > 0 ? imageDataUrls : undefined,
|
||||
onError: () => {
|
||||
@@ -473,6 +465,49 @@ async function showConfig(options: { config?: string }) {
|
||||
/**
|
||||
* Run authentication flow
|
||||
*/
|
||||
/**
|
||||
* Perform quick auth setup without UI - validates and saves configuration directly
|
||||
*/
|
||||
async function performQuickAuthSetup(
|
||||
ctx: CliContext,
|
||||
options: { provider: string; apikey: string; modelid: string; baseurl?: string },
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const { provider, apikey, modelid, baseurl } = options
|
||||
|
||||
const normalizedProvider = provider.toLowerCase().trim()
|
||||
|
||||
if (!isValidCliProvider(normalizedProvider)) {
|
||||
const validProviders = getValidCliProviders()
|
||||
return { success: false, error: `Invalid provider '${provider}'. Supported providers: ${validProviders.join(", ")}` }
|
||||
}
|
||||
|
||||
if (normalizedProvider === "bedrock") {
|
||||
return {
|
||||
success: false,
|
||||
error: "Bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup.",
|
||||
}
|
||||
}
|
||||
|
||||
if (baseurl && !["openai", "openai-native"].includes(normalizedProvider)) {
|
||||
return { success: false, error: "Base URL is only supported for OpenAI and OpenAI-compatible providers" }
|
||||
}
|
||||
|
||||
// Save configuration using shared utility
|
||||
await applyProviderConfig({
|
||||
providerId: normalizedProvider,
|
||||
apiKey: apikey,
|
||||
modelId: modelid,
|
||||
baseUrl: baseurl,
|
||||
controller: ctx.controller,
|
||||
})
|
||||
|
||||
// Mark onboarding as complete
|
||||
StateManager.get().setGlobalState("welcomeViewCompleted", true)
|
||||
await StateManager.get().flushPendingState()
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
async function runAuth(options: {
|
||||
provider?: string
|
||||
apikey?: string
|
||||
@@ -484,13 +519,34 @@ async function runAuth(options: {
|
||||
}) {
|
||||
const ctx = await initializeCli({ ...options, enableAuth: true })
|
||||
|
||||
const hasQuickSetupFlags = options.provider || options.apikey || options.modelid || options.baseurl
|
||||
const quickSetup = hasQuickSetupFlags
|
||||
? { provider: options.provider, apikey: options.apikey, modelid: options.modelid, baseurl: options.baseurl }
|
||||
: undefined
|
||||
const hasQuickSetupFlags = options.provider && options.apikey && options.modelid
|
||||
|
||||
telemetryService.captureHostEvent("auth_command", hasQuickSetupFlags ? "quick_setup" : "interactive")
|
||||
|
||||
// Quick setup mode - no UI, just save configuration and exit
|
||||
if (hasQuickSetupFlags) {
|
||||
const result = await performQuickAuthSetup(ctx, {
|
||||
provider: options.provider!,
|
||||
apikey: options.apikey!,
|
||||
modelid: options.modelid!,
|
||||
baseurl: options.baseurl,
|
||||
})
|
||||
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
|
||||
if (!result.success) {
|
||||
printWarning(result.error || "Quick setup failed")
|
||||
telemetryService.captureHostEvent("auth", "error")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
telemetryService.captureHostEvent("auth", "completed")
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Interactive mode - show Ink UI
|
||||
let authError = false
|
||||
|
||||
await runInkApp(
|
||||
@@ -506,7 +562,6 @@ async function runAuth(options: {
|
||||
telemetryService.captureHostEvent("auth", "error")
|
||||
authError = true
|
||||
},
|
||||
authQuickSetup: quickSetup,
|
||||
}),
|
||||
async () => {
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
@@ -596,44 +651,64 @@ devCommand
|
||||
})
|
||||
|
||||
/**
|
||||
* Check if the user has authentication configured.
|
||||
* Returns true if they have either:
|
||||
* - Cline provider with stored auth data
|
||||
* - OpenAI Codex provider with OAuth credentials
|
||||
* - BYO provider with an API key configured
|
||||
* Check if the user has completed onboarding (has any provider configured).
|
||||
*
|
||||
* Uses `welcomeViewCompleted` as the single source of truth, matching the VS Code extension's approach.
|
||||
* If `welcomeViewCompleted` is undefined (first run), checks if ANY provider has credentials
|
||||
* and sets the flag accordingly.
|
||||
*/
|
||||
async function isAuthConfigured(): Promise<boolean> {
|
||||
const stateManager = StateManager.get()
|
||||
const mode = stateManager.getGlobalSettingsKey("mode") as string
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const currentProvider = (stateManager.getGlobalSettingsKey(providerKey) as string) || "cline"
|
||||
|
||||
if (currentProvider === "cline") {
|
||||
// For Cline provider, check if we have stored auth data
|
||||
const authData = await secretStorage.get("cline:clineAccountId")
|
||||
return !!authData
|
||||
// Check welcomeViewCompleted first - this is the single source of truth
|
||||
const welcomeViewCompleted = stateManager.getGlobalStateKey("welcomeViewCompleted")
|
||||
if (welcomeViewCompleted !== undefined) {
|
||||
return welcomeViewCompleted
|
||||
}
|
||||
|
||||
if (currentProvider === "openai-codex") {
|
||||
// For OpenAI Codex, check if OAuth credentials are stored
|
||||
const isAuthenticated = await openAiCodexOAuthManager.isAuthenticated()
|
||||
return isAuthenticated
|
||||
}
|
||||
// welcomeViewCompleted is undefined - run migration logic to check if ANY provider has credentials
|
||||
// This mirrors the extension's migrateWelcomeViewCompleted behavior
|
||||
const hasAnyAuth = await checkAnyProviderConfigured()
|
||||
|
||||
// For BYO providers, check if the API key is configured
|
||||
const keyField = ProviderToApiKeyMap[currentProvider as keyof typeof ProviderToApiKeyMap]
|
||||
if (!keyField) {
|
||||
return false
|
||||
}
|
||||
// Set welcomeViewCompleted based on what we found
|
||||
stateManager.setGlobalState("welcomeViewCompleted", hasAnyAuth)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
const fields = Array.isArray(keyField) ? keyField : [keyField]
|
||||
for (const field of fields) {
|
||||
const value = await secretStorage.get(field)
|
||||
if (value) {
|
||||
return true
|
||||
return hasAnyAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ANY provider has valid credentials configured.
|
||||
* Used for migration when welcomeViewCompleted is undefined.
|
||||
*/
|
||||
async function checkAnyProviderConfigured(): Promise<boolean> {
|
||||
const stateManager = StateManager.get()
|
||||
const config = stateManager.getApiConfiguration() as Record<string, unknown>
|
||||
|
||||
// Check Cline account (stored as "cline:clineAccountId" in secrets, loaded into config)
|
||||
if (config["cline:clineAccountId"]) return true
|
||||
|
||||
// Check OpenAI Codex OAuth (stored in SECRETS_KEYS, loaded into config)
|
||||
if (config["openai-codex-oauth-credentials"]) return true
|
||||
|
||||
// Check all BYO provider API keys (loaded into config from secrets)
|
||||
for (const [provider, keyField] of Object.entries(ProviderToApiKeyMap)) {
|
||||
// Skip cline - already checked above with the correct key
|
||||
if (provider === "cline") continue
|
||||
|
||||
const fields = Array.isArray(keyField) ? keyField : [keyField]
|
||||
for (const field of fields) {
|
||||
if (config[field]) return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check provider-specific settings that indicate configuration
|
||||
// (for providers that don't require API keys like Bedrock with IAM, Ollama, LM Studio)
|
||||
if (config.awsRegion) return true
|
||||
if (config.vertexProjectId) return true
|
||||
if (config.ollamaBaseUrl) return true
|
||||
if (config.lmStudioBaseUrl) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -647,11 +722,6 @@ async function showWelcome(options: { verbose?: boolean; cwd?: string; config?:
|
||||
// Check if auth is configured
|
||||
const hasAuth = await isAuthConfigured()
|
||||
|
||||
// Query cursor position BEFORE Ink mounts
|
||||
const cursorPos = await queryCursorPos(process.stdin, process.stdout)
|
||||
const terminalRows = process.stdout.rows ?? 24
|
||||
const robotTopRow = calculateRobotTopRow(cursorPos, terminalRows)
|
||||
|
||||
let hadError = false
|
||||
|
||||
await runInkApp(
|
||||
@@ -661,7 +731,6 @@ async function showWelcome(options: { verbose?: boolean; cwd?: string; config?:
|
||||
verbose: options.verbose,
|
||||
controller: ctx.controller,
|
||||
isRawModeSupported: checkRawModeSupport(),
|
||||
robotTopRow,
|
||||
onWelcomeExit: () => {
|
||||
exit(0)
|
||||
},
|
||||
@@ -706,6 +775,12 @@ program
|
||||
// Always check for piped stdin content
|
||||
const stdinInput = await readStdinIfPiped()
|
||||
|
||||
// Error if stdin was piped but empty (e.g., `echo "" | cline`)
|
||||
if (stdinInput === "") {
|
||||
printWarning("Empty input received from stdin. Please provide content to process.")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// If no prompt argument, check if input is piped via stdin
|
||||
let effectivePrompt = prompt
|
||||
if (stdinInput) {
|
||||
@@ -733,5 +808,8 @@ program
|
||||
}
|
||||
})
|
||||
|
||||
// Background auto-update check (non-blocking)
|
||||
autoUpdateOnStartup(CLI_VERSION)
|
||||
|
||||
// Parse and run
|
||||
program.parse()
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Query terminal cursor position before Ink mounts
|
||||
* Must be called BEFORE render() to avoid escape sequence leaking into Ink's input handling
|
||||
*/
|
||||
|
||||
export async function queryCursorPos(
|
||||
stdin: NodeJS.ReadStream,
|
||||
stdout: NodeJS.WriteStream,
|
||||
{ timeoutMs = 75 } = {},
|
||||
): Promise<{ row: number; col: number } | null> {
|
||||
if (!stdin.isTTY || !stdout.isTTY || typeof (stdin as any).setRawMode !== "function") return null
|
||||
|
||||
const ttyIn = stdin as any as { isRaw?: boolean; setRawMode: (b: boolean) => void; on: any; off: any }
|
||||
const wasRaw = !!ttyIn.isRaw
|
||||
|
||||
ttyIn.setRawMode(true)
|
||||
stdin.resume()
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
let buf = ""
|
||||
const onData = (chunk: Buffer) => {
|
||||
buf += chunk.toString("utf8")
|
||||
const m = buf.match(/\x1b\[(\d+);(\d+)R/)
|
||||
if (!m) return
|
||||
cleanup()
|
||||
resolve({ row: Number(m[1]), col: Number(m[2]) })
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timer)
|
||||
stdin.off("data", onData)
|
||||
try {
|
||||
ttyIn.setRawMode(wasRaw)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
cleanup()
|
||||
resolve(null)
|
||||
}, timeoutMs)
|
||||
|
||||
stdin.on("data", onData)
|
||||
stdout.write("\x1b[6n") // DSR: cursor position
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate where the robot will be rendered on screen
|
||||
*/
|
||||
export function calculateRobotTopRow(cursorPos: { row: number } | null, terminalRows: number): number {
|
||||
const robotHeight = 12
|
||||
const firstFrameHeight = robotHeight + 8 // robot + welcome text + margins + input + footer
|
||||
|
||||
const startRow = cursorPos?.row ?? 1
|
||||
// If content doesn't fit below cursor, terminal scrolls
|
||||
return Math.max(1, Math.min(startRow, terminalRows - firstFrameHeight + 1))
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
import * as fs from "fs"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { anthropicDefaultModelId, geminiDefaultModelId, openAiNativeDefaultModelId } from "@/shared/api"
|
||||
import providersData from "@/shared/providers/providers.json"
|
||||
|
||||
// Import source types
|
||||
@@ -119,17 +120,17 @@ function findOpenCodeAuthPath(): string | null {
|
||||
* Map Codex key names to Cline providers
|
||||
*/
|
||||
const CODEX_KEY_MAP: Record<string, { provider: string; keyField: string; modelId?: string }> = {
|
||||
OPENAI_API_KEY: { provider: "openai-native", keyField: "openAiNativeApiKey", modelId: "gpt-4o" },
|
||||
ANTHROPIC_API_KEY: { provider: "anthropic", keyField: "apiKey", modelId: "claude-sonnet-4-20250514" },
|
||||
OPENAI_API_KEY: { provider: "openai-native", keyField: "openAiNativeApiKey", modelId: openAiNativeDefaultModelId },
|
||||
ANTHROPIC_API_KEY: { provider: "anthropic", keyField: "apiKey", modelId: anthropicDefaultModelId },
|
||||
}
|
||||
|
||||
/**
|
||||
* Map OpenCode provider IDs to Cline providers
|
||||
*/
|
||||
const OPENCODE_PROVIDER_MAP: Record<string, { provider: string; keyField: string; modelId?: string }> = {
|
||||
openai: { provider: "openai-native", keyField: "openAiNativeApiKey", modelId: "gpt-4o" },
|
||||
anthropic: { provider: "anthropic", keyField: "apiKey", modelId: "claude-sonnet-4-20250514" },
|
||||
gemini: { provider: "gemini", keyField: "geminiApiKey", modelId: "gemini-2.0-flash-001" },
|
||||
openai: { provider: "openai-native", keyField: "openAiNativeApiKey", modelId: openAiNativeDefaultModelId },
|
||||
anthropic: { provider: "anthropic", keyField: "apiKey", modelId: anthropicDefaultModelId },
|
||||
gemini: { provider: "gemini", keyField: "geminiApiKey", modelId: geminiDefaultModelId },
|
||||
mistral: { provider: "mistral", keyField: "mistralApiKey" },
|
||||
groq: { provider: "groq", keyField: "groqApiKey" },
|
||||
deepseek: { provider: "deepseek", keyField: "deepSeekApiKey" },
|
||||
|
||||
@@ -6,8 +6,12 @@
|
||||
* Check if input contains mouse escape sequences from terminal mouse tracking.
|
||||
* AsciiMotionCli enables mouse tracking which generates sequences like [<35;46;17M
|
||||
* These should be filtered out of text input handlers.
|
||||
*
|
||||
* NOTE: This must NOT filter keyboard escape sequences like Option+arrow keys.
|
||||
* Mouse sequences have specific patterns with coordinates (e.g., [<35;46;17M).
|
||||
*/
|
||||
export function isMouseEscapeSequence(input: string): boolean {
|
||||
// Mouse events look like: [<35;46;17M or contain escape characters
|
||||
return input.includes("\x1b") || input.includes("[<") || /\d+;\d+[Mm]/.test(input)
|
||||
// Mouse events look like: \x1b[<35;46;17M (SGR mouse format)
|
||||
// They contain [< followed by numbers, semicolons, and end with M or m
|
||||
return input.includes("[<") && /\[<\d+;\d+;\d+[Mm]/.test(input)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,10 @@ export async function readStdinIfPiped(): Promise<string | null> {
|
||||
|
||||
process.stdin.on("end", () => {
|
||||
clearTimeout(timeout)
|
||||
resolve(data.trim() || null)
|
||||
// Return empty string (not null) when stdin was piped but empty
|
||||
// This allows callers to distinguish between "no piped input" (null)
|
||||
// and "empty piped input" ("") for proper error handling
|
||||
resolve(data.trim())
|
||||
})
|
||||
|
||||
process.stdin.on("error", () => {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* Used by both AuthView (onboarding) and SettingsPanelContent (settings)
|
||||
*/
|
||||
|
||||
import { ProviderToApiKeyMap } from "@shared/storage"
|
||||
import type { ApiProvider } from "@shared/api"
|
||||
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@shared/storage"
|
||||
import { buildApiHandler } from "@/core/api"
|
||||
import type { Controller } from "@/core/controller"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
@@ -30,10 +31,23 @@ export async function applyProviderConfig(options: ApplyProviderConfigOptions):
|
||||
}
|
||||
|
||||
// Add model ID (use provided or fall back to default)
|
||||
// Use provider-specific model ID keys (e.g., actModeOpenRouterModelId for cline/openrouter)
|
||||
const finalModelId = modelId || getDefaultModelId(providerId)
|
||||
if (finalModelId) {
|
||||
config.actModeApiModelId = finalModelId
|
||||
config.planModeApiModelId = finalModelId
|
||||
const actModelKey = getProviderModelIdKey(providerId as ApiProvider, "act")
|
||||
const planModelKey = getProviderModelIdKey(providerId as ApiProvider, "plan")
|
||||
if (actModelKey) config[actModelKey] = finalModelId
|
||||
if (planModelKey) config[planModelKey] = finalModelId
|
||||
|
||||
// For cline/openrouter, also set model info (required for getModel() to return correct model)
|
||||
if ((providerId === "cline" || providerId === "openrouter") && controller) {
|
||||
const openRouterModels = await controller.readOpenRouterModels()
|
||||
const modelInfo = openRouterModels?.[finalModelId]
|
||||
if (modelInfo) {
|
||||
stateManager.setGlobalState("actModeOpenRouterModelInfo", modelInfo)
|
||||
stateManager.setGlobalState("planModeOpenRouterModelInfo", modelInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add API key if provided (maps to provider-specific field like anthropicApiKey, openAiApiKey, etc.)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Shared provider metadata utilities
|
||||
* Used by both UI components and CLI commands
|
||||
*/
|
||||
|
||||
import providersData from "@/shared/providers/providers.json"
|
||||
|
||||
// Create a lookup map from provider value to display label
|
||||
const providerLabels: Record<string, string> = Object.fromEntries(
|
||||
providersData.list.map((p: { value: string; label: string }) => [p.value, p.label]),
|
||||
)
|
||||
|
||||
// Get provider order from providers.json (same order as webview)
|
||||
const providerOrder: string[] = providersData.list.map((p: { value: string }) => p.value)
|
||||
|
||||
/**
|
||||
* Providers that are not supported in CLI.
|
||||
* - vscode-lm: Requires VS Code's Language Model API (see ENG-1490 for OAuth-based support)
|
||||
*/
|
||||
export const CLI_EXCLUDED_PROVIDERS = new Set<string>(["vscode-lm"])
|
||||
|
||||
/**
|
||||
* Get the display label for a provider ID
|
||||
*/
|
||||
export function getProviderLabel(providerId: string): string {
|
||||
return providerLabels[providerId] || providerId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ordered list of all provider IDs (from providers.json)
|
||||
*/
|
||||
export function getProviderOrder(): string[] {
|
||||
return providerOrder
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of valid CLI provider IDs (excluding unsupported providers)
|
||||
*/
|
||||
export function getValidCliProviders(): string[] {
|
||||
return providerOrder.filter((p) => !CLI_EXCLUDED_PROVIDERS.has(p))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider ID is valid for CLI use
|
||||
*/
|
||||
export function isValidCliProvider(providerId: string): boolean {
|
||||
return providerOrder.includes(providerId) && !CLI_EXCLUDED_PROVIDERS.has(providerId)
|
||||
}
|
||||
+215
-24
@@ -1,28 +1,175 @@
|
||||
import { spawn } from "node:child_process"
|
||||
import { realpathSync } from "node:fs"
|
||||
import { exit } from "node:process"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { printInfo, printWarning } from "./display"
|
||||
|
||||
export enum PackageManager {
|
||||
NPM = "npm",
|
||||
PNPM = "pnpm",
|
||||
YARN = "yarn",
|
||||
BUN = "bun",
|
||||
NPX = "npx",
|
||||
UNKNOWN = "unknown",
|
||||
}
|
||||
|
||||
interface InstallationInfo {
|
||||
packageManager: PackageManager
|
||||
updateCommand?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for updates and install if available
|
||||
* Check if a version string is a nightly build.
|
||||
*/
|
||||
function isNightlyVersion(version: string): boolean {
|
||||
return version.includes("-nightly.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the npm tag to use based on the current version.
|
||||
*/
|
||||
function getNpmTag(currentVersion: string): string {
|
||||
return isNightlyVersion(currentVersion) ? "nightly" : "latest"
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect how the CLI was installed and return the appropriate update command.
|
||||
* Uses the correct npm tag based on whether the current version is nightly.
|
||||
*/
|
||||
function getInstallationInfo(currentVersion: string): InstallationInfo {
|
||||
const tag = getNpmTag(currentVersion)
|
||||
|
||||
try {
|
||||
const scriptPath = realpathSync(process.argv[1] || "").replace(/\\/g, "/")
|
||||
|
||||
// npx - skip auto-update (ephemeral execution)
|
||||
if (scriptPath.includes("/.npm/_npx") || scriptPath.includes("/npm/_npx")) {
|
||||
return { packageManager: PackageManager.NPX }
|
||||
}
|
||||
|
||||
// pnpm global
|
||||
if (scriptPath.includes("/.pnpm/global") || scriptPath.includes("/pnpm/global")) {
|
||||
return {
|
||||
packageManager: PackageManager.PNPM,
|
||||
updateCommand: `pnpm add -g cline@${tag}`,
|
||||
}
|
||||
}
|
||||
|
||||
// yarn global
|
||||
if (scriptPath.includes("/.yarn/") || scriptPath.includes("/yarn/global")) {
|
||||
return {
|
||||
packageManager: PackageManager.YARN,
|
||||
updateCommand: `yarn global add cline@${tag}`,
|
||||
}
|
||||
}
|
||||
|
||||
// bun global
|
||||
if (scriptPath.includes("/.bun/bin")) {
|
||||
return {
|
||||
packageManager: PackageManager.BUN,
|
||||
updateCommand: `bun add -g cline@${tag}`,
|
||||
}
|
||||
}
|
||||
|
||||
// npm global (node_modules/cline)
|
||||
if (scriptPath.includes("/node_modules/cline/")) {
|
||||
return {
|
||||
packageManager: PackageManager.NPM,
|
||||
updateCommand: `npm install -g cline@${tag}`,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If we can't resolve the path, assume unknown
|
||||
}
|
||||
|
||||
return { packageManager: PackageManager.UNKNOWN }
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest version from npm registry.
|
||||
* Uses the appropriate tag based on whether the current version is nightly.
|
||||
*/
|
||||
async function getLatestVersion(currentVersion: string): Promise<string | null> {
|
||||
try {
|
||||
const tag = getNpmTag(currentVersion)
|
||||
const response = await fetch(`https://registry.npmjs.org/cline/${tag}`)
|
||||
if (!response.ok) return null
|
||||
const data = (await response.json()) as { version: string }
|
||||
return data.version || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-update check that runs on CLI startup.
|
||||
* Checks for updates asynchronously (non-blocking), then spawns a detached
|
||||
* process to install if a newer version is available.
|
||||
*
|
||||
* Supports npm, pnpm, yarn, and bun global installs.
|
||||
* Skipped for npx, local dev, and unknown installations.
|
||||
* Can be disabled with CLINE_NO_AUTO_UPDATE=1 environment variable.
|
||||
*/
|
||||
export function autoUpdateOnStartup(currentVersion: string): void {
|
||||
// Skip in dev mode
|
||||
if (process.env.IS_DEV === "true") {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if auto-update is disabled via env var
|
||||
if (process.env.CLINE_NO_AUTO_UPDATE === "1") {
|
||||
return
|
||||
}
|
||||
|
||||
const { updateCommand } = getInstallationInfo(currentVersion)
|
||||
if (!updateCommand) {
|
||||
return
|
||||
}
|
||||
|
||||
// Async version check - non-blocking, fire and forget
|
||||
checkAndUpdate(currentVersion, updateCommand)
|
||||
}
|
||||
|
||||
async function checkAndUpdate(currentVersion: string, updateCommand: string): Promise<void> {
|
||||
try {
|
||||
const latestVersion = await getLatestVersion(currentVersion)
|
||||
if (!latestVersion) return
|
||||
|
||||
// Only update if latest is newer
|
||||
if (compareVersions(currentVersion, latestVersion) >= 0) return
|
||||
|
||||
// Spawn detached process to run the update command
|
||||
const child = spawn(updateCommand, {
|
||||
shell: true,
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
env: process.env,
|
||||
})
|
||||
child.unref()
|
||||
} catch {
|
||||
// Silently ignore errors - auto-update is best-effort
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for updates and install if available (manual command)
|
||||
*/
|
||||
export async function checkForUpdates(currentVersion: string, options?: { verbose?: boolean }) {
|
||||
printInfo("Checking for updates...")
|
||||
|
||||
const { updateCommand, packageManager } = getInstallationInfo(currentVersion)
|
||||
|
||||
try {
|
||||
// Fetch latest version from npm registry
|
||||
const response = await fetch("https://registry.npmjs.org/cline/latest")
|
||||
if (!response.ok && response.statusText !== "OK") {
|
||||
printWarning(`Failed to check for updates: ${response.statusText}`)
|
||||
const latestVersion = await getLatestVersion(currentVersion)
|
||||
if (!latestVersion) {
|
||||
printWarning("Failed to check for updates: could not fetch latest version")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { version: string }
|
||||
const latestVersion = data.version
|
||||
|
||||
if (options?.verbose) {
|
||||
printInfo(`Current version: ${currentVersion}`)
|
||||
printInfo(`Latest version: ${latestVersion}`)
|
||||
printInfo(`Package manager: ${packageManager}`)
|
||||
}
|
||||
|
||||
// Compare versions
|
||||
@@ -38,6 +185,13 @@ export async function checkForUpdates(currentVersion: string, options?: { verbos
|
||||
}
|
||||
|
||||
printInfo(`New version available: ${latestVersion} (current: ${currentVersion})`)
|
||||
|
||||
if (!updateCommand) {
|
||||
printInfo("Unable to determine update command for your installation.")
|
||||
printInfo("Please update manually using your package manager.")
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Ask user to confirm update
|
||||
const userConfirmed = new Promise<boolean>((resolve) => {
|
||||
process.stdout.write("Do you want to update now? (y/N): ")
|
||||
@@ -52,31 +206,28 @@ export async function checkForUpdates(currentVersion: string, options?: { verbos
|
||||
exit(0)
|
||||
}
|
||||
|
||||
printInfo("Installing update...")
|
||||
printInfo(`Installing update via ${packageManager}...`)
|
||||
|
||||
// Run npm install -g cline@latest
|
||||
const npmProcess = spawn("npm", ["install", "-g", "cline@latest"], {
|
||||
const updateProcess = spawn(updateCommand, {
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
// Ensures the process uses the same environment
|
||||
env: process.env,
|
||||
detached: false,
|
||||
windowsHide: true,
|
||||
})
|
||||
|
||||
npmProcess.on("close", (code) => {
|
||||
updateProcess.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
printInfo(`Successfully updated to version ${latestVersion}`)
|
||||
exit(0)
|
||||
} else {
|
||||
printWarning("Update failed. Please try running: npm install -g cline@latest")
|
||||
printWarning(`Update failed. Please try running: ${updateCommand}`)
|
||||
exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
npmProcess.on("error", (err) => {
|
||||
printWarning(`Failed to run npm install: ${err.message}`)
|
||||
printInfo("Please try running manually: npm install -g cline@latest")
|
||||
updateProcess.on("error", (err) => {
|
||||
printWarning(`Failed to run update: ${err.message}`)
|
||||
printInfo(`Please try running manually: ${updateCommand}`)
|
||||
exit(1)
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -86,21 +237,61 @@ export async function checkForUpdates(currentVersion: string, options?: { verbos
|
||||
}
|
||||
}
|
||||
|
||||
interface ParsedVersion {
|
||||
base: number[]
|
||||
isNightly: boolean
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two semantic version strings
|
||||
* Parse a version string into its components.
|
||||
* Handles both stable versions (2.0.0) and nightly versions (2.0.0-nightly.1736365200).
|
||||
*/
|
||||
function parseVersion(version: string): ParsedVersion {
|
||||
const nightlyMatch = version.match(/^(\d+\.\d+\.\d+)-nightly\.(\d+)$/)
|
||||
if (nightlyMatch) {
|
||||
return {
|
||||
base: nightlyMatch[1].split(".").map(Number),
|
||||
isNightly: true,
|
||||
timestamp: parseInt(nightlyMatch[2], 10),
|
||||
}
|
||||
}
|
||||
return {
|
||||
base: version.split(".").map(Number),
|
||||
isNightly: false,
|
||||
timestamp: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two semantic version strings.
|
||||
* Handles both stable versions and nightly versions.
|
||||
* Nightly versions are compared by their timestamps.
|
||||
* Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
||||
*/
|
||||
function compareVersions(v1: string, v2: string): number {
|
||||
const parts1 = v1.split(".").map(Number)
|
||||
const parts2 = v2.split(".").map(Number)
|
||||
const p1 = parseVersion(v1)
|
||||
const p2 = parseVersion(v2)
|
||||
|
||||
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
||||
const part1 = parts1[i] || 0
|
||||
const part2 = parts2[i] || 0
|
||||
// Compare base versions first
|
||||
for (let i = 0; i < Math.max(p1.base.length, p2.base.length); i++) {
|
||||
const part1 = p1.base[i] || 0
|
||||
const part2 = p2.base[i] || 0
|
||||
|
||||
if (part1 > part2) return 1
|
||||
if (part1 < part2) return -1
|
||||
}
|
||||
|
||||
// Base versions are equal, check nightly status
|
||||
// Nightly is considered less than stable (it's a pre-release)
|
||||
if (p1.isNightly && !p2.isNightly) return -1
|
||||
if (!p1.isNightly && p2.isNightly) return 1
|
||||
|
||||
// Both are nightly, compare timestamps
|
||||
if (p1.isNightly && p2.isNightly) {
|
||||
if (p1.timestamp > p2.timestamp) return 1
|
||||
if (p1.timestamp < p2.timestamp) return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
+15
-1
@@ -117,7 +117,13 @@
|
||||
"features/auto-compact",
|
||||
"features/background-edit",
|
||||
"features/checkpoints",
|
||||
"features/cline-rules",
|
||||
{
|
||||
"group": "Cline Rules",
|
||||
"pages": [
|
||||
"features/cline-rules/overview",
|
||||
"features/cline-rules/conditional-rules"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Commands & Shortcuts",
|
||||
"pages": [
|
||||
@@ -424,6 +430,14 @@
|
||||
{
|
||||
"source": "/enterprise-solutions/team-management/roles-and-permissions",
|
||||
"destination": "/enterprise-solutions/team-management/managing-members"
|
||||
},
|
||||
{
|
||||
"source": "/features/cline-rules",
|
||||
"destination": "/features/cline-rules/overview"
|
||||
},
|
||||
{
|
||||
"source": "/features/conditional-rules",
|
||||
"destination": "/features/cline-rules/conditional-rules"
|
||||
}
|
||||
],
|
||||
"search": {
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
Cline Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way to include context and preferences for your projects or globally for every conversation.
|
||||
|
||||
## Creating a Rule
|
||||
|
||||
You can create a rule by clicking the `+` button in the Rules tab. This will open a new file in your IDE which you can use to write your rule.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-rules.png" alt="Create a Rule" />
|
||||
</Frame>
|
||||
|
||||
Once you save the file:
|
||||
|
||||
- Your rule will be stored in the `.clinerules/` directory in your project (if it's a Workspace Rule)
|
||||
- Or in the Global Rules directory (if it's a Global Rule):
|
||||
|
||||
### Global Rules Directory Location
|
||||
|
||||
The location of your Global Rules directory depends on your operating system:
|
||||
|
||||
| Operating System | Default Location | Notes |
|
||||
|------------------|------------------|-------|
|
||||
| **Windows** | `Documents\Cline\Rules` | Uses system Documents folder |
|
||||
| **macOS** | `~/Documents/Cline/Rules` | Uses user Documents folder |
|
||||
| **Linux/WSL** | `~/Documents/Cline/Rules` | May fall back to `~/Cline/Rules` on some systems |
|
||||
|
||||
> **Note for Linux/WSL users**: If you don't find your global rules in `~/Documents/Cline/Rules`, check `~/Cline/Rules` as the location may vary depending on your system configuration and whether the Documents directory exists.
|
||||
|
||||
You can also have Cline create a rule for you by using the [`/newrule` slash command](/features/slash-commands/new-rule) in the chat.
|
||||
|
||||
```markdown Example Cline Rule Structure [expandable]
|
||||
# Project Guidelines
|
||||
|
||||
## Documentation Requirements
|
||||
|
||||
- Update relevant documentation in /docs when modifying features
|
||||
- Keep README.md in sync with new capabilities
|
||||
- Maintain changelog entries in CHANGELOG.md
|
||||
|
||||
## Architecture Decision Records
|
||||
|
||||
Create ADRs in /docs/adr for:
|
||||
|
||||
- Major dependency changes
|
||||
- Architectural pattern changes
|
||||
- New integration patterns
|
||||
- Database schema changes
|
||||
Follow template in /docs/adr/template.md
|
||||
|
||||
## Code Style & Patterns
|
||||
|
||||
- Generate API clients using OpenAPI Generator
|
||||
- Use TypeScript axios template
|
||||
- Place generated code in /src/generated
|
||||
- Prefer composition over inheritance
|
||||
- Use repository pattern for data access
|
||||
- Follow error handling pattern in /src/utils/errors.ts
|
||||
|
||||
## Testing Standards
|
||||
|
||||
- Unit tests required for business logic
|
||||
- Integration tests for API endpoints
|
||||
- E2E tests for critical user flows
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
1. **Version Controlled**: The `.clinerules` file becomes part of your project's source code
|
||||
2. **Team Consistency**: Ensures consistent behavior across all team members
|
||||
3. **Project-Specific**: Rules and standards tailored to each project's needs
|
||||
4. **Institutional Knowledge**: Maintains project standards and practices in code
|
||||
|
||||
Place the `.clinerules` file in your project's root directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules
|
||||
├── src/
|
||||
├── docs/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline's system prompt, on the other hand, is not user-editable ([here's where you can find it](https://github.com/cline/cline/blob/main/src/core/prompts/system.ts)). For a broader look at prompt engineering best practices, check out [this resource](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview).
|
||||
|
||||
### AGENTS.md Standard Support
|
||||
|
||||
Cline also supports the [AGENTS.md](https://agents.md/) standard as a fallback
|
||||
(in addition to Cline Rules) by automatically detecting `AGENTS.md` files in
|
||||
your workspace root. This allows you to use the same rules file across different AI
|
||||
coding tools.
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── AGENTS.md
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Tips for Writing Effective Cline Rules
|
||||
|
||||
- Be Clear and Concise: Use simple language and avoid ambiguity.
|
||||
- Focus on Desired Outcomes: Describe the results you want, not the specific steps.
|
||||
- Test and Iterate: Experiment to find what works best for your workflow.
|
||||
|
||||
### .clinerules/ Folder System
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Folder containing active rules
|
||||
│ ├── 01-coding.md # Core coding standards
|
||||
│ ├── 02-documentation.md # Documentation requirements
|
||||
│ └── current-sprint.md # Rules specific to current work
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline automatically processes **all Markdown files** inside the `.clinerules/` directory, combining them into a unified set of rules. The numeric prefixes (optional) help organize files in a logical sequence.
|
||||
|
||||
#### Using a Rules Bank
|
||||
|
||||
For projects with multiple contexts or teams, maintain a rules bank directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Active rules - automatically applied
|
||||
│ ├── 01-coding.md
|
||||
│ └── client-a.md
|
||||
│
|
||||
├── clinerules-bank/ # Repository of available but inactive rules
|
||||
│ ├── clients/ # Client-specific rule sets
|
||||
│ │ ├── client-a.md
|
||||
│ │ └── client-b.md
|
||||
│ ├── frameworks/ # Framework-specific rules
|
||||
│ │ ├── react.md
|
||||
│ │ └── vue.md
|
||||
│ └── project-types/ # Project type standards
|
||||
│ ├── api-service.md
|
||||
│ └── frontend-app.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
#### Benefits of the Folder Approach
|
||||
|
||||
1. **Contextual Activation**: Copy only relevant rules from the bank to the active folder
|
||||
2. **Easier Maintenance**: Update individual rule files without affecting others
|
||||
3. **Team Flexibility**: Different team members can activate rules specific to their current task
|
||||
4. **Reduced Noise**: Keep the active ruleset focused and relevant
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
Switch between client projects:
|
||||
|
||||
```bash
|
||||
# Switch to Client B project
|
||||
rm .clinerules/client-a.md
|
||||
cp clinerules-bank/clients/client-b.md .clinerules/
|
||||
```
|
||||
|
||||
Adapt to different tech stacks:
|
||||
|
||||
```bash
|
||||
# Frontend React project
|
||||
cp clinerules-bank/frameworks/react.md .clinerules/
|
||||
```
|
||||
|
||||
#### Implementation Tips
|
||||
|
||||
- Keep individual rule files focused on specific concerns
|
||||
- Use descriptive filenames that clearly indicate the rule's purpose
|
||||
- Consider git-ignoring the active `.clinerules/` folder while tracking the `clinerules-bank/`
|
||||
- Create team scripts to quickly activate common rule combinations
|
||||
|
||||
The folder system transforms your Cline rules from a static document into a dynamic knowledge system that adapts to your team's changing contexts and requirements.
|
||||
|
||||
### Managing Rules with the Toggleable Popover
|
||||
|
||||
To make managing both single `.clinerules` files and the folder system even easier, Cline v3.13 introduces a dedicated popover UI directly accessible from the chat interface.
|
||||
|
||||
Located conveniently under the chat input field, this popover allows you to:
|
||||
|
||||
- **Instantly See Active Rules:** View which global rules (from your user settings) and workspace rules (`.clinerules` file or folder contents) are currently active.
|
||||
- **Quickly Toggle Rules:** Enable or disable specific rule files within your workspace `.clinerules/` folder with a single click. This is perfect for activating context-specific rules (like `react-rules.md` or `memory-bank.md`) only when needed.
|
||||
- **Easily Add/Manage Rules:** Quickly create a workspace `.clinerules` file or folder if one doesn't exist, or add new rule files to an existing folder.
|
||||
|
||||
This UI significantly simplifies switching contexts and managing different sets of instructions without needing to manually edit files or configurations during a conversation.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
|
||||
</Frame>
|
||||
@@ -0,0 +1,267 @@
|
||||
---
|
||||
title: "Conditional Rules"
|
||||
sidebarTitle: "Conditional Rules"
|
||||
description: "Activate rules automatically based on which files you're working with"
|
||||
---
|
||||
|
||||
Conditional rules let you scope rules to specific parts of your codebase. Rules activate only when you're working with matching files, keeping your context focused and relevant.
|
||||
|
||||
For an introduction to Cline Rules, see the [Overview](/features/cline-rules/overview).
|
||||
|
||||
- **Without conditionals**: every rule loads for every request.
|
||||
- **With conditionals**, rules activate only when your current files match their defined scope.
|
||||
|
||||
For example, React component rules should appear when you're working with React components, not when you're editing Python or documentation.
|
||||
|
||||
## How It Works
|
||||
|
||||
Conditional rules use YAML frontmatter at the top of your rule files. When Cline processes a request, it gathers context from your current work (open files, visible tabs, mentioned paths, edited files), evaluates each rule's conditions, and activates matching rules.
|
||||
|
||||
<Note>
|
||||
When a conditional rule activates, you'll see a notification: **"Conditional rules applied: workspace:frontend-rules.md"**
|
||||
</Note>
|
||||
|
||||
## Writing Conditional Rules
|
||||
|
||||
Add YAML frontmatter to the top of any rule file in your `.clinerules/` directory:
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "src/components/**"
|
||||
- "src/hooks/**"
|
||||
---
|
||||
|
||||
# React Component Guidelines
|
||||
|
||||
When creating or modifying React components:
|
||||
- Use functional components with React hooks
|
||||
- Extract reusable logic into custom React hooks
|
||||
- Keep components focused on a single responsibility
|
||||
```
|
||||
|
||||
The `---` markers delimit the frontmatter. Everything after the closing `---` is your rule content.
|
||||
|
||||
### The `paths` Conditional
|
||||
|
||||
Currently, `paths` is the supported conditional. It takes an array of glob patterns:
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "src/**" # All files under src/
|
||||
- "*.config.js" # Config files in root
|
||||
- "packages/*/src/" # Monorepo package sources
|
||||
---
|
||||
```
|
||||
|
||||
**Glob pattern syntax:**
|
||||
- `*` matches any characters except `/`
|
||||
- `**` matches any characters including `/` (recursive)
|
||||
- `?` matches a single character
|
||||
- `[abc]` matches any character in the brackets
|
||||
- `{a,b}` matches either pattern
|
||||
|
||||
**Examples:**
|
||||
|
||||
| Pattern | Matches |
|
||||
|---------|---------|
|
||||
| `src/**/*.ts` | All TypeScript files under `src/` |
|
||||
| `*.md` | Markdown files in root only |
|
||||
| `**/*.test.ts` | Test files anywhere in the project |
|
||||
| `packages/{web,api}/**` | Files in web or api packages |
|
||||
| `src/components/*.tsx` | TSX files directly in components (not nested) |
|
||||
|
||||
### Behavior Details
|
||||
|
||||
**Multiple patterns**: A rule activates if any pattern matches any file in your context.
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "frontend/**"
|
||||
- "mobile/**"
|
||||
---
|
||||
# Activates when working in frontend OR mobile
|
||||
```
|
||||
|
||||
**No frontmatter**: Rules without frontmatter are always active.
|
||||
|
||||
**Empty paths array**: `paths: []` means the rule never activates. Use this to temporarily disable a rule.
|
||||
|
||||
**Invalid YAML**: If frontmatter can't be parsed, Cline fails open: the rule activates with raw content visible to help debugging.
|
||||
|
||||
## What Counts as "Current Context"
|
||||
|
||||
Cline evaluates rules based on:
|
||||
|
||||
1. **Your message**: File paths mentioned in your prompt (e.g., "update `src/App.tsx`")
|
||||
2. **Open tabs**: Files currently open in your editor
|
||||
3. **Visible files**: Files visible in your active editor panes
|
||||
4. **Edited files**: Files Cline has created, modified, or deleted during the task
|
||||
5. **Pending operations**: Files Cline is about to edit
|
||||
|
||||
Conditional rules can activate on your first message, when relevant files are open, or mid-task when Cline starts working with matching files.
|
||||
|
||||
<Tip>
|
||||
Be explicit about file paths in your prompts. "Update `src/services/user.ts`" reliably triggers path-based rules; "update the user service" may not.
|
||||
</Tip>
|
||||
|
||||
## Practical Examples
|
||||
|
||||
Copy these patterns and adapt them to your project structure.
|
||||
|
||||
### Frontend vs Backend Rules
|
||||
|
||||
Keep frontend and backend rules separate to avoid noise. Frontend rules only load when working with UI code, backend rules only load when working with API or service code.
|
||||
|
||||
```yaml
|
||||
# .clinerules/frontend.md
|
||||
---
|
||||
paths:
|
||||
- "src/components/**"
|
||||
- "src/pages/**"
|
||||
- "src/hooks/**"
|
||||
---
|
||||
|
||||
# Frontend Guidelines
|
||||
|
||||
- Use Tailwind CSS for styling
|
||||
- Prefer server components where possible
|
||||
- Keep client components small and focused
|
||||
```
|
||||
|
||||
```yaml
|
||||
# .clinerules/backend.md
|
||||
---
|
||||
paths:
|
||||
- "src/api/**"
|
||||
- "src/services/**"
|
||||
- "src/db/**"
|
||||
---
|
||||
|
||||
# Backend Guidelines
|
||||
|
||||
- Use dependency injection for services
|
||||
- All database queries go through repositories
|
||||
- Return typed errors, not thrown exceptions
|
||||
```
|
||||
|
||||
### Test File Rules
|
||||
|
||||
Enforce testing standards automatically. This rule activates only when you're writing or modifying tests, so testing guidance appears exactly when you need it.
|
||||
|
||||
```yaml
|
||||
# .clinerules/testing.md
|
||||
---
|
||||
paths:
|
||||
- "**/*.test.ts"
|
||||
- "**/*.spec.ts"
|
||||
- "**/__tests__/**"
|
||||
---
|
||||
|
||||
# Testing Standards
|
||||
|
||||
- Use descriptive test names: "should [expected behavior] when [condition]"
|
||||
- One assertion per test when possible
|
||||
- Mock external dependencies, not internal modules
|
||||
- Use factories for test data, not fixtures
|
||||
```
|
||||
|
||||
### Documentation Rules
|
||||
|
||||
Apply documentation standards only when editing docs. Prevents style rules from cluttering your context when you're writing code.
|
||||
|
||||
```yaml
|
||||
# .clinerules/docs.md
|
||||
---
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "**/*.md"
|
||||
- "**/*.mdx"
|
||||
---
|
||||
|
||||
# Documentation Guidelines
|
||||
|
||||
- Use sentence case for headings
|
||||
- Include code examples for all features
|
||||
- Keep paragraphs short (3-4 sentences max)
|
||||
- Link to related documentation
|
||||
```
|
||||
|
||||
## Combining with Rule Toggles
|
||||
|
||||
Conditional rules work alongside the rule toggle UI. Toggle off a conditional rule to disable it entirely (it won't activate even if paths match). Toggle on to let it activate when conditions are met.
|
||||
|
||||
This provides two levels of control: manual toggles and automatic condition-based activation.
|
||||
|
||||
## Tips for Effective Conditional Rules
|
||||
|
||||
### Start Broad, Then Narrow
|
||||
|
||||
Begin with broader patterns and refine as you learn what works:
|
||||
|
||||
```yaml
|
||||
# Start here
|
||||
paths:
|
||||
- "src/**"
|
||||
|
||||
# Then narrow down
|
||||
paths:
|
||||
- "src/features/auth/**"
|
||||
```
|
||||
|
||||
### Use Descriptive Filenames
|
||||
|
||||
Name your rule files to indicate their scope:
|
||||
|
||||
```
|
||||
.clinerules/
|
||||
├── api-endpoints.md # Rules for API code
|
||||
├── database-models.md # Rules for DB layer
|
||||
├── react-components.md # Rules for React
|
||||
└── universal.md # No frontmatter = always active
|
||||
```
|
||||
|
||||
### Keep Universal Rules Separate
|
||||
|
||||
Put always-on rules (coding standards, project conventions) in files without frontmatter. Reserve conditional rules for context-specific guidance.
|
||||
|
||||
### Test Your Patterns
|
||||
|
||||
Not sure if a pattern matches? Create a simple test rule:
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "your/pattern/here/**"
|
||||
---
|
||||
|
||||
TEST: This rule should activate for your/pattern/here files.
|
||||
```
|
||||
|
||||
Then work with a file in that path and check if you see the activation notification.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Rule not activating:**
|
||||
- Check that file paths in your context match the glob pattern
|
||||
- Verify the rule is toggled on in the rules panel
|
||||
- Ensure YAML frontmatter has proper `---` delimiters
|
||||
|
||||
**Rule activating unexpectedly:**
|
||||
- Review glob patterns: `**` is recursive and may match more than intended
|
||||
- Check for open files that match the pattern
|
||||
- File paths mentioned in your message also count as context
|
||||
|
||||
**Frontmatter showing in output:**
|
||||
- YAML couldn't be parsed
|
||||
- Check for syntax errors (unquoted special characters, improper indentation)
|
||||
|
||||
## Related
|
||||
|
||||
- [Cline Rules Overview](/features/cline-rules/overview) - Complete rules system guide
|
||||
- [Skills](/features/skills) - Load instructions on demand with `/skill` command
|
||||
- [Workflows](/features/slash-commands/workflows/index) - Define explicit task automation
|
||||
- [@ Mentions](/features/at-mentions/overview) - Add files to context explicitly
|
||||
- [Understanding Context Management](/prompting/understanding-context-management) - How Cline manages context window
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: "Cline Rules"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Add persistent instructions and context to guide Cline's behavior"
|
||||
---
|
||||
|
||||
Cline Rules provide system-level guidance for your projects. Rules persist across conversations, ensuring consistent behavior without repeating instructions in every chat.
|
||||
|
||||
## How It Works
|
||||
|
||||
Rules are loaded when Cline starts a task. Here's what happens:
|
||||
|
||||
**Loading order**: Cline checks for rules in this sequence:
|
||||
1. `.clinerules/` folder (all `.md` files inside)
|
||||
2. Single `.clinerules` file
|
||||
3. `AGENTS.md` file
|
||||
|
||||
**Scope precedence**: Workspace rules override global rules when both define the same guidance.
|
||||
|
||||
**Multiple files**: When using a `.clinerules/` folder, all Markdown files are combined into one ruleset. Numeric prefixes (like `01-`, `02-`) control the order.
|
||||
|
||||
**Conditional activation**: Rules with YAML frontmatter activate only when you're working with matching files. See [Conditional Rules](/features/cline-rules/conditional-rules) for details.
|
||||
|
||||
## Supported Rule Files
|
||||
|
||||
Cline reads rules from multiple file formats in your workspace root, letting you share rules across different AI coding tools:
|
||||
|
||||
| File/Folder | Source | Notes |
|
||||
|-------------|--------|-------|
|
||||
| `.clinerules/` | Cline | Folder with `.md` files (recommended) |
|
||||
| `.cursor/rules/` | Cursor | Folder with `.mdc` files |
|
||||
| `.windsurf/rules` | Windsurf | Folder with multiple `md` files |
|
||||
| `AGENTS.md` | Universal | Follows [agents.md](https://agents.md/) standard, searched recursively |
|
||||
|
||||
Cline prioritizes `.clinerules` when present. Other formats load only if no `.clinerules` exists (except `AGENTS.md`, which always searches subdirectories). All rules appear in the Rules popover where you can toggle them.
|
||||
|
||||
## Creating Rules
|
||||
|
||||
Click the `+` button in the Rules tab to create a new rule. This opens a file in your editor where you write your guidance.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-rules.png" alt="Create a Rule" />
|
||||
</Frame>
|
||||
|
||||
When you save the file, it's stored in:
|
||||
- **Workspace rules**: `.clinerules/` in your project root
|
||||
- **Global rules**: Platform-specific location (see table below)
|
||||
|
||||
You can also use the [`/newrule` slash command](/features/slash-commands/new-rule) to have Cline generate a rule based on your description.
|
||||
|
||||
### Global Rules Location
|
||||
|
||||
| Operating System | Default Location |
|
||||
|------------------|------------------|
|
||||
| **Windows** | `Documents\Cline\Rules` |
|
||||
| **macOS** | `~/Documents/Cline/Rules` |
|
||||
| **Linux/WSL** | `~/Documents/Cline/Rules` or `~/Cline/Rules` |
|
||||
|
||||
<Note>
|
||||
Linux/WSL users: Check both locations if you don't find global rules in `~/Documents/Cline/Rules`.
|
||||
</Note>
|
||||
|
||||
## Managing Rules
|
||||
|
||||
The Rules popover (below the chat input) shows active rules and lets you toggle them on or off.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Rules Popover" />
|
||||
</Frame>
|
||||
|
||||
The popover displays:
|
||||
- **Global rules**: From your user-level Rules directory
|
||||
- **Workspace rules**: From `.clinerules/` in your project
|
||||
|
||||
Toggle any rule to enable or disable it. Disabled rules won't load, even if they match conditions.
|
||||
|
||||
## When to Use Rules
|
||||
|
||||
Rules work best for persistent project context:
|
||||
|
||||
- **Code standards**: Formatting preferences, naming conventions, project-specific patterns
|
||||
- **Documentation requirements**: Where to add docs, what format to follow
|
||||
- **Architecture decisions**: Design patterns, dependency rules, module boundaries
|
||||
- **Team conventions**: PR processes, branch naming, commit message format
|
||||
- **Technology constraints**: Required libraries, banned APIs, version requirements
|
||||
|
||||
Rules are less effective for:
|
||||
- One-time instructions (just say it in the chat)
|
||||
- Complex multi-step workflows (use [Workflows](/features/slash-commands/workflows/index) instead)
|
||||
- Dynamic decisions that depend on runtime context
|
||||
|
||||
## Example Rule
|
||||
|
||||
```markdown
|
||||
# Backend API Guidelines
|
||||
|
||||
## Route Handlers
|
||||
|
||||
- Use async/await, not callbacks
|
||||
- Validate request bodies with Zod schemas
|
||||
- Return typed errors from `src/errors.ts`
|
||||
- All routes require authentication unless in `publicRoutes` array
|
||||
|
||||
## Database Access
|
||||
|
||||
- All queries go through repository classes in `src/repositories/`
|
||||
- Use transactions for multi-table updates
|
||||
- Never expose raw database errors to clients
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit tests for business logic in `src/services/`
|
||||
- Integration tests for route handlers in `src/routes/`
|
||||
- Mock external APIs, not internal modules
|
||||
```
|
||||
|
||||
This rule provides clear, actionable guidance without explaining obvious concepts or using vague language.
|
||||
|
||||
## Using a Folder Structure
|
||||
|
||||
For projects with many rules, organize them in a `.clinerules/` folder:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/
|
||||
│ ├── 01-coding-standards.md
|
||||
│ ├── 02-documentation.md
|
||||
│ └── 03-testing.md
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline loads all Markdown files in `.clinerules/` automatically. The numeric prefixes help you control ordering, but they're optional.
|
||||
|
||||
### Organizing a Rules Bank
|
||||
|
||||
Maintain a separate folder for rules you might need but don't always want active:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Active rules
|
||||
│ ├── 01-coding.md
|
||||
│ └── client-a.md
|
||||
│
|
||||
├── clinerules-bank/ # Available but inactive
|
||||
│ ├── clients/
|
||||
│ │ ├── client-a.md
|
||||
│ │ └── client-b.md
|
||||
│ └── frameworks/
|
||||
│ ├── react.md
|
||||
│ └── vue.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
Copy files from the bank to `.clinerules/` when you need them. This keeps your active context lean while maintaining a library of reusable guidance.
|
||||
|
||||
Switch contexts with simple file operations:
|
||||
|
||||
```bash
|
||||
# Switch to Client B
|
||||
rm .clinerules/client-a.md
|
||||
cp clinerules-bank/clients/client-b.md .clinerules/
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Consider git-ignoring `.clinerules/` while tracking `clinerules-bank/` so team members can activate the rules relevant to their current work.
|
||||
</Tip>
|
||||
|
||||
## Conditional Rules
|
||||
|
||||
Scope rules to specific file patterns using YAML frontmatter. This keeps React guidance out of Python code and backend rules away from frontend work.
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "src/components/**"
|
||||
- "src/hooks/**"
|
||||
---
|
||||
|
||||
# React Guidelines
|
||||
|
||||
Use functional components with hooks. Extract reusable logic into custom hooks.
|
||||
```
|
||||
|
||||
This rule activates only when working with files matching those patterns. Read the [Conditional Rules guide](/features/cline-rules/conditional-rules) for pattern syntax, behavior details, and more examples.
|
||||
|
||||
## Tips for Effective Rules
|
||||
|
||||
**Be specific**: "Use async/await for all database calls" beats "write good async code."
|
||||
|
||||
**Show patterns**: Include file paths and real examples. "Follow the error handling in `src/utils/errors.ts`" gives Cline a concrete reference.
|
||||
|
||||
**Focus on outcomes**: Describe what you want, not step-by-step instructions. Let Cline figure out how.
|
||||
|
||||
**Test and refine**: Start with core standards. Add rules when you find yourself repeating the same feedback.
|
||||
|
||||
**Use conditional rules**: Load guidance only when relevant. This keeps context efficient and reduces noise.
|
||||
|
||||
## Related
|
||||
|
||||
- [Conditional Rules](/features/cline-rules/conditional-rules) - Activate rules based on file patterns
|
||||
- [Skills](/features/skills) - Load instructions on demand with `/skill` command
|
||||
- [Workflows](/features/slash-commands/workflows/index) - Define explicit task automation
|
||||
- [New Rule Slash Command](/features/slash-commands/new-rule) - Generate rules with AI assistance
|
||||
- [Plan and Act Mode](/features/plan-and-act) - Use different rules for planning vs execution
|
||||
@@ -8,10 +8,6 @@ Skills are modular instruction sets that extend Cline's capabilities for specifi
|
||||
|
||||
Unlike rules (which are always active), skills load on-demand. You can install dozens of skills without affecting context or performance because Cline only sees the skill name and description until it's actually needed.
|
||||
|
||||
<Note>
|
||||
Skills is an experimental feature. Enable it in Settings → Features → Enable Skills.
|
||||
</Note>
|
||||
|
||||
## Why Skills?
|
||||
|
||||
Consider how you'd onboard a new team member: you wouldn't dump every document on them at once. You'd give them a brief overview, then point them to detailed guides when they're working on specific tasks.
|
||||
|
||||
@@ -144,10 +144,6 @@ if (process.env.ERROR_SERVICE_API_KEY) {
|
||||
buildEnvVars["process.env.ERROR_SERVICE_API_KEY"] = JSON.stringify(process.env.ERROR_SERVICE_API_KEY)
|
||||
}
|
||||
|
||||
if (process.env.POSTHOG_TELEMETRY_ENABLED) {
|
||||
buildEnvVars["process.env.POSTHOG_TELEMETRY_ENABLED"] = JSON.stringify(process.env.POSTHOG_TELEMETRY_ENABLED)
|
||||
}
|
||||
|
||||
// OpenTelemetry configuration (injected at build time from GitHub secrets)
|
||||
// These provide production defaults that can be overridden at runtime via environment variables
|
||||
if (process.env.OTEL_TELEMETRY_ENABLED) {
|
||||
|
||||
Generated
+47
-81
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.53.1",
|
||||
"version": "3.56.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.53.1",
|
||||
"version": "3.56.2",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"cli"
|
||||
@@ -352,7 +352,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^2.1.3",
|
||||
"@csstools/css-color-parser": "^3.0.9",
|
||||
@@ -1343,6 +1342,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",
|
||||
@@ -2237,7 +2237,6 @@
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -2259,7 +2258,6 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -2285,7 +2283,6 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^5.1.0",
|
||||
"@csstools/css-calc": "^2.1.4"
|
||||
@@ -2893,6 +2890,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"
|
||||
@@ -3941,6 +3939,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
|
||||
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -4009,6 +4008,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"
|
||||
}
|
||||
@@ -5630,8 +5630,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -5644,8 +5643,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -5658,8 +5656,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -5672,8 +5669,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -5686,8 +5682,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -5700,8 +5695,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -5714,8 +5708,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -5728,8 +5721,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5742,8 +5734,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5756,8 +5747,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5770,8 +5760,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5784,8 +5773,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5798,8 +5786,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5812,8 +5799,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5826,8 +5812,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5840,8 +5825,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5854,8 +5838,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -5868,8 +5851,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5882,8 +5864,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5896,8 +5877,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5910,8 +5890,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5924,8 +5903,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@sap-ai-sdk/ai-api": {
|
||||
"version": "2.1.0",
|
||||
@@ -7527,6 +7505,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"
|
||||
}
|
||||
@@ -7577,6 +7556,7 @@
|
||||
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -8367,6 +8347,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"
|
||||
},
|
||||
@@ -9274,6 +9255,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -10260,7 +10242,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^3.2.0",
|
||||
"rrweb-cssom": "^0.8.0"
|
||||
@@ -10290,7 +10271,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.0.0"
|
||||
@@ -10306,7 +10286,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
@@ -10321,7 +10300,6 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -10333,7 +10311,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
@@ -10436,8 +10413,7 @@
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
@@ -10636,7 +10612,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",
|
||||
@@ -12660,7 +12637,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
@@ -12886,6 +12862,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.7.tgz",
|
||||
"integrity": "sha512-QHyxhNF5VonF5cRmdAJD/UPucB9nRx3FozWMjQrDGfBxfAL9lpyu72/MlFPgloS1TMTGsOt7YN6dTPPA6mh0Aw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
@@ -13531,8 +13508,7 @@
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
@@ -13975,6 +13951,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -14024,7 +14001,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
@@ -14066,7 +14042,6 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
@@ -14081,7 +14056,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
@@ -14096,7 +14070,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
@@ -14111,7 +14084,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
@@ -14126,7 +14098,6 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -14138,7 +14109,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
@@ -14347,6 +14317,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"
|
||||
},
|
||||
@@ -16082,8 +16053,7 @@
|
||||
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/nyc": {
|
||||
"version": "17.1.0",
|
||||
@@ -17643,7 +17613,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -17805,6 +17774,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -18260,8 +18230,7 @@
|
||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/run-applescript": {
|
||||
"version": "7.0.0",
|
||||
@@ -19605,8 +19574,7 @@
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/table": {
|
||||
"version": "6.9.0",
|
||||
@@ -19891,7 +19859,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tldts-core": "^6.1.86"
|
||||
},
|
||||
@@ -19905,8 +19872,7 @@
|
||||
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
@@ -19941,7 +19907,6 @@
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tldts": "^6.1.32"
|
||||
},
|
||||
@@ -20273,6 +20238,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -20525,6 +20491,7 @@
|
||||
"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",
|
||||
@@ -20839,7 +20806,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
@@ -21329,7 +21295,6 @@
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -21547,6 +21512,7 @@
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.53.1",
|
||||
"version": "3.56.2",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"workspaces": [
|
||||
"cli"
|
||||
@@ -433,7 +433,7 @@
|
||||
"publish:marketplace": "vsce publish --allow-package-secrets sendgrid && ovsx publish",
|
||||
"publish:marketplace:prerelease": "vsce publish --allow-package-secrets sendgrid --pre-release && ovsx publish --pre-release",
|
||||
"publish:marketplace:nightly": "node ./scripts/publish-nightly.mjs",
|
||||
"prepare": "husky",
|
||||
"prepare": "npx husky",
|
||||
"changeset": "changeset",
|
||||
"version-packages": "changeset version",
|
||||
"docs": "cd docs && npm run dev",
|
||||
|
||||
@@ -75,6 +75,19 @@ message McpResourceTemplate {
|
||||
optional string description = 4;
|
||||
}
|
||||
|
||||
message McpPromptArgument {
|
||||
string name = 1;
|
||||
optional string description = 2;
|
||||
optional bool required = 3;
|
||||
}
|
||||
|
||||
message McpPrompt {
|
||||
string name = 1;
|
||||
optional string title = 2;
|
||||
optional string description = 3;
|
||||
repeated McpPromptArgument arguments = 4;
|
||||
}
|
||||
|
||||
enum McpServerStatus {
|
||||
// Protobuf enums (in proto3) must have a zero value defined, which serves as the default if the field isn't explicitly set.
|
||||
// To align with the required nature of the TypeScript type and avoid an unnecessary UNSPECIFIED state, we map one of the existing statuses to this zero value.
|
||||
@@ -95,6 +108,7 @@ message McpServer {
|
||||
optional int32 timeout = 9;
|
||||
optional bool oauth_required = 10;
|
||||
optional string oauth_auth_status = 11;
|
||||
repeated McpPrompt prompts = 12;
|
||||
}
|
||||
|
||||
message McpServers {
|
||||
|
||||
@@ -485,6 +485,7 @@ message LiteLLMModelInfo {
|
||||
repeated ModelTier tiers = 12;
|
||||
optional double temperature = 13;
|
||||
optional ApiFormat api_format = 14;
|
||||
optional bool supports_reasoning = 15;
|
||||
}
|
||||
|
||||
// Main ApiConfiguration message
|
||||
|
||||
@@ -100,6 +100,8 @@ message Secrets {
|
||||
optional string oca_api_key = 41;
|
||||
optional string oca_refresh_token = 42;
|
||||
optional string mcp_o_auth_secrets = 43;
|
||||
optional string cline_api_key = 44;
|
||||
optional string openai_codex_oauth_credentials = 46;
|
||||
}
|
||||
|
||||
// NOTE: Add new fields under API_HANDLER_SETTINGS_FIELDS or USER_SETTINGS_FIELDS
|
||||
@@ -259,7 +261,6 @@ message Settings {
|
||||
optional bool subagents_enabled = 153;
|
||||
optional bool enable_parallel_tool_calling = 154;
|
||||
optional bool background_edit_enabled = 155;
|
||||
optional bool skills_enabled = 156;
|
||||
optional bool opt_out_of_remote_config = 157;
|
||||
optional bool open_telemetry_enabled = 158;
|
||||
optional string open_telemetry_metrics_exporter = 159;
|
||||
@@ -276,8 +277,8 @@ message Settings {
|
||||
optional int32 open_telemetry_log_batch_timeout = 170;
|
||||
optional int32 open_telemetry_log_max_queue_size = 171;
|
||||
optional bool worktrees_enabled = 172;
|
||||
map<string, string> open_ai_headers = 173;
|
||||
optional bool auto_approve_all_toggled = 174;
|
||||
map<string, string> open_ai_headers = 175;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
@@ -385,6 +386,7 @@ message UpdateTaskSettingsRequest {
|
||||
// Message for updating settings
|
||||
message UpdateSettingsRequest {
|
||||
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
|
||||
reserved 38; // was skills_enabled (removed - now always enabled)
|
||||
|
||||
Metadata metadata = 1;
|
||||
optional ModelsApiConfiguration api_configuration = 2;
|
||||
@@ -421,7 +423,6 @@ message UpdateSettingsRequest {
|
||||
optional bool enable_parallel_tool_calling = 35;
|
||||
optional bool background_edit_enabled = 36;
|
||||
optional string oca_reasoning_effort = 37;
|
||||
optional bool skills_enabled = 38;
|
||||
optional bool opt_out_of_remote_config = 39;
|
||||
optional bool worktrees_enabled = 40;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const TS_PROTO_PLUGIN = isWindows
|
||||
: require.resolve("ts-proto/protoc-gen-ts_proto")
|
||||
|
||||
const TS_PROTO_OPTIONS = [
|
||||
"env=node",
|
||||
"env=both",
|
||||
"esModuleInterop=true",
|
||||
"outputServices=generic-definitions", // output generic ServiceDefinitions
|
||||
"outputIndex=true", // output an index file for each package which exports all protos in the package.
|
||||
|
||||
@@ -19,10 +19,11 @@ const STATE_KEYS_PATH = "src/shared/storage/state-keys.ts"
|
||||
const STATE_PROTO_PATH = "proto/cline/state.proto"
|
||||
|
||||
/**
|
||||
* Convert camelCase to snake_case for proto field names
|
||||
* Convert field name to valid snake_case proto field name.
|
||||
* Handles camelCase (apiKey -> api_key) and hyphens (openai-codex -> openai_codex).
|
||||
*/
|
||||
function camelToSnake(str) {
|
||||
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
|
||||
function toProtoFieldName(str) {
|
||||
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`).replace(/-/g, "_")
|
||||
}
|
||||
|
||||
// Fields that should use int64 instead of int32
|
||||
@@ -321,7 +322,7 @@ function generateProtoMessage(messageName, fields, fieldNumbers) {
|
||||
const sortedFields = [...fields].sort((a, b) => fieldNumbers[a.name] - fieldNumbers[b.name])
|
||||
|
||||
for (const field of sortedFields) {
|
||||
const snakeName = camelToSnake(field.name)
|
||||
const snakeName = toProtoFieldName(field.name)
|
||||
const fieldNum = fieldNumbers[field.name]
|
||||
// Map types cannot have the 'optional' modifier in proto3
|
||||
const prefix = field.protoType.startsWith("map<") ? "" : "optional "
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* npm run test:e2e:build
|
||||
*
|
||||
* 2. From the repo root, start the interactive session:
|
||||
* npm run test:playwright:interactive
|
||||
* npm run test:e2e:ui
|
||||
*
|
||||
* 3. VS Code will launch with the Cline extension loaded and gRPC recording enabled.
|
||||
*
|
||||
|
||||
+49
-56
@@ -1,17 +1,13 @@
|
||||
import * as vscode from "vscode"
|
||||
import {
|
||||
cleanupMcpMarketplaceCatalogFromGlobalState,
|
||||
migrateCustomInstructionsToGlobalRules,
|
||||
migrateTaskHistoryToFile,
|
||||
migrateWelcomeViewCompleted,
|
||||
migrateWorkspaceToGlobalStorage,
|
||||
} from "./core/storage/state-migrations"
|
||||
import { WebviewProvider } from "./core/webview"
|
||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import { clearOnboardingModelsCache } from "./core/controller/models/getClineOnboardingModels"
|
||||
import { HookDiscoveryCache } from "./core/hooks/HookDiscoveryCache"
|
||||
import { HookProcessRegistry } from "./core/hooks/HookProcessRegistry"
|
||||
import { StateManager } from "./core/storage/StateManager"
|
||||
import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
|
||||
import { ExtensionRegistryInfo } from "./registry"
|
||||
@@ -22,6 +18,8 @@ import { featureFlagsService } from "./services/feature-flags"
|
||||
import { getDistinctId, initializeDistinctId } from "./services/logging/distinctId"
|
||||
import { telemetryService } from "./services/telemetry"
|
||||
import { PostHogClientProvider } from "./services/telemetry/providers/posthog/PostHogClientProvider"
|
||||
import { ClineTempManager } from "./services/temp"
|
||||
import { cleanupTestMode } from "./services/test/TestMode"
|
||||
import { ShowMessageType } from "./shared/proto/host/window"
|
||||
import { syncWorker } from "./shared/services/worker/sync"
|
||||
import { getBlobStoreSettingsFromEnv } from "./shared/services/worker/worker"
|
||||
@@ -46,66 +44,50 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
const { ClineEndpoint } = await import("./config")
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
try {
|
||||
await StateManager.initialize(context)
|
||||
} catch (error) {
|
||||
Logger.error("[Controller] CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to initialize Cline's application state. Please restart the extension.",
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize OpenAI Codex OAuth manager with extension context for secrets storage
|
||||
openAiCodexOAuthManager.initialize(context)
|
||||
|
||||
// Set the distinct ID for logging and telemetry
|
||||
await initializeDistinctId(context)
|
||||
|
||||
try {
|
||||
await StateManager.initialize(context)
|
||||
} catch (error) {
|
||||
Logger.error("[Cline] CRITICAL: Failed to initialize StateManager:", error)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to initialize storage. Please check logs for details or try restarting the client.",
|
||||
})
|
||||
}
|
||||
|
||||
// =============== External services ===============
|
||||
await ErrorService.initialize()
|
||||
// Initialize OpenAI Codex OAuth manager with extension context for secrets storage
|
||||
openAiCodexOAuthManager.initialize(context)
|
||||
// Initialize PostHog client provider (skip in self-hosted mode)
|
||||
if (!ClineEndpoint.isSelfHosted()) {
|
||||
PostHogClientProvider.getInstance()
|
||||
}
|
||||
|
||||
// Setup the external services
|
||||
await ErrorService.initialize()
|
||||
await featureFlagsService.poll(null)
|
||||
|
||||
// Migrate custom instructions to global Cline rules (one-time cleanup)
|
||||
await migrateCustomInstructionsToGlobalRules(context)
|
||||
|
||||
// Migrate welcomeViewCompleted setting based on existing API keys (one-time cleanup)
|
||||
await migrateWelcomeViewCompleted(context)
|
||||
|
||||
// Migrate workspace storage values back to global storage (reverting previous migration)
|
||||
await migrateWorkspaceToGlobalStorage(context)
|
||||
|
||||
// Ensure taskHistory.json exists and migrate legacy state (runs once)
|
||||
await migrateTaskHistoryToFile(context)
|
||||
|
||||
// Clean up MCP marketplace catalog from global state (moved to disk cache)
|
||||
await cleanupMcpMarketplaceCatalogFromGlobalState(context)
|
||||
|
||||
// Clean up orphaned file context warnings (startup cleanup)
|
||||
await FileContextTracker.cleanupOrphanedWarnings(context)
|
||||
|
||||
// =============== Webview services ===============
|
||||
const webview = HostProvider.get().createWebviewProvider()
|
||||
|
||||
await showVersionUpdateAnnouncement(context)
|
||||
|
||||
// Check if this workspace was opened from worktree quick launch
|
||||
await checkWorktreeAutoOpen(context)
|
||||
|
||||
// Initialize banner service (TEMPORARILY DISABLED - not fetching banners to prevent API hammering)
|
||||
BannerService.initialize(webview.controller)
|
||||
// DISABLED: .getActiveBanners(true)
|
||||
|
||||
const stateManager = StateManager.get()
|
||||
// Non-blocking announcement check and display
|
||||
showVersionUpdateAnnouncement(context)
|
||||
// Check if this workspace was opened from worktree quick launch
|
||||
await checkWorktreeAutoOpen(stateManager)
|
||||
|
||||
// =============== Background sync and cleanup tasks ===============
|
||||
// Use remote config blobStoreConfig if available, otherwise fall back to env vars
|
||||
const blobStoreSettings = stateManager.getRemoteConfigSettings()?.blobStoreConfig ?? getBlobStoreSettingsFromEnv()
|
||||
syncWorker().init({ ...blobStoreSettings, userDistinctId: getDistinctId() })
|
||||
// Clean up old temp files in background (non-blocking) and start periodic cleanup every 24 hours
|
||||
ClineTempManager.startPeriodicCleanup()
|
||||
// Clean up orphaned file context warnings (startup cleanup)
|
||||
FileContextTracker.cleanupOrphanedWarnings(context)
|
||||
|
||||
telemetryService.captureExtensionActivated()
|
||||
|
||||
// Use remote config blobStoreConfig if available, otherwise fall back to env vars
|
||||
const blobStoreSettings = StateManager.get().getRemoteConfigSettings()?.blobStoreConfig ?? getBlobStoreSettingsFromEnv()
|
||||
syncWorker().init({ ...blobStoreSettings, userDistinctId: getDistinctId() })
|
||||
|
||||
return webview
|
||||
}
|
||||
|
||||
@@ -145,11 +127,11 @@ async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
|
||||
* Checks if this workspace was opened from the worktree quick launch button.
|
||||
* If so, opens the Cline sidebar and clears the state.
|
||||
*/
|
||||
async function checkWorktreeAutoOpen(context: vscode.ExtensionContext): Promise<void> {
|
||||
async function checkWorktreeAutoOpen(stateManager: StateManager): Promise<void> {
|
||||
try {
|
||||
// Read directly from globalState (not StateManager cache) since this may have been
|
||||
// set by another window right before this one opened
|
||||
const worktreeAutoOpenPath = context.globalState.get<string>("worktreeAutoOpenPath")
|
||||
const worktreeAutoOpenPath = stateManager.getGlobalStateKey("worktreeAutoOpenPath")
|
||||
if (!worktreeAutoOpenPath) {
|
||||
return
|
||||
}
|
||||
@@ -165,7 +147,7 @@ async function checkWorktreeAutoOpen(context: vscode.ExtensionContext): Promise<
|
||||
// Check if current workspace matches the worktree path
|
||||
if (arePathsEqual(currentPath, worktreeAutoOpenPath)) {
|
||||
// Clear the state first to prevent re-triggering
|
||||
await context.globalState.update("worktreeAutoOpenPath", undefined)
|
||||
stateManager.setGlobalState("worktreeAutoOpenPath", undefined)
|
||||
// Open the Cline sidebar
|
||||
await HostProvider.workspace.openClineSidebarPanel({})
|
||||
}
|
||||
@@ -188,4 +170,15 @@ export async function tearDown(): Promise<void> {
|
||||
// Dispose all webview instances
|
||||
await WebviewProvider.disposeAllInstances()
|
||||
syncWorker().dispose()
|
||||
clearOnboardingModelsCache()
|
||||
|
||||
// Kill any running hook processes to prevent zombies
|
||||
await HookProcessRegistry.terminateAll()
|
||||
// Clean up hook discovery cache
|
||||
HookDiscoveryCache.getInstance().dispose()
|
||||
// Stop periodic temp file cleanup
|
||||
ClineTempManager.stopPeriodicCleanup()
|
||||
|
||||
// Clean up test mode
|
||||
cleanupTestMode()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { GenerateContentConfig, GoogleGenAI } from "@google/genai"
|
||||
import { ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { sanitizeAnthropicMessages } from "../transform/anthropic-format"
|
||||
@@ -44,6 +45,7 @@ export class AIhubmixHandler implements ApiHandler {
|
||||
baseURL: this.options.baseURL,
|
||||
defaultHeaders: {
|
||||
"APP-Code": this.options.appCode,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -64,6 +66,7 @@ export class AIhubmixHandler implements ApiHandler {
|
||||
baseURL: `${this.options.baseURL}/v1`,
|
||||
defaultHeaders: {
|
||||
"APP-Code": this.options.appCode,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -87,6 +90,7 @@ export class AIhubmixHandler implements ApiHandler {
|
||||
// @ts-expect-error
|
||||
"APP-Code": this.options.appCode,
|
||||
Authorization: `Bearer ${this.options.apiKey ?? ""}`,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
|
||||
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
|
||||
import { AnthropicModelId, anthropicDefaultModelId, anthropicModels, CLAUDE_SONNET_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
@@ -33,6 +34,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
this.client = new Anthropic({
|
||||
apiKey: this.options.apiKey,
|
||||
baseURL: this.options.anthropicBaseUrl || undefined,
|
||||
defaultHeaders: buildExternalBasicHeaders(),
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AskSageModelId, askSageDefaultModelId, askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -96,10 +97,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
// Make request to AskSage API
|
||||
const response = await fetch(`${this.apiUrl}/query`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-access-tokens": this.apiKey,
|
||||
},
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
|
||||
@@ -157,10 +155,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
try {
|
||||
const response = await fetch(`${this.apiUrl}/count-monthly-tokens`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-access-tokens": this.apiKey,
|
||||
},
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ app_name: "asksage" }),
|
||||
})
|
||||
|
||||
@@ -194,4 +189,12 @@ export class AskSageHandler implements ApiHandler {
|
||||
info: askSageModels[askSageDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
private headers() {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"x-access-tokens": this.apiKey,
|
||||
...buildExternalBasicHeaders(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BasetenModelId, basetenDefaultModelId, basetenModels, ModelInfo } from
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
@@ -34,6 +35,7 @@ export class BasetenHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://inference.baseten.co/v1",
|
||||
apiKey: this.options.basetenApiKey,
|
||||
defaultHeaders: buildExternalBasicHeaders(),
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Cerebras from "@cerebras/cerebras_cloud_sdk"
|
||||
import { CerebrasModelId, cerebrasDefaultModelId, cerebrasModels, ModelInfo } from "@shared/api"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
@@ -35,11 +36,13 @@ export class CerebrasHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
this.client = new Cerebras({
|
||||
apiKey: cleanApiKey,
|
||||
timeout: 30000, // 30 second timeout
|
||||
fetch, // Use configured fetch with proxy support
|
||||
defaultHeaders: {
|
||||
...externalHeaders,
|
||||
"X-Cerebras-3rd-Party-Integration": "cline",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -201,8 +201,10 @@ export class ClineHandler implements ApiHandler {
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
// @ts-ignore-next-line
|
||||
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
|
||||
const modelId = this.getModel().id
|
||||
const isFreeModel = ["kwaipilot/kat-coder-pro", "moonshotai/kimi-k2.5"].includes(modelId)
|
||||
|
||||
if (["kwaipilot/kat-coder-pro", "moonshotai/kimi-k2.5"].includes(this.getModel().id)) {
|
||||
if (isFreeModel) {
|
||||
totalCost = 0
|
||||
}
|
||||
|
||||
@@ -252,6 +254,14 @@ export class ClineHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
const generation = response.data
|
||||
let totalCost = generation?.total_cost || 0
|
||||
const modelId = this.getModel().id
|
||||
const isFreeModel = ["kwaipilot/kat-coder-pro", "moonshotai/kimi-k2.5"].includes(modelId)
|
||||
|
||||
if (isFreeModel) {
|
||||
totalCost = 0
|
||||
}
|
||||
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
@@ -259,7 +269,7 @@ export class ClineHandler implements ApiHandler {
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: (generation?.native_tokens_prompt || 0) - (generation?.native_tokens_cached || 0),
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
totalCost,
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
@@ -280,6 +290,11 @@ export class ClineHandler implements ApiHandler {
|
||||
if (modelId && modelInfo) {
|
||||
return { id: modelId, info: modelInfo }
|
||||
}
|
||||
// If we have a model ID but no model info (e.g., CLI featured models),
|
||||
// use the ID with default model info rather than falling back to a different model
|
||||
if (modelId) {
|
||||
return { id: modelId, info: openRouterDefaultModelInfo }
|
||||
}
|
||||
return { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ import { DeepSeekModelId, deepSeekDefaultModelId, deepSeekModels, ModelInfo } fr
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { addReasoningContent } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
@@ -33,6 +34,7 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.deepseek.com/v1",
|
||||
apiKey: this.options.deepSeekApiKey,
|
||||
defaultHeaders: buildExternalBasicHeaders(),
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -81,14 +83,10 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
|
||||
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
const convertedMessages = convertToOpenAiMessages(messages)
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = isDeepseekReasoner
|
||||
? [{ role: "system", content: systemPrompt }, ...addReasoningContent(convertedMessages, messages)]
|
||||
: [{ role: "system", content: systemPrompt }, ...convertedMessages]
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -123,10 +124,7 @@ export class DifyHandler implements ApiHandler {
|
||||
try {
|
||||
response = await fetch(fullUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
} catch (error: any) {
|
||||
@@ -438,9 +436,7 @@ export class DifyHandler implements ApiHandler {
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/files/upload`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
headers: this.headers(),
|
||||
body: formData,
|
||||
})
|
||||
|
||||
@@ -461,10 +457,7 @@ export class DifyHandler implements ApiHandler {
|
||||
async stopGeneration(taskId: string, user: string = "cline-user"): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/chat-messages/${taskId}/stop`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify({ user }),
|
||||
})
|
||||
|
||||
@@ -494,9 +487,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
headers: this.headers(),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -531,9 +522,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/conversations?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
headers: this.headers(),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -553,10 +542,7 @@ export class DifyHandler implements ApiHandler {
|
||||
async deleteConversation(conversationId: string, user: string = "cline-user"): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify({ user }),
|
||||
})
|
||||
|
||||
@@ -587,10 +573,7 @@ export class DifyHandler implements ApiHandler {
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}/name`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
@@ -623,10 +606,7 @@ export class DifyHandler implements ApiHandler {
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/messages/${messageId}/feedbacks`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
@@ -659,4 +639,19 @@ export class DifyHandler implements ApiHandler {
|
||||
this.conversationId = null
|
||||
this.currentTaskId = null
|
||||
}
|
||||
|
||||
private jsonHeaders() {
|
||||
return {
|
||||
...this.headers(),
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
}
|
||||
|
||||
private headers() {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
return {
|
||||
...externalHeaders,
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DoubaoModelId, doubaoDefaultModelId, doubaoModels, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -25,10 +25,9 @@ export class DoubaoHandler implements ApiHandler {
|
||||
throw new Error("Doubao API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
|
||||
apiKey: this.options.doubaoApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Doubao client: ${error.message}`)
|
||||
@@ -80,9 +79,9 @@ export class DoubaoHandler implements ApiHandler {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FireworksModelId, fireworksDefaultModelId, fireworksModels, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -28,10 +28,9 @@ export class FireworksHandler implements ApiHandler {
|
||||
throw new Error("Fireworks API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.fireworks.ai/inference/v1",
|
||||
apiKey: this.options.fireworksApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Fireworks client: ${error.message}`)
|
||||
@@ -88,9 +87,9 @@ export class FireworksHandler implements ApiHandler {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0, // (deepseek reports total input AND cache reads/writes, see context caching: https://api-docs.deepseek.com/guides/kv_cache) where the input tokens is the sum of the cache hits/misses, while anthropic reports them as separate tokens. This is important to know for 1) context management truncation algorithm, and 2) cost calculation (NOTE: we report both input and cache stats but for now set input price to 0 since all the cost calculation will be done using cache hits/misses)
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ThinkingLevel,
|
||||
} from "@google/genai"
|
||||
import { GeminiModelId, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -63,6 +64,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
private ensureClient(): GoogleGenAI {
|
||||
if (!this.client) {
|
||||
const options = this.options as GeminiHandlerOptions
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
|
||||
if (options.isVertex) {
|
||||
// Initialize with Vertex AI configuration
|
||||
@@ -74,6 +76,9 @@ export class GeminiHandler implements ApiHandler {
|
||||
vertexai: true,
|
||||
project,
|
||||
location,
|
||||
httpOptions: {
|
||||
headers: externalHeaders,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Gemini Vertex AI client: ${error.message}`)
|
||||
@@ -85,7 +90,12 @@ export class GeminiHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
|
||||
this.client = new GoogleGenAI({
|
||||
apiKey: options.geminiApiKey,
|
||||
httpOptions: {
|
||||
headers: externalHeaders,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Gemini client: ${error.message}`)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { GroqModelId, groqDefaultModelId, groqModels, ModelInfo } from "@shared/
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
@@ -103,6 +104,7 @@ export class GroqHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.groq.com/openai/v1",
|
||||
apiKey: this.options.groqApiKey,
|
||||
defaultHeaders: buildExternalBasicHeaders(),
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { hicapModelInfoSaneDefaults, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -29,7 +30,7 @@ export class HicapHandler implements ApiHandler {
|
||||
throw new Error("Model ID is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.hicap.ai/v2/openai",
|
||||
apiKey: this.options.hicapApiKey,
|
||||
defaultHeaders: {
|
||||
@@ -86,9 +87,8 @@ export class HicapHandler implements ApiHandler {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { HuaweiCloudMaasModelId, huaweiCloudMaasDefaultModelId, huaweiCloudMaasM
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -28,10 +28,9 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
throw new Error("Huawei Cloud MaaS API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.modelarts-maas.com/v1/",
|
||||
apiKey: this.options.huaweiCloudMaasApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Huawei Cloud MaaS client: ${error.message}`)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -32,13 +32,9 @@ export class HuggingFaceHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://router.huggingface.co/v1",
|
||||
apiKey: this.options.huggingFaceApiKey,
|
||||
defaultHeaders: {
|
||||
"User-Agent": "Cline/1.0",
|
||||
},
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Hugging Face client: ${error.message}`)
|
||||
|
||||
@@ -2,8 +2,9 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient, fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isAnthropicModelId } from "@/utils/model-utils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
@@ -64,6 +65,7 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"x-litellm-api-key": apiKey,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -78,6 +80,7 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -112,10 +115,9 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
throw new Error("LiteLLM API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
|
||||
apiKey: this.options.liteLlmApiKey || "noop",
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating LiteLLM client: ${error.message}`)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -26,11 +26,10 @@ export class LmStudioHandler implements ApiHandler {
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
// Docs on the new v0 api endpoint: https://lmstudio.ai/docs/app/api/endpoints/rest
|
||||
baseURL: new URL("api/v0", this.options.lmStudioBaseUrl || "http://localhost:1234").toString(),
|
||||
apiKey: "noop",
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating LM Studio client: ${error.message}`)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
|
||||
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { MinimaxModelId, ModelInfo, minimaxDefaultModelId, minimaxModels } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
@@ -30,12 +31,14 @@ export class MinimaxHandler implements ApiHandler {
|
||||
throw new Error("MiniMax API key is required")
|
||||
}
|
||||
try {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
this.client = new Anthropic({
|
||||
apiKey: this.options.minimaxApiKey,
|
||||
baseURL:
|
||||
this.options.minimaxApiLine === "china"
|
||||
? "https://api.minimaxi.com/anthropic"
|
||||
: "https://api.minimax.io/anthropic",
|
||||
defaultHeaders: externalHeaders,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { HTTPClient } from "@mistralai/mistralai/lib/http"
|
||||
import { Tool as MistralTool } from "@mistralai/mistralai/models/components/tool"
|
||||
import { MistralModelId, ModelInfo, mistralDefaultModelId, mistralModels } from "@shared/api"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
@@ -29,6 +30,7 @@ export class MistralHandler implements ApiHandler {
|
||||
throw new Error("Mistral API key is required")
|
||||
}
|
||||
try {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
// Create HTTP client with custom fetch for proxy support
|
||||
// The Mistral SDK's HTTPClient passes a Request object to the fetcher,
|
||||
// but we need to extract the URL and init options to pass to our fetch wrapper
|
||||
@@ -37,6 +39,11 @@ export class MistralHandler implements ApiHandler {
|
||||
fetcher: async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
// Handle both string/URL and Request object inputs
|
||||
if (input instanceof Request) {
|
||||
Object.keys(externalHeaders).forEach((key) => {
|
||||
if (!input.headers.has(key)) {
|
||||
input.headers.set(key, externalHeaders[key])
|
||||
}
|
||||
})
|
||||
return fetch(input.url, {
|
||||
method: input.method,
|
||||
headers: input.headers,
|
||||
@@ -48,7 +55,16 @@ export class MistralHandler implements ApiHandler {
|
||||
...init,
|
||||
} as RequestInit)
|
||||
}
|
||||
return fetch(input, init)
|
||||
|
||||
// Merge external headers with existing headers
|
||||
const mergedInit = {
|
||||
...init,
|
||||
headers: {
|
||||
...externalHeaders,
|
||||
...(init?.headers || {}),
|
||||
},
|
||||
}
|
||||
return fetch(input, mergedInit)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ModelInfo, MoonshotModelId, moonshotDefaultModelId, moonshotModels } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -15,6 +15,11 @@ interface MoonshotHandlerOptions extends CommonApiHandlerOptions {
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
// Enhanced usage interface to support Moonshot's cached token field
|
||||
interface MoonshotUsage extends OpenAI.CompletionUsage {
|
||||
cached_tokens?: number
|
||||
}
|
||||
|
||||
export class MoonshotHandler implements ApiHandler {
|
||||
private client: OpenAI | undefined
|
||||
|
||||
@@ -26,11 +31,10 @@ export class MoonshotHandler implements ApiHandler {
|
||||
throw new Error("Moonshot API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL:
|
||||
this.options.moonshotApiLine === "china" ? "https://api.moonshot.cn/v1" : "https://api.moonshot.ai/v1",
|
||||
apiKey: this.options.moonshotApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Moonshot client: ${error.message}`)
|
||||
@@ -52,7 +56,7 @@ export class MoonshotHandler implements ApiHandler {
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
temperature: model.info.temperature,
|
||||
max_tokens: model.info.maxTokens,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
@@ -82,10 +86,13 @@ export class MoonshotHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
const usage = chunk.usage as MoonshotUsage
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: usage.cached_tokens ?? 0,
|
||||
inputTokens: (usage.prompt_tokens || 0) - (usage.cached_tokens ?? 0),
|
||||
outputTokens: usage.completion_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type ModelInfo, type NebiusModelId, nebiusDefaultModelId, nebiusModels
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -26,10 +26,9 @@ export class NebiusHandler implements ApiHandler {
|
||||
throw new Error("Nebius API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.studio.nebius.ai/v1",
|
||||
apiKey: this.options.nebiusApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Nebius client: ${error.message}`)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ModelInfo, NousResearchModelId, nousResearchDefaultModelId, nousResearchModels } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -25,7 +26,7 @@ export class NousResearchHandler implements ApiHandler {
|
||||
throw new Error("NousResearch API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://inference-api.nousResearch.com/v1",
|
||||
apiKey: this.options.nousResearchApiKey,
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
OCI_HEADER_OPC_REQUEST_ID,
|
||||
} from "@/services/auth/oca/utils/constants"
|
||||
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { OcaModelInfo } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
@@ -35,12 +36,14 @@ export interface OcaHandlerOptions extends CommonApiHandlerOptions {
|
||||
export class OcaHandler implements ApiHandler {
|
||||
protected options: OcaHandlerOptions
|
||||
protected client: OpenAI | undefined
|
||||
protected externalHeaders: Record<string, string> = {}
|
||||
|
||||
constructor(options: OcaHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
protected initializeClient(options: OcaHandlerOptions) {
|
||||
protected initializeClient(options: OcaHandlerOptions): OpenAI {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
return new (class OCIOpenAI extends OpenAI {
|
||||
protected override async prepareOptions(opts: any): Promise<void> {
|
||||
const token = await OcaAuthService.getInstance().getAuthToken()
|
||||
@@ -50,7 +53,7 @@ export class OcaHandler implements ApiHandler {
|
||||
opts.headers ??= {}
|
||||
// OCA Headers
|
||||
const ociHeaders = await createOcaHeaders(token, options.taskId!)
|
||||
opts.headers = { ...opts.headers, ...ociHeaders }
|
||||
opts.headers = { ...opts.headers, ...externalHeaders, ...ociHeaders }
|
||||
Logger.log(`Making request with customer opc-request-id: ${opts.headers?.["opc-request-id"]}`)
|
||||
return super.prepareOptions(opts)
|
||||
}
|
||||
@@ -113,12 +116,13 @@ export class OcaHandler implements ApiHandler {
|
||||
if (!token) {
|
||||
throw new OpenAIError("Unable to handle auth, Oracle Code Assist (OCA) access token is not available")
|
||||
}
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
const ociHeaders = await createOcaHeaders(token, this.options.taskId!)
|
||||
Logger.log(`Making calculate cost request with customer opc-request-id: ${ociHeaders["opc-request-id"]}`)
|
||||
try {
|
||||
const response = await fetch(`${client.baseURL}/spend/calculate`, {
|
||||
method: "POST",
|
||||
headers: ociHeaders,
|
||||
headers: { ...externalHeaders, ...ociHeaders },
|
||||
body: JSON.stringify({
|
||||
completion_response: {
|
||||
model: modelId,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import { type Config, type Message, Ollama } from "ollama"
|
||||
import type { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -7,6 +9,7 @@ import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOllamaMessages } from "../transform/ollama-format"
|
||||
import type { ApiStream } from "../transform/stream"
|
||||
import { ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface OllamaHandlerOptions extends CommonApiHandlerOptions {
|
||||
ollamaBaseUrl?: string
|
||||
@@ -30,14 +33,17 @@ export class OllamaHandler implements ApiHandler {
|
||||
private ensureClient(): Ollama {
|
||||
if (!this.client) {
|
||||
try {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
const clientOptions: Partial<Config> = {
|
||||
host: this.options.ollamaBaseUrl,
|
||||
fetch,
|
||||
headers: externalHeaders,
|
||||
}
|
||||
|
||||
// Add API key if provided (for Ollama cloud or authenticated instances)
|
||||
if (this.options.ollamaApiKey) {
|
||||
clientOptions.headers = {
|
||||
...clientOptions.headers,
|
||||
Authorization: `Bearer ${this.options.ollamaApiKey}`,
|
||||
}
|
||||
}
|
||||
@@ -51,7 +57,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry({ retryAllErrors: true })
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
|
||||
|
||||
@@ -70,20 +76,44 @@ export class OllamaHandler implements ApiHandler {
|
||||
options: {
|
||||
num_ctx: Number(this.options.ollamaApiOptionsCtxNum),
|
||||
},
|
||||
tools: tools as any,
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
// Race the API request against the timeout
|
||||
const stream = (await Promise.race([apiPromise, timeoutPromise])) as Awaited<typeof apiPromise>
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
if (typeof chunk.message.content === "string") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.message.content,
|
||||
}
|
||||
Logger.debug("[OllamaHandler] Message Chunk" + JSON.stringify(chunk))
|
||||
|
||||
const delta = chunk.message
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
Logger.debug(`[OllamaHandler] Tool Calls Detected: ${JSON.stringify(delta.tool_calls)}`)
|
||||
yield* toolCallProcessor.processToolCallDeltas(
|
||||
delta.tool_calls?.map((tc, inx) => ({
|
||||
index: inx,
|
||||
id: `ollama-tool-${inx}`,
|
||||
function: {
|
||||
name: tc.function.name,
|
||||
arguments:
|
||||
typeof tc.function.arguments === "string"
|
||||
? tc.function.arguments
|
||||
: JSON.stringify(tc.function.arguments),
|
||||
},
|
||||
type: "function",
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof delta.content === "string") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
// Handle token usage if available
|
||||
if (chunk.eval_count !== undefined || chunk.prompt_eval_count !== undefined) {
|
||||
yield {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import * as os from "os"
|
||||
import { v7 as uuidv7 } from "uuid"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
@@ -184,6 +185,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
session_id: this.sessionId,
|
||||
"User-Agent": `cline/${process.env.npm_package_version || "1.0.0"} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`,
|
||||
...(accountId ? { "ChatGPT-Account-Id": accountId } : {}),
|
||||
...buildExternalBasicHeaders(),
|
||||
}
|
||||
|
||||
// Try using OpenAI SDK first
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user