Compare commits

..

1 Commits

Author SHA1 Message Date
celestial-vault b36fe247fd add dev command for cline-core changes for cli 2025-10-04 10:16:12 -07:00
151 changed files with 1225 additions and 13199 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Added updateApiConfigurationPartial with FieldMask to allow for partial ApiProvider updates
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Added checkpoints warning when users start a multiroot task
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add auto-retry with exponential backof for failed API requests
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Added markdown support to focus chain text, allowing the model to display more interesting focus chains
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add interactive provider configuration wizard with add/list capabilities, support for 8 API providers (Anthropic, OpenAI, OpenAI Native, OpenRouter, X AI, AWS Bedrock, Google Gemini, Ollama), and UpdateSettings gRPC implementation for persisting configurations to Cline Core state.
-48
View File
@@ -1,48 +0,0 @@
# Cline Development Environment Variables
# Copy this file to .env and fill in your actual values
# Values should be obtained from 1Password shared vault for development
# ============================================================================
# DEVELOPMENT FLAGS
# Recomend not changing these unless you know what you're doing they are set by the launch.json normally
# ============================================================================
# IS_DEV=true
# CLINE_ENVIRONMENT=local
# ============================================================================
# POSTHOG TELEMETRY (Existing)
# ============================================================================
# Get these values from 1Password shared vault
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
# ============================================================================
# OPTIONAL DEVELOPMENT SETTINGS
# ============================================================================
# Uncomment and modify as needed for development
# Multi-root workspace debugging
# MULTI_ROOT_TRACE=true
# gRPC recorder for testing
# GRPC_RECORDER_ENABLED=true
# GRPC_RECORDER_FILE_NAME=test-recording
# Test mode
# E2E_TEST=true
# IS_TEST=true
# ============================================================================
# USAGE INSTRUCTIONS
# ============================================================================
# 1. Copy this file: cp .env.example .env
# 2. Get PostHog keys from 1Password shared vault
# 3. Update the values in .env
# 4. The .env file is gitignored for security
-1
View File
@@ -1,4 +1,3 @@
/docs/
/.github/ @saoudrizwan @garoth @sjf
/README.md @saoudrizwan @nickbaumann98
/src/core/storage/ @celestial-vault
-1
View File
@@ -14,7 +14,6 @@ body:
options:
- VSCode Extension
- JetBrains Plugin
- CLI
default: 0
validations:
required: true
@@ -1,53 +0,0 @@
name: Auto-label Issues
on:
issues:
types: [opened, edited]
jobs:
label:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/github-script@v7
with:
script: |
const body = context.payload.issue.body || '';
const labels = context.payload.issue.labels.map(l => l.name);
// Check if JetBrains Plugin is selected
if (body.match(/###\s*Plugin Type\s*\n+JetBrains Plugin/i)) {
if (!labels.includes('JetBrains')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['JetBrains']
});
}
}
// Check if VSCode Extension is selected
if (body.match(/###\s*Plugin Type\s*\n+VSCode Extension/i)) {
if (!labels.includes('VS Code')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['VS Code']
});
}
}
// Check if CLI is selected
if (body.match(/###\s*Plugin Type\s*\n+CLI/i)) {
if (!labels.includes('CLI')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['CLI']
});
}
}
-179
View File
@@ -1,179 +0,0 @@
name: Release Standalone CLI
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., v3.32.6)'
required: true
type: string
permissions:
contents: write
jobs:
build:
name: Build ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: macos-13
platform: darwin-x64
arch: x64
- os: macos-14
platform: darwin-arm64
arch: arm64
- os: ubuntu-latest
platform: linux-x64
arch: x64
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache-dependency-path: cli/go.sum
- name: Install dependencies
run: npm ci
- name: Install webview dependencies
run: cd webview-ui && npm ci
- name: Download Node.js binaries
run: npm run download-node
- name: Build CLI binaries
run: npm run compile-cli
- name: Build standalone CLI package
run: npm run compile-standalone-cli
env:
NODE_ENV: production
- name: Get version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
else
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi
- name: Rename package
run: |
cd dist-standalone
mv standalone-cli.zip cline-${{ steps.version.outputs.version }}-${{ matrix.platform }}.tar.gz
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: cline-${{ matrix.platform }}
path: dist-standalone/cline-${{ steps.version.outputs.version }}-${{ matrix.platform }}.tar.gz
retention-days: 1
release:
name: Create Release
needs: build
runs-on: ubuntu-latest
environment: publish
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Get version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
else
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi
- name: Display structure
run: ls -R artifacts/
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.version }}
name: Cline CLI ${{ steps.version.outputs.version }}
draft: false
prerelease: false
generate_release_notes: true
files: |
artifacts/cline-darwin-x64/cline-${{ steps.version.outputs.version }}-darwin-x64.tar.gz
artifacts/cline-darwin-arm64/cline-${{ steps.version.outputs.version }}-darwin-arm64.tar.gz
artifacts/cline-linux-x64/cline-${{ steps.version.outputs.version }}-linux-x64.tar.gz
body: |
## Installation
Install Cline CLI with a single command:
```bash
curl -fsSL https://raw.githubusercontent.com/cline/cline/main/scripts/install.sh | bash
```
### Platform-Specific Downloads
- **macOS (Intel)**: `cline-${{ steps.version.outputs.version }}-darwin-x64.tar.gz`
- **macOS (Apple Silicon)**: `cline-${{ steps.version.outputs.version }}-darwin-arm64.tar.gz`
- **Linux (x64)**: `cline-${{ steps.version.outputs.version }}-linux-x64.tar.gz`
### Manual Installation
1. Download the appropriate package for your platform
2. Extract: `tar -xzf cline-*.tar.gz`
3. Move to installation directory: `mv cline-* ~/.cline`
4. Add to PATH: `export PATH="$HOME/.cline/bin:$PATH"`
### What's Included
- ✅ Node.js v22.15.0 (bundled)
- ✅ Cline CLI binary
- ✅ Cline Host bridge
- ✅ Cline Core (TypeScript compiled)
- ✅ All dependencies
### Getting Started
```bash
# Verify installation
cline version
# Sign in
cline auth login
# Get help
cline --help
```
### Documentation
- [Installation Guide](https://docs.cline.bot/getting-started/installing-cline)
- [CLI Documentation](https://docs.cline.bot/exploring-clines-tools/cline-tools-guide)
- [GitHub Repository](https://github.com/cline/cline)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
+2 -17
View File
@@ -187,20 +187,8 @@ jobs:
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache-dependency-path: cli/go.sum
- name: Download Node.js binaries
run: npm run download-node
- name: Build CLI binaries
run: npm run compile-cli
- name: Compile standalone CLI
run: npm run compile-standalone-cli
- name: Compile standalone
run: npm run compile-standalone
- name: Install testing platform dependencies
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
@@ -256,14 +244,11 @@ jobs:
- name: Download test platform integration core coverage artifact
uses: actions/download-artifact@v4
continue-on-error: true
id: download-integration-coverage
with:
name: test-platform-integration-core-coverage
path: integration-core-coverage-reports
- name: Upload core integration tests coverage to Qlty
if: steps.download-integration-coverage.outcome == 'success'
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
-1
View File
@@ -26,7 +26,6 @@ coverage-unit
!.github/scripts/coverage/
*evals.env
.env
## Generated files ##
src/generated/
-6
View File
@@ -19,7 +19,6 @@
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
@@ -40,7 +39,6 @@
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
@@ -61,7 +59,6 @@
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
@@ -87,7 +84,6 @@
"preLaunchTask": "clean-tmp-user",
"internalConsoleOptions": "openOnSessionStart",
"postDebugTask": "stop",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"TEMP_PROFILE": "true",
@@ -118,7 +114,6 @@
"tsx"
],
"program": "scripts/test-standalone-core-api-server.ts",
"envFile": "${workspaceFolder}/.env",
"env": {
"PROTOBUS_PORT": "26040",
"HOSTBRIDGE_PORT": "26041",
@@ -156,7 +151,6 @@
"--exit",
"${file}"
],
"envFile": "${workspaceFolder}/.env",
"env": {
"TS_NODE_PROJECT": "./tsconfig.unit-test.json",
"NODE_ENV": "test",
+1 -7
View File
@@ -1,16 +1,10 @@
# Changelog
## [3.32.7]
- Add JP and Global inference profile options to AWS Bedrock
- Adding Improvements to VSCode multi root workspaces
- Added markdown support to focus chain text, allowing the model to display more interesting focus chains
## [3.32.6]
- Add experimental support for VSCode multi root workspaces
- Add Claude Sonnet 4.5 to Claude Code provider
- Add Glm 4.6 to Z AI provider
- Add Glm 4.6 to Z AI provider
## [3.32.5]
+4 -3
View File
@@ -13,6 +13,7 @@ import (
var (
coreAddress string
cfgFile string
verbose bool
outputFormat string
)
@@ -31,6 +32,7 @@ monitoring capabilities from the terminal.`,
}
return global.InitializeGlobalConfig(&global.GlobalConfig{
ConfigPath: cfgFile,
Verbose: verbose,
OutputFormat: outputFormat,
CoreAddress: coreAddress,
@@ -39,18 +41,17 @@ monitoring capabilities from the terminal.`,
}
rootCmd.PersistentFlags().StringVar(&coreAddress, "address", fmt.Sprintf("localhost:%d", common.DEFAULT_CLINE_CORE_PORT), "Cline Core gRPC address")
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cline/config.yaml)")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "o", "rich", "output format (rich|json|plain)")
rootCmd.AddCommand(cli.NewTaskCommand())
rootCmd.AddCommand(cli.NewInstanceCommand())
rootCmd.AddCommand(cli.NewConfigCommand())
rootCmd.AddCommand(cli.NewVersionCommand())
rootCmd.AddCommand(cli.NewAuthCommand())
rootCmd.AddCommand(cli.NewTaskSendCommand())
rootCmd.AddCommand(cli.NewConfigCommand())
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
+1 -37
View File
@@ -3,56 +3,20 @@ module github.com/cline/cli
go 1.23.0
require (
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/huh v0.7.0
github.com/cline/grpc-go v0.0.0
github.com/mattn/go-sqlite3 v1.14.24
github.com/spf13/cobra v1.8.0
golang.org/x/term v0.32.0
google.golang.org/grpc v1.75.0
google.golang.org/protobuf v1.36.6
)
replace github.com/cline/grpc-go => ../src/generated/grpc-go
require (
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.0 // indirect
github.com/charmbracelet/bubbletea v1.3.4 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
-102
View File
@@ -1,62 +1,4 @@
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc=
github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -67,51 +9,15 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
@@ -124,18 +30,10 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+110 -2
View File
@@ -1,17 +1,125 @@
package cli
import (
"github.com/cline/cli/pkg/cli/auth"
"bufio"
"context"
"fmt"
"os"
"strings"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/grpc-go/cline"
"github.com/spf13/cobra"
)
var isSessionAuthenticated bool
func NewAuthCommand() *cobra.Command {
return &cobra.Command{
Use: "auth",
Short: "Sign in to Cline",
Long: `Complete the authentication flow in browser to sign in to Cline.`,
RunE: func(cmd *cobra.Command, args []string) error {
return auth.HandleAuthCommand(cmd.Context(), args)
return handleAuthCommand(cmd.Context())
},
}
}
func handleAuthCommand(ctx context.Context) error {
fmt.Print("Authenticating with Cline...\n")
if IsAuthenticated(ctx) {
return signOutDialog(ctx)
}
if err := signIn(ctx); err != nil {
return err
}
fmt.Println("You are signed in!")
return nil
}
func signOut(ctx context.Context) error {
client, err := global.GetDefaultClient(ctx)
if err != nil {
return err
}
if _, err = client.Account.AccountLogoutClicked(ctx, &cline.EmptyRequest{}); err != nil {
return err
}
isSessionAuthenticated = false
fmt.Println("You have been signed out of Cline.")
return nil
}
func signOutDialog(ctx context.Context) error {
fmt.Print("You are already signed in to Cline.\nWould you like to sign out? (y/N): ")
scanner := bufio.NewScanner(os.Stdin)
if !scanner.Scan() {
return nil
}
response := strings.ToLower(strings.TrimSpace(scanner.Text()))
if response == "y" || response == "yes" {
if err := signOut(ctx); err != nil {
fmt.Printf("Failed to sign out: %v\n", err)
return err
}
}
return nil
}
func signIn(ctx context.Context) error {
if IsAuthenticated(ctx) {
return nil
}
verboseLog("Ensuring default instance exists...")
if err := ensureDefaultInstance(ctx); err != nil {
verboseLog("Failed to ensure default instance: %v", err)
return err
}
verboseLog("Default instance ensured successfully.")
time.Sleep(2 * time.Second) // Allow services to start
client, err := global.GetDefaultClient(ctx)
if err != nil {
verboseLog("Failed to obtain client: %v", err)
return err
}
_, err = client.Account.AccountLoginClicked(ctx, &cline.EmptyRequest{})
if err != nil {
verboseLog("Failed to login: %v", err)
return err
}
isSessionAuthenticated = true
verboseLog("Login successful")
return nil
}
func IsAuthenticated(ctx context.Context) bool {
if isSessionAuthenticated {
return true
}
client, err := global.GetDefaultClient(ctx)
if err != nil {
return false
}
_, err = client.Account.GetUserCredits(ctx, &cline.EmptyRequest{})
return err == nil
}
func verboseLog(format string, args ...interface{}) {
if global.Config != nil && global.Config.Verbose {
fmt.Printf("[VERBOSE] "+format+"\n", args...)
}
}
-197
View File
@@ -1,197 +0,0 @@
package auth
import (
"context"
"fmt"
"time"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
var isSessionAuthenticated bool
// Cline provider specific code
func HandleClineAuth(ctx context.Context) error {
fmt.Println("Authenticating with Cline...")
// Check if already authenticated
if IsAuthenticated(ctx) {
return signOutDialog(ctx)
}
// Perform sign in
if err := signIn(ctx); err != nil {
return err
}
fmt.Println("✓ You are signed in!")
// Configure default Cline model after successful authentication
if err := configureDefaultClineModel(ctx); err != nil {
fmt.Printf("Warning: Could not configure default Cline model: %v\n", err)
fmt.Println("You can configure a model later with 'cline auth' and selecting 'Change Cline model'")
}
// Return to main auth menu after successful authentication
return HandleAuthMenuNoArgs(ctx)
}
func signOut(ctx context.Context) error {
client, err := global.GetDefaultClient(ctx)
if err != nil {
return err
}
if _, err = client.Account.AccountLogoutClicked(ctx, &cline.EmptyRequest{}); err != nil {
return err
}
isSessionAuthenticated = false
fmt.Println("You have been signed out of Cline.")
return nil
}
func signOutDialog(ctx context.Context) error {
var confirm bool
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("You are already signed in to Cline.").
Description("Would you like to sign out?").
Value(&confirm),
),
)
if err := form.Run(); err != nil {
return nil
}
if confirm {
if err := signOut(ctx); err != nil {
fmt.Printf("Failed to sign out: %v\n", err)
return err
}
}
return HandleAuthMenuNoArgs(ctx)
}
func signIn(ctx context.Context) error {
if IsAuthenticated(ctx) {
return nil
}
verboseLog("Ensuring default instance exists...")
if err := global.EnsureDefaultInstance(ctx); err != nil {
verboseLog("Failed to ensure default instance: %v", err)
return fmt.Errorf("failed to ensure default instance: %w", err)
}
verboseLog("Default instance ensured successfully.")
time.Sleep(2 * time.Second) // Allow services to start
// Subscribe to auth updates before initiating login
verboseLog("Subscribing to auth status updates...")
listener, err := NewAuthStatusListener(ctx)
if err != nil {
verboseLog("Failed to subscribe to auth updates: %v", err)
return fmt.Errorf("failed to subscribe to auth updates: %w", err)
}
defer listener.Stop()
if err := listener.Start(); err != nil {
verboseLog("Failed to start auth listener: %v", err)
return fmt.Errorf("failed to start auth listener: %w", err)
}
// Initiate login (opens browser with callback URL from cline-core's AuthHandler)
verboseLog("Initiating login...")
client, err := global.GetDefaultClient(ctx)
if err != nil {
verboseLog("Failed to obtain client: %v", err)
return fmt.Errorf("failed to obtain client: %w", err)
}
_, err = client.Account.AccountLoginClicked(ctx, &cline.EmptyRequest{})
if err != nil {
verboseLog("Failed to initiate login: %v", err)
return fmt.Errorf("failed to initiate login: %w", err)
}
fmt.Println("\n Opening browser for authentication...")
fmt.Println(" Waiting for you to complete authentication in your browser...")
fmt.Println(" (This may take a few moments. Timeout: 5 minutes)")
// Wait for auth status update confirming success
verboseLog("Waiting for authentication to complete...")
if err := listener.WaitForAuthentication(5 * time.Minute); err != nil {
verboseLog("Authentication failed or timed out: %v", err)
fmt.Println("\n Authentication failed or timed out.")
fmt.Println(" Please try again with 'cline auth'")
return err
}
// Only NOW set the session flag after confirmed authentication
isSessionAuthenticated = true
verboseLog("Login successful")
return nil
}
func IsAuthenticated(ctx context.Context) bool {
if isSessionAuthenticated {
verboseLog("Session is already authenticated")
return true
}
verboseLog("Verifying authentication with server...")
client, err := global.GetDefaultClient(ctx)
if err != nil {
verboseLog("Failed to get client for auth check: %v", err)
return false
}
_, err = client.Account.GetUserCredits(ctx, &cline.EmptyRequest{})
if err == nil {
// Update session variable for future fast-path checks
verboseLog("Server verification successful, updating session flag")
isSessionAuthenticated = true
return true
}
verboseLog("Server verification failed: %v", err)
return false
}
// HandleChangeClineModel allows Cline-authenticated users to change their Cline model selection. Hidden when not authenticated.
func HandleChangeClineModel(ctx context.Context) error {
// Ensure user is authenticated
if !IsAuthenticated(ctx) {
return fmt.Errorf("you must be authenticated with Cline to change models. Run 'cline auth' to sign in")
}
// Get task manager
manager, err := createTaskManager(ctx)
if err != nil {
return fmt.Errorf("failed to create task manager: %w", err)
}
// Launch Cline model selection
return SelectClineModel(ctx, manager)
}
// configureDefaultClineModel configures the default Cline model after authentication
func configureDefaultClineModel(ctx context.Context) error {
verboseLog("Configuring default Cline model...")
// Create task manager
manager, err := task.NewManagerForDefault(ctx)
if err != nil {
return fmt.Errorf("failed to create task manager: %w", err)
}
// Set default Cline model
return SetDefaultClineModel(ctx, manager)
}
-241
View File
@@ -1,241 +0,0 @@
package auth
import (
"context"
"fmt"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// AuthAction represents the type of authentication action
type AuthAction string
const (
AuthActionClineLogin AuthAction = "cline_login"
AuthActionBYOSetup AuthAction = "provider_setup"
AuthActionChangeClineModel AuthAction = "change_cline_model"
AuthActionSelectProvider AuthAction = "select_provider"
AuthActionExit AuthAction = "exit_wizard"
)
// Cline Auth Menu
// Example Layout
//
// ┃ Cline Account: <authenticated/not authenticated>
// ┃ Active Provider: <provider name or none configured>
// ┃ Active Model: <model name or none configured>
// ┃
// ┃ What would you like to do?
// ┃ Change Cline model (only if authenticated) - hidden if not authenticated
// ┃ Authenticate with Cline account / Sign out of Cline - changes based on auth status
// ┃ Select active provider (Cline or BYO) - always shown. Used to switch between Cline and BYO providers
// ┃ Configure API provider - always shown. Launches provider setup wizard
// ┃ Exit authorization wizard - always shown. Exits the auth menu
// Main entry point for handling the `cline auth` command
// HandleAuthCommand routes the auth command based on the number of arguments
func HandleAuthCommand(ctx context.Context, args []string) error {
switch len(args) {
case 0:
// No args: Show menu (ShowAuthMenuNoArgs)
return HandleAuthMenuNoArgs(ctx)
case 1:
// One arg: Provider ID only, prompt for API key
return QuickAPISetup(args[0], "")
case 2:
// Two args: Provider ID and API key
return QuickAPISetup(args[0], args[1])
default:
return fmt.Errorf("quick BYO API setup is currently stubbed - not yet implemented")
}
}
// HandleAuthMenuNoArgs prepares the auth menu when no arguments are provided
func HandleAuthMenuNoArgs(ctx context.Context) error {
// Check if Cline is authenticated
isClineAuth := IsAuthenticated(ctx)
// Get current provider config for display
var currentProvider string
var currentModel string
if manager, err := createTaskManager(ctx); err == nil {
if providerList, err := GetProviderConfigurations(ctx, manager); err == nil {
if providerList.ActProvider != nil {
currentProvider = getProviderDisplayName(providerList.ActProvider.Provider)
currentModel = providerList.ActProvider.ModelID
}
}
}
action, err := ShowAuthMenuWithStatus(isClineAuth, currentProvider, currentModel)
if err != nil {
return err
}
switch action {
case AuthActionClineLogin:
return HandleClineAuth(ctx)
case AuthActionBYOSetup:
return HandleAPIProviderSetup(ctx)
case AuthActionChangeClineModel:
return HandleChangeClineModel(ctx)
case AuthActionSelectProvider:
return HandleSelectProvider(ctx)
case AuthActionExit:
return nil
default:
return fmt.Errorf("invalid action")
}
}
// ShowAuthMenuWithStatus displays the main auth menu with Cline + provider status
func ShowAuthMenuWithStatus(isClineAuthenticated bool, currentProvider, currentModel string) (AuthAction, error) {
var action AuthAction
var options []huh.Option[AuthAction]
// Build menu options based on authentication status
if isClineAuthenticated {
options = []huh.Option[AuthAction]{
huh.NewOption("Change Cline model", AuthActionChangeClineModel),
huh.NewOption("Sign out of Cline", AuthActionClineLogin),
huh.NewOption("Select active provider (Cline or BYO)", AuthActionSelectProvider),
huh.NewOption("Configure API provider", AuthActionBYOSetup),
huh.NewOption("Exit authorization wizard", AuthActionExit),
}
} else {
options = []huh.Option[AuthAction]{
huh.NewOption("Authenticate with Cline account", AuthActionClineLogin),
huh.NewOption("Select active provider (Cline or BYO)", AuthActionSelectProvider),
huh.NewOption("Configure API provider", AuthActionBYOSetup),
huh.NewOption("Exit authorization wizard", AuthActionExit),
}
}
// Determine menu title based on status
var title string
// Always show Cline authentication status
if isClineAuthenticated {
title = "Cline Account: \033[32m✓\033[0m Authenticated\n"
} else {
title = "Cline Account: \033[31m✗\033[0m Not authenticated\n"
}
// Show active provider and model if configured (regardless of Cline auth status)
// ANSI color codes: Normal intensity = \033[22m, White = \033[37m, Reset = \033[0m
if currentProvider != "" && currentModel != "" {
title += fmt.Sprintf("Active Provider: \033[22m\033[37m%s\033[0m\nActive Model: \033[22m\033[37m%s\033[0m\n", currentProvider, currentModel)
}
// Always end with a huh?
title += "\nWhat would you like to do?"
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[AuthAction]().
Title(title).
Options(options...).
Value(&action),
),
)
if err := form.Run(); err != nil {
return "", fmt.Errorf("failed to get menu choice: %w", err)
}
return action, nil
}
// HandleAPIProviderSetup launches the API provider configuration wizard
func HandleAPIProviderSetup(ctx context.Context) error {
wizard, err := NewProviderWizard(ctx)
if err != nil {
return fmt.Errorf("failed to create provider wizard: %w", err)
}
return wizard.Run()
}
// HandleSelectProvider allows users to switch between Cline provider and BYO providers
func HandleSelectProvider(ctx context.Context) error {
// Get task manager
manager, err := createTaskManager(ctx)
if err != nil {
return fmt.Errorf("failed to create task manager: %w", err)
}
// Detect all providers with valid configurations (is an API key present)
availableProviders, err := DetectAllConfiguredProviders(ctx, manager)
if err != nil {
return fmt.Errorf("failed to detect configured providers: %w", err)
}
// Build list of available providers
var providerOptions []huh.Option[string]
var providerMapping = make(map[string]cline.ApiProvider)
// Add each configured provider to the selection menu
for _, provider := range availableProviders {
providerName := getProviderDisplayName(provider)
providerKey := fmt.Sprintf("provider_%d", provider)
providerOptions = append(providerOptions, huh.NewOption(providerName, providerKey))
providerMapping[providerKey] = provider
}
if len(providerOptions) == 0 {
fmt.Println("No providers available. Please configure a provider first.")
return HandleAuthMenuNoArgs(ctx)
}
if len(providerOptions) == 1 {
fmt.Println("Only one provider is configured. Configure another provider to switch between them.")
return HandleAuthMenuNoArgs(ctx)
}
providerOptions = append(providerOptions, huh.NewOption("(Cancel)", "cancel"))
// Show selection menu
var selected string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Select which provider to use").
Options(providerOptions...).
Value(&selected),
),
)
if err := form.Run(); err != nil {
return fmt.Errorf("failed to select provider: %w", err)
}
if selected == "cancel" {
return HandleAuthMenuNoArgs(ctx)
}
// Get the selected provider
selectedProvider := providerMapping[selected]
// Apply the selected provider
if selectedProvider == cline.ApiProvider_CLINE {
// Configure Cline as the active provider
return SelectClineModel(ctx, manager)
} else {
// Switch to the selected BYO provider
return SwitchToBYOProvider(ctx, manager, selectedProvider)
}
}
// createTaskManager is a helper to create a task manager (avoids import cycles)
func createTaskManager(ctx context.Context) (*task.Manager, error) {
return task.NewManagerForDefault(ctx)
}
func verboseLog(format string, args ...interface{}) {
if global.Config != nil && global.Config.Verbose {
fmt.Printf("[VERBOSE] "+format+"\n", args...)
}
}
-130
View File
@@ -1,130 +0,0 @@
package auth
import (
"context"
"fmt"
"io"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/grpc-go/cline"
)
// AuthStatusListener manages subscription to auth status updates
type AuthStatusListener struct {
stream cline.AccountService_SubscribeToAuthStatusUpdateClient
updatesCh chan *cline.AuthState
errCh chan error
ctx context.Context
cancel context.CancelFunc
}
// NewAuthStatusListener creates a new auth status listener
func NewAuthStatusListener(parentCtx context.Context) (*AuthStatusListener, error) {
client, err := global.GetDefaultClient(parentCtx)
if err != nil {
return nil, fmt.Errorf("failed to get client: %w", err)
}
// Create cancellable context
ctx, cancel := context.WithCancel(parentCtx)
// Subscribe to auth status updates
stream, err := client.Account.SubscribeToAuthStatusUpdate(ctx, &cline.EmptyRequest{})
if err != nil {
cancel()
return nil, fmt.Errorf("failed to subscribe to auth updates: %w", err)
}
return &AuthStatusListener{
stream: stream,
updatesCh: make(chan *cline.AuthState, 10),
errCh: make(chan error, 1),
ctx: ctx,
cancel: cancel,
}, nil
}
// Start begins listening to the auth status update stream
func (l *AuthStatusListener) Start() error {
verboseLog("Starting auth status listener...")
go l.readStream()
return nil
}
// readStream reads from the gRPC stream and forwards messages to channels
func (l *AuthStatusListener) readStream() {
defer close(l.updatesCh)
defer close(l.errCh)
for {
select {
case <-l.ctx.Done():
verboseLog("Auth listener context cancelled")
return
default:
state, err := l.stream.Recv()
if err != nil {
if err == io.EOF {
verboseLog("Auth status stream closed")
return
}
verboseLog("Error reading from auth status stream: %v", err)
select {
case l.errCh <- err:
case <-l.ctx.Done():
}
return
}
verboseLog("Received auth state update: user=%v", state.User != nil)
select {
case l.updatesCh <- state:
case <-l.ctx.Done():
return
}
}
}
}
// WaitForAuthentication blocks until authentication succeeds or timeout occurs
func (l *AuthStatusListener) WaitForAuthentication(timeout time.Duration) error {
verboseLog("Waiting for authentication (timeout: %v)...", timeout)
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case <-timer.C:
return fmt.Errorf("authentication timeout after %v - please try again", timeout)
case <-l.ctx.Done():
return fmt.Errorf("authentication cancelled")
case err := <-l.errCh:
return fmt.Errorf("authentication stream error: %w", err)
case state := <-l.updatesCh:
if isAuthenticated(state) {
verboseLog("Authentication successful!")
return nil
}
verboseLog("Received auth update but not authenticated yet...")
}
}
}
// Stop closes the stream and cleans up resources
func (l *AuthStatusListener) Stop() {
verboseLog("Stopping auth status listener...")
l.cancel()
}
// isAuthenticated checks if AuthState indicates successful authentication
func isAuthenticated(state *cline.AuthState) bool {
return state != nil && state.User != nil
}
-13
View File
@@ -1,13 +0,0 @@
package auth
import "fmt"
// QuickAPISetup performs quick provider setup with provider ID and optional API key
func QuickAPISetup(providerID, apiKey string) error {
fmt.Println("Quick BYO API setup is currently stubbed - not yet implemented.")
fmt.Printf("Requested provider: %s\n", providerID)
if apiKey != "" {
fmt.Println("Provided API key:", "<jk redacted>")
}
return nil
}
-1
View File
@@ -1 +0,0 @@
package auth
-123
View File
@@ -1,123 +0,0 @@
package auth
import (
"context"
"fmt"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// DefaultClineModelID is the default model ID for Cline provider.
// Cline uses OpenRouter-compatible model IDs.
const DefaultClineModelID = "anthropic/claude-sonnet-4.5"
// FetchClineModels fetches available Cline models from Cline Core.
// Note: Cline provider uses OpenRouter-compatible API and model format.
// The models are fetched using the same method as OpenRouter.
func FetchClineModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OpenRouterModelInfo, error) {
if global.Config.Verbose {
fmt.Println("Fetching Cline models (using OpenRouter-compatible API)")
}
// Cline uses OpenRouter model fetching
models, err := FetchOpenRouterModels(ctx, manager)
if err != nil {
return nil, fmt.Errorf("failed to fetch Cline models: %w", err)
}
return models, nil
}
// GetClineModelInfo retrieves information for a specific Cline model.
func GetClineModelInfo(modelID string, models map[string]*cline.OpenRouterModelInfo) (*cline.OpenRouterModelInfo, error) {
modelInfo, exists := models[modelID]
if !exists {
return nil, fmt.Errorf("model %s not found", modelID)
}
return modelInfo, nil
}
// SetDefaultClineModel configures the default Cline model after authentication.
// This is called automatically after successful Cline sign-in.
func SetDefaultClineModel(ctx context.Context, manager *task.Manager) error {
// Fetch available models
models, err := FetchClineModels(ctx, manager)
if err != nil {
// If we can't fetch models, we'll use the default without model info
fmt.Printf("Warning: Could not fetch Cline models: %v\n", err)
fmt.Printf("Using default model: %s\n", DefaultClineModelID)
return applyDefaultClineModel(ctx, manager, nil)
}
// Check if default model is available
modelInfo, err := GetClineModelInfo(DefaultClineModelID, models)
if err != nil {
fmt.Printf("Warning: Default model not found: %v\n", err)
// Try to use any available model
for modelID := range models {
fmt.Printf("Using available model: %s\n", modelID)
return applyClineModelConfiguration(ctx, manager, modelID, models[modelID])
}
return fmt.Errorf("no usable Cline models found")
}
// Apply the default model
return applyClineModelConfiguration(ctx, manager, DefaultClineModelID, modelInfo)
}
// SelectClineModel presents a menu to select a Cline model and applies the configuration.
func SelectClineModel(ctx context.Context, manager *task.Manager) error {
// Fetch models (uses OpenRouter-compatible format)
models, err := FetchClineModels(ctx, manager)
if err != nil {
return fmt.Errorf("failed to fetch Cline models: %w", err)
}
// Convert to interface map for generic utilities
modelMap := ConvertOpenRouterModelsToInterface(models)
// Get model IDs as a sorted list
modelIDs := ConvertModelsMapToSlice(modelMap)
// Display selection menu
selectedModelID, err := DisplayModelSelectionMenu(modelIDs, "Cline")
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Get the selected model info
modelInfo := models[selectedModelID]
// Apply the configuration
if err := applyClineModelConfiguration(ctx, manager, selectedModelID, modelInfo); err != nil {
return err
}
fmt.Println()
// Return to main auth menu after model selection
return HandleAuthMenuNoArgs(ctx)
}
// applyClineModelConfiguration applies a Cline model configuration to both Act and Plan modes using UpdateProviderPartial.
// Cline uses OpenRouter-compatible model format.
func applyClineModelConfiguration(ctx context.Context, manager *task.Manager, modelID string, modelInfo *cline.OpenRouterModelInfo) error {
provider := cline.ApiProvider_CLINE
updates := ProviderUpdatesPartial{
ModelID: &modelID,
ModelInfo: modelInfo,
}
return UpdateProviderPartial(ctx, manager, provider, updates, true)
}
// applyDefaultClineModel applies the default Cline model without model info.
// This is a fallback when model fetching fails.
func applyDefaultClineModel(ctx context.Context, manager *task.Manager, modelInfo *cline.OpenRouterModelInfo) error {
return applyClineModelConfiguration(ctx, manager, DefaultClineModelID, modelInfo)
}
-136
View File
@@ -1,136 +0,0 @@
package auth
import (
"context"
"fmt"
"os"
"sort"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
"golang.org/x/term"
)
// FetchOpenRouterModels fetches available OpenRouter models from Cline Core
func FetchOpenRouterModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OpenRouterModelInfo, error) {
resp, err := manager.GetClient().Models.RefreshOpenRouterModels(ctx, &cline.EmptyRequest{})
if err != nil {
return nil, fmt.Errorf("failed to fetch OpenRouter models: %w", err)
}
return resp.Models, nil
}
// FetchOpenAiModels fetches available OpenAI models from Cline Core
// Takes the API key and returns a list of model IDs
func FetchOpenAiModels(ctx context.Context, manager *task.Manager, baseURL, apiKey string) ([]string, error) {
req := &cline.OpenAiModelsRequest{
BaseUrl: baseURL,
ApiKey: apiKey,
}
resp, err := manager.GetClient().Models.RefreshOpenAiModels(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to fetch OpenAI models: %w", err)
}
return resp.Values, nil
}
// FetchOllamaModels fetches available Ollama models from Cline Core
// Takes the base URL (empty string for default) and returns a list of model IDs
func FetchOllamaModels(ctx context.Context, manager *task.Manager, baseURL string) ([]string, error) {
req := &cline.StringRequest{
Value: baseURL,
}
resp, err := manager.GetClient().Models.GetOllamaModels(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to fetch Ollama models: %w", err)
}
return resp.Values, nil
}
// DisplayModelSelectionMenu shows an interactive menu for selecting a model from a list.
// Models are displayed alphabetically. Uses model ID as the option value to avoid
// index-based bugs when list order changes.
// Returns the selected model ID.
func DisplayModelSelectionMenu(models []string, providerName string) (string, error) {
if len(models) == 0 {
return "", fmt.Errorf("no models available for selection")
}
// Use model ID as the value (not index) to avoid positional coupling bugs
var selectedModel string
options := make([]huh.Option[string], len(models))
for i, model := range models {
options[i] = huh.NewOption(model, model)
}
title := fmt.Sprintf("Select a %s model", providerName)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title(title).
Options(options...).
Height(calculateSelectHeight()).
Filtering(true).
Value(&selectedModel),
),
)
if err := form.Run(); err != nil {
return "", fmt.Errorf("failed to select model: %w", err)
}
return selectedModel, nil
}
// ConvertModelsMapToSlice converts a map of models to a sorted slice of model IDs.
// This is useful for displaying models in a consistent order in UI components.
func ConvertModelsMapToSlice(models map[string]interface{}) []string {
result := make([]string, 0, len(models))
for modelID := range models {
result = append(result, modelID)
}
// Sort alphabetically for consistent display
sort.Strings(result)
return result
}
// ConvertOpenRouterModelsToInterface converts OpenRouter model map to generic interface map.
// This allows OpenRouter and Cline models to be used with the generic fetching utilities.
func ConvertOpenRouterModelsToInterface(models map[string]*cline.OpenRouterModelInfo) map[string]interface{} {
result := make(map[string]interface{}, len(models))
for k, v := range models {
result[k] = v
}
return result
}
// getTerminalHeight returns the terminal height (rows)
func getTerminalHeight() int {
_, height, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || height <= 0 {
return 25 // safe fallback for non-TTY or errors
}
return height
}
// calculateSelectHeight computes appropriate height for Select component
// Reserves space for title, search UI, and margins
func calculateSelectHeight() int {
height := getTerminalHeight()
// Reserve ~10 rows for UI chrome (title, search, margins)
visibleRows := height - 10
// Clamp between 8 (minimum usable) and 25 (maximum before unwieldy)
if visibleRows < 8 {
return 8
}
if visibleRows > 25 {
return 25
}
return visibleRows
}
-69
View File
@@ -1,69 +0,0 @@
package auth
import (
"fmt"
"sort"
"github.com/cline/cli/pkg/generated"
"github.com/cline/grpc-go/cline"
)
// SupportsStaticModelList returns true if the provider has a predefined static model list
func SupportsStaticModelList(provider cline.ApiProvider) bool {
providerID := GetProviderIDForEnum(provider)
if providerID == "" {
return false
}
// Check if this provider has static models defined
def, err := generated.GetProviderDefinition(providerID)
if err != nil {
return false
}
// Return true if provider has models and isn't dynamic-only
// (Dynamic providers like OpenRouter/OpenAI/Ollama fetch from API)
return len(def.Models) > 0 && !def.HasDynamicModels
}
// FetchStaticModels retrieves the static model list for a provider from generated definitions
// Returns a sorted list of model IDs and a map of model IDs to their info
func FetchStaticModels(provider cline.ApiProvider) ([]string, map[string]generated.ModelInfo, error) {
providerID := GetProviderIDForEnum(provider)
if providerID == "" {
return nil, nil, fmt.Errorf("unknown provider enum: %v", provider)
}
def, err := generated.GetProviderDefinition(providerID)
if err != nil {
return nil, nil, fmt.Errorf("failed to get provider definition: %w", err)
}
if len(def.Models) == 0 {
return nil, nil, fmt.Errorf("no models defined for provider %s", providerID)
}
// Extract model IDs and sort them
modelIDs := make([]string, 0, len(def.Models))
for modelID := range def.Models {
modelIDs = append(modelIDs, modelID)
}
sort.Strings(modelIDs)
return modelIDs, def.Models, nil
}
// GetDefaultModelForProvider returns the default model ID for a provider if one is defined
func GetDefaultModelForProvider(provider cline.ApiProvider) string {
providerID := GetProviderIDForEnum(provider)
if providerID == "" {
return ""
}
def, err := generated.GetProviderDefinition(providerID)
if err != nil {
return ""
}
return def.DefaultModelID
}
-174
View File
@@ -1,174 +0,0 @@
package auth
import (
"fmt"
"github.com/charmbracelet/huh"
"github.com/cline/grpc-go/cline"
)
// BYOProviderOption represents a selectable BYO (bring-your-own) provider option
type BYOProviderOption struct {
Name string
Provider cline.ApiProvider
}
// GetBYOProviderList returns the list of supported BYO providers for CLI configuration.
// This list excludes Cline provider which is handled separately.
func GetBYOProviderList() []BYOProviderOption {
return []BYOProviderOption{
{Name: "Anthropic", Provider: cline.ApiProvider_ANTHROPIC},
{Name: "OpenAI", Provider: cline.ApiProvider_OPENAI},
{Name: "OpenAI Native", Provider: cline.ApiProvider_OPENAI_NATIVE},
{Name: "OpenRouter", Provider: cline.ApiProvider_OPENROUTER},
{Name: "X AI (Grok)", Provider: cline.ApiProvider_XAI},
{Name: "AWS Bedrock", Provider: cline.ApiProvider_BEDROCK},
{Name: "Google Gemini", Provider: cline.ApiProvider_GEMINI},
{Name: "Ollama", Provider: cline.ApiProvider_OLLAMA},
}
}
// SelectBYOProvider displays a menu for selecting a BYO provider.
func SelectBYOProvider() (cline.ApiProvider, error) {
providers := GetBYOProviderList()
var selectedIndex int
options := make([]huh.Option[int], len(providers)+1)
for i, provider := range providers {
options[i] = huh.NewOption(provider.Name, i)
}
options[len(providers)] = huh.NewOption("(Cancel)", -1)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[int]().
Title("Select an API provider").
Options(options...).
Value(&selectedIndex),
),
)
if err := form.Run(); err != nil {
return 0, fmt.Errorf("failed to select provider: %w", err)
}
if selectedIndex == -1 {
return 0, fmt.Errorf("provider selection cancelled")
}
return providers[selectedIndex].Provider, nil
}
// SupportsBYOModelFetching returns true if the provider supports fetching models dynamically
// from a remote API, or if it has a static list of predefined models.
// This is used to determine whether to show a model list before prompting for manual entry.
func SupportsBYOModelFetching(provider cline.ApiProvider) bool {
switch provider {
case cline.ApiProvider_OPENROUTER:
return true
case cline.ApiProvider_OPENAI:
return true
case cline.ApiProvider_OLLAMA:
return true
}
return SupportsStaticModelList(provider)
}
// GetBYOProviderPlaceholder returns a placeholder model ID for manual entry based on provider.
func GetBYOProviderPlaceholder(provider cline.ApiProvider) string {
switch provider {
case cline.ApiProvider_ANTHROPIC:
return "e.g., claude-sonnet-4-5-20250929"
case cline.ApiProvider_OPENAI:
return "e.g., gpt-5-2025-08-07"
case cline.ApiProvider_OPENAI_NATIVE:
return "e.g., openai/gpt-oss-120b"
case cline.ApiProvider_OPENROUTER:
return "e.g., google/gemini-2.0-flash-exp:free"
case cline.ApiProvider_XAI:
return "e.g., grok-code-fast-1"
case cline.ApiProvider_BEDROCK:
return "e.g., anthropic.claude-sonnet-4-5-20250929-v1:0"
case cline.ApiProvider_GEMINI:
return "e.g., gemini-2.5-pro"
case cline.ApiProvider_OLLAMA:
return "e.g., qwen3-coder:30b"
default:
return "Enter model ID"
}
}
// GetBYOAPIKeyFieldConfig returns field configuration for API key input based on provider.
type APIKeyFieldConfig struct {
Title string
EchoMode huh.EchoMode
IsRequired bool
}
// GetBYOAPIKeyFieldConfig returns the configuration for the API key field based on provider.
func GetBYOAPIKeyFieldConfig(provider cline.ApiProvider) APIKeyFieldConfig {
if provider == cline.ApiProvider_OLLAMA {
return APIKeyFieldConfig{
Title: "Base URL (optional, press Enter for default)",
EchoMode: huh.EchoModeNormal,
IsRequired: false,
}
}
return APIKeyFieldConfig{
Title: "API Key",
EchoMode: huh.EchoModePassword,
IsRequired: true,
}
}
// PromptForAPIKey prompts the user to enter an API key (or base URL for Ollama).
// For OpenAI Native provider, also prompts for an optional base URL.
func PromptForAPIKey(provider cline.ApiProvider) (string, error) {
var apiKey string
config := GetBYOAPIKeyFieldConfig(provider)
apiKeyField := huh.NewInput().
Title(config.Title).
EchoMode(config.EchoMode).
Value(&apiKey)
if config.IsRequired {
apiKeyField = apiKeyField.Validate(func(s string) error {
if s == "" {
return fmt.Errorf("API key cannot be empty")
}
return nil
})
}
form := huh.NewForm(huh.NewGroup(apiKeyField))
if err := form.Run(); err != nil {
return "", fmt.Errorf("failed to get API key: %w", err)
}
// For OpenAI Native provider, also prompt for base URL
if provider == cline.ApiProvider_OPENAI_NATIVE {
var baseURL string
baseURLForm := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Base URL (optional, for OpenAI-compatible providers)").
Placeholder("e.g., https://api.example.com/v1").
Value(&baseURL).
Description("Press Enter to skip if using standard OpenAI API"),
),
)
if err := baseURLForm.Run(); err != nil {
return "", fmt.Errorf("failed to get base URL: %w", err)
}
// TODO - connect baseURL
_ = baseURL
}
return apiKey, nil
}
-469
View File
@@ -1,469 +0,0 @@
package auth
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// ProviderDisplay represents a configured provider for display purposes
type ProviderDisplay struct {
Mode string // "Plan" or "Act"
Provider cline.ApiProvider // Provider enum
ModelID string // Model identifier
HasAPIKey bool // Whether an API key is configured (never show actual key)
BaseURL string // Base URL for providers like Ollama (can be shown publicly)
}
// ProviderListResult holds the parsed provider configuration from state
type ProviderListResult struct {
PlanProvider *ProviderDisplay
ActProvider *ProviderDisplay
apiConfig map[string]interface{} // Store the raw apiConfig for scanning all providers
}
// GetProviderConfigurations retrieves and parses provider configurations from Cline Core state
func GetProviderConfigurations(ctx context.Context, manager *task.Manager) (*ProviderListResult, error) {
if global.Config.Verbose {
fmt.Println("[DEBUG] Retrieving provider configurations from Cline Core")
}
// Get latest state from Cline Core
state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return nil, fmt.Errorf("failed to get state: %w", err)
}
stateJSON := state.StateJson
if global.Config.Verbose {
fmt.Printf("[DEBUG] Retrieved state, parsing JSON (length: %d)\n", len(stateJSON))
}
// Parse state_json as map[string]interface{}
var stateData map[string]interface{}
if err := json.Unmarshal([]byte(stateJSON), &stateData); err != nil {
return nil, fmt.Errorf("failed to parse state JSON: %w", err)
}
if global.Config.Verbose {
fmt.Printf("[DEBUG] Parsed state data with %d keys\n", len(stateData))
}
// Extract apiConfiguration object from state
apiConfig, ok := stateData["apiConfiguration"].(map[string]interface{})
if !ok {
if global.Config.Verbose {
fmt.Println("[DEBUG] No apiConfiguration found in state")
}
return &ProviderListResult{
apiConfig: make(map[string]interface{}),
}, nil
}
if global.Config.Verbose {
fmt.Printf("[DEBUG] Found apiConfiguration with %d keys\n", len(apiConfig))
}
// Extract plan mode configuration
planProvider := extractProviderFromState(apiConfig, "plan")
if global.Config.Verbose && planProvider != nil {
fmt.Printf("[DEBUG] Plan mode: provider=%v, model=%s\n", planProvider.Provider, planProvider.ModelID)
}
// Extract act mode configuration
actProvider := extractProviderFromState(apiConfig, "act")
if global.Config.Verbose && actProvider != nil {
fmt.Printf("[DEBUG] Act mode: provider=%v, model=%s\n", actProvider.Provider, actProvider.ModelID)
}
return &ProviderListResult{
PlanProvider: planProvider,
ActProvider: actProvider,
apiConfig: apiConfig,
}, nil
}
// GetAllReadyProviders returns all providers that have both a model and API key configured
func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
if r.apiConfig == nil {
return []*ProviderDisplay{}
}
var readyProviders []*ProviderDisplay
seenProviders := make(map[cline.ApiProvider]bool)
// Check all possible providers
allProviders := []cline.ApiProvider{
cline.ApiProvider_CLINE,
cline.ApiProvider_ANTHROPIC,
cline.ApiProvider_OPENAI,
cline.ApiProvider_OPENAI_NATIVE,
cline.ApiProvider_OPENROUTER,
cline.ApiProvider_XAI,
cline.ApiProvider_BEDROCK,
cline.ApiProvider_GEMINI,
cline.ApiProvider_OLLAMA,
}
// Check each provider to see if it's ready to use
// We use "plan" mode to check, since both plan and act should have the same providers configured
for _, provider := range allProviders {
// Skip if we've already seen this provider
if seenProviders[provider] {
continue
}
// Check if this provider has an API key
hasAPIKey := checkAPIKeyExists(r.apiConfig, provider)
if !hasAPIKey {
continue
}
// Check if this provider has a model configured
modelID := getProviderSpecificModelID(r.apiConfig, "plan", provider)
if modelID == "" {
continue
}
// Get base URL for Ollama
baseURL := ""
if provider == cline.ApiProvider_OLLAMA {
if url, ok := r.apiConfig["ollamaBaseUrl"].(string); ok {
baseURL = url
}
}
// This provider is ready to use
readyProviders = append(readyProviders, &ProviderDisplay{
Mode: "Ready",
Provider: provider,
ModelID: modelID,
HasAPIKey: hasAPIKey,
BaseURL: baseURL,
})
seenProviders[provider] = true
}
return readyProviders
}
// extractProviderFromState extracts provider configuration for specific plan/act mode
func extractProviderFromState(stateData map[string]interface{}, mode string) *ProviderDisplay {
// Build key names based on mode
providerKey := mode + "ModeApiProvider"
// Extract provider string from state
providerStr, ok := stateData[providerKey].(string)
if !ok || providerStr == "" {
if global.Config.Verbose {
fmt.Printf("[DEBUG] No provider configured for %s mode\n", mode)
}
return nil
}
// Map provider string to enum
provider, ok := mapProviderStringToEnum(providerStr)
if !ok {
if global.Config.Verbose {
fmt.Printf("[DEBUG] Unknown provider type: %s\n", providerStr)
}
return nil
}
// Get provider-specific model ID
modelID := getProviderSpecificModelID(stateData, mode, provider)
// Check if API key exists
hasAPIKey := checkAPIKeyExists(stateData, provider)
// Get base URL for Ollama (can be shown publicly)
baseURL := ""
if provider == cline.ApiProvider_OLLAMA {
if url, ok := stateData["ollamaBaseUrl"].(string); ok {
baseURL = url
}
}
return &ProviderDisplay{
Mode: capitalizeMode(mode),
Provider: provider,
ModelID: modelID,
HasAPIKey: hasAPIKey,
BaseURL: baseURL,
}
}
// mapProviderStringToEnum converts provider string from state to ApiProvider enum
// Returns (provider, ok) where ok is false if the provider is unknown
func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
// Map string values to enum values
switch providerStr {
case "anthropic":
return cline.ApiProvider_ANTHROPIC, true
case "openai":
return cline.ApiProvider_OPENAI, true
case "openai-native":
return cline.ApiProvider_OPENAI_NATIVE, true
case "openrouter":
return cline.ApiProvider_OPENROUTER, true
case "xai":
return cline.ApiProvider_XAI, true
case "bedrock":
return cline.ApiProvider_BEDROCK, true
case "gemini":
return cline.ApiProvider_GEMINI, true
case "ollama":
return cline.ApiProvider_OLLAMA, true
case "cline":
return cline.ApiProvider_CLINE, true
default:
return cline.ApiProvider_ANTHROPIC, false // Return 0 value with false
}
}
// GetProviderIDForEnum converts a provider enum to the provider ID string
// This is the inverse of mapProviderStringToEnum and is used for provider definitions
func GetProviderIDForEnum(provider cline.ApiProvider) string {
switch provider {
case cline.ApiProvider_ANTHROPIC:
return "anthropic"
case cline.ApiProvider_OPENAI:
return "openai"
case cline.ApiProvider_OPENAI_NATIVE:
return "openai-native"
case cline.ApiProvider_OPENROUTER:
return "openrouter"
case cline.ApiProvider_XAI:
return "xai"
case cline.ApiProvider_BEDROCK:
return "bedrock"
case cline.ApiProvider_GEMINI:
return "gemini"
case cline.ApiProvider_OLLAMA:
return "ollama"
case cline.ApiProvider_CLINE:
return "cline"
default:
return ""
}
}
// getProviderSpecificModelID gets the provider-specific model ID field from state
func getProviderSpecificModelID(stateData map[string]interface{}, mode string, provider cline.ApiProvider) string {
modelKey, err := GetModelIDFieldName(provider, mode)
if err != nil {
if global.Config.Verbose {
fmt.Printf("[DEBUG] Error getting model ID field name: %v\n", err)
}
return ""
}
if global.Config.Verbose {
fmt.Printf("[DEBUG] Looking for model ID in key: %s\n", modelKey)
}
// Extract model ID from state
modelID, _ := stateData[modelKey].(string)
return modelID
}
// checkAPIKeyExists checks if API key field exists in state (never retrieve actual key)
func checkAPIKeyExists(stateData map[string]interface{}, provider cline.ApiProvider) bool {
// Get field mapping from centralized function
fields, err := GetProviderFields(provider)
if err != nil {
return false
}
keyField := fields.APIKeyField
// Check if the key exists and is not empty
if value, ok := stateData[keyField]; ok {
if str, ok := value.(string); ok && str != "" {
return true
}
}
return false
}
// capitalizeMode capitalizes the mode string for display
func capitalizeMode(mode string) string {
if len(mode) == 0 {
return mode
}
return strings.ToUpper(mode[:1]) + mode[1:]
}
// getProviderDisplayName returns a user-friendly name for the provider
func getProviderDisplayName(provider cline.ApiProvider) string {
switch provider {
case cline.ApiProvider_ANTHROPIC:
return "Anthropic"
case cline.ApiProvider_OPENAI:
return "OpenAI"
case cline.ApiProvider_OPENAI_NATIVE:
return "OpenAI Native"
case cline.ApiProvider_OPENROUTER:
return "OpenRouter"
case cline.ApiProvider_XAI:
return "X AI (Grok)"
case cline.ApiProvider_BEDROCK:
return "AWS Bedrock"
case cline.ApiProvider_GEMINI:
return "Google Gemini"
case cline.ApiProvider_OLLAMA:
return "Ollama"
case cline.ApiProvider_CLINE:
return "Cline (Official)"
default:
return "Unknown"
}
}
// FormatProviderList formats the complete provider list for console display
// This now shows ALL providers that have both a model and API key configured
func FormatProviderList(result *ProviderListResult) string {
var output strings.Builder
output.WriteString("\n=== Configured API Providers ===\n\n")
// Get the currently active provider
var activeProvider cline.ApiProvider
var activeProviderSet bool
if result.ActProvider != nil {
activeProvider = result.ActProvider.Provider
activeProviderSet = true
}
// Get all ready-to-use providers (those with both API key and model configured)
readyProviders := result.GetAllReadyProviders()
if len(readyProviders) == 0 {
output.WriteString(" No providers ready to use.\n")
output.WriteString(" A provider is ready when it has both a model and API key configured.\n")
output.WriteString(" Use 'Configure a new provider' to configure one.\n\n")
} else {
//output.WriteString(fmt.Sprintf(" %d provider(s) ready to use:\n\n", len(readyProviders)))
for _, display := range readyProviders {
// Check if this is the active provider
isActive := activeProviderSet && display.Provider == activeProvider
if isActive {
output.WriteString(fmt.Sprintf(" ✓ %s (ACTIVE)\n", getProviderDisplayName(display.Provider)))
} else {
output.WriteString(fmt.Sprintf(" • %s\n", getProviderDisplayName(display.Provider)))
}
output.WriteString(fmt.Sprintf(" Model: %s\n", display.ModelID))
// Show status based on provider type
if display.Provider == cline.ApiProvider_OLLAMA {
if display.BaseURL != "" {
output.WriteString(fmt.Sprintf(" Base URL: %s\n", display.BaseURL))
} else {
output.WriteString(" Base URL: (default)\n")
}
} else if display.Provider == cline.ApiProvider_CLINE {
output.WriteString(" Status: Authenticated\n")
} else {
output.WriteString(" API Key: Configured\n")
}
output.WriteString("\n")
}
}
output.WriteString("================================\n")
return output.String()
}
// DetectAllConfiguredProviders scans the state to find all providers that have API keys configured.
// This allows switching between multiple providers even when only one is currently active.
func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([]cline.ApiProvider, error) {
verboseLog("[DEBUG] Detecting all configured providers...")
// Get latest state from Cline Core
state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return nil, fmt.Errorf("failed to get state: %w", err)
}
stateJSON := state.StateJson
// Parse state_json as map[string]interface{}
var stateData map[string]interface{}
if err := json.Unmarshal([]byte(stateJSON), &stateData); err != nil {
return nil, fmt.Errorf("failed to parse state JSON: %w", err)
}
// Extract apiConfiguration object from state
apiConfig, ok := stateData["apiConfiguration"].(map[string]interface{})
if !ok {
verboseLog("[DEBUG] No apiConfiguration found in state")
verboseLog("[DEBUG] Available keys in stateData: %v", getMapKeys(stateData))
return []cline.ApiProvider{}, nil
}
verboseLog("[DEBUG] apiConfiguration keys: %v", getMapKeys(apiConfig))
var configuredProviders []cline.ApiProvider
// Check for Cline provider (uses authentication instead of API key)
if IsAuthenticated(ctx) {
configuredProviders = append(configuredProviders, cline.ApiProvider_CLINE)
verboseLog("[DEBUG] Cline provider is authenticated")
}
// Check each BYO provider for API key presence
providersToCheck := []struct {
provider cline.ApiProvider
keyField string
}{
{cline.ApiProvider_ANTHROPIC, "apiKey"},
{cline.ApiProvider_OPENAI, "openAiApiKey"},
{cline.ApiProvider_OPENAI_NATIVE, "openAiNativeApiKey"},
{cline.ApiProvider_OPENROUTER, "openRouterApiKey"},
{cline.ApiProvider_XAI, "xaiApiKey"},
{cline.ApiProvider_BEDROCK, "awsAccessKey"},
{cline.ApiProvider_GEMINI, "geminiApiKey"},
{cline.ApiProvider_OLLAMA, "ollamaBaseUrl"}, // Ollama uses baseUrl instead of API key
}
for _, providerCheck := range providersToCheck {
verboseLog("[DEBUG] Checking for %s key: %s", getProviderDisplayName(providerCheck.provider), providerCheck.keyField)
if value, ok := apiConfig[providerCheck.keyField]; ok {
verboseLog("[DEBUG] Found key, value type: %T, is empty: %v", value, value == "")
if str, ok := value.(string); ok && str != "" {
configuredProviders = append(configuredProviders, providerCheck.provider)
verboseLog("[DEBUG] ✓ Provider %s is configured", getProviderDisplayName(providerCheck.provider))
}
} else {
verboseLog("[DEBUG] Key %s not found", providerCheck.keyField)
}
}
verboseLog("[DEBUG] Total configured providers: %d", len(configuredProviders))
for _, p := range configuredProviders {
verboseLog("[DEBUG] - %s", getProviderDisplayName(p))
}
return configuredProviders, nil
}
// getMapKeys returns the keys of a map for debugging
func getMapKeys(m map[string]interface{}) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
@@ -1,500 +0,0 @@
package auth
import (
"context"
"fmt"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// updateApiConfigurationPartial is a helper that calls the gRPC method with optional verbose logging.
// This replaces the Manager.UpdateApiConfigurationPartial method to keep auth-specific code in the auth package.
func updateApiConfigurationPartial(ctx context.Context, manager *task.Manager, request *cline.UpdateApiConfigurationPartialRequest) error {
if global.Config.Verbose {
fmt.Println("[DEBUG] Updating API configuration (partial)")
if request.UpdateMask != nil && len(request.UpdateMask.Paths) > 0 {
fmt.Printf("[DEBUG] Field mask paths: %v\n", request.UpdateMask.Paths)
}
if request.ApiConfiguration != nil {
apiConfig := request.ApiConfiguration
if apiConfig.PlanModeApiProvider != nil {
fmt.Printf("[DEBUG] Plan mode provider: %s\n", *apiConfig.PlanModeApiProvider)
}
if apiConfig.ActModeApiProvider != nil {
fmt.Printf("[DEBUG] Act mode provider: %s\n", *apiConfig.ActModeApiProvider)
}
}
}
// Call the Models service to update API configuration
_, err := manager.GetClient().Models.UpdateApiConfigurationPartial(ctx, request)
if err != nil {
return fmt.Errorf("failed to update API configuration (partial): %w", err)
}
if global.Config.Verbose {
fmt.Println("[DEBUG] API configuration updated successfully (partial)")
}
return nil
}
// ProviderFields defines all the field names associated with a specific provider
type ProviderFields struct {
APIKeyField string // API key field name (e.g., "apiKey", "openAiApiKey")
PlanModeModelIDField string // Plan mode model ID field (e.g., "planModeApiModelId")
ActModeModelIDField string // Act mode model ID field (e.g., "actModeApiModelId")
PlanModeModelInfoField string // Plan mode model info field (optional, empty if not applicable)
ActModeModelInfoField string // Act mode model info field (optional, empty if not applicable)
// Provider-specific additional model ID fields
PlanModeProviderSpecificModelIDField string // e.g., "planModeOpenRouterModelId"
ActModeProviderSpecificModelIDField string // e.g., "actModeOpenRouterModelId"
}
// GetProviderFields returns the field mapping for a given provider
func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
switch provider {
case cline.ApiProvider_ANTHROPIC:
return ProviderFields{
APIKeyField: "apiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_OPENAI:
return ProviderFields{
APIKeyField: "openAiApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeProviderSpecificModelIDField: "planModeOpenAiModelId",
ActModeProviderSpecificModelIDField: "actModeOpenAiModelId",
}, nil
case cline.ApiProvider_OPENROUTER:
return ProviderFields{
APIKeyField: "openRouterApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeModelInfoField: "planModeOpenRouterModelInfo",
ActModeModelInfoField: "actModeOpenRouterModelInfo",
PlanModeProviderSpecificModelIDField: "planModeOpenRouterModelId",
ActModeProviderSpecificModelIDField: "actModeOpenRouterModelId",
}, nil
case cline.ApiProvider_XAI:
return ProviderFields{
APIKeyField: "xaiApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_BEDROCK:
return ProviderFields{
APIKeyField: "awsAccessKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeProviderSpecificModelIDField: "planModeAwsBedrockCustomModelBaseId",
ActModeProviderSpecificModelIDField: "actModeAwsBedrockCustomModelBaseId",
}, nil
case cline.ApiProvider_GEMINI:
return ProviderFields{
APIKeyField: "geminiApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_OPENAI_NATIVE:
return ProviderFields{
APIKeyField: "openAiNativeApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_OLLAMA:
return ProviderFields{
APIKeyField: "ollamaBaseUrl",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeProviderSpecificModelIDField: "planModeOllamaModelId",
ActModeProviderSpecificModelIDField: "actModeOllamaModelId",
}, nil
case cline.ApiProvider_CLINE:
return ProviderFields{
APIKeyField: "clineApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeModelInfoField: "planModeOpenRouterModelInfo",
ActModeModelInfoField: "actModeOpenRouterModelInfo",
PlanModeProviderSpecificModelIDField: "planModeOpenRouterModelId",
ActModeProviderSpecificModelIDField: "actModeOpenRouterModelId",
}, nil
default:
return ProviderFields{}, fmt.Errorf("unsupported provider: %v", provider)
}
}
// ProviderUpdatesPartial defines optional fields for partial provider updates
// Uses pointers to distinguish between "not provided" and "set to empty"
type ProviderUpdatesPartial struct {
ModelID *string // New model ID (optional)
APIKey *string // New API key (optional)
ModelInfo interface{} // New model info (optional, provider-specific)
}
// GetModelIDFieldName returns the appropriate model ID field name for a provider and mode.
// This helper centralizes the logic for determining whether to use provider-specific
// or generic model ID fields.
func GetModelIDFieldName(provider cline.ApiProvider, mode string) (string, error) {
fields, err := GetProviderFields(provider)
if err != nil {
return "", err
}
if mode == "plan" {
// Use provider-specific field if available, otherwise use generic field
if fields.PlanModeProviderSpecificModelIDField != "" {
return fields.PlanModeProviderSpecificModelIDField, nil
}
return fields.PlanModeModelIDField, nil
}
// Act mode
if fields.ActModeProviderSpecificModelIDField != "" {
return fields.ActModeProviderSpecificModelIDField, nil
}
return fields.ActModeModelIDField, nil
}
// buildProviderFieldMask builds a list of camelCase field paths for the field mask.
// When includeProviderEnums is true, the provider enum fields are included (for setting active provider).
// When false, only the data fields are included (for configuring without activating).
func buildProviderFieldMask(fields ProviderFields, includeAPIKey bool, includeModelID bool, includeModelInfo bool, includeProviderEnums bool) []string {
var fieldPaths []string
// Include provider enums if requested (used when setting active provider)
if includeProviderEnums {
fieldPaths = append(fieldPaths, "planModeApiProvider", "actModeApiProvider")
}
// Add API key field if requested
if includeAPIKey {
fieldPaths = append(fieldPaths, fields.APIKeyField)
// Special case: Bedrock also needs secret key
if fields.APIKeyField == "awsAccessKey" {
fieldPaths = append(fieldPaths, "awsSecretKey")
}
}
// Add model ID fields if requested
if includeModelID {
// Only include provider-specific fields if they exist, otherwise use generic fields
if fields.PlanModeProviderSpecificModelIDField != "" {
// Provider has specific fields - use ONLY those
fieldPaths = append(fieldPaths, fields.PlanModeProviderSpecificModelIDField)
fieldPaths = append(fieldPaths, fields.ActModeProviderSpecificModelIDField)
} else {
// Provider uses generic fields - update those
fieldPaths = append(fieldPaths, fields.PlanModeModelIDField)
fieldPaths = append(fieldPaths, fields.ActModeModelIDField)
}
}
// Add model info fields if requested and applicable
if includeModelInfo && fields.PlanModeModelInfoField != "" {
fieldPaths = append(fieldPaths, fields.PlanModeModelInfoField)
fieldPaths = append(fieldPaths, fields.ActModeModelInfoField)
}
return fieldPaths
}
// setAPIKeyField sets the appropriate API key field in the config based on the field name
func setAPIKeyField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
switch fieldName {
case "apiKey":
apiConfig.ApiKey = value
case "openAiApiKey":
apiConfig.OpenAiApiKey = value
case "openAiNativeApiKey":
apiConfig.OpenAiNativeApiKey = value
case "openRouterApiKey":
apiConfig.OpenRouterApiKey = value
case "xaiApiKey":
apiConfig.XaiApiKey = value
case "awsAccessKey":
apiConfig.AwsAccessKey = value
case "geminiApiKey":
apiConfig.GeminiApiKey = value
case "ollamaBaseUrl":
apiConfig.OllamaBaseUrl = value
case "clineApiKey":
apiConfig.ClineApiKey = value
}
}
// setProviderSpecificModelID sets the appropriate provider-specific model ID fields when possible
func setProviderSpecificModelID(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
switch fieldName {
case "planModeOpenAiModelId":
apiConfig.PlanModeOpenAiModelId = value
apiConfig.ActModeOpenAiModelId = value
case "planModeOpenRouterModelId":
apiConfig.PlanModeOpenRouterModelId = value
apiConfig.ActModeOpenRouterModelId = value
case "planModeOllamaModelId":
apiConfig.PlanModeOllamaModelId = value
apiConfig.ActModeOllamaModelId = value
case "planModeAwsBedrockCustomModelBaseId":
apiConfig.PlanModeAwsBedrockCustomModelBaseId = value
apiConfig.ActModeAwsBedrockCustomModelBaseId = value
}
}
// AddProviderPartial configures a new provider with all necessary fields using partial updates.
func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID string, apiKey string, modelInfo interface{}) error {
// Get field mapping for this provider
fields, err := GetProviderFields(provider)
if err != nil {
return err
}
// Build a ModelsApiConfiguration with only the relevant provider fields set
apiConfig := &cline.ModelsApiConfiguration{}
// Set API key field
if apiKey != "" || fields.APIKeyField != "ollamaBaseUrl" {
setAPIKeyField(apiConfig, fields.APIKeyField, proto.String(apiKey))
}
// Set model ID fields
apiConfig.PlanModeApiModelId = proto.String(modelID)
apiConfig.ActModeApiModelId = proto.String(modelID)
// Set provider-specific model ID fields if applicable
if fields.PlanModeProviderSpecificModelIDField != "" {
setProviderSpecificModelID(apiConfig, fields.PlanModeProviderSpecificModelIDField, proto.String(modelID))
}
// Set model info if applicable and provided
if fields.PlanModeModelInfoField != "" && modelInfo != nil {
if openRouterInfo, ok := modelInfo.(*cline.OpenRouterModelInfo); ok {
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
}
}
// Build field mask including all fields we're setting (without provider enums)
includeModelInfo := fields.PlanModeModelInfoField != "" && modelInfo != nil
fieldPaths := buildProviderFieldMask(fields, true, true, includeModelInfo, false)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
// Apply the partial update
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to update API configuration: %w", err)
}
return nil
}
// UpdateProviderPartial updates specific fields for an existing provider using partial updates.
// If setAsActive is true, this will also set the provider as the active provider for both Plan and Act modes.
func UpdateProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, updates ProviderUpdatesPartial, setAsActive bool) error {
// Get field mapping for this provider
fields, err := GetProviderFields(provider)
if err != nil {
return err
}
// Build a ModelsApiConfiguration with only the fields being updated
apiConfig := &cline.ModelsApiConfiguration{}
// Set provider enum for BOTH Plan and Act modes if setAsActive is true
if setAsActive {
apiConfig.PlanModeApiProvider = &provider
apiConfig.ActModeApiProvider = &provider
}
// Track what we're updating for field mask
includeAPIKey := updates.APIKey != nil
includeModelID := updates.ModelID != nil
includeModelInfo := updates.ModelInfo != nil && fields.PlanModeModelInfoField != ""
// Update API key if provided
if updates.APIKey != nil {
setAPIKeyField(apiConfig, fields.APIKeyField, updates.APIKey)
}
// Update model ID if provided
if updates.ModelID != nil {
// Only set provider-specific fields if they exist, otherwise use generic fields
if fields.PlanModeProviderSpecificModelIDField != "" {
setProviderSpecificModelID(apiConfig, fields.PlanModeProviderSpecificModelIDField, updates.ModelID)
} else {
// Provider uses generic fields - set those
apiConfig.PlanModeApiModelId = updates.ModelID
apiConfig.ActModeApiModelId = updates.ModelID
}
}
// Update model info if provided
if updates.ModelInfo != nil && fields.PlanModeModelInfoField != "" {
if openRouterInfo, ok := updates.ModelInfo.(*cline.OpenRouterModelInfo); ok {
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
}
}
// Build field mask for only the fields being updated
fieldPaths := buildProviderFieldMask(fields, includeAPIKey, includeModelID, includeModelInfo, setAsActive)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
// Apply the partial update
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to update API configuration: %w", err)
}
return nil
}
// RemoveProviderPartial removes a provider by clearing its API key using partial updates
func RemoveProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider) error {
// Get field mapping for this provider
fields, err := GetProviderFields(provider)
if err != nil {
return err
}
// Build an EMPTY ModelsApiConfiguration (or one with empty API key field)
// Fields in the mask without values will be cleared
apiConfig := &cline.ModelsApiConfiguration{}
// Build field mask with only the API key field(s)
// For Bedrock, include both access key and secret key
fieldPaths := []string{fields.APIKeyField}
if provider == cline.ApiProvider_BEDROCK {
fieldPaths = append(fieldPaths, "awsSecretKey")
}
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
// Apply the partial update (clearing API key by including in mask without value)
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to update API configuration: %w", err)
}
return nil
}
// BedrockOptionalFields holds optional configuration fields for AWS Bedrock
type BedrockOptionalFields struct {
SessionToken *string // Optional: AWS session token for temporary credentials
Region *string // Optional: AWS region
UseCrossRegionInference *bool // Optional: Enable cross-region inference
UseGlobalInference *bool // Optional: Use global inference endpoint
UsePromptCache *bool // Optional: Enable prompt caching
Authentication *string // Optional: Authentication method
UseProfile *bool // Optional: Use AWS profile
Profile *string // Optional: AWS profile name
Endpoint *string // Optional: Custom endpoint URL
}
// setBedrockOptionalFields sets optional Bedrock-specific fields in the API configuration
func setBedrockOptionalFields(apiConfig *cline.ModelsApiConfiguration, fields *BedrockOptionalFields) {
if fields == nil {
return
}
if fields.SessionToken != nil {
apiConfig.AwsSessionToken = fields.SessionToken
}
if fields.Region != nil {
apiConfig.AwsRegion = fields.Region
}
if fields.UseCrossRegionInference != nil {
apiConfig.AwsUseCrossRegionInference = fields.UseCrossRegionInference
}
if fields.UseGlobalInference != nil {
apiConfig.AwsUseGlobalInference = fields.UseGlobalInference
}
if fields.UsePromptCache != nil {
apiConfig.AwsBedrockUsePromptCache = fields.UsePromptCache
}
if fields.Authentication != nil {
apiConfig.AwsAuthentication = fields.Authentication
}
if fields.UseProfile != nil {
apiConfig.AwsUseProfile = fields.UseProfile
}
if fields.Profile != nil {
apiConfig.AwsProfile = fields.Profile
}
if fields.Endpoint != nil {
apiConfig.AwsBedrockEndpoint = fields.Endpoint
}
}
// buildBedrockOptionalFieldMask builds field mask paths for Bedrock optional fields that have values
func buildBedrockOptionalFieldMask(fields *BedrockOptionalFields) []string {
if fields == nil {
return nil
}
var fieldPaths []string
if fields.SessionToken != nil {
fieldPaths = append(fieldPaths, "awsSessionToken")
}
if fields.Region != nil {
fieldPaths = append(fieldPaths, "awsRegion")
}
if fields.UseCrossRegionInference != nil {
fieldPaths = append(fieldPaths, "awsUseCrossRegionInference")
}
if fields.UseGlobalInference != nil {
fieldPaths = append(fieldPaths, "awsUseGlobalInference")
}
if fields.UsePromptCache != nil {
fieldPaths = append(fieldPaths, "awsBedrockUsePromptCache")
}
if fields.Authentication != nil {
fieldPaths = append(fieldPaths, "awsAuthentication")
}
if fields.UseProfile != nil {
fieldPaths = append(fieldPaths, "awsUseProfile")
}
if fields.Profile != nil {
fieldPaths = append(fieldPaths, "awsProfile")
}
if fields.Endpoint != nil {
fieldPaths = append(fieldPaths, "awsBedrockEndpoint")
}
return fieldPaths
}
-666
View File
@@ -1,666 +0,0 @@
package auth
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// ProviderWizard handles the interactive provider configuration process
type ProviderWizard struct {
ctx context.Context
manager *task.Manager
}
// NewProviderWizard prepares a new provider configuration wizard
func NewProviderWizard(ctx context.Context) (*ProviderWizard, error) {
if err := global.EnsureDefaultInstance(ctx); err != nil {
return nil, fmt.Errorf("failed to ensure Cline Core instance: %w", err)
}
manager, err := task.NewManagerForDefault(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create task manager: %w", err)
}
return &ProviderWizard{
ctx: ctx,
manager: manager,
}, nil
}
// showMainMenu displays the main provider configuration menu
func (pw *ProviderWizard) showMainMenu() (string, error) {
var action string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("What would you like to do?").
Options(
huh.NewOption("Configure a new provider", "add"),
huh.NewOption("Change model for API provider", "change-model"),
huh.NewOption("Remove a provider", "remove"),
huh.NewOption("List configured providers", "list"),
huh.NewOption("Return to main auth menu", "back"),
).
Value(&action),
),
)
if err := form.Run(); err != nil {
return "", fmt.Errorf("failed to get menu choice: %w", err)
}
return action, nil
}
// Run runs the provider configuration wizard
func (pw *ProviderWizard) Run() error {
for {
action, err := pw.showMainMenu()
if err != nil {
return err
}
switch action {
case "add":
if err := pw.handleAddProvider(); err != nil {
return err
}
case "change-model":
if err := pw.handleChangeModel(); err != nil {
return err
}
case "remove":
if err := pw.handleRemoveProvider(); err != nil {
return err
}
case "list":
if err := pw.handleListProviders(); err != nil {
return err
}
case "back":
// Return to main auth menu
return HandleAuthMenuNoArgs(pw.ctx)
}
fmt.Println()
}
}
// "Add a new provider" > handleAddProvider
func (pw *ProviderWizard) handleAddProvider() error {
// Step 1: Select provider
provider, err := SelectBYOProvider()
if err != nil {
if strings.Contains(err.Error(), "cancelled") {
return nil
}
return fmt.Errorf("provider selection failed: %w", err)
}
// Step 2: Special handling for Bedrock provider
if provider == cline.ApiProvider_BEDROCK {
return pw.handleAddBedrockProvider()
}
// Step 3: Get API key first (for non-Bedrock providers)
apiKey, err := PromptForAPIKey(provider)
if err != nil {
return fmt.Errorf("failed to get API key: %w", err)
}
// Step 4: Try to fetch models and let user select (with fallback to manual entry for providers that don't support fetch)
modelID, modelInfo, err := pw.selectModel(provider, apiKey)
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Step 5: Apply configuration using AddProviderPartial
if err := AddProviderPartial(pw.ctx, pw.manager, provider, modelID, apiKey, modelInfo); err != nil {
return fmt.Errorf("failed to save configuration: %w", err)
}
fmt.Println("✓ Provider configured successfully!")
return nil
}
// handleAddBedrockProvider handles the special case of adding Bedrock provider with its multi-field form
func (pw *ProviderWizard) handleAddBedrockProvider() error {
// Step 1: Get Bedrock configuration (all credentials and optional fields)
config, err := PromptForBedrockConfig(pw.ctx, pw.manager)
if err != nil {
if strings.Contains(err.Error(), "user declined profile authentication") {
return nil
}
return fmt.Errorf("failed to get Bedrock configuration: %w", err)
}
// Step 2: Select model
modelID, modelInfo, err := pw.selectModel(cline.ApiProvider_BEDROCK, "")
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Step 3: Apply Bedrock configuration
if err := ApplyBedrockConfig(pw.ctx, pw.manager, config, modelID, modelInfo); err != nil {
return fmt.Errorf("failed to save Bedrock configuration: %w", err)
}
fmt.Println("✓ Bedrock provider configured successfully!")
return nil
}
// handleListProviders retrieves and displays configured providers
func (pw *ProviderWizard) handleListProviders() error {
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
if err != nil {
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
}
output := FormatProviderList(result)
fmt.Println(output)
return nil
}
// selectModel attempts to fetch available models and let user select, or falls back to manual entry
func (pw *ProviderWizard) selectModel(provider cline.ApiProvider, apiKey string) (string, interface{}, error) {
// For providers that support model fetching, try to fetch and display models
canFetchModels := pw.supportsModelFetching(provider)
if canFetchModels {
fmt.Println("Fetching available models...")
models, modelInfoMap, err := pw.fetchModelsForProvider(provider, apiKey)
if err != nil {
fmt.Println("\n⚠ Unable to fetch model list from the provider. Please enter the model ID manually instead.")
if global.Config.Verbose {
fmt.Printf(" Error details: %v\n", err)
}
return pw.manualModelEntry(provider)
}
if len(models) == 0 {
fmt.Println("\n⚠ No models found from the provider. Please enter the model ID manually instead.")
return pw.manualModelEntry(provider)
}
// Let user select from available models (includes manual entry option)
modelID, err := pw.selectFromAvailableModels(models)
if err != nil {
return "", nil, fmt.Errorf("model selection failed: %w", err)
}
// Check if user chose manual entry
const manualEntryKey = "__MANUAL_ENTRY__"
if modelID == manualEntryKey {
return pw.manualModelEntry(provider)
}
// Get the model info for the selected model
var modelInfo interface{}
if modelInfoMap != nil {
modelInfo = modelInfoMap[modelID]
}
return modelID, modelInfo, nil
}
// For providers without model fetching support, use manual entry
return pw.manualModelEntry(provider)
}
// supportsModelFetching returns true if the provider supports fetching models
func (pw *ProviderWizard) supportsModelFetching(provider cline.ApiProvider) bool {
return SupportsBYOModelFetching(provider)
}
// fetchModelsForProvider fetches models for a given provider
// Supports both dynamic API fetching (OpenRouter, OpenAI, Ollama) and static model lists (Anthropic, Bedrock, Gemini, X AI)
func (pw *ProviderWizard) fetchModelsForProvider(provider cline.ApiProvider, apiKey string) ([]string, map[string]interface{}, error) {
// Try dynamic/remote model fetching first
switch provider {
case cline.ApiProvider_OPENROUTER:
models, err := FetchOpenRouterModels(pw.ctx, pw.manager)
if err != nil {
return nil, nil, err
}
interfaceMap := ConvertOpenRouterModelsToInterface(models)
return ConvertModelsMapToSlice(interfaceMap), interfaceMap, nil
case cline.ApiProvider_OPENAI:
// For OpenAI, we need to pass the base URL and API key
baseURL := "https://api.openai.com/v1" // Default OpenAI API base URL
modelIDs, err := FetchOpenAiModels(pw.ctx, pw.manager, baseURL, apiKey)
if err != nil {
return nil, nil, err
}
// OpenAI returns just model IDs without additional info, so modelInfo map is nil
return modelIDs, nil, nil
case cline.ApiProvider_OLLAMA:
// For Ollama, apiKey actually contains the base URL (or empty for default)
baseURL := apiKey // The "API key" field for Ollama is actually the base URL
modelIDs, err := FetchOllamaModels(pw.ctx, pw.manager, baseURL)
if err != nil {
return nil, nil, err
}
// Ollama returns just model IDs without additional info, so modelInfo map is nil
return modelIDs, nil, nil
}
// Fall back to static models for providers that don't support dynamic fetching
if SupportsStaticModelList(provider) {
modelIDs, _, err := FetchStaticModels(provider)
if err != nil {
return nil, nil, err
}
// Static models don't have detailed info maps for now, so modelInfo map is nil
return modelIDs, nil, nil
}
return nil, nil, fmt.Errorf("model fetching not supported for provider: %v", provider)
}
// selectFromAvailableModels displays available models and lets user select one.
// Includes an option to enter a model ID manually in case the desired model isn't listed.
func (pw *ProviderWizard) selectFromAvailableModels(models []string) (string, error) {
if len(models) == 0 {
return "", fmt.Errorf("no models available")
}
// Add a special "manual entry" option at the end
const manualEntryKey = "__MANUAL_ENTRY__"
// Use model ID as the value (not index)
var selectedModel string
options := make([]huh.Option[string], len(models)+1)
for i, model := range models {
options[i] = huh.NewOption(model, model)
}
// Add manual entry option at the end
options[len(models)] = huh.NewOption("Enter model ID manually...", manualEntryKey)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Select a model").
Options(options...).
Height(calculateSelectHeight()).
Filtering(true).
Value(&selectedModel),
),
)
if err := form.Run(); err != nil {
return "", fmt.Errorf("failed to select model: %w", err)
}
// If user selected manual entry, return special key to trigger manual input
if selectedModel == manualEntryKey {
return manualEntryKey, nil
}
return selectedModel, nil
}
// manualModelEntry prompts user to manually enter a model ID.
// Returns the model ID and an error. The modelInfo is always nil for manual entry.
func (pw *ProviderWizard) manualModelEntry(provider cline.ApiProvider) (string, interface{}, error) {
var modelID string
modelPlaceholder := GetBYOProviderPlaceholder(provider)
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Model ID").
Placeholder(modelPlaceholder).
Value(&modelID).
Validate(func(s string) error {
// Trim whitespace and validate
trimmed := strings.TrimSpace(s)
if trimmed == "" {
return fmt.Errorf("model ID cannot be empty")
}
return nil
}),
),
)
if err := form.Run(); err != nil {
return "", nil, fmt.Errorf("failed to get model ID: %w", err)
}
// Trim whitespace from the final value
modelID = strings.TrimSpace(modelID)
// modelInfo is always nil for manual entry
return modelID, nil, nil
}
// handleChangeModel allows changing the model for any configured provider
func (pw *ProviderWizard) handleChangeModel() error {
// Step 1: Get current provider configurations
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
if err != nil {
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
}
// Step 2: Get all configured providers with models
readyProviders := result.GetAllReadyProviders()
// Filter out Cline provider (it has its own model changer in the main menu)
var configurableProviders []*ProviderDisplay
for _, provider := range readyProviders {
if provider.Provider != cline.ApiProvider_CLINE {
configurableProviders = append(configurableProviders, provider)
}
}
// Step 3: Check if there are any configurable providers
if len(configurableProviders) == 0 {
fmt.Println("\nNo configurable providers found.")
fmt.Println("Note: Cline provider has its own model selection in the main menu.")
return nil
}
// Step 4: Let user select which provider to change the model for
var selectedIndex int
options := make([]huh.Option[int], len(configurableProviders)+1)
for i, providerDisplay := range configurableProviders {
displayName := fmt.Sprintf("%s (current: %s)",
getProviderDisplayName(providerDisplay.Provider),
providerDisplay.ModelID)
options[i] = huh.NewOption(displayName, i)
}
options[len(configurableProviders)] = huh.NewOption("(Cancel)", -1)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[int]().
Title("Select provider to change model for").
Options(options...).
Value(&selectedIndex),
),
)
if err := form.Run(); err != nil {
return fmt.Errorf("failed to select provider: %w", err)
}
if selectedIndex == -1 {
return nil
}
selectedProvider := configurableProviders[selectedIndex]
provider := selectedProvider.Provider
fmt.Printf("\nChanging model for %s\n", getProviderDisplayName(provider))
fmt.Printf("Current model: %s\n\n", selectedProvider.ModelID)
// Step 5: Retrieve API key if needed for model fetching
var apiKey string
if pw.supportsModelFetching(provider) {
// For providers that support fetching, we need to retrieve the API key from state
state, err := pw.manager.GetClient().State.GetLatestState(pw.ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to get state: %w", err)
}
var stateData map[string]interface{}
if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
return fmt.Errorf("failed to parse state JSON: %w", err)
}
apiConfig, ok := stateData["apiConfiguration"].(map[string]interface{})
if !ok {
return fmt.Errorf("no API configuration found in state")
}
apiKey = getProviderAPIKeyFromState(apiConfig, provider)
if apiKey == "" {
return fmt.Errorf("no API key found for provider %s", getProviderDisplayName(provider))
}
}
modelID, modelInfo, err := pw.selectModel(provider, apiKey)
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Step 6: Apply the model change (for both Plan and Act modes)
if err := pw.applyModelChange(provider, modelID, modelInfo); err != nil {
return fmt.Errorf("failed to apply model change: %w", err)
}
fmt.Printf("✓ Model changed successfully to: %s\n", modelID)
fmt.Println(" (Applied to both Plan and Act modes)")
return nil
}
// applyModelChange applies a model change for both Plan and Act modes using UpdateProviderPartial
func (pw *ProviderWizard) applyModelChange(provider cline.ApiProvider, modelID string, modelInfo interface{}) error {
updates := ProviderUpdatesPartial{
ModelID: &modelID,
ModelInfo: modelInfo,
}
return UpdateProviderPartial(pw.ctx, pw.manager, provider, updates, false)
}
// SwitchToBYOProvider switches to a BYO provider that's already configured.
// It retrieves the existing model configuration and sets it as the active provider for both Plan and Act modes.
func SwitchToBYOProvider(ctx context.Context, manager *task.Manager, provider cline.ApiProvider) error {
// Get the current state to retrieve the model ID and model info for this provider
state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to get state: %w", err)
}
// Parse state JSON
var stateData map[string]interface{}
if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
return fmt.Errorf("failed to parse state JSON: %w", err)
}
// Extract apiConfiguration
apiConfig, ok := stateData["apiConfiguration"].(map[string]interface{})
if !ok {
return fmt.Errorf("no API configuration found in state")
}
// Get the model ID for the selected provider
modelID := getProviderModelIDFromState(apiConfig, provider)
if modelID == "" {
return fmt.Errorf("no model configured for provider %s", getProviderDisplayName(provider))
}
// Get model info if available (for OpenRouter/Cline)
var modelInfo interface{}
if provider == cline.ApiProvider_OPENROUTER || provider == cline.ApiProvider_CLINE {
if modelInfoData, ok := apiConfig["planModeOpenRouterModelInfo"].(map[string]interface{}); ok {
modelInfo = convertMapToOpenRouterModelInfo(modelInfoData)
}
}
// Use UpdateProviderPartial to switch to this provider
updates := ProviderUpdatesPartial{
ModelID: &modelID,
ModelInfo: modelInfo,
}
if err := UpdateProviderPartial(ctx, manager, provider, updates, true); err != nil {
return fmt.Errorf("failed to switch provider: %w", err)
}
verboseLog("✓ Switched to %s\n", getProviderDisplayName(provider))
verboseLog(" Using model: %s\n", modelID)
return HandleAuthMenuNoArgs(ctx)
}
// getProviderModelIDFromState retrieves the model ID for a specific provider from state
func getProviderModelIDFromState(stateData map[string]interface{}, provider cline.ApiProvider) string {
modelKey, err := GetModelIDFieldName(provider, "plan")
if err != nil {
return ""
}
if modelID, ok := stateData[modelKey].(string); ok {
return modelID
}
return ""
}
// getProviderAPIKeyFromState retrieves the API key for a specific provider from state
func getProviderAPIKeyFromState(stateData map[string]interface{}, provider cline.ApiProvider) string {
fields, err := GetProviderFields(provider)
if err != nil {
return ""
}
if apiKey, ok := stateData[fields.APIKeyField].(string); ok {
return apiKey
}
return ""
}
// convertMapToOpenRouterModelInfo converts a map to OpenRouterModelInfo
func convertMapToOpenRouterModelInfo(data map[string]interface{}) *cline.OpenRouterModelInfo {
info := &cline.OpenRouterModelInfo{}
if val, ok := data["description"].(string); ok {
info.Description = &val
}
if val, ok := data["contextWindow"].(float64); ok {
contextWindow := int64(val)
info.ContextWindow = &contextWindow
}
if val, ok := data["maxTokens"].(float64); ok {
maxTokens := int64(val)
info.MaxTokens = &maxTokens
}
if val, ok := data["inputPrice"].(float64); ok {
info.InputPrice = &val
}
if val, ok := data["outputPrice"].(float64); ok {
info.OutputPrice = &val
}
if val, ok := data["cacheWritesPrice"].(float64); ok {
info.CacheWritesPrice = &val
}
if val, ok := data["cacheReadsPrice"].(float64); ok {
info.CacheReadsPrice = &val
}
if val, ok := data["supportsImages"].(bool); ok {
info.SupportsImages = &val
}
if val, ok := data["supportsPromptCache"].(bool); ok {
info.SupportsPromptCache = val
}
return info
}
// handleRemoveProvider allows removing a configured provider by clearing its API key
func (pw *ProviderWizard) handleRemoveProvider() error {
// Step 1: Get current provider configurations
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
if err != nil {
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
}
// Step 2: Get all ready providers
readyProviders := result.GetAllReadyProviders()
// Filter out Cline provider (uses account auth, not API keys)
var removableProviders []*ProviderDisplay
for _, provider := range readyProviders {
if provider.Provider != cline.ApiProvider_CLINE {
removableProviders = append(removableProviders, provider)
}
}
// Step 3: Check if there are providers to remove
if len(removableProviders) == 0 {
fmt.Println("\nNo providers available to remove.")
fmt.Println("Note: Cline provider cannot be removed via this menu.")
return nil
}
// Step 4: Display selection menu
var selectedIndex int
options := make([]huh.Option[int], len(removableProviders))
for i, provider := range removableProviders {
// Mark active provider
displayName := getProviderDisplayName(provider.Provider)
if result.ActProvider != nil && provider.Provider == result.ActProvider.Provider {
displayName += " (ACTIVE)"
}
options[i] = huh.NewOption(displayName, i)
}
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[int]().
Title("Select provider to remove").
Options(options...).
Value(&selectedIndex),
),
)
if err := form.Run(); err != nil {
return fmt.Errorf("failed to select provider: %w", err)
}
selectedProvider := removableProviders[selectedIndex]
// Step 5: Check if trying to remove the active provider
if result.ActProvider != nil && selectedProvider.Provider == result.ActProvider.Provider {
fmt.Printf("\nCannot remove %s because it is currently active.\n", getProviderDisplayName(selectedProvider.Provider))
fmt.Println("Please switch to a different provider first, then try again.")
return nil
}
// Step 6: Confirm removal
var confirm bool
confirmForm := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(fmt.Sprintf("Are you sure you want to remove %s?", getProviderDisplayName(selectedProvider.Provider))).
Description("This will clear the API key but preserve the model configuration.").
Value(&confirm),
),
)
if err := confirmForm.Run(); err != nil {
return fmt.Errorf("failed to get confirmation: %w", err)
}
if !confirm {
fmt.Println("Removal cancelled.")
return nil
}
// Step 7: Clear the API key for the selected provider
if err := pw.clearProviderAPIKey(selectedProvider.Provider); err != nil {
return fmt.Errorf("failed to remove provider: %w", err)
}
fmt.Printf("\n✓ %s removed successfully\n", getProviderDisplayName(selectedProvider.Provider))
return nil
}
// clearProviderAPIKey clears the API key field for a specific provider using RemoveProviderPartial
func (pw *ProviderWizard) clearProviderAPIKey(provider cline.ApiProvider) error {
return RemoveProviderPartial(pw.ctx, pw.manager, provider)
}
-193
View File
@@ -1,193 +0,0 @@
package auth
import (
"context"
"fmt"
"strings"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// BedrockConfig holds all AWS Bedrock-specific configuration fields
type BedrockConfig struct {
// Profile authentication fields
UseProfile bool // Always true for successful config
Profile string // Optional: AWS profile name (empty = default)
Region string // Required: AWS region
Endpoint string // Optional: Custom VPC endpoint URL
// Optional features
UseCrossRegionInference bool // Optional: Enable cross-region inference
UseGlobalInference bool // Optional: Use global inference endpoint
UsePromptCache bool // Optional: Enable prompt caching
// Authentication method (always "profile")
Authentication string // Always set to "profile"
// Legacy fields (no longer used in profile-only flow)
AccessKey string // No longer used
SecretKey string // No longer used
SessionToken string // No longer used
}
// PromptForBedrockConfig displays a profile-first authentication form for Bedrock configuration
func PromptForBedrockConfig(ctx context.Context, manager *task.Manager) (*BedrockConfig, error) {
config := &BedrockConfig{}
// First, ask if user wants to use AWS profile authentication
var useProfile bool
profileQuestion := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("Do you want to use an AWS profile for authentication?").
Description("AWS profiles are managed via 'aws configure'").
Value(&useProfile).
Affirmative("Yes").
Negative("No").
Inline(true),
),
)
if err := profileQuestion.Run(); err != nil {
return nil, fmt.Errorf("failed to get authentication method: %w", err)
}
// If user declines profile authentication, show message and return error
if !useProfile {
fmt.Println("\nAWS profile authentication is currently the only supported method in the CLI.")
fmt.Println("Please configure an AWS profile using 'aws configure' and try again.")
return nil, fmt.Errorf("user declined profile authentication")
}
// User wants profile auth - collect profile configuration
config.UseProfile = true
config.Authentication = "profile"
// Collect profile name, region, and optional settings
configForm := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("AWS Profile Name (optional, press Enter for default profile)").
Value(&config.Profile).
Description("Leave empty to use default AWS profile"),
huh.NewInput().
Title("AWS Region (required, e.g., us-east-1)").
Value(&config.Region).
Validate(func(s string) error {
if strings.TrimSpace(s) == "" {
return fmt.Errorf("AWS Region is required")
}
return nil
}),
huh.NewInput().
Title("Custom VPC Endpoint URL (optional)").
Value(&config.Endpoint).
Description("Press Enter to skip"),
huh.NewConfirm().
Title("Enable Prompt Cache? ").
Value(&config.UsePromptCache).
Affirmative("Yes").
Negative("No").
Inline(true),
huh.NewConfirm().
Title("Enable Cross-Region Inference? ").
Value(&config.UseCrossRegionInference).
Affirmative("Yes").
Negative("No").
Inline(true),
huh.NewConfirm().
Title("Use Global Inference Endpoint? ").
Value(&config.UseGlobalInference).
Affirmative("Yes").
Negative("No").
Inline(true),
),
)
if err := configForm.Run(); err != nil {
return nil, fmt.Errorf("failed to get Bedrock configuration: %w", err)
}
// Trim whitespace from string fields
config.Profile = strings.TrimSpace(config.Profile)
config.Region = strings.TrimSpace(config.Region)
config.Endpoint = strings.TrimSpace(config.Endpoint)
return config, nil
}
// ApplyBedrockConfig applies Bedrock configuration using partial updates (profile-only)
func ApplyBedrockConfig(ctx context.Context, manager *task.Manager, config *BedrockConfig, modelID string, modelInfo interface{}) error {
// Build the API configuration with all Bedrock fields
apiConfig := &cline.ModelsApiConfiguration{}
// Set model ID fields
apiConfig.PlanModeApiModelId = proto.String(modelID)
apiConfig.ActModeApiModelId = proto.String(modelID)
apiConfig.PlanModeAwsBedrockCustomModelBaseId = proto.String(modelID)
apiConfig.ActModeAwsBedrockCustomModelBaseId = proto.String(modelID)
// Set profile authentication fields (always required)
optionalFields := &BedrockOptionalFields{}
optionalFields.Authentication = proto.String("profile")
optionalFields.UseProfile = proto.Bool(true)
optionalFields.Region = proto.String(config.Region)
// Set profile name (can be empty for default profile)
if config.Profile != "" {
optionalFields.Profile = proto.String(config.Profile)
}
// Set optional fields if provided
if config.Endpoint != "" {
optionalFields.Endpoint = proto.String(config.Endpoint)
}
if config.UseCrossRegionInference {
optionalFields.UseCrossRegionInference = proto.Bool(true)
}
if config.UseGlobalInference {
optionalFields.UseGlobalInference = proto.Bool(true)
}
if config.UsePromptCache {
optionalFields.UsePromptCache = proto.Bool(true)
}
// Apply all fields to the config
setBedrockOptionalFields(apiConfig, optionalFields)
// Build field mask including all fields we're setting (excluding access keys)
fieldPaths := []string{
"planModeApiModelId",
"actModeApiModelId",
"planModeAwsBedrockCustomModelBaseId",
"actModeAwsBedrockCustomModelBaseId",
}
// Add profile authentication field paths
optionalPaths := buildBedrockOptionalFieldMask(optionalFields)
fieldPaths = append(fieldPaths, optionalPaths...)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
// Apply the partial update
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to apply Bedrock configuration: %w", err)
}
return nil
}
-149
View File
@@ -1,149 +0,0 @@
package cli
import (
"context"
"fmt"
"github.com/cline/cli/pkg/cli/config"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/spf13/cobra"
)
var configManager *config.Manager
func ensureConfigManager(ctx context.Context, address string) error {
if configManager == nil || (address != "" && configManager.GetCurrentInstance() != address) {
var err error
var instanceAddress string
if address != "" {
// Ensure instance exists at the specified address
if err := ensureInstanceAtAddress(ctx, address); err != nil {
return fmt.Errorf("failed to ensure instance at address %s: %w", address, err)
}
configManager, err = config.NewManager(ctx, address)
instanceAddress = address
} else {
// Ensure default instance exists
if err := global.EnsureDefaultInstance(ctx); err != nil {
return fmt.Errorf("failed to ensure default instance: %w", err)
}
configManager, err = config.NewManager(ctx, "")
if err == nil {
instanceAddress = configManager.GetCurrentInstance()
}
}
if err != nil {
return fmt.Errorf("failed to create config manager: %w", err)
}
// Always set the instance we're using as the default
registry := global.Clients.GetRegistry()
if err := registry.SetDefaultInstance(instanceAddress); err != nil {
// Log warning but don't fail - this is not critical
fmt.Printf("Warning: failed to set default instance: %v\n", err)
}
}
return nil
}
func NewConfigCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Aliases: []string{"c"},
Short: "Manage Cline configuration",
Long: `Set and manage global Cline configuration variables.`,
}
cmd.AddCommand(newConfigListCommand())
cmd.AddCommand(newConfigGetCommand())
cmd.AddCommand(setCommand())
return cmd
}
func newConfigGetCommand() *cobra.Command {
var address string
cmd := &cobra.Command{
Use: "get <key>",
Aliases: []string{"g"},
Short: "Get a specific configuration value",
Long: `Get the value of a specific configuration setting. Supports nested keys using dot notation (e.g., auto-approval-settings.actions.read-files).`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
key := args[0]
// Ensure config manager
if err := ensureConfigManager(ctx, address); err != nil {
return err
}
// Get the setting
return configManager.GetSetting(ctx, key)
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
func newConfigListCommand() *cobra.Command {
var address string
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
Short: "List all configuration settings",
Long: `List all configuration settings from the Cline instance.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Ensure config manager
if err := ensureConfigManager(ctx, address); err != nil {
return err
}
// List settings
return configManager.ListSettings(ctx)
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
func setCommand() *cobra.Command {
var address string
cmd := &cobra.Command{
Use: "set <key=value> [key=value...]",
Aliases: []string{"s"},
Short: "Set configuration variables",
Long: `Set one or more global configuration variables using key=value format.`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Parse using existing task parser
settings, secrets, err := task.ParseTaskSettings(args)
if err != nil {
return fmt.Errorf("failed to parse settings: %w", err)
}
// Ensure config manager
if err := ensureConfigManager(ctx, address); err != nil {
return err
}
// Update settings
return configManager.UpdateSettings(ctx, settings, secrets)
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
-208
View File
@@ -1,208 +0,0 @@
package config
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/grpc-go/client"
"github.com/cline/grpc-go/cline"
)
type Manager struct {
client *client.ClineClient
clientAddress string
}
func NewManager(ctx context.Context, address string) (*Manager, error) {
var c *client.ClineClient
var err error
if address != "" {
c, err = global.GetClientForAddress(ctx, address)
} else {
c, err = global.GetDefaultClient(ctx)
}
if err != nil {
return nil, fmt.Errorf("failed to get client: %w", err)
}
// Get the actual address being used
clientAddress := address
if address == "" && global.Clients != nil {
clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
}
return &Manager{
client: c,
clientAddress: clientAddress,
}, nil
}
// GetCurrentInstance returns the address of the current instance
func (m *Manager) GetCurrentInstance() string {
return m.clientAddress
}
func (m *Manager) UpdateSettings(ctx context.Context, settings *cline.Settings, secrets *cline.Secrets) error {
request := &cline.UpdateSettingsRequestCli{
Metadata: &cline.Metadata{},
Settings: settings,
Secrets: secrets,
}
// Call the updateSettingsCli RPC
_, err := m.client.State.UpdateSettingsCli(ctx, request)
if err != nil {
return fmt.Errorf("failed to update settings: %w", err)
}
fmt.Println("Settings updated successfully")
fmt.Printf("Instance: %s\n", m.clientAddress)
return nil
}
func (m *Manager) GetState(ctx context.Context) (map[string]interface{}, error) {
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return nil, fmt.Errorf("failed to get state: %w", err)
}
var stateData map[string]interface{}
if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
return nil, fmt.Errorf("failed to parse state: %w", err)
}
return stateData, nil
}
func (m *Manager) ListSettings(ctx context.Context) error {
// Get state
stateData, err := m.GetState(ctx)
if err != nil {
return err
}
// Subset of fields we will print the values for
settingsFields := []string{
"apiConfiguration",
"telemetrySetting",
"planActSeparateModelsSetting",
"enableCheckpointsSetting",
"mcpMarketplaceEnabled",
"shellIntegrationTimeout",
"terminalReuseEnabled",
"mcpResponsesCollapsed",
"mcpDisplayMode",
"terminalOutputLineLimit",
"mode",
"preferredLanguage",
"openaiReasoningEffort",
"strictPlanModeEnabled",
"focusChainSettings",
"useAutoCondense",
"customPrompt",
"browserSettings",
"defaultTerminalProfile",
"yoloModeToggled",
"dictationSettings",
"autoCondenseThreshold",
"autoApprovalSettings",
}
// Render each field using the renderer
for _, field := range settingsFields {
if value, ok := stateData[field]; ok {
if err := RenderField(field, value); err != nil {
fmt.Printf("Error rendering %s: %v\n", field, err)
}
fmt.Println()
}
}
return nil
}
func (m *Manager) GetSetting(ctx context.Context, key string) error {
// Get state
stateData, err := m.GetState(ctx)
if err != nil {
return err
}
// Convert kebab-case to camelCase path
parts := kebabToCamelPath(key)
rootField := parts[0]
// Get the value
value, found := getNestedValue(stateData, parts)
if !found {
return fmt.Errorf("setting '%s' not found", key)
}
// Render the value
if len(parts) == 1 {
// Top-level field: use RenderField for nice formatting
return RenderField(rootField, value)
} else {
// Nested field: simple print
fmt.Printf("%s: %s\n", key, formatValue(value))
}
return nil
}
// kebabToCamelPath converts a kebab-case path to camelCase
// e.g., "auto-approval-settings.actions.read-files" -> "autoApprovalSettings.actions.readFiles"
func kebabToCamelPath(path string) []string {
parts := strings.Split(path, ".")
for i, part := range parts {
parts[i] = kebabToCamel(part)
}
return parts
}
// kebabToCamel converts a single kebab-case string to camelCase
// e.g., "auto-approval-settings" -> "autoApprovalSettings"
func kebabToCamel(s string) string {
if s == "" {
return s
}
parts := strings.Split(s, "-")
if len(parts) == 1 {
return s
}
// First part stays lowercase, rest are capitalized
result := parts[0]
for i := 1; i < len(parts); i++ {
if parts[i] != "" {
result += strings.ToUpper(parts[i][:1]) + parts[i][1:]
}
}
return result
}
// getNestedValue retrieves a value from a nested map using dot notation
// e.g., "autoApprovalSettings.actions.readFiles"
func getNestedValue(data map[string]interface{}, parts []string) (interface{}, bool) {
current := interface{}(data)
for _, part := range parts {
// Try to access as map
if m, ok := current.(map[string]interface{}); ok {
if val, exists := m[part]; exists {
current = val
continue
}
return nil, false
}
return nil, false
}
return current, true
}
-170
View File
@@ -1,170 +0,0 @@
package config
import (
"fmt"
)
// camelToKebab converts camelCase to kebab-case
// e.g., "autoApprovalSettings" -> "auto-approval-settings"
func camelToKebab(s string) string {
if s == "" {
return s
}
var result []rune
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
result = append(result, '-')
}
result = append(result, r|32) // Convert to lowercase (works for A-Z)
}
return string(result)
}
// formatValue formats a value for display, handling empty strings
func formatValue(val interface{}) string {
// Handle empty strings specifically
if str, ok := val.(string); ok && str == "" {
return "''"
}
return fmt.Sprintf("%v", val)
}
// RenderField renders a single config field with proper formatting
func RenderField(key string, value interface{}) error {
switch key {
// Nested objects - render with header + nested fields
case "apiConfiguration":
return renderApiConfiguration(value)
case "browserSettings":
return renderBrowserSettings(value)
case "focusChainSettings":
return renderFocusChainSettings(value)
case "dictationSettings":
return renderDictationSettings(value)
case "autoApprovalSettings":
return renderAutoApprovalSettings(value)
// Simple values - just print key: value
case "mode", "telemetrySetting", "preferredLanguage", "customPrompt",
"defaultTerminalProfile", "mcpDisplayMode", "openaiReasoningEffort",
"planActSeparateModelsSetting", "enableCheckpointsSetting",
"mcpMarketplaceEnabled", "terminalReuseEnabled",
"mcpResponsesCollapsed", "strictPlanModeEnabled",
"useAutoCondense", "yoloModeToggled", "shellIntegrationTimeout",
"terminalOutputLineLimit", "autoCondenseThreshold":
fmt.Printf("%s: %s\n", camelToKebab(key), formatValue(value))
return nil
default:
return fmt.Errorf("unknown config field: %s", key)
}
}
// renderApiConfiguration renders the API configuration object
func renderApiConfiguration(value interface{}) error {
fmt.Println("api-configuration:")
configMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid api-configuration format")
}
// Print each field directly
for key, val := range configMap {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val))
}
return nil
}
// renderBrowserSettings renders browser settings
func renderBrowserSettings(value interface{}) error {
fmt.Println("browser-settings:")
settingsMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid browser-settings format")
}
// Handle nested viewport if present
if viewport, ok := settingsMap["viewport"].(map[string]interface{}); ok {
fmt.Println(" viewport:")
for key, val := range viewport {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val))
}
}
// Print other fields
for key, val := range settingsMap {
if key != "viewport" {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val))
}
}
return nil
}
// renderFocusChainSettings renders focus chain settings
func renderFocusChainSettings(value interface{}) error {
fmt.Println("focus-chain-settings:")
settingsMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid focus-chain-settings format")
}
for key, val := range settingsMap {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val))
}
return nil
}
// renderDictationSettings renders dictation settings
func renderDictationSettings(value interface{}) error {
fmt.Println("dictation-settings:")
settingsMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid dictation-settings format")
}
for key, val := range settingsMap {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val))
}
return nil
}
// renderAutoApprovalSettings renders auto approval settings
func renderAutoApprovalSettings(value interface{}) error {
fmt.Println("auto-approval-settings:")
settingsMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid auto-approval-settings format")
}
// Print top-level fields (skip version, handle actions specially)
for key, val := range settingsMap {
if key == "version" {
continue // Skip version
}
if key == "actions" {
// Handle nested actions with double indentation
fmt.Println(" actions:")
if actionsMap, ok := val.(map[string]interface{}); ok {
for actionKey, actionVal := range actionsMap {
fmt.Printf(" %s: %s\n", camelToKebab(actionKey), formatValue(actionVal))
}
}
} else {
// Print other fields normally (enabled, maxRequests, enableNotifications, favorites)
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val))
}
}
return nil
}
-27
View File
@@ -1,27 +0,0 @@
package display
import (
"fmt"
"os"
"golang.org/x/term"
)
func isTTY() bool {
return term.IsTerminal(int(os.Stdout.Fd()))
}
func ClearLine() {
if !isTTY() {
return
}
fmt.Print("\r\033[K")
}
// ClearToEnd clears from cursor to end of screen
func ClearToEnd() {
if !isTTY() {
return
}
fmt.Print("\033[J")
}
-52
View File
@@ -1,52 +0,0 @@
package display
import (
"os"
"strings"
"github.com/charmbracelet/glamour"
"golang.org/x/term"
)
type MarkdownRenderer struct {
renderer *glamour.TermRenderer
width int
}
func NewMarkdownRenderer() (*MarkdownRenderer, error) {
width := getTerminalWidth()
r, err := glamour.NewTermRenderer(
glamour.WithStandardStyle("auto"),
glamour.WithWordWrap(width),
glamour.WithPreservedNewLines(),
)
if err != nil {
return nil, err
}
return &MarkdownRenderer{
renderer: r,
width: width,
}, nil
}
func (mr *MarkdownRenderer) Render(markdown string) (string, error) {
rendered, err := mr.renderer.Render(markdown)
if err != nil {
return "", err
}
return strings.TrimLeft(strings.TrimRight(rendered, "\n"), "\n"), nil
}
func getTerminalWidth() int {
width, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || width == 0 {
return 120
}
if width > 150 {
return 150
}
return width
}
+23 -68
View File
@@ -3,6 +3,7 @@ package display
import (
"fmt"
"strings"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/types"
@@ -10,55 +11,36 @@ import (
)
type Renderer struct {
typewriter *TypewriterPrinter
mdRenderer *MarkdownRenderer
outputFormat string
typewriter *TypewriterPrinter
}
func NewRenderer(outputFormat string) *Renderer {
mdRenderer, err := NewMarkdownRenderer()
if err != nil {
mdRenderer = nil
}
func NewRenderer() *Renderer {
return &Renderer{
typewriter: NewTypewriterPrinter(DefaultTypewriterConfig()),
mdRenderer: mdRenderer,
outputFormat: outputFormat,
typewriter: NewTypewriterPrinter(DefaultTypewriterConfig()),
}
}
func (r *Renderer) RenderMessage(prefix, text string, newline bool) error {
// RenderMessage renders a message with timestamp and prefix
func (r *Renderer) RenderMessage(timestamp, prefix, text string) error {
if text == "" {
return nil
}
clean := r.sanitizeText(text)
if clean == "" {
cleanText := r.sanitizeText(text)
if cleanText == "" {
return nil
}
if newline {
fmt.Printf("%s: %s\n", prefix, clean)
} else {
fmt.Printf("%s: %s", prefix, clean)
}
r.typewriter.PrintMessageLine(timestamp, prefix, cleanText)
return nil
}
func (r *Renderer) RenderCheckpointMessage(timestamp, prefix string, id int64) error {
markdown := fmt.Sprintf("## [%s] Checkpoint created `%d`", timestamp, id)
rendered := r.RenderMarkdown(markdown)
fmt.Printf(rendered)
return nil
}
func (r *Renderer) RenderCommand(command string, isExecuting bool) error {
// RenderCommand renders a command execution
func (r *Renderer) RenderCommand(timestamp, command string, isExecuting bool) error {
if isExecuting {
r.typewriter.PrintMessageLine("EXEC", command)
r.typewriter.PrintMessageLine(timestamp, "EXEC", command)
} else {
r.typewriter.PrintMessageLine("CMD", command)
r.typewriter.PrintMessageLine(timestamp, "CMD", command)
}
return nil
}
@@ -84,28 +66,25 @@ func (r *Renderer) formatUsageInfo(tokensIn, tokensOut, cacheReads, cacheWrites
return fmt.Sprintf("%s ($%.4f)", tokenDetails, cost)
}
func (r *Renderer) RenderAPI(status string, apiInfo *types.APIRequestInfo) error {
// RenderAPI renders API request information
func (r *Renderer) RenderAPI(timestamp, status string, apiInfo *types.APIRequestInfo) error {
if apiInfo.Cost >= 0 {
usageInfo := r.formatUsageInfo(apiInfo.TokensIn, apiInfo.TokensOut, apiInfo.CacheReads, apiInfo.CacheWrites, apiInfo.Cost)
markdown := fmt.Sprintf("## API %s `%s`", status, usageInfo)
rendered := r.RenderMarkdown(markdown)
fmt.Printf(rendered)
message := fmt.Sprintf("%s %s", status, r.formatUsageInfo(apiInfo.TokensIn, apiInfo.TokensOut, apiInfo.CacheReads, apiInfo.CacheWrites, apiInfo.Cost))
r.typewriter.PrintMessageLine(timestamp, "API INFO", message)
} else {
// honestly i see no point in showing "### API processing request" here...
// markdown := fmt.Sprintf("## API %s", status)
// rendered := r.RenderMarkdown(markdown)
// fmt.Printf("\n%s\n", rendered)
r.typewriter.PrintMessageLine(timestamp, "API INFO", status)
}
return nil
}
func (r *Renderer) RenderRetry(attempt, maxAttempts, delaySec int) error {
// RenderRetry renders retry information
func (r *Renderer) RenderRetry(timestamp string, attempt, maxAttempts, delaySec int) error {
message := fmt.Sprintf("Retrying failed attempt %d/%d", attempt, maxAttempts)
if delaySec > 0 {
message += fmt.Sprintf(" in %d seconds", delaySec)
}
message += "..."
r.typewriter.PrintMessageLine("API INFO", message)
r.typewriter.PrintMessageLine(timestamp, "API INFO", message)
return nil
}
@@ -145,8 +124,9 @@ func (r *Renderer) RenderTaskList(tasks []*cline.TaskItem) error {
func (r *Renderer) RenderDebug(format string, args ...interface{}) error {
if global.Config.Verbose {
timestamp := time.Now().Format("15:04:05")
message := fmt.Sprintf(format, args...)
r.typewriter.PrintMessageLine("[DEBUG]", message)
r.typewriter.PrintMessageLine(timestamp, "[DEBUG]", message)
}
return nil
}
@@ -194,28 +174,3 @@ func (r *Renderer) SetTypewriterSpeed(multiplier float64) {
func (r *Renderer) GetTypewriter() *TypewriterPrinter {
return r.typewriter
}
func (r *Renderer) GetMdRenderer() *MarkdownRenderer {
return r.mdRenderer
}
// RenderMarkdown renders markdown text to terminal format with ANSI codes
// Falls back to plaintext if markdown rendering is unavailable or fails
// Respects output format - skips rendering in plain mode
func (r *Renderer) RenderMarkdown(markdown string) string {
// Skip markdown rendering in plain mode
if r.outputFormat == "plain" {
return markdown
}
if r.mdRenderer == nil {
return markdown
}
rendered, err := r.mdRenderer.Render(markdown)
if err != nil {
return markdown
}
return rendered
}
-242
View File
@@ -1,242 +0,0 @@
package display
import (
"encoding/json"
"fmt"
"strings"
"sync"
"github.com/cline/cli/pkg/cli/types"
)
type StreamingSegment struct {
mu sync.Mutex
sayType string
prefix string
buffer strings.Builder
frozen bool
mdRenderer *MarkdownRenderer
shouldMarkdown bool
outputFormat string
msg *types.ClineMessage
toolParser *ToolResultParser
}
func NewStreamingSegment(sayType, prefix string, mdRenderer *MarkdownRenderer, shouldMarkdown bool, msg *types.ClineMessage, outputFormat string) *StreamingSegment {
ss := &StreamingSegment{
sayType: sayType,
prefix: prefix,
mdRenderer: mdRenderer,
shouldMarkdown: shouldMarkdown,
outputFormat: outputFormat,
msg: msg,
toolParser: NewToolResultParser(mdRenderer),
}
// Render rich header immediately when creating segment (if in rich mode)
if shouldMarkdown && outputFormat != "plain" {
header := ss.generateRichHeader()
rendered, _ := mdRenderer.Render(header)
fmt.Println()
fmt.Print(rendered)
}
return ss
}
func (ss *StreamingSegment) AppendText(text string) {
ss.mu.Lock()
defer ss.mu.Unlock()
if ss.frozen {
return
}
// Replace buffer with FULL text - msg.Text contains complete accumulated content
ss.buffer.Reset()
ss.buffer.WriteString(text)
// No rendering during streaming - we'll render once on Freeze()
}
func (ss *StreamingSegment) Freeze() {
ss.mu.Lock()
defer ss.mu.Unlock()
if ss.frozen {
return
}
ss.frozen = true
currentBuffer := ss.buffer.String()
// Render and print the final markdown
ss.renderFinal(currentBuffer)
}
func (ss *StreamingSegment) renderFinal(currentBuffer string) {
// For ASK messages, parse JSON and extract response field
text := currentBuffer
if ss.sayType == "ask" {
var askData types.AskData
if err := json.Unmarshal([]byte(currentBuffer), &askData); err == nil {
// Use the response field as the text to render
text = askData.Response
// Add options if available
if len(askData.Options) > 0 {
text += "\n\nOptions:\n"
for i, option := range askData.Options {
text += fmt.Sprintf("%d. %s\n", i+1, option)
}
}
}
}
// For tools, parse JSON and render with enhanced formatting
if ss.sayType == string(types.SayTypeTool) {
var tool types.ToolMessage
if err := json.Unmarshal([]byte(currentBuffer), &tool); err == nil {
// Use tool parser for enhanced rendering
switch tool.Tool {
case "listFilesTopLevel", "listFilesRecursive",
"listCodeDefinitionNames", "searchFiles", "webFetch":
// Use enhanced tool result parser for final render
text = ss.toolParser.ParseToolResult(&tool)
case "readFile":
// readFile: show header only, no body
return
case "editedExistingFile":
// Show the diff (stored in Content field)
if tool.Content != "" {
text = "```diff\n" + tool.Content + "\n```"
} else {
return // No diff, just header
}
default:
// Other tools: suppress JSON body for now
return
}
}
}
if ss.sayType == string(types.SayTypeCommand) {
text = "```shell\n" + text + "\n```"
}
var rendered string
if ss.shouldMarkdown && ss.outputFormat != "plain" {
var err error
rendered, err = ss.mdRenderer.Render(text)
if err != nil {
rendered = ss.prefix + ": " + currentBuffer
}
} else {
rendered = ss.prefix + ": " + currentBuffer
}
// Print final render once (no clearing needed, header already printed)
if !strings.HasSuffix(rendered, "\n") {
fmt.Print(rendered)
fmt.Println()
} else {
fmt.Print(rendered)
}
}
// generateRichHeader generates a contextual header for the segment
func (ss *StreamingSegment) generateRichHeader() string {
switch ss.sayType {
case string(types.SayTypeReasoning):
return "### Cline is thinking\n"
case string(types.SayTypeText):
return "### Cline responds\n"
case string(types.SayTypeCompletionResult):
return "### Task completed\n"
case string(types.SayTypeTool):
return ss.generateToolHeader()
case "ask":
// Check the specific ask type
if ss.msg.Ask == string(types.AskTypePlanModeRespond) {
return "### Cline has a plan\n"
}
// For other ask types (tool approvals, questions, etc.), show the ask type
return fmt.Sprintf("### Cline is asking (%s)\n", ss.msg.Ask)
default:
return fmt.Sprintf("### %s\n", ss.prefix)
}
}
// generateToolHeader generates a contextual header for tool operations
func (ss *StreamingSegment) generateToolHeader() string {
// Parse tool JSON from message text
var tool types.ToolMessage
if err := json.Unmarshal([]byte(ss.msg.Text), &tool); err != nil {
return "### Tool operation\n"
}
switch tool.Tool {
case "readFile":
if tool.Path != "" {
return fmt.Sprintf("### Cline is reading `%s`\n", tool.Path)
}
return "### Cline is reading a file\n"
case "writeFile", "newFileCreated":
if tool.Path != "" {
return fmt.Sprintf("### Cline is writing `%s`\n", tool.Path)
}
return "### Cline is writing a file\n"
case "editedExistingFile":
if tool.Path != "" {
return fmt.Sprintf("### Cline is editing `%s`\n", tool.Path)
}
return "### Cline is editing a file\n"
case "searchFiles":
if tool.Regex != "" && tool.Path != "" {
return fmt.Sprintf("### Cline is searching for `%s` in `%s`\n", tool.Regex, tool.Path)
} else if tool.Regex != "" {
return fmt.Sprintf("### Cline is searching for `%s`\n", tool.Regex)
}
return "### Cline is searching files\n"
case "listFilesTopLevel":
if tool.Path != "" {
return fmt.Sprintf("### Cline is listing files in `%s`\n", tool.Path)
}
return "### Cline is listing files\n"
case "listFilesRecursive":
if tool.Path != "" {
return fmt.Sprintf("### Cline is recursively listing files in `%s`\n", tool.Path)
}
return "### Cline is recursively listing files\n"
case "listCodeDefinitionNames":
if tool.Path != "" {
return fmt.Sprintf("### Cline is listing code definitions in `%s`\n", tool.Path)
}
return "### Cline is listing code definitions\n"
case "webFetch":
if tool.Path != "" {
return fmt.Sprintf("### Cline is fetching `%s`\n", tool.Path)
}
return "### Cline is fetching a URL\n"
default:
return fmt.Sprintf("### Tool: %s\n", tool.Tool)
}
}
+51 -198
View File
@@ -11,26 +11,18 @@ import (
// StreamingDisplay manages streaming message display with deduplication
type StreamingDisplay struct {
mu sync.RWMutex
state *types.ConversationState
renderer *Renderer
dedupe *MessageDeduplicator
activeSegment *StreamingSegment
mdRenderer *MarkdownRenderer
mu sync.RWMutex
state *types.ConversationState
renderer *Renderer
dedupe *MessageDeduplicator
}
// NewStreamingDisplay creates a new streaming display manager
func NewStreamingDisplay(state *types.ConversationState, renderer *Renderer) *StreamingDisplay {
mdRenderer, err := NewMarkdownRenderer()
if err != nil {
mdRenderer = nil
}
return &StreamingDisplay{
state: state,
renderer: renderer,
dedupe: NewMessageDeduplicator(),
mdRenderer: mdRenderer,
state: state,
renderer: renderer,
dedupe: NewMessageDeduplicator(),
}
}
@@ -39,66 +31,25 @@ func (s *StreamingDisplay) HandlePartialMessage(msg *types.ClineMessage) error {
s.mu.Lock()
defer s.mu.Unlock()
messageKey := fmt.Sprintf("%d", msg.Timestamp)
timestamp := msg.GetTimestamp()
// Check for deduplication
if s.dedupe.IsDuplicate(msg) {
return nil
}
// Skip if markdown renderer not available, fall back to old behavior
if s.mdRenderer == nil {
messageKey := fmt.Sprintf("%d", msg.Timestamp)
timestamp := msg.GetTimestamp()
streamingMsg := s.state.GetStreamingMessage()
// Get current streaming state
streamingMsg := s.state.GetStreamingMessage()
switch msg.Type {
case types.MessageTypeAsk:
return s.handleStreamingAsk(msg, messageKey, timestamp, streamingMsg)
case types.MessageTypeSay:
return s.handleStreamingSay(msg, messageKey, timestamp, streamingMsg)
default:
return s.renderer.RenderMessage("CLINE", msg.Text, true)
}
switch msg.Type {
case types.MessageTypeAsk:
return s.handleStreamingAsk(msg, messageKey, timestamp, streamingMsg)
case types.MessageTypeSay:
return s.handleStreamingSay(msg, messageKey, timestamp, streamingMsg)
default:
return s.renderer.RenderMessage(timestamp, "🤖", msg.Text)
}
// Segment-based header-only streaming
// Partial stream only shows headers immediately, state stream will handle content bodies
sayType := msg.Say
if msg.Type == types.MessageTypeAsk {
sayType = "ask"
}
// Detect segment boundary
if s.activeSegment != nil && s.activeSegment.sayType != sayType {
// Just cleanup, don't freeze (no body to print)
s.activeSegment = nil
}
// On first partial message for a new segment type, create segment (prints header)
if s.activeSegment == nil && msg.Partial {
shouldMd := s.shouldRenderMarkdown(sayType)
prefix := s.getPrefix(sayType)
// NewStreamingSegment prints the header immediately
s.activeSegment = NewStreamingSegment(sayType, prefix, s.mdRenderer, shouldMd, msg, s.renderer.outputFormat)
// Header printed, done - don't append text or freeze
return nil
}
// For subsequent partial messages, do nothing (header already shown)
if msg.Partial {
return nil
}
// When message is complete (partial=false), render the content body
// Only if we have an active segment (header was shown earlier)
if s.activeSegment != nil {
// Append final text and freeze to render body
s.activeSegment.AppendText(msg.Text)
s.activeSegment.Freeze()
s.activeSegment = nil
}
// If no active segment, partial stream never started - state stream will handle it
return nil
}
// handleStreamingAsk handles streaming ASK messages
@@ -120,8 +71,8 @@ func (s *StreamingDisplay) handleStreamingAsk(msg *types.ClineMessage, messageKe
s.state.SetStreamingMessage(messageKey, cleanText)
}
} else {
// This is a new ASK message
s.finishCurrentStream()
fmt.Println()
s.streamAskMessage(cleanText, timestamp, true)
s.state.SetStreamingMessage(messageKey, cleanText)
}
@@ -132,17 +83,19 @@ func (s *StreamingDisplay) handleStreamingAsk(msg *types.ClineMessage, messageKe
// handleStreamingSay handles streaming SAY messages
func (s *StreamingDisplay) handleStreamingSay(msg *types.ClineMessage, messageKey, timestamp string, streamingMsg *types.StreamingMessage) error {
switch msg.Say {
case string(types.SayTypeText), string(types.SayTypeCompletionResult), string(types.SayTypeReasoning):
case string(types.SayTypeText), string(types.SayTypeCompletionResult):
return s.handleStreamingText(msg, messageKey, timestamp, streamingMsg)
case string(types.SayTypeCommand):
return s.handleStreamingCommand(msg, messageKey, timestamp, streamingMsg)
case string(types.SayTypeCommandOutput):
return s.handleStreamingCommandOutput(msg, messageKey, timestamp, streamingMsg)
case string(types.SayTypeTool):
return s.handleStreamingTool(msg, messageKey, timestamp, streamingMsg)
case string(types.SayTypeShellIntegrationWarning):
return s.handleShellIntegrationWarning(msg, messageKey, timestamp, streamingMsg)
default:
// For non-streaming message types, use regular display
return s.renderer.RenderMessage(s.getMessagePrefix(msg.Say), msg.Text, true)
return s.renderer.RenderMessage(timestamp, s.getMessagePrefix(msg.Say), msg.Text)
}
}
@@ -167,29 +120,20 @@ func (s *StreamingDisplay) handleStreamingText(msg *types.ClineMessage, messageK
s.typewriterPrint(newChars)
s.state.SetStreamingMessage(messageKey, cleanText)
} else {
// Text changed in a non-incremental way - replace the line
s.renderer.ClearLine()
prefix := s.getMessagePrefix(msg.Say)
if msg.Say == string(types.SayTypeReasoning) || msg.Say == string(types.SayTypeText) || msg.Say == string(types.SayTypeCompletionResult) {
s.renderer.typewriter.PrintfInstant("%s: ", prefix)
} else {
s.renderer.typewriter.PrintfInstant("[%s] %s: ", timestamp, prefix)
}
s.renderer.typewriter.PrintfInstant("[%s] %s: ", timestamp, prefix)
s.typewriterPrint(cleanText)
s.state.SetStreamingMessage(messageKey, cleanText)
}
} else {
// This is a new message
s.finishCurrentStream()
fmt.Println()
prefix := s.getMessagePrefix(msg.Say)
s.renderer.typewriter.PrintfInstant("[%s] %s: ", timestamp, prefix)
if msg.Say == string(types.SayTypeReasoning) || msg.Say == string(types.SayTypeText) || msg.Say == string(types.SayTypeCompletionResult) {
s.renderer.typewriter.PrintfInstant("%s: ", prefix)
} else {
s.renderer.typewriter.PrintfInstant("[%s] %s: ", timestamp, prefix)
}
// Add typewriter animation for new messages
s.typewriterPrint(cleanText)
s.state.SetStreamingMessage(messageKey, cleanText)
@@ -211,9 +155,9 @@ func (s *StreamingDisplay) handleStreamingCommand(msg *types.ClineMessage, messa
return nil
}
// Show command being executed with typewriter effect
s.finishCurrentStream()
fmt.Println()
s.renderer.typewriter.PrintfInstant("CMD: ")
s.renderer.typewriter.PrintfInstant("[%s] 🖥️ CMD: ", timestamp)
s.typewriterPrint(cleanText)
fmt.Println()
@@ -240,15 +184,16 @@ func (s *StreamingDisplay) handleStreamingCommandOutput(msg *types.ClineMessage,
s.typewriterPrint(newChars)
s.state.SetStreamingMessage(messageKey, cleanText)
} else {
// Non-incremental change - replace the line
s.renderer.ClearLine()
s.renderer.typewriter.PrintfInstant("OUT: ")
s.renderer.typewriter.PrintfInstant("[%s] 🖥️ OUT: ", timestamp)
s.typewriterPrint(cleanText)
s.state.SetStreamingMessage(messageKey, cleanText)
}
} else {
// New command output message
s.finishCurrentStream()
fmt.Println()
s.renderer.typewriter.PrintfInstant("OUT: ")
s.renderer.typewriter.PrintfInstant("[%s] 🖥️ OUT: ", timestamp)
s.typewriterPrint(cleanText)
s.state.SetStreamingMessage(messageKey, cleanText)
}
@@ -269,9 +214,9 @@ func (s *StreamingDisplay) handleShellIntegrationWarning(msg *types.ClineMessage
return nil
}
// Show a more concise shell integration warning
s.finishCurrentStream()
fmt.Println()
s.renderer.typewriter.PrintfInstant("NOTE: ")
s.renderer.typewriter.PrintfInstant("[%s] ️ NOTE: ", timestamp)
s.typewriterPrint("Command executed (output not streamed due to shell integration)")
fmt.Println()
@@ -285,19 +230,7 @@ func (s *StreamingDisplay) handleStreamingTool(msg *types.ClineMessage, messageK
return nil
}
// Parse the tool JSON to extract structured information
var toolData types.ToolMessage
if err := json.Unmarshal([]byte(cleanText), &toolData); err != nil {
// If parsing fails, just show generic tool message
s.finishCurrentStream()
fmt.Println()
fmt.Printf("TOOL: %s\n", cleanText)
s.state.StreamingMessage.LastToolMessage = cleanText
return nil
}
// Format the tool message nicely
formattedTool := s.formatStructuredToolMessage(&toolData)
formattedTool := s.formatToolMessage(cleanText)
// Check if this is the exact same tool message we just displayed
if streamingMsg.LastToolMessage == formattedTool {
@@ -306,12 +239,12 @@ func (s *StreamingDisplay) handleStreamingTool(msg *types.ClineMessage, messageK
// Check if this is a very similar tool message
if streamingMsg.LastToolMessage != "" && s.isSimilarToolMessage(streamingMsg.LastToolMessage, formattedTool) {
return nil
return nil // Similar duplicate - ignore it
}
// This is a genuinely new/different tool message
s.finishCurrentStream()
fmt.Println()
fmt.Printf("TOOL: %s\n", formattedTool)
fmt.Printf("[%s] 🔧 TOOL: %s\n", timestamp, formattedTool)
// Store the formatted tool message for deduplication
s.state.StreamingMessage.LastToolMessage = formattedTool
@@ -324,11 +257,12 @@ func (s *StreamingDisplay) streamAskMessage(text, timestamp string, isNew bool)
// Try to parse as JSON
var askData types.AskData
if err := s.parseJSON(text, &askData); err != nil {
fmt.Printf("ASK: %s", text)
// Display as text but sanitized
fmt.Printf("[%s] 🤖 ASK: %s", timestamp, text)
return
}
fmt.Printf("ASK: %s", askData.Response)
fmt.Printf("[%s] 🤖 ASK: %s", timestamp, askData.Response)
// Display options if available
if len(askData.Options) > 0 {
@@ -354,7 +288,7 @@ func (s *StreamingDisplay) streamAskMessageUpdate(newText, oldText, timestamp st
} else {
// Non-incremental change - clear line and reprint everything
s.renderer.ClearLine()
fmt.Printf("ASK: %s", newText)
fmt.Printf("[%s] 🤖 ASK: %s", timestamp, newText)
}
return
}
@@ -365,7 +299,7 @@ func (s *StreamingDisplay) streamAskMessageUpdate(newText, oldText, timestamp st
fmt.Print(newChars)
} else if oldAskData.Response != newAskData.Response {
s.renderer.ClearLine()
fmt.Printf("ASK: %s", newAskData.Response)
fmt.Printf("[%s] 🤖 ASK: %s", timestamp, newAskData.Response)
}
// Handle options changes
@@ -390,7 +324,7 @@ func (s *StreamingDisplay) typewriterPrint(text string) {
func (s *StreamingDisplay) finishCurrentStream() {
streamingMsg := s.state.GetStreamingMessage()
if streamingMsg.CurrentKey != "" {
fmt.Println()
//fmt.Println() // Add newline to finish the current streaming message
s.state.SetStreamingMessage("", "")
}
}
@@ -399,17 +333,15 @@ func (s *StreamingDisplay) finishCurrentStream() {
func (s *StreamingDisplay) getMessagePrefix(say string) string {
switch say {
case string(types.SayTypeCompletionResult):
return "RESULT"
return "RESULT"
case string(types.SayTypeText):
return "CLINE"
case string(types.SayTypeReasoning):
return "THINKING"
return "🤖"
default:
return "CLINE"
return "🤖"
}
}
// formatToolMessage formats tool call messages for better readability (legacy, keep for compatibility)
// formatToolMessage formats tool call messages for better readability
func (s *StreamingDisplay) formatToolMessage(text string) string {
var toolCall map[string]interface{}
if err := s.parseJSON(text, &toolCall); err == nil {
@@ -439,29 +371,6 @@ func (s *StreamingDisplay) formatToolMessage(text string) string {
return text
}
// formatStructuredToolMessage formats a parsed ToolMessage for display
func (s *StreamingDisplay) formatStructuredToolMessage(tool *types.ToolMessage) string {
parts := []string{tool.Tool}
if tool.Path != "" {
parts = append(parts, fmt.Sprintf("path=%s", tool.Path))
}
if tool.Content != "" {
if len(tool.Content) > 50 {
parts = append(parts, fmt.Sprintf("content=%s...", tool.Content[:50]))
} else {
parts = append(parts, fmt.Sprintf("content=%s", tool.Content))
}
}
if tool.Regex != "" {
parts = append(parts, fmt.Sprintf("regex=%s", tool.Regex))
}
return strings.Join(parts, " ")
}
// isSimilarToolMessage checks if two tool messages are similar enough to be considered duplicates
func (s *StreamingDisplay) isSimilarToolMessage(msg1, msg2 string) bool {
parts1 := strings.Fields(msg1)
@@ -532,64 +441,8 @@ func (s *StreamingDisplay) parseJSON(text string, v interface{}) error {
return json.Unmarshal([]byte(text), v)
}
func (s *StreamingDisplay) getMessageType(msg *types.ClineMessage) string {
if msg.Type == types.MessageTypeAsk {
return "ASK"
}
switch msg.Say {
case string(types.SayTypeText):
return "CLINE"
case string(types.SayTypeReasoning):
return "THINKING"
case string(types.SayTypeCompletionResult):
return "RESULT"
case string(types.SayTypeCommand):
return "CMD"
default:
return msg.Say
}
}
func (s *StreamingDisplay) shouldRenderMarkdown(sayType string) bool {
switch sayType {
case string(types.SayTypeReasoning), string(types.SayTypeText), string(types.SayTypeCompletionResult), string(types.SayTypeTool), "ask":
return true
default:
return false
}
}
func (s *StreamingDisplay) getPrefix(sayType string) string {
switch sayType {
case string(types.SayTypeReasoning):
return "THINKING"
case string(types.SayTypeText):
return "CLINE"
case string(types.SayTypeCompletionResult):
return "RESULT"
case "ask":
return "ASK"
case string(types.SayTypeCommand):
return "TERMINAL"
default:
return strings.ToUpper(sayType)
}
}
func (s *StreamingDisplay) FreezeActiveSegment() {
s.mu.Lock()
defer s.mu.Unlock()
if s.activeSegment != nil {
s.activeSegment.Freeze()
s.activeSegment = nil
}
}
// Cleanup cleans up streaming display resources
func (s *StreamingDisplay) Cleanup() {
s.FreezeActiveSegment()
if s.dedupe != nil {
s.dedupe.Stop()
}
-371
View File
@@ -1,371 +0,0 @@
package display
import (
"fmt"
"path/filepath"
"strings"
"github.com/cline/cli/pkg/cli/types"
)
// ToolResultParser handles parsing and formatting tool results for display
type ToolResultParser struct {
maxPreviewLines int
maxPreviewChars int
mdRenderer *MarkdownRenderer
}
// NewToolResultParser creates a new tool result parser
func NewToolResultParser(mdRenderer *MarkdownRenderer) *ToolResultParser {
return &ToolResultParser{
maxPreviewLines: 15,
maxPreviewChars: 500,
mdRenderer: mdRenderer,
}
}
// ParseReadFile formats readFile tool results with smart preview
func (p *ToolResultParser) ParseReadFile(content, path string) string {
lines := strings.Split(content, "\n")
totalLines := len(lines)
// Get file extension for syntax highlighting
ext := filepath.Ext(path)
lang := p.detectLanguage(ext)
var preview strings.Builder
// Show header with line count
preview.WriteString(fmt.Sprintf("*%d lines*\n\n", totalLines))
// Show preview of content
previewLines := p.maxPreviewLines
if totalLines < previewLines {
previewLines = totalLines
}
preview.WriteString(fmt.Sprintf("```%s\n", lang))
for i := 0; i < previewLines; i++ {
preview.WriteString(lines[i])
preview.WriteString("\n")
}
if totalLines > previewLines {
preview.WriteString("...\n")
}
preview.WriteString("```\n")
if totalLines > previewLines {
preview.WriteString(fmt.Sprintf("\n*[Content truncated - showing %d of %d lines]*", previewLines, totalLines))
}
return preview.String()
}
// ParseListFiles formats listFiles tool results with directory tree
func (p *ToolResultParser) ParseListFiles(content, path string) string {
if content == "" || content == "No files found." {
return "*No files found*"
}
lines := strings.Split(strings.TrimSpace(content), "\n")
// Check for truncation message
var truncationMsg string
lastLine := lines[len(lines)-1]
if strings.Contains(lastLine, "File list truncated") {
truncationMsg = lastLine
lines = lines[:len(lines)-1]
}
totalFiles := len(lines)
var result strings.Builder
result.WriteString(fmt.Sprintf("*%d %s*\n\n", totalFiles, p.pluralize(totalFiles, "file", "files")))
// Show up to 20 files in tree format
maxShow := 20
if totalFiles < maxShow {
maxShow = totalFiles
}
result.WriteString("```\n")
for i := 0; i < maxShow; i++ {
line := lines[i]
// Add tree characters for better visualization
if strings.HasPrefix(line, "🔒 ") {
result.WriteString("├── 🔒 ")
result.WriteString(strings.TrimPrefix(line, "🔒 "))
} else {
result.WriteString("├── ")
result.WriteString(line)
}
result.WriteString("\n")
}
if totalFiles > maxShow {
result.WriteString("└── ...\n")
}
result.WriteString("```\n")
if totalFiles > maxShow {
result.WriteString(fmt.Sprintf("\n*[Showing %d of %d files]*", maxShow, totalFiles))
}
if truncationMsg != "" {
result.WriteString(fmt.Sprintf("\n\n*%s*", truncationMsg))
}
return result.String()
}
// ParseSearchFiles formats searchFiles tool results with context
func (p *ToolResultParser) ParseSearchFiles(content string) string {
if content == "" || content == "Found 0 results." {
return "*No results found*"
}
lines := strings.Split(content, "\n")
if len(lines) == 0 {
return "*No results found*"
}
// Extract result count from first line
firstLine := lines[0]
var result strings.Builder
result.WriteString(fmt.Sprintf("*%s*\n\n", firstLine))
// Parse and group results by file
var currentFile string
var fileResults []string
filesShown := 0
maxFiles := 5
matchesShown := 0
maxMatches := 15
for i := 1; i < len(lines) && filesShown < maxFiles && matchesShown < maxMatches; i++ {
line := lines[i]
if line == "" {
continue
}
// Check if this is a file path (doesn't start with whitespace or line number)
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") && strings.Contains(line, ":") {
// Save previous file results
if currentFile != "" && len(fileResults) > 0 {
result.WriteString(p.formatFileMatches(currentFile, fileResults))
filesShown++
}
currentFile = line
fileResults = []string{}
} else if currentFile != "" {
// This is a match line
fileResults = append(fileResults, strings.TrimSpace(line))
matchesShown++
}
}
// Add last file's results
if currentFile != "" && len(fileResults) > 0 && filesShown < maxFiles {
result.WriteString(p.formatFileMatches(currentFile, fileResults))
filesShown++
}
// Add truncation notice
totalMatches := strings.Count(content, "\n") - 1 // Rough estimate
if matchesShown < totalMatches {
result.WriteString(fmt.Sprintf("\n*[Showing %d results - see full output for all matches]*", matchesShown))
}
return result.String()
}
// formatFileMatches formats matches for a single file
func (p *ToolResultParser) formatFileMatches(file string, matches []string) string {
var result strings.Builder
// Parse file path and extension for syntax highlighting
ext := filepath.Ext(file)
lang := p.detectLanguage(ext)
result.WriteString(fmt.Sprintf("**%s** (%d %s)\n", file, len(matches), p.pluralize(len(matches), "match", "matches")))
result.WriteString(fmt.Sprintf("```%s\n", lang))
maxMatches := 5
for i, match := range matches {
if i >= maxMatches {
result.WriteString("...\n")
break
}
result.WriteString(match)
result.WriteString("\n")
}
result.WriteString("```\n\n")
return result.String()
}
// ParseCodeDefinitions formats listCodeDefinitionNames tool results
func (p *ToolResultParser) ParseCodeDefinitions(content string) string {
if content == "" || content == "No source code definitions found." {
return "*No code definitions found*"
}
// Return the full content as-is
return content
}
// ParseWebFetch formats webFetch tool results with content preview
func (p *ToolResultParser) ParseWebFetch(content, url string) string {
if content == "" {
return fmt.Sprintf("*Fetched content from %s (empty response)*", url)
}
lines := strings.Split(content, "\n")
var result strings.Builder
// Try to extract title
var title string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, "##") {
title = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
break
}
}
if title != "" {
result.WriteString(fmt.Sprintf("**Title:** %s\n\n", title))
}
// Show preview of content
result.WriteString("**Preview:**\n")
charCount := 0
maxChars := 500
previewLines := []string{}
for _, line := range lines {
// Skip markdown headers
if strings.HasPrefix(strings.TrimSpace(line), "#") {
continue
}
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if charCount+len(trimmed) > maxChars {
break
}
previewLines = append(previewLines, trimmed)
charCount += len(trimmed)
}
result.WriteString(strings.Join(previewLines, " "))
result.WriteString("...\n\n")
// Extract sections
sections := []string{}
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "##") {
section := strings.TrimSpace(strings.TrimPrefix(trimmed, "##"))
sections = append(sections, section)
if len(sections) >= 5 {
break
}
}
}
if len(sections) > 0 {
result.WriteString("**Sections Found:**\n")
for _, section := range sections {
result.WriteString(fmt.Sprintf("- %s\n", section))
}
result.WriteString("\n")
}
// Word count estimate
wordCount := len(strings.Fields(content))
result.WriteString(fmt.Sprintf("*[Full content: ~%s]*", p.formatWordCount(wordCount)))
return result.String()
}
// detectLanguage returns syntax highlighting language based on file extension
func (p *ToolResultParser) detectLanguage(ext string) string {
langMap := map[string]string{
".ts": "typescript",
".tsx": "tsx",
".js": "javascript",
".jsx": "jsx",
".go": "go",
".py": "python",
".rb": "ruby",
".java": "java",
".c": "c",
".cpp": "cpp",
".cs": "csharp",
".php": "php",
".sh": "bash",
".bash": "bash",
".zsh": "bash",
".json": "json",
".yaml": "yaml",
".yml": "yaml",
".xml": "xml",
".html": "html",
".css": "css",
".scss": "scss",
".md": "markdown",
".sql": "sql",
".rs": "rust",
}
if lang, ok := langMap[ext]; ok {
return lang
}
return ""
}
// pluralize returns the correct plural form
func (p *ToolResultParser) pluralize(count int, singular, plural string) string {
if count == 1 {
return singular
}
return plural
}
// formatWordCount formats word count with appropriate unit
func (p *ToolResultParser) formatWordCount(count int) string {
if count < 1000 {
return fmt.Sprintf("%d words", count)
}
return fmt.Sprintf("%.1fk words", float64(count)/1000.0)
}
// ParseToolResult is the main entry point for parsing tool results
func (p *ToolResultParser) ParseToolResult(tool *types.ToolMessage) string {
switch tool.Tool {
case "readFile":
return p.ParseReadFile(tool.Content, tool.Path)
case "listFilesTopLevel", "listFilesRecursive":
return p.ParseListFiles(tool.Content, tool.Path)
case "searchFiles":
return p.ParseSearchFiles(tool.Content)
case "listCodeDefinitionNames":
return p.ParseCodeDefinitions(tool.Content)
case "webFetch":
return p.ParseWebFetch(tool.Content, tool.Path)
default:
return tool.Content
}
}
+8 -4
View File
@@ -160,8 +160,11 @@ func (tp *TypewriterPrinter) SetSpeed(multiplier float64) {
tp.config.PauseDelay = time.Duration(float64(150*time.Millisecond) / multiplier)
}
func (tp *TypewriterPrinter) PrintMessageLine(prefix, text string) {
tp.PrintfInstant("%s: ", prefix)
// PrintMessageLine prints a complete message line with typewriter effect
func (tp *TypewriterPrinter) PrintMessageLine(timestamp, prefix, text string) {
// Print the timestamp and prefix with 10-char padding
tp.PrintfInstant("[%s] %-10s: ", timestamp, prefix)
// Print the message text with typewriter effect
tp.Println(text)
}
@@ -190,8 +193,9 @@ func TypewriterPrintfLn(format string, args ...interface{}) {
globalTypewriter.PrintfLn(format, args...)
}
func TypewriterPrintMessageLine(prefix, text string) {
globalTypewriter.PrintMessageLine(prefix, text)
// TypewriterPrintMessageLine prints a message line with typewriter effect using the global instance
func TypewriterPrintMessageLine(timestamp, prefix, text string) {
globalTypewriter.PrintMessageLine(timestamp, prefix, text)
}
// SetGlobalTypewriterEnabled enables or disables the global typewriter effect
+11 -31
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"os"
"os/exec"
"path"
"time"
"github.com/cline/cli/pkg/common"
@@ -93,7 +92,7 @@ func (c *ClineClients) StartNewInstance(ctx context.Context) (*common.CoreInstan
return nil, fmt.Errorf("failed to start instance: %w", err)
}
fmt.Println("Services started and registered successfully!")
fmt.Println("Services started and registered successfully!")
fmt.Printf(" Address: %s\n", instance.Address)
fmt.Printf(" Core Port: %d\n", instance.CorePort())
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
@@ -165,7 +164,7 @@ func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int)
return nil, fmt.Errorf("failed to start instance at port %d: %w", corePort, err)
}
fmt.Println("Services started and registered successfully!")
fmt.Println("Services started and registered successfully!")
fmt.Printf(" Address: %s\n", instance.Address)
fmt.Printf(" Core Port: %d\n", instance.CorePort())
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
@@ -212,16 +211,8 @@ func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address stri
func startClineHost(hostPort, corePort int) (*exec.Cmd, error) {
fmt.Printf("Starting cline-host on port %d\n", hostPort)
// Get the directory where the cline binary is located
execPath, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("failed to get executable path: %w", err)
}
binDir := path.Dir(execPath)
clineHostPath := path.Join(binDir, "cline-host")
// Start the cline-host process
cmd := exec.Command(clineHostPath,
cmd := exec.Command("./cli/bin/cline-host",
"--verbose",
"--port", fmt.Sprintf("%d", hostPort))
@@ -236,16 +227,6 @@ func startClineHost(hostPort, corePort int) (*exec.Cmd, error) {
func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
fmt.Printf("Starting cline-core on port %d (with hostbridge on %d)\n", corePort, hostPort)
// Get paths relative to the cline binary location
execPath, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("failed to get executable path: %w", err)
}
binDir := path.Dir(execPath)
installDir := path.Dir(binDir)
nodePath := path.Join(binDir, "node")
clineCorePath := path.Join(installDir, "cline-core.js")
// Create port-tagged log file in OS temp directory with full address
logFileName := fmt.Sprintf("cline-core-debug-localhost-%d.log", corePort)
logFilePath := fmt.Sprintf("%s/%s", os.TempDir(), logFileName)
@@ -254,29 +235,28 @@ func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
return nil, fmt.Errorf("failed to create log file: %w", err)
}
// Start the cline-core process with --config flag
args := []string{clineCorePath,
// Start the cline-core process with --config flag instead of CLINE_DIR env var
args := []string{"cline-core.js",
"--port", fmt.Sprintf("%d", corePort),
"--host-bridge-port", fmt.Sprintf("%d", hostPort),
"--config", Config.ConfigPath}
fmt.Printf("DEBUG: Starting cline-core with command: %s %v\n", nodePath, args)
fmt.Printf("DEBUG: Working directory: %s\n", installDir)
fmt.Printf("DEBUG: Starting cline-core with command: node %v\n", args)
fmt.Printf("DEBUG: Working directory: ./dist-standalone\n")
fmt.Printf("DEBUG: Config path: %s\n", Config.ConfigPath)
cmd := exec.Command(nodePath, args...)
cmd := exec.Command("node", args...)
// Set working directory to installation root
cmd.Dir = installDir
// Set working directory to dist-standalone (relative to project root)
cmd.Dir = "./dist-standalone"
// Redirect stdout and stderr to log file
cmd.Stdout = logFile
cmd.Stderr = logFile
// Set environment variables with NODE_PATH for node_modules
// Set environment variables (removed CLINE_DIR)
env := os.Environ()
env = append(env,
fmt.Sprintf("NODE_PATH=%s", path.Join(installDir, "node_modules")),
"GRPC_TRACE=all",
"GRPC_VERBOSITY=DEBUG",
"NODE_ENV=development",
-24
View File
@@ -65,27 +65,3 @@ func GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
func GetClientForAddress(ctx context.Context, address string) (*client.ClineClient, error) {
return Clients.GetRegistry().GetClient(ctx, address)
}
// EnsureDefaultInstance ensures a default instance exists
func EnsureDefaultInstance(ctx context.Context) error {
if Clients == nil {
return fmt.Errorf("global clients not initialized")
}
// Check if we have any instances in the registry
registry := Clients.GetRegistry()
if registry.GetDefaultInstance() == "" {
// No default instance, start a new one
instance, err := Clients.StartNewInstance(ctx)
if err != nil {
return fmt.Errorf("failed to start new default instance: %w", err)
}
// Set the new instance as default
if err := registry.SetDefaultInstance(instance.Address); err != nil {
return fmt.Errorf("failed to set default instance: %w", err)
}
}
return nil
}
+3 -3
View File
@@ -223,15 +223,15 @@ func (r *ClientRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.Co
instances := r.ListInstances()
// 3. Ensure default is set if instances exist
if err := r.EnsureDefaultInstance(instances); err != nil {
if err := r.ensureDefaultInstance(instances); err != nil {
fmt.Printf("Warning: Failed to ensure default instance: %v\n", err)
}
return instances, nil
}
// EnsureDefaultInstance ensures a default instance is set if instances exist but no default is configured
func (r *ClientRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceInfo) error {
// ensureDefaultInstance ensures a default instance is set if instances exist but no default is configured
func (r *ClientRegistry) ensureDefaultInstance(instances []*common.CoreInstanceInfo) error {
currentDefault := r.GetDefaultInstance()
// If we have no instances, clear any stale default and remove settings file
+74 -80
View File
@@ -25,47 +25,50 @@ func (h *AskHandler) CanHandle(msg *types.ClineMessage) bool {
return msg.IsAsk()
}
// Handle processes ASK messages
func (h *AskHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
timestamp := msg.GetTimestamp()
switch msg.Ask {
case string(types.AskTypeFollowup):
return h.handleFollowup(msg, dc)
return h.handleFollowup(msg, dc, timestamp)
case string(types.AskTypePlanModeRespond):
return h.handlePlanModeRespond(msg, dc)
return h.handlePlanModeRespond(msg, dc, timestamp)
case string(types.AskTypeCommand):
return h.handleCommand(msg, dc)
return h.handleCommand(msg, dc, timestamp)
case string(types.AskTypeCommandOutput):
return h.handleCommandOutput(msg, dc)
return h.handleCommandOutput(msg, dc, timestamp)
case string(types.AskTypeCompletionResult):
return h.handleCompletionResult(msg, dc)
return h.handleCompletionResult(msg, dc, timestamp)
case string(types.AskTypeTool):
return h.handleTool(msg, dc)
return h.handleTool(msg, dc, timestamp)
case string(types.AskTypeAPIReqFailed):
return h.handleAPIReqFailed(msg, dc)
return h.handleAPIReqFailed(msg, dc, timestamp)
case string(types.AskTypeResumeTask):
return h.handleResumeTask(msg, dc)
return h.handleResumeTask(msg, dc, timestamp)
case string(types.AskTypeResumeCompletedTask):
return h.handleResumeCompletedTask(msg, dc)
return h.handleResumeCompletedTask(msg, dc, timestamp)
case string(types.AskTypeMistakeLimitReached):
return h.handleMistakeLimitReached(msg, dc)
return h.handleMistakeLimitReached(msg, dc, timestamp)
case string(types.AskTypeAutoApprovalMaxReached):
return h.handleAutoApprovalMaxReached(msg, dc)
return h.handleAutoApprovalMaxReached(msg, dc, timestamp)
case string(types.AskTypeBrowserActionLaunch):
return h.handleBrowserActionLaunch(msg, dc)
return h.handleBrowserActionLaunch(msg, dc, timestamp)
case string(types.AskTypeUseMcpServer):
return h.handleUseMcpServer(msg, dc)
return h.handleUseMcpServer(msg, dc, timestamp)
case string(types.AskTypeNewTask):
return h.handleNewTask(msg, dc)
return h.handleNewTask(msg, dc, timestamp)
case string(types.AskTypeCondense):
return h.handleCondense(msg, dc)
return h.handleCondense(msg, dc, timestamp)
case string(types.AskTypeReportBug):
return h.handleReportBug(msg, dc)
return h.handleReportBug(msg, dc, timestamp)
default:
return h.handleDefault(msg, dc)
return h.handleDefault(msg, dc, timestamp)
}
}
// handleFollowup handles followup questions
func (h *AskHandler) handleFollowup(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *AskHandler) handleFollowup(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
var question string
var options []string
@@ -81,7 +84,7 @@ func (h *AskHandler) handleFollowup(msg *types.ClineMessage, dc *DisplayContext)
return nil
}
err := dc.Renderer.RenderMessage("QUESTION", question, true)
err := dc.Renderer.RenderMessage(timestamp, "QUESTION", question)
if err != nil {
return err
}
@@ -98,7 +101,7 @@ func (h *AskHandler) handleFollowup(msg *types.ClineMessage, dc *DisplayContext)
}
// handlePlanModeRespond handles plan mode responses
func (h *AskHandler) handlePlanModeRespond(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *AskHandler) handlePlanModeRespond(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
var response string
var options []string
@@ -120,17 +123,9 @@ func (h *AskHandler) handlePlanModeRespond(msg *types.ClineMessage, dc *DisplayC
return nil
}
var rendered string
if dc.IsStreamingMode {
// In streaming mode, header was already shown by partial stream
// Just render the body content
rendered = dc.Renderer.RenderMarkdown(response)
fmt.Printf("%s\n", rendered)
} else {
// In non-streaming mode, render header + body together
markdown := fmt.Sprintf("### Cline has a plan\n\n%s", response)
rendered = dc.Renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
err := dc.Renderer.RenderMessage(timestamp, "ASST PLAN", response)
if err != nil {
return err
}
// Display options if available
@@ -145,7 +140,7 @@ func (h *AskHandler) handlePlanModeRespond(msg *types.ClineMessage, dc *DisplayC
}
// handleCommand handles command execution requests
func (h *AskHandler) handleCommand(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *AskHandler) handleCommand(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text == "" {
return nil
}
@@ -158,15 +153,12 @@ func (h *AskHandler) handleCommand(msg *types.ClineMessage, dc *DisplayContext)
command = strings.TrimSuffix(command, "REQ_APP")
}
err := dc.Renderer.RenderMessage("TERMINAL", "Cline wants to execute this command:", true)
err := dc.Renderer.RenderMessage(timestamp, "TERMINAL", "Cline wants to execute this command:")
if err != nil {
return fmt.Errorf("failed to render handleCommand: %w", err)
}
// Render markdown with syntax highlighting
markdown := fmt.Sprintf("```shell\n%s\n```", strings.TrimSpace(command))
rendered := dc.Renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
fmt.Printf("\n```shell\n%s\n```\n", strings.TrimSpace(command))
if hasAutoApprovalConflict {
fmt.Printf("\nThe model has determined this command requires explicit approval.\n")
@@ -178,59 +170,61 @@ func (h *AskHandler) handleCommand(msg *types.ClineMessage, dc *DisplayContext)
}
// handleCommandOutput handles command output requests
func (h *AskHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *AskHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text == "" {
return nil
}
commandOutput := msg.Text
markdown := fmt.Sprintf("```\n%s\n```", commandOutput)
rendered := dc.Renderer.RenderMarkdown(markdown)
err := dc.Renderer.RenderMessage(timestamp, "TERMINAL", fmt.Sprintf("Current terminal output: %s", commandOutput))
if err != nil {
return fmt.Errorf("failed to render handleCommandOutput: %w", err)
}
fmt.Printf("%s", rendered)
fmt.Printf("\nApprove to proceed while this command runs in the background.\n")
return nil
}
// handleCompletionResult handles completion result requests
func (h *AskHandler) handleCompletionResult(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *AskHandler) handleCompletionResult(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return nil
}
// handleTool handles tool execution requests
func (h *AskHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *AskHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
// Parse tool message
var tool types.ToolMessage
if err := json.Unmarshal([]byte(msg.Text), &tool); err != nil {
// Fallback to simple display
return dc.Renderer.RenderMessage("TOOL", msg.Text, true)
return dc.Renderer.RenderMessage(timestamp, "TOOL", msg.Text)
}
return h.renderToolMessage(&tool, dc)
return h.renderToolMessage(&tool, dc, timestamp)
}
// renderToolMessage renders a tool message with appropriate formatting
func (h *AskHandler) renderToolMessage(tool *types.ToolMessage, dc *DisplayContext) error {
func (h *AskHandler) renderToolMessage(tool *types.ToolMessage, dc *DisplayContext, timestamp string) error {
switch tool.Tool {
case string(types.ToolTypeEditedExistingFile):
dc.Renderer.RenderMessage("TOOL", fmt.Sprintf("Cline wants to edit file: %s", tool.Path), true)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to edit file: %s", tool.Path))
case string(types.ToolTypeNewFileCreated):
dc.Renderer.RenderMessage("TOOL", fmt.Sprintf("Cline wants to create file: %s", tool.Path), true)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to create file: %s", tool.Path))
case string(types.ToolTypeReadFile):
dc.Renderer.RenderMessage("TOOL", fmt.Sprintf("Cline wants to read file: %s", tool.Path), true)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to read file: %s", tool.Path))
case string(types.ToolTypeListFilesTopLevel):
dc.Renderer.RenderMessage("TOOL", fmt.Sprintf("Cline wants to list files in: %s", tool.Path), true)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to list files in: %s", tool.Path))
case string(types.ToolTypeListFilesRecursive):
dc.Renderer.RenderMessage("TOOL", fmt.Sprintf("Cline wants to recursively list files in: %s", tool.Path), true)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to recursively list files in: %s", tool.Path))
case string(types.ToolTypeSearchFiles):
dc.Renderer.RenderMessage("TOOL", fmt.Sprintf("Cline wants to search for '%s' in: %s", tool.Regex, tool.Path), true)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to search for '%s' in: %s", tool.Regex, tool.Path))
case string(types.ToolTypeWebFetch):
dc.Renderer.RenderMessage("TOOL", fmt.Sprintf("Cline wants to fetch URL: %s", tool.Path), true)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to fetch URL: %s", tool.Path))
case string(types.ToolTypeListCodeDefinitionNames):
dc.Renderer.RenderMessage("TOOL", fmt.Sprintf("Cline wants to list code definitions for: %s", tool.Path), true)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to list code definitions for: %s", tool.Path))
default:
dc.Renderer.RenderMessage("TOOL", fmt.Sprintf("Cline wants to use tool: %s", tool.Tool), true)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to use tool: %s", tool.Tool))
}
// Skip content preview for readFile and webFetch tools
@@ -255,38 +249,38 @@ func (h *AskHandler) renderToolMessage(tool *types.ToolMessage, dc *DisplayConte
}
// handleAPIReqFailed handles API request failures
func (h *AskHandler) handleAPIReqFailed(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("ERROR", fmt.Sprintf("API Request Failed: %s. Approve to retry request.", msg.Text), true)
func (h *AskHandler) handleAPIReqFailed(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "ERROR", fmt.Sprintf("API Request Failed: %s. Approve to retry request.", msg.Text))
}
// handleResumeTask handles resume task requests
func (h *AskHandler) handleResumeTask(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("GEN INFO", "Resuming interrupted task.", true)
func (h *AskHandler) handleResumeTask(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "GEN INFO", "Resuming interrupted task.")
}
// handleResumeCompletedTask handles resume completed task requests
func (h *AskHandler) handleResumeCompletedTask(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("GEN INFO", "Resuming completed task.", true)
func (h *AskHandler) handleResumeCompletedTask(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "GEN INFO", "Resuming completed task.")
}
// handleMistakeLimitReached handles mistake limit reached
func (h *AskHandler) handleMistakeLimitReached(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("ERROR", fmt.Sprintf("Mistake Limit Reached: %s. Approval required.", msg.Text), true)
func (h *AskHandler) handleMistakeLimitReached(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "ERROR", fmt.Sprintf("Mistake Limit Reached: %s. Approval required.", msg.Text))
}
// handleAutoApprovalMaxReached handles auto-approval max reached
func (h *AskHandler) handleAutoApprovalMaxReached(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("WARNING", fmt.Sprintf("Auto-approval limit reached: %s. Approval required.", msg.Text), true)
func (h *AskHandler) handleAutoApprovalMaxReached(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "WARNING", fmt.Sprintf("Auto-approval limit reached: %s. Approval required.", msg.Text))
}
// handleBrowserActionLaunch handles browser action launch requests
func (h *AskHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *AskHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
url := strings.TrimSpace(msg.Text)
return dc.Renderer.RenderMessage("BROWSER", fmt.Sprintf("Cline wants to launch browser and navigate to: %s. Approval required.", url), true)
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Cline wants to launch browser and navigate to: %s. Approval required.", url))
}
// handleUseMcpServer handles MCP server usage requests
func (h *AskHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *AskHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
// Parse MCP server usage request
type McpServerRequest struct {
ServerName string `json:"serverName"`
@@ -298,7 +292,7 @@ func (h *AskHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayCont
var mcpReq McpServerRequest
if err := json.Unmarshal([]byte(msg.Text), &mcpReq); err != nil {
return dc.Renderer.RenderMessage("MCP", msg.Text, true)
return dc.Renderer.RenderMessage(timestamp, "MCP", msg.Text)
}
var operation string
@@ -311,22 +305,22 @@ func (h *AskHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayCont
}
}
return dc.Renderer.RenderMessage("MCP",
fmt.Sprintf("Cline wants to %s on the %s MCP server", operation, mcpReq.ServerName), true)
return dc.Renderer.RenderMessage(timestamp, "MCP",
fmt.Sprintf("Cline wants to %s on the %s MCP server", operation, mcpReq.ServerName))
}
// handleNewTask handles new task creation requests
func (h *AskHandler) handleNewTask(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("NEW TASK", fmt.Sprintf("Cline wants to start a new task: %s. Approval required.", msg.Text), true)
func (h *AskHandler) handleNewTask(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "NEW TASK", fmt.Sprintf("Cline wants to start a new task: %s. Approval required.", msg.Text))
}
// handleCondense handles conversation condensing requests
func (h *AskHandler) handleCondense(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("CONDENSE", fmt.Sprintf("Cline wants to condense the conversation: %s. Approval required.", msg.Text), true)
func (h *AskHandler) handleCondense(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "CONDENSE", fmt.Sprintf("Cline wants to condense the conversation: %s. Approval required.", msg.Text))
}
// handleReportBug handles bug report requests
func (h *AskHandler) handleReportBug(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *AskHandler) handleReportBug(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
var bugData struct {
Title string `json:"title"`
WhatHappened string `json:"what_happened"`
@@ -336,10 +330,10 @@ func (h *AskHandler) handleReportBug(msg *types.ClineMessage, dc *DisplayContext
}
if err := json.Unmarshal([]byte(msg.Text), &bugData); err != nil {
return dc.Renderer.RenderMessage("BUG REPORT", fmt.Sprintf("Cline wants to create a GitHub issue: %s. Approval required.", msg.Text), true)
return dc.Renderer.RenderMessage(timestamp, "BUG REPORT", fmt.Sprintf("Cline wants to create a GitHub issue: %s. Approval required.", msg.Text))
}
err := dc.Renderer.RenderMessage("BUG REPORT", "Cline wants to create a GitHub issue:", true)
err := dc.Renderer.RenderMessage(timestamp, "BUG REPORT", "Cline wants to create a GitHub issue:")
if err != nil {
return fmt.Errorf("failed to render handleReportBug: %w", err)
}
@@ -355,6 +349,6 @@ func (h *AskHandler) handleReportBug(msg *types.ClineMessage, dc *DisplayContext
}
// handleDefault handles unknown ASK message types
func (h *AskHandler) handleDefault(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("ASK", msg.Text, true)
func (h *AskHandler) handleDefault(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "ASK", msg.Text)
}
+9 -8
View File
@@ -22,13 +22,12 @@ type MessageHandler interface {
// DisplayContext provides context and utilities for message handlers
type DisplayContext struct {
State *types.ConversationState
Renderer *display.Renderer
IsLast bool
IsPartial bool
Verbose bool
MessageIndex int
IsStreamingMode bool
State *types.ConversationState
Renderer *display.Renderer
IsLast bool
IsPartial bool
Verbose bool
MessageIndex int
}
// BaseHandler provides common functionality for message handlers
@@ -95,13 +94,15 @@ func (r *HandlerRegistry) Handle(msg *types.ClineMessage, dc *DisplayContext) er
// handleDefault provides default handling for unrecognized messages
func (r *HandlerRegistry) handleDefault(msg *types.ClineMessage, dc *DisplayContext) error {
timestamp := msg.GetTimestamp()
if msg.Text == "" {
return nil
}
prefix := "RESPONSE:"
return dc.Renderer.RenderMessage(prefix, msg.Text, true)
return dc.Renderer.RenderMessage(timestamp, prefix, msg.Text)
}
// GetHandlers returns all registered handlers
+124 -219
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/types"
)
@@ -32,361 +31,269 @@ func (h *SayHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
switch msg.Say {
case string(types.SayTypeTask):
return h.handleTask(msg, dc)
return h.handleTask(msg, dc, timestamp)
case string(types.SayTypeError):
return h.handleError(msg, dc)
return h.handleError(msg, dc, timestamp)
case string(types.SayTypeAPIReqStarted):
return h.handleAPIReqStarted(msg, dc)
return h.handleAPIReqStarted(msg, dc, timestamp)
case string(types.SayTypeAPIReqFinished):
return h.handleAPIReqFinished(msg, dc)
return h.handleAPIReqFinished(msg, dc, timestamp)
case string(types.SayTypeText):
return h.handleText(msg, dc)
return h.handleText(msg, dc, timestamp)
case string(types.SayTypeReasoning):
return h.handleReasoning(msg, dc)
return h.handleReasoning(msg, dc, timestamp)
case string(types.SayTypeCompletionResult):
return h.handleCompletionResult(msg, dc)
return h.handleCompletionResult(msg, dc, timestamp)
case string(types.SayTypeUserFeedback):
return h.handleUserFeedback(msg, dc)
return h.handleUserFeedback(msg, dc, timestamp)
case string(types.SayTypeUserFeedbackDiff):
return h.handleUserFeedbackDiff(msg, dc)
return h.handleUserFeedbackDiff(msg, dc, timestamp)
case string(types.SayTypeAPIReqRetried):
return h.handleAPIReqRetried(msg, dc)
return h.handleAPIReqRetried(msg, dc, timestamp)
case string(types.SayTypeCommand):
return h.handleCommand(msg, dc)
return h.handleCommand(msg, dc, timestamp)
case string(types.SayTypeCommandOutput):
return h.handleCommandOutput(msg, dc)
return h.handleCommandOutput(msg, dc, timestamp)
case string(types.SayTypeTool):
return h.handleTool(msg, dc)
return h.handleTool(msg, dc, timestamp)
case string(types.SayTypeShellIntegrationWarning):
return h.handleShellIntegrationWarning(msg, dc)
return h.handleShellIntegrationWarning(msg, dc, timestamp)
case string(types.SayTypeBrowserActionLaunch):
return h.handleBrowserActionLaunch(msg, dc)
return h.handleBrowserActionLaunch(msg, dc, timestamp)
case string(types.SayTypeBrowserAction):
return h.handleBrowserAction(msg, dc)
return h.handleBrowserAction(msg, dc, timestamp)
case string(types.SayTypeBrowserActionResult):
return h.handleBrowserActionResult(msg, dc)
return h.handleBrowserActionResult(msg, dc, timestamp)
case string(types.SayTypeMcpServerRequestStarted):
return h.handleMcpServerRequestStarted(msg, dc)
return h.handleMcpServerRequestStarted(msg, dc, timestamp)
case string(types.SayTypeMcpServerResponse):
return h.handleMcpServerResponse(msg, dc)
return h.handleMcpServerResponse(msg, dc, timestamp)
case string(types.SayTypeMcpNotification):
return h.handleMcpNotification(msg, dc)
return h.handleMcpNotification(msg, dc, timestamp)
case string(types.SayTypeUseMcpServer):
return h.handleUseMcpServer(msg, dc)
return h.handleUseMcpServer(msg, dc, timestamp)
case string(types.SayTypeDiffError):
return h.handleDiffError(msg, dc)
return h.handleDiffError(msg, dc, timestamp)
case string(types.SayTypeDeletedAPIReqs):
return h.handleDeletedAPIReqs(msg, dc)
return h.handleDeletedAPIReqs(msg, dc, timestamp)
case string(types.SayTypeClineignoreError):
return h.handleClineignoreError(msg, dc)
return h.handleClineignoreError(msg, dc, timestamp)
case string(types.SayTypeCheckpointCreated):
return h.handleCheckpointCreated(msg, dc, timestamp)
case string(types.SayTypeLoadMcpDocumentation):
return h.handleLoadMcpDocumentation(msg, dc)
return h.handleLoadMcpDocumentation(msg, dc, timestamp)
case string(types.SayTypeInfo):
return h.handleInfo(msg, dc)
return h.handleInfo(msg, dc, timestamp)
case string(types.SayTypeTaskProgress):
return h.handleTaskProgress(msg, dc)
return h.handleTaskProgress(msg, dc, timestamp)
default:
return h.handleDefault(msg, dc)
return h.handleDefault(msg, dc, timestamp)
}
}
// handleTask handles task messages
func (h *SayHandler) handleTask(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleTask(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return nil
}
// handleError handles error messages
func (h *SayHandler) handleError(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("ERROR", msg.Text, true)
func (h *SayHandler) handleError(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "ERROR", msg.Text)
}
// handleAPIReqStarted handles API request started messages
func (h *SayHandler) handleAPIReqStarted(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleAPIReqStarted(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
// Parse API request info
apiInfo := types.APIRequestInfo{Cost: -1}
if err := json.Unmarshal([]byte(msg.Text), &apiInfo); err != nil {
return dc.Renderer.RenderMessage("API INFO", msg.Text, true)
return dc.Renderer.RenderMessage(timestamp, "API INFO", msg.Text)
}
// Handle different API request states
if apiInfo.CancelReason != "" {
if apiInfo.CancelReason == "user_cancelled" {
return dc.Renderer.RenderMessage("API INFO", "Request Cancelled", true)
return dc.Renderer.RenderMessage(timestamp, "API INFO", "Request Cancelled")
} else if apiInfo.CancelReason == "retries_exhausted" {
return dc.Renderer.RenderMessage("API INFO", "Request Failed (Retries Exhausted)", true)
return dc.Renderer.RenderMessage(timestamp, "API INFO", "Request Failed (Retries Exhausted)")
}
return dc.Renderer.RenderMessage("API INFO", "Streaming Failed", true)
return dc.Renderer.RenderMessage(timestamp, "API INFO", "Streaming Failed")
}
if apiInfo.Cost >= 0 {
return dc.Renderer.RenderAPI("request completed", &apiInfo)
return dc.Renderer.RenderAPI(timestamp, "Request completed", &apiInfo)
}
// Check for retry status
if apiInfo.RetryStatus != nil {
return dc.Renderer.RenderRetry(
return dc.Renderer.RenderRetry(timestamp,
apiInfo.RetryStatus.Attempt,
apiInfo.RetryStatus.MaxAttempts,
apiInfo.RetryStatus.DelaySec)
}
return dc.Renderer.RenderAPI("processing request", &apiInfo)
return dc.Renderer.RenderAPI(timestamp, "Processing request", &apiInfo)
}
// handleAPIReqFinished handles API request finished messages
func (h *SayHandler) handleAPIReqFinished(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleAPIReqFinished(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
// This message type is typically not displayed as it's handled by the started message
return nil
}
// handleText handles regular text messages
func (h *SayHandler) handleText(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleText(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text == "" {
return nil
}
// Special case for the user's task input
prefix := "ASST TEXT"
if dc.MessageIndex == 0 {
markdown := formatUserMessage(msg.Text)
rendered := dc.Renderer.RenderMarkdown(markdown)
fmt.Printf("%s", rendered)
fmt.Printf("\n")
return nil
prefix = "USER"
}
// Regular Cline text response
var rendered string
if dc.IsStreamingMode {
// In streaming mode, header already shown by partial stream
rendered = dc.Renderer.RenderMarkdown(msg.Text)
fmt.Printf("%s\n", rendered)
} else {
// In non-streaming mode, render header + body together
markdown := fmt.Sprintf("### Cline responds\n\n%s", msg.Text)
rendered = dc.Renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
}
return nil
return dc.Renderer.RenderMessage(timestamp, prefix, msg.Text)
}
// handleReasoning handles reasoning messages
func (h *SayHandler) handleReasoning(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleReasoning(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text == "" {
return nil
}
var rendered string
if dc.IsStreamingMode {
// In streaming mode, header already shown by partial stream
rendered = dc.Renderer.RenderMarkdown(msg.Text)
fmt.Printf("%s\n", rendered)
} else {
// In non-streaming mode, render header + body together
markdown := fmt.Sprintf("### Cline is thinking\n\n%s", msg.Text)
rendered = dc.Renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
}
return nil
return dc.Renderer.RenderMessage(timestamp, "THINKING", msg.Text)
}
func (h *SayHandler) handleCompletionResult(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleCompletionResult(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
text := msg.Text
if strings.HasSuffix(text, "HAS_CHANGES") {
text = strings.TrimSuffix(text, "HAS_CHANGES")
}
var rendered string
if dc.IsStreamingMode {
// In streaming mode, header already shown by partial stream
rendered = dc.Renderer.RenderMarkdown(text)
fmt.Printf("%s\n", rendered)
} else {
// In non-streaming mode, render header + body together
markdown := fmt.Sprintf("### Task completed\n\n%s", text)
rendered = dc.Renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
}
return nil
return dc.Renderer.RenderMessage(timestamp, "RESULT", text)
}
func formatUserMessage(text string) string {
lines := strings.Split(text, "\n")
// Wrap each line in backticks
for i, line := range lines {
if line != "" {
lines[i] = fmt.Sprintf("`%s`", line)
}
}
return strings.Join(lines, "\n")
}
// handleUserFeedback handles user feedback messages
func (h *SayHandler) handleUserFeedback(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleUserFeedback(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text != "" {
markdown := formatUserMessage(msg.Text)
rendered := dc.Renderer.RenderMarkdown(markdown)
fmt.Printf("%s", rendered)
return nil
return dc.Renderer.RenderMessage(timestamp, "USER", msg.Text)
} else {
return dc.Renderer.RenderMessage("USER", "[Provided feedback without text]", true)
return dc.Renderer.RenderMessage(timestamp, "USER", "[Provided feedback without text]")
}
}
// handleUserFeedbackDiff handles user feedback diff messages
func (h *SayHandler) handleUserFeedbackDiff(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleUserFeedbackDiff(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
var toolMsg types.ToolMessage
if err := json.Unmarshal([]byte(msg.Text), &toolMsg); err != nil {
return dc.Renderer.RenderMessage("USER DIFF", msg.Text, true)
return dc.Renderer.RenderMessage(timestamp, "USER DIFF", msg.Text)
}
message := fmt.Sprintf("User manually edited: %s\n\nDiff:\n%s",
toolMsg.Path,
toolMsg.Diff)
return dc.Renderer.RenderMessage("USER DIFF", message, true)
return dc.Renderer.RenderMessage(timestamp, "USER DIFF", message)
}
// handleAPIReqRetried handles API request retry messages
func (h *SayHandler) handleAPIReqRetried(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("API INFO", "Retrying request", true)
func (h *SayHandler) handleAPIReqRetried(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "API INFO", "Retrying request")
}
// handleCommand handles command execution announcements
func (h *SayHandler) handleCommand(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleCommand(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text == "" {
return nil
}
command := strings.TrimSpace(msg.Text)
markdown := fmt.Sprintf("### Cline wants to run a command: `%s`", command)
rendered := dc.Renderer.RenderMarkdown(markdown)
err := dc.Renderer.RenderMessage(timestamp, "TERMINAL", "Running command:")
if err != nil {
return fmt.Errorf("failed to render handleCommand: %w", err)
}
// Render markdown with syntax highlighting
fmt.Printf("%s\n", rendered)
fmt.Printf("\n```shell\n%s\n```\n", command)
return nil
}
// handleCommandOutput handles command output messages
func (h *SayHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
commandOutput := msg.Text
return dc.Renderer.RenderMessage("TERMINAL", fmt.Sprintf("Current terminal output: %s", commandOutput), true)
return dc.Renderer.RenderMessage(timestamp, "TERMINAL", fmt.Sprintf("Current terminal output: %s", commandOutput))
}
func (h *SayHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
var tool types.ToolMessage
if err := json.Unmarshal([]byte(msg.Text), &tool); err != nil {
return dc.Renderer.RenderMessage("TOOL", msg.Text, true)
return dc.Renderer.RenderMessage(timestamp, "TOOL", msg.Text)
}
return h.renderToolMessage(&tool, dc)
return h.renderToolMessage(&tool, dc, timestamp)
}
func (h *SayHandler) renderToolMessage(tool *types.ToolMessage, dc *DisplayContext) error {
var markdown string
// Generate header with consistent phrasing
func (h *SayHandler) renderToolMessage(tool *types.ToolMessage, dc *DisplayContext, timestamp string) error {
switch tool.Tool {
case string(types.ToolTypeEditedExistingFile):
markdown = fmt.Sprintf("### Cline is editing `%s`", tool.Path)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline edited file: %s", tool.Path))
case string(types.ToolTypeNewFileCreated):
markdown = fmt.Sprintf("### Cline is writing `%s`", tool.Path)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline created file: %s", tool.Path))
case string(types.ToolTypeReadFile):
markdown = fmt.Sprintf("### Cline is reading `%s`", tool.Path)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline read file: %s", tool.Path))
case string(types.ToolTypeListFilesTopLevel):
markdown = fmt.Sprintf("### Cline is listing files in `%s`", tool.Path)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline listed files in: %s", tool.Path))
case string(types.ToolTypeListFilesRecursive):
markdown = fmt.Sprintf("### Cline is recursively listing files in `%s`", tool.Path)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline recursively listed files in: %s", tool.Path))
case string(types.ToolTypeSearchFiles):
if tool.Regex != "" && tool.Path != "" {
markdown = fmt.Sprintf("### Cline is searching for `%s` in `%s`", tool.Regex, tool.Path)
} else if tool.Regex != "" {
markdown = fmt.Sprintf("### Cline is searching for `%s`", tool.Regex)
} else {
markdown = "### Cline is searching files"
}
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline searched for '%s' in: %s", tool.Regex, tool.Path))
case string(types.ToolTypeWebFetch):
markdown = fmt.Sprintf("### Cline is fetching `%s`", tool.Path)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline fetched URL: %s", tool.Path))
case string(types.ToolTypeListCodeDefinitionNames):
markdown = fmt.Sprintf("### Cline is listing code definitions in `%s`", tool.Path)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline listed code definitions for: %s", tool.Path))
case string(types.ToolTypeSummarizeTask):
markdown = "### Cline condensed the conversation"
dc.Renderer.RenderMessage(timestamp, "TOOL", "Cline condensed the conversation")
default:
markdown = fmt.Sprintf("### Tool: %s", tool.Tool)
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline executed tool: %s", tool.Tool))
}
rendered := dc.Renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
// Use enhanced tool result parser for supported tools
toolParser := display.NewToolResultParser(dc.Renderer.GetMdRenderer())
switch tool.Tool {
case string(types.ToolTypeReadFile):
// readFile: show header only, no body
// Skip content preview for readFile and webFetch tools
if tool.Tool == string(types.ToolTypeReadFile) || tool.Tool == string(types.ToolTypeWebFetch) {
return nil
case string(types.ToolTypeListFilesTopLevel),
string(types.ToolTypeListFilesRecursive),
string(types.ToolTypeListCodeDefinitionNames),
string(types.ToolTypeSearchFiles),
string(types.ToolTypeWebFetch):
}
if tool.Content != "" {
preview := toolParser.ParseToolResult(tool)
previewRendered := dc.Renderer.RenderMarkdown(preview)
fmt.Printf("\n%s\n", previewRendered)
}
return nil
case string(types.ToolTypeEditedExistingFile):
// Show the diff if available
if tool.Content != "" {
diffMarkdown := fmt.Sprintf("```diff\n%s\n```", tool.Content)
diffRendered := dc.Renderer.RenderMarkdown(diffMarkdown)
fmt.Printf("%s", diffRendered)
}
return nil
default:
// Show content preview for other tools, truncating if necessary
preview := tool.Content
if preview != "" {
preview = strings.TrimSpace(tool.Content)
if len(preview) > 1000 {
preview = preview[:1000] + "..."
}
fmt.Printf("Content: %s\n", preview)
// Show content preview, truncating if necessary
preview := tool.Content
if preview != "" {
preview = strings.TrimSpace(tool.Content)
if len(preview) > 1000 {
preview = preview[:1000] + "..."
}
fmt.Printf("Content: %s\n", preview)
}
return nil
}
// handleShellIntegrationWarning handles shell integration warning messages
func (h *SayHandler) handleShellIntegrationWarning(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("WARNING", "Shell Integration Unavailable - Cline won't be able to view the command's output.", true)
func (h *SayHandler) handleShellIntegrationWarning(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "WARNING", "Shell Integration Unavailable - Cline won't be able to view the command's output.")
}
// handleBrowserActionLaunch handles browser action launch messages
func (h *SayHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
url := msg.Text
if url == "" {
return nil
}
return dc.Renderer.RenderMessage("BROWSER", fmt.Sprintf("Launching browser at: %s", url), true)
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Launching browser at: %s", url))
}
// handleBrowserAction handles browser action messages
func (h *SayHandler) handleBrowserAction(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleBrowserAction(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text == "" {
return nil
}
@@ -399,27 +306,27 @@ func (h *SayHandler) handleBrowserAction(msg *types.ClineMessage, dc *DisplayCon
var actionData BrowserActionData
if err := json.Unmarshal([]byte(msg.Text), &actionData); err != nil {
return dc.Renderer.RenderMessage("BROWSER", msg.Text, true)
return dc.Renderer.RenderMessage(timestamp, "BROWSER", msg.Text)
}
// Special handling for type action
if actionData.Action == "type" && actionData.Text != "" {
actionText := fmt.Sprintf("type '%s'", actionData.Text)
return dc.Renderer.RenderMessage("BROWSER", fmt.Sprintf("Next action: %s", actionText), true)
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Next action: %s", actionText))
}
// Special handling for click action
if actionData.Action == "click" && actionData.Coordinate != "" {
actionText := fmt.Sprintf("click (%s)", actionData.Coordinate)
return dc.Renderer.RenderMessage("BROWSER", fmt.Sprintf("Next action: %s", actionText), true)
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Next action: %s", actionText))
}
// Generic handling for all other actions
return dc.Renderer.RenderMessage("BROWSER", fmt.Sprintf("Next action: %s", actionData.Action), true)
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Next action: %s", actionData.Action))
}
// handleBrowserActionResult handles browser action result messages
func (h *SayHandler) handleBrowserActionResult(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleBrowserActionResult(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text == "" {
return nil
}
@@ -433,81 +340,79 @@ func (h *SayHandler) handleBrowserActionResult(msg *types.ClineMessage, dc *Disp
var result BrowserActionResult
if err := json.Unmarshal([]byte(msg.Text), &result); err != nil {
return dc.Renderer.RenderMessage("BROWSER", "Action completed", true)
return dc.Renderer.RenderMessage(timestamp, "BROWSER", "Action completed")
}
// If we have logs, include them in the message
if result.Logs != "" {
return dc.Renderer.RenderMessage("BROWSER", fmt.Sprintf("Action completed with logs: '%s'", result.Logs), true)
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Action completed with logs: '%s'", result.Logs))
}
// Default case
return dc.Renderer.RenderMessage("BROWSER", "Action completed", true)
return dc.Renderer.RenderMessage(timestamp, "BROWSER", "Action completed")
}
// handleMcpServerRequestStarted handles MCP server request started messages
func (h *SayHandler) handleMcpServerRequestStarted(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("MCP", "Sending request to server", true)
func (h *SayHandler) handleMcpServerRequestStarted(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "MCP", "Sending request to server")
}
// handleMcpServerResponse handles MCP server response messages
func (h *SayHandler) handleMcpServerResponse(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("MCP", fmt.Sprintf("Server response: %s", msg.Text), true)
func (h *SayHandler) handleMcpServerResponse(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "MCP", fmt.Sprintf("Server response: %s", msg.Text))
}
// handleMcpNotification handles MCP notification messages
func (h *SayHandler) handleMcpNotification(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("MCP", fmt.Sprintf("Server notification: %s", msg.Text), true)
func (h *SayHandler) handleMcpNotification(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "MCP", fmt.Sprintf("Server notification: %s", msg.Text))
}
// handleUseMcpServer handles MCP server usage messages
func (h *SayHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("MCP", "Server operation approved", true)
func (h *SayHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "MCP", "Server operation approved")
}
// handleDiffError handles diff error messages
func (h *SayHandler) handleDiffError(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("WARNING", "Diff Edit Failure - The model used an invalid diff edit format or used search patterns that don't match anything in the file.", true)
func (h *SayHandler) handleDiffError(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "WARNING", "Diff Edit Failure - The model used an invalid diff edit format or used search patterns that don't match anything in the file.")
}
// handleDeletedAPIReqs handles deleted API requests messages
func (h *SayHandler) handleDeletedAPIReqs(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleDeletedAPIReqs(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
// This message includes api metrics of deleted messages, which we do not log
return dc.Renderer.RenderMessage("GEN INFO", "Checkpoint restored", true)
return dc.Renderer.RenderMessage(timestamp, "GEN INFO", "Checkpoint restored")
}
// handleClineignoreError handles .clineignore error messages
func (h *SayHandler) handleClineignoreError(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("WARNING", fmt.Sprintf("Access Denied - Cline tried to access %s which is blocked by the .clineignore file", msg.Text), true)
func (h *SayHandler) handleClineignoreError(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "WARNING", fmt.Sprintf("Access Denied - Cline tried to access %s which is blocked by the .clineignore file", msg.Text))
}
// handleCheckpointCreated handles checkpoint created messages
func (h *SayHandler) handleCheckpointCreated(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderCheckpointMessage(timestamp, "GEN INFO", msg.Timestamp)
return dc.Renderer.RenderMessage(timestamp, "GEN INFO", "Checkpoint created")
}
// handleLoadMcpDocumentation handles load MCP documentation messages
func (h *SayHandler) handleLoadMcpDocumentation(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("GEN INFO", "Loading MCP documentation", true)
func (h *SayHandler) handleLoadMcpDocumentation(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "GEN INFO", "Loading MCP documentation")
}
// handleInfo handles info messages
func (h *SayHandler) handleInfo(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleInfo(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return nil
}
// handleTaskProgress handles task progress messages
func (h *SayHandler) handleTaskProgress(msg *types.ClineMessage, dc *DisplayContext) error {
func (h *SayHandler) handleTaskProgress(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text == "" {
return nil
}
markdown := fmt.Sprintf("### Progress\n\n%s", msg.Text)
rendered := dc.Renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
return nil
return dc.Renderer.RenderMessage(timestamp, "PROGRESS", fmt.Sprintf("Task Checklist: %s", msg.Text))
}
// handleDefault handles unknown SAY message types
func (h *SayHandler) handleDefault(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("SAY", msg.Text, true)
func (h *SayHandler) handleDefault(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "SAY", msg.Text)
}
+13 -79
View File
@@ -4,12 +4,10 @@ import (
"context"
"fmt"
"os"
"strings"
"syscall"
"text/tabwriter"
"time"
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/grpc-go/cline"
"github.com/spf13/cobra"
@@ -269,21 +267,14 @@ func newInstanceListCommand() *cobra.Command {
return nil
}
// Build instance data
type instanceRow struct {
address string
status string
version string
lastSeen string
pid string
isDefault string
}
// Always output a table
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ADDRESS\tSTATUS\tVERSION\tLAST SEEN\tPID\tDEFAULT")
var rows []instanceRow
for _, instance := range instances {
isDefault := ""
if instance.Address == defaultInstance {
isDefault = ""
isDefault = "*"
}
lastSeen := instance.LastSeen.Format("15:04:05")
@@ -305,74 +296,17 @@ func newInstanceListCommand() *cobra.Command {
}
}
rows = append(rows, instanceRow{
address: instance.Address,
status: instance.Status.String(),
version: instance.Version,
lastSeen: lastSeen,
pid: pid,
isDefault: isDefault,
})
}
// Check output format
if global.Config.OutputFormat == "plain" {
// Use tabwriter for plain output
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ADDRESS\tSTATUS\tVERSION\tLAST SEEN\tPID\tDEFAULT")
for _, row := range rows {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
row.address,
row.status,
row.version,
row.lastSeen,
row.pid,
row.isDefault,
)
}
w.Flush()
} else {
// Use markdown table for rich output
var markdown strings.Builder
markdown.WriteString("| **ADDRESS (ID)** | **STATUS** | **VERSION** | **LAST SEEN** | **PID** | **DEFAULT** |\n")
markdown.WriteString("|---------|--------|---------|-----------|-----|---------|")
for _, row := range rows {
markdown.WriteString(fmt.Sprintf("\n| %s | %s | %s | %s | %s | %s |",
row.address,
row.status,
row.version,
row.lastSeen,
row.pid,
row.isDefault,
))
}
// Render the markdown table
renderer, err := display.NewMarkdownRenderer()
if err != nil {
// Fallback to plain table if markdown renderer fails
fmt.Println(markdown.String())
} else {
rendered, err := renderer.Render(markdown.String())
if err != nil {
fmt.Println(markdown.String())
} else {
// Post-process to colorize status values
rendered = strings.ReplaceAll(rendered, "SERVING", "\033[32mSERVING\033[0m") // Green
rendered = strings.ReplaceAll(rendered, "✓", "\033[32m✓\033[0m") // Green
rendered = strings.ReplaceAll(rendered, "NOT_SERVING", "\033[31mNOT_SERVING\033[0m") // Red
rendered = strings.ReplaceAll(rendered, "UNKNOWN", "\033[33mUNKNOWN\033[0m") // Yellow
fmt.Print(strings.TrimLeft(rendered, "\n"))
}
fmt.Println("\n")
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
instance.Address,
instance.Status,
instance.Version,
lastSeen,
pid,
isDefault,
)
}
w.Flush()
return nil
},
}
+35 -142
View File
@@ -5,8 +5,6 @@ import (
"fmt"
"io"
"os"
"slices"
"strconv"
"strings"
"github.com/cline/cli/pkg/cli/global"
@@ -23,14 +21,12 @@ func NewTaskCommand() *cobra.Command {
}
cmd.AddCommand(newTaskNewCommand())
cmd.AddCommand(newTaskOneshotCommand())
cmd.AddCommand(newTaskCancelCommand())
cmd.AddCommand(newTaskFollowCommand())
cmd.AddCommand(NewTaskSendCommand())
cmd.AddCommand(newTaskSendCommand())
cmd.AddCommand(newTaskViewCommand())
cmd.AddCommand(newTaskListCommand())
cmd.AddCommand(newTaskResumeCommand())
cmd.AddCommand(newTaskRestoreCommand())
return cmd
}
@@ -51,7 +47,7 @@ func ensureTaskManager(ctx context.Context, address string) error {
instanceAddress = address
} else {
// Ensure default instance exists
if err := global.EnsureDefaultInstance(ctx); err != nil {
if err := ensureDefaultInstance(ctx); err != nil {
return fmt.Errorf("failed to ensure default instance: %w", err)
}
taskManager, err = task.NewManagerForDefault(ctx)
@@ -82,6 +78,30 @@ func ensureInstanceAtAddress(ctx context.Context, address string) error {
return global.Clients.EnsureInstanceAtAddress(ctx, address)
}
// ensureDefaultInstance ensures a default instance exists
func ensureDefaultInstance(ctx context.Context) error {
if global.Clients == nil {
return fmt.Errorf("global clients not initialized")
}
// Check if we have any instances in the registry
registry := global.Clients.GetRegistry()
if registry.GetDefaultInstance() == "" {
// No default instance, start a new one
instance, err := global.Clients.StartNewInstance(ctx)
if err != nil {
return fmt.Errorf("failed to start new default instance: %w", err)
}
// Set the new instance as default
if err := registry.SetDefaultInstance(instance.Address); err != nil {
return fmt.Errorf("failed to set default instance: %w", err)
}
}
return nil
}
func newTaskNewCommand() *cobra.Command {
var (
images []string
@@ -90,8 +110,6 @@ func newTaskNewCommand() *cobra.Command {
workspaces []string
address string
mode string
settings []string
yolo bool
)
cmd := &cobra.Command{
@@ -127,25 +145,19 @@ func newTaskNewCommand() *cobra.Command {
fmt.Printf("Mode set to: %s\n", mode)
}
// Inject yolo_mode_toggled setting if --yolo flag is set
// Will append to the -s settings to be parsed by the settings parser logic.
// If the yoloMode is also set in the settings, this will override that, since it will be set last.
if yolo {
settings = append(settings, "yolo_mode_toggled=true")
}
// Create the task
taskID, err := taskManager.CreateTask(ctx, prompt, images, files, workspaces, settings)
taskID, err := taskManager.CreateTask(ctx, prompt, images, files, workspaces)
if err != nil {
return fmt.Errorf("failed to create task: %w", err)
}
fmt.Printf("Task created successfully with ID: %s\n", taskID)
fmt.Printf("Using instance: %s\n", taskManager.GetCurrentInstance())
// Wait for completion if requested
if wait {
return taskManager.FollowConversation(ctx, taskManager.GetCurrentInstance())
fmt.Println("Following task conversation...")
return taskManager.FollowConversation(ctx)
}
return nil
@@ -158,73 +170,6 @@ func newTaskNewCommand() *cobra.Command {
cmd.Flags().StringSliceVarP(&workspaces, "workdir", "w", nil, "workdir directory paths")
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
cmd.Flags().StringVarP(&mode, "mode", "m", "", "mode (act|plan)")
cmd.Flags().StringSliceVarP(&settings, "setting", "s", nil, "task settings (key=value format, e.g., -s aws-region=us-west-2 -s mode=act)")
cmd.Flags().BoolVarP(&yolo, "yolo", "y", false, "enable yolo mode (non-interactive)")
return cmd
}
func newTaskOneshotCommand() *cobra.Command {
var (
images []string
files []string
workspaces []string
address string
settings []string
)
cmd := &cobra.Command{
Use: "oneshot <prompt>",
Aliases: []string{"o"},
Short: "Create a task in yolo+plan mode and view until completion",
Long: `Creates a new task in yolo mode (non-interactive) and plan mode, then streams the conversation until completion.`,
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Get prompt from args/stdin
prompt, err := getContentFromStdinAndArgs(args)
if err != nil {
return fmt.Errorf("failed to read prompt: %w", err)
}
if prompt == "" {
return fmt.Errorf("prompt required: provide as argument or pipe via stdin")
}
// Ensure task manager
if err := ensureTaskManager(ctx, address); err != nil {
return err
}
// Set mode to plan
if err := taskManager.SetMode(ctx, "plan", nil, nil, nil); err != nil {
return fmt.Errorf("failed to set plan mode: %w", err)
}
fmt.Println("Mode set to: plan")
// Inject yolo mode into settings
settings = append(settings, "yolo_mode_toggled=true")
// Create task
taskID, err := taskManager.CreateTask(ctx, prompt, images, files, workspaces, settings)
if err != nil {
return fmt.Errorf("failed to create task: %w", err)
}
fmt.Printf("Task created in yolo+plan mode (ID: %s)\n", taskID)
fmt.Printf("Using instance: %s\n", taskManager.GetCurrentInstance())
// Follow until completion
return taskManager.FollowConversationUntilCompletion(ctx)
},
}
cmd.Flags().StringSliceVarP(&images, "image", "i", nil, "attach image files")
cmd.Flags().StringSliceVarP(&files, "file", "f", nil, "attach files")
cmd.Flags().StringSliceVarP(&workspaces, "workdir", "w", nil, "workdir directory paths")
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
cmd.Flags().StringSliceVarP(&settings, "setting", "s", nil, "task settings (key=value format, e.g., -s model=claude)")
return cmd
}
@@ -257,7 +202,7 @@ func newTaskCancelCommand() *cobra.Command {
return cmd
}
func NewTaskSendCommand() *cobra.Command {
func newTaskSendCommand() *cobra.Command {
var (
images []string
files []string
@@ -351,8 +296,10 @@ func newTaskFollowCommand() *cobra.Command {
if err := ensureTaskManager(ctx, address); err != nil {
return err
}
return taskManager.FollowConversation(ctx, taskManager.GetCurrentInstance())
fmt.Printf("Using instance: %s\n", taskManager.GetCurrentInstance())
return taskManager.FollowConversation(ctx)
},
}
@@ -455,60 +402,6 @@ func newTaskResumeCommand() *cobra.Command {
return cmd
}
func newTaskRestoreCommand() *cobra.Command {
var (
restoreType string
address string
)
cmd := &cobra.Command{
Use: "restore <checkpoint-id>",
Short: "Restore task to a specific checkpoint",
Long: `Restore the current task to a specific checkpoint by checkpoint ID (timestamp) and by type.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
checkpointID := args[0]
// Convert checkpoint ID string to int64
id, err := strconv.ParseInt(checkpointID, 10, 64)
if err != nil {
return fmt.Errorf("invalid checkpoint ID '%s': must be a valid number", checkpointID)
}
validTypes := []string{"task", "workspace", "taskAndWorkspace"}
if !slices.Contains(validTypes, restoreType) {
return fmt.Errorf("invalid restore type '%s': must be one of [task, workspace, taskAndWorkspace]", restoreType)
}
// Ensure task manager is initialized
if err := ensureTaskManager(ctx, address); err != nil {
return err
}
// Validate checkpoint exists before attempting restore
if err := taskManager.ValidateCheckpointExists(ctx, id); err != nil {
return err
}
fmt.Printf("Using instance: %s\n", taskManager.GetCurrentInstance())
fmt.Printf("Restoring to checkpoint %d (type: %s)\n", id, restoreType)
if err := taskManager.RestoreCheckpoint(ctx, id, restoreType); err != nil {
return fmt.Errorf("failed to restore checkpoint: %w", err)
}
fmt.Println("Checkpoint restored successfully")
return nil
},
}
cmd.Flags().StringVarP(&restoreType, "type", "t", "task", "Restore type (task, workspace, taskAndWorkspace)")
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
// getContentFromStdinAndArgs reads content from both command line args and stdin, and combines them
func getContentFromStdinAndArgs(args []string) (string, error) {
var content strings.Builder
+47 -219
View File
@@ -24,13 +24,12 @@ type Manager struct {
renderer *display.Renderer
streamingDisplay *display.StreamingDisplay
handlerRegistry *handlers.HandlerRegistry
isStreamingMode bool
}
// NewManager creates a new task manager
func NewManager(client *client.ClineClient) *Manager {
state := types.NewConversationState()
renderer := display.NewRenderer(global.Config.OutputFormat)
renderer := display.NewRenderer()
streamingDisplay := display.NewStreamingDisplay(state, renderer)
// Create handler registry and register handlers
@@ -107,7 +106,7 @@ func (m *Manager) GetCurrentInstance() string {
}
// CreateTask creates a new task
func (m *Manager) CreateTask(ctx context.Context, prompt string, images, files []string, workspacePaths []string, settingsFlags []string) (string, error) {
func (m *Manager) CreateTask(ctx context.Context, prompt string, images, files []string, workspacePaths []string) (string, error) {
m.mu.Lock()
defer m.mu.Unlock()
@@ -122,9 +121,6 @@ func (m *Manager) CreateTask(ctx context.Context, prompt string, images, files [
if len(workspacePaths) > 0 {
m.renderer.RenderDebug("Workspaces: %v", workspacePaths)
}
if len(settingsFlags) > 0 {
m.renderer.RenderDebug("Settings: %v", settingsFlags)
}
}
// Check if there's an active task and cancel it first
@@ -132,22 +128,11 @@ func (m *Manager) CreateTask(ctx context.Context, prompt string, images, files [
return "", fmt.Errorf("failed to cancel existing task: %w", err)
}
// Parse task settings if provided
var taskSettings *cline.Settings
if len(settingsFlags) > 0 {
var err error
taskSettings, _, err = ParseTaskSettings(settingsFlags)
if err != nil {
return "", fmt.Errorf("failed to parse task settings: %w", err)
}
}
// Create task request
req := &cline.NewTaskRequest{
Text: prompt,
Images: images,
Files: files,
TaskSettings: taskSettings,
Text: prompt,
Images: images,
Files: files,
}
resp, err := m.client.Task.NewTask(ctx, req)
@@ -199,38 +184,11 @@ func (m *Manager) cancelExistingTaskIfNeeded(ctx context.Context) error {
fmt.Println("Cancelled existing task to start new one")
}
}
}
}
return nil
}
// ValidateCheckpointExists checks if a checkpoint ID is valid
func (m *Manager) ValidateCheckpointExists(ctx context.Context, checkpointID int64) error {
// Get current state
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to get state: %w", err)
}
// Extract messages
messages, err := m.extractMessagesFromState(state.StateJson)
if err != nil {
return fmt.Errorf("failed to extract messages: %w", err)
}
// Find and validate the checkpoint message
for _, msg := range messages {
if msg.Timestamp == checkpointID {
if msg.Say != string(types.SayTypeCheckpointCreated) {
return fmt.Errorf("timestamp %d is not a checkpoint (type: %s)", checkpointID, msg.Type)
}
return nil // Valid checkpoint
}
}
return fmt.Errorf("checkpoint ID %d not found in task history", checkpointID)
}
// CheckSendDisabled determines if we can send a message to the current task
// We duplicate the logic from buttonConfig::getButtonConfig
func (m *Manager) CheckSendDisabled(ctx context.Context) (bool, error) {
@@ -491,27 +449,6 @@ func (m *Manager) ResumeTask(ctx context.Context, taskID string) error {
return nil
}
// RestoreCheckpoint restores the task to a specific checkpoint
func (m *Manager) RestoreCheckpoint(ctx context.Context, checkpointID int64, restoreType string) error {
if global.Config.Verbose {
m.renderer.RenderDebug("Restoring checkpoint: %d (type: %s)", checkpointID, restoreType)
}
// Create the checkpoint restore request
req := &cline.CheckpointRestoreRequest{
Metadata: &cline.Metadata{},
Number: checkpointID,
RestoreType: restoreType,
}
_, err := m.client.Checkpoints.CheckpointRestore(ctx, req)
if err != nil {
return fmt.Errorf("failed to restore checkpoint %d: %w", checkpointID, err)
}
return nil
}
// CancelTask cancels the current task
func (m *Manager) CancelTask(ctx context.Context) error {
m.mu.Lock()
@@ -579,11 +516,6 @@ func (m *Manager) GatherFinalSummary(ctx context.Context) error {
// ShowConversation displays the current conversation
func (m *Manager) ShowConversation(ctx context.Context) error {
// Disable streaming mode for static view
m.mu.Lock()
m.isStreamingMode = false
m.mu.Unlock()
m.mu.RLock()
defer m.mu.RUnlock()
@@ -604,30 +536,16 @@ func (m *Manager) ShowConversation(ctx context.Context) error {
return nil
}
// Display messages
for i, msg := range messages {
if msg.Partial {
continue
}
m.displayMessage(msg, false, false, i)
}
return nil
}
func (m *Manager) FollowConversation(ctx context.Context, instanceAddress string) error {
// Enable streaming mode
m.mu.Lock()
m.isStreamingMode = true
m.mu.Unlock()
if global.Config.OutputFormat != "plain" {
markdown := fmt.Sprintf("*Using instance: %s*\n*Press Ctrl+C to exit*", instanceAddress)
rendered := m.renderer.RenderMarkdown(markdown)
fmt.Printf("%s", rendered)
} else {
fmt.Printf("Using instance: %s\n", instanceAddress)
fmt.Println("Following task conversation... (Press Ctrl+C to exit)")
}
func (m *Manager) FollowConversation(ctx context.Context) error {
fmt.Println("Following task conversation... (Press Ctrl+C to exit)")
ctx, cancel := context.WithCancel(ctx)
defer cancel()
@@ -643,6 +561,8 @@ func (m *Manager) FollowConversation(ctx context.Context, instanceAddress string
}
coordinator.SetConversationTurnStartIndex(totalMessageCount)
fmt.Println("\n--- Live updates ---")
// Start both streams concurrently
errChan := make(chan error, 2)
@@ -665,12 +585,7 @@ func (m *Manager) FollowConversation(ctx context.Context, instanceAddress string
// FollowConversationUntilCompletion streams conversation updates until task completion
func (m *Manager) FollowConversationUntilCompletion(ctx context.Context) error {
// Enable streaming mode
m.mu.Lock()
m.isStreamingMode = true
m.mu.Unlock()
fmt.Println("Following task conversation until completion... (Press Ctrl+C to exit)")
fmt.Println("Streaming conversation until completion... (Press Ctrl+C to exit)")
ctx, cancel := context.WithCancel(ctx)
defer cancel()
@@ -678,10 +593,10 @@ func (m *Manager) FollowConversationUntilCompletion(ctx context.Context) error {
// Create stream coordinator
coordinator := NewStreamCoordinator()
// Load history first
totalMessageCount, err := m.loadAndDisplayRecentHistory(ctx)
// Get current message count without displaying history
totalMessageCount, err := m.getCurrentMessageCount(ctx)
if err != nil {
m.renderer.RenderDebug("Warning: Failed to load conversation history: %v", err)
m.renderer.RenderDebug("Warning: Failed to get current message count: %v", err)
totalMessageCount = 0
}
coordinator.SetConversationTurnStartIndex(totalMessageCount)
@@ -834,96 +749,27 @@ func (m *Manager) processStateUpdate(stateUpdate *cline.State, coordinator *Stre
foundCompletion = true
}
// Currently handling a subset of message types for displaying
switch {
case msg.Say == string(types.SayTypeUserFeedback):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println()
if !coordinator.IsProcessedInCurrentTurn("user_msg") {
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Say == string(types.SayTypeCommand):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println()
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Say == string(types.SayTypeCommandOutput):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Say == string(types.SayTypeBrowserActionLaunch):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println()
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Say == string(types.SayTypeMcpServerRequestStarted):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println()
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
coordinator.MarkProcessedInCurrentTurn("user_msg")
}
case msg.Say == string(types.SayTypeCheckpointCreated):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println()
if !coordinator.IsProcessedInCurrentTurn("checkpoint") {
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
coordinator.MarkProcessedInCurrentTurn("checkpoint")
}
case msg.Say == string(types.SayTypeAPIReqStarted):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
apiInfo := types.APIRequestInfo{Cost: -1}
if err := json.Unmarshal([]byte(msg.Text), &apiInfo); err == nil && apiInfo.Cost >= 0 {
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println() // adds a separator between cline message and usage message
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
coordinator.CompleteTurn(len(messages))
displayedUsage = true
}
}
case msg.Ask == string(types.AskTypeCommandOutput):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println() // adds a separator between cline message and usage message
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Ask == string(types.AskTypePlanModeRespond):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
// Only process when message is complete (partial=false)
if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println()
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Type == types.MessageTypeAsk:
msgKey := fmt.Sprintf("%d", msg.Timestamp)
// In streaming mode, partial stream handles headers for ask messages
// State stream should skip them to avoid duplication
if m.isStreamingMode {
// Skip - partial stream already handled this
} else {
// Non-streaming mode: render normally
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println()
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
coordinator.CompleteTurn(len(messages))
displayedUsage = true
}
}
}
@@ -944,10 +790,6 @@ func (m *Manager) handlePartialMessageStream(ctx context.Context, coordinator *S
return
}
defer func() {
m.streamingDisplay.FreezeActiveSegment()
}()
for {
select {
case <-ctx.Done():
@@ -1004,17 +846,12 @@ func (m *Manager) displayMessage(msg *types.ClineMessage, isLast, isPartial bool
if global.Config.OutputFormat == "json" {
return m.outputMessageAsJSON(msg)
} else {
m.mu.RLock()
isStreaming := m.isStreamingMode
m.mu.RUnlock()
dc := &handlers.DisplayContext{
State: m.state,
Renderer: m.renderer,
IsLast: isLast,
IsPartial: isPartial,
MessageIndex: messageIndex,
IsStreamingMode: isStreaming,
State: m.state,
Renderer: m.renderer,
IsLast: isLast,
IsPartial: isPartial,
MessageIndex: messageIndex,
}
return m.handlerRegistry.Handle(msg, dc)
@@ -1032,6 +869,21 @@ func (m *Manager) outputMessageAsJSON(msg *types.ClineMessage) error {
return nil
}
// getCurrentMessageCount gets the current message count without displaying messages
func (m *Manager) getCurrentMessageCount(ctx context.Context) (int, error) {
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return 0, fmt.Errorf("failed to get state: %w", err)
}
messages, err := m.extractMessagesFromState(state.StateJson)
if err != nil {
return 0, fmt.Errorf("failed to extract messages: %w", err)
}
return len(messages), nil
}
// loadAndDisplayRecentHistory loads and displays recent conversation history and returns the total number of existing messages
func (m *Manager) loadAndDisplayRecentHistory(ctx context.Context) (int, error) {
// Get the latest state which contains messages
@@ -1056,35 +908,18 @@ func (m *Manager) loadAndDisplayRecentHistory(ctx context.Context) (int, error)
totalMessages := len(messages)
startIndex := 0
if totalMessages > maxHistoryMessages {
startIndex = totalMessages - maxHistoryMessages
if global.Config.OutputFormat != "plain" {
markdown := fmt.Sprintf("*Conversation history (%d of %d messages)*", maxHistoryMessages, totalMessages)
rendered := m.renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n\n", rendered)
} else {
fmt.Printf("--- Conversation history (%d of %d messages) ---\n", maxHistoryMessages, totalMessages)
}
fmt.Printf("--- Conversation history (%d of %d messages) ---\n", maxHistoryMessages, totalMessages)
} else {
if global.Config.OutputFormat != "plain" {
markdown := fmt.Sprintf("*Conversation history (%d messages)*", totalMessages)
rendered := m.renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n\n", rendered)
} else {
fmt.Printf("--- Conversation history (%d messages) ---\n", totalMessages)
}
fmt.Printf("--- Conversation history (%d messages) ---\n", totalMessages)
}
// Display recent messages
for i := startIndex; i < len(messages); i++ {
msg := messages[i]
if msg.Partial {
continue
}
// Display the message
m.displayMessage(msg, false, false, i)
}
@@ -1102,13 +937,6 @@ func (m *Manager) GetState() *types.ConversationState {
return m.state
}
// GetClient returns the underlying ClineClient for direct gRPC calls
func (m *Manager) GetClient() *client.ClineClient {
m.mu.RLock()
defer m.mu.RUnlock()
return m.client
}
// Cleanup cleans up resources
func (m *Manager) Cleanup() {
// Clean up streaming display resources if needed
-762
View File
@@ -1,762 +0,0 @@
package task
import (
"fmt"
"strconv"
"strings"
"github.com/cline/grpc-go/cline"
)
func ParseTaskSettings(settingsFlags []string) (*cline.Settings, *cline.Secrets, error) {
if len(settingsFlags) == 0 {
return nil, nil, nil
}
settings := &cline.Settings{}
secrets := &cline.Secrets{}
nestedSettings := make(map[string]map[string]string)
for _, flag := range settingsFlags {
// Parse key=value
parts := strings.SplitN(flag, "=", 2)
if len(parts) != 2 {
return nil, nil, fmt.Errorf("invalid setting format '%s': expected key=value", flag)
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
// Convert kebab-case to snake_case
key = kebabToSnake(key)
// Check if this is a nested setting (contains a dot)
if strings.Contains(key, ".") {
dotParts := strings.SplitN(key, ".", 2)
parentField := dotParts[0]
childField := dotParts[1]
if nestedSettings[parentField] == nil {
nestedSettings[parentField] = make(map[string]string)
}
nestedSettings[parentField][childField] = value
} else {
// Check if it's a secret field first, then settings field
if err := setSecretField(secrets, key, value); err == nil {
// Successfully set as secret, continue
continue
}
// Not a secret, try as a settings field
if err := setSimpleField(settings, key, value); err != nil {
return nil, nil, fmt.Errorf("error setting field '%s': %w", key, err)
}
}
}
// Process nested settings
for parentField, childFields := range nestedSettings {
if err := setNestedField(settings, parentField, childFields); err != nil {
return nil, nil, fmt.Errorf("error setting nested field '%s': %w", parentField, err)
}
}
return settings, secrets, nil
}
// kebabToSnake converts kebab-case to snake_case
func kebabToSnake(s string) string {
return strings.ReplaceAll(s, "-", "_")
}
// Pointer helper functions for optional protobuf fields
func strPtr(s string) *string { return &s }
func boolPtr(b bool) *bool { return &b }
func int32Ptr(i int32) *int32 { return &i }
func int64Ptr(i int64) *int64 { return &i }
func float64Ptr(f float64) *float64 { return &f }
// setSimpleField sets a simple (non-nested) field on Settings
func setSimpleField(settings *cline.Settings, key, value string) error {
switch key {
// String fields
case "aws_region":
settings.AwsRegion = strPtr(value)
case "aws_bedrock_endpoint":
settings.AwsBedrockEndpoint = strPtr(value)
case "aws_profile":
settings.AwsProfile = strPtr(value)
case "aws_authentication":
settings.AwsAuthentication = strPtr(value)
case "vertex_project_id":
settings.VertexProjectId = strPtr(value)
case "vertex_region":
settings.VertexRegion = strPtr(value)
case "requesty_base_url":
settings.RequestyBaseUrl = strPtr(value)
case "open_ai_base_url":
settings.OpenAiBaseUrl = strPtr(value)
case "ollama_base_url":
settings.OllamaBaseUrl = strPtr(value)
case "ollama_api_options_ctx_num":
settings.OllamaApiOptionsCtxNum = strPtr(value)
case "lm_studio_base_url":
settings.LmStudioBaseUrl = strPtr(value)
case "lm_studio_max_tokens":
settings.LmStudioMaxTokens = strPtr(value)
case "anthropic_base_url":
settings.AnthropicBaseUrl = strPtr(value)
case "gemini_base_url":
settings.GeminiBaseUrl = strPtr(value)
case "azure_api_version":
settings.AzureApiVersion = strPtr(value)
case "open_router_provider_sorting":
settings.OpenRouterProviderSorting = strPtr(value)
case "lite_llm_base_url":
settings.LiteLlmBaseUrl = strPtr(value)
case "qwen_api_line":
settings.QwenApiLine = strPtr(value)
case "moonshot_api_line":
settings.MoonshotApiLine = strPtr(value)
case "zai_api_line":
settings.ZaiApiLine = strPtr(value)
case "telemetry_setting":
settings.TelemetrySetting = strPtr(value)
case "asksage_api_url":
settings.AsksageApiUrl = strPtr(value)
case "default_terminal_profile":
settings.DefaultTerminalProfile = strPtr(value)
case "sap_ai_core_token_url":
settings.SapAiCoreTokenUrl = strPtr(value)
case "sap_ai_core_base_url":
settings.SapAiCoreBaseUrl = strPtr(value)
case "sap_ai_resource_group":
settings.SapAiResourceGroup = strPtr(value)
case "claude_code_path":
settings.ClaudeCodePath = strPtr(value)
case "qwen_code_oauth_path":
settings.QwenCodeOauthPath = strPtr(value)
case "preferred_language":
settings.PreferredLanguage = strPtr(value)
case "custom_prompt":
settings.CustomPrompt = strPtr(value)
case "dify_base_url":
settings.DifyBaseUrl = strPtr(value)
case "oca_base_url":
settings.OcaBaseUrl = strPtr(value)
case "plan_mode_api_model_id":
settings.PlanModeApiModelId = strPtr(value)
case "plan_mode_reasoning_effort":
settings.PlanModeReasoningEffort = strPtr(value)
case "plan_mode_aws_bedrock_custom_model_base_id":
settings.PlanModeAwsBedrockCustomModelBaseId = strPtr(value)
case "plan_mode_open_router_model_id":
settings.PlanModeOpenRouterModelId = strPtr(value)
case "plan_mode_open_ai_model_id":
settings.PlanModeOpenAiModelId = strPtr(value)
case "plan_mode_ollama_model_id":
settings.PlanModeOllamaModelId = strPtr(value)
case "plan_mode_lm_studio_model_id":
settings.PlanModeLmStudioModelId = strPtr(value)
case "plan_mode_lite_llm_model_id":
settings.PlanModeLiteLlmModelId = strPtr(value)
case "plan_mode_requesty_model_id":
settings.PlanModeRequestyModelId = strPtr(value)
case "plan_mode_together_model_id":
settings.PlanModeTogetherModelId = strPtr(value)
case "plan_mode_fireworks_model_id":
settings.PlanModeFireworksModelId = strPtr(value)
case "plan_mode_sap_ai_core_model_id":
settings.PlanModeSapAiCoreModelId = strPtr(value)
case "plan_mode_sap_ai_core_deployment_id":
settings.PlanModeSapAiCoreDeploymentId = strPtr(value)
case "plan_mode_groq_model_id":
settings.PlanModeGroqModelId = strPtr(value)
case "plan_mode_baseten_model_id":
settings.PlanModeBasetenModelId = strPtr(value)
case "plan_mode_hugging_face_model_id":
settings.PlanModeHuggingFaceModelId = strPtr(value)
case "plan_mode_huawei_cloud_maas_model_id":
settings.PlanModeHuaweiCloudMaasModelId = strPtr(value)
case "plan_mode_oca_model_id":
settings.PlanModeOcaModelId = strPtr(value)
case "plan_mode_vercel_ai_gateway_model_id":
settings.PlanModeVercelAiGatewayModelId = strPtr(value)
case "act_mode_api_model_id":
settings.ActModeApiModelId = strPtr(value)
case "act_mode_reasoning_effort":
settings.ActModeReasoningEffort = strPtr(value)
case "act_mode_aws_bedrock_custom_model_base_id":
settings.ActModeAwsBedrockCustomModelBaseId = strPtr(value)
case "act_mode_open_router_model_id":
settings.ActModeOpenRouterModelId = strPtr(value)
case "act_mode_open_ai_model_id":
settings.ActModeOpenAiModelId = strPtr(value)
case "act_mode_ollama_model_id":
settings.ActModeOllamaModelId = strPtr(value)
case "act_mode_lm_studio_model_id":
settings.ActModeLmStudioModelId = strPtr(value)
case "act_mode_lite_llm_model_id":
settings.ActModeLiteLlmModelId = strPtr(value)
case "act_mode_requesty_model_id":
settings.ActModeRequestyModelId = strPtr(value)
case "act_mode_together_model_id":
settings.ActModeTogetherModelId = strPtr(value)
case "act_mode_fireworks_model_id":
settings.ActModeFireworksModelId = strPtr(value)
case "act_mode_sap_ai_core_model_id":
settings.ActModeSapAiCoreModelId = strPtr(value)
case "act_mode_sap_ai_core_deployment_id":
settings.ActModeSapAiCoreDeploymentId = strPtr(value)
case "act_mode_groq_model_id":
settings.ActModeGroqModelId = strPtr(value)
case "act_mode_baseten_model_id":
settings.ActModeBasetenModelId = strPtr(value)
case "act_mode_hugging_face_model_id":
settings.ActModeHuggingFaceModelId = strPtr(value)
case "act_mode_huawei_cloud_maas_model_id":
settings.ActModeHuaweiCloudMaasModelId = strPtr(value)
case "act_mode_oca_model_id":
settings.ActModeOcaModelId = strPtr(value)
case "act_mode_vercel_ai_gateway_model_id":
settings.ActModeVercelAiGatewayModelId = strPtr(value)
// Boolean fields
case "aws_use_cross_region_inference":
val, err := parseBool(value)
if err != nil {
return err
}
settings.AwsUseCrossRegionInference = boolPtr(val)
case "aws_bedrock_use_prompt_cache":
val, err := parseBool(value)
if err != nil {
return err
}
settings.AwsBedrockUsePromptCache = boolPtr(val)
case "aws_use_profile":
val, err := parseBool(value)
if err != nil {
return err
}
settings.AwsUseProfile = boolPtr(val)
case "lite_llm_use_prompt_cache":
val, err := parseBool(value)
if err != nil {
return err
}
settings.LiteLlmUsePromptCache = boolPtr(val)
case "plan_act_separate_models_setting":
val, err := parseBool(value)
if err != nil {
return err
}
settings.PlanActSeparateModelsSetting = boolPtr(val)
case "enable_checkpoints_setting":
val, err := parseBool(value)
if err != nil {
return err
}
settings.EnableCheckpointsSetting = boolPtr(val)
case "sap_ai_core_use_orchestration_mode":
val, err := parseBool(value)
if err != nil {
return err
}
settings.SapAiCoreUseOrchestrationMode = boolPtr(val)
case "strict_plan_mode_enabled":
val, err := parseBool(value)
if err != nil {
return err
}
settings.StrictPlanModeEnabled = boolPtr(val)
case "yolo_mode_toggled":
val, err := parseBool(value)
if err != nil {
return err
}
settings.YoloModeToggled = boolPtr(val)
case "use_auto_condense":
val, err := parseBool(value)
if err != nil {
return err
}
settings.UseAutoCondense = boolPtr(val)
case "plan_mode_aws_bedrock_custom_selected":
val, err := parseBool(value)
if err != nil {
return err
}
settings.PlanModeAwsBedrockCustomSelected = boolPtr(val)
case "act_mode_aws_bedrock_custom_selected":
val, err := parseBool(value)
if err != nil {
return err
}
settings.ActModeAwsBedrockCustomSelected = boolPtr(val)
// Integer fields
case "request_timeout_ms":
val, err := parseInt32(value)
if err != nil {
return err
}
settings.RequestTimeoutMs = int32Ptr(val)
case "shell_integration_timeout":
val, err := parseInt32(value)
if err != nil {
return err
}
settings.ShellIntegrationTimeout = int32Ptr(val)
case "terminal_output_line_limit":
val, err := parseInt32(value)
if err != nil {
return err
}
settings.TerminalOutputLineLimit = int32Ptr(val)
case "fireworks_model_max_completion_tokens":
val, err := parseInt32(value)
if err != nil {
return err
}
settings.FireworksModelMaxCompletionTokens = int32Ptr(val)
case "fireworks_model_max_tokens":
val, err := parseInt32(value)
if err != nil {
return err
}
settings.FireworksModelMaxTokens = int32Ptr(val)
// Int64 fields
case "plan_mode_thinking_budget_tokens":
val, err := parseInt64(value)
if err != nil {
return err
}
settings.PlanModeThinkingBudgetTokens = int64Ptr(val)
case "act_mode_thinking_budget_tokens":
val, err := parseInt64(value)
if err != nil {
return err
}
settings.ActModeThinkingBudgetTokens = int64Ptr(val)
// Double fields
case "auto_condense_threshold":
val, err := parseFloat64(value)
if err != nil {
return err
}
settings.AutoCondenseThreshold = float64Ptr(val)
// Enum fields
// Note: We can use &val directly for enums because the parser functions return a new local variable.
// This is different from using &value (the loop variable), which would cause all fields to share
// the same memory address.
case "openai_reasoning_effort":
val, err := parseOpenaiReasoningEffort(value)
if err != nil {
return err
}
settings.OpenaiReasoningEffort = &val
case "mode":
val, err := parsePlanActMode(value)
if err != nil {
return err
}
settings.Mode = &val
case "plan_mode_api_provider":
val, err := parseApiProvider(value)
if err != nil {
return err
}
settings.PlanModeApiProvider = &val
case "act_mode_api_provider":
val, err := parseApiProvider(value)
if err != nil {
return err
}
settings.ActModeApiProvider = &val
default:
return fmt.Errorf("unsupported field '%s'", key)
}
return nil
}
// setNestedField sets a nested field on Settings
// Currently supports: auto_approval_settings, browser_settings
func setNestedField(settings *cline.Settings, parentField string, childFields map[string]string) error {
switch parentField {
case "auto_approval_settings":
if settings.AutoApprovalSettings == nil {
settings.AutoApprovalSettings = &cline.AutoApprovalSettings{}
}
return setAutoApprovalSettings(settings.AutoApprovalSettings, childFields)
case "browser_settings":
if settings.BrowserSettings == nil {
settings.BrowserSettings = &cline.BrowserSettings{}
}
return setBrowserSettings(settings.BrowserSettings, childFields)
default:
return fmt.Errorf("unsupported nested field '%s' (complex nested types are not supported via -s flags)", parentField)
}
}
// setAutoApprovalSettings sets fields on AutoApprovalSettings
func setAutoApprovalSettings(settings *cline.AutoApprovalSettings, fields map[string]string) error {
for key, value := range fields {
switch key {
case "enabled":
val, err := parseBool(value)
if err != nil {
return err
}
settings.Enabled = val
case "max_requests":
val, err := parseInt32(value)
if err != nil {
return err
}
settings.MaxRequests = val
case "enable_notifications":
val, err := parseBool(value)
if err != nil {
return err
}
settings.EnableNotifications = val
case "actions":
return fmt.Errorf("auto_approval_settings.actions requires nested dot notation (e.g., auto-approval-settings.actions.read-files=true)")
default:
// Check if this is an action field (actions.*)
if strings.HasPrefix(key, "actions.") {
actionField := strings.TrimPrefix(key, "actions.")
if settings.Actions == nil {
settings.Actions = &cline.AutoApprovalActions{}
}
if err := setAutoApprovalAction(settings.Actions, actionField, value); err != nil {
return err
}
// Continue processing other fields
} else {
return fmt.Errorf("unsupported auto_approval_settings field '%s'", key)
}
}
}
return nil
}
// setAutoApprovalAction sets fields on AutoApprovalActions
func setAutoApprovalAction(actions *cline.AutoApprovalActions, key, value string) error {
val, err := parseBool(value)
if err != nil {
return err
}
switch key {
case "read_files":
actions.ReadFiles = val
case "read_files_externally":
actions.ReadFilesExternally = val
case "edit_files":
actions.EditFiles = val
case "edit_files_externally":
actions.EditFilesExternally = val
case "execute_safe_commands":
actions.ExecuteSafeCommands = val
case "execute_all_commands":
actions.ExecuteAllCommands = val
case "use_browser":
actions.UseBrowser = val
case "use_mcp":
actions.UseMcp = val
default:
return fmt.Errorf("unsupported auto_approval_actions field '%s'", key)
}
return nil
}
// setBrowserSettings sets fields on BrowserSettings
func setBrowserSettings(settings *cline.BrowserSettings, fields map[string]string) error {
for key, value := range fields {
switch key {
case "viewport_width":
val, err := parseInt32(value)
if err != nil {
return err
}
if settings.Viewport == nil {
settings.Viewport = &cline.Viewport{}
}
settings.Viewport.Width = val
case "viewport_height":
val, err := parseInt32(value)
if err != nil {
return err
}
if settings.Viewport == nil {
settings.Viewport = &cline.Viewport{}
}
settings.Viewport.Height = val
case "remote_browser_host":
settings.RemoteBrowserHost = strPtr(value)
case "remote_browser_enabled":
val, err := parseBool(value)
if err != nil {
return err
}
settings.RemoteBrowserEnabled = boolPtr(val)
case "chrome_executable_path":
settings.ChromeExecutablePath = strPtr(value)
case "disable_tool_use":
val, err := parseBool(value)
if err != nil {
return err
}
settings.DisableToolUse = boolPtr(val)
case "custom_args":
settings.CustomArgs = strPtr(value)
default:
return fmt.Errorf("unsupported browser_settings field '%s'", key)
}
}
return nil
}
// Type parsing helpers
func parseBool(value string) (bool, error) {
lower := strings.ToLower(value)
switch lower {
case "true", "t", "yes", "y", "1":
return true, nil
case "false", "f", "no", "n", "0":
return false, nil
default:
return false, fmt.Errorf("invalid boolean value '%s': expected true/false", value)
}
}
func parseInt32(value string) (int32, error) {
val, err := strconv.ParseInt(value, 10, 32)
if err != nil {
return 0, fmt.Errorf("invalid integer value '%s': %w", value, err)
}
return int32(val), nil
}
func parseInt64(value string) (int64, error) {
val, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid integer value '%s': %w", value, err)
}
return val, nil
}
func parseFloat64(value string) (float64, error) {
val, err := strconv.ParseFloat(value, 64)
if err != nil {
return 0, fmt.Errorf("invalid float value '%s': %w", value, err)
}
return val, nil
}
// Enum parsing helpers
func parseOpenaiReasoningEffort(value string) (cline.OpenaiReasoningEffort, error) {
lower := strings.ToLower(value)
switch lower {
case "low":
return cline.OpenaiReasoningEffort_LOW, nil
case "medium":
return cline.OpenaiReasoningEffort_MEDIUM, nil
case "high":
return cline.OpenaiReasoningEffort_HIGH, nil
default:
return cline.OpenaiReasoningEffort_LOW, fmt.Errorf("invalid openai_reasoning_effort '%s': expected low/medium/high", value)
}
}
func parsePlanActMode(value string) (cline.PlanActMode, error) {
lower := strings.ToLower(value)
switch lower {
case "plan":
return cline.PlanActMode_PLAN, nil
case "act":
return cline.PlanActMode_ACT, nil
default:
return cline.PlanActMode_ACT, fmt.Errorf("invalid mode '%s': expected plan/act", value)
}
}
func parseApiProvider(value string) (cline.ApiProvider, error) {
lower := strings.ToLower(value)
switch lower {
case "anthropic":
return cline.ApiProvider_ANTHROPIC, nil
case "openrouter":
return cline.ApiProvider_OPENROUTER, nil
case "bedrock":
return cline.ApiProvider_BEDROCK, nil
case "vertex":
return cline.ApiProvider_VERTEX, nil
case "openai":
return cline.ApiProvider_OPENAI, nil
case "ollama":
return cline.ApiProvider_OLLAMA, nil
case "lmstudio":
return cline.ApiProvider_LMSTUDIO, nil
case "gemini":
return cline.ApiProvider_GEMINI, nil
case "openai_native":
return cline.ApiProvider_OPENAI_NATIVE, nil
case "requesty":
return cline.ApiProvider_REQUESTY, nil
case "together":
return cline.ApiProvider_TOGETHER, nil
case "deepseek":
return cline.ApiProvider_DEEPSEEK, nil
case "qwen":
return cline.ApiProvider_QWEN, nil
case "doubao":
return cline.ApiProvider_DOUBAO, nil
case "mistral":
return cline.ApiProvider_MISTRAL, nil
case "vscode_lm":
return cline.ApiProvider_VSCODE_LM, nil
case "cline":
return cline.ApiProvider_CLINE, nil
case "litellm":
return cline.ApiProvider_LITELLM, nil
case "nebius":
return cline.ApiProvider_NEBIUS, nil
case "fireworks":
return cline.ApiProvider_FIREWORKS, nil
case "asksage":
return cline.ApiProvider_ASKSAGE, nil
case "xai", "grok":
return cline.ApiProvider_XAI, nil
case "sambanova":
return cline.ApiProvider_SAMBANOVA, nil
case "cerebras":
return cline.ApiProvider_CEREBRAS, nil
case "groq":
return cline.ApiProvider_GROQ, nil
case "sapaicore", "sap_ai_core":
return cline.ApiProvider_SAPAICORE, nil
case "claude_code":
return cline.ApiProvider_CLAUDE_CODE, nil
case "moonshot":
return cline.ApiProvider_MOONSHOT, nil
case "huggingface":
return cline.ApiProvider_HUGGINGFACE, nil
case "huawei_cloud_maas":
return cline.ApiProvider_HUAWEI_CLOUD_MAAS, nil
case "baseten":
return cline.ApiProvider_BASETEN, nil
case "zai":
return cline.ApiProvider_ZAI, nil
case "vercel_ai_gateway":
return cline.ApiProvider_VERCEL_AI_GATEWAY, nil
case "qwen_code":
return cline.ApiProvider_QWEN_CODE, nil
case "dify":
return cline.ApiProvider_DIFY, nil
case "oca":
return cline.ApiProvider_OCA, nil
default:
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("invalid api_provider '%s'", value)
}
}
// setSecretField sets a secret field on Secrets
// All secret fields are optional strings
// Returns nil if field was successfully set, error otherwise
func setSecretField(secrets *cline.Secrets, key, value string) error {
switch key {
case "api_key":
secrets.ApiKey = strPtr(value)
case "open_router_api_key":
secrets.OpenRouterApiKey = strPtr(value)
case "aws_access_key":
secrets.AwsAccessKey = strPtr(value)
case "aws_secret_key":
secrets.AwsSecretKey = strPtr(value)
case "aws_session_token":
secrets.AwsSessionToken = strPtr(value)
case "aws_bedrock_api_key":
secrets.AwsBedrockApiKey = strPtr(value)
case "open_ai_api_key":
secrets.OpenAiApiKey = strPtr(value)
case "gemini_api_key":
secrets.GeminiApiKey = strPtr(value)
case "open_ai_native_api_key":
secrets.OpenAiNativeApiKey = strPtr(value)
case "ollama_api_key":
secrets.OllamaApiKey = strPtr(value)
case "deep_seek_api_key":
secrets.DeepSeekApiKey = strPtr(value)
case "requesty_api_key":
secrets.RequestyApiKey = strPtr(value)
case "together_api_key":
secrets.TogetherApiKey = strPtr(value)
case "fireworks_api_key":
secrets.FireworksApiKey = strPtr(value)
case "qwen_api_key":
secrets.QwenApiKey = strPtr(value)
case "doubao_api_key":
secrets.DoubaoApiKey = strPtr(value)
case "mistral_api_key":
secrets.MistralApiKey = strPtr(value)
case "lite_llm_api_key":
secrets.LiteLlmApiKey = strPtr(value)
case "auth_nonce":
secrets.AuthNonce = strPtr(value)
case "asksage_api_key":
secrets.AsksageApiKey = strPtr(value)
case "xai_api_key":
secrets.XaiApiKey = strPtr(value)
case "moonshot_api_key":
secrets.MoonshotApiKey = strPtr(value)
case "zai_api_key":
secrets.ZaiApiKey = strPtr(value)
case "hugging_face_api_key":
secrets.HuggingFaceApiKey = strPtr(value)
case "nebius_api_key":
secrets.NebiusApiKey = strPtr(value)
case "sambanova_api_key":
secrets.SambanovaApiKey = strPtr(value)
case "cerebras_api_key":
secrets.CerebrasApiKey = strPtr(value)
case "sap_ai_core_client_id":
secrets.SapAiCoreClientId = strPtr(value)
case "sap_ai_core_client_secret":
secrets.SapAiCoreClientSecret = strPtr(value)
case "groq_api_key":
secrets.GroqApiKey = strPtr(value)
case "huawei_cloud_maas_api_key":
secrets.HuaweiCloudMaasApiKey = strPtr(value)
case "baseten_api_key":
secrets.BasetenApiKey = strPtr(value)
case "vercel_ai_gateway_api_key":
secrets.VercelAiGatewayApiKey = strPtr(value)
case "dify_api_key":
secrets.DifyApiKey = strPtr(value)
case "oca_api_key":
secrets.OcaApiKey = strPtr(value)
case "oca_refresh_token":
secrets.OcaRefreshToken = strPtr(value)
default:
return fmt.Errorf("unsupported secret field '%s'", key)
}
return nil
}
// Note: message types not supported via -s flags:
// - OpenRouterModelInfo, OpenAiCompatibleModelInfo, LiteLLMModelInfo, OcaModelInfo
// - LanguageModelChatSelector
// - DictationSettings
// - FocusChainSettings
+2 -3
View File
@@ -34,9 +34,8 @@ func (sc *StreamCoordinator) IsProcessedInCurrentTurn(key string) bool {
return sc.processedInCurrentTurn[key]
}
// CompleteTurn updates the start index for the next batch of messages
// Note: Does NOT reset the processed map - that persists across state updates
// CompleteTurn resets the coordinator for the next conversation turn
func (sc *StreamCoordinator) CompleteTurn(totalMessages int) {
sc.conversationTurnStartIndex = totalMessages
// Don't reset processedInCurrentTurn - it should persist across state updates
sc.processedInCurrentTurn = make(map[string]bool)
}
+17 -24
View File
@@ -3,26 +3,22 @@ package types
import (
"encoding/json"
"fmt"
"strconv"
"time"
"strconv"
"github.com/cline/grpc-go/cline"
)
// ClineMessage represents a conversation message in the CLI
type ClineMessage struct {
Type MessageType `json:"type"`
Text string `json:"text"`
Timestamp int64 `json:"ts"`
Reasoning string `json:"reasoning,omitempty"`
Say string `json:"say,omitempty"`
Ask string `json:"ask,omitempty"`
Partial bool `json:"partial,omitempty"`
Images []string `json:"images,omitempty"`
Files []string `json:"files,omitempty"`
LastCheckpointHash string `json:"lastCheckpointHash,omitempty"`
IsCheckpointCheckedOut bool `json:"isCheckpointCheckedOut,omitempty"`
IsOperationOutsideWorkspace bool `json:"isOperationOutsideWorkspace,omitempty"`
Type MessageType `json:"type"`
Text string `json:"text"`
Timestamp int64 `json:"ts"`
Reasoning string `json:"reasoning,omitempty"`
Say string `json:"say,omitempty"`
Ask string `json:"ask,omitempty"`
Partial bool `json:"partial,omitempty"`
Images []string `json:"images,omitempty"`
Files []string `json:"files,omitempty"`
}
// MessageType represents the type of message
@@ -210,16 +206,13 @@ func ConvertProtoToMessage(protoMsg *cline.ClineMessage) *ClineMessage {
}
return &ClineMessage{
Type: msgType,
Text: protoMsg.Text,
Timestamp: protoMsg.Ts,
Reasoning: protoMsg.Reasoning,
Say: say,
Ask: ask,
Partial: protoMsg.Partial,
LastCheckpointHash: protoMsg.LastCheckpointHash,
IsCheckpointCheckedOut: protoMsg.IsCheckpointCheckedOut,
IsOperationOutsideWorkspace: protoMsg.IsOperationOutsideWorkspace,
Type: msgType,
Text: protoMsg.Text,
Timestamp: protoMsg.Ts,
Reasoning: protoMsg.Reasoning,
Say: say,
Ask: ask,
Partial: protoMsg.Partial,
}
}
-39
View File
@@ -1,39 +0,0 @@
package generated
// FieldOverrides allows manual control over field relevance per provider
// This file is NOT auto-generated and can be edited manually to override
// the automatic field filtering logic.
//
// Usage:
// - Add provider-specific overrides to force include/exclude fields
// - true = force include this field for this provider
// - false = force exclude this field for this provider
// - If no override exists, automatic filtering logic applies
var FieldOverrides = map[string]map[string]bool{
// Format: "provider_id": {"field_name": shouldInclude}
// Example overrides (uncomment and modify as needed):
// "anthropic": {
// "requestTimeoutMs": true, // explicitly include
// "ollamaBaseUrl": false, // explicitly exclude
// },
// "bedrock": {
// "awsSessionToken": true, // include even if marked optional
// "azureApiVersion": false, // exclude even if general
// },
// Add more provider-specific overrides as needed
}
// GetFieldOverride returns the override setting for a field, if one exists
// Returns (shouldInclude, hasOverride)
func GetFieldOverride(providerID, fieldName string) (bool, bool) {
if providerOverrides, exists := FieldOverrides[providerID]; exists {
if override, hasOverride := providerOverrides[fieldName]; hasOverride {
return override, true
}
}
return false, false
}
File diff suppressed because it is too large Load Diff
+20 -26
View File
@@ -4,11 +4,8 @@ import (
"context"
"log"
"github.com/atotto/clipboard"
"github.com/cline/cli/pkg/cli"
"github.com/cline/grpc-go/cline"
"github.com/cline/grpc-go/host"
"google.golang.org/protobuf/proto"
)
// Global shutdown channel - simple approach
@@ -34,17 +31,11 @@ func NewEnvService(verbose bool) *EnvService {
// ClipboardWriteText writes text to the system clipboard
func (s *EnvService) ClipboardWriteText(ctx context.Context, req *cline.StringRequest) (*cline.Empty, error) {
if s.verbose {
log.Printf("ClipboardWriteText called with text length: %d", len(req.GetValue()))
}
err := clipboard.WriteAll(req.GetValue())
if err != nil {
if s.verbose {
log.Printf("Failed to write to clipboard: %v", err)
}
// Don't fail if clipboard is not available (e.g., headless environment)
log.Printf("ClipboardWriteText called with: %s", req.GetValue())
}
// TODO: Implement actual clipboard functionality
// For now, just return success
return &cline.Empty{}, nil
}
@@ -54,17 +45,23 @@ func (s *EnvService) ClipboardReadText(ctx context.Context, req *cline.EmptyRequ
log.Printf("ClipboardReadText called")
}
text, err := clipboard.ReadAll()
if err != nil {
if s.verbose {
log.Printf("Failed to read from clipboard: %v", err)
}
// Return empty string if clipboard is not available
text = ""
// TODO: Implement actual clipboard functionality
// For now, return empty string
return &cline.String{
Value: "",
}, nil
}
// GetMachineId returns a stable machine identifier for telemetry distinctId purposes
func (s *EnvService) GetMachineId(ctx context.Context, req *cline.EmptyRequest) (*cline.String, error) {
if s.verbose {
log.Printf("GetMachineId called")
}
// TODO: Implement actual machine ID functionality
// For now, return empty string
return &cline.String{
Value: text,
Value: "",
}, nil
}
@@ -74,12 +71,9 @@ func (s *EnvService) GetHostVersion(ctx context.Context, req *cline.EmptyRequest
log.Printf("GetHostVersion called")
}
return &host.GetHostVersionResponse{
Platform: proto.String("Cline CLI"),
Version: proto.String(""),
ClineType: proto.String("CLI"),
ClineVersion: proto.String(cli.Version),
}, nil
// TODO: Implement actual host version functionality
// For now, return empty response
return &host.GetHostVersionResponse{}, nil
}
// Shutdown initiates a graceful shutdown of the host bridge service
-20
View File
@@ -63,23 +63,3 @@ func (s *SimpleWorkspaceService) GetDiagnostics(ctx context.Context, req *host.G
FileDiagnostics: []*cline.FileDiagnostics{},
}, nil
}
// OpenProblemsPanel opens the problems panel - no-op for console implementation
func (s *SimpleWorkspaceService) OpenProblemsPanel(ctx context.Context, req *host.OpenProblemsPanelRequest) (*host.OpenProblemsPanelResponse, error) {
return &host.OpenProblemsPanelResponse{}, nil
}
// OpenInFileExplorerPanel opens a file/folder in the file explorer - no-op for console implementation
func (s *SimpleWorkspaceService) OpenInFileExplorerPanel(ctx context.Context, req *host.OpenInFileExplorerPanelRequest) (*host.OpenInFileExplorerPanelResponse, error) {
return &host.OpenInFileExplorerPanelResponse{}, nil
}
// OpenClineSidebarPanel opens the Cline sidebar panel - no-op for console implementation
func (s *SimpleWorkspaceService) OpenClineSidebarPanel(ctx context.Context, req *host.OpenClineSidebarPanelRequest) (*host.OpenClineSidebarPanelResponse, error) {
return &host.OpenClineSidebarPanelResponse{}, nil
}
// OpenTerminalPanel opens the terminal panel - no-op for console implementation
func (s *SimpleWorkspaceService) OpenTerminalPanel(ctx context.Context, req *host.OpenTerminalRequest) (*host.OpenTerminalResponse, error) {
return &host.OpenTerminalResponse{}, nil
}
-1
View File
@@ -117,7 +117,6 @@
"features/drag-and-drop",
"features/editing-messages",
"features/focus-chain",
"features/multiroot-workspace",
"features/plan-and-act",
{
"group": "Slash Commands",
-131
View File
@@ -1,131 +0,0 @@
---
title: "Multiroot Workspace Support"
sidebarTitle: "Multiroot Workspace"
---
Cline's Multiroot feature _(experimental - Oct 1 2025)_ works seamlessly with VSCode's multi-root workspaces, letting you manage multiple project folders in a single workspace.
## What is Multiroot Workspace Support?
Instead of being limited to one project folder, Cline can read files, write code, and run commands across all folders in your VSCode workspace. This is helpful when working with monorepos, microservices, or when you're working on related projects simultaneously.
## Getting Started
### Setting Up Multi-Root Workspaces
1. **Add folders to your workspace:**
- Use `File > Add Folder to Workspace` in VSCode
- Or create a `.code-workspace` file with multiple folder paths
- Drag and drop folders to the File Explorer
- Select multiple folders when opening a new workspace
2. **Start using Cline** - Cline will automatically detect all your workspace folders and interact with them as needed.
For detailed instructions on setting up multi-root workspaces in VS Code, see [Microsoft's official guide](https://code.visualstudio.com/docs/editing/workspaces/multi-root-workspaces).
### How Cline Handles Multiple Workspaces
Once you have multiple folders, Cline automatically:
- Detects all your workspace folders
- Works with files across different projects
- Executes commands in the right context
- Handles path resolution intelligently
## Working Across Workspaces
### Let Cline explore, or guide it precisely
You can reference different workspaces naturally in your prompts:
```
"Read the package.json in my frontend folder and compare it with the backend dependencies"
```
```
"Create a shared utility function and update both the client and server to use it"
```
```
"Search for TODO comments across all my workspace folders"
```
## Common Use Cases
### Monorepo Development
Perfect for when you have related projects in one repository:
```
my-app.code-workspace
├── web/ (React frontend)
├── api/ (Node.js backend)
├── mobile/ (React Native)
└── shared/ (Common utilities)
```
Ask Cline: *"Update the API endpoint in both web and mobile apps to match the new backend route"*
### Microservices Architecture
Manage multiple services from one workspace:
```
services.code-workspace
├── user-service/
├── payment-service/
├── notifications/
└── infrastructure/
```
### Full-Stack Development
Keep everything together while maintaining separation:
```
fullstack.code-workspace
├── client/ (Frontend)
├── server/ (Backend API)
├── docs/ (Documentation)
└── deploy/ (Scripts & config)
```
### Auto-Approve Integration
Multiroot workspaces work with [Auto Approve](/features/auto-approve):
- Enable permissions for operations within workspace folders
- Restrict auto-approve for files outside your workspace(s)
- Configure different levels for different workspace folders
### Cross-Workspace Operations
Cline can complete tasks spanning multiple workspaces:
- **Refactoring**: Update imports and references across projects
- **Feature development**: Implement features requiring changes in multiple services
- **Documentation**: Generate docs referencing code from multiple folders
- **Testing**: Build & run tests across all workspaces and analyze results
When working with large multiroot workspaces, start in [Plan mode](/features/plan-and-act) to let Cline understand your project structure before making changes.
## Best Practices
### Organizing Your Workspaces
1. **Group related projects** that often need coordinated changes
2. **Use consistent folder structures** across workspaces when possible
3. **Name folders clearly** so Cline can understand your project structure
### Effective Prompting & Tips
When working with multiroot workspaces, these approaches work best:
- **Be specific** about which workspace when it matters: *"Update the user model in the backend workspace"*
- **Reference relationships**: *"The frontend uses the API types from the shared workspace"*
- **Describe cross-workspace operations**: *"This change needs to be reflected in both the web and mobile apps"*
- **Scope your searches** when dealing with large codebases: *"Search for 'TODO' in just the frontend workspace"*
- **Break down large tasks** into workspace-specific operations when possible
- **Consider excluding large folders** like `node_modules` from your workspace search Scope
-4
View File
@@ -139,10 +139,6 @@ if (process.env.TELEMETRY_SERVICE_API_KEY) {
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)
}
// Base configuration shared between extension and standalone builds
const baseConfig = {
bundle: true,
+2 -3
View File
@@ -2,7 +2,6 @@ cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
@@ -10,15 +9,15 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJP
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
+2 -9
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.32.7",
"version": "3.32.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.32.7",
"version": "3.32.6",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -60,7 +60,6 @@
"jwt-decode": "^4.0.0",
"mammoth": "^1.8.0",
"nice-grpc": "^2.1.12",
"node-machine-id": "^1.1.12",
"ollama": "^0.5.13",
"open": "^10.1.2",
"open-graph-scraper": "^6.9.0",
@@ -12408,12 +12407,6 @@
"version": "0.4.0",
"license": "MIT"
},
"node_modules/node-machine-id": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz",
"integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==",
"license": "MIT"
},
"node_modules/node-preload": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz",
+2 -8
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.32.7",
"version": "3.32.6",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -295,20 +295,15 @@
"vscode:prepublish": "npm run package",
"compile": "npm run check-types && npm run lint && node esbuild.mjs",
"compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
"compile-standalone-cli": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
"compile-cli": "scripts/build-cli.sh",
"download-node": "node scripts/download-node.mjs",
"test:install": "bash scripts/test-install.sh",
"dev:cli:watch": "node scripts/dev-cli-watch.mjs",
"dev:cli": "npm run compile-standalone && npm run compile-cli && ./cli/bin/cline instance kill --all && ./cli/bin/cline instance new",
"postcompile-standalone": "node scripts/package-standalone.mjs",
"postcompile-standalone-cli": "node scripts/package-standalone.mjs --target=cli",
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.mjs --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
"protos": "node scripts/build-proto.mjs",
"protos-go": "node scripts/build-go-proto.mjs",
"cli-providers": "node scripts/cli-providers.mjs",
"postprotos": "biome format src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --no-errors-on-unmatched",
"clean:build": "rimraf dist dist-standalone webview-ui/build src/generated out/",
"clean:deps": "rimraf node_modules webview-ui/node_modules",
@@ -449,7 +444,6 @@
"jwt-decode": "^4.0.0",
"mammoth": "^1.8.0",
"nice-grpc": "^2.1.12",
"node-machine-id": "^1.1.12",
"ollama": "^0.5.13",
"open": "^10.1.2",
"open-graph-scraper": "^6.9.0",
+1 -5
View File
@@ -2,6 +2,7 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
import "cline/state.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
@@ -31,11 +32,6 @@ message ChromePath {
bool is_bundled = 2;
}
message Viewport {
int32 width = 1;
int32 height = 2;
}
message BrowserSettings {
Viewport viewport = 1;
optional string remote_browser_host = 2;
-2
View File
@@ -107,7 +107,6 @@ message FileSearchRequest {
optional string mentions_request_id = 3; // Optional request ID for tracking requests
optional int32 limit = 4; // Optional limit for results (default: 20)
optional FileSearchType selected_type = 5; // Optional selected type filter
optional string workspace_hint = 6; // Optional workspace name to search in
}
// Result for file search operations
@@ -121,7 +120,6 @@ message FileInfo {
string path = 1; // Relative path from workspace root
string type = 2; // "file" or "folder"
optional string label = 3; // Display name (usually basename)
optional string workspace_name = 4; // Workspace this result came from
}
// Response for searchCommits
-20
View File
@@ -2,7 +2,6 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
import "google/protobuf/field_mask.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
@@ -29,8 +28,6 @@ service ModelsService {
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
// Updates API configuration
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
// Updates API configuration with partial values (only updates fields that are explicitly set)
rpc updateApiConfigurationPartial(UpdateApiConfigurationPartialRequest) returns (Empty);
// Refreshes and returns Groq models
rpc refreshGroqModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Baseten models
@@ -133,21 +130,6 @@ message UpdateApiConfigurationRequest {
ModelsApiConfiguration api_configuration = 2;
}
// Request for partially updating API configuration using FieldMask
// Only fields specified in update_mask will be updated from api_configuration
message UpdateApiConfigurationPartialRequest {
Metadata metadata = 1;
// The API configuration with values to update.
// Only fields listed in update_mask will be applied from this configuration.
ModelsApiConfiguration api_configuration = 2;
// Mask specifying which top-level fields from api_configuration to update.
// Field names should use camelCase (e.g., "apiKey", "planModeApiProvider").
// If a field is in the mask but not set in api_configuration, it will be cleared (set to undefined).
google.protobuf.FieldMask update_mask = 3;
}
// Model info for OCA (OpenAI-compatible) models exposed by the OCA provider
message OcaModelInfo {
// Maximum completion tokens per request supported by this model
@@ -343,8 +325,6 @@ message ModelsApiConfiguration {
optional string oca_base_url = 73;
optional string oca_api_key = 74;
optional string oca_refresh_token = 75;
optional string oca_mode = 76;
optional bool aws_use_global_inference = 77;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
+6 -197
View File
@@ -2,7 +2,6 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
import "cline/models.proto";
import "cline/browser.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
@@ -18,198 +17,12 @@ service StateService {
rpc togglePlanActModeProto(TogglePlanActModeRequest) returns (Boolean);
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
rpc updateSettingsCli(UpdateSettingsRequestCli) returns (Empty);
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
rpc updateInfoBannerVersion(Int64Request) returns (Empty);
rpc updateModelBannerVersion(Int64Request) returns (Empty);
rpc getProcessInfo(EmptyRequest) returns (ProcessInfo);
}
message AutoApprovalActions {
bool read_files = 1;
bool read_files_externally = 2;
bool edit_files = 3;
bool edit_files_externally = 4;
bool execute_safe_commands = 5;
bool execute_all_commands = 6;
bool use_browser = 7;
bool use_mcp = 8;
}
// Auto approval settings for task execution
message AutoApprovalSettings {
int32 version = 1;
bool enabled = 2;
AutoApprovalActions actions = 3;
int32 max_requests = 4;
bool enable_notifications = 5;
repeated string favorites = 6;
}
message Secrets {
optional string api_key = 1;
optional string open_router_api_key = 4;
optional string aws_access_key = 5;
optional string aws_secret_key = 6;
optional string aws_session_token = 7;
optional string aws_bedrock_api_key = 8;
optional string open_ai_api_key = 9;
optional string gemini_api_key = 10;
optional string open_ai_native_api_key = 11;
optional string ollama_api_key = 12;
optional string deep_seek_api_key = 13;
optional string requesty_api_key = 14;
optional string together_api_key = 15;
optional string fireworks_api_key = 16;
optional string qwen_api_key = 17;
optional string doubao_api_key = 18;
optional string mistral_api_key = 19;
optional string lite_llm_api_key = 20;
optional string auth_nonce = 21;
optional string asksage_api_key = 22;
optional string xai_api_key = 23;
optional string moonshot_api_key = 24;
optional string zai_api_key = 25;
optional string hugging_face_api_key = 26;
optional string nebius_api_key = 27;
optional string sambanova_api_key = 28;
optional string cerebras_api_key = 29;
optional string sap_ai_core_client_id = 30;
optional string sap_ai_core_client_secret = 31;
optional string groq_api_key = 32;
optional string huawei_cloud_maas_api_key = 33;
optional string baseten_api_key = 34;
optional string vercel_ai_gateway_api_key = 35;
optional string dify_api_key = 36;
optional string oca_api_key = 37;
optional string oca_refresh_token = 38;
}
message Settings {
optional string aws_region = 1;
optional bool aws_use_cross_region_inference = 2;
optional bool aws_bedrock_use_prompt_cache = 3;
optional string aws_bedrock_endpoint = 4;
optional string aws_profile = 5;
optional string aws_authentication = 6;
optional bool aws_use_profile = 7;
optional string vertex_project_id = 8;
optional string vertex_region = 9;
optional string requesty_base_url = 10;
optional string open_ai_base_url = 11;
// map<string, string> open_ai_headers = 12;
optional string ollama_base_url = 13;
optional string ollama_api_options_ctx_num = 14;
optional string lm_studio_base_url = 15;
optional string lm_studio_max_tokens = 16;
optional string anthropic_base_url = 17;
optional string gemini_base_url = 18;
optional string azure_api_version = 19;
optional string open_router_provider_sorting = 20;
optional AutoApprovalSettings auto_approval_settings = 21;
optional BrowserSettings browser_settings = 24;
optional string lite_llm_base_url = 25;
optional bool lite_llm_use_prompt_cache = 26;
optional int32 fireworks_model_max_completion_tokens = 27;
optional int32 fireworks_model_max_tokens = 28;
optional string qwen_api_line = 29;
optional string moonshot_api_line = 30;
optional string zai_api_line = 31;
optional string telemetry_setting = 32;
optional string asksage_api_url = 33;
optional bool plan_act_separate_models_setting = 34;
optional bool enable_checkpoints_setting = 35;
optional int32 request_timeout_ms = 36;
optional int32 shell_integration_timeout = 37;
optional string default_terminal_profile = 38;
optional int32 terminal_output_line_limit = 39;
optional string sap_ai_core_token_url = 40;
optional string sap_ai_core_base_url = 41;
optional string sap_ai_resource_group = 42;
optional bool sap_ai_core_use_orchestration_mode = 43;
optional string claude_code_path = 44;
optional string qwen_code_oauth_path = 45;
optional bool strict_plan_mode_enabled = 46;
optional bool yolo_mode_toggled = 47;
optional bool use_auto_condense = 48;
optional string preferred_language = 49;
optional OpenaiReasoningEffort openai_reasoning_effort = 50;
optional PlanActMode mode = 51;
optional DictationSettings dictation_settings = 52;
optional FocusChainSettings focus_chain_settings = 53;
optional string custom_prompt = 54;
optional string dify_base_url = 55;
optional double auto_condense_threshold = 56;
optional string oca_base_url = 57;
optional ApiProvider plan_mode_api_provider = 58;
optional string plan_mode_api_model_id = 59;
optional int64 plan_mode_thinking_budget_tokens = 60;
optional string plan_mode_reasoning_effort = 61;
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 62;
optional bool plan_mode_aws_bedrock_custom_selected = 63;
optional string plan_mode_aws_bedrock_custom_model_base_id = 64;
optional string plan_mode_open_router_model_id = 65;
optional OpenRouterModelInfo plan_mode_open_router_model_info = 66;
optional string plan_mode_open_ai_model_id = 67;
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 68;
optional string plan_mode_ollama_model_id = 69;
optional string plan_mode_lm_studio_model_id = 70;
optional string plan_mode_lite_llm_model_id = 71;
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 72;
optional string plan_mode_requesty_model_id = 73;
optional OpenRouterModelInfo plan_mode_requesty_model_info = 74;
optional string plan_mode_together_model_id = 75;
optional string plan_mode_fireworks_model_id = 76;
optional string plan_mode_sap_ai_core_model_id = 77;
optional string plan_mode_sap_ai_core_deployment_id = 78;
optional string plan_mode_groq_model_id = 79;
optional OpenRouterModelInfo plan_mode_groq_model_info = 80;
optional string plan_mode_baseten_model_id = 81;
optional OpenRouterModelInfo plan_mode_baseten_model_info = 82;
optional string plan_mode_hugging_face_model_id = 83;
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 84;
optional string plan_mode_huawei_cloud_maas_model_id = 85;
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 86;
optional string plan_mode_oca_model_id = 87;
optional OcaModelInfo plan_mode_oca_model_info = 88;
optional ApiProvider act_mode_api_provider = 89;
optional string act_mode_api_model_id = 90;
optional int64 act_mode_thinking_budget_tokens = 91;
optional string act_mode_reasoning_effort = 92;
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 93;
optional bool act_mode_aws_bedrock_custom_selected = 94;
optional string act_mode_aws_bedrock_custom_model_base_id = 95;
optional string act_mode_open_router_model_id = 96;
optional OpenRouterModelInfo act_mode_open_router_model_info = 97;
optional string act_mode_open_ai_model_id = 98;
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 99;
optional string act_mode_ollama_model_id = 100;
optional string act_mode_lm_studio_model_id = 101;
optional string act_mode_lite_llm_model_id = 102;
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 103;
optional string act_mode_requesty_model_id = 104;
optional OpenRouterModelInfo act_mode_requesty_model_info = 105;
optional string act_mode_together_model_id = 106;
optional string act_mode_fireworks_model_id = 107;
optional string act_mode_sap_ai_core_model_id = 108;
optional string act_mode_sap_ai_core_deployment_id = 109;
optional string act_mode_groq_model_id = 110;
optional OpenRouterModelInfo act_mode_groq_model_info = 111;
optional string act_mode_baseten_model_id = 112;
optional OpenRouterModelInfo act_mode_baseten_model_info = 113;
optional string act_mode_hugging_face_model_id = 114;
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 115;
optional string act_mode_huawei_cloud_maas_model_id = 116;
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 117;
optional string plan_mode_vercel_ai_gateway_model_id = 118;
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 119;
optional string act_mode_vercel_ai_gateway_model_id = 120;
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 121;
optional string act_mode_oca_model_id = 122;
optional OcaModelInfo act_mode_oca_model_info = 123;
}
message DictationSettings {
bool feature_enabled = 1;
bool dictation_enabled = 2;
@@ -312,12 +125,6 @@ message BrowserSettingsUpdate {
optional string custom_args = 6;
}
message UpdateSettingsRequestCli {
Metadata metadata = 1;
optional Settings settings = 2;
optional Secrets secrets = 3;
}
// Message for updating settings
message UpdateSettingsRequest {
Metadata metadata = 1;
@@ -342,9 +149,8 @@ message UpdateSettingsRequest {
optional string default_terminal_profile = 21;
optional bool yolo_mode_toggled = 22;
optional DictationSettings dictation_settings = 23;
optional double auto_condense_threshold = 24;
optional int32 auto_condense_threshold = 24;
optional bool multi_root_enabled = 25;
optional bool hooks_enabled = 26;
}
// Complete API Configuration message
@@ -418,8 +224,6 @@ message ApiConfiguration {
optional string oca_base_url = 66;
optional string oca_api_key = 67;
optional string oca_refresh_token = 68;
optional string oca_mode = 69;
optional bool aws_use_global_inference = 70;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
@@ -493,6 +297,11 @@ message FocusChainSettings {
int32 remind_cline_interval = 2;
}
message Viewport {
int32 width = 1;
int32 height = 2;
}
message UpdateTerminalConnectionTimeoutResponse {
optional int32 timeout_ms = 1;
}
+149 -1
View File
@@ -3,10 +3,33 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
import "cline/state.proto";
import "cline/models.proto";
import "cline/browser.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
message AutoApprovalActions {
bool read_files = 1;
bool read_files_externally = 2;
bool edit_files = 3;
bool edit_files_externally = 4;
bool execute_safe_commands = 5;
bool execute_all_commands = 6;
bool use_browser = 7;
bool use_mcp = 8;
}
// Auto approval settings for task execution
message AutoApprovalSettings {
int32 version = 1;
bool enabled = 2;
AutoApprovalActions actions = 3;
int32 max_requests = 4;
bool enable_notifications = 5;
repeated string favorites = 6;
}
service TaskService {
// Cancels the currently running task
rpc cancelTask(EmptyRequest) returns (Empty);
@@ -38,13 +61,138 @@ service TaskService {
rpc deleteAllTaskHistory(EmptyRequest) returns (DeleteAllTaskHistoryCount);
}
// Task-specific settings
message TaskSettings {
string aws_region = 1;
bool aws_use_cross_region_inference = 2;
bool aws_bedrock_use_prompt_cache = 3;
string aws_bedrock_endpoint = 4;
string aws_profile = 5;
string aws_authentication = 6;
bool aws_use_profile = 7;
string vertex_project_id = 8;
string vertex_region = 9;
string requesty_base_url = 10;
string open_ai_base_url = 11;
// map<string, string> open_ai_headers = 12;
string ollama_base_url = 13;
string ollama_api_options_ctx_num = 14;
string lm_studio_base_url = 15;
string lm_studio_max_tokens = 16;
string anthropic_base_url = 17;
string gemini_base_url = 18;
string azure_api_version = 19;
string open_router_provider_sorting = 20;
AutoApprovalSettings auto_approval_settings = 21;
BrowserSettings browser_settings = 24;
string lite_llm_base_url = 25;
bool lite_llm_use_prompt_cache = 26;
int32 fireworks_model_max_completion_tokens = 27;
int32 fireworks_model_max_tokens = 28;
string qwen_api_line = 29;
string moonshot_api_line = 30;
string zai_api_line = 31;
string telemetry_setting = 32;
string asksage_api_url = 33;
bool plan_act_separate_models_setting = 34;
bool enable_checkpoints_setting = 35;
int32 request_timeout_ms = 36;
int32 shell_integration_timeout = 37;
string default_terminal_profile = 38;
int32 terminal_output_line_limit = 39;
string sap_ai_core_token_url = 40;
string sap_ai_core_base_url = 41;
string sap_ai_resource_group = 42;
bool sap_ai_core_use_orchestration_mode = 43;
string claude_code_path = 44;
string qwen_code_oauth_path = 45;
bool strict_plan_mode_enabled = 46;
bool yolo_mode_toggled = 47;
bool use_auto_condense = 48;
string preferred_language = 49;
OpenaiReasoningEffort openai_reasoning_effort = 50;
PlanActMode mode = 51;
DictationSettings dictation_settings = 52;
FocusChainSettings focus_chain_settings = 53;
string custom_prompt = 54;
string dify_base_url = 55;
double auto_condense_threshold = 56;
string oca_base_url = 57;
ApiProvider plan_mode_api_provider = 58;
string plan_mode_api_model_id = 59;
int64 plan_mode_thinking_budget_tokens = 60;
string plan_mode_reasoning_effort = 61;
LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 62;
bool plan_mode_aws_bedrock_custom_selected = 63;
string plan_mode_aws_bedrock_custom_model_base_id = 64;
string plan_mode_open_router_model_id = 65;
OpenRouterModelInfo plan_mode_open_router_model_info = 66;
string plan_mode_open_ai_model_id = 67;
OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 68;
string plan_mode_ollama_model_id = 69;
string plan_mode_lm_studio_model_id = 70;
string plan_mode_lite_llm_model_id = 71;
LiteLLMModelInfo plan_mode_lite_llm_model_info = 72;
string plan_mode_requesty_model_id = 73;
OpenRouterModelInfo plan_mode_requesty_model_info = 74;
string plan_mode_together_model_id = 75;
string plan_mode_fireworks_model_id = 76;
string plan_mode_sap_ai_core_model_id = 77;
string plan_mode_sap_ai_core_deployment_id = 78;
string plan_mode_groq_model_id = 79;
OpenRouterModelInfo plan_mode_groq_model_info = 80;
string plan_mode_baseten_model_id = 81;
OpenRouterModelInfo plan_mode_baseten_model_info = 82;
string plan_mode_hugging_face_model_id = 83;
OpenRouterModelInfo plan_mode_hugging_face_model_info = 84;
string plan_mode_huawei_cloud_maas_model_id = 85;
OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 86;
string plan_mode_oca_model_id = 87;
OcaModelInfo plan_mode_oca_model_info = 88;
ApiProvider act_mode_api_provider = 89;
string act_mode_api_model_id = 90;
int64 act_mode_thinking_budget_tokens = 91;
string act_mode_reasoning_effort = 92;
LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 93;
bool act_mode_aws_bedrock_custom_selected = 94;
string act_mode_aws_bedrock_custom_model_base_id = 95;
string act_mode_open_router_model_id = 96;
OpenRouterModelInfo act_mode_open_router_model_info = 97;
string act_mode_open_ai_model_id = 98;
OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 99;
string act_mode_ollama_model_id = 100;
string act_mode_lm_studio_model_id = 101;
string act_mode_lite_llm_model_id = 102;
LiteLLMModelInfo act_mode_lite_llm_model_info = 103;
string act_mode_requesty_model_id = 104;
OpenRouterModelInfo act_mode_requesty_model_info = 105;
string act_mode_together_model_id = 106;
string act_mode_fireworks_model_id = 107;
string act_mode_sap_ai_core_model_id = 108;
string act_mode_sap_ai_core_deployment_id = 109;
string act_mode_groq_model_id = 110;
OpenRouterModelInfo act_mode_groq_model_info = 111;
string act_mode_baseten_model_id = 112;
OpenRouterModelInfo act_mode_baseten_model_info = 113;
string act_mode_hugging_face_model_id = 114;
OpenRouterModelInfo act_mode_hugging_face_model_info = 115;
string act_mode_huawei_cloud_maas_model_id = 116;
OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 117;
string plan_mode_vercel_ai_gateway_model_id = 118;
OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 119;
string act_mode_vercel_ai_gateway_model_id = 120;
OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 121;
string act_mode_oca_model_id = 122;
OcaModelInfo act_mode_oca_model_info = 123;
}
// Request message for creating a new task
message NewTaskRequest {
Metadata metadata = 1;
string text = 2;
repeated string images = 3;
repeated string files = 4;
optional Settings task_settings = 5;
optional TaskSettings task_settings = 5;
}
// Request message for toggling task favorite status
-1
View File
@@ -63,7 +63,6 @@ enum ClineSay {
LOAD_MCP_DOCUMENTATION = 25;
INFO = 26;
TASK_PROGRESS = 27;
ERROR_RETRY = 28;
}
// Enum for ClineSayTool tool types
+3
View File
@@ -15,6 +15,9 @@ service EnvService {
// Reads text from the system clipboard.
rpc clipboardReadText(cline.EmptyRequest) returns (cline.String);
// Returns a stable machine identifier for telemetry distinctId purposes.
rpc getMachineId(cline.EmptyRequest) returns (cline.String);
// Returns the name and version of the host IDE or environment.
rpc getHostVersion(cline.EmptyRequest) returns (GetHostVersionResponse);
-374
View File
@@ -1,374 +0,0 @@
/**
* API Secrets Parser Module
*
* Parses the ApiHandlerSecrets TypeScript interface from src/shared/api.ts
* to automatically discover API key fields for all providers.
*
* This eliminates the need for manual maintenance of provider-to-API-key mappings.
*/
/**
* Parses the ApiHandlerSecrets interface from api.ts content
*
* @param {string} content - Content of api.ts file
* @returns {Object} Parsed API key fields with metadata
* @returns {Object.fields} - Map of field names to their metadata
* @returns {Object.fieldNames} - Array of all field names
*/
export function parseApiHandlerSecrets(content) {
// Find the ApiHandlerSecrets interface definition
const interfaceMatch = content.match(/export interface ApiHandlerSecrets \{([\s\S]*?)\}/m)
if (!interfaceMatch) {
throw new Error("Could not find ApiHandlerSecrets interface definition")
}
const interfaceContent = interfaceMatch[1]
const fields = {}
const fieldNames = []
// Match field definitions like: fieldName?: string // comment
const fieldMatches = interfaceContent.matchAll(/^\s*([a-zA-Z][a-zA-Z0-9_]*)\?\s*:\s*([^/\n]+)(?:\/\/\s*(.*))?$/gm)
for (const match of fieldMatches) {
const [, name, type, comment] = match
fields[name] = {
name,
type: type.trim(),
comment: comment?.trim() || "",
isSecret: true, // All fields in ApiHandlerSecrets are secrets
}
fieldNames.push(name)
}
return { fields, fieldNames }
}
/**
* Maps provider IDs to their required API key fields
*
* @param {Array<string>} providerIds - List of provider IDs from ApiProvider type
* @param {Object} apiSecretsFields - Parsed fields from ApiHandlerSecrets
* @returns {Object} Map of provider ID to array of API key field names
*
* Example output:
* {
* "anthropic": ["apiKey"],
* "bedrock": ["awsAccessKey", "awsSecretKey"],
* "cerebras": ["cerebrasApiKey"],
* ...
* }
*/
export function mapProviderToApiKeys(providerIds, apiSecretsFields) {
const providerApiKeyMap = {}
// Track which fields have been assigned to prevent duplicates
const assignedFields = new Set()
// First pass: Map provider-specific API key fields
for (const providerId of providerIds) {
const apiKeyFields = []
for (const fieldName of apiSecretsFields.fieldNames) {
if (assignedFields.has(fieldName)) {
continue
}
const providerFromField = extractProviderFromFieldName(fieldName)
if (providerFromField === providerId) {
apiKeyFields.push(fieldName)
assignedFields.add(fieldName)
}
}
if (apiKeyFields.length > 0) {
providerApiKeyMap[providerId] = apiKeyFields
}
}
// Second pass: Handle special cases and multi-key providers
applySpecialCaseMappings(providerApiKeyMap, apiSecretsFields, assignedFields)
return providerApiKeyMap
}
/**
* Determines the provider ID from an API key field name
* Uses pattern matching on common naming conventions
*
* @param {string} fieldName - API key field name (e.g., "cerebrasApiKey")
* @returns {string|null} Provider ID or null if not a provider-specific key
*/
export function extractProviderFromFieldName(fieldName) {
// Normalize field name to lowercase for matching
const lowerFieldName = fieldName.toLowerCase()
// SPECIAL CASES FIRST (before pattern matching)
// Special case: "apiKey" alone maps to "anthropic" (primary provider)
if (fieldName === "apiKey") {
return "anthropic"
}
// Special case: clineAccountId maps to "cline"
if (lowerFieldName === "clineaccountid") {
return "cline"
}
// Special case: authNonce is not provider-specific
if (lowerFieldName === "authnonce") {
return null
}
// Special case: Vertex fields (not in ApiHandlerSecrets but in ApiHandlerOptions)
if (lowerFieldName === "vertexprojectid" || lowerFieldName === "vertexregion") {
return "vertex"
}
// Pattern 1: AWS-specific fields (check before generic pattern to avoid false positives)
if (lowerFieldName.startsWith("aws")) {
// awsAccessKey, awsSecretKey, awsSessionToken, awsRegion -> bedrock
if (
lowerFieldName.includes("accesskey") ||
lowerFieldName.includes("secretkey") ||
lowerFieldName.includes("sessiontoken") ||
lowerFieldName.includes("region")
) {
return "bedrock"
}
// awsBedrockApiKey is explicitly bedrock
if (lowerFieldName.includes("bedrock")) {
return "bedrock"
}
}
// Pattern 2: Vertex-specific fields
if (lowerFieldName.startsWith("vertex")) {
return "vertex"
}
// Pattern 3: SAP AI Core fields
if (lowerFieldName.startsWith("sapaicore") || lowerFieldName.startsWith("sapai")) {
return "sapaicore"
}
// Pattern 4: Provider name in the middle (e.g., openAiNativeApiKey) - check before generic pattern
const providerPatterns = [
{ pattern: "openainative", providerId: "openai-native" },
{ pattern: "openrouter", providerId: "openrouter" },
{ pattern: "openai", providerId: "openai" },
{ pattern: "gemini", providerId: "gemini" },
{ pattern: "deepseek", providerId: "deepseek" },
{ pattern: "ollama", providerId: "ollama" },
{ pattern: "lmstudio", providerId: "lmstudio" },
{ pattern: "litellm", providerId: "litellm" },
{ pattern: "qwen", providerId: "qwen" },
{ pattern: "doubao", providerId: "doubao" },
{ pattern: "mistral", providerId: "mistral" },
{ pattern: "fireworks", providerId: "fireworks" },
{ pattern: "asksage", providerId: "asksage" },
{ pattern: "xai", providerId: "xai" },
{ pattern: "moonshot", providerId: "moonshot" },
{ pattern: "sambanova", providerId: "sambanova" },
{ pattern: "cerebras", providerId: "cerebras" },
{ pattern: "groq", providerId: "groq" },
{ pattern: "huggingface", providerId: "huggingface" },
{ pattern: "huawei", providerId: "huawei-cloud-maas" },
{ pattern: "baseten", providerId: "baseten" },
{ pattern: "vercel", providerId: "vercel-ai-gateway" },
{ pattern: "zai", providerId: "zai" },
{ pattern: "requesty", providerId: "requesty" },
{ pattern: "together", providerId: "together" },
{ pattern: "dify", providerId: "dify" },
]
for (const { pattern, providerId } of providerPatterns) {
if (lowerFieldName.includes(pattern)) {
return providerId
}
}
// Pattern 5: <provider>ApiKey format (most common) - checked LAST to avoid false positives
if (lowerFieldName.endsWith("apikey")) {
// Extract from ORIGINAL fieldName to preserve camelCase for normalization
const providerPart = fieldName.slice(0, -6) // Remove "ApiKey"
return normalizeProviderName(providerPart)
}
return null
}
/**
* Normalizes provider name extracted from field name to match provider ID format
*
* @param {string} providerPart - Provider part extracted from field name
* @returns {string} Normalized provider ID
*/
function normalizeProviderName(providerPart) {
// Handle camelCase to kebab-case conversion
const normalized = providerPart
.replace(/([A-Z])/g, "-$1")
.toLowerCase()
.replace(/^-/, "")
// Handle special cases
const specialCases = {
"open-router": "openrouter",
"open-ai-native": "openai-native",
"open-ai": "openai",
"lite-llm": "litellm",
"deep-seek": "deepseek",
"ask-sage": "asksage",
"hugging-face": "huggingface",
"huawei-cloud-maas": "huawei-cloud-maas",
"sap-ai-core": "sapaicore",
"vercel-ai-gateway": "vercel-ai-gateway",
}
return specialCases[normalized] || normalized
}
/**
* Applies special case mappings for complex provider relationships
*
* @param {Object} providerApiKeyMap - Current map being built
* @param {Object} apiSecretsFields - Parsed API secrets fields
* @param {Set<string>} assignedFields - Set of already assigned field names
*/
function applySpecialCaseMappings(providerApiKeyMap, apiSecretsFields, assignedFields) {
// Special case 1: Bedrock needs AWS fields (if not already assigned)
const awsFields = ["awsAccessKey", "awsSecretKey", "awsRegion"]
const bedrockFields = providerApiKeyMap["bedrock"] || []
for (const field of awsFields) {
if (apiSecretsFields.fieldNames.includes(field) && !bedrockFields.includes(field)) {
bedrockFields.push(field)
assignedFields.add(field)
}
}
// Optional: awsSessionToken for temporary credentials
if (apiSecretsFields.fieldNames.includes("awsSessionToken") && !bedrockFields.includes("awsSessionToken")) {
bedrockFields.push("awsSessionToken")
assignedFields.add("awsSessionToken")
}
if (bedrockFields.length > 0) {
providerApiKeyMap["bedrock"] = bedrockFields
}
// Special case 2: Vertex needs project ID and region
if (providerApiKeyMap["vertex"]) {
// Vertex typically uses application default credentials,
// but requires project ID and region configuration
// These are already captured if they exist in ApiHandlerSecrets
}
// Special case 3: SAP AI Core multi-key authentication
if (providerApiKeyMap["sapaicore"]) {
const sapFields = providerApiKeyMap["sapaicore"]
const requiredSapFields = ["sapAiCoreClientId", "sapAiCoreClientSecret"]
for (const field of requiredSapFields) {
if (apiSecretsFields.fieldNames.includes(field) && !sapFields.includes(field)) {
sapFields.push(field)
assignedFields.add(field)
}
}
}
}
/**
* Generates display name for an API key field
* Converts camelCase to Title Case with proper spacing
*
* @param {string} fieldName - API key field name
* @returns {string} Human-readable display name
*/
export function generateApiKeyDisplayName(fieldName) {
// Special cases for known abbreviations
const specialCases = {
apiKey: "API Key",
awsAccessKey: "AWS Access Key",
awsSecretKey: "AWS Secret Key",
awsSessionToken: "AWS Session Token",
awsRegion: "AWS Region",
awsBedrockApiKey: "AWS Bedrock API Key",
openRouterApiKey: "OpenRouter API Key",
openAiApiKey: "OpenAI API Key",
openAiNativeApiKey: "OpenAI Native API Key",
geminiApiKey: "Gemini API Key",
ollamaApiKey: "Ollama API Key",
deepSeekApiKey: "DeepSeek API Key",
liteLlmApiKey: "LiteLLM API Key",
qwenApiKey: "Qwen API Key",
doubaoApiKey: "Doubao API Key",
mistralApiKey: "Mistral API Key",
fireworksApiKey: "Fireworks API Key",
asksageApiKey: "AskSage API Key",
xaiApiKey: "X AI API Key",
moonshotApiKey: "Moonshot API Key",
sambanovaApiKey: "SambaNova API Key",
cerebrasApiKey: "Cerebras API Key",
groqApiKey: "Groq API Key",
huggingFaceApiKey: "Hugging Face API Key",
nebiusApiKey: "Nebius API Key",
basetenApiKey: "Baseten API Key",
vercelAiGatewayApiKey: "Vercel AI Gateway API Key",
zaiApiKey: "Z AI API Key",
requestyApiKey: "Requesty API Key",
togetherApiKey: "Together AI API Key",
difyApiKey: "Dify API Key",
clineAccountId: "Cline Account ID",
vertexProjectId: "Vertex Project ID",
vertexRegion: "Vertex Region",
sapAiCoreClientId: "SAP AI Core Client ID",
sapAiCoreClientSecret: "SAP AI Core Client Secret",
huaweiCloudMaasApiKey: "Huawei Cloud MaaS API Key",
}
if (specialCases[fieldName]) {
return specialCases[fieldName]
}
// Generic conversion: camelCase -> Title Case
return fieldName
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase())
.trim()
}
/**
* Validates that all providers have at least one API key field mapped
*
* @param {Array<string>} providerIds - All provider IDs
* @param {Object} providerApiKeyMap - Generated mapping
* @returns {Object} Validation result with warnings for unmapped providers
*/
export function validateApiKeyMappings(providerIds, providerApiKeyMap) {
const unmappedProviders = []
const warnings = []
for (const providerId of providerIds) {
if (!providerApiKeyMap[providerId] || providerApiKeyMap[providerId].length === 0) {
// Some providers don't require API keys - they use alternative authentication:
const noKeyProviders = ["vscode-lm", "ollama", "lmstudio", "claude-code", "oca", "vertex", "qwen-code"]
if (!noKeyProviders.includes(providerId)) {
unmappedProviders.push(providerId)
warnings.push(`WARNING: Provider "${providerId}" has no API key fields mapped`)
}
}
}
return {
valid: unmappedProviders.length === 0,
unmappedProviders,
warnings,
totalProviders: providerIds.length,
mappedProviders: Object.keys(providerApiKeyMap).length,
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ cp package.json dist-standalone/extension
cd cli
GO111MODULE=on go build -o bin/cline ./cmd/cline
echo 'cli/bin/cline built'
echo '🖥️ cli/bin/cline built'
GO111MODULE=on go build -o bin/cline-host ./cmd/cline-host
echo 'cli/bin/cline-host built'
echo '🖥️ cli/bin/cline-host built'
File diff suppressed because it is too large Load Diff
-306
View File
@@ -1,306 +0,0 @@
#!/usr/bin/env node
import { execSync, spawn } from "child_process"
import chokidar from "chokidar"
import path from "path"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const projectRoot = path.resolve(__dirname, "..")
// ANSI color codes
const colors = {
reset: "\x1b[0m",
bright: "\x1b[1m",
dim: "\x1b[2m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
red: "\x1b[31m",
cyan: "\x1b[36m",
}
let isBuilding = false
let debounceTimer = null
let esbuildProcess = null
let initialBuildDone = false
console.log(`${colors.bright}${colors.cyan}🚀 Cline CLI Dev Watch Mode (Fast Incremental)${colors.reset}`)
console.log(`${colors.dim}Starting initial build...${colors.reset}\n`)
// Function to kill all CLI instances
function killAllInstances() {
try {
execSync("./cli/bin/cline instance kill --all", {
cwd: projectRoot,
stdio: "pipe",
})
} catch (error) {
// Ignore errors - instances might not be running
}
}
// Function to start a new CLI instance
function startNewInstance() {
try {
console.log(`${colors.blue}▶️ Starting new CLI instance...${colors.reset}`)
const result = execSync("./cli/bin/cline instance new", {
cwd: projectRoot,
stdio: "pipe",
encoding: "utf-8",
})
console.log(`${colors.green}✓ CLI instance started${colors.reset}`)
console.log(`${colors.dim}${result.trim()}${colors.reset}\n`)
} catch (error) {
console.error(`${colors.red}✗ Failed to start instance: ${error.message}${colors.reset}\n`)
}
}
// Function to rebuild Go CLI
async function rebuildGo() {
if (isBuilding) {
return
}
isBuilding = true
const startTime = Date.now()
try {
console.log(`${colors.cyan}🔨 Rebuilding Go CLI...${colors.reset}`)
killAllInstances()
// Just rebuild Go binaries (skip proto generation)
execSync("cd cli && GO111MODULE=on go build -o bin/cline ./cmd/cline", {
cwd: projectRoot,
stdio: "inherit",
shell: true,
})
execSync("cd cli && GO111MODULE=on go build -o bin/cline-host ./cmd/cline-host", {
cwd: projectRoot,
stdio: "inherit",
shell: true,
})
startNewInstance()
const duration = ((Date.now() - startTime) / 1000).toFixed(2)
console.log(`${colors.green}✓ Go rebuild complete in ${duration}s${colors.reset}`)
console.log(`${colors.dim}Watching for changes...${colors.reset}\n`)
} catch (error) {
console.error(`${colors.red}✗ Go build failed: ${error.message}${colors.reset}\n`)
} finally {
isBuilding = false
}
}
// Function to regenerate protos and rebuild everything
async function rebuildProtos() {
if (isBuilding) {
return
}
isBuilding = true
const startTime = Date.now()
try {
console.log(`${colors.cyan}🔨 Regenerating protos...${colors.reset}`)
killAllInstances()
// Regenerate protos
execSync("npm run protos", { cwd: projectRoot, stdio: "inherit" })
execSync("npm run protos-go", { cwd: projectRoot, stdio: "inherit" })
// esbuild will auto-rebuild TS due to changed generated files
// Rebuild Go CLI
execSync("cd cli && GO111MODULE=on go build -o bin/cline ./cmd/cline", {
cwd: projectRoot,
stdio: "inherit",
shell: true,
})
execSync("cd cli && GO111MODULE=on go build -o bin/cline-host ./cmd/cline-host", {
cwd: projectRoot,
stdio: "inherit",
shell: true,
})
startNewInstance()
const duration = ((Date.now() - startTime) / 1000).toFixed(2)
console.log(`${colors.green}✓ Proto rebuild complete in ${duration}s${colors.reset}`)
console.log(`${colors.dim}Watching for changes...${colors.reset}\n`)
} catch (error) {
console.error(`${colors.red}✗ Proto build failed: ${error.message}${colors.reset}\n`)
} finally {
isBuilding = false
}
}
// Debounced rebuild trigger
function triggerGoRebuild(filepath) {
if (debounceTimer) {
clearTimeout(debounceTimer)
}
debounceTimer = setTimeout(() => {
const relativePath = path.relative(projectRoot, filepath)
console.log(`${colors.dim}Go file changed: ${relativePath}${colors.reset}`)
rebuildGo()
}, 300)
}
function triggerProtoRebuild(filepath) {
if (debounceTimer) {
clearTimeout(debounceTimer)
}
debounceTimer = setTimeout(() => {
const relativePath = path.relative(projectRoot, filepath)
console.log(`${colors.dim}Proto file changed: ${relativePath}${colors.reset}`)
rebuildProtos()
}, 300)
}
// Initial build
async function initialBuild() {
try {
// Run protos first
console.log(`${colors.blue}📦 Generating protos...${colors.reset}`)
execSync("npm run protos", { cwd: projectRoot, stdio: "inherit" })
execSync("npm run protos-go", { cwd: projectRoot, stdio: "inherit" })
// Build standalone (skip check-types and lint for speed)
console.log(`${colors.blue}📦 Building standalone...${colors.reset}`)
execSync("node esbuild.mjs --standalone", { cwd: projectRoot, stdio: "inherit" })
// Build Go CLI
console.log(`${colors.blue}🔧 Building Go CLI...${colors.reset}`)
execSync("cd cli && GO111MODULE=on go build -o bin/cline ./cmd/cline", {
cwd: projectRoot,
stdio: "inherit",
shell: true,
})
execSync("cd cli && GO111MODULE=on go build -o bin/cline-host ./cmd/cline-host", {
cwd: projectRoot,
stdio: "inherit",
shell: true,
})
// Start CLI instance
startNewInstance()
console.log(`${colors.green}${colors.bright}✓ Initial build complete!${colors.reset}`)
console.log(`${colors.cyan}Now watching for changes with fast incremental rebuilds...${colors.reset}\n`)
initialBuildDone = true
// Start esbuild in watch mode for TypeScript (incremental rebuilds)
console.log(`${colors.dim}Starting esbuild watch mode...${colors.reset}`)
esbuildProcess = spawn("node", ["esbuild.mjs", "--watch", "--standalone"], {
cwd: projectRoot,
stdio: ["inherit", "pipe", "inherit"], // Pipe stdout to parse it
})
// Parse esbuild output to detect when rebuild completes
esbuildProcess.stdout.on("data", (data) => {
const output = data.toString()
// Forward esbuild output to console
process.stdout.write(output)
// Detect when esbuild finishes a rebuild
if (output.includes("[watch] build finished") && initialBuildDone && !isBuilding) {
console.log(`${colors.cyan}📦 TypeScript rebuilt by esbuild${colors.reset}`)
killAllInstances()
startNewInstance()
}
})
esbuildProcess.on("error", (error) => {
console.error(`${colors.red}esbuild error: ${error.message}${colors.reset}`)
})
} catch (error) {
console.error(`${colors.red}✗ Initial build failed: ${error.message}${colors.reset}`)
process.exit(1)
}
}
// Watch Proto files (chokidar v4 - no glob support, watch directory and filter)
const protoWatcher = chokidar.watch("proto", {
ignored: (filepath, stats) => {
// Ignore if it's a file but not a .proto file
return stats?.isFile() && !filepath.endsWith(".proto")
},
persistent: true,
ignoreInitial: true,
cwd: projectRoot,
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 50,
},
})
protoWatcher
.on("change", (filepath) => {
if (initialBuildDone) {
console.log(`${colors.dim}[DEBUG] Proto change event: ${filepath}${colors.reset}`)
triggerProtoRebuild(path.join(projectRoot, filepath))
}
})
.on("add", (filepath) => {
if (initialBuildDone) {
console.log(`${colors.dim}[DEBUG] Proto add event: ${filepath}${colors.reset}`)
triggerProtoRebuild(path.join(projectRoot, filepath))
}
})
// Watch Go files (chokidar v4 - no glob support, watch directory and filter)
const goWatcher = chokidar.watch("cli", {
ignored: (filepath, stats) => {
// Ignore node_modules and non-.go files
if (filepath.includes("node_modules")) return true
return stats?.isFile() && !filepath.endsWith(".go")
},
persistent: true,
ignoreInitial: true,
cwd: projectRoot,
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 50,
},
})
goWatcher
.on("change", (filepath) => {
if (initialBuildDone) {
console.log(`${colors.dim}[DEBUG] Go change event: ${filepath}${colors.reset}`)
triggerGoRebuild(path.join(projectRoot, filepath))
}
})
.on("add", (filepath) => {
if (initialBuildDone) {
console.log(`${colors.dim}[DEBUG] Go add event: ${filepath}${colors.reset}`)
triggerGoRebuild(path.join(projectRoot, filepath))
}
})
// Handle shutdown gracefully
process.on("SIGINT", () => {
console.log(`\n${colors.yellow}Shutting down...${colors.reset}`)
if (esbuildProcess) {
esbuildProcess.kill()
}
killAllInstances()
process.exit(0)
})
process.on("SIGTERM", () => {
console.log(`\n${colors.yellow}Shutting down...${colors.reset}`)
if (esbuildProcess) {
esbuildProcess.kill()
}
killAllInstances()
process.exit(0)
})
// Start
initialBuild()
-187
View File
@@ -1,187 +0,0 @@
#!/usr/bin/env node
/**
* Download Node.js binaries for all target platforms
* This script downloads official Node.js binaries from nodejs.org
* and extracts them to dist-standalone/node-binaries/
*/
import fs from "fs"
import https from "https"
import path from "path"
import { pipeline } from "stream/promises"
import tar from "tar"
import { createGunzip } from "zlib"
const NODE_VERSION = "22.15.0"
const OUTPUT_DIR = "dist-standalone/node-binaries"
// Platform configurations
const PLATFORMS = [
{
name: "darwin-x64",
nodeArch: "darwin-x64",
url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-x64.tar.gz`,
},
{
name: "darwin-arm64",
nodeArch: "darwin-arm64",
url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-arm64.tar.gz`,
},
{
name: "linux-x64",
nodeArch: "linux-x64",
url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz`,
},
]
/**
* Download a file from a URL
*/
async function downloadFile(url, destPath) {
return new Promise((resolve, reject) => {
console.log(` Downloading: ${url}`)
const file = fs.createWriteStream(destPath)
https
.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
// Handle redirect
return downloadFile(response.headers.location, destPath).then(resolve).catch(reject)
}
if (response.statusCode !== 200) {
reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`))
return
}
response.pipe(file)
file.on("finish", () => {
file.close()
resolve()
})
})
.on("error", (err) => {
fs.unlink(destPath, () => {}) // Delete the file on error
reject(err)
})
file.on("error", (err) => {
fs.unlink(destPath, () => {}) // Delete the file on error
reject(err)
})
})
}
/**
* Extract a tar.gz file
*/
async function extractTarGz(tarPath, destDir) {
console.log(` Extracting to: ${destDir}`)
return pipeline(
fs.createReadStream(tarPath),
createGunzip(),
tar.extract({
cwd: destDir,
strip: 1, // Remove the top-level directory from the archive
}),
)
}
/**
* Download and extract Node.js for a specific platform
*/
async function downloadNodeForPlatform(platform) {
console.log(`\n📦 Processing ${platform.name}...`)
const platformDir = path.join(OUTPUT_DIR, platform.name)
const tarPath = path.join(OUTPUT_DIR, `node-${platform.name}.tar.gz`)
// Create output directory
fs.mkdirSync(platformDir, { recursive: true })
try {
// Download
await downloadFile(platform.url, tarPath)
console.log(` ✓ Downloaded`)
// Extract
await extractTarGz(tarPath, platformDir)
console.log(` ✓ Extracted`)
// Verify the binary exists
const binaryPath = path.join(platformDir, "bin", "node")
if (!fs.existsSync(binaryPath)) {
throw new Error(`Binary not found at ${binaryPath}`)
}
// Make binary executable
fs.chmodSync(binaryPath, 0o755)
console.log(` ✓ Binary ready: ${binaryPath}`)
// Clean up tar file
fs.unlinkSync(tarPath)
console.log(` ✓ Cleaned up`)
return true
} catch (error) {
console.error(` ✗ Failed: ${error.message}`)
throw error
}
}
/**
* Main function
*/
async function main() {
console.log("🚀 Node.js Binary Downloader")
console.log(` Version: ${NODE_VERSION}`)
console.log(` Output: ${OUTPUT_DIR}`)
// Create output directory
fs.mkdirSync(OUTPUT_DIR, { recursive: true })
// Download for all platforms
const results = []
for (const platform of PLATFORMS) {
try {
await downloadNodeForPlatform(platform)
results.push({ platform: platform.name, success: true })
} catch (error) {
results.push({ platform: platform.name, success: false, error: error.message })
}
}
// Print summary
console.log("\n" + "=".repeat(50))
console.log("📊 Summary:")
console.log("=".repeat(50))
let successCount = 0
for (const result of results) {
const status = result.success ? "✅" : "❌"
console.log(`${status} ${result.platform}`)
if (result.success) {
successCount++
} else {
console.log(` Error: ${result.error}`)
}
}
console.log("=".repeat(50))
console.log(`${successCount}/${PLATFORMS.length} platforms successful`)
if (successCount < PLATFORMS.length) {
process.exit(1)
}
console.log("\n✅ All Node.js binaries downloaded successfully!")
}
// Run the script
main().catch((error) => {
console.error("\n❌ Fatal error:", error)
process.exit(1)
})
-266
View File
@@ -1,266 +0,0 @@
#!/bin/bash
# Cline Installation Script
# Usage: curl -fsSL https://raw.githubusercontent.com/cline/cline/main/scripts/install.sh | bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
INSTALL_DIR="${CLINE_INSTALL_DIR:-$HOME/.cline/cli}"
GITHUB_REPO="cline/cline"
RELEASE_TAG="${CLINE_VERSION:-latest}"
# Detect OS and architecture
detect_platform() {
local os=$(uname -s | tr '[:upper:]' '[:lower:]')
local arch=$(uname -m)
case "$os" in
darwin)
case "$arch" in
x86_64) echo "darwin-x64" ;;
arm64) echo "darwin-arm64" ;;
*) echo "unsupported" ;;
esac
;;
linux)
case "$arch" in
x86_64) echo "linux-x64" ;;
*) echo "unsupported" ;;
esac
;;
*)
echo "unsupported"
;;
esac
}
# Print colored message
print_message() {
local color=$1
shift
echo -e "${color}$@${NC}"
}
# Print error and exit
error_exit() {
print_message "$RED" "Error: $1"
exit 1
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check prerequisites
check_prerequisites() {
print_message "$BLUE" "Checking prerequisites..."
if ! command_exists curl; then
error_exit "curl is required but not installed. Please install curl and try again."
fi
if ! command_exists tar; then
error_exit "tar is required but not installed. Please install tar and try again."
fi
print_message "$GREEN" "✓ Prerequisites satisfied"
}
# Get download URL for the release
get_download_url() {
local platform=$1
local api_url
if [ "$RELEASE_TAG" = "latest" ]; then
api_url="https://api.github.com/repos/$GITHUB_REPO/releases/latest"
else
api_url="https://api.github.com/repos/$GITHUB_REPO/releases/tags/$RELEASE_TAG"
fi
print_message "$BLUE" "Fetching release information..." >&2
local release_data=$(curl -fsSL "$api_url")
local download_url=$(echo "$release_data" | grep -o "\"browser_download_url\": \"[^\"]*${platform}[^\"]*\"" | head -1 | cut -d'"' -f4)
if [ -z "$download_url" ]; then
error_exit "Could not find download URL for platform: $platform"
fi
echo "$download_url"
}
# Download and extract Cline
install_cline() {
local platform=$1
local download_url=$2
print_message "$BLUE" "Installing Cline to $INSTALL_DIR..."
# Create temporary directory
local tmp_dir=$(mktemp -d)
trap "rm -rf $tmp_dir" EXIT
# Download package
print_message "$BLUE" "Downloading Cline..."
local package_file="$tmp_dir/cline.tar.gz"
if ! curl -fsSL -o "$package_file" "$download_url"; then
error_exit "Failed to download Cline package"
fi
# Remove existing installation
if [ -d "$INSTALL_DIR" ]; then
print_message "$YELLOW" "Removing existing installation..."
rm -rf "$INSTALL_DIR"
fi
# Create installation directory
mkdir -p "$INSTALL_DIR"
# Extract package
print_message "$BLUE" "Extracting package..."
if ! tar -xzf "$package_file" -C "$INSTALL_DIR" --strip-components=0; then
error_exit "Failed to extract package"
fi
# Make binaries executable
chmod +x "$INSTALL_DIR/bin/"*
# Copy platform-specific native modules to node_modules
if [ -d "$INSTALL_DIR/binaries/$platform/node_modules" ]; then
print_message "$BLUE" "Installing platform-specific native modules..."
if ! cp -r "$INSTALL_DIR/binaries/$platform/node_modules/"* "$INSTALL_DIR/node_modules/"; then
error_exit "Failed to install platform-specific native modules"
fi
print_message "$GREEN" "✓ Native modules installed"
fi
print_message "$GREEN" "✓ Cline installed successfully"
}
# Configure PATH
configure_path() {
local bin_dir="$INSTALL_DIR/bin"
local shell_rc=""
# Detect shell configuration file
if [ -n "$BASH_VERSION" ]; then
if [ -f "$HOME/.bashrc" ]; then
shell_rc="$HOME/.bashrc"
elif [ -f "$HOME/.bash_profile" ]; then
shell_rc="$HOME/.bash_profile"
fi
elif [ -n "$ZSH_VERSION" ]; then
shell_rc="$HOME/.zshrc"
fi
if [ -z "$shell_rc" ]; then
print_message "$YELLOW" "⚠ Could not detect shell configuration file"
print_message "$YELLOW" "Please manually add the following to your shell configuration:"
print_message "$YELLOW" " export PATH=\"$bin_dir:\$PATH\""
return
fi
# Check if PATH is already configured
if grep -q "CLINE_INSTALL_DIR" "$shell_rc" 2>/dev/null; then
print_message "$GREEN" "✓ PATH already configured in $shell_rc"
return
fi
# Add to PATH
print_message "$BLUE" "Configuring PATH in $shell_rc..."
cat >> "$shell_rc" << EOF
# Cline CLI
export PATH="$bin_dir:\$PATH"
EOF
print_message "$GREEN" "✓ PATH configured in $shell_rc"
print_message "$YELLOW" "⚠ Please restart your shell or run: source $shell_rc"
}
# Verify installation
verify_installation() {
print_message "$BLUE" "Verifying installation..."
local cline_bin="$INSTALL_DIR/bin/cline"
if [ ! -f "$cline_bin" ]; then
error_exit "Installation verification failed: cline binary not found"
fi
if [ ! -x "$cline_bin" ]; then
error_exit "Installation verification failed: cline binary not executable"
fi
# Check version (the binary now handles service management internally)
local version_output=$("$cline_bin" version 2>&1 || true)
if [ -z "$version_output" ]; then
error_exit "Installation verification failed: could not get version"
fi
print_message "$GREEN" "✓ Installation verified"
}
# Print success message
print_success() {
echo ""
print_message "$GREEN" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_message "$GREEN" " Cline installed successfully! 🎉"
print_message "$GREEN" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
print_message "$BLUE" "Installation directory: $INSTALL_DIR"
echo ""
print_message "$YELLOW" "To get started:"
print_message "$YELLOW" " 1. Restart your shell or run: source ~/.zshrc (or ~/.bashrc)"
print_message "$YELLOW" " 2. Run: cline --help"
print_message "$YELLOW" " 3. Sign in: cline auth login"
echo ""
print_message "$BLUE" "Documentation: https://docs.cline.bot"
echo ""
}
# Main installation flow
main() {
echo ""
print_message "$BLUE" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
print_message "$BLUE" " Cline Installation Script"
print_message "$BLUE" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Detect platform
local platform=$(detect_platform)
if [ "$platform" = "unsupported" ]; then
error_exit "Unsupported platform: $(uname -s) $(uname -m)"
fi
print_message "$GREEN" "✓ Detected platform: $platform"
# Check prerequisites
check_prerequisites
# Get download URL
local download_url=$(get_download_url "$platform")
print_message "$GREEN" "✓ Found release: $download_url"
# Install Cline
install_cline "$platform" "$download_url"
# Configure PATH
configure_path
# Verify installation
verify_installation
# Print success message
print_success
}
# Run main function
main "$@"
+7 -158
View File
@@ -13,8 +13,6 @@ import { rmrf } from "./file-utils.mjs"
const BUILD_DIR = "dist-standalone"
const BINARIES_DIR = `${BUILD_DIR}/binaries`
const RUNTIME_DEPS_DIR = "standalone/runtime-files"
const NODE_BINARIES_DIR = `${BUILD_DIR}/node-binaries`
const CLI_BINARIES_DIR = "cli/bin"
const IS_DEBUG_BUILD = process.env.IS_DEBUG_BUILD === "true"
// This should match the node version packaged with the JetBrains plugin.
@@ -30,56 +28,15 @@ const SUPPORTED_BINARY_MODULES = ["better-sqlite3"]
const UNIVERSAL_BUILD = !process.argv.includes("-s")
const IS_VERBOSE = process.argv.includes("-v") || process.argv.includes("--verbose")
// Parse --target flag (e.g., --target=cli)
// Default behavior is JetBrains build (no binaries)
// Use --target=cli for standalone CLI build (with binaries)
const targetArg = process.argv.find((arg) => arg.startsWith("--target="))
const BUILD_TARGET = targetArg ? targetArg.split("=")[1] : "jetbrains"
const IS_CLI_BUILD = BUILD_TARGET === "cli"
// Detect current platform
function getCurrentPlatform() {
const platform = os.platform()
const arch = os.arch()
if (platform === "darwin") {
return arch === "arm64" ? "darwin-arm64" : "darwin-x64"
} else if (platform === "linux") {
return "linux-x64"
} else if (platform === "win32") {
return "win-x64"
}
throw new Error(`Unsupported platform: ${platform}-${arch}`)
}
async function main() {
console.log(`🚀 Building Cline ${IS_CLI_BUILD ? "Standalone CLI" : "JetBrains"} Package\n`)
// Step 1: Install Node.js dependencies
await installNodeDependencies()
// Step 2: Copy Node.js binary (only for CLI builds)
// Step 3: Copy CLI binaries (only for CLI builds)
// Step 4: Create VERSION file (only for CLI builds)
if (IS_CLI_BUILD) {
await copyNodeBinary()
await copyCliBinaries()
await createVersionFile()
}
// Step 6: Package platform-specific binary modules
if (UNIVERSAL_BUILD) {
console.log("\nBuilding universal package for all platforms...")
console.log("Building universal package for all platforms...")
await packageAllBinaryDeps()
} else {
console.log(`\nBuilding package for ${os.platform()}-${os.arch()}...`)
console.log(`Building package for ${os.platform()}-${os.arch()}...`)
}
// Step 7: Create final package
console.log("\n📦 Creating final package...")
await zipDistribution()
console.log("\n✅ Build complete!")
}
async function installNodeDependencies() {
@@ -97,94 +54,6 @@ async function installNodeDependencies() {
fs.renameSync(`${BUILD_DIR}/vscode`, `${BUILD_DIR}/node_modules/vscode`)
}
/**
* Copy Node.js binary for the current platform
*/
async function copyNodeBinary() {
const currentPlatform = getCurrentPlatform()
const nodeBinarySource = path.join(NODE_BINARIES_DIR, currentPlatform, "bin", "node")
const nodeBinaryDest = path.join(BUILD_DIR, "bin", "node")
console.log(`Copying Node.js binary for ${currentPlatform}...`)
// Check if Node.js binaries exist
if (!fs.existsSync(nodeBinarySource)) {
console.error(`Error: Node.js binary not found at ${nodeBinarySource}`)
console.error(`Please run: npm run download-node`)
process.exit(1)
}
// Create bin directory
fs.mkdirSync(path.join(BUILD_DIR, "bin"), { recursive: true })
// Copy Node.js binary
await cpr(nodeBinarySource, nodeBinaryDest)
// Make it executable
fs.chmodSync(nodeBinaryDest, 0o755)
console.log(`✓ Node.js binary copied to ${nodeBinaryDest}`)
}
/**
* Copy CLI binaries (cline and cline-host)
* The Go binary is named 'cline' and includes service management
*/
async function copyCliBinaries() {
console.log("Copying CLI binaries...")
const binaries = [
{ source: "cline", dest: "cline" },
{ source: "cline-host", dest: "cline-host" },
]
const binDir = path.join(BUILD_DIR, "bin")
// Create bin directory
fs.mkdirSync(binDir, { recursive: true })
for (const { source, dest } of binaries) {
const sourcePath = path.join(CLI_BINARIES_DIR, source)
const destPath = path.join(binDir, dest)
// Check if binary exists
if (!fs.existsSync(sourcePath)) {
console.error(`Error: CLI binary not found at ${sourcePath}`)
console.error(`Please run: npm run compile-cli`)
process.exit(1)
}
// Copy binary
await cpr(sourcePath, destPath)
// Make it executable
fs.chmodSync(destPath, 0o755)
console.log(`${source} copied to ${destPath}`)
}
}
/**
* Create a VERSION file with build metadata
*/
async function createVersionFile() {
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"))
const version = packageJson.version
const platform = getCurrentPlatform()
const buildDate = new Date().toISOString()
const versionInfo = {
version,
platform,
buildDate,
nodeVersion: TARGET_NODE_VERSION,
}
const versionPath = path.join(BUILD_DIR, "VERSION.txt")
fs.writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2))
console.log(`✓ VERSION file created: ${version} (${platform})`)
}
/**
* Downloads prebuilt binaries for each platform for the modules that include binaries. It uses `npx prebuild-install`
* to download the binary.
@@ -237,10 +106,8 @@ async function packageAllBinaryDeps() {
}
async function zipDistribution() {
// Use different filename for CLI builds
// Default (JetBrains) = standalone.zip, CLI = standalone-cli.zip
const zipFilename = IS_CLI_BUILD ? "standalone-cli.zip" : "standalone.zip"
const zipPath = path.join(BUILD_DIR, zipFilename)
// Zip the build directory (excluding any pre-existing output zip).
const zipPath = path.join(BUILD_DIR, "standalone.zip")
const output = fs.createWriteStream(zipPath)
const startTime = Date.now()
const archive = archiver("zip", { zlib: { level: 6 } })
@@ -258,33 +125,15 @@ async function zipDistribution() {
})
archive.pipe(output)
// Build ignore lists for build directory and extension directory
const ignorePatterns = ["standalone.zip", "standalone-cli.zip"]
const extensionIgnores = ["dist/**"]
// For JetBrains (default) builds, exclude binaries from both directories
if (!IS_CLI_BUILD) {
// JetBrains provides their own Node.js, so exclude all binaries
ignorePatterns.push(
"bin/**", // Exclude entire bin directory
"node-binaries/**", // Exclude all platform-specific Node.js binaries
)
extensionIgnores.push(
"cli/bin/**", // Exclude CLI binaries from extension
"node-binaries/**", // Exclude node-binaries from extension
)
console.log("JetBrains build: Excluding Node.js and CLI binaries (JetBrains provides its own Node.js)")
}
// Add all the files from the standalone build dir.
archive.glob("**/*", {
cwd: BUILD_DIR,
ignore: ignorePatterns,
ignore: ["standalone.zip"],
})
// Exclude the same files as the VCE vscode extension packager.
const isIgnored = createIsIgnored(extensionIgnores)
// Also ignore the dist directory, the build directory for the extension.
const isIgnored = createIsIgnored(["dist/**"])
// Add the whole cline directory under "extension", except the for the ignored files.
archive.directory(process.cwd(), "extension", (entry) => {
-112
View File
@@ -1,112 +0,0 @@
#!/bin/bash
# Test script for install.sh
# This validates the install script without actually running it
set -e
echo "Testing install.sh script..."
echo ""
# Test 1: Script syntax
echo "Test 1: Script Syntax Verification"
if bash -n scripts/install.sh; then
echo " ✅ PASS: Script syntax is valid"
else
echo " ❌ FAIL: Script has syntax errors"
exit 1
fi
echo ""
# Test 2: Check for required functions
echo "Test 2: Required Functions Check"
required_functions=(
"detect_platform"
"print_message"
"error_exit"
"command_exists"
"check_prerequisites"
"get_download_url"
"install_cline"
"configure_path"
"verify_installation"
"print_success"
"main"
)
for func in "${required_functions[@]}"; do
if grep -q "^$func()" scripts/install.sh || grep -q "^${func} ()" scripts/install.sh; then
echo " ✅ PASS: Function '$func' exists"
else
echo " ❌ FAIL: Function '$func' not found"
exit 1
fi
done
echo ""
# Test 3: Check for required variables
echo "Test 3: Required Variables Check"
required_vars=(
"INSTALL_DIR"
"GITHUB_REPO"
"RELEASE_TAG"
)
for var in "${required_vars[@]}"; do
if grep -q "$var=" scripts/install.sh; then
echo " ✅ PASS: Variable '$var' is defined"
else
echo " ❌ FAIL: Variable '$var' not found"
exit 1
fi
done
echo ""
# Test 4: Check for platform support
echo "Test 4: Platform Support Check"
platforms=("darwin-x64" "darwin-arm64" "linux-x64")
for platform in "${platforms[@]}"; do
if grep -q "$platform" scripts/install.sh; then
echo " ✅ PASS: Platform '$platform' supported"
else
echo " ❌ FAIL: Platform '$platform' not found"
exit 1
fi
done
echo ""
# Test 5: Check for error handling
echo "Test 5: Error Handling Check"
if grep -q "error_exit" scripts/install.sh && grep -q "set -e" scripts/install.sh; then
echo " ✅ PASS: Error handling present"
else
echo " ❌ FAIL: Error handling missing"
exit 1
fi
echo ""
# Test 6: Check for PATH configuration
echo "Test 6: PATH Configuration Check"
if grep -q "export PATH=" scripts/install.sh; then
echo " ✅ PASS: PATH configuration present"
else
echo " ❌ FAIL: PATH configuration missing"
exit 1
fi
echo ""
# Test 7: Check for verification step
echo "Test 7: Installation Verification Check"
if grep -q "verify_installation" scripts/install.sh; then
echo " ✅ PASS: Installation verification present"
else
echo " ❌ FAIL: Installation verification missing"
exit 1
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "All tests passed! ✅"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "The install script is ready for use:"
echo " curl -fsSL https://raw.githubusercontent.com/cline/cline/main/scripts/install.sh | bash"
+1 -5
View File
@@ -1,6 +1,5 @@
import * as vscode from "vscode"
import {
cleanupMcpMarketplaceCatalogFromGlobalState,
migrateCustomInstructionsToGlobalRules,
migrateTaskHistoryToFile,
migrateWelcomeViewCompleted,
@@ -18,8 +17,8 @@ import { audioRecordingService } from "./services/dictation/AudioRecordingServic
import { ErrorService } from "./services/error"
import { featureFlagsService } from "./services/feature-flags"
import { initializeDistinctId } from "./services/logging/distinctId"
import { PostHogClientProvider } from "./services/posthog/PostHogClientProvider"
import { telemetryService } from "./services/telemetry"
import { PostHogClientProvider } from "./services/telemetry/providers/posthog/PostHogClientProvider"
import { ShowMessageType } from "./shared/proto/host/window"
import { getLatestAnnouncementId } from "./utils/announcements"
/**
@@ -61,9 +60,6 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
// 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)
-2
View File
@@ -102,7 +102,6 @@ function createHandlerForProvider(
awsAuthentication: options.awsAuthentication,
awsBedrockApiKey: options.awsBedrockApiKey,
awsUseCrossRegionInference: options.awsUseCrossRegionInference,
awsUseGlobalInference: options.awsUseGlobalInference,
awsBedrockUsePromptCache: options.awsBedrockUsePromptCache,
awsUseProfile: options.awsUseProfile,
awsProfile: options.awsProfile,
@@ -377,7 +376,6 @@ function createHandlerForProvider(
})
case "oca":
return new OcaHandler({
ocaMode: options.ocaMode || "internal",
ocaBaseUrl: options.ocaBaseUrl,
ocaModelId: mode === "plan" ? options.planModeOcaModelId : options.actModeOcaModelId,
ocaModelInfo: mode === "plan" ? options.planModeOcaModelInfo : options.actModeOcaModelInfo,
+85 -125
View File
@@ -213,7 +213,6 @@ describe("AwsBedrockHandler", () => {
awsBedrockApiKey: "",
awsBedrockUsePromptCache: false,
awsUseCrossRegionInference: false,
awsUseGlobalInference: false,
awsBedrockEndpoint: "",
awsBedrockCustomSelected: false,
awsBedrockCustomModelBaseId: undefined,
@@ -613,141 +612,102 @@ describe("AwsBedrockHandler", () => {
})
})
describe("getModelId", () => {
it("should return raw model ID for custom models", async () => {
const customOptions: AwsBedrockHandlerOptions = {
...mockOptions,
awsBedrockCustomSelected: true,
apiModelId:
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
}
const customHandler = new AwsBedrockHandler(customOptions)
// TODO: Re-enable or remove these tests.
// describe("getModelId", () => {
// it("should return raw model ID for custom models", async () => {
// const customOptions: ApiHandlerOptions = {
// ...mockOptions,
// actModeAwsBedrockCustomSelected: true,
// actModeApiModelId:
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
// }
// const customHandler = new AwsBedrockHandler(customOptions)
const modelId = await customHandler.getModelId()
modelId.should.equal(
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
)
})
// const modelId = await customHandler.getModelId()
// modelId.should.equal(
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
// )
// })
it("should not encode custom model IDs with slashes", async () => {
const customOptions: AwsBedrockHandlerOptions = {
...mockOptions,
awsBedrockCustomSelected: true,
apiModelId: "my-namespace/my-custom-model",
}
const customHandler = new AwsBedrockHandler(customOptions)
// it("should not encode custom model IDs with slashes", async () => {
// const customOptions: ApiHandlerOptions = {
// ...mockOptions,
// actModeAwsBedrockCustomSelected: true,
// actModeApiModelId: "my-namespace/my-custom-model",
// }
// const customHandler = new AwsBedrockHandler(customOptions)
const modelId = await customHandler.getModelId()
modelId.should.equal("my-namespace/my-custom-model")
modelId.should.not.match(/%2F/)
})
// const modelId = await customHandler.getModelId()
// modelId.should.equal("my-namespace/my-custom-model")
// modelId.should.not.match(/%2F/)
// })
it("should apply cross-region prefix for non-custom models when enabled", async () => {
const crossRegionOptions: AwsBedrockHandlerOptions = {
...mockOptions,
awsUseCrossRegionInference: true,
awsRegion: "us-west-2",
}
const crossRegionHandler = new AwsBedrockHandler(crossRegionOptions)
// it("should apply cross-region prefix for non-custom models when enabled", async () => {
// const crossRegionOptions: ApiHandlerOptions = {
// ...mockOptions,
// awsUseCrossRegionInference: true,
// awsRegion: "us-west-2",
// }
// const crossRegionHandler = new AwsBedrockHandler(crossRegionOptions)
const modelId = await crossRegionHandler.getModelId()
modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
})
// const modelId = await crossRegionHandler.getModelId()
// modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
// })
it("should apply EU cross-region prefix", async () => {
const euOptions: AwsBedrockHandlerOptions = {
...mockOptions,
awsUseCrossRegionInference: true,
awsRegion: "eu-central-1",
}
const euHandler = new AwsBedrockHandler(euOptions)
// it("should apply EU cross-region prefix", async () => {
// const euOptions: ApiHandlerOptions = {
// ...mockOptions,
// awsUseCrossRegionInference: true,
// awsRegion: "eu-central-1",
// }
// const euHandler = new AwsBedrockHandler(euOptions)
const modelId = await euHandler.getModelId()
modelId.should.equal("eu.anthropic.claude-3-7-sonnet-20250219-v1:0")
})
// const modelId = await euHandler.getModelId()
// modelId.should.equal("eu.anthropic.claude-3-7-sonnet-20250219-v1:0")
// })
it("should apply JP cross-region prefix for sonnet 4.5", async () => {
const jpOptions: AwsBedrockHandlerOptions = {
...mockOptions,
awsUseCrossRegionInference: true,
apiModelId: "anthropic.claude-sonnet-4-5-20250929-v1:0",
awsRegion: "ap-northeast-1",
}
const jpHandler = new AwsBedrockHandler(jpOptions)
// it("should apply APAC cross-region prefix", async () => {
// const apacOptions: ApiHandlerOptions = {
// ...mockOptions,
// awsUseCrossRegionInference: true,
// awsRegion: "ap-northeast-1",
// }
// const apacHandler = new AwsBedrockHandler(apacOptions)
const modelId = await jpHandler.getModelId()
modelId.should.equal("jp.anthropic.claude-sonnet-4-5-20250929-v1:0")
})
// const modelId = await apacHandler.getModelId()
// modelId.should.equal("apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
// })
it("should apply global cross-region prefix for supported models", async () => {
const globalOptions: AwsBedrockHandlerOptions = {
...mockOptions,
awsUseCrossRegionInference: true,
awsUseGlobalInference: true,
apiModelId: "anthropic.claude-sonnet-4-5-20250929-v1:0",
awsRegion: "ap-northeast-1",
}
const globalHandler = new AwsBedrockHandler(globalOptions)
// it("should not apply cross-region prefix for custom models even when enabled", async () => {
// const customCrossRegionOptions: ApiHandlerOptions = {
// ...mockOptions,
// actModeAwsBedrockCustomSelected: true,
// actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
// awsUseCrossRegionInference: true,
// }
// const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
const modelId = await globalHandler.getModelId()
modelId.should.equal("global.anthropic.claude-sonnet-4-5-20250929-v1:0")
})
// const modelId = await customCrossRegionHandler.getModelId()
// modelId.should.equal("arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model")
// })
it("should NOT apply global cross-region prefix for unsupported models", async () => {
const options: AwsBedrockHandlerOptions = {
...mockOptions,
awsUseCrossRegionInference: true,
awsUseGlobalInference: true,
apiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0", // 3.7 does not support a global inference profile
awsRegion: "us-west-2",
}
const usHandler = new AwsBedrockHandler(options)
// it("should handle UltraThink model ARN correctly", async () => {
// const ultraThinkOptions: ApiHandlerOptions = {
// ...mockOptions,
// actModeAwsBedrockCustomSelected: true,
// actModeApiModelId:
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
// actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
// }
// const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
const modelId = await usHandler.getModelId()
modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
})
it("should apply APAC cross-region prefix", async () => {
const apacOptions: AwsBedrockHandlerOptions = {
...mockOptions,
awsUseCrossRegionInference: true,
awsRegion: "ap-northeast-1",
}
const apacHandler = new AwsBedrockHandler(apacOptions)
const modelId = await apacHandler.getModelId()
modelId.should.equal("apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
})
it("should not apply cross-region prefix for custom models even when enabled", async () => {
const customCrossRegionOptions: AwsBedrockHandlerOptions = {
...mockOptions,
awsBedrockCustomSelected: true,
apiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
awsUseCrossRegionInference: true,
}
const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
const modelId = await customCrossRegionHandler.getModelId()
modelId.should.equal("arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model")
})
it("should handle UltraThink model ARN correctly", async () => {
const ultraThinkOptions: AwsBedrockHandlerOptions = {
...mockOptions,
awsBedrockCustomSelected: true,
apiModelId:
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
}
const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
const modelId = await ultraThinkHandler.getModelId()
// Should return the raw ARN without any encoding
modelId.should.equal(
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
)
modelId.should.not.match(/%2F/)
modelId.should.not.match(/%3A/)
})
})
// const modelId = await ultraThinkHandler.getModelId()
// // Should return the raw ARN without any encoding
// modelId.should.equal(
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
// )
// modelId.should.not.match(/%2F/)
// modelId.should.not.match(/%3A/)
// })
// })
})
-11
View File
@@ -25,7 +25,6 @@ export interface AwsBedrockHandlerOptions extends CommonApiHandlerOptions {
awsAuthentication?: string
awsBedrockApiKey?: string
awsUseCrossRegionInference?: boolean
awsUseGlobalInference?: boolean
awsBedrockUsePromptCache?: boolean
awsUseProfile?: boolean
awsProfile?: string
@@ -107,10 +106,6 @@ interface ProviderChainOptions {
profile?: string
}
// a special jp inference profile was created for sonnet 4.5
// https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html
const JP_SUPPORTED_CRIS_MODELS = ["anthropic.claude-sonnet-4-5-20250929-v1:0", "anthropic.claude-sonnet-4-5-20250929-v1:0:1m"]
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
export class AwsBedrockHandler implements ApiHandler {
private options: AwsBedrockHandlerOptions
@@ -276,9 +271,6 @@ export class AwsBedrockHandler implements ApiHandler {
*/
async getModelId(): Promise<string> {
if (!this.options.awsBedrockCustomSelected && this.options.awsUseCrossRegionInference) {
if (this.getModel().info.supportsGlobalEndpoint && this.options.awsUseGlobalInference) {
return `global.${this.getModel().id}`
}
const regionPrefix = this.getRegion().slice(0, 3)
switch (regionPrefix) {
case "us-":
@@ -286,9 +278,6 @@ export class AwsBedrockHandler implements ApiHandler {
case "eu-":
return `eu.${this.getModel().id}`
case "ap-":
if (JP_SUPPORTED_CRIS_MODELS.includes(this.getModel().id)) {
return `jp.${this.getModel().id}`
}
return `apac.${this.getModel().id}`
default:
// cross region inference is not supported in this region, falling back to default model
+2 -9
View File
@@ -3,11 +3,7 @@ import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults }
import OpenAI, { APIError, OpenAIError } from "openai"
import type { FinalRequestOptions, Headers as OpenAIHeaders } from "openai/core"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
import {
DEFAULT_EXTERNAL_OCA_BASE_URL,
DEFAULT_INTERNAL_OCA_BASE_URL,
OCI_HEADER_OPC_REQUEST_ID,
} from "@/services/auth/oca/utils/constants"
import { DEFAULT_OCA_BASE_URL, OCI_HEADER_OPC_REQUEST_ID } from "@/services/auth/oca/utils/constants"
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
import { Logger } from "@/services/logging/Logger"
import { ApiHandler, type CommonApiHandlerOptions } from ".."
@@ -22,7 +18,6 @@ export interface OcaHandlerOptions extends CommonApiHandlerOptions {
thinkingBudgetTokens?: number
ocaUsePromptCache?: boolean
taskId?: string
ocaMode?: string // "internal" or "external"
}
export class OcaHandler implements ApiHandler {
@@ -75,9 +70,7 @@ export class OcaHandler implements ApiHandler {
return super.makeStatusError(status, error, ociErrorMessage, headers)
}
})({
baseURL:
options.ocaBaseUrl ||
(options.ocaMode === "internal" ? DEFAULT_INTERNAL_OCA_BASE_URL : DEFAULT_EXTERNAL_OCA_BASE_URL),
baseURL: options.ocaBaseUrl || DEFAULT_OCA_BASE_URL,
apiKey: "noop",
})
}
@@ -1,7 +1,6 @@
import type { EmptyRequest } from "@shared/proto/cline/common"
import { Empty } from "@shared/proto/cline/common"
import { AuthService } from "@/services/auth/AuthService"
import { LogoutReason } from "@/services/auth/types"
import type { Controller } from "../index"
/**
@@ -12,6 +11,6 @@ import type { Controller } from "../index"
*/
export async function accountLogoutClicked(controller: Controller, _request: EmptyRequest): Promise<Empty> {
await controller.handleSignOut()
await AuthService.getInstance().handleDeauth(LogoutReason.USER_INITIATED)
await AuthService.getInstance().handleDeauth()
return Empty.create({})
}
+22 -36
View File
@@ -1,4 +1,4 @@
import { searchWorkspaceFiles, searchWorkspaceFilesMultiroot } from "@services/search/file-search"
import { searchWorkspaceFiles } from "@services/search/file-search"
import { telemetryService } from "@services/telemetry"
import { FileSearchRequest, FileSearchResults, FileSearchType } from "@shared/proto/cline/file"
import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/file/search-result-conversion"
@@ -8,10 +8,22 @@ import { Controller } from ".."
/**
* Searches for files in the workspace with fuzzy matching
* @param controller The controller instance
* @param request The request containing search query, and optionally a mentionsRequestId and workspace_hint
* @param request The request containing search query and optionally a mentionsRequestId
* @returns Results containing matching files/folders
*/
export async function searchFiles(controller: Controller, request: FileSearchRequest): Promise<FileSearchResults> {
export async function searchFiles(_controller: Controller, request: FileSearchRequest): Promise<FileSearchResults> {
const workspacePath = await getWorkspacePath()
if (!workspacePath) {
// Handle case where workspace path is not available
console.error("Error in searchFiles: No workspace path available")
// Track as a specific failure type - no workspace available
await telemetryService.captureMentionFailed("folder", "not_found", "No workspace path available")
return { results: [], mentionsRequestId: request.mentionsRequestId }
}
try {
// Map enum to string for the search service
let selectedTypeString: "file" | "folder" | undefined
@@ -21,39 +33,13 @@ export async function searchFiles(controller: Controller, request: FileSearchReq
selectedTypeString = "folder"
}
// Extract hint, ensure workspaceManager is ready, check for multiroot
const workspaceHint = request.workspaceHint
const workspaceManager = await controller.ensureWorkspaceManager()
const hasMultirootSupport = workspaceManager && workspaceManager.getRoots()?.length > 0
let searchResults: Array<{ path: string; type: "file" | "folder"; label?: string; workspaceName?: string }>
if (hasMultirootSupport) {
searchResults = await searchWorkspaceFilesMultiroot(
request.query || "",
workspaceManager,
request.limit || 20,
selectedTypeString,
workspaceHint,
)
} else {
// Legacy single workspace search
const workspacePath = await getWorkspacePath()
if (!workspacePath) {
console.error("Error in searchFiles: No workspace path available")
await telemetryService.captureMentionFailed("folder", "not_found", "No workspace path available")
return { results: [], mentionsRequestId: request.mentionsRequestId }
}
// Call file search service with query from request
searchResults = await searchWorkspaceFiles(
request.query || "",
workspacePath,
request.limit || 20, // Use default limit of 20 if not specified
selectedTypeString,
)
}
// Call file search service with query from request
const searchResults = await searchWorkspaceFiles(
request.query || "",
workspacePath,
request.limit || 20, // Use default limit of 20 if not specified
selectedTypeString,
)
// Convert search results to proto FileInfo objects using the conversion function
const protoResults = convertSearchResultsToProtoFileInfos(searchResults)
+7 -32
View File
@@ -26,7 +26,6 @@ import { HostProvider } from "@/hosts/host-provider"
import { ExtensionRegistryInfo } from "@/registry"
import { AuthService } from "@/services/auth/AuthService"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
import { LogoutReason } from "@/services/auth/types"
import { featureFlagsService } from "@/services/feature-flags"
import { getDistinctId } from "@/services/logging/distinctId"
import { telemetryService } from "@/services/telemetry"
@@ -39,7 +38,6 @@ import {
ensureMcpServersDirectoryExists,
ensureSettingsDirectoryExists,
GlobalFileNames,
writeMcpMarketplaceCatalogToCache,
} from "../storage/disk"
import { PersistenceErrorEvent, StateManager } from "../storage/StateManager"
import { Settings } from "../storage/state-keys"
@@ -67,26 +65,6 @@ export class Controller {
// NEW: Add workspace manager (optional initially)
private workspaceManager?: WorkspaceRootManager
// Public getter for workspace manager with lazy initialization - To get workspaces when task isn't initialized (Used by file mentions)
async ensureWorkspaceManager(): Promise<WorkspaceRootManager | undefined> {
if (!this.workspaceManager) {
try {
this.workspaceManager = await setupWorkspaceManager({
stateManager: this.stateManager,
detectRoots: detectWorkspaceRoots,
})
} catch (error) {
console.error("[Controller] Failed to initialize workspace manager:", error)
}
}
return this.workspaceManager
}
// Synchronous getter for workspace manager
getWorkspaceManager(): WorkspaceRootManager | undefined {
return this.workspaceManager
}
constructor(readonly context: vscode.ExtensionContext) {
PromptRegistry.getInstance() // Ensure prompts and tools are registered
HostProvider.get().logToChannel("ClineProvider instantiated")
@@ -147,7 +125,8 @@ export class Controller {
// Auth methods
async handleSignOut() {
try {
// AuthService now handles its own storage cleanup in handleDeauth()
// TODO: update to clineAccountId and then move clineApiKey to a clear function.
this.stateManager.setSecret("clineAccountId", undefined)
this.stateManager.setGlobalState("userInfo", undefined)
// Update API providers through cache service
@@ -175,7 +154,7 @@ export class Controller {
// Oca Auth methods
async handleOcaSignOut() {
try {
await this.ocaAuthService.handleDeauth(LogoutReason.USER_INITIATED)
await this.ocaAuthService.handleDeauth()
await this.postStateToWebview()
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
@@ -497,8 +476,8 @@ export class Controller {
})),
}
// Store in cache file
await writeMcpMarketplaceCatalogToCache(catalog)
// Store in global state
this.stateManager.setGlobalState("mcpMarketplaceCatalog", catalog)
return catalog
} catch (error) {
console.error("Failed to fetch MCP marketplace:", error)
@@ -535,8 +514,8 @@ export class Controller {
})),
}
// Store in cache file
await writeMcpMarketplaceCatalogToCache(catalog)
// Store in global state
this.stateManager.setGlobalState("mcpMarketplaceCatalog", catalog)
return catalog
} catch (error) {
console.error("Failed to fetch MCP marketplace:", error)
@@ -812,10 +791,6 @@ export class Controller {
user: this.stateManager.getGlobalStateKey("multiRootEnabled"),
featureFlag: featureFlagsService.getMultiRootEnabled(),
},
hooksEnabled: {
user: this.stateManager.getGlobalStateKey("hooksEnabled"),
featureFlag: featureFlagsService.getHooksEnabled(),
},
lastDismissedInfoBannerVersion,
lastDismissedModelBannerVersion,
}
@@ -3,7 +3,7 @@ import { OcaCompatibleModelInfo, OcaModelInfo } from "@shared/proto/cline/models
import axios from "axios"
import { HostProvider } from "@/hosts/host-provider"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
import { DEFAULT_EXTERNAL_OCA_BASE_URL, DEFAULT_INTERNAL_OCA_BASE_URL } from "@/services/auth/oca/utils/constants"
import { DEFAULT_OCA_BASE_URL } from "@/services/auth/oca/utils/constants"
import { createOcaHeaders, getAxiosSettings } from "@/services/auth/oca/utils/utils"
import { Logger } from "@/services/logging/Logger"
import { ShowMessageType } from "@/shared/proto/index.host"
@@ -32,8 +32,7 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
})
return OcaCompatibleModelInfo.create({ error: "Not authenticated with OCA" })
}
const ocaMode = controller.stateManager.getGlobalSettingsKey("ocaMode") || "internal"
const baseUrl = request.value || (ocaMode === "internal" ? DEFAULT_INTERNAL_OCA_BASE_URL : DEFAULT_EXTERNAL_OCA_BASE_URL)
const baseUrl = request.value || DEFAULT_OCA_BASE_URL
const modelsUrl = `${baseUrl}/v1/model/info`
const headers = await createOcaHeaders(ocaAccessToken!, "models-refresh")
try {
@@ -1,57 +0,0 @@
import { buildApiHandler } from "@core/api"
import { Empty } from "@shared/proto/cline/common"
import { UpdateApiConfigurationPartialRequest } from "@shared/proto/cline/models"
import { convertProtoToApiConfiguration } from "@shared/proto-conversions/models/api-configuration-conversion"
import type { Controller } from "../index"
/**
* Updates API configuration with partial values using FieldMask
*
* Allows clients to update individual API configuration fields without
* overwriting the entire configuration. Only fields specified in the update_mask
* are updated from api_configuration.
*
* @param controller The controller instance
* @param request The partial update API configuration request with FieldMask
* @returns Empty response
*/
export async function updateApiConfigurationPartial(
controller: Controller,
request: UpdateApiConfigurationPartialRequest,
): Promise<Empty> {
try {
// Validate request
if (!request.updateMask || request.updateMask.length === 0) {
throw new Error("update_mask is required and must contain at least one field")
}
if (!request.apiConfiguration) {
throw new Error("api_configuration is required")
}
// Get current config and convert new values from proto format
const currentConfig = controller.stateManager.getApiConfiguration()
const newConfigValues = convertProtoToApiConfiguration(request.apiConfiguration)
// Apply only the fields specified in the mask
const updatedConfig = { ...currentConfig }
for (const field of request.updateMask) {
;(updatedConfig as Record<string, any>)[field] = (newConfigValues as Record<string, any>)[field]
}
// Update storage and task API handler
controller.stateManager.setApiConfiguration(updatedConfig)
if (controller.task) {
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
controller.task.api = buildApiHandler({ ...updatedConfig, ulid: controller.task.ulid }, currentMode)
}
// Notify webview
await controller.postStateToWebview()
return Empty.create()
} catch (error) {
console.error(`Failed to update API configuration (partial): ${error}`)
throw error
}
}
@@ -291,10 +291,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
controller.stateManager.setGlobalState("multiRootEnabled", !!request.multiRootEnabled)
}
if (request.hooksEnabled !== undefined) {
controller.stateManager.setGlobalState("hooksEnabled", !!request.hooksEnabled)
}
// Post updated state to webview
await controller.postStateToWebview()
@@ -1,247 +0,0 @@
import { buildApiHandler } from "@core/api"
import { Empty } from "@shared/proto/cline/common"
import {
PlanActMode,
OpenaiReasoningEffort as ProtoOpenaiReasoningEffort,
UpdateSettingsRequestCli,
} from "@shared/proto/cline/state"
import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-configuration-conversion"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { Settings } from "@/core/storage/state-keys"
import { HostProvider } from "@/hosts/host-provider"
import { TerminalInfo } from "@/integrations/terminal/TerminalRegistry"
import { ShowMessageType } from "@/shared/proto/host/window"
import { convertProtoToAutoApprovalSettings } from "@/shared/proto-conversions/models/auto-approval-settings-conversion"
import { Mode, OpenaiReasoningEffort } from "@/shared/storage/types"
import { telemetryService } from "../../../services/telemetry"
import { Controller } from ".."
/**
* Updates multiple extension settings in a single request
* @param controller The controller instance
* @param request The request containing the settings to update
* @returns An empty response
*/
export async function updateSettingsCli(controller: Controller, request: UpdateSettingsRequestCli): Promise<Empty> {
const convertOpenaiReasoningEffort = (effort: ProtoOpenaiReasoningEffort): OpenaiReasoningEffort => {
switch (effort) {
case ProtoOpenaiReasoningEffort.LOW:
return "low"
case ProtoOpenaiReasoningEffort.MEDIUM:
return "medium"
case ProtoOpenaiReasoningEffort.HIGH:
return "high"
case ProtoOpenaiReasoningEffort.MINIMAL:
return "minimal"
default:
return "medium"
}
}
const convertPlanActMode = (mode: PlanActMode): Mode => {
return mode === PlanActMode.PLAN ? "plan" : "act"
}
try {
if (request.settings) {
// Extract all special case fields that need dedicated handlers
// These should NOT be included in the batch update
const {
// Fields requiring conversion
autoApprovalSettings,
openaiReasoningEffort,
mode,
customPrompt,
planModeApiProvider,
actModeApiProvider,
// Fields requiring special logic (telemetry, merging, etc.)
telemetrySetting,
yoloModeToggled,
useAutoCondense,
focusChainSettings,
browserSettings,
defaultTerminalProfile,
...simpleSettings
} = request.settings
// Batch update for simple pass-through fields
const filteredSettings: Partial<Settings> = Object.fromEntries(
Object.entries(simpleSettings).filter(([_, value]) => value !== undefined),
)
controller.stateManager.setGlobalStateBatch(filteredSettings)
// Handle fields requiring type conversion from generated protobuf types to application types
if (autoApprovalSettings) {
const converted = convertProtoToAutoApprovalSettings({
...autoApprovalSettings,
metadata: {},
})
controller.stateManager.setGlobalState("autoApprovalSettings", converted)
}
if (openaiReasoningEffort !== undefined) {
const converted = convertOpenaiReasoningEffort(openaiReasoningEffort)
controller.stateManager.setGlobalState("openaiReasoningEffort", converted)
}
if (mode !== undefined) {
const converted = convertPlanActMode(mode)
controller.stateManager.setGlobalState("mode", converted)
}
if (customPrompt === "compact") {
controller.stateManager.setGlobalState("customPrompt", "compact")
}
if (planModeApiProvider !== undefined) {
const converted = convertProtoToApiProvider(planModeApiProvider)
controller.stateManager.setGlobalState("planModeApiProvider", converted)
}
if (actModeApiProvider !== undefined) {
const converted = convertProtoToApiProvider(actModeApiProvider)
controller.stateManager.setGlobalState("actModeApiProvider", converted)
}
if (controller.task) {
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
const apiConfigForHandler = {
...controller.stateManager.getApiConfiguration(),
ulid: controller.task.ulid,
}
controller.task.api = buildApiHandler(apiConfigForHandler, currentMode)
}
// Update telemetry setting
if (telemetrySetting) {
await controller.updateTelemetrySetting(telemetrySetting as TelemetrySetting)
}
// Update yolo mode setting (requires telemetry)
if (yoloModeToggled !== undefined) {
if (controller.task) {
telemetryService.captureYoloModeToggle(controller.task.ulid, yoloModeToggled)
}
controller.stateManager.setGlobalState("yoloModeToggled", yoloModeToggled)
}
// Update auto-condense setting (requires telemetry)
if (useAutoCondense !== undefined) {
if (controller.task) {
telemetryService.captureAutoCondenseToggle(
controller.task.ulid,
useAutoCondense,
controller.task.api.getModel().id,
)
}
controller.stateManager.setGlobalState("useAutoCondense", useAutoCondense)
}
// Update focus chain settings (requires telemetry on state change)
if (focusChainSettings !== undefined) {
const currentSettings = controller.stateManager.getGlobalSettingsKey("focusChainSettings")
const wasEnabled = currentSettings?.enabled ?? false
const isEnabled = focusChainSettings.enabled
const newFocusChainSettings = {
enabled: isEnabled,
remindClineInterval: focusChainSettings.remindClineInterval,
}
controller.stateManager.setGlobalState("focusChainSettings", newFocusChainSettings)
// Capture telemetry when setting changes
if (wasEnabled !== isEnabled) {
telemetryService.captureFocusChainToggle(isEnabled)
}
}
// Update browser settings (requires careful merging to avoid protobuf defaults)
if (browserSettings !== undefined) {
const currentSettings = controller.stateManager.getGlobalSettingsKey("browserSettings")
const newBrowserSettings = {
...currentSettings,
viewport: {
width: browserSettings.viewport?.width || currentSettings.viewport.width,
height: browserSettings.viewport?.height || currentSettings.viewport.height,
},
...(browserSettings.remoteBrowserEnabled !== undefined && {
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
}),
...(browserSettings.remoteBrowserHost !== undefined && {
remoteBrowserHost: browserSettings.remoteBrowserHost,
}),
...(browserSettings.chromeExecutablePath !== undefined && {
chromeExecutablePath: browserSettings.chromeExecutablePath,
}),
...(browserSettings.disableToolUse !== undefined && {
disableToolUse: browserSettings.disableToolUse,
}),
...(browserSettings.customArgs !== undefined && {
customArgs: browserSettings.customArgs,
}),
}
controller.stateManager.setGlobalState("browserSettings", newBrowserSettings)
}
// Update default terminal profile (requires terminal manager updates and notifications)
if (defaultTerminalProfile !== undefined) {
const profileId = defaultTerminalProfile
// Update the terminal profile in the state
controller.stateManager.setGlobalState("defaultTerminalProfile", profileId)
let closedCount = 0
let busyTerminals: TerminalInfo[] = []
// Update the terminal manager of the current task if it exists
if (controller.task) {
// Call the updated setDefaultTerminalProfile method that returns closed terminal info
const result = controller.task.terminalManager.setDefaultTerminalProfile(profileId)
closedCount = result.closedCount
busyTerminals = result.busyTerminals
// Show information message if terminals were closed
if (closedCount > 0) {
const message = `Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
}
// Show warning if there are busy terminals that couldn't be closed
if (busyTerminals.length > 0) {
const message =
`${busyTerminals.length} busy ${busyTerminals.length === 1 ? "terminal has" : "terminals have"} a different profile. ` +
`Close ${busyTerminals.length === 1 ? "it" : "them"} to use the new profile for all commands.`
HostProvider.window.showMessage({
type: ShowMessageType.WARNING,
message,
})
}
}
}
}
// Handle secrets update
if (request.secrets) {
const filteredSecrets = Object.fromEntries(
Object.entries(request.secrets).filter(([_, value]) => value !== undefined),
)
controller.stateManager.setSecretsBatch(filteredSecrets)
}
// Post updated state to webview
await controller.postStateToWebview()
return Empty.create()
} catch (error) {
console.error("Failed to update settings:", error)
throw error
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
import { McpMarketplaceCatalog } from "@shared/mcp"
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
import { readMcpMarketplaceCatalogFromCache } from "@/core/storage/disk"
import { telemetryService } from "@/services/telemetry"
import type { Controller } from "../index"
import { sendMcpMarketplaceCatalogEvent } from "../mcp/subscribeToMcpMarketplaceCatalog"
@@ -213,10 +213,10 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
// Prefetch marketplace and OpenRouter models
// Send stored MCP marketplace catalog if available
const mcpMarketplaceCatalog = await readMcpMarketplaceCatalogFromCache()
const mcpMarketplaceCatalog = controller.stateManager.getGlobalStateKey("mcpMarketplaceCatalog")
if (mcpMarketplaceCatalog) {
sendMcpMarketplaceCatalogEvent(mcpMarketplaceCatalog)
sendMcpMarketplaceCatalogEvent(mcpMarketplaceCatalog as McpMarketplaceCatalog)
}
// Silently refresh MCP marketplace catalog
-57
View File
@@ -412,61 +412,4 @@ Content
expect(result).to.equal(expectedOutput)
})
})
describe("Multiroot workspace mentions", () => {
let workspaceManagerStub: any
beforeEach(() => {
// Create a mock multiroot workspace manager
workspaceManagerStub = {
getRoots: sandbox.stub().returns([
{ name: "frontend", path: "/test/frontend" },
{ name: "backend", path: "/test/backend" },
]),
getRootByName: sandbox.stub().callsFake((name: string) => {
const roots = [
{ name: "frontend", path: "/test/frontend" },
{ name: "backend", path: "/test/backend" },
]
return roots.find((r) => r.name === name)
}),
}
})
it("should handle workspace-prefixed file mention", async () => {
const text = "Check @frontend:/src/index.ts"
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
isBinaryFileStub.resolves(false)
extractTextStub.resolves("console.log('Frontend');")
const result = await parseMentions(text, cwd, urlContentFetcherStub, fileContextTrackerStub, workspaceManagerStub)
const expectedOutput = `Check 'frontend:src/index.ts' (see below for file content)
<file_content path="src/index.ts" workspace="frontend">
console.log('Frontend');
</file_content>`
expect(result).to.equal(expectedOutput)
expect(fileContextTrackerStub.trackFileContext.calledWith("src/index.ts", "file_mentioned")).to.be.true
})
it("should handle file in multiple workspaces without hint", async () => {
const text = "Check @/config.json"
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
isBinaryFileStub.resolves(false)
extractTextStub.withArgs(path.resolve("/test/frontend", "config.json")).resolves('{"env": "dev"}')
extractTextStub.withArgs(path.resolve("/test/backend", "config.json")).resolves('{"env": "prod"}')
const result = await parseMentions(text, cwd, urlContentFetcherStub, fileContextTrackerStub, workspaceManagerStub)
// Should include both files with workspace annotations
expect(result).to.include('workspace="frontend"')
expect(result).to.include('workspace="backend"')
expect(result).to.include('{"env": "dev"}')
expect(result).to.include('{"env": "prod"}')
})
})
})
+22 -170
View File
@@ -16,7 +16,6 @@ import { DiagnosticSeverity } from "@/shared/proto/index.cline"
import { isDirectory } from "@/utils/fs"
import { getCwd } from "@/utils/path"
import { FileContextTracker } from "../context/context-tracking/FileContextTracker"
import type { WorkspaceRoot, WorkspaceRootManager } from "../workspace"
export async function openMention(mention?: string): Promise<void> {
if (!mention) {
@@ -59,7 +58,6 @@ export async function parseMentions(
cwd: string,
urlContentFetcher: UrlContentFetcher,
fileContextTracker?: FileContextTracker,
workspaceManager?: WorkspaceRootManager,
): Promise<string> {
const mentions: Set<string> = new Set()
let parsedText = text.replace(mentionRegexGlobal, (match, mention) => {
@@ -68,13 +66,6 @@ export async function parseMentions(
return `'${mention}' (see below for site content)`
} else if (isFileMention(mention)) {
const mentionPath = getFilePathFromMention(mention)
const workspaceHint = getWorkspaceHintFromMention(mention)
// For workspace-prefixed mentions, include the workspace name in the same format the model uses for tool calls
if (workspaceHint) {
return mentionPath.endsWith("/")
? `'${workspaceHint}:${mentionPath}' (see below for folder content)`
: `'${workspaceHint}:${mentionPath}' (see below for file content)`
}
return mentionPath.endsWith("/")
? `'${mentionPath}' (see below for folder content)`
: `'${mentionPath}' (see below for file content)`
@@ -142,130 +133,34 @@ export async function parseMentions(
} else if (isFileMention(mention)) {
const mentionPath = getFilePathFromMention(mention)
const mentionType = mention.endsWith("/") ? "folder" : "file"
const workspaceHint = getWorkspaceHintFromMention(mention)
const isMultiRoot = workspaceManager && workspaceManager.getRoots().length > 1
if (isMultiRoot && !workspaceHint) {
// Parallel search across all workspaces
const workspaceRoots = workspaceManager.getRoots()
const searchPromises = workspaceRoots.map(async (root: WorkspaceRoot) => {
try {
const content = await getFileOrFolderContent(mentionPath, root.path)
return {
workspaceName: root.name || path.basename(root.path),
content,
success: true,
}
} catch (error) {
return {
workspaceName: root.name || path.basename(root.path),
content: null,
success: false,
error: error.message,
}
}
})
const results = await Promise.all(searchPromises)
const successfulResults = results.filter((r) => r.success && r.content)
if (successfulResults.length === 0) {
const errorMsg = `File not found in any workspace. Searched: ${results.map((r) => r.workspaceName).join(", ")}`
if (mention.endsWith("/")) {
parsedText += `\n\n<folder_content path="${mentionPath}">\nError fetching content: ${errorMsg}\n</folder_content>`
} else {
parsedText += `\n\n<file_content path="${mentionPath}">\nError fetching content: ${errorMsg}\n</file_content>`
}
telemetryService.captureMentionFailed(mentionType, "not_found", errorMsg)
} else if (successfulResults.length === 1) {
// Found in exactly one workspace
const result = successfulResults[0]
if (mention.endsWith("/")) {
parsedText += `\n\n<folder_content path="${mentionPath}" workspace="${result.workspaceName}">\n${result.content}\n</folder_content>`
} else {
parsedText += `\n\n<file_content path="${mentionPath}" workspace="${result.workspaceName}">\n${result.content}\n</file_content>`
if (fileContextTracker) {
await fileContextTracker.trackFileContext(mentionPath, "file_mentioned")
}
}
telemetryService.captureMentionUsed(mentionType, result.content!.length)
try {
const content = await getFileOrFolderContent(mentionPath, cwd)
if (mention.endsWith("/")) {
parsedText += `\n\n<folder_content path="${mentionPath}">\n${content}\n</folder_content>`
} else {
// Found in multiple workspaces - include all candidates with workspace name
for (const result of successfulResults) {
if (mention.endsWith("/")) {
parsedText += `\n\n<folder_content path="${mentionPath}" workspace="${result.workspaceName}">\n${result.content}\n</folder_content>`
} else {
parsedText += `\n\n<file_content path="${mentionPath}" workspace="${result.workspaceName}">\n${result.content}\n</file_content>`
}
parsedText += `\n\n<file_content path="${mentionPath}">\n${content}\n</file_content>`
// Track that this file was mentioned and its content was included
if (fileContextTracker) {
await fileContextTracker.trackFileContext(mentionPath, "file_mentioned")
}
const totalLength = successfulResults.reduce((sum, r) => sum + (r.content?.length || 0), 0)
telemetryService.captureMentionUsed(mentionType, totalLength)
}
} else if (isMultiRoot && workspaceHint) {
// Search only in specified workspace
const targetRoot = workspaceManager.getRootByName(workspaceHint)
if (!targetRoot) {
const errorMsg = `Workspace '${workspaceHint}' not found`
if (mention.endsWith("/")) {
parsedText += `\n\n<folder_content path="${mentionPath}" workspace="${workspaceHint}">\nError fetching content: ${errorMsg}\n</folder_content>`
} else {
parsedText += `\n\n<file_content path="${mentionPath}" workspace="${workspaceHint}">\nError fetching content: ${errorMsg}\n</file_content>`
}
telemetryService.captureMentionFailed(mentionType, "not_found", errorMsg)
// Track successful file/folder mention
telemetryService.captureMentionUsed(mentionType, content.length)
} catch (error) {
if (mention.endsWith("/")) {
parsedText += `\n\n<folder_content path="${mentionPath}">\nError fetching content: ${error.message}\n</folder_content>`
} else {
try {
const content = await getFileOrFolderContent(mentionPath, targetRoot.path)
if (mention.endsWith("/")) {
parsedText += `\n\n<folder_content path="${mentionPath}" workspace="${workspaceHint}">\n${content}\n</folder_content>`
} else {
parsedText += `\n\n<file_content path="${mentionPath}" workspace="${workspaceHint}">\n${content}\n</file_content>`
if (fileContextTracker) {
await fileContextTracker.trackFileContext(mentionPath, "file_mentioned")
}
}
telemetryService.captureMentionUsed(mentionType, content.length)
} catch (error) {
if (mention.endsWith("/")) {
parsedText += `\n\n<folder_content path="${mentionPath}" workspace="${workspaceHint}">\nError fetching content: ${error.message}\n</folder_content>`
} else {
parsedText += `\n\n<file_content path="${mentionPath}" workspace="${workspaceHint}">\nError fetching content: ${error.message}\n</file_content>`
}
let errorType: "not_found" | "permission_denied" | "unknown" = "unknown"
if (error.message.includes("ENOENT") || error.message.includes("Failed to access")) {
errorType = "not_found"
} else if (error.message.includes("EACCES") || error.message.includes("permission")) {
errorType = "permission_denied"
}
telemetryService.captureMentionFailed(mentionType, errorType, error.message)
}
parsedText += `\n\n<file_content path="${mentionPath}">\nError fetching content: ${error.message}\n</file_content>`
}
} else {
// Legacy single workspace mode
try {
const content = await getFileOrFolderContent(mentionPath, cwd)
if (mention.endsWith("/")) {
parsedText += `\n\n<folder_content path="${mentionPath}">\n${content}\n</folder_content>`
} else {
parsedText += `\n\n<file_content path="${mentionPath}">\n${content}\n</file_content>`
if (fileContextTracker) {
await fileContextTracker.trackFileContext(mentionPath, "file_mentioned")
}
}
telemetryService.captureMentionUsed(mentionType, content.length)
} catch (error) {
if (mention.endsWith("/")) {
parsedText += `\n\n<folder_content path="${mentionPath}">\nError fetching content: ${error.message}\n</folder_content>`
} else {
parsedText += `\n\n<file_content path="${mentionPath}">\nError fetching content: ${error.message}\n</file_content>`
}
let errorType: "not_found" | "permission_denied" | "unknown" = "unknown"
if (error.message.includes("ENOENT") || error.message.includes("Failed to access")) {
errorType = "not_found"
} else if (error.message.includes("EACCES") || error.message.includes("permission")) {
errorType = "permission_denied"
}
telemetryService.captureMentionFailed(mentionType, errorType, error.message)
// Track failed file/folder mention
// Map file access errors to appropriate error types
let errorType: "not_found" | "permission_denied" | "unknown" = "unknown"
if (error.message.includes("ENOENT") || error.message.includes("Failed to access")) {
errorType = "not_found"
} else if (error.message.includes("EACCES") || error.message.includes("permission")) {
errorType = "permission_denied"
}
telemetryService.captureMentionFailed(mentionType, errorType, error.message)
}
} else if (mention === "problems") {
try {
@@ -392,57 +287,14 @@ async function getWorkspaceProblems(): Promise<string> {
])
}
/**
* Parse a workspace mention to extract workspace hint and path
* @param mention The raw mention string (e.g., "workspace:name/path/to/file")
* @returns Object with workspaceHint and path, or null if not a workspace mention
*/
function parseWorkspaceMention(mention: string): { workspaceHint: string; path: string } | null {
// Match workspace:name/path or workspace:"name/path with spaces"
const workspaceMatch = mention.match(/^([\w-]+):(.+)$/)
if (!workspaceMatch) {
return null
}
const [, workspaceHint, pathPart] = workspaceMatch
// Check if it's actually a URL (has ://)
if (mention.includes("://")) {
return null
}
// Remove quotes from path if present
const quotedPathMatch = pathPart.match(/^"(.*)"$/)
const cleanPath = quotedPathMatch ? quotedPathMatch[1] : pathPart
return { workspaceHint, path: cleanPath }
}
function isFileMention(mention: string): boolean {
// Check for workspace-prefixed mentions first
if (parseWorkspaceMention(mention)) {
return true
}
// Check for regular file mentions
return mention.startsWith("/") || mention.startsWith('"/')
}
function getFilePathFromMention(mention: string): string {
// Check for workspace-prefixed mentions first
const workspaceMention = parseWorkspaceMention(mention)
if (workspaceMention) {
// Return path without leading slash (already cleaned)
return workspaceMention.path.startsWith("/") ? workspaceMention.path.slice(1) : workspaceMention.path
}
// Remove quotes
const match = mention.match(/^"(.*)"$/)
const filePath = match ? match[1] : mention
// Remove leading slash
return filePath.slice(1)
}
function getWorkspaceHintFromMention(mention: string): string | undefined {
const workspaceMention = parseWorkspaceMention(mention)
return workspaceMention?.workspaceHint
}
-6
View File
@@ -388,7 +388,6 @@ export class StateManager {
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
awsUseGlobalInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsBedrockApiKey,
@@ -453,7 +452,6 @@ export class StateManager {
zaiApiKey,
requestTimeoutMs,
ocaBaseUrl,
ocaMode,
// Plan mode configurations
planModeApiProvider,
planModeApiModelId,
@@ -599,7 +597,6 @@ export class StateManager {
// Global state updates
awsRegion,
awsUseCrossRegionInference,
awsUseGlobalInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
@@ -635,7 +632,6 @@ export class StateManager {
difyBaseUrl,
qwenCodeOauthPath,
ocaBaseUrl,
ocaMode,
})
// Batch update secrets
@@ -942,7 +938,6 @@ export class StateManager {
awsRegion: this.taskStateCache["awsRegion"] || this.globalStateCache["awsRegion"],
awsUseCrossRegionInference:
this.taskStateCache["awsUseCrossRegionInference"] || this.globalStateCache["awsUseCrossRegionInference"],
awsUseGlobalInference: this.taskStateCache["awsUseGlobalInference"] || this.globalStateCache["awsUseGlobalInference"],
awsBedrockUsePromptCache:
this.taskStateCache["awsBedrockUsePromptCache"] || this.globalStateCache["awsBedrockUsePromptCache"],
awsBedrockEndpoint: this.taskStateCache["awsBedrockEndpoint"] || this.globalStateCache["awsBedrockEndpoint"],
@@ -985,7 +980,6 @@ export class StateManager {
qwenCodeOauthPath: this.taskStateCache["qwenCodeOauthPath"] || this.globalStateCache["qwenCodeOauthPath"],
difyBaseUrl: this.taskStateCache["difyBaseUrl"] || this.globalStateCache["difyBaseUrl"],
ocaBaseUrl: this.globalStateCache["ocaBaseUrl"],
ocaMode: this.globalStateCache["ocaMode"],
// Plan mode configurations
planModeApiProvider: this.taskStateCache["planModeApiProvider"] || this.globalStateCache["planModeApiProvider"],
-26
View File
@@ -8,7 +8,6 @@ import fs from "fs/promises"
import os from "os"
import * as path from "path"
import { HostProvider } from "@/hosts/host-provider"
import { McpMarketplaceCatalog } from "@/shared/mcp"
import { GlobalState, Settings } from "./state-keys"
export const GlobalFileNames = {
@@ -26,7 +25,6 @@ export const GlobalFileNames = {
cursorRulesFile: ".cursorrules",
windsurfRules: ".windsurfrules",
taskMetadata: "task_metadata.json",
mcpMarketplaceCatalog: "mcp_marketplace_catalog.json",
}
export async function getDocumentsPath(): Promise<string> {
@@ -181,30 +179,6 @@ export async function ensureCacheDirectoryExists(): Promise<string> {
return getGlobalStorageDir("cache")
}
export async function readMcpMarketplaceCatalogFromCache(): Promise<McpMarketplaceCatalog | undefined> {
try {
const mcpMarketplaceCatalogFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.mcpMarketplaceCatalog)
const fileExists = await fileExistsAtPath(mcpMarketplaceCatalogFilePath)
if (fileExists) {
const fileContents = await fs.readFile(mcpMarketplaceCatalogFilePath, "utf8")
return JSON.parse(fileContents)
}
return undefined
} catch (error) {
console.error("Failed to read MCP marketplace catalog from cache:", error)
return undefined
}
}
export async function writeMcpMarketplaceCatalogToCache(catalog: McpMarketplaceCatalog): Promise<void> {
try {
const mcpMarketplaceCatalogFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.mcpMarketplaceCatalog)
await fs.writeFile(mcpMarketplaceCatalogFilePath, JSON.stringify(catalog))
} catch (error) {
console.error("Failed to write MCP marketplace catalog to cache:", error)
}
}
async function getGlobalStorageDir(...subdirs: string[]) {
const fullPath = path.resolve(HostProvider.get().globalStorageFsPath, ...subdirs)
await fs.mkdir(fullPath, { recursive: true })
+2 -4
View File
@@ -8,6 +8,7 @@ import { ClineRulesToggles } from "@/shared/cline-rules"
import { DictationSettings } from "@/shared/DictationSettings"
import { HistoryItem } from "@/shared/HistoryItem"
import { McpDisplayMode } from "@/shared/McpDisplayMode"
import { McpMarketplaceCatalog } from "@/shared/mcp"
import { Mode, OpenaiReasoningEffort } from "@/shared/storage/types"
import { TelemetrySetting } from "@/shared/TelemetrySetting"
import { UserInfo } from "@/shared/UserInfo"
@@ -27,6 +28,7 @@ export interface GlobalState {
lastShownAnnouncementId: string | undefined
taskHistory: HistoryItem[]
userInfo: UserInfo | undefined
mcpMarketplaceCatalog: McpMarketplaceCatalog | undefined
favoritedModelIds: string[]
mcpMarketplaceEnabled: boolean
mcpResponsesCollapsed: boolean
@@ -38,7 +40,6 @@ export interface GlobalState {
workspaceRoots: WorkspaceRoot[] | undefined
primaryRootIndex: number
multiRootEnabled: boolean
hooksEnabled: boolean
lastDismissedInfoBannerVersion: number
lastDismissedModelBannerVersion: number
}
@@ -46,7 +47,6 @@ export interface GlobalState {
export interface Settings {
awsRegion: string | undefined
awsUseCrossRegionInference: boolean | undefined
awsUseGlobalInference: boolean | undefined
awsBedrockUsePromptCache: boolean | undefined
awsBedrockEndpoint: string | undefined
awsProfile: string | undefined
@@ -102,7 +102,6 @@ export interface Settings {
difyBaseUrl: string | undefined
autoCondenseThreshold: number | undefined // number from 0 to 1
ocaBaseUrl: string | undefined
ocaMode: string | undefined
// Plan mode configurations
planModeApiProvider: ApiProvider
@@ -177,7 +176,6 @@ export interface Settings {
export interface Secrets {
apiKey: string | undefined
clineAccountId: string | undefined
"cline:clineAccountId": string | undefined // Auth_Provider:AccountId
openRouterApiKey: string | undefined
awsAccessKey: string | undefined
awsSecretKey: string | undefined
-19
View File
@@ -638,22 +638,3 @@ export async function migrateWelcomeViewCompleted(context: vscode.ExtensionConte
// Continue execution - migration failure shouldn't break extension startup
}
}
export async function cleanupMcpMarketplaceCatalogFromGlobalState(context: vscode.ExtensionContext) {
try {
// Check if mcpMarketplaceCatalog exists in global state
const mcpMarketplaceCatalog = await context.globalState.get("mcpMarketplaceCatalog")
if (mcpMarketplaceCatalog !== undefined) {
console.log("Cleaning up mcpMarketplaceCatalog from global state...")
// Delete it from global state
await context.globalState.update("mcpMarketplaceCatalog", undefined)
console.log("Successfully removed mcpMarketplaceCatalog from global state")
}
} catch (error) {
console.error("Failed to cleanup mcpMarketplaceCatalog from global state:", error)
// Continue execution - cleanup failure shouldn't break extension startup
}
}
+5 -12
View File
@@ -14,7 +14,6 @@ export async function readSecretsFromDisk(context: ExtensionContext): Promise<Se
const [
apiKey,
openRouterApiKey,
firebaseClineAccountId,
clineAccountId,
awsAccessKey,
awsSecretKey,
@@ -54,7 +53,6 @@ export async function readSecretsFromDisk(context: ExtensionContext): Promise<Se
context.secrets.get("apiKey") as Promise<Secrets["apiKey"]>,
context.secrets.get("openRouterApiKey") as Promise<Secrets["openRouterApiKey"]>,
context.secrets.get("clineAccountId") as Promise<Secrets["clineAccountId"]>,
context.secrets.get("cline:clineAccountId") as Promise<Secrets["cline:clineAccountId"]>,
context.secrets.get("awsAccessKey") as Promise<Secrets["awsAccessKey"]>,
context.secrets.get("awsSecretKey") as Promise<Secrets["awsSecretKey"]>,
context.secrets.get("awsSessionToken") as Promise<Secrets["awsSessionToken"]>,
@@ -95,8 +93,7 @@ export async function readSecretsFromDisk(context: ExtensionContext): Promise<Se
authNonce,
apiKey,
openRouterApiKey,
clineAccountId: firebaseClineAccountId,
"cline:clineAccountId": clineAccountId,
clineAccountId,
huggingFaceApiKey,
huaweiCloudMaasApiKey,
basetenApiKey,
@@ -160,8 +157,6 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
const awsRegion = context.globalState.get<GlobalStateAndSettings["awsRegion"]>("awsRegion")
const awsUseCrossRegionInference =
context.globalState.get<GlobalStateAndSettings["awsUseCrossRegionInference"]>("awsUseCrossRegionInference")
const awsUseGlobalInference =
context.globalState.get<GlobalStateAndSettings["awsUseGlobalInference"]>("awsUseGlobalInference")
const awsBedrockUsePromptCache =
context.globalState.get<GlobalStateAndSettings["awsBedrockUsePromptCache"]>("awsBedrockUsePromptCache")
const awsBedrockEndpoint = context.globalState.get<GlobalStateAndSettings["awsBedrockEndpoint"]>("awsBedrockEndpoint")
@@ -231,7 +226,6 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
const claudeCodePath = context.globalState.get<GlobalStateAndSettings["claudeCodePath"]>("claudeCodePath")
const difyBaseUrl = context.globalState.get<GlobalStateAndSettings["difyBaseUrl"]>("difyBaseUrl")
const ocaBaseUrl = context.globalState.get("ocaBaseUrl") as string | undefined
const ocaMode = context.globalState.get("ocaMode") as string | undefined
const openaiReasoningEffort =
context.globalState.get<GlobalStateAndSettings["openaiReasoningEffort"]>("openaiReasoningEffort")
const preferredLanguage = context.globalState.get<GlobalStateAndSettings["preferredLanguage"]>("preferredLanguage")
@@ -239,6 +233,9 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
const dictationSettings = context.globalState.get<GlobalStateAndSettings["dictationSettings"]>("dictationSettings") as
| DictationSettings
| undefined
const mcpMarketplaceCatalog =
context.globalState.get<GlobalStateAndSettings["mcpMarketplaceCatalog"]>("mcpMarketplaceCatalog")
const lastDismissedInfoBannerVersion =
context.globalState.get<GlobalStateAndSettings["lastDismissedInfoBannerVersion"]>("lastDismissedInfoBannerVersion")
const lastDismissedModelBannerVersion = context.globalState.get<
@@ -248,7 +245,6 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
const customPrompt = context.globalState.get<GlobalStateAndSettings["customPrompt"]>("customPrompt")
const autoCondenseThreshold =
context.globalState.get<GlobalStateAndSettings["autoCondenseThreshold"]>("autoCondenseThreshold") // number from 0 to 1
const hooksEnabled = context.globalState.get<GlobalStateAndSettings["hooksEnabled"]>("hooksEnabled")
// Get mode-related configurations
const mode = context.globalState.get<GlobalStateAndSettings["mode"]>("mode")
@@ -426,7 +422,6 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
claudeCodePath,
awsRegion,
awsUseCrossRegionInference,
awsUseGlobalInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
@@ -461,7 +456,6 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
difyBaseUrl,
sapAiCoreUseOrchestrationMode: sapAiCoreUseOrchestrationMode ?? true,
ocaBaseUrl,
ocaMode: ocaMode || "internal",
// Plan mode configurations
planModeApiProvider: planModeApiProvider || apiProvider,
planModeApiModelId,
@@ -561,11 +555,10 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
defaultTerminalProfile: defaultTerminalProfile ?? "default",
globalWorkflowToggles: globalWorkflowToggles || {},
mcpMarketplaceCatalog,
qwenCodeOauthPath,
customPrompt,
autoCondenseThreshold: autoCondenseThreshold || 0.75, // default to 0.75 if not set
// Hooks require explicit user opt-in
hooksEnabled: hooksEnabled ?? false,
lastDismissedInfoBannerVersion: lastDismissedInfoBannerVersion ?? 0,
lastDismissedModelBannerVersion: lastDismissedModelBannerVersion ?? 0,
// Multi-root workspace support
-3
View File
@@ -45,9 +45,6 @@ export class TaskState {
didAutomaticallyRetryFailedApiRequest = false
checkpointManagerErrorMessage?: string
// Retry tracking for auto-retry feature
autoRetryAttempts: number = 0
// Task Initialization
isInitialized = false
+8 -152
View File
@@ -68,7 +68,7 @@ import * as vscode from "vscode"
import type { SystemPromptContext } from "@/core/prompts/system-prompt"
import { getSystemPrompt } from "@/core/prompts/system-prompt"
import { HostProvider } from "@/hosts/host-provider"
import { ClineError, ClineErrorType, ErrorService } from "@/services/error"
import { ErrorService } from "@/services/error"
import { TerminalHangStage, TerminalUserInterventionAction, telemetryService } from "@/services/telemetry"
import { ShowMessageType } from "@/shared/proto/index.host"
import { isInTestMode } from "../../services/test/TestMode"
@@ -1467,72 +1467,7 @@ export class Task {
// this.ask will trigger postStateToWebview, so this change should be picked up.
}
// Check if this is a Cline provider insufficient credits error - don't auto-retry these
const isClineProviderInsufficientCredits = (() => {
if (providerId !== "cline") {
return false
}
try {
const parsedError = ClineError.transform(error, model.id, providerId)
return parsedError.isErrorType(ClineErrorType.Balance)
} catch {
return false
}
})()
let response: ClineAskResponse
// Skip auto-retry for Cline provider insufficient credits errors
if (!isClineProviderInsufficientCredits && this.taskState.autoRetryAttempts < 3) {
// Auto-retry enabled with max 3 attempts: automatically approve the retry
this.taskState.autoRetryAttempts++
// Calculate delay: 2s, 4s, 8s
const delay = 2000 * 2 ** (this.taskState.autoRetryAttempts - 1)
await updateApiReqMsg({
messageStateHandler: this.messageStateHandler,
lastApiReqIndex: lastApiReqStartedIndex,
inputTokens: 0,
outputTokens: 0,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: undefined,
api: this.api,
cancelReason: "streaming_failed",
streamingFailedMessage,
})
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
await this.postStateToWebview()
response = "yesButtonClicked"
await this.say(
"error_retry",
JSON.stringify({
attempt: this.taskState.autoRetryAttempts,
maxAttempts: 3,
delaySeconds: delay / 1000,
}),
)
await setTimeoutPromise(delay)
} else {
// Show error_retry with failed flag to indicate all retries exhausted (but not for insufficient credits)
if (!isClineProviderInsufficientCredits) {
await this.say(
"error_retry",
JSON.stringify({
attempt: 3,
maxAttempts: 3,
delaySeconds: 0,
failed: true, // Special flag to indicate retries exhausted
}),
)
}
const askResult = await this.ask("api_req_failed", streamingFailedMessage)
response = askResult.response
if (response === "yesButtonClicked") {
this.taskState.autoRetryAttempts = 0
}
}
const { response } = await this.ask("api_req_failed", streamingFailedMessage)
if (response !== "yesButtonClicked") {
// this will never happen since if noButtonClicked, we will clear current task, aborting this instance
@@ -1749,7 +1684,6 @@ export class Task {
userContent = feedbackUserContent
}
this.taskState.consecutiveMistakeCount = 0
this.taskState.autoRetryAttempts = 0 // need to reset this if the user chooses to manually retry after the mistake limit is reached
}
const autoApprovalSettings = this.stateManager.getGlobalSettingsKey("autoApprovalSettings")
@@ -2186,49 +2120,9 @@ export class Task {
} catch (error) {
// abandoned happens when extension is no longer waiting for the cline instance to finish aborting (error is thrown here when any function in the for loop throws due to this.abort)
if (!this.taskState.abandoned) {
this.abortTask() // if the stream failed, there's various states the task could be in (i.e. could have streamed some tools the user may have executed), so we just resort to replicating a cancel task
const clineError = ErrorService.get().toClineError(error, this.api.getModel().id)
const errorMessage = clineError.serialize()
// Auto-retry for streaming failures (always enabled)
if (this.taskState.autoRetryAttempts < 3) {
this.taskState.autoRetryAttempts++
// Calculate exponential backoff for streaming failures: 2s, 4s, 8s
const delay = 2000 * 2 ** (this.taskState.autoRetryAttempts - 1)
// API Request component is updated to show error message, we then display retry information underneath that...
await this.say(
"error_retry",
JSON.stringify({
attempt: this.taskState.autoRetryAttempts,
maxAttempts: 3,
delaySeconds: delay / 1000,
}),
)
// Wait with exponential backoff before auto-resuming
setTimeoutPromise(delay).then(async () => {
// Programmatically click the resume button on the new task instance
if (this.controller.task) {
// Pass retry state to the new task instance
this.controller.task.taskState.autoRetryAttempts = this.taskState.autoRetryAttempts
await this.controller.task.handleWebviewAskResponse("yesButtonClicked", "", [])
}
})
} else if (this.taskState.autoRetryAttempts >= 3) {
// Show error_retry with failed flag to indicate all retries exhausted
await this.say(
"error_retry",
JSON.stringify({
attempt: 3,
maxAttempts: 3,
delaySeconds: 0,
failed: true, // Special flag to indicate retries exhausted
}),
)
}
// needs to happen after the say, otherwise the say would fail
this.abortTask() // if the stream failed, there's various states the task could be in (i.e. could have streamed some tools the user may have executed), so we just resort to replicating a cancel task
await abortStream("streaming_failed", errorMessage)
await this.reinitExistingTaskFromId(this.taskId)
@@ -2347,9 +2241,6 @@ export class Task {
this.taskState.consecutiveMistakeCount++
}
// Reset auto-retry counter for each new API request
this.taskState.autoRetryAttempts = 0
const recDidEndLoop = await this.recursivelyMakeClineRequests(this.taskState.userMessageContent)
didEndLoop = recDidEndLoop
} else {
@@ -2387,45 +2278,11 @@ export class Task {
],
})
let response: ClineAskResponse
if (this.taskState.autoRetryAttempts < 3) {
// Auto-retry enabled with max 3 attempts: automatically approve the retry
this.taskState.autoRetryAttempts++
// Calculate delay: 2s, 4s, 8s
const delay = 2000 * 2 ** (this.taskState.autoRetryAttempts - 1)
response = "yesButtonClicked"
await this.say(
"error_retry",
JSON.stringify({
attempt: this.taskState.autoRetryAttempts,
maxAttempts: 3,
delaySeconds: delay / 1000,
}),
)
await setTimeoutPromise(delay)
} else {
// Max retries exhausted (>= 3 attempts), ask user
await this.say(
"error_retry",
JSON.stringify({
attempt: 3,
maxAttempts: 3,
delaySeconds: 0,
failed: true, // Special flag to indicate retries exhausted
}),
)
const askResult = await this.ask(
"api_req_failed",
"No assistant message was received. Would you like to retry the request?",
)
response = askResult.response
// Reset retry counter if user chooses to manually retry
if (response === "yesButtonClicked") {
this.taskState.autoRetryAttempts = 0
}
}
// Offer the user a chance to retry this API request
const { response } = await this.ask(
"api_req_failed",
"No assistant message was received. Would you like to retry the request?",
)
if (response === "yesButtonClicked") {
// Signal the loop to continue (i.e., do not end), so it will attempt again
@@ -2472,7 +2329,6 @@ export class Task {
this.cwd,
this.urlContentFetcher,
this.fileContextTracker,
this.workspaceManager,
)
// when parsing slash commands, we still want to allow the user to provide their desired context

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