Compare commits

..

4 Commits

Author SHA1 Message Date
Tony Loehr 58bc26bd01 Merge branch 'main' into worktree-docs 2026-01-20 18:58:49 -08:00
Tony Loehr 120c912ff3 Merge branch 'main' into worktree-docs 2026-01-19 16:35:32 -08:00
Tony Loehr 39224edaf3 Merge branch 'main' into worktree-docs 2026-01-19 16:29:28 -08:00
Tony Loehr a98253516c worktree docs 2026-01-19 16:24:54 -08:00
469 changed files with 4240 additions and 4518 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add claude 4.5 haiku
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
improve cline command permission validation logic. add cline command permission man page documentation
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: finalize document content during approval flow
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Expose --version in cline cli command
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Log Persistence errors to PostHog
+13
View File
@@ -0,0 +1,13 @@
---
"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
@@ -0,0 +1,9 @@
---
"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.
+9
View File
@@ -0,0 +1,9 @@
---
"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
-7
View File
@@ -1,7 +0,0 @@
---
"claude-dev": patch
---
fix(extract-text): strip notebook outputs to reduce context size
Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing the amount of context sent to the LLM while preserving the essential code and markdown content.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
OpenAI GPT-5 Codex models are now using Apply Patch tool for diff edits.
+5
View File
@@ -0,0 +1,5 @@
---
"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.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: add chat output on skill use
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Adding telemetry for background exec terminal
+5
View File
@@ -0,0 +1,5 @@
---
"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.
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
Remove deprecated zai-glm-4.6 model from Cerebras provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Wiring-up "conditionals" for Cline Rules files.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: removes retry message from UI after retry succeeds
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Support native tool calling for LM Studio and Ollama provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
add claude 4.5 opus into sap provider.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix drag & drop files from SSH remote workspaces into chat
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Limite Vertex and LiteLLM options when they're remote configured
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix crash when the Context Menu has a type but no options
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Updated switch component to adhere to 3:1 contrast accessibility standard for both light and dark vscode themes
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Throttle the remote config fetch
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Improve history view filter menu
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
Add git worktree management UI for running parallel Cline sessions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Remove hooks feature setting and make it always enabled (except on Windows).
+12 -3
View File
@@ -187,11 +187,20 @@ 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 Standalone
run: npm run compile-standalone
- name: Compile NPM package
run: npm run compile-standalone-npm
- name: Install testing platform dependencies
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
@@ -204,7 +213,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
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage || true
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
+2 -15
View File
@@ -1,23 +1,10 @@
# 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]
@@ -1750,4 +1737,4 @@ Add Opus 4.1 through Claude Code
## [0.0.6]
- Initial release
- Initial release
+15
View File
@@ -137,6 +137,21 @@ For example, when working with a local web server, you can use 'Restore Workspac
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
### Worktrees: Parallel Development
Worktrees let you work on multiple branches simultaneously, each in its own folder. This enables Cline to work on tasks in parallel across separate VS Code windows, or lets Cline work independently while you continue coding in your main workspace.
Cline takes over your VS Code window while working on a task. With worktrees, you can:
- **Run Cline in parallel** - Have Cline work on multiple tasks simultaneously
- **Keep working while Cline works** - Let Cline handle a task in a separate worktree while you continue coding
- **Isolate experimental changes** - Test risky changes in a worktree without affecting your main branch
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## Contributing
To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.md) to learn the basics. You can also join our [Discord](https://discord.gg/cline) to chat with other contributors in the `#contributors` channel. If you're looking for full-time work, check out our open positions on our [careers page](https://cline.bot/join-us)!
-23
View File
@@ -147,29 +147,6 @@
"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": [
"**",
+86 -75
View File
@@ -10,12 +10,11 @@ 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"
@@ -72,6 +71,8 @@ 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
@@ -88,13 +89,13 @@ see the manual page: man cline`,
if global.Config.Verbose {
fmt.Println("Starting new Cline instance...")
}
instance, err := global.Instances.StartNewInstance(ctx, allWorkspaces...)
instance, err := global.Clients.StartNewInstance(ctx, allWorkspaces...)
if err != nil {
return fmt.Errorf("failed to start new instance: %w", err)
}
global.Config.CoreAddress = instance.CoreAddress
instanceAddress = instance.Address
if global.Config.Verbose {
fmt.Printf("Started instance at %s\n\n", global.Config.CoreAddress)
fmt.Printf("Started instance at %s\n\n", instanceAddress)
}
// Set up cleanup on exit
@@ -102,35 +103,38 @@ see the manual page: man cline`,
if global.Config.Verbose {
fmt.Println("\nCleaning up instance...")
}
registry := global.Instances.GetRegistry()
if err := global.KillInstanceByAddress(context.Background(), registry, global.Config.CoreAddress); err != nil {
registry := global.Clients.GetRegistry()
if err := global.KillInstanceByAddress(context.Background(), registry, instanceAddress); 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) {
// 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, 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"))
if err := auth.HandleAuthMenuNoArgs(ctx); err != nil {
// Check if user cancelled - exit cleanly
if err == huh.ErrUserAborted {
return nil
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)
}
return fmt.Errorf("auth setup failed: %w", err)
}
// Re-check after auth wizard
if !isUserReadyToUse(ctx) {
return fmt.Errorf("credentials still not configured - please run 'cline auth' to complete setup")
}
// 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"))
fmt.Printf("\n%s\n\n", renderer.Dim("✓ Setup complete, you can now use the Cline CLI"))
}
} else {
// User specified --address flag, use that
instanceAddress = coreAddress
}
// Get content from both args and stdin
@@ -139,13 +143,10 @@ see the manual page: man cline`,
return fmt.Errorf("failed to read prompt: %w", err)
}
// 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 == "" {
// If no prompt from args or stdin, show interactive input
if prompt == "" {
// Pass the mode flag and workspaces to banner so it shows correct info
prompt, err = promptForInitialTask(ctx, mode, allWorkspaces, !bannerShown)
bannerShown = true
prompt, err = promptForInitialTask(ctx, instanceAddress, mode, allWorkspaces)
if err != nil {
// Check if user cancelled - exit cleanly without error
if err == huh.ErrUserAborted {
@@ -153,23 +154,6 @@ 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")
}
@@ -187,7 +171,7 @@ see the manual page: man cline`,
Mode: mode,
Settings: settings,
Yolo: yolo,
Address: global.Config.CoreAddress,
Address: instanceAddress,
Verbose: verbose,
Workspaces: allWorkspaces,
})
@@ -223,30 +207,51 @@ see the manual page: man cline`,
}
}
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)
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
}
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),
)
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()
if err != nil {
if err == output.ErrUserAborted {
// 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
return "", huh.ErrUserAborted
}
return "", err
}
return prompt, nil
return strings.TrimSpace(prompt), nil
}
// showSessionBanner displays session info before initial prompt
func showSessionBanner(ctx context.Context, modeFlag string, workspaces []string) {
func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string, workspaces []string) {
bannerInfo := display.BannerInfo{
Version: global.CliVersion,
Mode: modeFlag, // Use the mode from command flag, not state
@@ -260,18 +265,21 @@ func showSessionBanner(ctx context.Context, modeFlag string, workspaces []string
bannerInfo.Workdirs = workspaces
// Get provider/model using auth functions (same logic as auth menu)
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
}
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 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
}
}
}
@@ -284,22 +292,25 @@ func showSessionBanner(ctx context.Context, modeFlag string, workspaces []string
// 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) bool {
grpcClient, err := global.GetClientForAddress(ctx, global.Config.CoreAddress)
func isUserReadyToUse(ctx context.Context, instanceAddress string) bool {
manager, err := cli.NewTaskManagerForAddress(ctx, instanceAddress)
if err != nil {
return false
}
state, err := grpcClient.State.GetLatestState(ctx, &cline.EmptyRequest{})
// Get state
state, err := manager.GetClient().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
}
+10 -10
View File
@@ -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].CoreAddress
firstAddr := out1.CoreInstances[0].Address
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.CoreAddress, defaultTimeout)
waitForAddressHealthy(t, target.Address, defaultTimeout)
// Set as default
_ = mustRunCLI(ctx, t, "instance", "use", target.CoreAddress)
_ = mustRunCLI(ctx, t, "instance", "use", target.Address)
// Verify default switched
out = listInstancesJSON(ctx, t)
if out.DefaultInstance != target.CoreAddress {
t.Fatalf("default_instance not updated to %s (got %s)", target.CoreAddress, out.DefaultInstance)
if out.DefaultInstance != target.Address {
t.Fatalf("default_instance not updated to %s (got %s)", target.Address, out.DefaultInstance)
}
// Kill the default instance using runtime PID discovery
corePID := getCorePID(t, target.CoreAddress)
corePID := getCorePID(t, target.Address)
if corePID <= 0 {
t.Fatalf("could not find PID for core process at %s", target.CoreAddress)
t.Fatalf("could not find PID for core process at %s", target.Address)
}
t.Logf("Killing cline-core process PID %d for instance %s", corePID, target.CoreAddress)
t.Logf("Killing cline-core process PID %d for instance %s", corePID, target.Address)
if err := syscall.Kill(corePID, syscall.SIGKILL); err != nil {
t.Fatalf("kill pid %d: %v", corePID, err)
}
// Wait for removal
waitForAddressRemoved(t, target.CoreAddress, longTimeout)
waitForAddressRemoved(t, target.Address, 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.CoreAddress {
if out.DefaultInstance == it.Address {
found = true
break
}
+3 -3
View File
@@ -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.CoreAddress == addr {
if it.Address == 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.CoreAddress == addr {
if it.Address == 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.Instances.GetRegistry().GetClient(ctx, address)
client, err := global.Clients.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)
+10 -10
View File
@@ -26,7 +26,7 @@ func TestMixedLocalhostVs127Coexist(t *testing.T) {
t.Fatalf("expected at least 1 instance")
}
inst := out.CoreInstances[0]
waitForAddressHealthy(t, inst.CoreAddress, defaultTimeout)
waitForAddressHealthy(t, inst.Address, 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.CoreAddress, defaultTimeout)
waitForAddressHealthy(t, inst.Address, defaultTimeout)
waitForAddressHealthy(t, addr127, defaultTimeout)
out = listInstancesJSON(ctx, t)
if !hasAddress(out, inst.CoreAddress) || !hasAddress(out, addr127) {
t.Fatalf("expected both %s and %s present", inst.CoreAddress, addr127)
if !hasAddress(out, inst.Address) || !hasAddress(out, addr127) {
t.Fatalf("expected both %s and %s present", inst.Address, 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.CoreAddress] = struct{}{}
beforeSet[it.Address] = 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.CoreAddress]; !ok {
newAddr = it.CoreAddress
if _, ok := beforeSet[it.Address]; !ok {
newAddr = it.Address
return true, ""
}
}
@@ -88,12 +88,12 @@ func TestStartStopStress(t *testing.T) {
}
// Get PID using runtime discovery
corePID := getCorePID(t, info.CoreAddress)
corePID := getCorePID(t, info.Address)
if corePID <= 0 {
t.Fatalf("could not find PID for new instance at %s", info.CoreAddress)
t.Fatalf("could not find PID for new instance at %s", info.Address)
}
t.Logf("Killing new instance %s (PID %d) for iteration %d", info.CoreAddress, corePID, i)
t.Logf("Killing new instance %s (PID %d) for iteration %d", info.Address, corePID, i)
if err := syscall.Kill(corePID, syscall.SIGKILL); err != nil {
t.Fatalf("kill pid %d: %v", corePID, err)
}
+1 -1
View File
@@ -55,7 +55,7 @@ func readInstancesFromSQLite(t *testing.T, clineDir string) []common.CoreInstanc
// Create InstanceInfo
info := common.CoreInstanceInfo{
CoreAddress: heldBy,
Address: 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
View File
@@ -29,7 +29,7 @@ func TestStartAndList(t *testing.T) {
t.Fatalf("expected 1 instance, got %d", len(out.CoreInstances))
}
addr := out.CoreInstances[0].CoreAddress
addr := out.CoreInstances[0].Address
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].CoreAddress {
t.Fatalf("expected default_instance=%s, got %s", out.CoreInstances[0].CoreAddress, out.DefaultInstance)
if out.DefaultInstance != out.CoreInstances[0].Address {
t.Fatalf("expected default_instance=%s, got %s", out.CoreInstances[0].Address, 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].CoreAddress
addr := out.CoreInstances[0].Address
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.CoreAddress, defaultTimeout)
waitForAddressHealthy(t, gracefulTarget.Address, defaultTimeout)
// Get PID using runtime discovery
gracefulPID := getCorePID(t, gracefulTarget.CoreAddress)
gracefulPID := getCorePID(t, gracefulTarget.Address)
if gracefulPID <= 0 {
t.Fatalf("could not find PID for graceful target at %s", gracefulTarget.CoreAddress)
t.Fatalf("could not find PID for graceful target at %s", gracefulTarget.Address)
}
t.Logf("Testing graceful shutdown (SIGTERM) for instance %s (PID %d)", gracefulTarget.CoreAddress, gracefulPID)
t.Logf("Testing graceful shutdown (SIGTERM) for instance %s (PID %d)", gracefulTarget.Address, 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.CoreAddress, longTimeout)
waitForAddressRemoved(t, gracefulTarget.Address, 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.CoreAddress, defaultTimeout)
waitForAddressHealthy(t, crashTarget.Address, defaultTimeout)
// Get PID using runtime discovery
crashPID := getCorePID(t, crashTarget.CoreAddress)
crashPID := getCorePID(t, crashTarget.Address)
if crashPID <= 0 {
t.Fatalf("could not find PID for crash target at %s", crashTarget.CoreAddress)
t.Fatalf("could not find PID for crash target at %s", crashTarget.Address)
}
t.Logf("Testing crash cleanup (SIGKILL) for instance %s (PID %d)", crashTarget.CoreAddress, crashPID)
t.Logf("Testing crash cleanup (SIGKILL) for instance %s (PID %d)", crashTarget.Address, 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.CoreAddress, longTimeout)
waitForAddressRemoved(t, crashTarget.Address, longTimeout)
// Verify the instance is removed from SQLite (no file to check anymore)
// The waitForAddressRemoved already confirms the instance is gone from the registry
-58
View File
@@ -298,64 +298,6 @@ 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
-36
View File
@@ -323,42 +323,6 @@ 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
View File
@@ -1,6 +1,6 @@
{
"name": "cline",
"version": "1.0.10",
"version": "1.0.9",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "cline-core.js",
"bin": {
+10 -8
View File
@@ -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.Instances.StartNewInstance(ctx)
instanceInfo, err := global.Clients.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.CoreAddress)
if err := global.KillInstanceByAddress(context.Background(), global.Instances.GetRegistry(), instanceInfo.CoreAddress); err != nil {
verboseLog("Shutting down auth instance at %s", instanceInfo.Address)
if err := global.KillInstanceByAddress(context.Background(), global.Clients.GetRegistry(), instanceInfo.Address); 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.CoreAddress)
authCtx := context.WithValue(ctx, authInstanceAddressKey, instanceInfo.Address)
// Route to existing auth flow
return HandleAuthCommand(authCtx, args)
@@ -108,10 +108,12 @@ func HandleAuthMenuNoArgs(ctx context.Context) error {
// Get current provider config for display
var currentProvider string
var currentModel string
if providerList, err := GetProviderConfigurations(ctx); err == nil {
if providerList.ActProvider != nil {
currentProvider = GetProviderDisplayName(providerList.ActProvider.Provider)
currentModel = providerList.ActProvider.ModelID
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
}
}
}
+2 -6
View File
@@ -29,17 +29,13 @@ type ProviderListResult struct {
}
// GetProviderConfigurations retrieves and parses provider configurations from Cline Core state
func GetProviderConfigurations(ctx context.Context) (*ProviderListResult, error) {
func GetProviderConfigurations(ctx context.Context, manager *task.Manager) (*ProviderListResult, error) {
if global.Config.Verbose {
fmt.Println("[DEBUG] Retrieving provider configurations from Cline Core")
}
// Get latest state from Cline Core
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{})
state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return nil, fmt.Errorf("failed to get state: %w", err)
}
+6 -5
View File
@@ -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)
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
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)
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
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)
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
if err != nil {
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
}
@@ -748,6 +748,7 @@ 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 {
+3 -3
View File
@@ -40,7 +40,7 @@ func ensureConfigManager(ctx context.Context, address string) error {
}
// Always set the instance we're using as the default
registry := global.Instances.GetRegistry()
registry := global.Clients.GetRegistry()
if err := registry.SetDefaultInstance(instanceAddress); err != nil {
// Log warning but don't fail - this is not critical
fmt.Printf("Warning: failed to set default instance: %v\n", err)
@@ -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()
+3 -2
View File
@@ -32,8 +32,8 @@ func NewManager(ctx context.Context, address string) (*Manager, error) {
// Get the actual address being used
clientAddress := address
if address == "" && global.Instances != nil {
clientAddress = global.Instances.GetRegistry().GetDefaultInstance()
if address == "" && global.Clients != nil {
clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
}
return &Manager{
@@ -110,6 +110,7 @@ func (m *Manager) ListSettings(ctx context.Context) error {
"dictationSettings",
"autoCondenseThreshold",
"autoApprovalSettings",
"hooksEnabled",
}
// Render each field using the renderer
+1 -1
View File
@@ -79,7 +79,7 @@ func RenderField(key string, value interface{}, censor bool) error {
"planActSeparateModelsSetting", "enableCheckpointsSetting",
"terminalReuseEnabled", "mcpResponsesCollapsed", "strictPlanModeEnabled",
"useAutoCondense", "yoloModeToggled", "shellIntegrationTimeout",
"terminalOutputLineLimit", "autoCondenseThreshold":
"terminalOutputLineLimit", "autoCondenseThreshold", "hooksEnabled":
fmt.Printf("%s: %s\n", camelToKebab(key), formatValue(value, key, censor))
return nil
@@ -14,20 +14,21 @@ import (
"github.com/cline/grpc-go/cline"
)
type ClineInstances struct {
registry *InstanceRegistry
// ClineClients manages Cline instances using the new registry system
type ClineClients struct {
registry *ClientRegistry
}
// NewClineInstances creates a new ClineInstances instance
func NewClineInstances(configPath string) *ClineInstances {
registry := NewInstanceRegistry(configPath)
return &ClineInstances{
// NewClineClients creates a new ClineClients instance
func NewClineClients(configPath string) *ClineClients {
registry := NewClientRegistry(configPath)
return &ClineClients{
registry: registry,
}
}
// Initialize performs cleanup of stale instances
func (c *ClineInstances) Initialize(ctx context.Context) error {
func (c *ClineClients) Initialize(ctx context.Context) error {
// Clean up stale entries (direct SQLite operations)
_ = c.registry.CleanupStaleInstances(ctx)
@@ -35,8 +36,7 @@ func (c *ClineInstances) Initialize(ctx context.Context) error {
}
// StartNewInstance starts a new Cline instance and waits for cline-core to self-register
// An "instance" is a pair of cline-core and cline-host processes
func (c *ClineInstances) StartNewInstance(ctx context.Context, workspaces ...string) (*common.CoreInstanceInfo, error) {
func (c *ClineClients) 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 *ClineInstances) StartNewInstance(ctx context.Context, workspaces ...str
if Config.Verbose {
fmt.Println("Services started and registered successfully!")
fmt.Printf(" Address: %s\n", instance.CoreAddress)
fmt.Printf(" Address: %s\n", instance.Address)
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 *ClineInstances) StartNewInstance(ctx context.Context, workspaces ...str
}
// StartNewInstanceAtPort starts a new Cline instance at the specified port and waits for self-registration
func (c *ClineInstances) StartNewInstanceAtPort(ctx context.Context, corePort int, workspaces ...string) (*common.CoreInstanceInfo, error) {
func (c *ClineClients) 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 *ClineInstances) StartNewInstanceAtPort(ctx context.Context, corePort in
if Config.Verbose {
fmt.Println("Services started and registered successfully!")
fmt.Printf(" Address: %s\n", instance.CoreAddress)
fmt.Printf(" Address: %s\n", instance.Address)
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 *ClineInstances) StartNewInstanceAtPort(ctx context.Context, corePort in
}
// GetRegistry returns the client registry
func (c *ClineInstances) GetRegistry() *InstanceRegistry {
func (c *ClineClients) GetRegistry() *ClientRegistry {
return c.registry
}
// EnsureInstanceAtAddress ensures an instance exists at the given address, starting one if needed
func (c *ClineInstances) EnsureInstanceAtAddress(ctx context.Context, address string) error {
func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address string) error {
// Expect host:port everywhere
normalized := address
if normalized == "" {
@@ -230,7 +230,7 @@ func (c *ClineInstances) EnsureInstanceAtAddress(ctx context.Context, address st
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 *InstanceRegistry, address string) error {
func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, 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 *InstanceRegistry, addr
defaultInstance := registry.GetDefaultInstance()
if defaultInstance == address || defaultInstance == "" {
if len(instances) > 0 {
if err := registry.SetDefaultInstance(instances[0].CoreAddress); err == nil {
if err := registry.SetDefaultInstance(instances[0].Address); err == nil {
if Config.Verbose {
fmt.Printf("Updated default instance to: %s\n", instances[0].CoreAddress)
fmt.Printf("Updated default instance to: %s\n", instances[0].Address)
}
}
}
+11 -12
View File
@@ -22,8 +22,8 @@ type GlobalConfig struct {
}
var (
Config *GlobalConfig
Instances *ClineInstances
Config *GlobalConfig
Clients *ClineClients
// 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
Instances = NewClineInstances(cfg.ConfigPath)
Clients = NewClineClients(cfg.ConfigPath)
// Initialize the clients registry
ctx := context.Background()
if err := Instances.Initialize(ctx); err != nil {
if err := Clients.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 Instances.GetRegistry().GetClient(ctx, Config.CoreAddress)
return Clients.GetRegistry().GetClient(ctx, Config.CoreAddress)
}
// Use the default instance from registry
return Instances.GetRegistry().GetDefaultClient(ctx)
return Clients.GetRegistry().GetDefaultClient(ctx)
}
// GetClientForAddress returns a client for a specific address
func GetClientForAddress(ctx context.Context, address string) (*client.ClineClient, error) {
return Instances.GetRegistry().GetClient(ctx, address)
return Clients.GetRegistry().GetClient(ctx, address)
}
// EnsureDefaultInstance ensures a default instance exists
func EnsureDefaultInstance(ctx context.Context) error {
if Instances == nil {
if Clients == nil {
return fmt.Errorf("global clients not initialized")
}
registry := Instances.GetRegistry()
registry := Clients.GetRegistry()
// First, check if there are any instances already registered in SQLite
instances := registry.ListInstances()
@@ -108,12 +108,11 @@ 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 := Instances.StartNewInstance(ctx)
_, err := Clients.StartNewInstance(ctx)
if err != nil {
return fmt.Errorf("failed to start new default instance: %w", err)
}
}
return nil
}
}
+28 -27
View File
@@ -18,28 +18,28 @@ import (
"google.golang.org/grpc/health/grpc_health_v1"
)
// InstanceRegistry manages Cline client connections using direct SQLite operations
type InstanceRegistry struct {
// ClientRegistry manages Cline client connections using direct SQLite operations
type ClientRegistry struct {
lockManager *sqlite.LockManager
configPath string
}
// NewInstanceRegistry creates a new instance registry
func NewInstanceRegistry(configPath string) *InstanceRegistry {
// NewClientRegistry creates a new client registry
func NewClientRegistry(configPath string) *ClientRegistry {
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 &InstanceRegistry{
return &ClientRegistry{
lockManager: lockManager,
configPath: configPath,
}
}
// GetDefaultInstance returns the default instance address from settings file
func (r *InstanceRegistry) GetDefaultInstance() string {
func (r *ClientRegistry) GetDefaultInstance() string {
defaultAddr, err := sqlite.GetDefaultInstance(r.configPath)
if err != nil {
return ""
@@ -48,7 +48,7 @@ func (r *InstanceRegistry) GetDefaultInstance() string {
}
// SetDefaultInstance sets the default instance (writes default.json)
func (r *InstanceRegistry) SetDefaultInstance(address string) error {
func (r *ClientRegistry) 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 *InstanceRegistry) SetDefaultInstance(address string) error {
}
// GetInstance returns instance information directly from SQLite
func (r *InstanceRegistry) GetInstance(address string) (*common.CoreInstanceInfo, error) {
func (r *ClientRegistry) GetInstance(address string) (*common.CoreInstanceInfo, error) {
if r.lockManager == nil {
return nil, fmt.Errorf("lock manager not available")
}
@@ -73,7 +73,7 @@ func (r *InstanceRegistry) GetInstance(address string) (*common.CoreInstanceInfo
}
// GetClient returns a connected client for the given address (created on-demand)
func (r *InstanceRegistry) GetClient(ctx context.Context, address string) (*client.ClineClient, error) {
func (r *ClientRegistry) 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 *InstanceRegistry) GetClient(ctx context.Context, address string) (*clie
}
// GetDefaultClient returns a client for the default instance
func (r *InstanceRegistry) GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
func (r *ClientRegistry) 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 *InstanceRegistry) GetDefaultClient(ctx context.Context) (*client.ClineC
// 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 *InstanceRegistry) GetDefaultClient(ctx context.Context) (*client.ClineC
} 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 *InstanceRegistry) GetDefaultClient(ctx context.Context) (*client.ClineC
return r.GetClient(ctx, newDefaultAddr)
}
}
return nil, fmt.Errorf("no default instance configured")
}
}
@@ -151,7 +151,7 @@ func (r *InstanceRegistry) GetDefaultClient(ctx context.Context) (*client.ClineC
}
// ListInstances returns all registered instances directly from SQLite
func (r *InstanceRegistry) ListInstances() []*common.CoreInstanceInfo {
func (r *ClientRegistry) ListInstances() []*common.CoreInstanceInfo {
if r.lockManager == nil {
return []*common.CoreInstanceInfo{}
}
@@ -170,7 +170,7 @@ func (r *InstanceRegistry) ListInstances() []*common.CoreInstanceInfo {
}
// HasInstanceAtAddress checks if an instance exists at the given address (delegates to SQLite)
func (r *InstanceRegistry) HasInstanceAtAddress(address string) bool {
func (r *ClientRegistry) HasInstanceAtAddress(address string) bool {
if r.lockManager == nil {
return false
}
@@ -185,7 +185,7 @@ func (r *InstanceRegistry) HasInstanceAtAddress(address string) bool {
}
// CleanupStaleInstances removes stale instances using direct SQLite operations
func (r *InstanceRegistry) CleanupStaleInstances(ctx context.Context) error {
func (r *ClientRegistry) CleanupStaleInstances(ctx context.Context) error {
if r.lockManager == nil {
return nil
}
@@ -202,15 +202,15 @@ func (r *InstanceRegistry) 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.CoreAddress)
instance.HostServiceAddress, instance.Address)
r.tryShutdownHostProcess(instance.HostServiceAddress)
// Remove from SQLite database
if err := r.lockManager.RemoveInstanceLock(instance.CoreAddress); err != nil {
return fmt.Errorf("failed to remove stale instance %s: %w", instance.CoreAddress, err)
if err := r.lockManager.RemoveInstanceLock(instance.Address); err != nil {
return fmt.Errorf("failed to remove stale instance %s: %w", instance.Address, err)
}
fmt.Printf("Removed stale instance: %s\n", instance.CoreAddress)
fmt.Printf("Removed stale instance: %s\n", instance.Address)
}
}
@@ -218,7 +218,8 @@ func (r *InstanceRegistry) CleanupStaleInstances(ctx context.Context) error {
}
// tryShutdownHostProcess attempts to gracefully shutdown a host process via RPC
func (r *InstanceRegistry) tryShutdownHostProcess(hostServiceAddress string) {
// Best effort, don't throw errors i guess
func (r *ClientRegistry) 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)
@@ -251,7 +252,7 @@ func (r *InstanceRegistry) tryShutdownHostProcess(hostServiceAddress string) {
}
// ListInstancesCleaned performs cleanup and returns instances with health checks
func (r *InstanceRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.CoreInstanceInfo, error) {
func (r *ClientRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.CoreInstanceInfo, error) {
// 1. Clean up stale entries (best-effort)
_ = r.CleanupStaleInstances(ctx)
@@ -267,7 +268,7 @@ func (r *InstanceRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.
}
// EnsureDefaultInstance ensures a default instance is set if instances exist but no default is configured
func (r *InstanceRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceInfo) error {
func (r *ClientRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceInfo) error {
currentDefault := r.GetDefaultInstance()
// If we have no instances, clear any stale default and remove settings file
@@ -282,13 +283,13 @@ func (r *InstanceRegistry) EnsureDefaultInstance(instances []*common.CoreInstanc
// If we have instances but no default, pick the first one
if currentDefault == "" {
return sqlite.SetDefaultInstance(r.configPath, instances[0].CoreAddress)
return sqlite.SetDefaultInstance(r.configPath, instances[0].Address)
}
// Validate current default still exists in the instances
defaultExists := false
for _, instance := range instances {
if instance.CoreAddress == currentDefault {
if instance.Address == currentDefault {
defaultExists = true
break
}
@@ -296,7 +297,7 @@ func (r *InstanceRegistry) EnsureDefaultInstance(instances []*common.CoreInstanc
if !defaultExists {
// Current default doesn't exist, pick a new one from available instances
return sqlite.SetDefaultInstance(r.configPath, instances[0].CoreAddress)
return sqlite.SetDefaultInstance(r.configPath, instances[0].Address)
}
return nil
+25 -25
View File
@@ -92,12 +92,12 @@ func newInstanceKillCommand() *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if global.Instances == nil {
if global.Clients == nil {
return fmt.Errorf("clients not initialized")
}
ctx := cmd.Context()
registry := global.Instances.GetRegistry()
registry := global.Clients.GetRegistry()
if killAllCLI {
return killAllCLIInstances(ctx, registry)
@@ -112,7 +112,7 @@ func newInstanceKillCommand() *cobra.Command {
return cmd
}
func killAllCLIInstances(ctx context.Context, registry *global.InstanceRegistry) error {
func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) 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.InstanceRegistry)
cliInstances = append(cliInstances, instance)
} else {
skippedNonCLI++
fmt.Printf("⊘ Skipping %s instance: %s\n", platform, instance.CoreAddress)
fmt.Printf("⊘ Skipping %s instance: %s\n", platform, instance.Address)
}
}
}
@@ -160,16 +160,16 @@ func killAllCLIInstances(ctx context.Context, registry *global.InstanceRegistry)
// Kill all CLI instances
for _, instance := range cliInstances {
result := killInstanceProcess(ctx, registry, instance.CoreAddress)
result := killInstanceProcess(ctx, registry, instance.Address)
killResults = append(killResults, result)
if result.err != nil {
fmt.Printf("✗ Failed to kill %s: %v\n", instance.CoreAddress, result.err)
fmt.Printf("✗ Failed to kill %s: %v\n", instance.Address, result.err)
} else if result.alreadyDead {
fmt.Printf("⚠ Instance %s appears to be already dead\n", instance.CoreAddress)
fmt.Printf("⚠ Instance %s appears to be already dead\n", instance.Address)
} else {
fmt.Printf("✓ Killed %s (PID %d)\n", instance.CoreAddress, result.pid)
killedAddresses[instance.CoreAddress] = true
fmt.Printf("✓ Killed %s (PID %d)\n", instance.Address, result.pid)
killedAddresses[instance.Address] = true
}
}
@@ -190,8 +190,8 @@ func killAllCLIInstances(ctx context.Context, registry *global.InstanceRegistry)
// Check if any of the killed instances are still in the registry
stillPresent := []string{}
for _, remaining := range remainingInstances {
if killedAddresses[remaining.CoreAddress] {
stillPresent = append(stillPresent, remaining.CoreAddress)
if killedAddresses[remaining.Address] {
stillPresent = append(stillPresent, remaining.Address)
}
}
@@ -247,7 +247,7 @@ type killResult struct {
err error
}
func killInstanceProcess(ctx context.Context, registry *global.InstanceRegistry, address string) killResult {
func killInstanceProcess(ctx context.Context, registry *global.ClientRegistry, 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.Instances == nil {
if global.Clients == nil {
return fmt.Errorf("clients not initialized")
}
ctx := cmd.Context()
registry := global.Instances.GetRegistry()
registry := global.Clients.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.CoreAddress == defaultInstance {
if instance.Address == 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.CoreAddress); err == nil {
if client, err := registry.GetClient(ctx, instance.Address); 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.CoreAddress,
address: instance.Address,
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.Instances == nil {
if global.Clients == nil {
return fmt.Errorf("clients not initialized")
}
registry := global.Instances.GetRegistry()
registry := global.Clients.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.Instances == nil {
if global.Clients == nil {
return fmt.Errorf("clients not initialized")
}
fmt.Println("Starting new Cline instance...")
instance, err := global.Instances.StartNewInstance(ctx)
instance, err := global.Clients.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.CoreAddress)
fmt.Printf(" Address: %s\n", instance.Address)
fmt.Printf(" Core Port: %d\n", instance.CorePort())
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
registry := global.Instances.GetRegistry()
registry := global.Clients.GetRegistry()
// If --default flag provided, set this instance as the default
if setDefault {
if err := registry.SetDefaultInstance(instance.CoreAddress); err != nil {
if err := registry.SetDefaultInstance(instance.Address); 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.CoreAddress {
if registry.GetDefaultInstance() == instance.Address {
fmt.Printf(" Status: Default instance\n")
}
}
+1 -72
View File
@@ -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\nctrl+c to exit",
Placeholder: "/plan or /act to switch modes\nctrl+e to open editor",
}
}
}
@@ -547,74 +547,3 @@ 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
}
+22 -39
View File
@@ -2,11 +2,10 @@ package slash
import (
"context"
"fmt"
"strings"
"sync"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/grpc-go/client"
"github.com/cline/grpc-go/cline"
)
@@ -33,31 +32,25 @@ var cliLocalCommands = []Command{
}
// NewRegistry creates a new slash command registry
func NewRegistry(ctx context.Context) *Registry {
defaultCommands := append([]Command{}, cliLocalCommands...)
r := &Registry{
commands: defaultCommands,
func NewRegistry() *Registry {
return &Registry{
commands: make([]Command, 0),
}
r.FetchFromBackend(ctx)
return r
}
// FetchFromBackend fetches available commands from cline-core backend
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
func (r *Registry) FetchFromBackend(ctx context.Context, c *client.ClineClient) error {
resp, err := c.Slash.GetAvailableSlashCommands(ctx, &cline.EmptyRequest{})
if err != nil {
return err
}
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 {
@@ -73,6 +66,17 @@ func (r *Registry) FetchFromBackend(ctx context.Context) error {
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()
@@ -119,24 +123,3 @@ 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
}
+7 -7
View File
@@ -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{
CoreAddress: heldBy,
Address: 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{
CoreAddress: lock.HeldBy,
Address: lock.HeldBy,
HostServiceAddress: lock.LockTarget,
Status: status,
LastSeen: time.Unix(lock.LockedAt/1000, 0),
+10 -5
View File
@@ -80,7 +80,7 @@ func ensureTaskManager(ctx context.Context, address string) error {
}
// Always set the instance we're using as the default
registry := global.Instances.GetRegistry()
registry := global.Clients.GetRegistry()
if err := registry.SetDefaultInstance(instanceAddress); err != nil {
// Log warning but don't fail - this is not critical
fmt.Printf("Warning: failed to set default instance: %v\n", err)
@@ -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.Instances == nil {
if global.Clients == nil {
return fmt.Errorf("global clients not initialized")
}
return global.Instances.EnsureInstanceAtAddress(ctx, address)
return global.Clients.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.Instances.GetRegistry().GetDefaultInstance() == "" {
if address == "" && global.Clients.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.Instances.GetRegistry().GetDefaultInstance() == "" {
if address == "" && global.Clients.GetRegistry().GetDefaultInstance() == "" {
fmt.Println("No instances available for sending messages")
return nil
}
@@ -620,6 +620,11 @@ 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 {
+44 -29
View File
@@ -13,41 +13,38 @@ 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
slashCommandRegistry *slash.Registry // Slash command registry for autocomplete
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
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),
slashCommandRegistry: slash.NewRegistry(context.Background()),
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),
cancelChan: make(chan struct{}, 1),
}
}
@@ -166,7 +163,7 @@ func (ih *InputHandler) Start(ctx context.Context, errChan chan error) {
if shouldSend {
// Check for mode switch commands first
newMode, remainingMessage, isModeSwitch := slash.ParseModeSwitch(message)
newMode, remainingMessage, isModeSwitch := ih.parseModeSwitch(message)
if isModeSwitch {
// Create styles for mode switch messages (respect global color profile)
actStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true)
@@ -287,9 +284,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\ntab to autocomplete commands\nctrl+e to open editor\nctrl+c to exit",
"/plan or /act to switch modes\nctrl+e to open editor\ntab to autocomplete commands",
currentMode,
ih.slashCommandRegistry,
ih.manager.GetSlashRegistry(),
)
return ih.runInputProgram(ctx, model)
@@ -305,7 +302,7 @@ func (ih *InputHandler) promptForApproval(ctx context.Context, msg *types.ClineM
"Let Cline use this tool?",
"",
ih.manager.GetCurrentMode(),
ih.slashCommandRegistry,
ih.manager.GetSlashRegistry(), // Pass registry for feedback input after approval
)
message, shouldSend, err := ih.runInputProgram(ctx, model)
@@ -481,6 +478,24 @@ 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)) {
+34 -7
View File
@@ -14,6 +14,7 @@ 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"
@@ -37,6 +38,7 @@ type Manager struct {
systemRenderer *display.SystemMessageRenderer
streamingDisplay *display.StreamingDisplay
handlerRegistry *handlers.HandlerRegistry
slashRegistry *slash.Registry
isStreamingMode bool
isInteractive bool
currentMode string // "plan" or "act"
@@ -66,6 +68,7 @@ func NewManager(client *client.ClineClient) *Manager {
systemRenderer: systemRenderer,
streamingDisplay: streamingDisplay,
handlerRegistry: registry,
slashRegistry: slash.NewRegistry(),
currentMode: "plan", // Default mode
}
}
@@ -80,6 +83,9 @@ 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
}
@@ -93,13 +99,29 @@ func NewManagerForDefault(ctx context.Context) (*Manager, error) {
manager := NewManager(client)
// Get the default instance address
if global.Instances != nil {
manager.clientAddress = global.Instances.GetRegistry().GetDefaultInstance()
if global.Clients != nil {
manager.clientAddress = global.Clients.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()
@@ -1184,11 +1206,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,
@@ -1298,6 +1320,11 @@ 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{}
+6
View File
@@ -290,6 +290,12 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
return err
}
settings.ActModeAwsBedrockCustomSelected = boolPtr(val)
case "hooks_enabled":
val, err := parseBool(value)
if err != nil {
return err
}
settings.HooksEnabled = boolPtr(val)
case "azure_identity":
val, err := parseBool(value)
if err != nil {
+2 -2
View File
@@ -10,7 +10,7 @@ import (
// This is the canonical definition used across all CLI packages
type CoreInstanceInfo struct {
// Full core address including port
CoreAddress string `json:"address"`
Address 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.CoreAddress)
_, port, _ := ParseHostPort(c.Address)
return port
}
+39
View File
@@ -0,0 +1,39 @@
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
}
+4 -6
View File
@@ -5,7 +5,6 @@ import (
"log"
"os"
"github.com/cline/grpc-go/cline"
"github.com/cline/grpc-go/host"
)
@@ -44,25 +43,24 @@ 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: %v", req.FilePath)
log.Printf("SaveOpenDocumentIfDirty called for path: %s", req.GetPath())
}
// 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: &f, // Assume no changes to save
WasSaved: false, // 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")
log.Printf("GetDiagnostics called for path: %s", req.GetPath())
}
// For console implementation, return empty diagnostics
return &host.GetDiagnosticsResponse{
FileDiagnostics: []*cline.FileDiagnostics{},
Diagnostics: []*host.Diagnostic{},
}, nil
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 MiB

-2
View File
@@ -148,7 +148,6 @@
"features/hooks/samples"
]
},
"features/jupyter-notebooks",
"features/multiroot-workspace",
"features/plan-and-act",
"features/skills",
@@ -199,7 +198,6 @@
"provider-config/anthropic",
"provider-config/claude-code",
"provider-config/openai",
"provider-config/openai-codex",
"provider-config/openrouter",
"provider-config/cerebras",
"provider-config/deepseek",
-112
View File
@@ -1,112 +0,0 @@
---
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.
![Generate Cell demo](../assets/jupyter-generate-cell.gif)
**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.
![Explain and Improve Cell demo](../assets/jupyter-explain-improve-cell.gif)
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
+2 -19
View File
@@ -25,28 +25,11 @@ The easiest way to get started is with **Cline** as your provider:
## Alternative: Use Another Provider
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:
If you prefer to use your own API keys, you can select from providers like:
- **OpenRouter** - Great value, multiple models
- **Anthropic** - Direct access to Claude models
- **OpenAI** - Access to GPT models via API key
- **OpenAI** - Access to GPT models
- **Google Gemini** - Google's AI models
- **Ollama** - Run models locally on your computer
+2 -2
View File
@@ -103,8 +103,8 @@ Master planning vs. execution for complex tasks
Set project-specific guidelines for consistent results
</Card>
<Card title="Prompting Guide" href="/prompting/prompt-engineering-guide" icon="wand-magic-sparkles">
Learn to write prompts that get the best results
<Card title="Worktrees" href="/features/worktrees" icon="git-branch">
Work on multiple branches simultaneously with parallel Cline sessions
</Card>
</CardGroup>
+3 -1
View File
@@ -99,6 +99,9 @@ Cline explores your codebase and works with you to create comprehensive plans be
<div>
<strong><a href="/features/dictation">Dictation</a></strong> • Speak naturally to Cline for rapid planning and complex requirements
</div>
<div>
<strong><a href="/features/worktrees">Worktrees</a></strong> • Work on multiple branches simultaneously with parallel Cline sessions
</div>
</div>
</Accordion>
@@ -144,4 +147,3 @@ Cline explores your codebase and works with you to create comprehensive plans be
See every file read, every decision considered, every token used. No black box, no surprises.
</Card>
</CardGroup>
+1
View File
@@ -18,6 +18,7 @@ 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
-105
View File
@@ -1,105 +0,0 @@
---
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
+65 -48
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.52.0",
"version": "3.51.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.52.0",
"version": "3.51.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -1184,7 +1184,6 @@
"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",
@@ -2647,7 +2646,6 @@
"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"
@@ -3231,7 +3229,6 @@
"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",
@@ -3300,7 +3297,6 @@
"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"
}
@@ -4916,7 +4912,8 @@
"optional": true,
"os": [
"android"
]
],
"peer": true
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.4",
@@ -4929,7 +4926,8 @@
"optional": true,
"os": [
"android"
]
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.4",
@@ -4942,7 +4940,8 @@
"optional": true,
"os": [
"darwin"
]
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.4",
@@ -4955,7 +4954,8 @@
"optional": true,
"os": [
"darwin"
]
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.4",
@@ -4968,7 +4968,8 @@
"optional": true,
"os": [
"freebsd"
]
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.4",
@@ -4981,7 +4982,8 @@
"optional": true,
"os": [
"freebsd"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.4",
@@ -4994,7 +4996,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.4",
@@ -5007,7 +5010,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.4",
@@ -5020,7 +5024,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.4",
@@ -5033,7 +5038,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.4",
@@ -5046,7 +5052,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.4",
@@ -5059,7 +5066,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.4",
@@ -5072,7 +5080,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.4",
@@ -5085,7 +5094,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.4",
@@ -5098,7 +5108,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.4",
@@ -5111,7 +5122,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.4",
@@ -5124,7 +5136,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.4",
@@ -5137,7 +5150,8 @@
"optional": true,
"os": [
"openharmony"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.4",
@@ -5150,7 +5164,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.4",
@@ -5163,7 +5178,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.4",
@@ -5176,7 +5192,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.4",
@@ -5189,7 +5206,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@sap-ai-sdk/ai-api": {
"version": "2.1.0",
@@ -6752,7 +6770,8 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/get-folder-size": {
"version": "3.0.4",
@@ -6784,7 +6803,6 @@
"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"
}
@@ -7501,7 +7519,6 @@
"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"
},
@@ -8303,7 +8320,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -8638,9 +8654,9 @@
}
},
"node_modules/cheerio/node_modules/undici": {
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
"license": "MIT",
"engines": {
"node": ">=18.17"
@@ -9509,8 +9525,7 @@
},
"node_modules/devtools-protocol": {
"version": "0.0.1342118",
"license": "BSD-3-Clause",
"peer": true
"license": "BSD-3-Clause"
},
"node_modules/diff": {
"version": "5.2.0",
@@ -12461,7 +12476,6 @@
"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"
}
@@ -12695,7 +12709,6 @@
"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"
},
@@ -14847,9 +14860,9 @@
}
},
"node_modules/open-graph-scraper/node_modules/undici": {
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
"license": "MIT",
"engines": {
"node": ">=18.17"
@@ -15417,7 +15430,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15599,6 +15611,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -15619,6 +15632,7 @@
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -16308,6 +16322,7 @@
"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"
},
@@ -17785,6 +17800,7 @@
"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"
@@ -17801,6 +17817,7 @@
"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"
},
@@ -18161,7 +18178,6 @@
"version": "5.5.3",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -18234,9 +18250,9 @@
"license": "MIT"
},
"node_modules/undici": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.19.0.tgz",
"integrity": "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@@ -18414,6 +18430,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -18488,6 +18505,7 @@
"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"
},
@@ -19175,7 +19193,6 @@
"node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.52.0",
"version": "3.51.0",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
+3 -3
View File
@@ -108,6 +108,7 @@ 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;
@@ -256,6 +257,7 @@ 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;
@@ -276,7 +278,6 @@ 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 {
@@ -383,8 +384,6 @@ 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;
@@ -409,6 +408,7 @@ 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;
-1
View File
@@ -70,7 +70,6 @@ enum ClineSay {
HOOK_STATUS = 30;
HOOK_OUTPUT_STREAM = 31;
COMMAND_PERMISSION_DENIED = 32;
CONDITIONAL_RULES_APPLIED = 33;
}
// Enum for ClineSayTool tool types
+1 -1
View File
@@ -105,7 +105,7 @@ async function main(): Promise<void> {
console.log("Extracting standalone.zip to extensions directory...")
try {
if (!fs.existsSync(extensionsDir)) {
execSync(`unzip -o -q "${standaloneZipPath}" -d "${extensionsDir}"`, { stdio: "inherit" })
execSync(`unzip -q "${standaloneZipPath}" -d "${extensionsDir}"`, { stdio: "inherit" })
}
console.log(`Successfully extracted standalone.zip to: ${extensionsDir}`)
} catch (error) {
+8 -7
View File
@@ -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,6 +26,7 @@ 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.
@@ -34,13 +35,10 @@ 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) {
Logger.error("[Controller] CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
console.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.",
@@ -72,6 +70,9 @@ 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)
@@ -126,7 +127,7 @@ async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
Logger.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
console.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
}
}
+1
View File
@@ -44,6 +44,7 @@ class ClineEndpoint {
this.environment = Environment.production
break
}
console.info("Cline environment updated: ", this.environment)
}
public getEnvironment(): EnvironmentConfig {
+1 -2
View File
@@ -1,7 +1,6 @@
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"
@@ -481,7 +480,7 @@ export function buildApiHandler(configuration: ApiConfiguration, mode: Mode): Ap
}
}
} catch (error) {
Logger.error("buildApiHandler error:", error)
console.error("buildApiHandler error:", error)
}
return createHandlerForProvider(apiProvider, options, mode)
+3 -4
View File
@@ -1,7 +1,6 @@
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"
@@ -59,7 +58,7 @@ export class AskSageHandler implements ApiHandler {
private apiKey: string
constructor(options: AskSageHandlerOptions) {
Logger.log("init api url", options.asksageApiUrl, askSageDefaultURL)
console.log("init api url", options.asksageApiUrl, askSageDefaultURL)
this.options = options
this.apiKey = options.asksageApiKey || ""
this.apiUrl = options.asksageApiUrl || askSageDefaultURL
@@ -165,7 +164,7 @@ export class AskSageHandler implements ApiHandler {
})
if (!response.ok) {
Logger.error("Failed to fetch AskSage usage", await response.text())
console.error("Failed to fetch AskSage usage", await response.text())
return undefined
}
@@ -178,7 +177,7 @@ export class AskSageHandler implements ApiHandler {
outputTokens: 0,
}
} catch (error) {
Logger.error("Error fetching AskSage usage:", error)
console.error("Error fetching AskSage usage:", error)
return undefined
}
}
+6 -7
View File
@@ -12,7 +12,6 @@ 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"
@@ -450,7 +449,7 @@ export class AwsBedrockHandler implements ApiHandler {
}
}
} catch (error) {
Logger.error("Error parsing Deepseek response chunk:", error)
console.error("Error parsing Deepseek response chunk:", error)
// Propagate the error by yielding a text response with error information
yield {
type: "text",
@@ -551,7 +550,7 @@ export class AwsBedrockHandler implements ApiHandler {
for await (const chunk of response.stream) {
// Debug logging to see actual response structure
// Logger.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
// console.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
// Handle thinking response in additionalModelResponseFields (LangChain format)
const metadata = chunk.metadata as ExtendedMetadata | undefined
@@ -855,7 +854,7 @@ export class AwsBedrockHandler implements ApiHandler {
}
// Log unsupported content types for debugging
Logger.warn(`Unsupported content type: ${(item as ContentItem).type}`)
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
return null
})
.filter((item): item is ContentBlock => item !== null)
@@ -913,7 +912,7 @@ export class AwsBedrockHandler implements ApiHandler {
},
}
} catch (error) {
Logger.error("Failed to process image content:", error)
console.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 {
@@ -1112,7 +1111,7 @@ export class AwsBedrockHandler implements ApiHandler {
}
}
} catch (error) {
Logger.error("Error with OpenAI model via Converse API:", error)
console.error("Error with OpenAI model via Converse API:", error)
// Try to extract more detailed error information
let errorMessage = "Failed to process OpenAI model request"
@@ -1247,7 +1246,7 @@ export class AwsBedrockHandler implements ApiHandler {
}
}
} catch (error) {
Logger.error("Error with Qwen model via Converse API:", error)
console.error("Error with Qwen model via Converse API:", error)
// Try to extract more detailed error information
let errorMessage = "Failed to process Qwen model request"
+6 -6
View File
@@ -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"]
Logger.error(`Cline API Error: ${error?.code} - ${error?.message}`)
console.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
Logger.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
console.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) {
Logger.warn("Cline API did not return usage chunk, fetching from generation endpoint")
console.warn("Cline API did not return usage chunk, fetching from generation endpoint")
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
}
}
} catch (error) {
Logger.error("Cline API Error:", error)
console.error("Cline API Error:", error)
throw error
}
}
@@ -260,7 +260,7 @@ export class ClineHandler implements ApiHandler {
}
} catch (error) {
// ignore if fails
Logger.error("Error fetching cline generation details:", error)
console.error("Error fetching cline generation details:", error)
}
}
return undefined
+37 -38
View File
@@ -1,6 +1,5 @@
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"
@@ -85,7 +84,7 @@ export class DifyHandler implements ApiHandler {
this.apiKey = options.difyApiKey || ""
this.baseUrl = options.difyBaseUrl || ""
Logger.log("[DIFY DEBUG] Constructor called with:", {
console.log("[DIFY DEBUG] Constructor called with:", {
hasApiKey: !!this.apiKey,
baseUrl: this.baseUrl,
})
@@ -99,7 +98,7 @@ export class DifyHandler implements ApiHandler {
}
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
Logger.log("[DIFY DEBUG] createMessage called with:", {
console.log("[DIFY DEBUG] createMessage called with:", {
systemPromptLength: systemPrompt?.length || 0,
messagesCount: messages?.length || 0,
})
@@ -116,8 +115,8 @@ export class DifyHandler implements ApiHandler {
}
const fullUrl = `${this.baseUrl}/chat-messages`
Logger.log("[DIFY DEBUG] Making request to:", fullUrl)
Logger.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
console.log("[DIFY DEBUG] Making request to:", fullUrl)
console.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
let response: Response
try {
@@ -130,21 +129,21 @@ export class DifyHandler implements ApiHandler {
body: JSON.stringify(requestBody),
})
} catch (error: any) {
Logger.error("[DIFY DEBUG] Network error during fetch:", error)
console.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}`)
}
Logger.log("[DIFY DEBUG] Response status:", response.status)
console.log("[DIFY DEBUG] Response status:", response.status)
const headersObj: Record<string, string> = {}
response.headers.forEach((value, key) => {
headersObj[key] = value
})
Logger.log("[DIFY DEBUG] Response headers:", headersObj)
console.log("[DIFY DEBUG] Response headers:", headersObj)
if (!response.ok) {
const errorText = await response.text()
Logger.error("[DIFY DEBUG] Error response:", errorText)
console.error("[DIFY DEBUG] Error response:", errorText)
throw new Error(`Dify API error: ${response.status} ${response.statusText} - ${errorText}`)
}
@@ -160,14 +159,14 @@ export class DifyHandler implements ApiHandler {
const processedEvents: string[] = []
let lastEventTime = Date.now()
Logger.log("[DIFY DEBUG] Starting to read streaming response...")
console.log("[DIFY DEBUG] Starting to read streaming response...")
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
Logger.log("[DIFY DEBUG] Stream ended naturally")
Logger.log(
console.log("[DIFY DEBUG] Stream ended naturally")
console.log(
"[DIFY DEBUG] Final state - hasYieldedContent:",
hasYieldedContent,
"fullText length:",
@@ -179,7 +178,7 @@ export class DifyHandler implements ApiHandler {
}
const chunk = decoder.decode(value, { stream: true })
Logger.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
console.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
buffer += chunk
const lines = buffer.split("\n")
@@ -188,41 +187,41 @@ export class DifyHandler implements ApiHandler {
buffer = lines.pop() || ""
for (const line of lines) {
Logger.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
console.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
if (line.startsWith("data: ")) {
const data = line.slice(6).trim()
Logger.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
console.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
if (data === "[DONE]") {
Logger.log("[DIFY DEBUG] Received [DONE] signal")
console.log("[DIFY DEBUG] Received [DONE] signal")
break
}
if (data === "") {
Logger.log("[DIFY DEBUG] Empty data line, skipping")
console.log("[DIFY DEBUG] Empty data line, skipping")
continue
}
try {
const parsed = JSON.parse(data)
Logger.log("[DIFY DEBUG] Parsed JSON:", parsed)
console.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
Logger.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
console.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
}
// Handle different Dify event types based on actual Dify API
if (parsed.event === "message") {
Logger.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
console.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
Logger.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
console.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
@@ -230,10 +229,10 @@ export class DifyHandler implements ApiHandler {
hasYieldedContent = true
}
} else if (parsed.event === "message_replace") {
Logger.log("[DIFY DEBUG] Replace message event:", parsed)
console.log("[DIFY DEBUG] Replace message event:", parsed)
if (parsed.answer) {
fullText = parsed.answer // Replace instead of append
Logger.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
console.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
@@ -241,7 +240,7 @@ export class DifyHandler implements ApiHandler {
hasYieldedContent = true
}
} else if (parsed.event === "message_end") {
Logger.log("[DIFY DEBUG] Message end event", parsed)
console.log("[DIFY DEBUG] Message end event", parsed)
// Message completed. Yield final text if we have any.
if (fullText) {
yield {
@@ -261,19 +260,19 @@ export class DifyHandler implements ApiHandler {
}
return // End of stream
} else if (parsed.event === "error") {
Logger.error("[DIFY DEBUG] Error event:", parsed)
console.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") {
Logger.log("[DIFY DEBUG] Workflow event:", parsed.event)
console.log("[DIFY DEBUG] Workflow event:", parsed.event)
// These are informational events, continue processing
} else if (parsed.event === "node_started" || parsed.event === "node_finished") {
Logger.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
console.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
// These are informational events, continue processing
} else if (parsed.event === "ping") {
Logger.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
console.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
// Ping event, do nothing
} else {
Logger.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
console.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
@@ -300,17 +299,17 @@ export class DifyHandler implements ApiHandler {
}
}
} catch (e) {
Logger.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
console.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
}
} else if (line.trim() !== "") {
Logger.log(
console.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())
Logger.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
console.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
processedEvents.push(parsed.event || "direct-json")
// Handle the same event types as above
@@ -331,7 +330,7 @@ export class DifyHandler implements ApiHandler {
}
return
} else if (parsed.event === "error") {
Logger.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
console.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
@@ -345,7 +344,7 @@ export class DifyHandler implements ApiHandler {
}
} catch (e) {
// Not JSON, continue
Logger.log("[DIFY DEBUG] Line is not direct JSON, continuing")
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
}
}
}
@@ -360,11 +359,11 @@ export class DifyHandler implements ApiHandler {
streamDuration: Date.now() - lastEventTime,
conversationId: this.conversationId,
}
Logger.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
console.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()) {
Logger.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
console.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
yield {
type: "text",
text: fullText,
@@ -381,7 +380,7 @@ export class DifyHandler implements ApiHandler {
}
} finally {
reader.releaseLock()
Logger.log("[DIFY DEBUG] Stream reader released")
console.log("[DIFY DEBUG] Stream reader released")
}
}
@@ -400,7 +399,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) {
Logger.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
console.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
return `${systemPrompt}\n\n---\n\n${userQuery}`
}
+5 -6
View File
@@ -11,7 +11,6 @@ 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"
@@ -333,7 +332,7 @@ export class GeminiHandler implements ApiHandler {
throughputTokensPerSec: throughputTokensPerSecSdk,
})
} else {
Logger.warn("GeminiHandler: ulid not available for telemetry in createMessage.")
console.warn("GeminiHandler: ulid not available for telemetry in createMessage.")
}
}
}
@@ -409,7 +408,7 @@ export class GeminiHandler implements ApiHandler {
trace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens ?? 0, cost: cacheReadCost }
}
// Logger.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
// console.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
return totalCost
}
@@ -451,13 +450,13 @@ export class GeminiHandler implements ApiHandler {
})
if (response.totalTokens === undefined) {
Logger.warn("Gemini token counting returned undefined, using fallback")
console.warn("Gemini token counting returned undefined, using fallback")
return this.estimateTokens(content)
}
return response.totalTokens
} catch (error) {
Logger.warn("Gemini token counting failed, using fallback", error)
console.warn("Gemini token counting failed, using fallback", error)
return this.estimateTokens(content)
}
}
@@ -476,7 +475,7 @@ export class GeminiHandler implements ApiHandler {
const jsonStr = JSON.stringify(block)
return total + jsonStr.length
} catch (e) {
Logger.warn("Failed to stringify block for token estimation", e)
console.warn("Failed to stringify block for token estimation", e)
return total
}
}
+5 -6
View File
@@ -4,7 +4,6 @@ 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"
@@ -71,7 +70,7 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
const data: LiteLlmModelInfoResponse = await response.json()
return data
} else {
Logger.error("Failed to fetch LiteLLM model info:", response.statusText)
console.error("Failed to fetch LiteLLM model info:", response.statusText)
// Try with Authorization header instead
const retryResponse = await fetch(url, {
method: "GET",
@@ -85,12 +84,12 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
const data: LiteLlmModelInfoResponse = await retryResponse.json()
return data
} else {
Logger.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
console.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
throw new Error(`Failed to fetch LiteLLM model info: ${retryResponse.statusText}`)
}
}
} catch (error) {
Logger.error("Error fetching LiteLLM model info:", error)
console.error("Error fetching LiteLLM model info:", error)
throw error
}
}
@@ -170,7 +169,7 @@ export class LiteLlmHandler implements ApiHandler {
}
}
} catch (error) {
Logger.warn("Error getting LiteLLM model cost info:", error)
console.warn("Error getting LiteLLM model cost info:", error)
}
// Fallback to zero costs if we can't get the information
@@ -201,7 +200,7 @@ export class LiteLlmHandler implements ApiHandler {
return totalCost
} catch (error) {
Logger.error("Error calculating spend:", error)
console.error("Error calculating spend:", error)
return undefined
}
}
+3 -3
View File
@@ -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 {
Logger.error("Error calculating spend:", response.statusText)
console.error("Error calculating spend:", response.statusText)
return undefined
}
} catch (error) {
Logger.error("Error calculating spend:", error)
console.error("Error calculating spend:", error)
return undefined
}
}
+2 -3
View File
@@ -2,7 +2,6 @@ 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"
@@ -94,7 +93,7 @@ export class OllamaHandler implements ApiHandler {
}
}
} catch (streamError: any) {
Logger.error("Error processing Ollama stream:", streamError)
console.error("Error processing Ollama stream:", streamError)
throw new Error(`Ollama stream processing error: ${streamError.message || "Unknown error"}`)
}
} catch (error) {
@@ -108,7 +107,7 @@ export class OllamaHandler implements ApiHandler {
const statusCode = error.status || error.statusCode
const errorMessage = error.message || "Unknown error"
Logger.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
console.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
throw error
}
}
+41 -17
View File
@@ -1,11 +1,16 @@
import { ModelInfo, OpenAiCodexModelId, openAiCodexDefaultModelId, openAiCodexModels } from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool } from "openai/resources/chat/completions"
import * as os from "os"
import { v7 as uuidv7 } from "uuid"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import OpenAI from "openai"
import type { ChatCompletionTool } from "openai/resources/chat/completions"
import {
ModelInfo,
OpenAiCodexModelId,
openAiCodexDefaultModelId,
openAiCodexModels,
} from "@shared/api"
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"
@@ -48,9 +53,7 @@ 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
@@ -87,7 +90,11 @@ 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
@@ -97,7 +104,9 @@ 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
@@ -170,7 +179,11 @@ 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()
@@ -224,7 +237,11 @@ 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
@@ -285,7 +302,10 @@ 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 = ""
@@ -297,9 +317,7 @@ 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")
@@ -362,7 +380,10 @@ 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
@@ -463,7 +484,10 @@ 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]
+5 -5
View File
@@ -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) {
Logger.error("Error processing tool call delta:", error, delta.tool_calls)
console.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"
) {
Logger.log("Ran out of tokens")
console.log("Ran out of tokens")
if (chunk.response?.output_text?.length > 0) {
Logger.log("Partial output:", chunk.response.output_text)
console.log("Partial output:", chunk.response.output_text)
} else {
Logger.log("Ran out of tokens during reasoning")
console.log("Ran out of tokens during reasoning")
}
}
+6 -7
View File
@@ -7,7 +7,6 @@ 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"
@@ -81,7 +80,7 @@ export class OpenRouterHandler implements ApiHandler {
// Check for error field directly on chunk
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
Logger.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
console.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}`)
@@ -96,7 +95,7 @@ export class OpenRouterHandler implements ApiHandler {
const choiceWithError = choice as any
if (choiceWithError.error) {
const error = choiceWithError.error
Logger.error(
console.error(
`OpenRouter Mid-Stream Error: ${error?.code || "Unknown"} - ${error?.message || "Unknown error"}`,
)
// Format error details
@@ -180,7 +179,7 @@ export class OpenRouterHandler implements ApiHandler {
try {
const generationIterator = this.fetchGenerationDetails(this.lastGenerationId)
const generation = (await generationIterator.next()).value
// Logger.log("OpenRouter generation details:", generation)
// console.log("OpenRouter generation details:", generation)
return {
type: "usage",
cacheWriteTokens: 0,
@@ -192,7 +191,7 @@ export class OpenRouterHandler implements ApiHandler {
}
} catch (error) {
// ignore if fails
Logger.error("Error fetching OpenRouter generation details:", error)
console.error("Error fetching OpenRouter generation details:", error)
}
}
return undefined
@@ -200,7 +199,7 @@ export class OpenRouterHandler implements ApiHandler {
@withRetry({ maxRetries: 4, baseDelay: 250, maxDelay: 1000, retryAllErrors: true })
async *fetchGenerationDetails(genId: string) {
// Logger.log("Fetching generation details for:", genId)
// console.log("Fetching generation details for:", genId)
try {
const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, {
headers: {
@@ -212,7 +211,7 @@ export class OpenRouterHandler implements ApiHandler {
yield response.data?.data
} catch (error) {
// ignore if fails
Logger.error("Error fetching OpenRouter generation details:", error)
console.error("Error fetching OpenRouter generation details:", error)
throw error
}
}
+1 -2
View File
@@ -6,7 +6,6 @@ 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"
@@ -77,7 +76,7 @@ export class QwenCodeHandler implements ApiHandler {
const credsStr = await fs.readFile(keyFile, "utf-8")
return JSON.parse(credsStr)
} catch (error) {
Logger.error(
console.error(
`Error reading or parsing credentials file at ${getQwenCachedCredentialPath(this.options.qwenCodeOauthPath)}`,
)
throw new Error(`Failed to load Qwen OAuth credentials: ${error}`)
+1 -2
View File
@@ -12,7 +12,6 @@ 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"
@@ -135,7 +134,7 @@ export class QwenHandler implements ApiHandler {
try {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
} catch (error) {
Logger.error("Error processing tool call delta:", error, delta.tool_calls)
console.error("Error processing tool call delta:", error, delta.tool_calls)
}
}
+21 -22
View File
@@ -12,7 +12,6 @@ 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"
@@ -145,7 +144,7 @@ namespace Bedrock {
}
// Log unsupported content types for debugging
Logger.warn(`Unsupported content type: ${(item as ContentItem).type}`)
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
return null
})
.filter((item): item is BedrockContentBlock => item !== null)
@@ -214,7 +213,7 @@ namespace Bedrock {
},
}
} catch (error) {
Logger.error("Failed to process image content:", error)
console.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 {
@@ -413,7 +412,7 @@ export class SapAiCoreHandler implements ApiHandler {
url: destination.url || this.options.sapAiCoreBaseUrl!,
}
} catch (error) {
Logger.error("Failed to create AI Core destination:", error)
console.error("Failed to create AI Core destination:", error)
throw new Error(`Unable to create AI Core destination: ${error instanceof Error ? error.message : String(error)}`)
}
}
@@ -474,7 +473,7 @@ export class SapAiCoreHandler implements ApiHandler {
})
.filter((deployment: any) => deployment !== null)
} catch (error) {
Logger.error("Error fetching deployments:", error)
console.error("Error fetching deployments:", error)
throw new Error("Failed to fetch deployments")
}
}
@@ -578,7 +577,7 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
} catch (error) {
Logger.error("Error in SAP orchestration mode:", error)
console.error("Error in SAP orchestration mode:", error)
throw error
}
}
@@ -597,7 +596,7 @@ export class SapAiCoreHandler implements ApiHandler {
if (!deploymentId) {
// Fallback to runtime deployment id fetching for users who haven't opened the SAP provider UI
Logger.log(`No pre-configured deployment ID found for model ${model.id}, falling back to runtime fetching`)
console.log(`No pre-configured deployment ID found for model ${model.id}, falling back to runtime fetching`)
deploymentId = await this.getDeploymentForModel(model.id)
}
@@ -801,8 +800,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
Logger.error("Error status:", error.response.status)
Logger.error("Error headers:", error.response.headers)
console.error("Error status:", error.response.status)
console.error("Error headers:", error.response.headers)
// Handle error data - need to read stream if responseType was 'stream'
let errorMessage = "Unknown error"
@@ -831,10 +830,10 @@ export class SapAiCoreHandler implements ApiHandler {
} else if (typeof error.response.data === "object") {
errorMessage = JSON.stringify(error.response.data, null, 2)
}
Logger.error("Error data:", errorMessage)
console.error("Error data:", errorMessage)
} catch (e) {
Logger.error("Failed to read error data:", e)
Logger.error("Raw error data:", error.response.data)
console.error("Failed to read error data:", e)
console.error("Raw error data:", error.response.data)
}
}
@@ -847,11 +846,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
Logger.error("Error request:", error.request)
console.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
Logger.error("Error message:", error.message)
console.error("Error message:", error.message)
throw new Error(`Error setting up request: ${error.message}`)
}
}
@@ -899,13 +898,13 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
} catch (error) {
Logger.error("Failed to parse JSON data:", error)
console.error("Failed to parse JSON data:", error)
}
}
}
}
} catch (error) {
Logger.error("Error streaming completion:", error)
console.error("Error streaming completion:", error)
throw error
}
}
@@ -966,7 +965,7 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
} catch (error) {
Logger.error("Failed to parse JSON data:", error)
console.error("Failed to parse JSON data:", error)
yield {
type: "text",
text: `[ERROR] Failed to parse response data: ${error instanceof Error ? error.message : String(error)}`,
@@ -976,7 +975,7 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
} catch (error) {
Logger.error("Error streaming completion:", error)
console.error("Error streaming completion:", error)
yield {
type: "text",
text: `[ERROR] Failed to process stream: ${error instanceof Error ? error.message : String(error)}`,
@@ -1045,13 +1044,13 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
} catch (error) {
Logger.error("Failed to parse GPT JSON data:", error)
console.error("Failed to parse GPT JSON data:", error)
}
}
}
}
} catch (error) {
Logger.error("Error streaming GPT completion:", error)
console.error("Error streaming GPT completion:", error)
throw error
}
}
@@ -1110,13 +1109,13 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
} catch (error) {
Logger.error("Failed to parse Gemini JSON data:", error)
console.error("Failed to parse Gemini JSON data:", error)
}
}
}
}
} catch (error) {
Logger.error("Error streaming Gemini completion:", error)
console.error("Error streaming Gemini completion:", error)
throw error
}
}
+3 -4
View File
@@ -4,7 +4,6 @@ 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"
@@ -126,11 +125,11 @@ export class VercelAIGatewayHandler implements ApiHandler {
}
if (!didOutputUsage) {
Logger.warn("Vercel AI Gateway did not provide usage information in stream")
console.warn("Vercel AI Gateway did not provide usage information in stream")
}
} catch (error: any) {
Logger.error("Vercel AI Gateway error details:", error)
Logger.error("Error stack:", error.stack)
console.error("Vercel AI Gateway error details:", error)
console.error("Error stack:", error.stack)
throw new Error(`Vercel AI Gateway error: ${error.message}`)
}
}
+17 -18
View File
@@ -3,7 +3,6 @@ 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"
@@ -124,7 +123,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)) {
* Logger.log(chunk);
* console.log(chunk);
* }
* ```
*/
@@ -148,7 +147,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
this.client = null
this.ensureCleanState()
} catch (error) {
Logger.error("Error during configuration change cleanup:", error)
console.error("Error during configuration change cleanup:", error)
}
}
})
@@ -280,7 +279,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
private async getClient(): Promise<vscode.LanguageModelChat> {
if (!this.client) {
Logger.debug("Cline <Language Model API>: Getting client with options:", {
console.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) : [],
@@ -289,11 +288,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 || {}
Logger.debug("Cline <Language Model API>: Creating client with selector:", selector)
console.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"
Logger.error("Cline <Language Model API>: Client creation failed:", message)
console.error("Cline <Language Model API>: Client creation failed:", message)
throw new Error(`Cline <Language Model API>: Failed to create client: ${message}`)
}
}
@@ -414,7 +413,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
if (chunk instanceof vscode.LanguageModelTextPart) {
// Validate text part value
if (typeof chunk.value !== "string") {
Logger.warn("Cline <Language Model API>: Invalid text part value received:", chunk.value)
console.warn("Cline <Language Model API>: Invalid text part value received:", chunk.value)
continue
}
@@ -427,18 +426,18 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
try {
// Validate tool call parameters
if (!chunk.name || typeof chunk.name !== "string") {
Logger.warn("Cline <Language Model API>: Invalid tool name received:", chunk.name)
console.warn("Cline <Language Model API>: Invalid tool name received:", chunk.name)
continue
}
if (!chunk.callId || typeof chunk.callId !== "string") {
Logger.warn("Cline <Language Model API>: Invalid tool callId received:", chunk.callId)
console.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") {
Logger.warn("Cline <Language Model API>: Invalid tool input received:", chunk.input)
console.warn("Cline <Language Model API>: Invalid tool input received:", chunk.input)
continue
}
@@ -454,7 +453,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
accumulatedText += toolCallText
// Log tool call for debugging
Logger.debug("Cline <Language Model API>: Processing tool call:", {
console.debug("Cline <Language Model API>: Processing tool call:", {
name: chunk.name,
callId: chunk.callId,
inputSize: JSON.stringify(chunk.input).length,
@@ -465,10 +464,10 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
text: toolCallText,
}
} catch (error) {
Logger.error("Cline <Language Model API>: Failed to process tool call:", error)
console.error("Cline <Language Model API>: Failed to process tool call:", error)
}
} else {
Logger.warn("Cline <Language Model API>: Unknown chunk type received:", chunk)
console.warn("Cline <Language Model API>: Unknown chunk type received:", chunk)
}
}
@@ -490,7 +489,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
}
if (error instanceof Error) {
Logger.error("Cline <Language Model API>: Stream error details:", {
console.error("Cline <Language Model API>: Stream error details:", {
message: error.message,
stack: error.stack,
name: error.name,
@@ -501,12 +500,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)
Logger.error("Cline <Language Model API>: Stream error object:", errorDetails)
console.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)
Logger.error("Cline <Language Model API>: Unknown stream error:", errorMessage)
console.error("Cline <Language Model API>: Unknown stream error:", errorMessage)
throw new Error(`Cline <Language Model API>: Response stream error: ${errorMessage}`)
}
}
@@ -527,7 +526,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) {
Logger.warn(`Cline <Language Model API>: Client missing ${prop} property`)
console.warn(`Cline <Language Model API>: Client missing ${prop} property`)
}
}
@@ -558,7 +557,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
? stringifyVsCodeLmModelSelector(this.options.vsCodeLmModelSelector)
: "vscode-lm"
Logger.debug("Cline <Language Model API>: No client available, using fallback model info")
console.debug("Cline <Language Model API>: No client available, using fallback model info")
return {
id: fallbackId,
+1 -3
View File
@@ -1,5 +1,3 @@
import { Logger } from "@/shared/services/Logger"
interface RetryOptions {
maxRetries?: number
baseDelay?: number
@@ -74,7 +72,7 @@ export function withRetry(options: RetryOptions = {}) {
try {
await handlerInstance.options.onRetryAttempt(attempt + 1, maxRetries, delay, error)
} catch (e) {
Logger.error("Error in onRetryAttempt callback:", e)
console.error("Error in onRetryAttempt callback:", e)
}
}
+4 -5
View File
@@ -1,6 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { Logger } from "@/shared/services/Logger"
const o1SystemPrompt = (systemPrompt: string) => `
# System Prompt
@@ -318,7 +317,7 @@ function parseToolCall(toolName: string, content: string): ToolCall | null {
// Validate required parameters
if (!validateToolInput(toolName, tool_input)) {
Logger.error(`Invalid tool call for ${toolName}:`, content)
console.error(`Invalid tool call for ${toolName}:`, content)
return null
}
@@ -359,8 +358,8 @@ function validateToolInput(toolName: string, tool_input: Record<string, string>)
// </write_to_file>`;
//
// const { normalText, toolCalls } = parseAIResponse(aiResponse);
// Logger.log(normalText);
// Logger.log(toolCalls);
// console.log(normalText);
// console.log(toolCalls);
// Convert OpenAI response to Anthropic format
export function convertO1ResponseToAnthropicMessage(
@@ -433,4 +432,4 @@ export function convertO1ResponseToAnthropicMessage(
// usage: { prompt_tokens: 50, completion_tokens: 100 }
// };
// const anthropicMessage = convertO1ResponseToAnthropicMessage(openAICompletion);
// Logger.log(anthropicMessage);
// console.log(anthropicMessage);
+2 -3
View File
@@ -9,7 +9,6 @@ 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
@@ -425,7 +424,7 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
try {
parsedInput = JSON.parse(toolCall.function?.arguments || "{}")
} catch (error) {
Logger.error("Failed to parse tool arguments:", error)
console.error("Failed to parse tool arguments:", error)
}
return {
type: "tool_use",
@@ -440,7 +439,7 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
return anthropicMessage
}
} catch (error) {
Logger.error("Error converting OpenAI message to Anthropic format:", error)
console.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 "@/shared/services/Logger"
import { Logger } from "@/services/logging/Logger"
import type { ApiStreamToolCallsChunk } from "./stream"
/**
+1 -2
View File
@@ -1,6 +1,5 @@
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.
@@ -24,7 +23,7 @@ export function asObjectSafe(value: any): object {
return {}
} catch (error) {
Logger.warn("Cline <Language Model API>: Failed to parse object:", error)
console.warn("Cline <Language Model API>: Failed to parse object:", error)
return {}
}
}
+4 -4
View File
@@ -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"
) {
Logger.log("Ran out of tokens")
console.log("Ran out of tokens")
if (chunk.response?.output_text?.length > 0) {
Logger.log("Partial output:", chunk.response.output_text)
console.log("Partial output:", chunk.response.output_text)
} else {
Logger.log("Ran out of tokens during reasoning")
console.log("Ran out of tokens during reasoning")
}
}
+11 -12
View File
@@ -3,8 +3,7 @@ import { describe, it } from "mocha"
import { constructNewFileContent as cnfc } from "./diff"
async function cnfc2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
const result = await cnfc(diffContent, originalContent, isFinal, "v2")
return result.newContent
return cnfc(diffContent, originalContent, isFinal, "v2")
}
describe("constructNewFileContent", () => {
@@ -218,13 +217,13 @@ replaced
} else {
const result1 = await cnfc(diff, original, isFinal ?? true)
const result2 = await cnfc2(diff, original, isFinal ?? true)
const _equal = result1.newContent === result2
const _equal2 = result1.newContent === expected
const _equal = result1 === result2
const _equal2 = result1 === expected
// Verify both implementations produce same result
expect(result1.newContent).to.equal(result2)
expect(result1).to.equal(result2)
// Verify result matches expected
expect(result1.newContent).to.equal(expected)
expect(result1).to.equal(expected)
}
})
})
@@ -265,7 +264,7 @@ replaced`
// Should still work and replace line2 with "replaced"
const expected = "line1\nreplaced\nline3"
expect(result1.newContent).to.equal(expected)
expect(result1).to.equal(expected)
})
it("should handle missing final REPLACE marker with multiple lines of replacement", async () => {
@@ -282,7 +281,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.newContent).to.equal(expected)
expect(result1).to.equal(expected)
})
// it("should NOT process incomplete replacement when isFinal is false", async () => {
@@ -320,7 +319,7 @@ new second
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "first\nnew second\nthird\nnew fourth\n"
expect(result1.newContent).to.equal(expectedResult)
expect(result1).to.equal(expectedResult)
})
it("should handle multiple out-of-order replacements", async () => {
@@ -343,7 +342,7 @@ fifth
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "one\nsecond\nthree\nfourth\nfifth\n"
expect(result1.newContent).to.equal(expectedResult)
expect(result1).to.equal(expectedResult)
})
it("should handle out-of-order replacements with indentation", async () => {
@@ -361,7 +360,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.newContent).to.equal(expectedResult)
expect(result1).to.equal(expectedResult)
})
it("should handle out-of-order replacements with empty lines", async () => {
@@ -381,6 +380,6 @@ new body content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "header\nnew body content\nnew footer\n"
expect(result1.newContent).to.equal(expectedResult)
expect(result1).to.equal(expectedResult)
})
})

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