mirror of
https://github.com/cline/cline.git
synced 2026-09-02 07:42:19 +08:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f548eda7f | |||
| 8118e11596 | |||
| f7b593df35 | |||
| 2e0358a7a1 | |||
| 0fbc10f807 | |||
| c093ca1760 | |||
| dc85299a73 | |||
| 4533ed3ea8 | |||
| 66d7664d1b | |||
| 50aeb7098f | |||
| c2e6eafb45 | |||
| 0220628c39 | |||
| 5052220195 | |||
| abf3081e56 | |||
| 125cb78a31 | |||
| 17539228ea | |||
| bdb7cc36fa | |||
| d3e1065707 | |||
| 6303951f20 | |||
| fc184e07a2 | |||
| e88f4ea412 | |||
| 133ade3f1a | |||
| 857411c035 | |||
| c59f068584 | |||
| f7561c31fa | |||
| f0a97dafc8 | |||
| d422f689a6 | |||
| 00efcc1c2b |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add claude 4.5 haiku
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
improve cline command permission validation logic. add cline command permission man page documentation
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: finalize document content during approval flow
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Expose --version in cline cli command
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Log Persistence errors to PostHog
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat: add OpenAI Codex (ChatGPT Plus/Pro) provider
|
||||
|
||||
Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
|
||||
|
||||
Models available:
|
||||
- gpt-5.2-codex (default)
|
||||
- gpt-5.1-codex-max
|
||||
- gpt-5.1-codex-mini
|
||||
- gpt-5.2
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix two bugs in DiffViewProvider file editing:
|
||||
|
||||
1. **Line boundary validation**: Add `safelyTruncateDocument()` to prevent out-of-bounds line errors on JetBrains hosts (fixes #8423, #8429). The gRPC protocol strictly validates line numbers, causing "truncateDocument INTERNAL: Wrong line" errors when `truncateDocument()` was called with a line number >= document line count.
|
||||
|
||||
2. **Content concatenation on final update**: When replacing content without a trailing newline, the old content at line N+1 was concatenated to the new content. Fixed by extending the replacement range to cover the entire document on final update.
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
docs: fix outdated Ollama model names in documentation
|
||||
|
||||
Updated recommended Ollama models to use correct identifiers:
|
||||
- Changed qwen3-coder-30b to qwen2.5-coder:32b
|
||||
- Changed devstral-small to codellama:34b-code
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix(extract-text): strip notebook outputs to reduce context size
|
||||
|
||||
Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing the amount of context sent to the LLM while preserving the essential code and markdown content.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
OpenAI GPT-5 Codex models are now using Apply Patch tool for diff edits.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix issue where tool call ids are invalid when switching between models using the chat completion format and the responses api format.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: add chat output on skill use
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding telemetry for background exec terminal
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
This pull request introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness. The feature allows users to seamlessly work with Jupyter notebooks using Cline's AI capabilities while preserving the notebook's JSON structure.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Remove deprecated zai-glm-4.6 model from Cerebras provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Wiring-up "conditionals" for Cline Rules files.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: removes retry message from UI after retry succeeds
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Support native tool calling for LM Studio and Ollama provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add claude 4.5 opus into sap provider.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix drag & drop files from SSH remote workspaces into chat
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Limite Vertex and LiteLLM options when they're remote configured
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix crash when the Context Menu has a type but no options
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Updated switch component to adhere to 3:1 contrast accessibility standard for both light and dark vscode themes
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Throttle the remote config fetch
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Improve history view filter menu
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Add git worktree management UI for running parallel Cline sessions
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove hooks feature setting and make it always enabled (except on Windows).
|
||||
@@ -187,20 +187,11 @@ 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: Build CLI binaries
|
||||
run: npm run compile-cli-all-platforms
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
run: npm run download-ripgrep
|
||||
|
||||
- name: Compile NPM package
|
||||
run: npm run compile-standalone-npm
|
||||
- name: Compile Standalone
|
||||
run: npm run compile-standalone
|
||||
|
||||
- name: Install testing platform dependencies
|
||||
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
|
||||
@@ -213,7 +204,7 @@ jobs:
|
||||
# This prevents the job from showing as failed and avoids distracting developers
|
||||
# until the integration tests are ready to be enforced.
|
||||
run: |
|
||||
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage || true
|
||||
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
+15
-2
@@ -1,10 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## [3.52.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
|
||||
- Grok models are now moving out of free tier and into paid plans.
|
||||
- Introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness.
|
||||
|
||||
### Fixed
|
||||
- Bugs in DiffViewProvider for file editing
|
||||
- Ollama's recommended models to use correct identifiers
|
||||
|
||||
|
||||
## [3.51.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Adding OpenAI gpt-5.2-codex model to the model picker
|
||||
- Adding OpenAI gpt-5.2-codex model to the model picker
|
||||
|
||||
## [3.50.0]
|
||||
|
||||
@@ -1737,4 +1750,4 @@ Add Opus 4.1 through Claude Code
|
||||
|
||||
## [0.0.6]
|
||||
|
||||
- Initial release
|
||||
- Initial release
|
||||
|
||||
+23
@@ -147,6 +147,29 @@
|
||||
"src/dev/grit/vscode-api.grit"
|
||||
]
|
||||
},
|
||||
{
|
||||
// Do not use console logging directly, use the Logger service instead.
|
||||
"plugins": [
|
||||
"src/dev/grit/console-log.grit"
|
||||
],
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/webview-ui/**",
|
||||
"!**/evals/**",
|
||||
"!**/standalone/**",
|
||||
"!**/e2e/**",
|
||||
"!**/test/**",
|
||||
"!**/__tests__/**",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.stories.ts",
|
||||
"!src/dev/**",
|
||||
"!**/*.mjs",
|
||||
"!**/*.js",
|
||||
"!**/scripts/**",
|
||||
"!**/*.tsx",
|
||||
"!**/testing-platform/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"includes": [
|
||||
"**",
|
||||
|
||||
+76
-87
@@ -10,11 +10,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/cline/cli/pkg/cli"
|
||||
"github.com/cline/cli/pkg/cli/auth"
|
||||
"github.com/cline/cli/pkg/cli/display"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/output"
|
||||
"github.com/cline/cli/pkg/cli/slash"
|
||||
"github.com/cline/cli/pkg/common"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -71,8 +72,6 @@ see the manual page: man cline`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
var instanceAddress string
|
||||
|
||||
// Validate workspace paths exist
|
||||
if err := common.ValidateDirsExist(workspaces); err != nil {
|
||||
return err
|
||||
@@ -89,13 +88,13 @@ see the manual page: man cline`,
|
||||
if global.Config.Verbose {
|
||||
fmt.Println("Starting new Cline instance...")
|
||||
}
|
||||
instance, err := global.Clients.StartNewInstance(ctx, allWorkspaces...)
|
||||
instance, err := global.Instances.StartNewInstance(ctx, allWorkspaces...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start new instance: %w", err)
|
||||
}
|
||||
instanceAddress = instance.Address
|
||||
global.Config.CoreAddress = instance.CoreAddress
|
||||
if global.Config.Verbose {
|
||||
fmt.Printf("Started instance at %s\n\n", instanceAddress)
|
||||
fmt.Printf("Started instance at %s\n\n", global.Config.CoreAddress)
|
||||
}
|
||||
|
||||
// Set up cleanup on exit
|
||||
@@ -103,38 +102,35 @@ see the manual page: man cline`,
|
||||
if global.Config.Verbose {
|
||||
fmt.Println("\nCleaning up instance...")
|
||||
}
|
||||
registry := global.Clients.GetRegistry()
|
||||
if err := global.KillInstanceByAddress(context.Background(), registry, instanceAddress); err != nil {
|
||||
registry := global.Instances.GetRegistry()
|
||||
if err := global.KillInstanceByAddress(context.Background(), registry, global.Config.CoreAddress); err != nil {
|
||||
if global.Config.Verbose {
|
||||
fmt.Printf("Warning: Failed to clean up instance: %v\n", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Check if user has credentials configured
|
||||
if !isUserReadyToUse(ctx, instanceAddress) {
|
||||
// Create renderer for welcome messages
|
||||
renderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
fmt.Printf("\n%s\n\n", renderer.Dim("Hey there! Looks like you're new here. Let's get you set up"))
|
||||
// Check if user has credentials configured
|
||||
if !isUserReadyToUse(ctx) {
|
||||
// Create renderer for welcome messages
|
||||
renderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
fmt.Printf("\n%s\n\n", renderer.Dim("Hey there! Looks like you're new here. Let's get you set up"))
|
||||
|
||||
if err := auth.HandleAuthMenuNoArgs(ctx); err != nil {
|
||||
// Check if user cancelled - exit cleanly
|
||||
if err == huh.ErrUserAborted {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("auth setup failed: %w", err)
|
||||
if err := auth.HandleAuthMenuNoArgs(ctx); err != nil {
|
||||
// Check if user cancelled - exit cleanly
|
||||
if err == huh.ErrUserAborted {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Re-check after auth wizard
|
||||
if !isUserReadyToUse(ctx, instanceAddress) {
|
||||
return fmt.Errorf("credentials still not configured - please run 'cline auth' to complete setup")
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s\n\n", renderer.Dim("✓ Setup complete, you can now use the Cline CLI"))
|
||||
return fmt.Errorf("auth setup failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
// User specified --address flag, use that
|
||||
instanceAddress = coreAddress
|
||||
|
||||
// Re-check after auth wizard
|
||||
if !isUserReadyToUse(ctx) {
|
||||
return fmt.Errorf("credentials still not configured - please run 'cline auth' to complete setup")
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s\n\n", renderer.Dim("✓ Setup complete, you can now use the Cline CLI"))
|
||||
}
|
||||
|
||||
// Get content from both args and stdin
|
||||
@@ -143,10 +139,13 @@ see the manual page: man cline`,
|
||||
return fmt.Errorf("failed to read prompt: %w", err)
|
||||
}
|
||||
|
||||
// If no prompt from args or stdin, show interactive input
|
||||
if prompt == "" {
|
||||
// If no prompt (or just a mode switch with no message), show interactive input
|
||||
// Loop to allow mode switches without a message
|
||||
bannerShown := false
|
||||
for prompt == "" {
|
||||
// Pass the mode flag and workspaces to banner so it shows correct info
|
||||
prompt, err = promptForInitialTask(ctx, instanceAddress, mode, allWorkspaces)
|
||||
prompt, err = promptForInitialTask(ctx, mode, allWorkspaces, !bannerShown)
|
||||
bannerShown = true
|
||||
if err != nil {
|
||||
// Check if user cancelled - exit cleanly without error
|
||||
if err == huh.ErrUserAborted {
|
||||
@@ -154,6 +153,23 @@ see the manual page: man cline`,
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if user entered a mode switch command
|
||||
if newMode, remaining, isModeSwitch := slash.ParseModeSwitch(prompt); isModeSwitch {
|
||||
mode = newMode
|
||||
prompt = remaining
|
||||
// If just a mode switch with no message, continue loop to re-prompt
|
||||
if prompt == "" {
|
||||
renderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
if mode == "act" {
|
||||
fmt.Printf("\n%s\n\n", renderer.Success("Switched to act mode"))
|
||||
} else {
|
||||
fmt.Printf("\n%s\n\n", renderer.Success("Switched to plan mode"))
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if prompt == "" {
|
||||
return fmt.Errorf("prompt required")
|
||||
}
|
||||
@@ -171,7 +187,7 @@ see the manual page: man cline`,
|
||||
Mode: mode,
|
||||
Settings: settings,
|
||||
Yolo: yolo,
|
||||
Address: instanceAddress,
|
||||
Address: global.Config.CoreAddress,
|
||||
Verbose: verbose,
|
||||
Workspaces: allWorkspaces,
|
||||
})
|
||||
@@ -207,51 +223,30 @@ see the manual page: man cline`,
|
||||
}
|
||||
}
|
||||
|
||||
func promptForInitialTask(ctx context.Context, instanceAddress, modeFlag string, workspaces []string) (string, error) {
|
||||
// Show session banner before the initial input
|
||||
showSessionBanner(ctx, instanceAddress, modeFlag, workspaces)
|
||||
|
||||
var prompt string
|
||||
|
||||
// Create custom theme with mode-colored cursor and title
|
||||
theme := huh.ThemeCharm()
|
||||
|
||||
// Set cursor and title color based on mode
|
||||
modeColor := lipgloss.Color("3") // Yellow for plan
|
||||
if modeFlag == "act" {
|
||||
modeColor = lipgloss.Color("39") // Blue for act
|
||||
func promptForInitialTask(ctx context.Context, modeFlag string, workspaces []string, showBanner bool) (string, error) {
|
||||
// Show session banner before the initial input (only on first prompt)
|
||||
if showBanner {
|
||||
showSessionBanner(ctx, modeFlag, workspaces)
|
||||
}
|
||||
|
||||
theme.Focused.TextInput.Cursor = theme.Focused.TextInput.Cursor.Foreground(modeColor)
|
||||
theme.Focused.Title = theme.Focused.Title.Foreground(modeColor)
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewText().
|
||||
Title("Start a new Cline task").
|
||||
Description("What would you like Cline to help you with?").
|
||||
Placeholder("e.g., Create a REST API with authentication...").
|
||||
Lines(5).
|
||||
Value(&prompt),
|
||||
),
|
||||
).WithWidth(48).WithTheme(theme)
|
||||
|
||||
err := form.Run()
|
||||
prompt, err := output.PromptForInitialTask(
|
||||
"Start a new Cline task",
|
||||
"/plan or /act to switch modes\ntab to autocomplete commands\nctrl+e to open editor\nctrl+c to exit",
|
||||
modeFlag,
|
||||
slash.NewRegistry(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
// Check if user cancelled with Control-C
|
||||
if err == huh.ErrUserAborted {
|
||||
// Return a special error that indicates clean cancellation
|
||||
// This allows deferred cleanup to run
|
||||
if err == output.ErrUserAborted {
|
||||
return "", huh.ErrUserAborted
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.TrimSpace(prompt), nil
|
||||
return prompt, nil
|
||||
}
|
||||
|
||||
// showSessionBanner displays session info before initial prompt
|
||||
func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string, workspaces []string) {
|
||||
func showSessionBanner(ctx context.Context, modeFlag string, workspaces []string) {
|
||||
bannerInfo := display.BannerInfo{
|
||||
Version: global.CliVersion,
|
||||
Mode: modeFlag, // Use the mode from command flag, not state
|
||||
@@ -265,21 +260,18 @@ func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string, wo
|
||||
bannerInfo.Workdirs = workspaces
|
||||
|
||||
// Get provider/model using auth functions (same logic as auth menu)
|
||||
manager, err := cli.NewTaskManagerForAddress(ctx, instanceAddress)
|
||||
if err == nil {
|
||||
if providerList, err := auth.GetProviderConfigurations(ctx, manager); err == nil {
|
||||
// Show provider/model for the mode we'll be using
|
||||
var providerDisplay *auth.ProviderDisplay
|
||||
if bannerInfo.Mode == "plan" && providerList.PlanProvider != nil {
|
||||
providerDisplay = providerList.PlanProvider
|
||||
} else if bannerInfo.Mode == "act" && providerList.ActProvider != nil {
|
||||
providerDisplay = providerList.ActProvider
|
||||
}
|
||||
if providerList, err := auth.GetProviderConfigurations(ctx); err == nil {
|
||||
// Show provider/model for the mode we'll be using
|
||||
var providerDisplay *auth.ProviderDisplay
|
||||
if bannerInfo.Mode == "plan" && providerList.PlanProvider != nil {
|
||||
providerDisplay = providerList.PlanProvider
|
||||
} else if bannerInfo.Mode == "act" && providerList.ActProvider != nil {
|
||||
providerDisplay = providerList.ActProvider
|
||||
}
|
||||
|
||||
if providerDisplay != nil {
|
||||
bannerInfo.Provider = auth.GetProviderIDForEnum(providerDisplay.Provider)
|
||||
bannerInfo.ModelID = providerDisplay.ModelID
|
||||
}
|
||||
if providerDisplay != nil {
|
||||
bannerInfo.Provider = auth.GetProviderIDForEnum(providerDisplay.Provider)
|
||||
bannerInfo.ModelID = providerDisplay.ModelID
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,25 +284,22 @@ func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string, wo
|
||||
// isUserReadyToUse checks if the user has completed initial setup
|
||||
// Returns true if welcomeViewCompleted flag is set OR user is authenticated
|
||||
// Matches extension logic: welcomeViewCompleted = Boolean(globalState.welcomeViewCompleted || user?.uid)
|
||||
func isUserReadyToUse(ctx context.Context, instanceAddress string) bool {
|
||||
manager, err := cli.NewTaskManagerForAddress(ctx, instanceAddress)
|
||||
func isUserReadyToUse(ctx context.Context) bool {
|
||||
grpcClient, err := global.GetClientForAddress(ctx, global.Config.CoreAddress)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get state
|
||||
state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
|
||||
state, err := grpcClient.State.GetLatestState(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse state JSON
|
||||
stateMap := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(state.StateJson), &stateMap); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check 1: welcomeViewCompleted flag
|
||||
if welcomeCompleted, ok := stateMap["welcomeViewCompleted"].(bool); ok && welcomeCompleted {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestMultiInstanceDefaultUnchanged(t *testing.T) {
|
||||
if len(out1.CoreInstances) != 1 {
|
||||
t.Fatalf("expected 1 instance, got %d", len(out1.CoreInstances))
|
||||
}
|
||||
firstAddr := out1.CoreInstances[0].Address
|
||||
firstAddr := out1.CoreInstances[0].CoreAddress
|
||||
waitForAddressHealthy(t, firstAddr, defaultTimeout)
|
||||
|
||||
// Start second instance
|
||||
@@ -56,29 +56,29 @@ func TestDefaultJsonUpdateAfterRemoval(t *testing.T) {
|
||||
|
||||
// Choose second as new default
|
||||
target := out.CoreInstances[1]
|
||||
waitForAddressHealthy(t, target.Address, defaultTimeout)
|
||||
waitForAddressHealthy(t, target.CoreAddress, defaultTimeout)
|
||||
|
||||
// Set as default
|
||||
_ = mustRunCLI(ctx, t, "instance", "use", target.Address)
|
||||
_ = mustRunCLI(ctx, t, "instance", "use", target.CoreAddress)
|
||||
|
||||
// Verify default switched
|
||||
out = listInstancesJSON(ctx, t)
|
||||
if out.DefaultInstance != target.Address {
|
||||
t.Fatalf("default_instance not updated to %s (got %s)", target.Address, out.DefaultInstance)
|
||||
if out.DefaultInstance != target.CoreAddress {
|
||||
t.Fatalf("default_instance not updated to %s (got %s)", target.CoreAddress, out.DefaultInstance)
|
||||
}
|
||||
|
||||
// Kill the default instance using runtime PID discovery
|
||||
corePID := getCorePID(t, target.Address)
|
||||
corePID := getCorePID(t, target.CoreAddress)
|
||||
if corePID <= 0 {
|
||||
t.Fatalf("could not find PID for core process at %s", target.Address)
|
||||
t.Fatalf("could not find PID for core process at %s", target.CoreAddress)
|
||||
}
|
||||
t.Logf("Killing cline-core process PID %d for instance %s", corePID, target.Address)
|
||||
t.Logf("Killing cline-core process PID %d for instance %s", corePID, target.CoreAddress)
|
||||
if err := syscall.Kill(corePID, syscall.SIGKILL); err != nil {
|
||||
t.Fatalf("kill pid %d: %v", corePID, err)
|
||||
}
|
||||
|
||||
// Wait for removal
|
||||
waitForAddressRemoved(t, target.Address, longTimeout)
|
||||
waitForAddressRemoved(t, target.CoreAddress, longTimeout)
|
||||
|
||||
// Clean up dangling host process (SIGKILL leaves these behind by design)
|
||||
t.Logf("Cleaning up dangling host process on port %d", target.HostPort())
|
||||
@@ -91,7 +91,7 @@ func TestDefaultJsonUpdateAfterRemoval(t *testing.T) {
|
||||
if len(out.CoreInstances) > 0 {
|
||||
found := false
|
||||
for _, it := range out.CoreInstances {
|
||||
if out.DefaultInstance == it.Address {
|
||||
if out.DefaultInstance == it.CoreAddress {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func listInstancesJSON(ctx context.Context, t *testing.T) common.InstancesOutput
|
||||
|
||||
func hasAddress(in common.InstancesOutput, addr string) bool {
|
||||
for _, it := range in.CoreInstances {
|
||||
if it.Address == addr {
|
||||
if it.CoreAddress == addr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func hasAddress(in common.InstancesOutput, addr string) bool {
|
||||
|
||||
func getByAddress(in common.InstancesOutput, addr string) (common.CoreInstanceInfo, bool) {
|
||||
for _, it := range in.CoreInstances {
|
||||
if it.Address == addr {
|
||||
if it.CoreAddress == addr {
|
||||
return it, true
|
||||
}
|
||||
}
|
||||
@@ -313,7 +313,7 @@ func getCorePIDViaRPC(t *testing.T, address string) int {
|
||||
defer cancel()
|
||||
|
||||
// Get client for the address
|
||||
client, err := global.Clients.GetRegistry().GetClient(ctx, address)
|
||||
client, err := global.Instances.GetRegistry().GetClient(ctx, address)
|
||||
if err != nil {
|
||||
t.Logf("Warning: failed to get client for %s, falling back to lsof: %v", address, err)
|
||||
return getCorePIDViaLsof(t, address)
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestMixedLocalhostVs127Coexist(t *testing.T) {
|
||||
t.Fatalf("expected at least 1 instance")
|
||||
}
|
||||
inst := out.CoreInstances[0]
|
||||
waitForAddressHealthy(t, inst.Address, defaultTimeout)
|
||||
waitForAddressHealthy(t, inst.CoreAddress, defaultTimeout)
|
||||
|
||||
// Manually add a SQLite entry for the same port but 127.0.0.1 host
|
||||
addr127 := fmt.Sprintf("127.0.0.1:%d", inst.CorePort())
|
||||
@@ -37,12 +37,12 @@ func TestMixedLocalhostVs127Coexist(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify both addresses appear and are healthy
|
||||
waitForAddressHealthy(t, inst.Address, defaultTimeout)
|
||||
waitForAddressHealthy(t, inst.CoreAddress, defaultTimeout)
|
||||
waitForAddressHealthy(t, addr127, defaultTimeout)
|
||||
|
||||
out = listInstancesJSON(ctx, t)
|
||||
if !hasAddress(out, inst.Address) || !hasAddress(out, addr127) {
|
||||
t.Fatalf("expected both %s and %s present", inst.Address, addr127)
|
||||
if !hasAddress(out, inst.CoreAddress) || !hasAddress(out, addr127) {
|
||||
t.Fatalf("expected both %s and %s present", inst.CoreAddress, addr127)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestStartStopStress(t *testing.T) {
|
||||
before := listInstancesJSON(ctx, t)
|
||||
beforeSet := map[string]struct{}{}
|
||||
for _, it := range before.CoreInstances {
|
||||
beforeSet[it.Address] = struct{}{}
|
||||
beforeSet[it.CoreAddress] = struct{}{}
|
||||
}
|
||||
|
||||
// Start a new instance
|
||||
@@ -69,8 +69,8 @@ func TestStartStopStress(t *testing.T) {
|
||||
waitFor(t, defaultTimeout, func() (bool, string) {
|
||||
after := listInstancesJSON(ctx, t)
|
||||
for _, it := range after.CoreInstances {
|
||||
if _, ok := beforeSet[it.Address]; !ok {
|
||||
newAddr = it.Address
|
||||
if _, ok := beforeSet[it.CoreAddress]; !ok {
|
||||
newAddr = it.CoreAddress
|
||||
return true, ""
|
||||
}
|
||||
}
|
||||
@@ -88,12 +88,12 @@ func TestStartStopStress(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get PID using runtime discovery
|
||||
corePID := getCorePID(t, info.Address)
|
||||
corePID := getCorePID(t, info.CoreAddress)
|
||||
if corePID <= 0 {
|
||||
t.Fatalf("could not find PID for new instance at %s", info.Address)
|
||||
t.Fatalf("could not find PID for new instance at %s", info.CoreAddress)
|
||||
}
|
||||
|
||||
t.Logf("Killing new instance %s (PID %d) for iteration %d", info.Address, corePID, i)
|
||||
t.Logf("Killing new instance %s (PID %d) for iteration %d", info.CoreAddress, corePID, i)
|
||||
if err := syscall.Kill(corePID, syscall.SIGKILL); err != nil {
|
||||
t.Fatalf("kill pid %d: %v", corePID, err)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func readInstancesFromSQLite(t *testing.T, clineDir string) []common.CoreInstanc
|
||||
|
||||
// Create InstanceInfo
|
||||
info := common.CoreInstanceInfo{
|
||||
Address: heldBy,
|
||||
CoreAddress: heldBy,
|
||||
HostServiceAddress: lockTarget,
|
||||
Status: grpc_health_v1.HealthCheckResponse_UNKNOWN, // Will be updated by health check
|
||||
LastSeen: time.Unix(lockedAt/1000, 0), // Convert from milliseconds
|
||||
|
||||
+14
-14
@@ -29,7 +29,7 @@ func TestStartAndList(t *testing.T) {
|
||||
t.Fatalf("expected 1 instance, got %d", len(out.CoreInstances))
|
||||
}
|
||||
|
||||
addr := out.CoreInstances[0].Address
|
||||
addr := out.CoreInstances[0].CoreAddress
|
||||
t.Logf("Instance address: %s, status: %s", addr, out.CoreInstances[0].Status)
|
||||
|
||||
t.Logf("Waiting for address %s to become healthy...", addr)
|
||||
@@ -44,8 +44,8 @@ func TestStartAndList(t *testing.T) {
|
||||
if out.DefaultInstance == "" {
|
||||
t.Fatalf("default_instance not set")
|
||||
}
|
||||
if out.DefaultInstance != out.CoreInstances[0].Address {
|
||||
t.Fatalf("expected default_instance=%s, got %s", out.CoreInstances[0].Address, out.DefaultInstance)
|
||||
if out.DefaultInstance != out.CoreInstances[0].CoreAddress {
|
||||
t.Fatalf("expected default_instance=%s, got %s", out.CoreInstances[0].CoreAddress, out.DefaultInstance)
|
||||
}
|
||||
|
||||
t.Logf("TestStartAndList completed successfully")
|
||||
@@ -64,7 +64,7 @@ func TestTaskNewDefault(t *testing.T) {
|
||||
if len(out.CoreInstances) != 1 {
|
||||
t.Fatalf("expected 1 instance, got %d", len(out.CoreInstances))
|
||||
}
|
||||
addr := out.CoreInstances[0].Address
|
||||
addr := out.CoreInstances[0].CoreAddress
|
||||
waitForAddressHealthy(t, addr, defaultTimeout)
|
||||
|
||||
// Create a new task at default (success is sufficient)
|
||||
@@ -108,21 +108,21 @@ func TestCrashCleanup(t *testing.T) {
|
||||
|
||||
// Test 1: Graceful shutdown (SIGTERM) - should clean up both processes
|
||||
gracefulTarget := out.CoreInstances[0]
|
||||
waitForAddressHealthy(t, gracefulTarget.Address, defaultTimeout)
|
||||
waitForAddressHealthy(t, gracefulTarget.CoreAddress, defaultTimeout)
|
||||
|
||||
// Get PID using runtime discovery
|
||||
gracefulPID := getCorePID(t, gracefulTarget.Address)
|
||||
gracefulPID := getCorePID(t, gracefulTarget.CoreAddress)
|
||||
if gracefulPID <= 0 {
|
||||
t.Fatalf("could not find PID for graceful target at %s", gracefulTarget.Address)
|
||||
t.Fatalf("could not find PID for graceful target at %s", gracefulTarget.CoreAddress)
|
||||
}
|
||||
|
||||
t.Logf("Testing graceful shutdown (SIGTERM) for instance %s (PID %d)", gracefulTarget.Address, gracefulPID)
|
||||
t.Logf("Testing graceful shutdown (SIGTERM) for instance %s (PID %d)", gracefulTarget.CoreAddress, gracefulPID)
|
||||
if err := syscall.Kill(gracefulPID, syscall.SIGTERM); err != nil {
|
||||
t.Fatalf("kill SIGTERM pid %d: %v", gracefulPID, err)
|
||||
}
|
||||
|
||||
// Wait for registry cleanup
|
||||
waitForAddressRemoved(t, gracefulTarget.Address, longTimeout)
|
||||
waitForAddressRemoved(t, gracefulTarget.CoreAddress, longTimeout)
|
||||
|
||||
// Verify both core and host ports are freed (no dangling processes)
|
||||
waitForPortsClosed(t, gracefulTarget.CorePort(), gracefulTarget.HostPort(), defaultTimeout)
|
||||
@@ -132,21 +132,21 @@ func TestCrashCleanup(t *testing.T) {
|
||||
|
||||
// Test 2: Crash cleanup (SIGKILL) - creates dangling host process that we must clean up
|
||||
crashTarget := out.CoreInstances[1]
|
||||
waitForAddressHealthy(t, crashTarget.Address, defaultTimeout)
|
||||
waitForAddressHealthy(t, crashTarget.CoreAddress, defaultTimeout)
|
||||
|
||||
// Get PID using runtime discovery
|
||||
crashPID := getCorePID(t, crashTarget.Address)
|
||||
crashPID := getCorePID(t, crashTarget.CoreAddress)
|
||||
if crashPID <= 0 {
|
||||
t.Fatalf("could not find PID for crash target at %s", crashTarget.Address)
|
||||
t.Fatalf("could not find PID for crash target at %s", crashTarget.CoreAddress)
|
||||
}
|
||||
|
||||
t.Logf("Testing crash cleanup (SIGKILL) for instance %s (PID %d)", crashTarget.Address, crashPID)
|
||||
t.Logf("Testing crash cleanup (SIGKILL) for instance %s (PID %d)", crashTarget.CoreAddress, crashPID)
|
||||
if err := syscall.Kill(crashPID, syscall.SIGKILL); err != nil {
|
||||
t.Fatalf("kill SIGKILL pid %d: %v", crashPID, err)
|
||||
}
|
||||
|
||||
// Wait for registry cleanup
|
||||
waitForAddressRemoved(t, crashTarget.Address, longTimeout)
|
||||
waitForAddressRemoved(t, crashTarget.CoreAddress, longTimeout)
|
||||
|
||||
// Verify the instance is removed from SQLite (no file to check anymore)
|
||||
// The waitForAddressRemoved already confirms the instance is gone from the registry
|
||||
|
||||
@@ -298,6 +298,64 @@ cline task view
|
||||
\f[I]# Start interactive chat with this task\f[R]
|
||||
cline task chat
|
||||
.EE
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
\f[B]CLINE_COMMAND_PERMISSIONS\f[R]
|
||||
JSON configuration for restricting which shell commands Cline can
|
||||
execute.
|
||||
When set, commands are validated against allow/deny patterns before
|
||||
execution.
|
||||
When not set, all commands are allowed (backward compatibility).
|
||||
.RS
|
||||
.PP
|
||||
Format:
|
||||
\f[CR]{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirects": true}\f[R]
|
||||
.PP
|
||||
\f[B]Fields:\f[R]
|
||||
.IP \(bu 2
|
||||
\f[B]allow\f[R] (array of strings): Glob patterns for allowed commands.
|
||||
If specified, only matching commands are permitted.
|
||||
Uses \f[CR]*\f[R] to match any characters and \f[CR]?\f[R] to match a
|
||||
single character.
|
||||
.IP \(bu 2
|
||||
\f[B]deny\f[R] (array of strings): Glob patterns for denied commands.
|
||||
Deny rules take precedence over allow rules.
|
||||
.IP \(bu 2
|
||||
\f[B]allowRedirects\f[R] (boolean): Whether to allow shell redirects
|
||||
(\f[CR]>\f[R], \f[CR]>>\f[R], \f[CR]<\f[R], etc.).
|
||||
Defaults to false.
|
||||
.PP
|
||||
\f[B]Rule evaluation:\f[R]
|
||||
.IP "1." 3
|
||||
Check for dangerous characters (backticks outside single quotes,
|
||||
unquoted newlines)
|
||||
.IP "2." 3
|
||||
Parse command into segments split by operators (\f[CR]&&\f[R],
|
||||
\f[CR]||\f[R], \f[CR]|\f[R], \f[CR];\f[R])
|
||||
.IP "3." 3
|
||||
If redirects detected and \f[CR]allowRedirects\f[R] is not true, command
|
||||
is denied
|
||||
.IP "4." 3
|
||||
Each segment is validated against deny rules first, then allow rules
|
||||
.IP "5." 3
|
||||
Subshell contents (\f[CR]$(...)\f[R] and \f[CR](...)\f[R]) are
|
||||
recursively validated
|
||||
.IP "6." 3
|
||||
All segments must pass for the command to be allowed
|
||||
.PP
|
||||
\f[B]Examples:\f[R]
|
||||
.IP
|
||||
.EX
|
||||
\f[I]# Allow only npm and git commands\f[R]
|
||||
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["npm *", "git *"]}\(aq
|
||||
|
||||
\f[I]# Allow development commands but deny dangerous ones\f[R]
|
||||
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["npm *", "git *", "node *"], "deny": ["rm \-rf *", "sudo *"]}\(aq
|
||||
|
||||
\f[I]# Allow file operations with redirects\f[R]
|
||||
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["cat *", "echo *"], "allowRedirects": true}\(aq
|
||||
.EE
|
||||
.RE
|
||||
.SH ARCHITECTURE
|
||||
Cline operates on a three\-layer architecture:
|
||||
.TP
|
||||
|
||||
@@ -323,6 +323,42 @@ cline task view
|
||||
cline task chat
|
||||
```
|
||||
|
||||
# ENVIRONMENT
|
||||
|
||||
**CLINE_COMMAND_PERMISSIONS**
|
||||
|
||||
: JSON configuration for restricting which shell commands Cline can execute. When set, commands are validated against allow/deny patterns before execution. When not set, all commands are allowed.
|
||||
|
||||
Format: `{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirects": true}`
|
||||
|
||||
**Fields:**
|
||||
|
||||
- **allow** (array of strings): Glob patterns for allowed commands. If specified, only matching commands are permitted. Uses `*` to match any characters and `?` to match a single character. Setting allow on anything will deny all others.
|
||||
- **deny** (array of strings): Glob patterns for denied commands. Deny rules take precedence over allow rules.
|
||||
- **allowRedirects** (boolean): Whether to allow shell redirects (`>`, `>>`, `<`, etc.). Defaults to false.
|
||||
|
||||
**Rule evaluation:**
|
||||
|
||||
1. Check for dangerous characters (backticks outside single quotes, unquoted newlines)
|
||||
2. Parse command into segments split by operators (`&&`, `||`, `|`, `;`)
|
||||
3. If redirects detected and `allowRedirects` is not true, command is denied
|
||||
4. Each segment is validated against deny rules first, then allow rules
|
||||
5. Subshell contents (`$(...)` and `(...)`) are recursively validated
|
||||
6. All segments must pass for the command to be allowed
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Allow only npm and git commands.
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"]}'
|
||||
|
||||
# Allow development commands but deny dangerous ones. Deny not strictly required here since allow is set.
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *", "node *"], "deny": ["rm -rf *", "sudo *"]}'
|
||||
|
||||
# Allow file operations with redirects
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["cat *", "echo *"], "allowRedirects": true}'
|
||||
```
|
||||
|
||||
# ARCHITECTURE
|
||||
|
||||
Cline operates on a three-layer architecture:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "1.0.9",
|
||||
"version": "1.0.10",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "cline-core.js",
|
||||
"bin": {
|
||||
|
||||
@@ -46,21 +46,21 @@ const (
|
||||
// It spawns a fresh instance for auth operations and cleans it up when done
|
||||
func RunAuthFlow(ctx context.Context, args []string) error {
|
||||
// Spawn a fresh instance for auth operations
|
||||
instanceInfo, err := global.Clients.StartNewInstance(ctx)
|
||||
instanceInfo, err := global.Instances.StartNewInstance(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start auth instance: %w", err)
|
||||
}
|
||||
|
||||
// Cleanup when done (success, error, or panic)
|
||||
defer func() {
|
||||
verboseLog("Shutting down auth instance at %s", instanceInfo.Address)
|
||||
if err := global.KillInstanceByAddress(context.Background(), global.Clients.GetRegistry(), instanceInfo.Address); err != nil {
|
||||
verboseLog("Shutting down auth instance at %s", instanceInfo.CoreAddress)
|
||||
if err := global.KillInstanceByAddress(context.Background(), global.Instances.GetRegistry(), instanceInfo.CoreAddress); err != nil {
|
||||
verboseLog("Warning: Failed to kill auth instance: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Store instance address in context for all auth handlers to use
|
||||
authCtx := context.WithValue(ctx, authInstanceAddressKey, instanceInfo.Address)
|
||||
authCtx := context.WithValue(ctx, authInstanceAddressKey, instanceInfo.CoreAddress)
|
||||
|
||||
// Route to existing auth flow
|
||||
return HandleAuthCommand(authCtx, args)
|
||||
@@ -108,12 +108,10 @@ func HandleAuthMenuNoArgs(ctx context.Context) error {
|
||||
// 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
|
||||
}
|
||||
if providerList, err := GetProviderConfigurations(ctx); err == nil {
|
||||
if providerList.ActProvider != nil {
|
||||
currentProvider = GetProviderDisplayName(providerList.ActProvider.Provider)
|
||||
currentModel = providerList.ActProvider.ModelID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,13 +29,17 @@ type ProviderListResult struct {
|
||||
}
|
||||
|
||||
// GetProviderConfigurations retrieves and parses provider configurations from Cline Core state
|
||||
func GetProviderConfigurations(ctx context.Context, manager *task.Manager) (*ProviderListResult, error) {
|
||||
func GetProviderConfigurations(ctx context.Context) (*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{})
|
||||
grpcClient, err := global.GetDefaultClient(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get provider configs due to unable to get gRPC client: %w", err)
|
||||
}
|
||||
state, err := grpcClient.State.GetLatestState(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get state: %w", err)
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ func (pw *ProviderWizard) handleAddOcaProvider() error {
|
||||
|
||||
// handleListProviders retrieves and displays configured providers
|
||||
func (pw *ProviderWizard) handleListProviders() error {
|
||||
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
|
||||
result, err := GetProviderConfigurations(pw.ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
|
||||
}
|
||||
@@ -413,7 +413,7 @@ func (pw *ProviderWizard) manualModelEntry(provider cline.ApiProvider) (string,
|
||||
// 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)
|
||||
result, err := GetProviderConfigurations(pw.ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
|
||||
}
|
||||
@@ -585,11 +585,11 @@ func getProviderModelIDFromState(stateData map[string]interface{}, provider clin
|
||||
return ""
|
||||
}
|
||||
|
||||
// getProviderAPIKeyFromState retrieves the API key for a specific provider from state
|
||||
// getProviderAPIKeyFromState retrieves the API key for a specific provider from state
|
||||
func getProviderAPIKeyFromState(stateData map[string]interface{}, provider cline.ApiProvider) string {
|
||||
// OCA uses account authentication, not API keys. Consider it "present" if authenticated.
|
||||
if provider == cline.ApiProvider_OCA {
|
||||
if state, _ := GetLatestOCAState(context.TODO(), 2 * time.Second); state != nil && state.User != nil {
|
||||
if state, _ := GetLatestOCAState(context.TODO(), 2*time.Second); state != nil && state.User != nil {
|
||||
// Return a sentinel non-empty string so upstream checks pass.
|
||||
return "OCA_AUTH_VERIFIED"
|
||||
}
|
||||
@@ -648,7 +648,7 @@ func convertMapToOpenRouterModelInfo(data map[string]interface{}) *cline.OpenRou
|
||||
// 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)
|
||||
result, err := GetProviderConfigurations(pw.ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
|
||||
}
|
||||
@@ -748,7 +748,6 @@ func (pw *ProviderWizard) clearProviderAPIKey(provider cline.ApiProvider) error
|
||||
return RemoveProviderPartial(pw.ctx, pw.manager, provider)
|
||||
}
|
||||
|
||||
|
||||
func signOutOca(ctx context.Context) error {
|
||||
client, err := global.GetDefaultClient(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -40,7 +40,7 @@ func ensureConfigManager(ctx context.Context, address string) error {
|
||||
}
|
||||
|
||||
// Always set the instance we're using as the default
|
||||
registry := global.Clients.GetRegistry()
|
||||
registry := global.Instances.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)
|
||||
@@ -123,11 +123,11 @@ func setCommand() *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.
|
||||
Long: `Set one or more global configuration variables using key=value format.
|
||||
|
||||
This command merges the provided settings with existing values, preserving
|
||||
unspecified fields. Only the fields you explicitly set will be updated.`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ func NewManager(ctx context.Context, address string) (*Manager, error) {
|
||||
|
||||
// Get the actual address being used
|
||||
clientAddress := address
|
||||
if address == "" && global.Clients != nil {
|
||||
clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
|
||||
if address == "" && global.Instances != nil {
|
||||
clientAddress = global.Instances.GetRegistry().GetDefaultInstance()
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
@@ -110,7 +110,6 @@ func (m *Manager) ListSettings(ctx context.Context) error {
|
||||
"dictationSettings",
|
||||
"autoCondenseThreshold",
|
||||
"autoApprovalSettings",
|
||||
"hooksEnabled",
|
||||
}
|
||||
|
||||
// Render each field using the renderer
|
||||
|
||||
@@ -79,7 +79,7 @@ func RenderField(key string, value interface{}, censor bool) error {
|
||||
"planActSeparateModelsSetting", "enableCheckpointsSetting",
|
||||
"terminalReuseEnabled", "mcpResponsesCollapsed", "strictPlanModeEnabled",
|
||||
"useAutoCondense", "yoloModeToggled", "shellIntegrationTimeout",
|
||||
"terminalOutputLineLimit", "autoCondenseThreshold", "hooksEnabled":
|
||||
"terminalOutputLineLimit", "autoCondenseThreshold":
|
||||
fmt.Printf("%s: %s\n", camelToKebab(key), formatValue(value, key, censor))
|
||||
return nil
|
||||
|
||||
|
||||
@@ -14,21 +14,20 @@ import (
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
// ClineClients manages Cline instances using the new registry system
|
||||
type ClineClients struct {
|
||||
registry *ClientRegistry
|
||||
type ClineInstances struct {
|
||||
registry *InstanceRegistry
|
||||
}
|
||||
|
||||
// NewClineClients creates a new ClineClients instance
|
||||
func NewClineClients(configPath string) *ClineClients {
|
||||
registry := NewClientRegistry(configPath)
|
||||
return &ClineClients{
|
||||
// NewClineInstances creates a new ClineInstances instance
|
||||
func NewClineInstances(configPath string) *ClineInstances {
|
||||
registry := NewInstanceRegistry(configPath)
|
||||
return &ClineInstances{
|
||||
registry: registry,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize performs cleanup of stale instances
|
||||
func (c *ClineClients) Initialize(ctx context.Context) error {
|
||||
func (c *ClineInstances) Initialize(ctx context.Context) error {
|
||||
// Clean up stale entries (direct SQLite operations)
|
||||
_ = c.registry.CleanupStaleInstances(ctx)
|
||||
|
||||
@@ -36,7 +35,8 @@ func (c *ClineClients) Initialize(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// StartNewInstance starts a new Cline instance and waits for cline-core to self-register
|
||||
func (c *ClineClients) StartNewInstance(ctx context.Context, workspaces ...string) (*common.CoreInstanceInfo, error) {
|
||||
// An "instance" is a pair of cline-core and cline-host processes
|
||||
func (c *ClineInstances) StartNewInstance(ctx context.Context, workspaces ...string) (*common.CoreInstanceInfo, error) {
|
||||
// Find available ports
|
||||
corePort, hostPort, err := common.FindAvailablePortPair()
|
||||
if err != nil {
|
||||
@@ -102,7 +102,7 @@ func (c *ClineClients) StartNewInstance(ctx context.Context, workspaces ...strin
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Println("Services started and registered successfully!")
|
||||
fmt.Printf(" Address: %s\n", instance.Address)
|
||||
fmt.Printf(" Address: %s\n", instance.CoreAddress)
|
||||
fmt.Printf(" Core Port: %d\n", instance.CorePort())
|
||||
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
|
||||
fmt.Printf(" Process PID: %d\n", coreCmd.Process.Pid)
|
||||
@@ -120,7 +120,7 @@ func (c *ClineClients) StartNewInstance(ctx context.Context, workspaces ...strin
|
||||
}
|
||||
|
||||
// StartNewInstanceAtPort starts a new Cline instance at the specified port and waits for self-registration
|
||||
func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int, workspaces ...string) (*common.CoreInstanceInfo, error) {
|
||||
func (c *ClineInstances) StartNewInstanceAtPort(ctx context.Context, corePort int, workspaces ...string) (*common.CoreInstanceInfo, error) {
|
||||
// Find available host port (core port + 1000)
|
||||
hostPort := corePort + 1000
|
||||
coreAddress := fmt.Sprintf("localhost:%d", corePort)
|
||||
@@ -189,7 +189,7 @@ func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int,
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Println("Services started and registered successfully!")
|
||||
fmt.Printf(" Address: %s\n", instance.Address)
|
||||
fmt.Printf(" Address: %s\n", instance.CoreAddress)
|
||||
fmt.Printf(" Core Port: %d\n", instance.CorePort())
|
||||
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
|
||||
fmt.Printf(" Process PID: %d\n", coreCmd.Process.Pid)
|
||||
@@ -207,12 +207,12 @@ func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int,
|
||||
}
|
||||
|
||||
// GetRegistry returns the client registry
|
||||
func (c *ClineClients) GetRegistry() *ClientRegistry {
|
||||
func (c *ClineInstances) GetRegistry() *InstanceRegistry {
|
||||
return c.registry
|
||||
}
|
||||
|
||||
// EnsureInstanceAtAddress ensures an instance exists at the given address, starting one if needed
|
||||
func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address string) error {
|
||||
func (c *ClineInstances) EnsureInstanceAtAddress(ctx context.Context, address string) error {
|
||||
// Expect host:port everywhere
|
||||
normalized := address
|
||||
if normalized == "" {
|
||||
@@ -230,7 +230,7 @@ func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address stri
|
||||
return fmt.Errorf("invalid address format %s", address)
|
||||
}
|
||||
|
||||
// Use IPv6-compatible localhost detection
|
||||
// Use IPv6-compatible localhost detection
|
||||
if common.IsLocalAddress(host) {
|
||||
_, err := c.StartNewInstanceAtPort(ctx, port)
|
||||
if err != nil {
|
||||
@@ -305,7 +305,7 @@ func startClineHost(hostPort int, workspaces []string) (*exec.Cmd, error) {
|
||||
}
|
||||
|
||||
// KillInstanceByAddress kills a Cline instance by its address
|
||||
func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, address string) error {
|
||||
func KillInstanceByAddress(ctx context.Context, registry *InstanceRegistry, address string) error {
|
||||
// Check if the instance exists in the registry
|
||||
_, err := registry.GetInstance(address)
|
||||
if err != nil {
|
||||
@@ -355,9 +355,9 @@ func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, addres
|
||||
defaultInstance := registry.GetDefaultInstance()
|
||||
if defaultInstance == address || defaultInstance == "" {
|
||||
if len(instances) > 0 {
|
||||
if err := registry.SetDefaultInstance(instances[0].Address); err == nil {
|
||||
if err := registry.SetDefaultInstance(instances[0].CoreAddress); err == nil {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Updated default instance to: %s\n", instances[0].Address)
|
||||
fmt.Printf("Updated default instance to: %s\n", instances[0].CoreAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ type GlobalConfig struct {
|
||||
}
|
||||
|
||||
var (
|
||||
Config *GlobalConfig
|
||||
Clients *ClineClients
|
||||
Config *GlobalConfig
|
||||
Instances *ClineInstances
|
||||
|
||||
// Version info - set at build time via ldflags
|
||||
// Version is the Cline Core version (from root package.json)
|
||||
@@ -61,11 +61,11 @@ func InitializeGlobalConfig(cfg *GlobalConfig) error {
|
||||
// Otherwise lipgloss auto-detects terminal capabilities (default behavior)
|
||||
|
||||
Config = cfg
|
||||
Clients = NewClineClients(cfg.ConfigPath)
|
||||
Instances = NewClineInstances(cfg.ConfigPath)
|
||||
|
||||
// Initialize the clients registry
|
||||
ctx := context.Background()
|
||||
if err := Clients.Initialize(ctx); err != nil {
|
||||
if err := Instances.Initialize(ctx); err != nil {
|
||||
return fmt.Errorf("failed to initialize clients: %w", err)
|
||||
}
|
||||
|
||||
@@ -76,25 +76,25 @@ func InitializeGlobalConfig(cfg *GlobalConfig) error {
|
||||
func GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
|
||||
if Config.CoreAddress != "" && Config.CoreAddress != fmt.Sprintf("localhost:%d", common.DEFAULT_CLINE_CORE_PORT) {
|
||||
// User specified a specific address, use that
|
||||
return Clients.GetRegistry().GetClient(ctx, Config.CoreAddress)
|
||||
return Instances.GetRegistry().GetClient(ctx, Config.CoreAddress)
|
||||
}
|
||||
|
||||
// Use the default instance from registry
|
||||
return Clients.GetRegistry().GetDefaultClient(ctx)
|
||||
return Instances.GetRegistry().GetDefaultClient(ctx)
|
||||
}
|
||||
|
||||
// GetClientForAddress returns a client for a specific address
|
||||
func GetClientForAddress(ctx context.Context, address string) (*client.ClineClient, error) {
|
||||
return Clients.GetRegistry().GetClient(ctx, address)
|
||||
return Instances.GetRegistry().GetClient(ctx, address)
|
||||
}
|
||||
|
||||
// EnsureDefaultInstance ensures a default instance exists
|
||||
func EnsureDefaultInstance(ctx context.Context) error {
|
||||
if Clients == nil {
|
||||
if Instances == nil {
|
||||
return fmt.Errorf("global clients not initialized")
|
||||
}
|
||||
|
||||
registry := Clients.GetRegistry()
|
||||
registry := Instances.GetRegistry()
|
||||
|
||||
// First, check if there are any instances already registered in SQLite
|
||||
instances := registry.ListInstances()
|
||||
@@ -108,11 +108,12 @@ func EnsureDefaultInstance(ctx context.Context) error {
|
||||
if registry.GetDefaultInstance() == "" {
|
||||
// No instances exist, start a new one
|
||||
// Note: StartNewInstance will automatically set it as default since it's the first instance
|
||||
_, err := Clients.StartNewInstance(ctx)
|
||||
_, err := Instances.StartNewInstance(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start new default instance: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,28 +18,28 @@ import (
|
||||
"google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
// ClientRegistry manages Cline client connections using direct SQLite operations
|
||||
type ClientRegistry struct {
|
||||
// InstanceRegistry manages Cline client connections using direct SQLite operations
|
||||
type InstanceRegistry struct {
|
||||
lockManager *sqlite.LockManager
|
||||
configPath string
|
||||
}
|
||||
|
||||
// NewClientRegistry creates a new client registry
|
||||
func NewClientRegistry(configPath string) *ClientRegistry {
|
||||
// NewInstanceRegistry creates a new instance registry
|
||||
func NewInstanceRegistry(configPath string) *InstanceRegistry {
|
||||
lockManager, err := sqlite.NewLockManager(configPath)
|
||||
if err != nil {
|
||||
// Log error but continue - we can still function without SQLite
|
||||
log.Fatalf("Warning: Failed to initialize SQLite lock manager: %v\n", err)
|
||||
}
|
||||
|
||||
return &ClientRegistry{
|
||||
return &InstanceRegistry{
|
||||
lockManager: lockManager,
|
||||
configPath: configPath,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDefaultInstance returns the default instance address from settings file
|
||||
func (r *ClientRegistry) GetDefaultInstance() string {
|
||||
func (r *InstanceRegistry) GetDefaultInstance() string {
|
||||
defaultAddr, err := sqlite.GetDefaultInstance(r.configPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -48,7 +48,7 @@ func (r *ClientRegistry) GetDefaultInstance() string {
|
||||
}
|
||||
|
||||
// SetDefaultInstance sets the default instance (writes default.json)
|
||||
func (r *ClientRegistry) SetDefaultInstance(address string) error {
|
||||
func (r *InstanceRegistry) SetDefaultInstance(address string) error {
|
||||
// Verify the instance exists in SQLite
|
||||
if r.lockManager != nil {
|
||||
exists, err := r.lockManager.HasInstanceAtAddress(address)
|
||||
@@ -64,7 +64,7 @@ func (r *ClientRegistry) SetDefaultInstance(address string) error {
|
||||
}
|
||||
|
||||
// GetInstance returns instance information directly from SQLite
|
||||
func (r *ClientRegistry) GetInstance(address string) (*common.CoreInstanceInfo, error) {
|
||||
func (r *InstanceRegistry) GetInstance(address string) (*common.CoreInstanceInfo, error) {
|
||||
if r.lockManager == nil {
|
||||
return nil, fmt.Errorf("lock manager not available")
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (r *ClientRegistry) GetInstance(address string) (*common.CoreInstanceInfo,
|
||||
}
|
||||
|
||||
// GetClient returns a connected client for the given address (created on-demand)
|
||||
func (r *ClientRegistry) GetClient(ctx context.Context, address string) (*client.ClineClient, error) {
|
||||
func (r *InstanceRegistry) GetClient(ctx context.Context, address string) (*client.ClineClient, error) {
|
||||
// Verify instance exists in SQLite
|
||||
if r.lockManager != nil {
|
||||
exists, err := r.lockManager.HasInstanceAtAddress(address)
|
||||
@@ -104,7 +104,7 @@ func (r *ClientRegistry) GetClient(ctx context.Context, address string) (*client
|
||||
}
|
||||
|
||||
// GetDefaultClient returns a client for the default instance
|
||||
func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
|
||||
func (r *InstanceRegistry) GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
|
||||
defaultAddr := r.GetDefaultInstance()
|
||||
if defaultAddr == "" {
|
||||
return nil, fmt.Errorf("no default instance configured")
|
||||
@@ -117,7 +117,7 @@ func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineCli
|
||||
// Database is unavailable - Return error instead of attempting cleanup
|
||||
return nil, fmt.Errorf("cannot verify default instance: database unavailable: %w", err)
|
||||
}
|
||||
|
||||
|
||||
if !exists {
|
||||
// Instance doesn't exist in database but config file references it
|
||||
// This is a stale config - remove it and try to find another instance
|
||||
@@ -127,14 +127,14 @@ func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineCli
|
||||
} else {
|
||||
fmt.Printf("Removed stale default instance config (instance %s not found in database)\n", defaultAddr)
|
||||
}
|
||||
|
||||
|
||||
// Try to find and set a new default instance
|
||||
instances := r.ListInstances()
|
||||
if len(instances) > 0 {
|
||||
if err := r.EnsureDefaultInstance(instances); err != nil {
|
||||
return nil, fmt.Errorf("failed to set new default instance: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// Retry with the new default
|
||||
newDefaultAddr := r.GetDefaultInstance()
|
||||
if newDefaultAddr != "" {
|
||||
@@ -142,7 +142,7 @@ func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineCli
|
||||
return r.GetClient(ctx, newDefaultAddr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil, fmt.Errorf("no default instance configured")
|
||||
}
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineCli
|
||||
}
|
||||
|
||||
// ListInstances returns all registered instances directly from SQLite
|
||||
func (r *ClientRegistry) ListInstances() []*common.CoreInstanceInfo {
|
||||
func (r *InstanceRegistry) ListInstances() []*common.CoreInstanceInfo {
|
||||
if r.lockManager == nil {
|
||||
return []*common.CoreInstanceInfo{}
|
||||
}
|
||||
@@ -170,7 +170,7 @@ func (r *ClientRegistry) ListInstances() []*common.CoreInstanceInfo {
|
||||
}
|
||||
|
||||
// HasInstanceAtAddress checks if an instance exists at the given address (delegates to SQLite)
|
||||
func (r *ClientRegistry) HasInstanceAtAddress(address string) bool {
|
||||
func (r *InstanceRegistry) HasInstanceAtAddress(address string) bool {
|
||||
if r.lockManager == nil {
|
||||
return false
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func (r *ClientRegistry) HasInstanceAtAddress(address string) bool {
|
||||
}
|
||||
|
||||
// CleanupStaleInstances removes stale instances using direct SQLite operations
|
||||
func (r *ClientRegistry) CleanupStaleInstances(ctx context.Context) error {
|
||||
func (r *InstanceRegistry) CleanupStaleInstances(ctx context.Context) error {
|
||||
if r.lockManager == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -202,15 +202,15 @@ func (r *ClientRegistry) CleanupStaleInstances(ctx context.Context) error {
|
||||
// Try to gracefully shutdown the paired host process before cleanup
|
||||
|
||||
fmt.Printf("Attempting to shutdown dangling host service %s for stale cline core instance %s\n",
|
||||
instance.HostServiceAddress, instance.Address)
|
||||
instance.HostServiceAddress, instance.CoreAddress)
|
||||
r.tryShutdownHostProcess(instance.HostServiceAddress)
|
||||
|
||||
// Remove from SQLite database
|
||||
if err := r.lockManager.RemoveInstanceLock(instance.Address); err != nil {
|
||||
return fmt.Errorf("failed to remove stale instance %s: %w", instance.Address, err)
|
||||
if err := r.lockManager.RemoveInstanceLock(instance.CoreAddress); err != nil {
|
||||
return fmt.Errorf("failed to remove stale instance %s: %w", instance.CoreAddress, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Removed stale instance: %s\n", instance.Address)
|
||||
fmt.Printf("Removed stale instance: %s\n", instance.CoreAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,8 +218,7 @@ func (r *ClientRegistry) CleanupStaleInstances(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// tryShutdownHostProcess attempts to gracefully shutdown a host process via RPC
|
||||
// Best effort, don't throw errors i guess
|
||||
func (r *ClientRegistry) tryShutdownHostProcess(hostServiceAddress string) {
|
||||
func (r *InstanceRegistry) tryShutdownHostProcess(hostServiceAddress string) {
|
||||
err := common.RetryOperation(3, 2*time.Second, func() error {
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
@@ -252,7 +251,7 @@ func (r *ClientRegistry) tryShutdownHostProcess(hostServiceAddress string) {
|
||||
}
|
||||
|
||||
// ListInstancesCleaned performs cleanup and returns instances with health checks
|
||||
func (r *ClientRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.CoreInstanceInfo, error) {
|
||||
func (r *InstanceRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.CoreInstanceInfo, error) {
|
||||
// 1. Clean up stale entries (best-effort)
|
||||
_ = r.CleanupStaleInstances(ctx)
|
||||
|
||||
@@ -268,7 +267,7 @@ func (r *ClientRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.Co
|
||||
}
|
||||
|
||||
// EnsureDefaultInstance ensures a default instance is set if instances exist but no default is configured
|
||||
func (r *ClientRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceInfo) error {
|
||||
func (r *InstanceRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceInfo) error {
|
||||
currentDefault := r.GetDefaultInstance()
|
||||
|
||||
// If we have no instances, clear any stale default and remove settings file
|
||||
@@ -283,13 +282,13 @@ func (r *ClientRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceI
|
||||
|
||||
// If we have instances but no default, pick the first one
|
||||
if currentDefault == "" {
|
||||
return sqlite.SetDefaultInstance(r.configPath, instances[0].Address)
|
||||
return sqlite.SetDefaultInstance(r.configPath, instances[0].CoreAddress)
|
||||
}
|
||||
|
||||
// Validate current default still exists in the instances
|
||||
defaultExists := false
|
||||
for _, instance := range instances {
|
||||
if instance.Address == currentDefault {
|
||||
if instance.CoreAddress == currentDefault {
|
||||
defaultExists = true
|
||||
break
|
||||
}
|
||||
@@ -297,7 +296,7 @@ func (r *ClientRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceI
|
||||
|
||||
if !defaultExists {
|
||||
// Current default doesn't exist, pick a new one from available instances
|
||||
return sqlite.SetDefaultInstance(r.configPath, instances[0].Address)
|
||||
return sqlite.SetDefaultInstance(r.configPath, instances[0].CoreAddress)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+25
-25
@@ -92,12 +92,12 @@ func newInstanceKillCommand() *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if global.Clients == nil {
|
||||
if global.Instances == nil {
|
||||
return fmt.Errorf("clients not initialized")
|
||||
}
|
||||
|
||||
ctx := cmd.Context()
|
||||
registry := global.Clients.GetRegistry()
|
||||
registry := global.Instances.GetRegistry()
|
||||
|
||||
if killAllCLI {
|
||||
return killAllCLIInstances(ctx, registry)
|
||||
@@ -112,7 +112,7 @@ func newInstanceKillCommand() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) error {
|
||||
func killAllCLIInstances(ctx context.Context, registry *global.InstanceRegistry) error {
|
||||
// Get all instances from registry
|
||||
instances, err := registry.ListInstancesCleaned(ctx)
|
||||
if err != nil {
|
||||
@@ -135,7 +135,7 @@ func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) e
|
||||
cliInstances = append(cliInstances, instance)
|
||||
} else {
|
||||
skippedNonCLI++
|
||||
fmt.Printf("⊘ Skipping %s instance: %s\n", platform, instance.Address)
|
||||
fmt.Printf("⊘ Skipping %s instance: %s\n", platform, instance.CoreAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,16 +160,16 @@ func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) e
|
||||
|
||||
// Kill all CLI instances
|
||||
for _, instance := range cliInstances {
|
||||
result := killInstanceProcess(ctx, registry, instance.Address)
|
||||
result := killInstanceProcess(ctx, registry, instance.CoreAddress)
|
||||
killResults = append(killResults, result)
|
||||
|
||||
if result.err != nil {
|
||||
fmt.Printf("✗ Failed to kill %s: %v\n", instance.Address, result.err)
|
||||
fmt.Printf("✗ Failed to kill %s: %v\n", instance.CoreAddress, result.err)
|
||||
} else if result.alreadyDead {
|
||||
fmt.Printf("⚠ Instance %s appears to be already dead\n", instance.Address)
|
||||
fmt.Printf("⚠ Instance %s appears to be already dead\n", instance.CoreAddress)
|
||||
} else {
|
||||
fmt.Printf("✓ Killed %s (PID %d)\n", instance.Address, result.pid)
|
||||
killedAddresses[instance.Address] = true
|
||||
fmt.Printf("✓ Killed %s (PID %d)\n", instance.CoreAddress, result.pid)
|
||||
killedAddresses[instance.CoreAddress] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,8 +190,8 @@ func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) e
|
||||
// Check if any of the killed instances are still in the registry
|
||||
stillPresent := []string{}
|
||||
for _, remaining := range remainingInstances {
|
||||
if killedAddresses[remaining.Address] {
|
||||
stillPresent = append(stillPresent, remaining.Address)
|
||||
if killedAddresses[remaining.CoreAddress] {
|
||||
stillPresent = append(stillPresent, remaining.CoreAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ type killResult struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func killInstanceProcess(ctx context.Context, registry *global.ClientRegistry, address string) killResult {
|
||||
func killInstanceProcess(ctx context.Context, registry *global.InstanceRegistry, address string) killResult {
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
@@ -276,12 +276,12 @@ func newInstanceListCommand() *cobra.Command {
|
||||
Short: "List all registered Cline instances",
|
||||
Long: `List all registered Cline instances with their status and connection details.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if global.Clients == nil {
|
||||
if global.Instances == nil {
|
||||
return fmt.Errorf("clients not initialized")
|
||||
}
|
||||
|
||||
ctx := cmd.Context()
|
||||
registry := global.Clients.GetRegistry()
|
||||
registry := global.Instances.GetRegistry()
|
||||
|
||||
// Load, cleanup stale local entries, and update health
|
||||
instances, err := registry.ListInstancesCleaned(ctx)
|
||||
@@ -310,7 +310,7 @@ func newInstanceListCommand() *cobra.Command {
|
||||
var rows []instanceRow
|
||||
for _, instance := range instances {
|
||||
isDefault := ""
|
||||
if instance.Address == defaultInstance {
|
||||
if instance.CoreAddress == defaultInstance {
|
||||
isDefault = "✓"
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ func newInstanceListCommand() *cobra.Command {
|
||||
platform := platformNA
|
||||
if instance.Status == grpc_health_v1.HealthCheckResponse_SERVING {
|
||||
// Get PID from core
|
||||
if client, err := registry.GetClient(ctx, instance.Address); err == nil {
|
||||
if client, err := registry.GetClient(ctx, instance.CoreAddress); err == nil {
|
||||
if processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{}); err == nil {
|
||||
pid = fmt.Sprintf("%d", processInfo.ProcessId)
|
||||
// Update version from RPC if available
|
||||
@@ -341,7 +341,7 @@ func newInstanceListCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
rows = append(rows, instanceRow{
|
||||
address: instance.Address,
|
||||
address: instance.CoreAddress,
|
||||
status: instance.Status.String(),
|
||||
version: instance.Version,
|
||||
lastSeen: lastSeen,
|
||||
@@ -428,11 +428,11 @@ func newInstanceDefaultCommand() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
address := args[0]
|
||||
|
||||
if global.Clients == nil {
|
||||
if global.Instances == nil {
|
||||
return fmt.Errorf("clients not initialized")
|
||||
}
|
||||
|
||||
registry := global.Clients.GetRegistry()
|
||||
registry := global.Instances.GetRegistry()
|
||||
|
||||
// Verify the instance exists
|
||||
_, err := registry.GetInstance(address)
|
||||
@@ -464,34 +464,34 @@ func newInstanceNewCommand() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
if global.Clients == nil {
|
||||
if global.Instances == nil {
|
||||
return fmt.Errorf("clients not initialized")
|
||||
}
|
||||
|
||||
fmt.Println("Starting new Cline instance...")
|
||||
|
||||
instance, err := global.Clients.StartNewInstance(ctx)
|
||||
instance, err := global.Instances.StartNewInstance(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start instance: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully started new instance:\n")
|
||||
fmt.Printf(" Address: %s\n", instance.Address)
|
||||
fmt.Printf(" Address: %s\n", instance.CoreAddress)
|
||||
fmt.Printf(" Core Port: %d\n", instance.CorePort())
|
||||
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
|
||||
|
||||
registry := global.Clients.GetRegistry()
|
||||
registry := global.Instances.GetRegistry()
|
||||
|
||||
// If --default flag provided, set this instance as the default
|
||||
if setDefault {
|
||||
if err := registry.SetDefaultInstance(instance.Address); err != nil {
|
||||
if err := registry.SetDefaultInstance(instance.CoreAddress); err != nil {
|
||||
fmt.Printf("Warning: Failed to set as default: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" Status: Set as default instance\n")
|
||||
}
|
||||
} else {
|
||||
// Otherwise, check if EnsureDefaultInstance already set it as default
|
||||
if registry.GetDefaultInstance() == instance.Address {
|
||||
if registry.GetDefaultInstance() == instance.CoreAddress {
|
||||
fmt.Printf(" Status: Default instance\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@ func (m *InputModel) handleSubmit() (tea.Model, tea.Cmd) {
|
||||
return ChangeInputTypeMsg{
|
||||
InputType: InputTypeFeedback,
|
||||
Title: "Your feedback",
|
||||
Placeholder: "/plan or /act to switch modes\nctrl+e to open editor",
|
||||
Placeholder: "/plan or /act to switch modes\nctrl+e to open editor\nctrl+c to exit",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,3 +547,74 @@ func (m *InputModel) openEditor() tea.Cmd {
|
||||
func (m *InputModel) SetSlashRegistry(registry *slash.Registry) {
|
||||
m.completion.SetRegistry(registry)
|
||||
}
|
||||
|
||||
// initialPromptWrapper wraps InputModel to capture the submit result for initial task prompts
|
||||
type initialPromptWrapper struct {
|
||||
model *InputModel
|
||||
result string
|
||||
cancelled bool
|
||||
}
|
||||
|
||||
func (w *initialPromptWrapper) Init() tea.Cmd {
|
||||
return w.model.Init()
|
||||
}
|
||||
|
||||
func (w *initialPromptWrapper) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case InputSubmitMsg:
|
||||
w.result = msg.Value
|
||||
clearCodes := w.model.ClearScreen()
|
||||
if clearCodes != "" {
|
||||
fmt.Print(clearCodes)
|
||||
}
|
||||
return w, tea.Quit
|
||||
|
||||
case InputCancelMsg:
|
||||
w.cancelled = true
|
||||
clearCodes := w.model.ClearScreen()
|
||||
if clearCodes != "" {
|
||||
fmt.Print(clearCodes)
|
||||
}
|
||||
return w, tea.Quit
|
||||
}
|
||||
|
||||
// Forward to wrapped model
|
||||
_, cmd := w.model.Update(msg)
|
||||
return w, cmd
|
||||
}
|
||||
|
||||
func (w *initialPromptWrapper) View() string {
|
||||
return w.model.View()
|
||||
}
|
||||
|
||||
// ErrUserAborted is returned when the user cancels the input prompt
|
||||
var ErrUserAborted = fmt.Errorf("user aborted")
|
||||
|
||||
// PromptForInitialTask displays an interactive prompt for the initial task with slash command autocomplete.
|
||||
// Returns the entered text, or ErrUserAborted if cancelled.
|
||||
func PromptForInitialTask(title, placeholder, mode string, registry *slash.Registry) (string, error) {
|
||||
model := NewInputModelWithRegistry(
|
||||
InputTypeMessage,
|
||||
title,
|
||||
placeholder,
|
||||
mode,
|
||||
registry,
|
||||
)
|
||||
|
||||
wrapper := &initialPromptWrapper{
|
||||
model: &model,
|
||||
}
|
||||
|
||||
p := tea.NewProgram(wrapper)
|
||||
|
||||
_, err := p.Run()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("input prompt failed: %w", err)
|
||||
}
|
||||
|
||||
if wrapper.cancelled {
|
||||
return "", ErrUserAborted
|
||||
}
|
||||
|
||||
return strings.TrimSpace(wrapper.result), nil
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ package slash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cline/grpc-go/client"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
@@ -32,25 +33,31 @@ var cliLocalCommands = []Command{
|
||||
}
|
||||
|
||||
// NewRegistry creates a new slash command registry
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
commands: make([]Command, 0),
|
||||
func NewRegistry(ctx context.Context) *Registry {
|
||||
defaultCommands := append([]Command{}, cliLocalCommands...)
|
||||
r := &Registry{
|
||||
commands: defaultCommands,
|
||||
}
|
||||
r.FetchFromBackend(ctx)
|
||||
return r
|
||||
}
|
||||
|
||||
// FetchFromBackend fetches available commands from cline-core backend
|
||||
func (r *Registry) FetchFromBackend(ctx context.Context, c *client.ClineClient) error {
|
||||
resp, err := c.Slash.GetAvailableSlashCommands(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
func (r *Registry) FetchFromBackend(ctx context.Context) error {
|
||||
grpcClient, err := global.GetDefaultClient(ctx)
|
||||
if err != nil && global.Config.Verbose {
|
||||
fmt.Printf("Warning: could not get gRPC client: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
resp, err := grpcClient.Slash.GetAvailableSlashCommands(ctx, &cline.EmptyRequest{})
|
||||
if err != nil && global.Config.Verbose {
|
||||
fmt.Printf("Warning: could not get gRPC client: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// Start with CLI-local commands
|
||||
r.commands = append([]Command{}, cliLocalCommands...)
|
||||
|
||||
// Add backend commands (only CLI-compatible ones)
|
||||
for _, cmd := range resp.Commands {
|
||||
if cmd.CliCompatible {
|
||||
@@ -66,17 +73,6 @@ func (r *Registry) FetchFromBackend(ctx context.Context, c *client.ClineClient)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCommands returns all available commands
|
||||
func (r *Registry) GetCommands() []Command {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
// Return a copy to avoid race conditions
|
||||
result := make([]Command, len(r.commands))
|
||||
copy(result, r.commands)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetMatching returns commands that start with the given prefix (case-insensitive)
|
||||
func (r *Registry) GetMatching(prefix string) []Command {
|
||||
r.mu.RLock()
|
||||
@@ -123,3 +119,24 @@ func (r *Registry) HasCommands() bool {
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.commands) > 0
|
||||
}
|
||||
|
||||
// ParseModeSwitch checks if message starts with /act or /plan and extracts the mode and remaining message.
|
||||
// Returns (mode, remainingMessage, isModeSwitch).
|
||||
// This is a package-level function so it can be used both during initial task creation
|
||||
// and during interactive input handling.
|
||||
func ParseModeSwitch(message string) (string, string, bool) {
|
||||
trimmed := strings.TrimSpace(message)
|
||||
lower := strings.ToLower(trimmed)
|
||||
|
||||
if strings.HasPrefix(lower, "/plan") {
|
||||
remaining := strings.TrimSpace(trimmed[5:])
|
||||
return "plan", remaining, true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(lower, "/act") {
|
||||
remaining := strings.TrimSpace(trimmed[4:])
|
||||
return "act", remaining, true
|
||||
}
|
||||
|
||||
return "", message, false
|
||||
}
|
||||
|
||||
@@ -19,20 +19,20 @@ import (
|
||||
// Handles localhost/127.0.0.1 equivalence by returning both forms.
|
||||
func normalizeAddressVariants(address string) []string {
|
||||
variants := []string{address}
|
||||
|
||||
|
||||
// Extract host and port
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return variants
|
||||
}
|
||||
|
||||
|
||||
// Add the alternate form for localhost/127.0.0.1
|
||||
if host == "localhost" {
|
||||
variants = append(variants, net.JoinHostPort("127.0.0.1", port))
|
||||
} else if host == "127.0.0.1" {
|
||||
variants = append(variants, net.JoinHostPort("localhost", port))
|
||||
}
|
||||
|
||||
|
||||
return variants
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ func (lm *LockManager) GetInstanceInfo(address string) (*common.CoreInstanceInfo
|
||||
|
||||
query := common.SelectInstanceLockByHolderSQL
|
||||
variants := normalizeAddressVariants(address)
|
||||
|
||||
|
||||
var heldBy, lockTarget string
|
||||
var lockedAt int64
|
||||
var lastErr error
|
||||
@@ -193,7 +193,7 @@ func (lm *LockManager) GetInstanceInfo(address string) (*common.CoreInstanceInfo
|
||||
if err == nil {
|
||||
// Found it!
|
||||
return &common.CoreInstanceInfo{
|
||||
Address: heldBy,
|
||||
CoreAddress: heldBy,
|
||||
HostServiceAddress: lockTarget,
|
||||
Status: grpc_health_v1.HealthCheckResponse_UNKNOWN,
|
||||
LastSeen: time.Unix(lockedAt/1000, 0),
|
||||
@@ -204,7 +204,7 @@ func (lm *LockManager) GetInstanceInfo(address string) (*common.CoreInstanceInfo
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// None of the variants were found
|
||||
if lastErr != nil {
|
||||
return nil, fmt.Errorf("failed to query instance: %w", lastErr)
|
||||
@@ -235,7 +235,7 @@ func (lm *LockManager) ListInstancesWithHealthCheck(ctx context.Context) ([]*com
|
||||
}
|
||||
|
||||
info := &common.CoreInstanceInfo{
|
||||
Address: lock.HeldBy,
|
||||
CoreAddress: lock.HeldBy,
|
||||
HostServiceAddress: lock.LockTarget,
|
||||
Status: status,
|
||||
LastSeen: time.Unix(lock.LockedAt/1000, 0),
|
||||
|
||||
+5
-10
@@ -80,7 +80,7 @@ func ensureTaskManager(ctx context.Context, address string) error {
|
||||
}
|
||||
|
||||
// Always set the instance we're using as the default
|
||||
registry := global.Clients.GetRegistry()
|
||||
registry := global.Instances.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)
|
||||
@@ -91,10 +91,10 @@ func ensureTaskManager(ctx context.Context, address string) error {
|
||||
|
||||
// ensureInstanceAtAddress ensures an instance exists at the given address
|
||||
func ensureInstanceAtAddress(ctx context.Context, address string) error {
|
||||
if global.Clients == nil {
|
||||
if global.Instances == nil {
|
||||
return fmt.Errorf("global clients not initialized")
|
||||
}
|
||||
return global.Clients.EnsureInstanceAtAddress(ctx, address)
|
||||
return global.Instances.EnsureInstanceAtAddress(ctx, address)
|
||||
}
|
||||
|
||||
func newTaskNewCommand() *cobra.Command {
|
||||
@@ -117,7 +117,7 @@ func newTaskNewCommand() *cobra.Command {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Check if an instance exists when no address specified
|
||||
if address == "" && global.Clients.GetRegistry().GetDefaultInstance() == "" {
|
||||
if address == "" && global.Instances.GetRegistry().GetDefaultInstance() == "" {
|
||||
fmt.Println("No instances available for creating tasks")
|
||||
return nil
|
||||
}
|
||||
@@ -230,7 +230,7 @@ func newTaskSendCommand() *cobra.Command {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Check if an instance exists when no address specified
|
||||
if address == "" && global.Clients.GetRegistry().GetDefaultInstance() == "" {
|
||||
if address == "" && global.Instances.GetRegistry().GetDefaultInstance() == "" {
|
||||
fmt.Println("No instances available for sending messages")
|
||||
return nil
|
||||
}
|
||||
@@ -620,11 +620,6 @@ func CleanupTaskManager() {
|
||||
}
|
||||
}
|
||||
|
||||
// NewTaskManagerForAddress is an exported wrapper around task.NewManagerForAddress
|
||||
func NewTaskManagerForAddress(ctx context.Context, address string) (*task.Manager, error) {
|
||||
return task.NewManagerForAddress(ctx, address)
|
||||
}
|
||||
|
||||
// CreateAndFollowTask creates a new task and immediately follows it in interactive mode
|
||||
// This is used by the root command to provide a streamlined UX
|
||||
func CreateAndFollowTask(ctx context.Context, prompt string, opts TaskOptions) error {
|
||||
|
||||
@@ -13,38 +13,41 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/output"
|
||||
"github.com/cline/cli/pkg/cli/slash"
|
||||
"github.com/cline/cli/pkg/cli/types"
|
||||
)
|
||||
|
||||
// InputHandler manages interactive user input during follow mode
|
||||
type InputHandler struct {
|
||||
manager *Manager
|
||||
coordinator *StreamCoordinator
|
||||
cancelFunc context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
isRunning bool
|
||||
pollTicker *time.Ticker
|
||||
program *tea.Program
|
||||
programRunning bool
|
||||
programDoneChan chan struct{} // Signals when program actually exits
|
||||
resultChan chan output.InputSubmitMsg
|
||||
cancelChan chan struct{}
|
||||
feedbackApproval bool // Track if we're in feedback after approval
|
||||
feedbackApproved bool // Track the approval decision
|
||||
approvalMessage *types.ClineMessage // Store the approval message for determining action
|
||||
ctx context.Context // Context for restart callback
|
||||
manager *Manager
|
||||
coordinator *StreamCoordinator
|
||||
cancelFunc context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
isRunning bool
|
||||
pollTicker *time.Ticker
|
||||
program *tea.Program
|
||||
programRunning bool
|
||||
programDoneChan chan struct{} // Signals when program actually exits
|
||||
resultChan chan output.InputSubmitMsg
|
||||
cancelChan chan struct{}
|
||||
feedbackApproval bool // Track if we're in feedback after approval
|
||||
feedbackApproved bool // Track the approval decision
|
||||
approvalMessage *types.ClineMessage // Store the approval message for determining action
|
||||
slashCommandRegistry *slash.Registry // Slash command registry for autocomplete
|
||||
ctx context.Context // Context for restart callback
|
||||
}
|
||||
|
||||
// NewInputHandler creates a new input handler
|
||||
func NewInputHandler(manager *Manager, coordinator *StreamCoordinator, cancelFunc context.CancelFunc) *InputHandler {
|
||||
return &InputHandler{
|
||||
manager: manager,
|
||||
coordinator: coordinator,
|
||||
cancelFunc: cancelFunc,
|
||||
isRunning: false,
|
||||
pollTicker: time.NewTicker(500 * time.Millisecond),
|
||||
resultChan: make(chan output.InputSubmitMsg, 1),
|
||||
cancelChan: make(chan struct{}, 1),
|
||||
manager: manager,
|
||||
coordinator: coordinator,
|
||||
cancelFunc: cancelFunc,
|
||||
isRunning: false,
|
||||
pollTicker: time.NewTicker(500 * time.Millisecond),
|
||||
resultChan: make(chan output.InputSubmitMsg, 1),
|
||||
slashCommandRegistry: slash.NewRegistry(context.Background()),
|
||||
cancelChan: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +166,7 @@ func (ih *InputHandler) Start(ctx context.Context, errChan chan error) {
|
||||
|
||||
if shouldSend {
|
||||
// Check for mode switch commands first
|
||||
newMode, remainingMessage, isModeSwitch := ih.parseModeSwitch(message)
|
||||
newMode, remainingMessage, isModeSwitch := slash.ParseModeSwitch(message)
|
||||
if isModeSwitch {
|
||||
// Create styles for mode switch messages (respect global color profile)
|
||||
actStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true)
|
||||
@@ -284,9 +287,9 @@ func (ih *InputHandler) promptForInput(ctx context.Context) (string, bool, error
|
||||
model := output.NewInputModelWithRegistry(
|
||||
output.InputTypeMessage,
|
||||
"Cline is ready for your message...",
|
||||
"/plan or /act to switch modes\nctrl+e to open editor\ntab to autocomplete commands",
|
||||
"/plan or /act to switch modes\ntab to autocomplete commands\nctrl+e to open editor\nctrl+c to exit",
|
||||
currentMode,
|
||||
ih.manager.GetSlashRegistry(),
|
||||
ih.slashCommandRegistry,
|
||||
)
|
||||
|
||||
return ih.runInputProgram(ctx, model)
|
||||
@@ -302,7 +305,7 @@ func (ih *InputHandler) promptForApproval(ctx context.Context, msg *types.ClineM
|
||||
"Let Cline use this tool?",
|
||||
"",
|
||||
ih.manager.GetCurrentMode(),
|
||||
ih.manager.GetSlashRegistry(), // Pass registry for feedback input after approval
|
||||
ih.slashCommandRegistry,
|
||||
)
|
||||
|
||||
message, shouldSend, err := ih.runInputProgram(ctx, model)
|
||||
@@ -478,24 +481,6 @@ func (w *inputProgramWrapper) View() string {
|
||||
return w.model.View()
|
||||
}
|
||||
|
||||
// parseModeSwitch checks if message starts with /act or /plan and extracts the mode and remaining message
|
||||
func (ih *InputHandler) parseModeSwitch(message string) (string, string, bool) {
|
||||
trimmed := strings.TrimSpace(message)
|
||||
lower := strings.ToLower(trimmed)
|
||||
|
||||
if strings.HasPrefix(lower, "/plan") {
|
||||
remaining := strings.TrimSpace(trimmed[5:])
|
||||
return "plan", remaining, true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(lower, "/act") {
|
||||
remaining := strings.TrimSpace(trimmed[4:])
|
||||
return "act", remaining, true
|
||||
}
|
||||
|
||||
return "", message, false
|
||||
}
|
||||
|
||||
// handleSpecialCommand processes special commands like /cancel, /exit
|
||||
func (ih *InputHandler) handleSpecialCommand(ctx context.Context, message string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(message)) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/cline/cli/pkg/cli/display"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/handlers"
|
||||
"github.com/cline/cli/pkg/cli/slash"
|
||||
"github.com/cline/cli/pkg/cli/types"
|
||||
"github.com/cline/grpc-go/client"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
@@ -38,7 +37,6 @@ type Manager struct {
|
||||
systemRenderer *display.SystemMessageRenderer
|
||||
streamingDisplay *display.StreamingDisplay
|
||||
handlerRegistry *handlers.HandlerRegistry
|
||||
slashRegistry *slash.Registry
|
||||
isStreamingMode bool
|
||||
isInteractive bool
|
||||
currentMode string // "plan" or "act"
|
||||
@@ -68,7 +66,6 @@ func NewManager(client *client.ClineClient) *Manager {
|
||||
systemRenderer: systemRenderer,
|
||||
streamingDisplay: streamingDisplay,
|
||||
handlerRegistry: registry,
|
||||
slashRegistry: slash.NewRegistry(),
|
||||
currentMode: "plan", // Default mode
|
||||
}
|
||||
}
|
||||
@@ -83,9 +80,6 @@ func NewManagerForAddress(ctx context.Context, address string) (*Manager, error)
|
||||
manager := NewManager(client)
|
||||
manager.clientAddress = address
|
||||
|
||||
// Fetch slash commands from backend (non-blocking, errors are logged)
|
||||
manager.fetchSlashCommands(ctx)
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
@@ -99,29 +93,13 @@ func NewManagerForDefault(ctx context.Context) (*Manager, error) {
|
||||
manager := NewManager(client)
|
||||
|
||||
// Get the default instance address
|
||||
if global.Clients != nil {
|
||||
manager.clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
|
||||
if global.Instances != nil {
|
||||
manager.clientAddress = global.Instances.GetRegistry().GetDefaultInstance()
|
||||
}
|
||||
|
||||
// Fetch slash commands from backend (non-blocking, errors are logged)
|
||||
manager.fetchSlashCommands(ctx)
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
// fetchSlashCommands fetches available slash commands from the backend
|
||||
// This is non-blocking and errors are logged but don't prevent manager creation
|
||||
func (m *Manager) fetchSlashCommands(ctx context.Context) {
|
||||
if err := m.slashRegistry.FetchFromBackend(ctx, m.client); err != nil {
|
||||
if global.Config.Verbose {
|
||||
m.renderer.RenderDebug("Failed to fetch slash commands: %v", err)
|
||||
}
|
||||
// Non-fatal: CLI-local commands are still available
|
||||
} else if global.Config.Verbose {
|
||||
m.renderer.RenderDebug("Loaded %d slash commands", len(m.slashRegistry.GetCommands()))
|
||||
}
|
||||
}
|
||||
|
||||
// SwitchToInstance switches the manager to use a different Cline instance
|
||||
func (m *Manager) SwitchToInstance(ctx context.Context, address string) error {
|
||||
m.mu.Lock()
|
||||
@@ -1206,11 +1184,11 @@ func (m *Manager) displayMessage(msg *types.ClineMessage, isLast, isPartial bool
|
||||
m.mu.RUnlock()
|
||||
|
||||
dc := &handlers.DisplayContext{
|
||||
State: m.state,
|
||||
Renderer: m.renderer,
|
||||
ToolRenderer: m.toolRenderer,
|
||||
HookRenderer: m.hookRenderer,
|
||||
SystemRenderer: m.systemRenderer,
|
||||
State: m.state,
|
||||
Renderer: m.renderer,
|
||||
ToolRenderer: m.toolRenderer,
|
||||
HookRenderer: m.hookRenderer,
|
||||
SystemRenderer: m.systemRenderer,
|
||||
IsLast: isLast,
|
||||
IsPartial: isPartial,
|
||||
Verbose: global.Config.Verbose,
|
||||
@@ -1320,11 +1298,6 @@ func (m *Manager) GetCurrentMode() string {
|
||||
return m.currentMode
|
||||
}
|
||||
|
||||
// GetSlashRegistry returns the slash command registry
|
||||
func (m *Manager) GetSlashRegistry() *slash.Registry {
|
||||
return m.slashRegistry
|
||||
}
|
||||
|
||||
// extractModeFromState extracts the current mode from state JSON
|
||||
func (m *Manager) extractModeFromState(stateJson string) string {
|
||||
var rawState map[string]interface{}
|
||||
|
||||
@@ -290,12 +290,6 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
|
||||
return err
|
||||
}
|
||||
settings.ActModeAwsBedrockCustomSelected = boolPtr(val)
|
||||
case "hooks_enabled":
|
||||
val, err := parseBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.HooksEnabled = boolPtr(val)
|
||||
case "azure_identity":
|
||||
val, err := parseBool(value)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
// This is the canonical definition used across all CLI packages
|
||||
type CoreInstanceInfo struct {
|
||||
// Full core address including port
|
||||
Address string `json:"address"`
|
||||
CoreAddress string `json:"address"`
|
||||
// Host bridge service address that core holds (host is ALWAYS running on localhost FYI)
|
||||
HostServiceAddress string `json:"host_port"`
|
||||
Status grpc_health_v1.HealthCheckResponse_ServingStatus `json:"status"`
|
||||
@@ -20,7 +20,7 @@ type CoreInstanceInfo struct {
|
||||
}
|
||||
|
||||
func (c *CoreInstanceInfo) CorePort() int {
|
||||
_, port, _ := ParseHostPort(c.Address)
|
||||
_, port, _ := ParseHostPort(c.CoreAddress)
|
||||
return port
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package hostbridge
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/cline/grpc-go/host"
|
||||
)
|
||||
|
||||
// WatchService implements the host.WatchServiceServer interface
|
||||
type WatchService struct {
|
||||
host.UnimplementedWatchServiceServer
|
||||
coreAddress string
|
||||
verbose bool
|
||||
}
|
||||
|
||||
// NewWatchService creates a new WatchService
|
||||
func NewWatchService(coreAddress string, verbose bool) *WatchService {
|
||||
return &WatchService{
|
||||
coreAddress: coreAddress,
|
||||
verbose: verbose,
|
||||
}
|
||||
}
|
||||
|
||||
// SubscribeToFile subscribes to file change notifications
|
||||
func (s *WatchService) SubscribeToFile(req *host.SubscribeToFileRequest, stream host.WatchService_SubscribeToFileServer) error {
|
||||
if s.verbose {
|
||||
log.Printf("SubscribeToFile called for path: %s", req.GetPath())
|
||||
}
|
||||
|
||||
// For console implementation, we'll just log that we would watch the file
|
||||
// In a real implementation, we'd use fsnotify or similar to watch file changes
|
||||
log.Printf("[Cline] Would watch file: %s", req.GetPath())
|
||||
|
||||
// Keep the stream open but don't send any events for now
|
||||
// In a real implementation, we'd send FileChangeEvent messages when files change
|
||||
<-stream.Context().Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/cline/grpc-go/cline"
|
||||
"github.com/cline/grpc-go/host"
|
||||
)
|
||||
|
||||
@@ -43,24 +44,25 @@ func (s *WorkspaceService) GetWorkspacePaths(ctx context.Context, req *host.GetW
|
||||
// SaveOpenDocumentIfDirty saves an open document if it has unsaved changes
|
||||
func (s *WorkspaceService) SaveOpenDocumentIfDirty(ctx context.Context, req *host.SaveOpenDocumentIfDirtyRequest) (*host.SaveOpenDocumentIfDirtyResponse, error) {
|
||||
if s.verbose {
|
||||
log.Printf("SaveOpenDocumentIfDirty called for path: %s", req.GetPath())
|
||||
log.Printf("SaveOpenDocumentIfDirty called for path: %v", req.FilePath)
|
||||
}
|
||||
|
||||
// For console implementation, we'll assume the document is already saved
|
||||
// In a real implementation, we'd check if the file has unsaved changes
|
||||
f := false
|
||||
return &host.SaveOpenDocumentIfDirtyResponse{
|
||||
WasSaved: false, // Assume no changes to save
|
||||
WasSaved: &f, // Assume no changes to save
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetDiagnostics returns diagnostic information for a file
|
||||
func (s *WorkspaceService) GetDiagnostics(ctx context.Context, req *host.GetDiagnosticsRequest) (*host.GetDiagnosticsResponse, error) {
|
||||
if s.verbose {
|
||||
log.Printf("GetDiagnostics called for path: %s", req.GetPath())
|
||||
log.Printf("GetDiagnostics called")
|
||||
}
|
||||
|
||||
// For console implementation, return empty diagnostics
|
||||
return &host.GetDiagnosticsResponse{
|
||||
Diagnostics: []*host.Diagnostic{},
|
||||
FileDiagnostics: []*cline.FileDiagnostics{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.2 MiB |
@@ -148,6 +148,7 @@
|
||||
"features/hooks/samples"
|
||||
]
|
||||
},
|
||||
"features/jupyter-notebooks",
|
||||
"features/multiroot-workspace",
|
||||
"features/plan-and-act",
|
||||
"features/skills",
|
||||
@@ -198,6 +199,7 @@
|
||||
"provider-config/anthropic",
|
||||
"provider-config/claude-code",
|
||||
"provider-config/openai",
|
||||
"provider-config/openai-codex",
|
||||
"provider-config/openrouter",
|
||||
"provider-config/cerebras",
|
||||
"provider-config/deepseek",
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: "Jupyter Notebooks"
|
||||
description: "AI-assisted editing of Jupyter notebooks with cell-level context awareness"
|
||||
---
|
||||
|
||||
# Jupyter Notebooks
|
||||
|
||||
Cline provides comprehensive support for Jupyter notebooks (`.ipynb` files), enabling AI-assisted editing with full cell-level context awareness. This feature was developed in collaboration with Amazon to bring AI coding assistance to data science workflows.
|
||||
|
||||
## Getting started
|
||||
|
||||
Jupyter notebook support is a built-in feature of Cline. To use it, you just need to have the Jupyter notebook extension enabled in VS Code. Once you open any `.ipynb` file, you'll see AI-assisted buttons in your notebook interface.
|
||||
|
||||
## How to use
|
||||
|
||||
### Generate Cell
|
||||
|
||||
Click the sparkle icon (✨) in the notebook toolbar to generate new cells with AI assistance.
|
||||
|
||||

|
||||
|
||||
**How it works:** The AI receives context from surrounding cells, so it understands the variables and imports already in scope. This means you can reference existing DataFrames, functions, and other objects without re-explaining them.
|
||||
|
||||
**Example prompt:** "Create a visualization showing the correlation matrix of numeric columns with a heatmap"
|
||||
|
||||
The cell is inserted with proper notebook JSON structure, preserving metadata and ready to execute.
|
||||
|
||||
### Explain Cell
|
||||
|
||||
Click the Explain button in any cell's title bar to get a detailed explanation of what the cell does.
|
||||
|
||||
This is useful for:
|
||||
|
||||
- Revisiting old notebooks
|
||||
- Onboarding to a teammate's analysis
|
||||
- Understanding complex transformations
|
||||
|
||||
**How it works:** Cline extracts the full cell context, including outputs, so explanations can reference actual results like column names, row counts, and computed values.
|
||||
|
||||
### Improve Cell
|
||||
|
||||
Click the Improve button in any cell's title bar to enhance existing cells with AI suggestions.
|
||||
|
||||

|
||||
|
||||
Use this to:
|
||||
|
||||
- Optimize slow pandas operations
|
||||
- Add error handling
|
||||
- Refactor for clarity
|
||||
- Convert loops to vectorized operations
|
||||
|
||||
**How it works:** Cline suggests improvements while preserving the cell's position and metadata in the notebook structure. The AI explains what was changed and why.
|
||||
|
||||
## How cell context works
|
||||
|
||||
Unlike traditional file editing, Jupyter notebooks are JSON documents containing arrays of cells. Each cell has its own type, source content, metadata, execution count, and outputs.
|
||||
|
||||
When you use a Jupyter command, Cline extracts structured context that includes:
|
||||
|
||||
- **Cell type** (code, markdown, or raw)
|
||||
- **Source content** as an array of lines
|
||||
- **Cell metadata** and configuration
|
||||
- **Execution count** for code cells
|
||||
- **Outputs** including data, text, and error traces
|
||||
|
||||
This structured representation allows the AI to understand not just the code, but its context within the notebook and its actual output.
|
||||
|
||||
### JSON structure preservation
|
||||
|
||||
Cline is designed to work carefully with the cell JSON structure, aiming to:
|
||||
|
||||
- Keep cell boundaries intact
|
||||
- Preserve execution counts
|
||||
- Maintain cell metadata
|
||||
- Keep outputs associated with their source cells
|
||||
|
||||
The AI is specifically prompted to preserve notebook structure, though you should always review changes to ensure your notebook format remains correct.
|
||||
|
||||
## Keyboard shortcuts
|
||||
|
||||
You can bind any of these commands to keyboard shortcuts for faster access:
|
||||
|
||||
1. Open VS Code keyboard shortcuts (Cmd/Ctrl + K, Cmd/Ctrl + S)
|
||||
2. Search for `cline.jupyterGenerateCell`, `cline.jupyterExplainCell`, or `cline.jupyterImproveCell`
|
||||
3. Assign your preferred key combinations
|
||||
|
||||
## Tips for best results
|
||||
|
||||
**For Generate Cell:**
|
||||
- Be specific about what you want the cell to do
|
||||
- Reference existing variables by name (the AI can see them)
|
||||
- Mention preferred libraries if you have a preference (e.g., "use seaborn" or "use plotly")
|
||||
|
||||
**For Explain Cell:**
|
||||
- Works best on cells that have been executed (outputs provide additional context)
|
||||
- Good for complex chained operations like pandas groupby/merge sequences
|
||||
|
||||
**For Improve Cell:**
|
||||
- Mention what aspect you want to improve (performance, readability, error handling)
|
||||
- The AI will explain the changes it suggests
|
||||
|
||||
## Limitations
|
||||
|
||||
- Notebook support requires the Jupyter notebook extension to be enabled in VS Code
|
||||
- Cell context extraction depends on VS Code's notebook API
|
||||
- Very large notebooks may require more context than some models can handle efficiently
|
||||
|
||||
## Related
|
||||
|
||||
- [Cline Tools Guide](/exploring-clines-tools/cline-tools-guide) -- Overview of all Cline commands
|
||||
- [Model Selection Guide](/core-features/model-selection-guide) -- Choosing the right model for your workflow
|
||||
@@ -25,11 +25,28 @@ The easiest way to get started is with **Cline** as your provider:
|
||||
|
||||
## Alternative: Use Another Provider
|
||||
|
||||
If you prefer to use your own API keys, you can select from providers like:
|
||||
If you prefer to use your own accounts or API keys, you have several options:
|
||||
|
||||
### Sign in with OpenAI (Recommended)
|
||||
|
||||
The easiest way to use OpenAI models is with **OpenAI Codex**—no API keys needed:
|
||||
|
||||
1. Select **"OpenAI Codex"** from the API Provider dropdown
|
||||
2. Click **"Sign in with OpenAI"**
|
||||
3. Authorize Cline in your browser
|
||||
4. Choose your model
|
||||
|
||||
<Card title="OpenAI Codex Setup Guide" icon="key" href="/provider-config/openai-codex">
|
||||
See the full setup guide with screenshots and troubleshooting tips.
|
||||
</Card>
|
||||
|
||||
### Other Providers
|
||||
|
||||
You can also use API keys with these providers:
|
||||
|
||||
- **OpenRouter** - Great value, multiple models
|
||||
- **Anthropic** - Direct access to Claude models
|
||||
- **OpenAI** - Access to GPT models
|
||||
- **OpenAI** - Access to GPT models via API key
|
||||
- **Google Gemini** - Google's AI models
|
||||
- **Ollama** - Run models locally on your computer
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ Cerebras delivers the world's fastest AI inference through their revolutionary w
|
||||
|
||||
Cline supports the following Cerebras models:
|
||||
|
||||
- `zai-glm-4.6` - Fast general-purpose model on Cerebras (up to 1,000 tokens/s). To be deprecated soon.
|
||||
- `zai-glm-4.7` - Highly capable general-purpose model on Cerebras (up to 1,000 tokens/s), competitive with leading proprietary models on coding tasks.
|
||||
- `qwen-3-235b-a22b-instruct-2507` - Advanced instruction-following model
|
||||
- `qwen-3-235b-a22b-thinking-2507` - Reasoning model with step-by-step thinking
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: "OpenAI Codex"
|
||||
sidebarTitle: "OpenAI Codex"
|
||||
description: "Connect your OpenAI account to Cline via OAuth for seamless access to OpenAI models."
|
||||
---
|
||||
|
||||
OpenAI Codex lets you connect your OpenAI account directly to Cline using OAuth, meaning you don't need to manage API keys. Simply sign in with your OpenAI account through a one-click browser authentication, and you'll automatically have access to all the models available on your OpenAI plan. No additional configuration required.
|
||||
|
||||
<Tip>
|
||||
If you prefer to use API keys instead, see the [OpenAI (API Key)](/provider-config/openai) provider configuration.
|
||||
</Tip>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An [OpenAI account](https://platform.openai.com/signup)
|
||||
- An active OpenAI subscription or API access plan
|
||||
|
||||
<Note>
|
||||
The models available to you depend on your OpenAI account's subscription tier. See [OpenAI's pricing page](https://openai.com/pricing) for details on what's included in each plan.
|
||||
</Note>
|
||||
|
||||
## Connecting Your OpenAI Account
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Cline Settings">
|
||||
Click the settings gear icon in the top-right corner of the Cline extension.
|
||||
</Step>
|
||||
<Step title="Select OpenAI Codex Provider">
|
||||
Choose **"OpenAI Codex"** from the "API Provider" dropdown menu.
|
||||
</Step>
|
||||
<Step title="Sign In with OpenAI">
|
||||
Click the **"Sign in with OpenAI"** button. This will open a browser window for authentication.
|
||||
</Step>
|
||||
<Step title="Authorize Cline">
|
||||
In the browser window that opens:
|
||||
1. Sign in to your OpenAI account (if not already signed in)
|
||||
2. Review the permissions Cline is requesting
|
||||
3. Click **"Authorize"** to grant access
|
||||
</Step>
|
||||
<Step title="Select Your Model">
|
||||
Once authorized, you'll be returned to Cline. Choose your desired model from the **"Model"** dropdown.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Note>
|
||||
If a model doesn't appear in your dropdown, it may not be available on your OpenAI subscription tier.
|
||||
</Note>
|
||||
|
||||
For the most current list of available models and their capabilities, see the official [OpenAI Models documentation](https://platform.openai.com/docs/models).
|
||||
|
||||
## Managing Your Connection
|
||||
|
||||
### Disconnecting Your Account
|
||||
|
||||
To disconnect your OpenAI account from Cline:
|
||||
|
||||
1. Open Cline Settings
|
||||
2. With OpenAI Codex selected as the provider, click **"Sign out"**
|
||||
3. Confirm the disconnection
|
||||
|
||||
This removes the OAuth connection. You can reconnect at any time by signing in again.
|
||||
|
||||
### Token Refresh
|
||||
|
||||
OAuth tokens are automatically refreshed by Cline. If you encounter authentication errors:
|
||||
|
||||
1. Try disconnecting and reconnecting your account
|
||||
2. Ensure your OpenAI account is in good standing
|
||||
3. Check that your subscription is active
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Authentication failed" error
|
||||
|
||||
- Ensure you're signing in with the correct OpenAI account
|
||||
- Check that your OpenAI subscription is active
|
||||
- Try clearing your browser cache and signing in again
|
||||
|
||||
### Models not appearing
|
||||
|
||||
- The available models depend on your OpenAI subscription tier
|
||||
- Some models may require specific plan upgrades
|
||||
- Check [OpenAI's pricing page](https://openai.com/pricing) for model availability by plan
|
||||
|
||||
### OAuth window doesn't open
|
||||
|
||||
- Check if pop-ups are blocked in your browser
|
||||
- Try using a different browser
|
||||
- Ensure you have a stable internet connection
|
||||
|
||||
### Connection keeps expiring
|
||||
|
||||
- This is rare but can happen if your OpenAI account session expired
|
||||
- Disconnect and reconnect to refresh your authentication
|
||||
|
||||
## Good to Know
|
||||
|
||||
Cline only requests the permissions necessary to make API calls on your behalf. Your OpenAI credentials are never stored by Cline.
|
||||
|
||||
When using OpenAI Codex, Cline accesses models through your OpenAI/ChatGPT subscription. There is no separate per-token API billing or consumption of OpenAI API credits for this provider; usage and limits are governed by your ChatGPT (or OpenAI account) subscription plan, not by Cline.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [OpenAI (API Key)](/provider-config/openai): Alternative configuration using API keys
|
||||
- [Model Selection Guide](/core-features/model-selection-guide): Help choosing the right model
|
||||
Generated
+48
-65
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.51.0",
|
||||
"version": "3.52.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.51.0",
|
||||
"version": "3.52.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
@@ -1184,6 +1184,7 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -2646,6 +2647,7 @@
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.9.15",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.8",
|
||||
"@types/node": ">=12.12.47"
|
||||
@@ -3229,6 +3231,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
|
||||
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -3297,6 +3300,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -4912,8 +4916,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4926,8 +4929,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4940,8 +4942,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4954,8 +4955,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4968,8 +4968,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4982,8 +4981,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -4996,8 +4994,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -5010,8 +5007,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5024,8 +5020,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5038,8 +5033,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5052,8 +5046,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5066,8 +5059,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5080,8 +5072,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5094,8 +5085,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5108,8 +5098,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5122,8 +5111,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5136,8 +5124,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -5150,8 +5137,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5164,8 +5150,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5178,8 +5163,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5192,8 +5176,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5206,8 +5189,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@sap-ai-sdk/ai-api": {
|
||||
"version": "2.1.0",
|
||||
@@ -6770,8 +6752,7 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/get-folder-size": {
|
||||
"version": "3.0.4",
|
||||
@@ -6803,6 +6784,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz",
|
||||
"integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -7519,6 +7501,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -8320,6 +8303,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -8654,9 +8638,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/undici": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
|
||||
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
@@ -9525,7 +9509,8 @@
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1342118",
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.2.0",
|
||||
@@ -12476,6 +12461,7 @@
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
@@ -12709,6 +12695,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
@@ -14860,9 +14847,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/open-graph-scraper/node_modules/undici": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
|
||||
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
@@ -15430,6 +15417,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15611,7 +15599,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -15632,7 +15619,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -16322,7 +16308,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
|
||||
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -17800,7 +17785,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
@@ -17817,7 +17801,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -18178,6 +18161,7 @@
|
||||
"version": "5.5.3",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -18250,9 +18234,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
|
||||
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.19.0.tgz",
|
||||
"integrity": "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
@@ -18430,7 +18414,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -18505,7 +18488,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -19193,6 +19175,7 @@
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.51.0",
|
||||
"version": "3.52.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
|
||||
@@ -108,7 +108,6 @@ message Secrets {
|
||||
message Settings {
|
||||
optional string lite_llm_base_url = 1;
|
||||
optional bool lite_llm_use_prompt_cache = 2;
|
||||
map<string, string> open_ai_headers = 3;
|
||||
optional string anthropic_base_url = 4;
|
||||
optional string open_router_provider_sorting = 5;
|
||||
optional string aws_region = 6;
|
||||
@@ -257,7 +256,6 @@ message Settings {
|
||||
optional FocusChainSettings focus_chain_settings = 149;
|
||||
optional string custom_prompt = 150;
|
||||
optional double auto_condense_threshold = 151;
|
||||
optional bool hooks_enabled = 152;
|
||||
optional bool subagents_enabled = 153;
|
||||
optional bool enable_parallel_tool_calling = 154;
|
||||
optional bool background_edit_enabled = 155;
|
||||
@@ -278,6 +276,7 @@ message Settings {
|
||||
optional int32 open_telemetry_log_batch_timeout = 170;
|
||||
optional int32 open_telemetry_log_max_queue_size = 171;
|
||||
optional bool worktrees_enabled = 172;
|
||||
map<string, string> open_ai_headers = 173;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
@@ -384,6 +383,8 @@ message UpdateTaskSettingsRequest {
|
||||
|
||||
// Message for updating settings
|
||||
message UpdateSettingsRequest {
|
||||
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
|
||||
|
||||
Metadata metadata = 1;
|
||||
optional ModelsApiConfiguration api_configuration = 2;
|
||||
optional string telemetry_setting = 3;
|
||||
@@ -408,7 +409,6 @@ message UpdateSettingsRequest {
|
||||
optional DictationSettings dictation_settings = 23;
|
||||
optional double auto_condense_threshold = 24;
|
||||
optional bool multi_root_enabled = 25;
|
||||
optional bool hooks_enabled = 26;
|
||||
optional string vscode_terminal_execution_mode = 27;
|
||||
optional int32 max_consecutive_mistakes = 28;
|
||||
optional bool subagents_enabled = 29;
|
||||
|
||||
@@ -70,6 +70,7 @@ enum ClineSay {
|
||||
HOOK_STATUS = 30;
|
||||
HOOK_OUTPUT_STREAM = 31;
|
||||
COMMAND_PERMISSION_DENIED = 32;
|
||||
CONDITIONAL_RULES_APPLIED = 33;
|
||||
}
|
||||
|
||||
// Enum for ClineSayTool tool types
|
||||
|
||||
@@ -105,7 +105,7 @@ async function main(): Promise<void> {
|
||||
console.log("Extracting standalone.zip to extensions directory...")
|
||||
try {
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
execSync(`unzip -q "${standaloneZipPath}" -d "${extensionsDir}"`, { stdio: "inherit" })
|
||||
execSync(`unzip -o -q "${standaloneZipPath}" -d "${extensionsDir}"`, { stdio: "inherit" })
|
||||
}
|
||||
console.log(`Successfully extracted standalone.zip to: ${extensionsDir}`)
|
||||
} catch (error) {
|
||||
|
||||
+7
-8
@@ -2,18 +2,18 @@ import * as vscode from "vscode"
|
||||
import {
|
||||
cleanupMcpMarketplaceCatalogFromGlobalState,
|
||||
migrateCustomInstructionsToGlobalRules,
|
||||
migrateHooksEnabledToBoolean,
|
||||
migrateTaskHistoryToFile,
|
||||
migrateWelcomeViewCompleted,
|
||||
migrateWorkspaceToGlobalStorage,
|
||||
} from "./core/storage/state-migrations"
|
||||
import { WebviewProvider } from "./core/webview"
|
||||
import { Logger } from "./services/logging/Logger"
|
||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import { StateManager } from "./core/storage/StateManager"
|
||||
import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
|
||||
import { ExtensionRegistryInfo } from "./registry"
|
||||
import { BannerService } from "./services/banner/BannerService"
|
||||
import { audioRecordingService } from "./services/dictation/AudioRecordingService"
|
||||
@@ -26,7 +26,6 @@ import { ShowMessageType } from "./shared/proto/host/window"
|
||||
import { syncWorker } from "./shared/services/worker/sync"
|
||||
import { getBlobStoreSettingsFromEnv } from "./shared/services/worker/worker"
|
||||
import { getLatestAnnouncementId } from "./utils/announcements"
|
||||
import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
|
||||
import { arePathsEqual } from "./utils/path"
|
||||
/**
|
||||
* Performs intialization for Cline that is common to all platforms.
|
||||
@@ -35,10 +34,13 @@ import { arePathsEqual } from "./utils/path"
|
||||
* @returns The webview provider
|
||||
*/
|
||||
export async function initialize(context: vscode.ExtensionContext): Promise<WebviewProvider> {
|
||||
// Configure the shared Logging class to use HostProvider's output channel
|
||||
Logger.setOutput((msg: string) => HostProvider.get().logToChannel(msg))
|
||||
|
||||
try {
|
||||
await StateManager.initialize(context)
|
||||
} catch (error) {
|
||||
console.error("[Controller] CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
|
||||
Logger.error("[Controller] CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to initialize Cline's application state. Please restart the extension.",
|
||||
@@ -70,9 +72,6 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
// Ensure taskHistory.json exists and migrate legacy state (runs once)
|
||||
await migrateTaskHistoryToFile(context)
|
||||
|
||||
// Migrate hooksEnabled from ClineFeatureSetting to boolean (one-time cleanup)
|
||||
await migrateHooksEnabledToBoolean(context)
|
||||
|
||||
// Clean up MCP marketplace catalog from global state (moved to disk cache)
|
||||
await cleanupMcpMarketplaceCatalogFromGlobalState(context)
|
||||
|
||||
@@ -127,7 +126,7 @@ async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
|
||||
Logger.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ class ClineEndpoint {
|
||||
this.environment = Environment.production
|
||||
break
|
||||
}
|
||||
console.info("Cline environment updated: ", this.environment)
|
||||
}
|
||||
|
||||
public getEnvironment(): EnvironmentConfig {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ApiConfiguration, ModelInfo, QwenApiRegions } from "@shared/api"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ClineTool } from "@/shared/tools"
|
||||
import { AIhubmixHandler } from "./providers/aihubmix"
|
||||
import { AnthropicHandler } from "./providers/anthropic"
|
||||
@@ -480,7 +481,7 @@ export function buildApiHandler(configuration: ApiConfiguration, mode: Mode): Ap
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("buildApiHandler error:", error)
|
||||
Logger.error("buildApiHandler error:", error)
|
||||
}
|
||||
|
||||
return createHandlerForProvider(apiProvider, options, mode)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AskSageModelId, askSageDefaultModelId, askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -58,7 +59,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
private apiKey: string
|
||||
|
||||
constructor(options: AskSageHandlerOptions) {
|
||||
console.log("init api url", options.asksageApiUrl, askSageDefaultURL)
|
||||
Logger.log("init api url", options.asksageApiUrl, askSageDefaultURL)
|
||||
this.options = options
|
||||
this.apiKey = options.asksageApiKey || ""
|
||||
this.apiUrl = options.asksageApiUrl || askSageDefaultURL
|
||||
@@ -164,7 +165,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch AskSage usage", await response.text())
|
||||
Logger.error("Failed to fetch AskSage usage", await response.text())
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -177,7 +178,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
outputTokens: 0,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching AskSage usage:", error)
|
||||
Logger.error("Error fetching AskSage usage:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { BedrockModelId, bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_1M_
|
||||
import { calculateApiCostOpenAI, calculateApiCostQwen } from "@utils/cost"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
@@ -449,7 +450,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing Deepseek response chunk:", error)
|
||||
Logger.error("Error parsing Deepseek response chunk:", error)
|
||||
// Propagate the error by yielding a text response with error information
|
||||
yield {
|
||||
type: "text",
|
||||
@@ -550,7 +551,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
|
||||
for await (const chunk of response.stream) {
|
||||
// Debug logging to see actual response structure
|
||||
// console.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
|
||||
// Logger.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
|
||||
|
||||
// Handle thinking response in additionalModelResponseFields (LangChain format)
|
||||
const metadata = chunk.metadata as ExtendedMetadata | undefined
|
||||
@@ -854,7 +855,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Log unsupported content types for debugging
|
||||
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
Logger.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
return null
|
||||
})
|
||||
.filter((item): item is ContentBlock => item !== null)
|
||||
@@ -912,7 +913,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to process image content:", error)
|
||||
Logger.error("Failed to process image content:", error)
|
||||
// Return a text content indicating the error instead of null
|
||||
// This ensures users are aware of the issue
|
||||
return {
|
||||
@@ -1111,7 +1112,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error with OpenAI model via Converse API:", error)
|
||||
Logger.error("Error with OpenAI model via Converse API:", error)
|
||||
|
||||
// Try to extract more detailed error information
|
||||
let errorMessage = "Failed to process OpenAI model request"
|
||||
@@ -1246,7 +1247,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error with Qwen model via Converse API:", error)
|
||||
Logger.error("Error with Qwen model via Converse API:", error)
|
||||
|
||||
// Try to extract more detailed error information
|
||||
let errorMessage = "Failed to process Qwen model request"
|
||||
|
||||
@@ -7,10 +7,10 @@ import { ClineEnv } from "@/config"
|
||||
import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { buildClineExtraHeaders } from "@/services/EnvUtils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch, getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
@@ -126,7 +126,7 @@ export class ClineHandler implements ApiHandler {
|
||||
// openrouter returns an error object instead of the openai sdk throwing an error
|
||||
if ("error" in chunk) {
|
||||
const error = chunk.error as OpenRouterErrorResponse["error"]
|
||||
console.error(`Cline API Error: ${error?.code} - ${error?.message}`)
|
||||
Logger.error(`Cline API Error: ${error?.code} - ${error?.message}`)
|
||||
// Include metadata in the error message if available
|
||||
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
|
||||
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
|
||||
@@ -143,7 +143,7 @@ export class ClineHandler implements ApiHandler {
|
||||
const choiceWithError = choice as any
|
||||
if (choiceWithError.error) {
|
||||
const error = choiceWithError.error
|
||||
console.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
Logger.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
} else {
|
||||
throw new Error(
|
||||
@@ -217,14 +217,14 @@ export class ClineHandler implements ApiHandler {
|
||||
|
||||
// Fallback to generation endpoint if usage chunk not returned
|
||||
if (!didOutputUsage) {
|
||||
console.warn("Cline API did not return usage chunk, fetching from generation endpoint")
|
||||
Logger.warn("Cline API did not return usage chunk, fetching from generation endpoint")
|
||||
const apiStreamUsage = await this.getApiStreamUsage()
|
||||
if (apiStreamUsage) {
|
||||
yield apiStreamUsage
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Cline API Error:", error)
|
||||
Logger.error("Cline API Error:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -260,7 +260,7 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching cline generation details:", error)
|
||||
Logger.error("Error fetching cline generation details:", error)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ModelInfo } from "../../../shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -84,7 +85,7 @@ export class DifyHandler implements ApiHandler {
|
||||
this.apiKey = options.difyApiKey || ""
|
||||
this.baseUrl = options.difyBaseUrl || ""
|
||||
|
||||
console.log("[DIFY DEBUG] Constructor called with:", {
|
||||
Logger.log("[DIFY DEBUG] Constructor called with:", {
|
||||
hasApiKey: !!this.apiKey,
|
||||
baseUrl: this.baseUrl,
|
||||
})
|
||||
@@ -98,7 +99,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
|
||||
console.log("[DIFY DEBUG] createMessage called with:", {
|
||||
Logger.log("[DIFY DEBUG] createMessage called with:", {
|
||||
systemPromptLength: systemPrompt?.length || 0,
|
||||
messagesCount: messages?.length || 0,
|
||||
})
|
||||
@@ -115,8 +116,8 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const fullUrl = `${this.baseUrl}/chat-messages`
|
||||
console.log("[DIFY DEBUG] Making request to:", fullUrl)
|
||||
console.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
|
||||
Logger.log("[DIFY DEBUG] Making request to:", fullUrl)
|
||||
Logger.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
|
||||
|
||||
let response: Response
|
||||
try {
|
||||
@@ -129,21 +130,21 @@ export class DifyHandler implements ApiHandler {
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error("[DIFY DEBUG] Network error during fetch:", error)
|
||||
Logger.error("[DIFY DEBUG] Network error during fetch:", error)
|
||||
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
|
||||
throw new Error(`Dify API network error: ${error.message}${cause}`)
|
||||
}
|
||||
|
||||
console.log("[DIFY DEBUG] Response status:", response.status)
|
||||
Logger.log("[DIFY DEBUG] Response status:", response.status)
|
||||
const headersObj: Record<string, string> = {}
|
||||
response.headers.forEach((value, key) => {
|
||||
headersObj[key] = value
|
||||
})
|
||||
console.log("[DIFY DEBUG] Response headers:", headersObj)
|
||||
Logger.log("[DIFY DEBUG] Response headers:", headersObj)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error("[DIFY DEBUG] Error response:", errorText)
|
||||
Logger.error("[DIFY DEBUG] Error response:", errorText)
|
||||
throw new Error(`Dify API error: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
@@ -159,14 +160,14 @@ export class DifyHandler implements ApiHandler {
|
||||
const processedEvents: string[] = []
|
||||
let lastEventTime = Date.now()
|
||||
|
||||
console.log("[DIFY DEBUG] Starting to read streaming response...")
|
||||
Logger.log("[DIFY DEBUG] Starting to read streaming response...")
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
console.log("[DIFY DEBUG] Stream ended naturally")
|
||||
console.log(
|
||||
Logger.log("[DIFY DEBUG] Stream ended naturally")
|
||||
Logger.log(
|
||||
"[DIFY DEBUG] Final state - hasYieldedContent:",
|
||||
hasYieldedContent,
|
||||
"fullText length:",
|
||||
@@ -178,7 +179,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
console.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
|
||||
Logger.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
|
||||
|
||||
buffer += chunk
|
||||
const lines = buffer.split("\n")
|
||||
@@ -187,41 +188,41 @@ export class DifyHandler implements ApiHandler {
|
||||
buffer = lines.pop() || ""
|
||||
|
||||
for (const line of lines) {
|
||||
console.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
|
||||
Logger.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
|
||||
|
||||
if (line.startsWith("data: ")) {
|
||||
const data = line.slice(6).trim()
|
||||
console.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
|
||||
Logger.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
|
||||
|
||||
if (data === "[DONE]") {
|
||||
console.log("[DIFY DEBUG] Received [DONE] signal")
|
||||
Logger.log("[DIFY DEBUG] Received [DONE] signal")
|
||||
break
|
||||
}
|
||||
|
||||
if (data === "") {
|
||||
console.log("[DIFY DEBUG] Empty data line, skipping")
|
||||
Logger.log("[DIFY DEBUG] Empty data line, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
console.log("[DIFY DEBUG] Parsed JSON:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Parsed JSON:", parsed)
|
||||
processedEvents.push(parsed.event || "unknown")
|
||||
lastEventTime = Date.now()
|
||||
|
||||
// Capture conversation_id as soon as it's available
|
||||
if (parsed.conversation_id && !this.conversationId) {
|
||||
this.conversationId = parsed.conversation_id
|
||||
console.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
|
||||
Logger.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
|
||||
}
|
||||
|
||||
// Handle different Dify event types based on actual Dify API
|
||||
if (parsed.event === "message") {
|
||||
console.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
|
||||
Logger.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
|
||||
// Dify sends the full text in each "answer" chunk, so we replace.
|
||||
if (typeof parsed.answer === "string") {
|
||||
fullText = parsed.answer
|
||||
console.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
|
||||
Logger.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
@@ -229,10 +230,10 @@ export class DifyHandler implements ApiHandler {
|
||||
hasYieldedContent = true
|
||||
}
|
||||
} else if (parsed.event === "message_replace") {
|
||||
console.log("[DIFY DEBUG] Replace message event:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Replace message event:", parsed)
|
||||
if (parsed.answer) {
|
||||
fullText = parsed.answer // Replace instead of append
|
||||
console.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
|
||||
Logger.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
@@ -240,7 +241,7 @@ export class DifyHandler implements ApiHandler {
|
||||
hasYieldedContent = true
|
||||
}
|
||||
} else if (parsed.event === "message_end") {
|
||||
console.log("[DIFY DEBUG] Message end event", parsed)
|
||||
Logger.log("[DIFY DEBUG] Message end event", parsed)
|
||||
// Message completed. Yield final text if we have any.
|
||||
if (fullText) {
|
||||
yield {
|
||||
@@ -260,19 +261,19 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
return // End of stream
|
||||
} else if (parsed.event === "error") {
|
||||
console.error("[DIFY DEBUG] Error event:", parsed)
|
||||
Logger.error("[DIFY DEBUG] Error event:", parsed)
|
||||
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
|
||||
} else if (parsed.event === "workflow_started" || parsed.event === "workflow_finished") {
|
||||
console.log("[DIFY DEBUG] Workflow event:", parsed.event)
|
||||
Logger.log("[DIFY DEBUG] Workflow event:", parsed.event)
|
||||
// These are informational events, continue processing
|
||||
} else if (parsed.event === "node_started" || parsed.event === "node_finished") {
|
||||
console.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
|
||||
Logger.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
|
||||
// These are informational events, continue processing
|
||||
} else if (parsed.event === "ping") {
|
||||
console.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
|
||||
Logger.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
|
||||
// Ping event, do nothing
|
||||
} else {
|
||||
console.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
|
||||
// Try to extract text from other possible fields
|
||||
if (parsed.text) {
|
||||
fullText += parsed.text
|
||||
@@ -299,17 +300,17 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
|
||||
Logger.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
|
||||
}
|
||||
} else if (line.trim() !== "") {
|
||||
console.log(
|
||||
Logger.log(
|
||||
"[DIFY DEBUG] Non-data line (not starting with 'data:'), trying to parse as direct JSON:",
|
||||
JSON.stringify(line),
|
||||
)
|
||||
// Try to parse as direct JSON (fallback for non-SSE responses, though Dify uses SSE)
|
||||
try {
|
||||
const parsed = JSON.parse(line.trim())
|
||||
console.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
|
||||
processedEvents.push(parsed.event || "direct-json")
|
||||
|
||||
// Handle the same event types as above
|
||||
@@ -330,7 +331,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
return
|
||||
} else if (parsed.event === "error") {
|
||||
console.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
|
||||
Logger.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
|
||||
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
|
||||
} else if (parsed.answer || parsed.text || parsed.content) {
|
||||
// Fallback for any content in direct JSON
|
||||
@@ -344,7 +345,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, continue
|
||||
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
|
||||
Logger.log("[DIFY DEBUG] Line is not direct JSON, continuing")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,11 +360,11 @@ export class DifyHandler implements ApiHandler {
|
||||
streamDuration: Date.now() - lastEventTime,
|
||||
conversationId: this.conversationId,
|
||||
}
|
||||
console.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
|
||||
Logger.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
|
||||
|
||||
// If we have any accumulated text at all, yield it as a fallback
|
||||
if (fullText.trim()) {
|
||||
console.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
|
||||
Logger.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
@@ -380,7 +381,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
console.log("[DIFY DEBUG] Stream reader released")
|
||||
Logger.log("[DIFY DEBUG] Stream reader released")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +400,7 @@ export class DifyHandler implements ApiHandler {
|
||||
|
||||
// Only prepend the system prompt if it's the very first message of a new conversation.
|
||||
if (!this.conversationId && systemPrompt) {
|
||||
console.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
|
||||
Logger.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
|
||||
return `${systemPrompt}\n\n---\n\n${userQuery}`
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { GeminiModelId, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { RetriableError, withRetry } from "../retry"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
@@ -332,7 +333,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
throughputTokensPerSec: throughputTokensPerSecSdk,
|
||||
})
|
||||
} else {
|
||||
console.warn("GeminiHandler: ulid not available for telemetry in createMessage.")
|
||||
Logger.warn("GeminiHandler: ulid not available for telemetry in createMessage.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -408,7 +409,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
trace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens ?? 0, cost: cacheReadCost }
|
||||
}
|
||||
|
||||
// console.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
|
||||
// Logger.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
|
||||
return totalCost
|
||||
}
|
||||
|
||||
@@ -450,13 +451,13 @@ export class GeminiHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
if (response.totalTokens === undefined) {
|
||||
console.warn("Gemini token counting returned undefined, using fallback")
|
||||
Logger.warn("Gemini token counting returned undefined, using fallback")
|
||||
return this.estimateTokens(content)
|
||||
}
|
||||
|
||||
return response.totalTokens
|
||||
} catch (error) {
|
||||
console.warn("Gemini token counting failed, using fallback", error)
|
||||
Logger.warn("Gemini token counting failed, using fallback", error)
|
||||
return this.estimateTokens(content)
|
||||
}
|
||||
}
|
||||
@@ -475,7 +476,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
const jsonStr = JSON.stringify(block)
|
||||
return total + jsonStr.length
|
||||
} catch (e) {
|
||||
console.warn("Failed to stringify block for token estimation", e)
|
||||
Logger.warn("Failed to stringify block for token estimation", e)
|
||||
return total
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import OpenAI from "openai"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isAnthropicModelId } from "@/utils/model-utils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
@@ -70,7 +71,7 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
const data: LiteLlmModelInfoResponse = await response.json()
|
||||
return data
|
||||
} else {
|
||||
console.error("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
Logger.error("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
// Try with Authorization header instead
|
||||
const retryResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
@@ -84,12 +85,12 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
const data: LiteLlmModelInfoResponse = await retryResponse.json()
|
||||
return data
|
||||
} else {
|
||||
console.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
Logger.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
throw new Error(`Failed to fetch LiteLLM model info: ${retryResponse.statusText}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching LiteLLM model info:", error)
|
||||
Logger.error("Error fetching LiteLLM model info:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -169,7 +170,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error getting LiteLLM model cost info:", error)
|
||||
Logger.warn("Error getting LiteLLM model cost info:", error)
|
||||
}
|
||||
|
||||
// Fallback to zero costs if we can't get the information
|
||||
@@ -200,7 +201,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
|
||||
return totalCost
|
||||
} catch (error) {
|
||||
console.error("Error calculating spend:", error)
|
||||
Logger.error("Error calculating spend:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
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 { OcaModelInfo } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiFormat } from "@/shared/proto/index.cline"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, type CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -134,11 +134,11 @@ export class OcaHandler implements ApiHandler {
|
||||
const data: { cost: number } = await response.json()
|
||||
return data.cost
|
||||
} else {
|
||||
console.error("Error calculating spend:", response.statusText)
|
||||
Logger.error("Error calculating spend:", response.statusText)
|
||||
return undefined
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error calculating spend:", error)
|
||||
Logger.error("Error calculating spend:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import { type Config, type Message, Ollama } from "ollama"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOllamaMessages } from "../transform/ollama-format"
|
||||
@@ -93,7 +94,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (streamError: any) {
|
||||
console.error("Error processing Ollama stream:", streamError)
|
||||
Logger.error("Error processing Ollama stream:", streamError)
|
||||
throw new Error(`Ollama stream processing error: ${streamError.message || "Unknown error"}`)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -107,7 +108,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
const statusCode = error.status || error.statusCode
|
||||
const errorMessage = error.message || "Unknown error"
|
||||
|
||||
console.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
|
||||
Logger.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import * as os from "os"
|
||||
import { v7 as uuidv7 } from "uuid"
|
||||
import { ModelInfo, OpenAiCodexModelId, openAiCodexDefaultModelId, openAiCodexModels } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import {
|
||||
ModelInfo,
|
||||
OpenAiCodexModelId,
|
||||
openAiCodexDefaultModelId,
|
||||
openAiCodexModels,
|
||||
} from "@shared/api"
|
||||
import * as os from "os"
|
||||
import { v7 as uuidv7 } from "uuid"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { convertToOpenAIResponsesInput } from "../transform/openai-response-format"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
@@ -53,7 +48,9 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
private normalizeUsage(usage: any, model: { id: string; info: ModelInfo }): ApiStreamUsageChunk | undefined {
|
||||
if (!usage) return undefined
|
||||
if (!usage) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const inputDetails = usage.input_tokens_details ?? usage.prompt_tokens_details
|
||||
|
||||
@@ -90,11 +87,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
return out
|
||||
}
|
||||
|
||||
async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: ClineStorageMessage[],
|
||||
tools?: ChatCompletionTool[],
|
||||
): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
|
||||
// Reset state for this request
|
||||
@@ -104,9 +97,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
// Get access token from OAuth manager
|
||||
let accessToken = await openAiCodexOAuthManager.getAccessToken()
|
||||
if (!accessToken) {
|
||||
throw new Error(
|
||||
"Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow in settings.",
|
||||
)
|
||||
throw new Error("Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow in settings.")
|
||||
}
|
||||
|
||||
// Format conversation for Responses API
|
||||
@@ -179,11 +170,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
return body
|
||||
}
|
||||
|
||||
private async *executeRequest(
|
||||
requestBody: any,
|
||||
model: { id: string; info: ModelInfo },
|
||||
accessToken: string,
|
||||
): ApiStream {
|
||||
private async *executeRequest(requestBody: any, model: { id: string; info: ModelInfo }, accessToken: string): ApiStream {
|
||||
// Create AbortController for cancellation
|
||||
this.abortController = new AbortController()
|
||||
|
||||
@@ -237,11 +224,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async *makeCodexRequest(
|
||||
requestBody: any,
|
||||
model: { id: string; info: ModelInfo },
|
||||
accessToken: string,
|
||||
): ApiStream {
|
||||
private async *makeCodexRequest(requestBody: any, model: { id: string; info: ModelInfo }, accessToken: string): ApiStream {
|
||||
const url = `${CODEX_API_BASE_URL}/responses`
|
||||
|
||||
// Get ChatGPT account ID for organization subscriptions
|
||||
@@ -302,10 +285,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async *handleStreamResponse(
|
||||
body: ReadableStream<Uint8Array>,
|
||||
model: { id: string; info: ModelInfo },
|
||||
): ApiStream {
|
||||
private async *handleStreamResponse(body: ReadableStream<Uint8Array>, model: { id: string; info: ModelInfo }): ApiStream {
|
||||
const reader = body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ""
|
||||
@@ -317,7 +297,9 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split("\n")
|
||||
@@ -380,10 +362,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Handle tool/function call deltas
|
||||
if (
|
||||
event?.type === "response.tool_call_arguments.delta" ||
|
||||
event?.type === "response.function_call_arguments.delta"
|
||||
) {
|
||||
if (event?.type === "response.tool_call_arguments.delta" || event?.type === "response.function_call_arguments.delta") {
|
||||
const callId = event.call_id || event.tool_call_id || event.id || this.pendingToolCallId
|
||||
const name = event.name || event.function_name || this.pendingToolCallName
|
||||
const args = event.delta || event.arguments
|
||||
@@ -484,10 +463,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
getModel(): { id: OpenAiCodexModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
|
||||
const id =
|
||||
modelId && modelId in openAiCodexModels
|
||||
? (modelId as OpenAiCodexModelId)
|
||||
: openAiCodexDefaultModelId
|
||||
const id = modelId && modelId in openAiCodexModels ? (modelId as OpenAiCodexModelId) : openAiCodexDefaultModelId
|
||||
|
||||
const info: ModelInfo = openAiCodexModels[id]
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiFormat } from "@/shared/proto/cline/models"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isGPT5ModelFamily } from "@/utils/model-utils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
@@ -135,7 +135,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
try {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
} catch (error) {
|
||||
console.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
Logger.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,11 +316,11 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
chunk.response?.status === "incomplete" &&
|
||||
chunk.response?.incomplete_details?.reason === "max_output_tokens"
|
||||
) {
|
||||
console.log("Ran out of tokens")
|
||||
Logger.log("Ran out of tokens")
|
||||
if (chunk.response?.output_text?.length > 0) {
|
||||
console.log("Partial output:", chunk.response.output_text)
|
||||
Logger.log("Partial output:", chunk.response.output_text)
|
||||
} else {
|
||||
console.log("Ran out of tokens during reasoning")
|
||||
Logger.log("Ran out of tokens during reasoning")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch, getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
@@ -80,7 +81,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
// Check for error field directly on chunk
|
||||
if ("error" in chunk) {
|
||||
const error = chunk.error as OpenRouterErrorResponse["error"]
|
||||
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
|
||||
Logger.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
|
||||
// Include metadata in the error message if available
|
||||
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
|
||||
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
|
||||
@@ -95,7 +96,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
const choiceWithError = choice as any
|
||||
if (choiceWithError.error) {
|
||||
const error = choiceWithError.error
|
||||
console.error(
|
||||
Logger.error(
|
||||
`OpenRouter Mid-Stream Error: ${error?.code || "Unknown"} - ${error?.message || "Unknown error"}`,
|
||||
)
|
||||
// Format error details
|
||||
@@ -179,7 +180,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
try {
|
||||
const generationIterator = this.fetchGenerationDetails(this.lastGenerationId)
|
||||
const generation = (await generationIterator.next()).value
|
||||
// console.log("OpenRouter generation details:", generation)
|
||||
// Logger.log("OpenRouter generation details:", generation)
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
@@ -191,7 +192,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching OpenRouter generation details:", error)
|
||||
Logger.error("Error fetching OpenRouter generation details:", error)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
@@ -199,7 +200,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
|
||||
@withRetry({ maxRetries: 4, baseDelay: 250, maxDelay: 1000, retryAllErrors: true })
|
||||
async *fetchGenerationDetails(genId: string) {
|
||||
// console.log("Fetching generation details for:", genId)
|
||||
// Logger.log("Fetching generation details for:", genId)
|
||||
try {
|
||||
const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, {
|
||||
headers: {
|
||||
@@ -211,7 +212,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
yield response.data?.data
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching OpenRouter generation details:", error)
|
||||
Logger.error("Error fetching OpenRouter generation details:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -76,7 +77,7 @@ export class QwenCodeHandler implements ApiHandler {
|
||||
const credsStr = await fs.readFile(keyFile, "utf-8")
|
||||
return JSON.parse(credsStr)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
Logger.error(
|
||||
`Error reading or parsing credentials file at ${getQwenCachedCredentialPath(this.options.qwenCodeOauthPath)}`,
|
||||
)
|
||||
throw new Error(`Failed to load Qwen OAuth credentials: ${error}`)
|
||||
|
||||
@@ -12,6 +12,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -134,7 +135,7 @@ export class QwenHandler implements ApiHandler {
|
||||
try {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
} catch (error) {
|
||||
console.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
Logger.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import JSON5 from "json5"
|
||||
import OpenAI from "openai"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -144,7 +145,7 @@ namespace Bedrock {
|
||||
}
|
||||
|
||||
// Log unsupported content types for debugging
|
||||
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
Logger.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
return null
|
||||
})
|
||||
.filter((item): item is BedrockContentBlock => item !== null)
|
||||
@@ -213,7 +214,7 @@ namespace Bedrock {
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to process image content:", error)
|
||||
Logger.error("Failed to process image content:", error)
|
||||
// Return a text content indicating the error instead of null
|
||||
// This ensures users are aware of the issue
|
||||
return {
|
||||
@@ -412,7 +413,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
url: destination.url || this.options.sapAiCoreBaseUrl!,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create AI Core destination:", error)
|
||||
Logger.error("Failed to create AI Core destination:", error)
|
||||
throw new Error(`Unable to create AI Core destination: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
@@ -473,7 +474,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
})
|
||||
.filter((deployment: any) => deployment !== null)
|
||||
} catch (error) {
|
||||
console.error("Error fetching deployments:", error)
|
||||
Logger.error("Error fetching deployments:", error)
|
||||
throw new Error("Failed to fetch deployments")
|
||||
}
|
||||
}
|
||||
@@ -577,7 +578,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in SAP orchestration mode:", error)
|
||||
Logger.error("Error in SAP orchestration mode:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -596,7 +597,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
|
||||
if (!deploymentId) {
|
||||
// Fallback to runtime deployment id fetching for users who haven't opened the SAP provider UI
|
||||
console.log(`No pre-configured deployment ID found for model ${model.id}, falling back to runtime fetching`)
|
||||
Logger.log(`No pre-configured deployment ID found for model ${model.id}, falling back to runtime fetching`)
|
||||
deploymentId = await this.getDeploymentForModel(model.id)
|
||||
}
|
||||
|
||||
@@ -800,8 +801,8 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.error("Error status:", error.response.status)
|
||||
console.error("Error headers:", error.response.headers)
|
||||
Logger.error("Error status:", error.response.status)
|
||||
Logger.error("Error headers:", error.response.headers)
|
||||
|
||||
// Handle error data - need to read stream if responseType was 'stream'
|
||||
let errorMessage = "Unknown error"
|
||||
@@ -830,10 +831,10 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
} else if (typeof error.response.data === "object") {
|
||||
errorMessage = JSON.stringify(error.response.data, null, 2)
|
||||
}
|
||||
console.error("Error data:", errorMessage)
|
||||
Logger.error("Error data:", errorMessage)
|
||||
} catch (e) {
|
||||
console.error("Failed to read error data:", e)
|
||||
console.error("Raw error data:", error.response.data)
|
||||
Logger.error("Failed to read error data:", e)
|
||||
Logger.error("Raw error data:", error.response.data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -846,11 +847,11 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
throw new Error(`HTTP ${error.response.status}: ${errorMessage}`)
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
console.error("Error request:", error.request)
|
||||
Logger.error("Error request:", error.request)
|
||||
throw new Error("No response received from server")
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.error("Error message:", error.message)
|
||||
Logger.error("Error message:", error.message)
|
||||
throw new Error(`Error setting up request: ${error.message}`)
|
||||
}
|
||||
}
|
||||
@@ -898,13 +899,13 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse JSON data:", error)
|
||||
Logger.error("Failed to parse JSON data:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming completion:", error)
|
||||
Logger.error("Error streaming completion:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -965,7 +966,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse JSON data:", error)
|
||||
Logger.error("Failed to parse JSON data:", error)
|
||||
yield {
|
||||
type: "text",
|
||||
text: `[ERROR] Failed to parse response data: ${error instanceof Error ? error.message : String(error)}`,
|
||||
@@ -975,7 +976,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming completion:", error)
|
||||
Logger.error("Error streaming completion:", error)
|
||||
yield {
|
||||
type: "text",
|
||||
text: `[ERROR] Failed to process stream: ${error instanceof Error ? error.message : String(error)}`,
|
||||
@@ -1044,13 +1045,13 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse GPT JSON data:", error)
|
||||
Logger.error("Failed to parse GPT JSON data:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming GPT completion:", error)
|
||||
Logger.error("Error streaming GPT completion:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1109,13 +1110,13 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse Gemini JSON data:", error)
|
||||
Logger.error("Failed to parse Gemini JSON data:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming Gemini completion:", error)
|
||||
Logger.error("Error streaming Gemini completion:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -125,11 +126,11 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
if (!didOutputUsage) {
|
||||
console.warn("Vercel AI Gateway did not provide usage information in stream")
|
||||
Logger.warn("Vercel AI Gateway did not provide usage information in stream")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Vercel AI Gateway error details:", error)
|
||||
console.error("Error stack:", error.stack)
|
||||
Logger.error("Vercel AI Gateway error details:", error)
|
||||
Logger.error("Error stack:", error.stack)
|
||||
throw new Error(`Vercel AI Gateway error: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCo
|
||||
import { calculateApiCostAnthropic } from "@utils/cost"
|
||||
import * as vscode from "vscode"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions, SingleCompletionHandler } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -123,7 +124,7 @@ declare module "vscode" {
|
||||
* const systemPrompt = "You are a helpful assistant";
|
||||
* const messages = [{ role: "user", content: "Hello!" }];
|
||||
* for await (const chunk of handler.createMessage(systemPrompt, messages)) {
|
||||
* console.log(chunk);
|
||||
* Logger.log(chunk);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@@ -147,7 +148,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
this.client = null
|
||||
this.ensureCleanState()
|
||||
} catch (error) {
|
||||
console.error("Error during configuration change cleanup:", error)
|
||||
Logger.error("Error during configuration change cleanup:", error)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -279,7 +280,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
|
||||
private async getClient(): Promise<vscode.LanguageModelChat> {
|
||||
if (!this.client) {
|
||||
console.debug("Cline <Language Model API>: Getting client with options:", {
|
||||
Logger.debug("Cline <Language Model API>: Getting client with options:", {
|
||||
vsCodeLmModelSelector: this.options.vsCodeLmModelSelector,
|
||||
hasOptions: !!this.options,
|
||||
selectorKeys: this.options.vsCodeLmModelSelector ? Object.keys(this.options.vsCodeLmModelSelector) : [],
|
||||
@@ -288,11 +289,11 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
try {
|
||||
// Use default empty selector if none provided to get all available models
|
||||
const selector = this.options?.vsCodeLmModelSelector || {}
|
||||
console.debug("Cline <Language Model API>: Creating client with selector:", selector)
|
||||
Logger.debug("Cline <Language Model API>: Creating client with selector:", selector)
|
||||
this.client = await this.createClient(selector)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Cline <Language Model API>: Client creation failed:", message)
|
||||
Logger.error("Cline <Language Model API>: Client creation failed:", message)
|
||||
throw new Error(`Cline <Language Model API>: Failed to create client: ${message}`)
|
||||
}
|
||||
}
|
||||
@@ -413,7 +414,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
if (chunk instanceof vscode.LanguageModelTextPart) {
|
||||
// Validate text part value
|
||||
if (typeof chunk.value !== "string") {
|
||||
console.warn("Cline <Language Model API>: Invalid text part value received:", chunk.value)
|
||||
Logger.warn("Cline <Language Model API>: Invalid text part value received:", chunk.value)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -426,18 +427,18 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
try {
|
||||
// Validate tool call parameters
|
||||
if (!chunk.name || typeof chunk.name !== "string") {
|
||||
console.warn("Cline <Language Model API>: Invalid tool name received:", chunk.name)
|
||||
Logger.warn("Cline <Language Model API>: Invalid tool name received:", chunk.name)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!chunk.callId || typeof chunk.callId !== "string") {
|
||||
console.warn("Cline <Language Model API>: Invalid tool callId received:", chunk.callId)
|
||||
Logger.warn("Cline <Language Model API>: Invalid tool callId received:", chunk.callId)
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure input is a valid object
|
||||
if (!chunk.input || typeof chunk.input !== "object") {
|
||||
console.warn("Cline <Language Model API>: Invalid tool input received:", chunk.input)
|
||||
Logger.warn("Cline <Language Model API>: Invalid tool input received:", chunk.input)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -453,7 +454,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
accumulatedText += toolCallText
|
||||
|
||||
// Log tool call for debugging
|
||||
console.debug("Cline <Language Model API>: Processing tool call:", {
|
||||
Logger.debug("Cline <Language Model API>: Processing tool call:", {
|
||||
name: chunk.name,
|
||||
callId: chunk.callId,
|
||||
inputSize: JSON.stringify(chunk.input).length,
|
||||
@@ -464,10 +465,10 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
text: toolCallText,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Cline <Language Model API>: Failed to process tool call:", error)
|
||||
Logger.error("Cline <Language Model API>: Failed to process tool call:", error)
|
||||
}
|
||||
} else {
|
||||
console.warn("Cline <Language Model API>: Unknown chunk type received:", chunk)
|
||||
Logger.warn("Cline <Language Model API>: Unknown chunk type received:", chunk)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,7 +490,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
console.error("Cline <Language Model API>: Stream error details:", {
|
||||
Logger.error("Cline <Language Model API>: Stream error details:", {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name,
|
||||
@@ -500,12 +501,12 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
} else if (typeof error === "object" && error !== null) {
|
||||
// Handle error-like objects
|
||||
const errorDetails = JSON.stringify(error, null, 2)
|
||||
console.error("Cline <Language Model API>: Stream error object:", errorDetails)
|
||||
Logger.error("Cline <Language Model API>: Stream error object:", errorDetails)
|
||||
throw new Error(`Cline <Language Model API>: Response stream error: ${errorDetails}`)
|
||||
} else {
|
||||
// Fallback for unknown error types
|
||||
const errorMessage = String(error)
|
||||
console.error("Cline <Language Model API>: Unknown stream error:", errorMessage)
|
||||
Logger.error("Cline <Language Model API>: Unknown stream error:", errorMessage)
|
||||
throw new Error(`Cline <Language Model API>: Response stream error: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
@@ -526,7 +527,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
// Log any missing properties for debugging
|
||||
for (const [prop, value] of Object.entries(requiredProps)) {
|
||||
if (!value && value !== 0) {
|
||||
console.warn(`Cline <Language Model API>: Client missing ${prop} property`)
|
||||
Logger.warn(`Cline <Language Model API>: Client missing ${prop} property`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,7 +558,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
? stringifyVsCodeLmModelSelector(this.options.vsCodeLmModelSelector)
|
||||
: "vscode-lm"
|
||||
|
||||
console.debug("Cline <Language Model API>: No client available, using fallback model info")
|
||||
Logger.debug("Cline <Language Model API>: No client available, using fallback model info")
|
||||
|
||||
return {
|
||||
id: fallbackId,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
interface RetryOptions {
|
||||
maxRetries?: number
|
||||
baseDelay?: number
|
||||
@@ -72,7 +74,7 @@ export function withRetry(options: RetryOptions = {}) {
|
||||
try {
|
||||
await handlerInstance.options.onRetryAttempt(attempt + 1, maxRetries, delay, error)
|
||||
} catch (e) {
|
||||
console.error("Error in onRetryAttempt callback:", e)
|
||||
Logger.error("Error in onRetryAttempt callback:", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
const o1SystemPrompt = (systemPrompt: string) => `
|
||||
# System Prompt
|
||||
@@ -317,7 +318,7 @@ function parseToolCall(toolName: string, content: string): ToolCall | null {
|
||||
|
||||
// Validate required parameters
|
||||
if (!validateToolInput(toolName, tool_input)) {
|
||||
console.error(`Invalid tool call for ${toolName}:`, content)
|
||||
Logger.error(`Invalid tool call for ${toolName}:`, content)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -358,8 +359,8 @@ function validateToolInput(toolName: string, tool_input: Record<string, string>)
|
||||
// </write_to_file>`;
|
||||
//
|
||||
// const { normalText, toolCalls } = parseAIResponse(aiResponse);
|
||||
// console.log(normalText);
|
||||
// console.log(toolCalls);
|
||||
// Logger.log(normalText);
|
||||
// Logger.log(toolCalls);
|
||||
|
||||
// Convert OpenAI response to Anthropic format
|
||||
export function convertO1ResponseToAnthropicMessage(
|
||||
@@ -432,4 +433,4 @@ export function convertO1ResponseToAnthropicMessage(
|
||||
// usage: { prompt_tokens: 50, completion_tokens: 100 }
|
||||
// };
|
||||
// const anthropicMessage = convertO1ResponseToAnthropicMessage(openAICompletion);
|
||||
// console.log(anthropicMessage);
|
||||
// Logger.log(anthropicMessage);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ClineTextContentBlock,
|
||||
ClineUserToolResultContentBlock,
|
||||
} from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
// OpenAI API has a maximum tool call ID length of 40 characters
|
||||
const MAX_TOOL_CALL_ID_LENGTH = 40
|
||||
@@ -424,7 +425,7 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
|
||||
try {
|
||||
parsedInput = JSON.parse(toolCall.function?.arguments || "{}")
|
||||
} catch (error) {
|
||||
console.error("Failed to parse tool arguments:", error)
|
||||
Logger.error("Failed to parse tool arguments:", error)
|
||||
}
|
||||
return {
|
||||
type: "tool_use",
|
||||
@@ -439,7 +440,7 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
|
||||
return anthropicMessage
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error converting OpenAI message to Anthropic format:", error)
|
||||
Logger.error("Error converting OpenAI message to Anthropic format:", error)
|
||||
}
|
||||
|
||||
return anthropicMessage
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
ChatCompletionToolChoiceOption,
|
||||
ChatCompletionTool as OpenAITool,
|
||||
} from "openai/resources/chat/completions"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { ApiStreamToolCallsChunk } from "./stream"
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import * as vscode from "vscode"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
/**
|
||||
* Safely converts a value into a plain object.
|
||||
@@ -23,7 +24,7 @@ export function asObjectSafe(value: any): object {
|
||||
|
||||
return {}
|
||||
} catch (error) {
|
||||
console.warn("Cline <Language Model API>: Failed to parse object:", error)
|
||||
Logger.warn("Cline <Language Model API>: Failed to parse object:", error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import OpenAI from "openai"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { ModelInfo } from "@/shared/api"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
// Type that represents the OpenAI ResponseStream with its private properties
|
||||
// The #private property issue can be resolved by using the AsyncIterable interface
|
||||
@@ -143,11 +143,11 @@ export async function* handleResponsesApiStreamResponse(
|
||||
chunk.response?.status === "incomplete" &&
|
||||
chunk.response?.incomplete_details?.reason === "max_output_tokens"
|
||||
) {
|
||||
console.log("Ran out of tokens")
|
||||
Logger.log("Ran out of tokens")
|
||||
if (chunk.response?.output_text?.length > 0) {
|
||||
console.log("Partial output:", chunk.response.output_text)
|
||||
Logger.log("Partial output:", chunk.response.output_text)
|
||||
} else {
|
||||
console.log("Ran out of tokens during reasoning")
|
||||
Logger.log("Ran out of tokens during reasoning")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import { describe, it } from "mocha"
|
||||
import { constructNewFileContent as cnfc } from "./diff"
|
||||
|
||||
async function cnfc2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
|
||||
return cnfc(diffContent, originalContent, isFinal, "v2")
|
||||
const result = await cnfc(diffContent, originalContent, isFinal, "v2")
|
||||
return result.newContent
|
||||
}
|
||||
|
||||
describe("constructNewFileContent", () => {
|
||||
@@ -217,13 +218,13 @@ replaced
|
||||
} else {
|
||||
const result1 = await cnfc(diff, original, isFinal ?? true)
|
||||
const result2 = await cnfc2(diff, original, isFinal ?? true)
|
||||
const _equal = result1 === result2
|
||||
const _equal2 = result1 === expected
|
||||
const _equal = result1.newContent === result2
|
||||
const _equal2 = result1.newContent === expected
|
||||
// Verify both implementations produce same result
|
||||
expect(result1).to.equal(result2)
|
||||
expect(result1.newContent).to.equal(result2)
|
||||
|
||||
// Verify result matches expected
|
||||
expect(result1).to.equal(expected)
|
||||
expect(result1.newContent).to.equal(expected)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -264,7 +265,7 @@ replaced`
|
||||
// Should still work and replace line2 with "replaced"
|
||||
const expected = "line1\nreplaced\nline3"
|
||||
|
||||
expect(result1).to.equal(expected)
|
||||
expect(result1.newContent).to.equal(expected)
|
||||
})
|
||||
|
||||
it("should handle missing final REPLACE marker with multiple lines of replacement", async () => {
|
||||
@@ -281,7 +282,7 @@ replaced`
|
||||
const result1 = await cnfc(diff, original, true) // isFinal = true
|
||||
const expected = "function test() {\n\tconst a = 42;\n\tconsole.log('updated');\n\treturn a;\n}"
|
||||
|
||||
expect(result1).to.equal(expected)
|
||||
expect(result1.newContent).to.equal(expected)
|
||||
})
|
||||
|
||||
// it("should NOT process incomplete replacement when isFinal is false", async () => {
|
||||
@@ -319,7 +320,7 @@ new second
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const expectedResult = "first\nnew second\nthird\nnew fourth\n"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
it("should handle multiple out-of-order replacements", async () => {
|
||||
@@ -342,7 +343,7 @@ fifth
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const expectedResult = "one\nsecond\nthree\nfourth\nfifth\n"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
it("should handle out-of-order replacements with indentation", async () => {
|
||||
@@ -360,7 +361,7 @@ fifth
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const expectedResult = "function test() {\n\tconst a = 10;\n\tconst b = 2;\n\tconst c = 30;\n\n}"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
it("should handle out-of-order replacements with empty lines", async () => {
|
||||
@@ -380,6 +381,6 @@ new body content
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const expectedResult = "header\nnew body content\nnew footer\n"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,17 @@ const SEARCH_BLOCK_START = "------- SEARCH"
|
||||
const SEARCH_BLOCK_END = "======="
|
||||
const REPLACE_BLOCK_END = "+++++++ REPLACE"
|
||||
|
||||
/**
|
||||
* Converts a character index in a string to a 1-based line number.
|
||||
* @param content - The full content string
|
||||
* @param charIndex - The character index in the content
|
||||
* @returns The 1-based line number where charIndex falls
|
||||
*/
|
||||
export function getLineNumberFromCharIndex(content: string, charIndex: number): number {
|
||||
if (charIndex <= 0) return 1
|
||||
return content.substring(0, charIndex).split("\n").length
|
||||
}
|
||||
|
||||
const SEARCH_BLOCK_CHAR = "-"
|
||||
const REPLACE_BLOCK_CHAR = "+"
|
||||
const LEGACY_SEARCH_BLOCK_CHAR = "<"
|
||||
@@ -236,7 +247,7 @@ export async function constructNewFileContent(
|
||||
originalContent: string,
|
||||
isFinal: boolean,
|
||||
version: "v1" | "v2" = "v1",
|
||||
): Promise<string> {
|
||||
): Promise<{ newContent: string; matchIndices: number[] }> {
|
||||
const constructor = constructNewFileContentVersionMapping[version]
|
||||
if (!constructor) {
|
||||
throw new Error(`Invalid version '${version}' for file content constructor`)
|
||||
@@ -246,13 +257,17 @@ export async function constructNewFileContent(
|
||||
|
||||
const constructNewFileContentVersionMapping: Record<
|
||||
string,
|
||||
(diffContent: string, originalContent: string, isFinal: boolean) => Promise<string>
|
||||
(diffContent: string, originalContent: string, isFinal: boolean) => Promise<{ newContent: string; matchIndices: number[] }>
|
||||
> = {
|
||||
v1: constructNewFileContentV1,
|
||||
v2: constructNewFileContentV2,
|
||||
} as const
|
||||
|
||||
async function constructNewFileContentV1(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
|
||||
async function constructNewFileContentV1(
|
||||
diffContent: string,
|
||||
originalContent: string,
|
||||
isFinal: boolean,
|
||||
): Promise<{ newContent: string; matchIndices: number[] }> {
|
||||
let result = ""
|
||||
let lastProcessedIndex = 0
|
||||
|
||||
@@ -470,7 +485,9 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
|
||||
result += originalContent.slice(currentPos)
|
||||
}
|
||||
|
||||
return result
|
||||
// Return all match indices from the replacements
|
||||
// This is used to determine the line numbers for each SEARCH/REPLACE block in the UI
|
||||
return { newContent: result, matchIndices: replacements.map((r) => r.start) }
|
||||
}
|
||||
|
||||
enum ProcessingState {
|
||||
@@ -566,7 +583,7 @@ class NewFileContentConstructor {
|
||||
this.internalProcessLine(line, true, this.pendingNonStandardLines.length)
|
||||
}
|
||||
|
||||
public getResult() {
|
||||
public getResult(): { newContent: string; matchIndices: number[] } {
|
||||
// If this is the final chunk, append any remaining original content
|
||||
if (this.isFinal && this.lastProcessedIndex < this.originalContent.length) {
|
||||
this.result += this.originalContent.slice(this.lastProcessedIndex)
|
||||
@@ -574,7 +591,10 @@ class NewFileContentConstructor {
|
||||
if (this.isFinal && this.state !== ProcessingState.Idle) {
|
||||
throw new Error("File processing incomplete - SEARCH/REPLACE operations still active during finalization")
|
||||
}
|
||||
return this.result
|
||||
// Note: V2 implementation doesn't currently track match indices
|
||||
// For now, return empty array. If V2 becomes the default and we need line numbers,
|
||||
// we should add state to track all match indices.
|
||||
return { newContent: this.result, matchIndices: [] }
|
||||
}
|
||||
|
||||
private internalProcessLine(
|
||||
@@ -800,7 +820,11 @@ class NewFileContentConstructor {
|
||||
}
|
||||
}
|
||||
|
||||
export async function constructNewFileContentV2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
|
||||
export async function constructNewFileContentV2(
|
||||
diffContent: string,
|
||||
originalContent: string,
|
||||
isFinal: boolean,
|
||||
): Promise<{ newContent: string; matchIndices: number[] }> {
|
||||
const newFileContentConstructor = new NewFileContentConstructor(originalContent, isFinal)
|
||||
|
||||
const lines = diffContent.split("\n")
|
||||
|
||||
@@ -3,7 +3,8 @@ import { describe, it } from "mocha"
|
||||
import { constructNewFileContent as cnfc } from "./diff"
|
||||
|
||||
async function cnfc2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
|
||||
return cnfc(diffContent, originalContent, isFinal, "v2")
|
||||
const result = await cnfc(diffContent, originalContent, isFinal, "v2")
|
||||
return result.newContent
|
||||
}
|
||||
|
||||
describe("Diff Format Edge Cases", () => {
|
||||
@@ -18,7 +19,7 @@ new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nnew content\nafter"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
@@ -33,7 +34,7 @@ new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nnew content\nafter"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
@@ -48,7 +49,7 @@ new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nnew content\nafter"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
@@ -62,7 +63,7 @@ new content
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
expect(result1).to.equal("before\nnew content\nafter")
|
||||
expect(result1.newContent).to.equal("before\nnew content\nafter")
|
||||
expect(result2).to.equal("before\nnew content\nafter")
|
||||
})
|
||||
|
||||
@@ -77,7 +78,7 @@ new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nnew content\nafter"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
@@ -92,7 +93,7 @@ new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nnew content\nafter"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
@@ -112,7 +113,7 @@ second new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nfirst new content\nafter\nsecond new content\nend"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
@@ -132,7 +133,7 @@ second new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nfirst new content\nafter\nsecond new content\nend"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ShowMessageType } from "@shared/proto/host/window"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import * as path from "path"
|
||||
import { ulid } from "ulid"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
interface TaskReconstructionResult {
|
||||
totalTasks: number
|
||||
@@ -143,7 +144,7 @@ async function backupExistingTaskHistory(): Promise<void> {
|
||||
}
|
||||
} catch (error) {
|
||||
// Non-fatal error, just log it
|
||||
console.warn("Failed to backup existing task history:", error)
|
||||
Logger.warn("Failed to backup existing task history:", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user