Compare commits

..

56 Commits

Author SHA1 Message Date
nighttrek 2e11e07411 refactor(ts): improve type safety for request ID capture and empty-assistant diagnostics
cline.ts: strongly type custom fetch override using Parameters<typeof fetch>/ReturnType<typeof fetch>; safe URL extraction for string|URL|Request; removed any casts.

task/index.ts: encapsulate requestId retrieval via getApiRequestIdSafe(); avoid any; no behavior change. Scope limited to PR 6066-related changes.
2025-09-08 15:46:09 -07:00
Daniel Steigman 32bdc3884e Merge branch 'main' into nighttrek/api-error-tracking 2025-09-08 12:07:30 -07:00
Jose R. Perez 4f35a5975a feat: adding hubspot analytics to docs (#6081)
* feat: adding hubspot analytics to docs

* Update docs/hubspot.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update docs/hubspot.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-08 12:06:38 -07:00
Sarah Fortune 17fd4ceb7f Prevent VSCode API calls from being re-introduced. (#6075)
* Prevent VSCode API calls from being re-introduced.

Add more grit patterns for the VSCode.

* Dont check for telemetry APIs yet
2025-09-08 11:58:36 -07:00
Sarah Fortune a231ee4d5c Use HostBridge to open diff (#6076)
* Use HostBridge to open diff

Use the host bridge to multi file diff. Using the VSCode API directly will not work on JetBrains.

* Use await
2025-09-08 11:20:36 -07:00
Daniel Steigman c389507c7b Delete PR_BODY_nighttrek_api_error_tracking.md 2025-09-08 10:54:22 -07:00
Sarah Fortune 531a272a63 Remove uses of VSCode API (#6073)
Remove uses of the VSCode API that was reintroduced in #5667.

The VSCode API is not portable, code using it will not work on JetBrains,
2025-09-08 07:13:10 -07:00
yuvalman 6b9ac49d0a feat: fetch deployment id in design-time instead of runtime (#6052) 2025-09-08 06:56:55 -07:00
Sarah Fortune 6d42a3fe0c Refactoring: Move the host bridge client out of the protobus-service file. (#6062) 2025-09-08 02:10:30 -07:00
Daniel Steigman d291c11850 Merge branch 'main' into nighttrek/api-error-tracking 2025-09-08 01:36:29 -07:00
NightTrek 57fa7678c1 feat(cline): capture real HTTP request ID (X-Request-ID) in ClineHandler via custom fetch; expose getLastRequestId(); prefer true requestId over generationId in Task telemetry for empty-assistant-message 2025-09-08 01:30:04 -07:00
Sarah Fortune 5c9c37a075 Remove uses of the VSCode API (#6071)
* Remove use of VSCode API

Remove unused field `disposables`.

* Remove unused field disposables
2025-09-08 01:28:49 -07:00
NightTrek edf18b318d added telemetry around uexpected API responses 2025-09-07 22:38:20 -07:00
Sarah Fortune d08c394f68 Use the correct binary path for cline-core (#6065)
Binaries will be the cwd.

When tools fail with an error, log the exception to the console. This makes debugging cline-core easier.

# Conflicts:
#	src/services/ripgrep/index.ts
#	src/standalone/cline-core.ts
2025-09-07 22:14:29 -07:00
Sarah Fortune c0ca9889ff Add platform-agnostic support for ripgrep (#6063)
Add a method to the host provider that returns the location of the ripgrep binary.

Update the places where ripgrep is called.
2025-09-07 21:39:13 -07:00
Sarah Fortune fc19697fad Support node modules that include binaries in cline-core (#6046)
* Fix check for debug build in package-standalone script

* Support node modules that include binaries

Add support to the scripts/package-standalone.js for node modules that use platform-specific binary modules.

By default the script bundles the module for all platforms, this can be disabled with -s for single platform builds.

The module for each platform will be bundled into standalone.zip in platform specific directories:
```
binaries/linux-arm64/node_modules
binaries/darwin-x64/node_modules
binaries/win32-x64/node_modules
binaries/darwin-arm64/node_modules
binaries/linux-x64/node_modules
```

When running cline-core add the correct directory to the NODE_PATH, e.g.
```
$ export NODE_PATH=./node_modules/:./binaries/darwin-arm64/node_modules/
cline:/tmp/1$ node cline-core.js
Loading stubs...
Finished loading stubs
Loading stub impls...
Finished loading stub impls...
Cline environment: production
XS variant configuration warnings: [
  'Component overrides for unused components: TOOL_USE_SECTION, TOOLS_SECTION, MCP_SECTION, TODO_SECTION, FEEDBACK_SECTION',
  'Missing recommended components: TOOL_USE_SECTION'
]
[2025-09-05T19:53:00.853] #bot.cline.server.ts Running standalone cline  0.0.1
[2025-09-05T19:53:00.854] #bot.cline.server.ts Using settings dir: /Users/sjf/.cline/data
Finished loading vscode context...
[2025-09-05T19:53:00.858] #bot.cline.server.ts

Starting cline-core service...

sjfsjf created DBBBBBBBBB:  Database {
  name: '/tmp/db.sql',
  open: true,
  inTransaction: false,
  readonly: false,
  memory: false
}
```

* Apply suggestion from @ellipsis-dev[bot]

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Apply suggestion from @ellipsis-dev[bot]

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Support node modules that include binaries

Add support to the scripts/package-standalone.js for node modules that use platform-specific binary modules.

By default the script bundles the module for all platforms, this can be disabled with -s for single platform builds.

The module for each platform will be bundled into standalone.zip in platform specific directories:
```
binaries/linux-arm64/node_modules
binaries/darwin-x64/node_modules
binaries/win32-x64/node_modules
binaries/darwin-arm64/node_modules
binaries/linux-x64/node_modules
```

When running cline-core add the correct directory to the NODE_PATH, e.g.
```
$ export NODE_PATH=./node_modules/:./binaries/darwin-arm64/node_modules/
cline:/tmp/1$ node cline-core.js
Loading stubs...
Finished loading stubs
Loading stub impls...
Finished loading stub impls...
Cline environment: production
XS variant configuration warnings: [
  'Component overrides for unused components: TOOL_USE_SECTION, TOOLS_SECTION, MCP_SECTION, TODO_SECTION, FEEDBACK_SECTION',
  'Missing recommended components: TOOL_USE_SECTION'
]
[2025-09-05T19:53:00.853] #bot.cline.server.ts Running standalone cline  0.0.1
[2025-09-05T19:53:00.854] #bot.cline.server.ts Using settings dir: /Users/sjf/.cline/data
Finished loading vscode context...
[2025-09-05T19:53:00.858] #bot.cline.server.ts

Starting cline-core service...

sjfsjf created DBBBBBBBBB:  Database {
  name: '/tmp/db.sql',
  open: true,
  inTransaction: false,
  readonly: false,
  memory: false
}
```

* Apply suggestion from @ellipsis-dev[bot]

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Apply suggestion from @ellipsis-dev[bot]

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Apply suggestion from @ellipsis-dev[bot]

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Remove test code

* Use the correct target directory for the binaries

The directory structure that the JB host expects is does
not exactly match ${arch}-${os}

* Update package-standalone script

Warn if there is module that needs binaries, but it is not being used.
Reset the binaries dir before packaging.

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-09-07 16:31:06 -07:00
Sarah Fortune f64e8ddbf7 Remove warning about using int64 in proto files (#6061) 2025-09-07 14:42:22 -07:00
Sarah Fortune 70d1dce34f Increase the compression level for standalone.zip (#6056)
This reduces the size of the zip from 18MB to 15MB, it doesn't really change the amount of time it takes to make the zip.
2025-09-06 11:57:54 -07:00
watany 950142fdfb feat(gpt) reasoning effort minimal (#5900)
* feat(OpenAI): Reasoning Effort Minimal

* changeset
2025-09-05 21:32:47 -07:00
Bee 41765fc29a Fix styling for TelemetryBanner and SettingsView (#6031)
* Fix styling for TelemetryBanner and SettingsView

- Replace styled-components with Tailwind classes in TelemetryBanner
- Add performance optimizations to SettingsView with memoization and debouncing
- Update Tailwind config to support new banner color variables
- Improve component structure and reduce bundle size

* use lodash

* memo content map

* Update E2E test
2025-09-05 20:51:35 -07:00
Bee e5f8c048ce Re-render account view on auth change (#6043)
* Add key prop to ClineAccountView

Add key prop to ClineAccountView to force re-render on user change

Add key={clineUser.uid} to ClineAccountView component to ensure proper
component re-mounting when user changes, preventing stale state issues.

* changeset added
2025-09-05 20:38:50 -07:00
Saoud Rizwan 83de7bb2f1 Fix bug where user message for mistake_limit_reached was not being shown in chat view (#5965) 2025-09-05 20:12:02 -07:00
Bee 6c842c145f sets Biome as global default formatter (#6045)
Replace language-specific Biome formatter settings with a single global default formatter configuration for the repo.
2025-09-05 17:25:32 -07:00
Sarah Fortune 09adaca575 Fix check for debug build in package-standalone script (#6044) 2025-09-05 15:41:29 -07:00
Bee 0b1a237290 Unify tool name vars (#6012)
* Use ClineDefaultTool

* Use ClineDefaultTool
2025-09-05 11:18:55 -07:00
canvrno cb1bda9b5c Checkpoints refactor (#4452)
* add grok coder free model to cline provider (#5808)

* add free grok-coder-free model to cline provider

* add changeset

* fix typo

* checkpoints class created

* Added saveCheckpoint to Checkpoint class

* Rebased and updated for new ClineMessages

* Moved things around, started on saveCheckpoint

* implemented handler for checking and initializing the checkpointTracker if not already done

* Moved restoreCheckpoint and handleSucessfullRestore to checkpoints class

* Moved presentMultiDiff, not yet connected

* Migrated doesLatestTaskCompletionHaveNewChanges and compelted migration on presentMultifileDiff

* Better init handling

* moved fileContextTracker to new checkpoints class

* Checkpoints state management

* refactoring and cleanup in saveCheckpoint, init handler

* More saveCheckpoint refactoring

* Added sayTs return to say function for better async clineMessages updates

* Refactor checkpoint system with timestamp tracking and dependency separation

* More refactoring

* Friendship ended with checkpointTracker, checkpointManager is new best friend

* Better error handling

* checkpointTrackerErrorMessage > checkpointManagerErrorMessage

* Addressed possible race condition with message(Ts)

* Better error handling and 15s timeout changes

* Remove checkpoint delegation methods and call checkpoint manager directly

* updating checkpoints protos

* cleanup

* Restored autoApprove entry to task class

* cleanup

* cleanup

* Post-rebase fixes

* Migrated timeout and state changes from PR #5015

* Extract toolExecutor callback functions to private methods for readability

* Updated info/error messages to use HostProvider

* post rebase fixes

* Compare/diff button fix

* Fix lint errors

* Update webview-ui/src/components/chat/task-header/TaskHeader.tsx

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* Update src/integrations/checkpoints/index.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Fixed bad merge, updated focus change for compatability with new say()

* Fixed error message propagation issue

---------

Co-authored-by: pashpashpash <nik@cline.bot>
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-09-05 00:06:53 -07:00
canvrno 5dfd16359d e2e tests for slash/mentions in chat text area (#6022)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-04 22:46:23 -07:00
arman-sambanova 64a8096f96 sambanova provider: add DeepSeek-V3.1 (#6001) 2025-09-04 22:19:00 -07:00
Saoud Rizwan e5a1d11179 Fix empty taskHistory.json file being read and parsed on startup, leading to crash (#6016) 2025-09-04 22:16:10 -07:00
Wei Chen 5af6e8d5ed Improve the deep-planning prompt by excluding dependency folders on find commands (#5884) 2025-09-04 22:15:50 -07:00
Bee 6ecadfc7a1 fix parameterless tool docs in system prompt (#6018)
- Recognize GPT-5 IDs (including openai/gpt-5) in model family detection and variants
- Add GPT-5 (OpenAI) to integration matrix and unit tests
- Update snapshot test instructions to use npm run test:unit -- --update-snapshots
- Fix PromptBuilder: don’t early-return on missing params; init to [] to keep output consistent
- Tidy test diff formatting (braces, explicit returns)
- Polish load_mcp_documentation tool description
2025-09-04 21:03:59 -07:00
Saoud Rizwan eb3158c83d Fix updating focus chain when attempt_completion is called (#6019) 2025-09-04 20:54:13 -07:00
Saoud Rizwan 625b701a68 Update branch for hotfix adding new Kimi K2 model to Groq and Moonshot providers (#6017)
* Add 200k context window variant for Claude Sonnet 4 to OpenRouter and Cline providers

* v3.26.7 Release Notes

* Fix grok-code-fast-1 info

* v3.27.0 Release Notes

* Add new kimi model to groq and moonshot providers

* v3.27.1 Release Notes
2025-09-04 20:27:06 -07:00
pashpashpash dd59acf0fb Change supportsPromptCache to false (#6013) 2025-09-04 18:38:11 -07:00
pashpashpash 335d0ffa05 adding new kimi model to groq and moonshot providers (#6006)
* adding new kimi model to groq and moonshot providers

* added fireworks provider too

* fixing fireworks test

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-04 17:47:58 -07:00
canvrno 3f341d9cf7 Fix: Slash command / mention features remove following word (#5986)
* Fixed issue where slash command feature would remove first word after the command name

* Removed slashCommandsQueryRef

* Add fix for mentions as well

* Updated mentions test

---------

Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-04 12:01:13 -10:00
celestial-vault 0adb1046e4 fix taskHistory migration (#6004)
* fix taskHistory migration when run across main and the prod version which use the old and the new location

* Update src/core/storage/state-migrations.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* Update src/core/storage/state-migrations.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* Update src/core/storage/state-migrations.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* Update src/core/storage/state-migrations.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* Update src/core/storage/state-migrations.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* Adding Multi root algo

* Adding Multi root algo

* Simplify the migration function

Simplify the migration function by consolidating conditional logic,
removing redundant logging, and using concurrent operations for
better performance. The logic now handles both empty and populated
destination scenarios more clearly.

* update

---------

Co-authored-by: Ara <arafat.da.khan@gmail.com>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
Co-authored-by: abeatrix <beatrix@cline.bot>
2025-09-04 14:45:24 -07:00
Sarah Fortune 324564af72 Don't keep regenerating the telemetry UUID (#6005)
Some environments do not return a value for the vscode machine ID.
For these cases we are generating a UUID. But the UUID was not being stored, so a new one was being created everytime the extension started.

Start storing the generated ID in the global state.

I am changing the name of the key to be more descriptive, because we only want to store the generated ID. If there is an actual machine ID, we should just use that and not store it. The old key name was only ever been read, it was never written, so this will have no affect on existing users.
2025-09-04 12:39:27 -07:00
JicLotus 291287c8e4 chord: Optional gRPC session recording system to improve observability (#5999)
chord: Optional gRPC session recording system to improve observability
2025-09-04 21:26:58 +02:00
Bee d69fb10cfd fix: only focus chat input when in chat view (#5991)
* fix: only focus chat input when in chat view

- Only focus chat input when chat view is visible, not when hidden (in other view)
- Wrap onDone callback in arrow function for consistency
- Replace inline styles with Tailwind classes for button container

* update gap value
2025-09-04 10:39:09 -07:00
celestial-vault 0f6eab2bfe make proto objects not optional by default (#5985) 2025-09-03 19:47:14 -07:00
Saoud Rizwan 016ac1eade Fix diff viewer failing showing incorrect error message (#5988) 2025-09-03 19:17:34 -07:00
Bee 2606ed7abf Remove CI environment check from PostHog config selection (#5987)
Remove process.env.CI check and rely only on IS_DEV flag to determine whether to use development or production PostHog configuration.
This is because process.env.CI is always true during the publish GitHub workflow.
2025-09-03 19:07:32 -07:00
Ara 3ed4d55a36 Add telemetry tracking for terminal hang detection and user intervention (#5955)
* Add telemetry tracking for terminal hang detection and user interventions

- Add terminal hang detection with configurable timeouts for buffer stuck, stream timeout, and completion waiting stages
- Track user interventions when clicking "Process while Running" button
- Add telemetry for terminal output failures with specific failure reasons
- Implement comprehensive monitoring of terminal process lifecycle events
- Add metrics for shell integration usage and terminal operation performance

* Remove Shortcut of Cline from its title

* Remove Shortcut of Cline from its title

* Remove Shortcut of Cline from its title
2025-09-03 16:42:45 -07:00
celestial-vault a3b4ad53dc Add filewatcher to taskHistory (#5910)
* add filewatcher to taskHistory to update across Cline instances

* changeset
2025-09-03 12:08:51 -07:00
celestial-vault bd98955f57 Move updateDefaultTerminalProfile RPC into updateSettings RPC (#5976)
* move browserSettings rpc into updateSettings

* move updateDefaultTerminalProfile rpc into updateSettings rpc
2025-09-03 11:53:56 -07:00
celestial-vault 297cfbb895 move browserSettings rpc into updateSettings (#5959) 2025-09-03 11:40:29 -07:00
canvrno b4c95fbeab Respect user's settings when evaluating feature flag for focus chain (#5969)
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-03 08:32:35 -07:00
JicLotus 5e8102e588 chord: Helper script for testing standalone Core API server (#5944)
* chord: Adding a new standalone core api server script for local and integration testing purposes
2025-09-03 08:54:58 +02:00
Saoud Rizwan 60f2c3a22b Update bug report template: correct model name to "Claude Sonnet 4" and change model section validation from required to optional. 2025-09-02 21:48:11 -07:00
Saoud Rizwan 7472b1cb4a Fix github issue default value 2025-09-02 21:45:57 -07:00
Saoud Rizwan 794d8bccd3 Fix github issue template formatting 2025-09-02 21:45:02 -07:00
Saoud Rizwan 0db3cfe34d Enhance bug report template with additional fields for plugin type and Cline version. Update placeholder text for provider/model input. Remove required validation for logs section. 2025-09-02 21:41:52 -07:00
Nick Baumann 5c878b5b84 Jetbrains docs (early access language tweaks) (#5957)
* docs: Update Claude Code documentation to include Pro plans alongside Max plans

* Add JetBrains installation documentation

- Create comprehensive installation guide for JetBrains IDEs
- Include both marketplace and manual installation methods
- Document supported IDEs and key differences from VSCode
- Add alpha status note and terminal integration limitations
- Update docs.json navigation to include new page

* Add images to JetBrains installation documentation

- Add demo GIF showing Cline in action in JetBrains IDE
- Add screenshot of JetBrains marketplace download page
- Add screenshot of Install Plugin from Disk dialog
- Add screenshot of file selection dialog with zip file
- Complete visual walkthrough of installation process

* Update JetBrains demo to high-quality GIF

- Replace jetbrains-demo.gif with jetbrains-demo-hifi.gif
- Improved visual quality for better user experience

* Add JetBrains settings dialog screenshot

- Add screenshot showing the main settings dialog
- Provides visual guidance for accessing IDE settings
- Complete visual walkthrough now includes 5 screenshots

* Add JetBrains logo and finalize documentation

- Add JetBrains logo at top of page with proper styling
- Update content with user revisions (BYOK note, streamlined structure)
- Complete visual installation walkthrough with 5 images
- Ready for PR review

* Update early access messaging

- Change from 'You're getting early access' to 'Cline is in early access'
- More professional and product-focused messaging
- Maintains excitement while being clearer about the product status

* Update installation link for Cline plugin

---------

Co-authored-by: pashpashpash <nik@cline.bot>
2025-09-02 17:13:38 -07:00
Ara e3bc5f0143 Setup Raw Structure for implementing multi-workspace support with WorkspaceRoot (#5849)
* Setup Raw Structure for implementing multi-workspace support with WorkspaceRoot

* Remove Shortcut of Cline from its title
2025-09-02 16:14:55 -07:00
Igor Tceglevskii f2dbab814e root folder path comparison on windows (#5938) 2025-09-02 14:26:05 -07:00
164 changed files with 5444 additions and 1835 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Reset account view on sign out
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Adding telemetry for terminal command issue
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Sync taskHistory across Cline instances so that it will update for all Cline instances when one instance creates, adds to, or deletes a task.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Adding a grpc recording class with its builder
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fixed minor bug where entering slash commands would result in the following word being removed from the chat text area
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix issue where editor panel gets reset to chat view on each chat input focus event
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Exclude dependency folders for deep-planning prompt
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Sambanova provider: add DeepSeek-V3.1
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
fetch deployment id in design-time instead of runtime
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
add free grok-coder-free model to cline provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Adding a new standalone core api server script for local and integration testing purposes
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Setup Raw Structure for implementing multi-workspace support with WorkspaceRoot
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Reasoning Effort Minimal on OpenAI
+66 -61
View File
@@ -1,64 +1,69 @@
name: 🐛 Bug Report
description: File a bug report
labels: ["bug"]
labels: ['bug']
body:
- type: markdown
attributes:
value: |
**Important:** All bug reports must be reproducible using Claude 4 Sonnet. Cline uses complex prompts so less capable models may not work as expected.
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: How do you trigger this bug? Please walk us through it step by step.
value: |
1.
2.
3.
validations:
required: false
- type: textarea
id: logs
attributes:
label: Relevant API REQUEST output
description: Please copy and paste any relevant output. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: input
id: provider-model
attributes:
label: Provider/Model
description: What provider and model were you using when the issue occurred?
placeholder: "e.g., cline:anthropic/claude-3.7-sonnet, gemini:gemini-2.5-pro-exp-03-25"
validations:
required: true
- type: textarea
id: system-info
attributes:
label: System Information
description: What operating system and hardware are you using?
placeholder: |
Operating System: Windows 11, macOS Sonoma, Ubuntu 22.04, etc.
Hardware: CPU, GPU, RAM specifications if relevant
e.g.,
OS: Windows 11
CPU: Intel Core i7-11700K
GPU: NVIDIA GeForce RTX 3070
RAM: 32GB DDR4
validations:
required: true
- type: input
id: cline-version
attributes:
label: Cline Version
description: What version of Cline are you using? (You can find this at the bottom of the Settings view)
placeholder: "e.g., 1.2.3"
validations:
required: true
- type: markdown
attributes:
value: |
**Important:** All bug reports must be reproducible using Claude Sonnet 4. Cline uses complex prompts so less capable models may not work as expected.
- type: dropdown
id: plugin-type
attributes:
label: Plugin Type
description: Which plugin are you reporting a bug for?
options:
- VSCode Extension
- JetBrains Plugin
default: 0
validations:
required: true
- type: input
id: cline-version
attributes:
label: Cline Version
description: What version of Cline are you using? (You can find this at the bottom of the Settings view)
placeholder: 'e.g., 1.2.3'
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: How do you trigger this bug? Please walk us through it step by step.
value: |
1.
2.
3.
validations:
required: false
- type: input
id: provider-model
attributes:
label: Provider/Model
description: What provider and model were you using when the issue occurred?
placeholder: 'e.g., cline:anthropic/claude-sonnet-4, gemini:gemini-2.5-pro-exp-03-25'
validations:
required: false
- type: textarea
id: system-info
attributes:
label: System Information
description: What operating system and hardware are you using?
placeholder: |
Operating System: Windows 11, macOS Sonoma, Ubuntu 22.04, etc.
Hardware: CPU, GPU, RAM specifications if relevant
e.g.,
OS: Windows 11
CPU: Intel Core i7-11700K
GPU: NVIDIA GeForce RTX 3070
RAM: 32GB DDR4
validations:
required: false
+51 -7
View File
@@ -91,7 +91,7 @@
{
"type": "node",
"request": "launch",
"name": "Run cline-core service",
"name": "Debug Test Standalone Core Api Server (test:sca-server)",
"skipFiles": [
"<node_internals>/**"
],
@@ -100,18 +100,62 @@
"${workspaceFolder}/**",
"!**/node_modules/**"
],
"cwd": "${workspaceFolder}/dist-standalone",
"cwd": "${workspaceFolder}",
"outFiles": [
"${workspaceFolder}/dist/**/*.js",
"${workspaceFolder}/dist-standalone/**/*.js"
],
"preLaunchTask": "compile-standalone",
"runtimeExecutable": "npx",
"runtimeArgs": [
"tsx"
],
"program": "scripts/test-standalone-core-api-server.ts",
"env": {
// Turns on grpc debug log.
//"GRPC_TRACE": "all",
//"GRPC_VERBOSITY": "DEBUG",
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
"PROTOBUS_PORT": "26040",
"HOSTBRIDGE_PORT": "26041",
"WORKSPACE_DIR": "${workspaceFolder}",
"E2E_TEST": "true",
"CLINE_ENVIRONMENT": "local"
},
"program": "cline-core.js"
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "launch",
"name": "Debug Current Test File",
"skipFiles": [
"<node_internals>/**"
],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${workspaceFolder}/**",
"!**/node_modules/**"
],
"cwd": "${workspaceFolder}",
"runtimeExecutable": "npx",
"runtimeArgs": [
"mocha"
],
"args": [
"--require",
"ts-node/register",
"--require",
"source-map-support/register",
"--require",
"./src/test/requires.ts",
"--exit",
"${file}"
],
"env": {
"TS_NODE_PROJECT": "./tsconfig.unit-test.json",
"NODE_ENV": "test",
"IS_DEV": "true",
"CLINE_ENVIRONMENT": "local"
},
"console": "integratedTerminal",
"internalConsoleOptions": "openOnSessionStart"
}
]
}
+1 -12
View File
@@ -21,16 +21,5 @@
},
// Enable Lint and format using Biome
"biome.enabled": true,
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
}
"editor.defaultFormatter": "biomejs.biome"
}
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## [3.27.1]
- Add new Kimi K2 model to groq and moonshot providers
## [3.27.0]
- Fix `grok-code-fast-1` model information
+7 -1
View File
@@ -135,7 +135,12 @@
"**",
"!**/hosts/vscode/**",
"!**/test/**",
"!src/extension.ts"
"!**/*.test.ts",
"!src/dev/**",
"!src/extension.ts",
"!src/integrations/git/commit-message-generator.ts",
"!src/integrations/terminal/**",
"!src/core/controller/ui/openWalkthrough.ts"
],
"plugins": [
"src/dev/grit/vscode-api.grit"
@@ -148,6 +153,7 @@
"!src/core/storage/FileContextTracker.ts",
"!src/core/context/context-tracking/FileContextTracker.ts",
"!src/common.ts",
"!src/services/logging/distinctId.ts",
"!src/core/storage/utils/state-helpers.ts",
"!src/extension.ts"
],
@@ -1,6 +1,6 @@
---
title: "Installing Cline for JetBrains"
description: "Get Cline running in your favorite JetBrains IDE with the same powerful AI assistance you know from VSCode."
title: "Installing Cline for JetBrains (Early Access)"
description: "Get early access to Cline in your favorite JetBrains IDE with the same powerful AI assistance you know from VSCode."
---
<Frame>
@@ -20,16 +20,16 @@ Cline for JetBrains works almost identically to Cline in VSCode. All the core fe
/>
</Frame>
<Note>Cline for JetBrains is currently in alpha. While all core features are functional, you may encounter occasional issues.</Note>
<Note>Cline for JetBrains is in early access. All core features are functional, with ongoing improvements based on user feedback.</Note>
## Installation
Since Cline for JetBrains is currently in alpha, it's not yet available on the JetBrains Marketplace. You'll need to install it manually from a downloaded file:
As part of our early access program, Cline for JetBrains is available through direct download before its official marketplace release. You'll need to install it manually from a downloaded file:
### Manual Installation from Disk
1. **Download the Plugin:**
- Go to [https://plugins.jetbrains.com/plugin/28247-cline/versions/stable](https://plugins.jetbrains.com/plugin/28247-cline/versions/stable)
- Go to [https://plugins.jetbrains.com/plugin/28247-cline/versions/eap](https://plugins.jetbrains.com/plugin/28247-cline/versions/eap)
- Click **Download** to get the `.zip` file
<Frame>
+14
View File
@@ -0,0 +1,14 @@
// HubSpot Tracking Code for Cline Documentation
(function() {
// Check if HubSpot script is already loaded to prevent duplicates
if (!document.getElementById('hs-script-loader')) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.id = 'hs-script-loader';
script.async = true;
script.src = 'https://js-na2.hs-scripts.com/243656267.js';
// Append the script to the document head
document.head.appendChild(script);
}
})();
+2 -2
View File
@@ -160,9 +160,9 @@ const standaloneConfig = {
...baseConfig,
entryPoints: ["src/standalone/cline-core.ts"],
outfile: `${destDir}/cline-core.js`,
// These gRPC protos need to load files from the module directory at runtime,
// These modules need to load files from the module directory at runtime,
// so they cannot be bundled.
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
external: ["vscode", "@grpc/reflection", "grpc-health-check", "better-sqlite3"],
}
// E2E build script configuration
+3 -2
View File
@@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ToolUseName, ToolParamName } from "../../src/core/assistant-message"
import { ToolParamName } from "../../src/core/assistant-message"
import { ClineDefaultTool } from "../../src/shared/tools"
export interface InputMessage {
role: "user" | "assistant"
@@ -88,7 +89,7 @@ export interface TestResult {
}
export interface ExtractedToolCall {
name: ToolUseName
name: ClineDefaultTool
input: Partial<Record<ToolParamName, string>>
}
+22 -30
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.27.0",
"version": "3.27.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.27.0",
"version": "3.27.1",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -86,6 +86,7 @@
"@biomejs/biome": "^2.1.4",
"@bufbuild/buf": "^1.54.0",
"@changesets/cli": "^2.27.12",
"@types/better-sqlite3": "^7.6.13",
"@types/chai": "^5.0.1",
"@types/clone-deep": "^4.0.4",
"@types/diff": "^5.2.1",
@@ -109,6 +110,7 @@
"lint-staged": "^16.1.0",
"minimatch": "^3.0.3",
"npm-run-all": "^4.1.5",
"prebuild-install": "^7.1.3",
"protoc-gen-ts": "^0.8.7",
"proxyquire": "^2.1.3",
"rimraf": "^6.0.1",
@@ -5090,6 +5092,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/better-sqlite3": {
"version": "7.6.13",
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/chai": {
"version": "5.0.1",
"dev": true,
@@ -7113,7 +7125,6 @@
"version": "6.0.0",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"mimic-response": "^3.1.0"
},
@@ -7128,7 +7139,6 @@
"version": "3.1.0",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10"
},
@@ -7151,7 +7161,6 @@
"version": "0.6.0",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=4.0.0"
}
@@ -8105,7 +8114,6 @@
"version": "2.0.3",
"dev": true,
"license": "(MIT OR WTFPL)",
"optional": true,
"engines": {
"node": ">=6"
}
@@ -8963,8 +8971,7 @@
"node_modules/github-from-package": {
"version": "0.0.0",
"dev": true,
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/glob": {
"version": "10.4.3",
@@ -9394,8 +9401,7 @@
"node_modules/ini": {
"version": "1.3.8",
"dev": true,
"license": "ISC",
"optional": true
"license": "ISC"
},
"node_modules/internal-slot": {
"version": "1.1.0",
@@ -10975,8 +10981,7 @@
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"dev": true,
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/mocha": {
"version": "10.6.0",
@@ -11204,8 +11209,7 @@
"node_modules/napi-build-utils": {
"version": "2.0.0",
"dev": true,
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/negotiator": {
"version": "1.0.0",
@@ -11269,7 +11273,6 @@
"version": "3.75.0",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"semver": "^7.3.5"
},
@@ -12411,9 +12414,10 @@
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
@@ -12439,7 +12443,6 @@
"version": "4.1.0",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
@@ -12464,7 +12467,6 @@
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
@@ -12473,14 +12475,12 @@
"node_modules/prebuild-install/node_modules/chownr": {
"version": "1.1.4",
"dev": true,
"license": "ISC",
"optional": true
"license": "ISC"
},
"node_modules/prebuild-install/node_modules/detect-libc": {
"version": "2.0.4",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
@@ -12489,7 +12489,6 @@
"version": "3.6.2",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -12503,7 +12502,6 @@
"version": "2.1.3",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
@@ -12515,7 +12513,6 @@
"version": "2.2.0",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
@@ -12745,7 +12742,6 @@
"version": "1.2.8",
"dev": true,
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"optional": true,
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
@@ -12771,7 +12767,6 @@
"version": "2.0.1",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
@@ -13652,8 +13647,7 @@
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
@@ -13673,7 +13667,6 @@
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
@@ -14567,7 +14560,6 @@
"version": "0.6.0",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"safe-buffer": "^5.0.1"
},
+6 -3
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.27.0",
"version": "3.27.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -355,6 +355,7 @@
"test:integration": "vscode-test",
"test:unit": "TS_NODE_PROJECT='./tsconfig.unit-test.json' mocha",
"test:coverage": "vscode-test --coverage",
"test:sca-server": "npx tsx watch scripts/test-standalone-core-api-server.ts",
"e2e": "playwright test -c playwright.config.ts",
"test:e2e": "playwright install && vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test",
"test:e2e:optimal": "vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test",
@@ -381,6 +382,7 @@
"@biomejs/biome": "^2.1.4",
"@bufbuild/buf": "^1.54.0",
"@changesets/cli": "^2.27.12",
"@types/better-sqlite3": "^7.6.13",
"@types/chai": "^5.0.1",
"@types/clone-deep": "^4.0.4",
"@types/diff": "^5.2.1",
@@ -404,6 +406,7 @@
"lint-staged": "^16.1.0",
"minimatch": "^3.0.3",
"npm-run-all": "^4.1.5",
"prebuild-install": "^7.1.3",
"protoc-gen-ts": "^0.8.7",
"proxyquire": "^2.1.3",
"rimraf": "^6.0.1",
@@ -419,8 +422,6 @@
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
"@aws-sdk/credential-providers": "^3.840.0",
"@sap-ai-sdk/ai-api": "^1.17.0",
"@sap-ai-sdk/orchestration": "^1.17.0",
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@google-cloud/vertexai": "^1.9.3",
@@ -436,6 +437,8 @@
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
"@playwright/test": "^1.53.2",
"@sap-ai-sdk/ai-api": "^1.17.0",
"@sap-ai-sdk/orchestration": "^1.17.0",
"@sentry/browser": "^9.12.0",
"@streamparser/json": "^0.0.22",
"@types/uuid": "^10.0.0",
+1 -6
View File
@@ -2,6 +2,7 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
import "cline/state.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
@@ -10,7 +11,6 @@ service BrowserService {
rpc testBrowserConnection(StringRequest) returns (BrowserConnection);
rpc discoverBrowser(EmptyRequest) returns (BrowserConnection);
rpc getDetectedChromePath(EmptyRequest) returns (ChromePath);
rpc updateBrowserSettings(UpdateBrowserSettingsRequest) returns (Boolean);
rpc relaunchChromeDebugMode(EmptyRequest) returns (String);
}
@@ -31,11 +31,6 @@ message ChromePath {
bool is_bundled = 2;
}
message Viewport {
int32 width = 1;
int32 height = 2;
}
message BrowserSettings {
Viewport viewport = 1;
optional string remote_browser_host = 2;
-6
View File
@@ -8,19 +8,16 @@ message Metadata {
}
message EmptyRequest {
Metadata metadata = 1;
}
message Empty {
}
message StringRequest {
Metadata metadata = 1;
string value = 2;
}
message StringArrayRequest {
Metadata metadata = 1;
repeated string value = 2;
}
@@ -29,7 +26,6 @@ message String {
}
message Int64Request {
Metadata metadata = 1;
int64 value = 2;
}
@@ -38,7 +34,6 @@ message Int64 {
}
message BytesRequest {
Metadata metadata = 1;
bytes value = 2;
}
@@ -47,7 +42,6 @@ message Bytes {
}
message BooleanRequest {
Metadata metadata = 1;
bool value = 2;
}
+32 -21
View File
@@ -108,9 +108,16 @@ message SapAiCoreModelsRequest {
string resource_group = 6;
}
// SAP AI Core model with deployment information
message SapAiCoreModelDeployment {
string model_name = 1;
string deployment_id = 2;
}
// Response for SAP AI Core models with orchestration availability
message SapAiCoreModelsResponse {
repeated string model_names = 1;
repeated SapAiCoreModelDeployment deployments = 1;
bool orchestration_available = 2;
}
@@ -291,16 +298,18 @@ message ModelsApiConfiguration {
optional string plan_mode_together_model_id = 117;
optional string plan_mode_fireworks_model_id = 118;
optional string plan_mode_sap_ai_core_model_id = 119;
optional string plan_mode_groq_model_id = 120;
optional OpenRouterModelInfo plan_mode_groq_model_info = 121;
optional string plan_mode_hugging_face_model_id = 122;
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 123;
optional string plan_mode_huawei_cloud_maas_model_id = 124;
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 125;
optional string plan_mode_baseten_model_id = 126;
optional OpenRouterModelInfo plan_mode_baseten_model_info = 127;
optional string plan_mode_vercel_ai_gateway_model_id = 128;
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 129;
optional string plan_mode_sap_ai_core_deployment_id = 120;
optional string plan_mode_groq_model_id = 121;
optional OpenRouterModelInfo plan_mode_groq_model_info = 122;
optional string plan_mode_hugging_face_model_id = 123;
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 124;
optional string plan_mode_huawei_cloud_maas_model_id = 125;
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 126;
optional string plan_mode_baseten_model_id = 127;
optional OpenRouterModelInfo plan_mode_baseten_model_info = 128;
optional string plan_mode_vercel_ai_gateway_model_id = 129;
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
@@ -323,16 +332,18 @@ message ModelsApiConfiguration {
optional string act_mode_together_model_id = 217;
optional string act_mode_fireworks_model_id = 218;
optional string act_mode_sap_ai_core_model_id = 219;
optional string act_mode_groq_model_id = 220;
optional OpenRouterModelInfo act_mode_groq_model_info = 221;
optional string act_mode_hugging_face_model_id = 222;
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 223;
optional string act_mode_huawei_cloud_maas_model_id = 224;
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 225;
optional string act_mode_baseten_model_id = 226;
optional OpenRouterModelInfo act_mode_baseten_model_info = 227;
optional string act_mode_vercel_ai_gateway_model_id = 228;
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 229;
optional string act_mode_sap_ai_core_deployment_id = 220;
optional string act_mode_groq_model_id = 221;
optional OpenRouterModelInfo act_mode_groq_model_info = 222;
optional string act_mode_hugging_face_model_id = 223;
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 224;
optional string act_mode_huawei_cloud_maas_model_id = 225;
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 226;
optional string act_mode_baseten_model_id = 227;
optional OpenRouterModelInfo act_mode_baseten_model_info = 228;
optional string act_mode_vercel_ai_gateway_model_id = 229;
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230;
repeated string favorited_model_ids = 300;
}
+18 -1
View File
@@ -8,7 +8,6 @@ service StateService {
rpc getLatestState(EmptyRequest) returns (State);
rpc updateTerminalConnectionTimeout(UpdateTerminalConnectionTimeoutRequest) returns (UpdateTerminalConnectionTimeoutResponse);
rpc updateTerminalReuseEnabled(BooleanRequest) returns (Empty);
rpc updateDefaultTerminalProfile(StringRequest) returns (TerminalProfileUpdateResponse);
rpc getAvailableTerminalProfiles(EmptyRequest) returns (TerminalProfiles);
rpc subscribeToState(EmptyRequest) returns (stream State);
rpc toggleFavoriteModel(StringRequest) returns (Empty);
@@ -56,6 +55,7 @@ enum OpenaiReasoningEffort {
LOW = 0;
MEDIUM = 1;
HIGH = 2;
MINIMAL = 3;
}
enum McpDisplayMode {
@@ -106,6 +106,16 @@ message TelemetrySettingRequest {
TelemetrySettingEnum setting = 2;
}
// Browser settings for UpdateSettingsRequest
message BrowserSettingsUpdate {
optional Viewport viewport = 1;
optional string remote_browser_host = 2;
optional bool remote_browser_enabled = 3;
optional string chrome_executable_path = 4;
optional bool disable_tool_use = 5;
optional string custom_args = 6;
}
// Message for updating settings
message UpdateSettingsRequest {
Metadata metadata = 1;
@@ -126,6 +136,8 @@ message UpdateSettingsRequest {
optional FocusChainSettings focus_chain_settings = 17;
optional bool use_auto_condense = 18;
optional string custom_prompt = 19;
optional BrowserSettingsUpdate browser_settings = 20;
optional string default_terminal_profile = 21;
}
// Complete API Configuration message
@@ -268,6 +280,11 @@ message FocusChainSettings {
int32 remind_cline_interval = 2;
}
message Viewport {
int32 width = 1;
int32 height = 2;
}
message UpdateTerminalConnectionTimeoutResponse {
optional int32 timeout_ms = 1;
}
-4
View File
@@ -4,8 +4,6 @@ package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "cline/common.proto";
// Provides methods for working with IDE windows and editors.
service WindowService {
// Opens a text document in the IDE editor and returns editor information.
@@ -40,7 +38,6 @@ service WindowService {
}
message ShowTextDocumentRequest {
cline.Metadata metadata = 1;
string path = 2;
optional ShowTextDocumentOptions options = 3;
}
@@ -59,7 +56,6 @@ message TextEditorInfo {
}
message ShowOpenDialogueRequest {
cline.Metadata metadata = 1;
optional bool can_select_many = 2;
optional string open_label = 3;
optional ShowOpenDialogueFilterOption filters = 4;
+1 -48
View File
@@ -10,7 +10,6 @@ import * as path from "path"
import { rmrf } from "./file-utils.mjs"
import { main as generateHostBridgeClient } from "./generate-host-bridge-client.mjs"
import { main as generateProtoBusSetup } from "./generate-protobus-setup.mjs"
import { loadProtoDescriptorSet } from "./proto-utils.mjs"
const require = createRequire(import.meta.url)
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
@@ -31,14 +30,13 @@ const TS_PROTO_OPTIONS = [
"esModuleInterop=true",
"outputServices=generic-definitions", // output generic ServiceDefinitions
"outputIndex=true", // output an index file for each package which exports all protos in the package.
"useOptionals=messages", // Message fields are optional, scalars are not.
"useOptionals=none", // scalar and message fields are required unless they are marked as optional.
"useDate=false", // Timestamp fields will not be automatically converted to Date.
]
async function main() {
await cleanup()
await compileProtos()
await checkProtos()
await generateProtoBusSetup()
await generateHostBridgeClient()
}
@@ -187,51 +185,6 @@ function checkAppleSiliconCompatibility() {
}
}
const int64TypeNames = ["TYPE_INT64", "TYPE_UINT64", "TYPE_SINT64", "TYPE_FIXED64", "TYPE_SFIXED64"]
async function checkProtos() {
const proto = await loadProtoDescriptorSet()
const int64Fields = []
for (const [packageName, packageDef] of Object.entries(proto)) {
for (const [messageName, def] of Object.entries(packageDef)) {
// Skip service definitions
if (def && typeof def === "object" && "service" in def) {
continue
}
// Check message fields
if (def && def.type && def.type.field) {
for (const field of def.type.field) {
if (int64TypeNames.includes(field.type)) {
const name = `${packageName}.${messageName}.${field.name}`
int64Fields.push({
name: name,
type: field.type,
})
}
}
}
}
}
if (int64Fields.length > 0) {
console.log(chalk.yellow(`\nWarning: Found ${int64Fields.length} fields using 64-bit integer types`))
for (const field of int64Fields) {
const typeNames = {
TYPE_INT64: "int64",
TYPE_UINT64: "uint64",
TYPE_SINT64: "sint64",
TYPE_FIXED64: "fixed64",
TYPE_SFIXED64: "sfixed64",
}
log_verbose(chalk.yellow(` - ${field.name} (${typeNames[field.type]})`))
}
log_verbose(chalk.yellow("\nWARNING: 64-bit integer fields detected in proto definitions"))
log_verbose(chalk.yellow("JavaScript cannot safely represent integers larger than 2^53-1 (Number.MAX_SAFE_INTEGER)."))
log_verbose(chalk.yellow("Consider using string representation for large numbers or implementing BigInt support.\n"))
}
}
function log_verbose(s) {
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
console.log(s)
+87 -21
View File
@@ -6,40 +6,102 @@ import fs from "fs"
import { cp } from "fs/promises"
import { glob } from "glob"
import minimatch from "minimatch"
import os from "os"
import path from "path"
import { rmrf } from "./file-utils.mjs"
const BUILD_DIR = "dist-standalone"
const BINARIES_DIR = `${BUILD_DIR}/binaries`
const RUNTIME_DEPS_DIR = "standalone/runtime-files"
const IS_DEBUG_BUILD = process.env.IS_DEBUG_BUILD === "true"
// This should match the node version packaged with the JetBrains plugin.
const TARGET_NODE_VERSION = "22.15.0"
const TARGET_PLATFORMS = [
{ platform: "win32", arch: "x64", targetDir: "win-x64" },
{ platform: "darwin", arch: "x64", targetDir: "darwin-x64" },
{ platform: "darwin", arch: "arm64", targetDir: "darwin-arm64" },
{ platform: "linux", arch: "x64", targetDir: "linux-x64" },
]
const SUPPORTED_BINARY_MODULES = ["better-sqlite3"]
const UNIVERSAL_BUILD = !process.argv.includes("-s")
const IS_VERBOSE = process.argv.includes("-v") || process.argv.includes("--verbose")
async function main() {
await installNodeDependencies()
if (UNIVERSAL_BUILD) {
console.log("Building universal package for all platforms...")
await packageAllBinaryDeps()
} else {
console.log(`Building package for ${os.platform()}-${os.arch()}...`)
}
await zipDistribution()
}
async function installNodeDependencies() {
// Clean modules from any previous builds
await rmrf(path.join(BUILD_DIR, "node_modules"))
await rmrf(path.join(BINARIES_DIR))
await cpr(RUNTIME_DEPS_DIR, BUILD_DIR)
console.log("Running npm install in distribution directory...")
const cwd = process.cwd()
process.chdir(BUILD_DIR)
execSync("npm install", { stdio: "inherit", cwd: BUILD_DIR })
try {
execSync("npm install", { stdio: "inherit" })
// Move the vscode directory into node_modules.
// It can't be installed using npm because it will create a symlink which cannot be unzipped correctly on windows.
fs.renameSync("vscode", path.join("node_modules", "vscode"))
} catch (error) {
console.error("Error during setup:", error)
// Move the vscode directory into node_modules.
// It can't be installed using npm because it will create a symlink which cannot be unzipped correctly on windows.
fs.renameSync(`${BUILD_DIR}/vscode`, `${BUILD_DIR}/node_modules/vscode`)
}
/**
* Downloads prebuilt binaries for each platform for the modules that include binaries. It uses `npx prebuild-install`
* to download the binary.
*
* The modules are downloaded to dist-standalone/binaries/{os}-{platform}/.
* When cline-core is installed, the installer should use the correct module for the current platform.
*/
async function packageAllBinaryDeps() {
// Check for native .node modules.
const allNativeModules = await glob("**/*.node", { cwd: path.join(BUILD_DIR, "node_modules"), nodir: true })
const isAllowed = (path) => SUPPORTED_BINARY_MODULES.some((allowed) => path.includes(allowed))
const blocked = allNativeModules.filter((x) => !isAllowed(x))
if (blocked.length > 0) {
console.error(`Error: Native node modules cannot be included in the standalone distribution:\n\n${blocked.join("\n")}`)
console.error(
"\nThese modules must support prebuilt-install and be added to the supported list in scripts/package-standalone.mjs",
)
process.exit(1)
} finally {
process.chdir(cwd)
}
// Check for native .node modules.
const nativeModules = await glob("**/*.node", { cwd: BUILD_DIR, nodir: true })
if (nativeModules.length > 0) {
console.error("Native node modules cannot be included in the standalone distribution:\n", nativeModules.join("\n"))
process.exit(1)
for (const module of SUPPORTED_BINARY_MODULES) {
console.log(`Installing binaries for ${module}...`)
const src = path.join(BUILD_DIR, "node_modules", module)
if (!fs.existsSync(src)) {
console.warn(`Warning: Trying to install binaries for the module '${module}', but it is not being used by cline.`)
continue
}
for (const { platform, arch, targetDir } of TARGET_PLATFORMS) {
const binaryDir = `${BINARIES_DIR}/${targetDir}/node_modules`
fs.mkdirSync(binaryDir, { recursive: true })
// Copy the module from the build dir
const dest = path.join(binaryDir, module)
await cpr(src, dest)
// Download the binary libs
const v = IS_VERBOSE ? "--verbose" : ""
const cmd = `npx prebuild-install --platform=${platform} --arch=${arch} --target=${TARGET_NODE_VERSION} ${v}`
log_verbose(`${module}: ${cmd}`)
execSync(cmd, { cwd: dest, stdio: "inherit" })
log_verbose("")
}
// Remove the original module with the host platform binaries installed directly into node_modules.
log_verbose(`Cleaning up host version of ${module}`)
await rmrf(src)
log_verbose("")
}
}
@@ -47,10 +109,13 @@ async function zipDistribution() {
// Zip the build directory (excluding any pre-existing output zip).
const zipPath = path.join(BUILD_DIR, "standalone.zip")
const output = fs.createWriteStream(zipPath)
const archive = archiver("zip", { zlib: { level: 3 } })
const startTime = Date.now()
const archive = archiver("zip", { zlib: { level: 6 } })
output.on("close", () => {
console.log(`Created ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(1)} MB)`)
const endTime = Date.now()
const duration = (endTime - startTime) / 1000
console.log(`Created ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(1)} MB) in ${duration.toFixed(2)} seconds`)
})
archive.on("warning", (err) => {
console.warn(`Warning: ${err}`)
@@ -73,7 +138,7 @@ async function zipDistribution() {
// Add the whole cline directory under "extension", except the for the ignored files.
archive.directory(process.cwd(), "extension", (entry) => {
if (isIgnored(entry.name)) {
log_verbose("Ignoring", entry.name)
//log_verbose("Ignoring", entry.name)
return false
}
return entry
@@ -145,7 +210,7 @@ function createIsIgnored(standaloneIgnores) {
let allIgnore = [...defaultIgnore, ...expandedIgnore, ...standaloneIgnores]
// Map files need to be included in the debug build. Remove .map ignores when IS_DEBUG_BUILD is set
if (process.env.IS_DEBUG_BUILD) {
if (IS_DEBUG_BUILD) {
allIgnore = allIgnore.filter((pattern) => !pattern.endsWith(".map"))
console.log("Debug build: Including .map files in package")
}
@@ -167,6 +232,7 @@ function createIsIgnored(standaloneIgnores) {
/* cp -r */
async function cpr(source, dest) {
log_verbose(`Copying ${source} -> ${dest}`)
await cp(source, dest, {
recursive: true,
preserveTimestamps: true,
@@ -175,7 +241,7 @@ async function cpr(source, dest) {
}
function log_verbose(...args) {
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
if (IS_VERBOSE) {
console.log(...args)
}
}
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env npx tsx
/**
* Simple Cline gRPC Server
*
* This script provides a minimal way to run the Cline core gRPC service
* without requiring the full installation, while automatically mocking all external services. Simply run:
*
* # One-time setup (generates protobuf files)
* npm run compile-standalone
* npm run test:sca-server
*
* The following components are started automatically:
* 1. HostBridge test server
* 2. ClineApiServerMock (mock implementation of the Cline API)
* 3. AuthServiceMock (activated if E2E_TEST="true")
*
* Environment Variables for Customization:
* PROJECT_ROOT - Override project root directory (default: parent of scripts dir)
* CLINE_DIST_DIR - Override distribution directory (default: PROJECT_ROOT/dist-standalone)
* CLINE_CORE_FILE - Override core file name (default: cline-core.js)
* PROTOBUS_PORT - gRPC server port (default: 26040)
* HOSTBRIDGE_PORT - HostBridge server port (default: 26041)
* WORKSPACE_DIR - Working directory (default: current directory)
* E2E_TEST - Enable E2E test mode (default: true)
* CLINE_ENVIRONMENT - Environment setting (default: local)
*
* Ideal for local development, testing, or lightweight E2E scenarios.
*/
import { ChildProcess, spawn } from "child_process"
import * as fs from "fs"
import * as path from "path"
import { ClineApiServerMock } from "../src/test/e2e/fixtures/server/index"
// Configuration
const PROTOBUS_PORT = process.env.PROTOBUS_PORT || "26040"
const HOSTBRIDGE_PORT = process.env.HOSTBRIDGE_PORT || "26041"
const WORKSPACE_DIR = process.env.WORKSPACE_DIR || process.cwd()
const E2E_TEST = process.env.E2E_TEST || "true"
const CLINE_ENVIRONMENT = process.env.CLINE_ENVIRONMENT || "local"
// Locate the standalone build directory and core file with flexible path resolution
const projectRoot = process.env.PROJECT_ROOT || path.resolve(__dirname, "..")
const distDir = process.env.CLINE_DIST_DIR || path.join(projectRoot, "dist-standalone")
const clineCoreFile = process.env.CLINE_CORE_FILE || "cline-core.js"
const coreFile = path.join(distDir, clineCoreFile)
async function main(): Promise<void> {
console.log("Starting Simple Cline gRPC Server...")
console.log(`Workspace: ${WORKSPACE_DIR}`)
console.log(`ProtoBus Port: ${PROTOBUS_PORT}`)
console.log(`HostBridge Port: ${HOSTBRIDGE_PORT}`)
console.log(`Looking for standalone build at: ${coreFile}`)
if (!fs.existsSync(coreFile)) {
console.error(`Standalone build not found at: ${coreFile}`)
console.error("Available environment variables for customization:")
console.error(" PROJECT_ROOT - Override project root directory")
console.error(" CLINE_DIST_DIR - Override distribution directory")
console.error(" CLINE_CORE_FILE - Override core file name")
console.error("")
console.error("To build the standalone version, run: npm run compile-standalone")
process.exit(1)
}
try {
const apiServer = await ClineApiServerMock.startGlobalServer()
console.log("Cline API Server started in-process")
} catch (error) {
console.error("Failed to start Cline API Server:", error)
process.exit(1)
}
// Start hostbridge test server in background.
// We run it as a child process to emulate how the extension currently operates
console.log("Starting HostBridge test server...")
const hostbridge: ChildProcess = spawn("npx", ["tsx", path.join(__dirname, "test-hostbridge-server.ts")], {
stdio: "pipe",
detached: false,
})
// Start the core service
// We run it as a child process to emulate how the extension currently operates
console.log("Starting Cline Core Service...")
const coreService: ChildProcess = spawn("node", [clineCoreFile], {
cwd: distDir,
env: {
...process.env,
NODE_PATH: "./node_modules",
DEV_WORKSPACE_FOLDER: WORKSPACE_DIR,
PROTOBUS_ADDRESS: `127.0.0.1:${PROTOBUS_PORT}`,
HOST_BRIDGE_ADDRESS: `localhost:${HOSTBRIDGE_PORT}`,
E2E_TEST: E2E_TEST,
CLINE_ENVIRONMENT: CLINE_ENVIRONMENT,
},
stdio: "inherit",
})
// Handle graceful shutdown
const shutdown = async (): Promise<void> => {
console.log("\n Shutting down services...")
hostbridge.kill()
coreService.kill()
await ClineApiServerMock.stopGlobalServer()
process.exit(0)
}
process.on("SIGINT", shutdown)
process.on("SIGTERM", shutdown)
coreService.on("exit", (code) => {
console.log(`Core service exited with code ${code}`)
hostbridge.kill()
process.exit(code || 0)
})
hostbridge.on("exit", (code) => {
console.log(`HostBridge exited with code ${code}`)
coreService.kill()
process.exit(code || 0)
})
console.log("Cline gRPC Server is running!")
console.log(`Connect to: 127.0.0.1:${PROTOBUS_PORT}`)
console.log("Press Ctrl+C to stop")
}
if (require.main === module) {
main().catch((error) => {
console.error("Failed to start simple Cline server:", error)
process.exit(1)
})
}
+1
View File
@@ -331,6 +331,7 @@ function createHandlerForProvider(
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
deploymentId: mode === "plan" ? options.planModeSapAiCoreDeploymentId : options.actModeSapAiCoreDeploymentId,
sapAiCoreUseOrchestrationMode: options.sapAiCoreUseOrchestrationMode,
})
case "claude-code":
+33
View File
@@ -32,6 +32,7 @@ export class ClineHandler implements ApiHandler {
private client: OpenAI | undefined
private readonly _baseUrl = clineEnvConfig.apiBaseUrl
lastGenerationId?: string
private lastRequestId?: string
constructor(options: ClineHandlerOptions) {
this.options = options
@@ -54,6 +55,31 @@ export class ClineHandler implements ApiHandler {
"X-Task-ID": this.options.ulid || "",
"X-Cline-Version": extensionVersion,
},
// Capture real HTTP request ID from initial streaming response headers
fetch: async (...args: Parameters<typeof fetch>): Promise<Awaited<ReturnType<typeof fetch>>> => {
const [input, init] = args
const resp = await fetch(input, init)
try {
let urlStr = ""
if (typeof input === "string") {
urlStr = input
} else if (input instanceof URL) {
urlStr = input.toString()
} else if (typeof (input as { url?: unknown }).url === "string") {
urlStr = (input as { url: string }).url
}
// Only record for chat completions (the primary streaming request)
if (urlStr.includes("/chat/completions")) {
const rid = resp.headers.get("x-request-id") || resp.headers.get("request-id")
if (rid) {
this.lastRequestId = rid
}
}
} catch {
// ignore header capture errors
}
return resp
},
})
} catch (error: any) {
throw new Error(`Error creating Cline client: ${error.message}`)
@@ -70,6 +96,7 @@ export class ClineHandler implements ApiHandler {
const client = await this.ensureClient()
this.lastGenerationId = undefined
this.lastRequestId = undefined
let didOutputUsage: boolean = false
@@ -92,6 +119,7 @@ export class ClineHandler implements ApiHandler {
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
}
if (!this.lastGenerationId && chunk.id) {
this.lastGenerationId = chunk.id
}
@@ -203,6 +231,11 @@ export class ClineHandler implements ApiHandler {
return undefined
}
// Expose the last HTTP request ID captured from response headers (X-Request-ID)
getLastRequestId(): string | undefined {
return this.lastRequestId
}
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.openRouterModelId
const modelInfo = this.options.openRouterModelInfo
+10 -5
View File
@@ -21,6 +21,7 @@ interface SapAiCoreHandlerOptions extends CommonApiHandlerOptions {
apiModelId?: string
sapAiCoreUseOrchestrationMode?: boolean
thinkingBudgetTokens?: number
deploymentId?: string
reasoningEffort?: string
}
@@ -28,6 +29,7 @@ interface Deployment {
id: string
name: string
}
interface Token {
access_token: string
expires_in: number
@@ -394,11 +396,8 @@ export class SapAiCoreHandler implements ApiHandler {
return this.token.access_token
}
// TODO: these fallback fetching deployment id methods can be removed in future version if decided that users migration to fetching deployment id in design-time (open SAP AI Core provider UI) considered as completed.
private async getAiCoreDeployments(): Promise<Deployment[]> {
if (this.options.sapAiCoreClientSecret === "") {
return [{ id: "notconfigured", name: "ai-core-not-configured" }]
}
const token = await this.getToken()
const headers = {
Authorization: `Bearer ${token}`,
@@ -546,7 +545,13 @@ export class SapAiCoreHandler implements ApiHandler {
}
const model = this.getModel()
const deploymentId = await this.getDeploymentForModel(model.id)
let deploymentId = this.options.deploymentId
if (!deploymentId) {
// Fallback to runtime deployment id fetching for users who haven't opened the SAP provider UI
console.log(`No pre-configured deployment ID found for model ${model.id}, falling back to runtime fetching`)
deploymentId = await this.getDeploymentForModel(model.id)
}
const anthropicModels = [
"anthropic--claude-4-sonnet",
+2 -27
View File
@@ -1,3 +1,4 @@
import { ClineDefaultTool } from "@shared/tools"
export type AssistantMessageContent = TextContent | ToolUse
export { parseAssistantMessageV2 } from "./parse-assistant-message"
@@ -8,32 +9,6 @@ export interface TextContent {
partial: boolean
}
export const toolUseNames = [
"execute_command",
"read_file",
"write_to_file",
"replace_in_file",
"search_files",
"list_files",
"list_code_definition_names",
"browser_action",
"use_mcp_tool",
"access_mcp_resource",
"ask_followup_question",
"plan_mode_respond",
"load_mcp_documentation",
"attempt_completion",
"new_task",
"condense",
"summarize_task",
"report_bug",
"new_rule",
"web_fetch",
] as const
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)
export type ToolUseName = (typeof toolUseNames)[number]
export const toolParamNames = [
"command",
"requires_approval",
@@ -69,7 +44,7 @@ export type ToolParamName = (typeof toolParamNames)[number]
export interface ToolUse {
type: "tool_use"
name: ToolUseName
name: ClineDefaultTool // id of the tool being used
// params is a partial record, allowing only some or none of the possible parameters to be used
params: Partial<Record<ToolParamName, string>>
partial: boolean
@@ -1,4 +1,5 @@
import { AssistantMessageContent, TextContent, ToolParamName, ToolUse, ToolUseName, toolParamNames, toolUseNames } from "." // Assuming types are defined in index.ts or a similar file
import { ClineDefaultTool, toolUseNames } from "@shared/tools"
import { AssistantMessageContent, TextContent, ToolParamName, ToolUse, toolParamNames } from "." // Assuming types are defined in index.ts or a similar file
// parseAssistantmessageV1 removed in https://github.com/cline/cline/pull/5425
@@ -33,7 +34,7 @@ export function parseAssistantMessageV2(assistantMessage: string): AssistantMess
let currentParamName: ToolParamName | undefined
// Precompute tags for faster lookups
const toolUseOpenTags = new Map<string, ToolUseName>()
const toolUseOpenTags = new Map<string, ClineDefaultTool>()
const toolParamOpenTags = new Map<string, ToolParamName>()
for (const name of toolUseNames) {
toolUseOpenTags.set(`<${name}>`, name)
@@ -1,62 +0,0 @@
import { UpdateBrowserSettingsRequest } from "@shared/proto/cline/browser"
import { Boolean } from "@shared/proto/cline/common"
import { DEFAULT_BROWSER_SETTINGS, BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings"
import { Controller } from "../index"
/**
* Update browser settings
* @param controller The controller instance
* @param request The browser settings request message
* @returns Success response
*/
export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise<Boolean> {
try {
// Get current browser settings to preserve fields not in the request
const currentSettings = controller.stateManager.getGlobalStateKey("browserSettings")
const mergedWithDefaults = { ...DEFAULT_BROWSER_SETTINGS, ...currentSettings }
// Convert from protobuf format to shared format, merging with existing settings
const newBrowserSettings: SharedBrowserSettings = {
...mergedWithDefaults, // Start with existing settings (and defaults)
viewport: {
// Apply updates from request
width: request.viewport?.width || mergedWithDefaults.viewport.width,
height: request.viewport?.height || mergedWithDefaults.viewport.height,
},
// Explicitly handle optional boolean and string fields from the request
remoteBrowserEnabled:
request.remoteBrowserEnabled === undefined
? mergedWithDefaults.remoteBrowserEnabled
: request.remoteBrowserEnabled,
remoteBrowserHost:
request.remoteBrowserHost === undefined ? mergedWithDefaults.remoteBrowserHost : request.remoteBrowserHost,
chromeExecutablePath:
// If chromeExecutablePath is explicitly in the request (even as ""), use it.
// Otherwise, fall back to mergedWithDefaults.
"chromeExecutablePath" in request ? request.chromeExecutablePath : mergedWithDefaults.chromeExecutablePath,
disableToolUse: request.disableToolUse === undefined ? mergedWithDefaults.disableToolUse : request.disableToolUse,
customArgs: "customArgs" in request ? request.customArgs : mergedWithDefaults.customArgs,
}
// Update global state with new settings
controller.stateManager.setGlobalState("browserSettings", newBrowserSettings)
// Update task browser settings if task exists
if (controller.task) {
controller.task.browserSettings = newBrowserSettings
controller.task.browserSession.browserSettings = newBrowserSettings
}
// Post updated state to webview
await controller.postStateToWebview()
return Boolean.create({
value: true,
})
} catch (error) {
console.error("Error updating browser settings:", error)
return Boolean.create({
value: false,
})
}
}
@@ -3,7 +3,7 @@ import { Controller } from ".."
export async function checkpointDiff(controller: Controller, request: Int64Request): Promise<Empty> {
if (request.value) {
await controller.task?.presentMultifileDiff(request.value, false)
await controller.task?.checkpointManager?.presentMultifileDiff(request.value, false)
}
return Empty
return Empty.create()
}
@@ -23,7 +23,11 @@ export async function checkpointRestore(controller: Controller, request: Checkpo
})
// NOTE: cancelTask awaits abortTask, which awaits diffViewProvider.revertChanges, which reverts any edited files, allowing us to reset to a checkpoint rather than running into a state where the revertChanges function is called alongside or after the checkpoint reset
await controller.task?.restoreCheckpoint(request.number, request.restoreType as ClineCheckpointRestore, request.offset)
await controller.task?.checkpointManager?.restoreCheckpoint(
request.number,
request.restoreType as ClineCheckpointRestore,
request.offset,
)
}
return Empty.create({})
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import { getWorkspaceBasename } from "@core/workspace"
import { RuleFile, RuleFileRequest } from "@shared/proto/cline/file"
import * as path from "path"
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
import { HostProvider } from "@/hosts/host-provider"
import { ShowMessageType } from "@/shared/proto/host/window"
@@ -68,7 +68,7 @@ export async function createRuleFile(controller: Controller, request: RuleFileRe
return RuleFile.create({
filePath: filePath,
displayName: path.basename(filePath),
displayName: getWorkspaceBasename(filePath, "Controller.createRuleFile"),
alreadyExists: fileExists,
})
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import { getWorkspaceBasename } from "@core/workspace"
import { RuleFile, RuleFileRequest } from "@shared/proto/cline/file"
import * as path from "path"
import { HostProvider } from "@/hosts/host-provider"
import { ShowMessageType } from "@/shared/proto/host/window"
import { Controller } from ".."
@@ -40,7 +40,7 @@ export async function deleteRuleFile(controller: Controller, request: RuleFileRe
//await refreshWorkflowToggles(controller.context, cwd)
await controller.postStateToWebview()
const fileName = path.basename(request.rulePath)
const fileName = getWorkspaceBasename(request.rulePath, "Controller.deleteRuleFile")
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
@@ -1,7 +1,7 @@
import { workspaceResolver } from "@core/workspace"
import { BooleanResponse, StringRequest } from "@shared/proto/cline/common"
import { getWorkspacePath } from "@utils/path"
import * as fs from "fs"
import * as path from "path"
import { Controller } from ".."
/**
@@ -25,7 +25,12 @@ export async function ifFileExistsRelativePath(_controller: Controller, request:
}
// Resolve the relative path to absolute path
const absolutePath = path.resolve(workspacePath, request.value)
const resolvedPath = workspaceResolver.resolveWorkspacePath(
workspacePath,
request.value,
"Controller.ifFileExistsRelativePath",
)
const absolutePath = typeof resolvedPath === "string" ? resolvedPath : resolvedPath.absolutePath
// Check if the file exists
try {
return BooleanResponse.create({ value: fs.statSync(absolutePath).isFile() })
@@ -1,7 +1,7 @@
import { workspaceResolver } from "@core/workspace"
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
import { Empty, StringRequest } from "@shared/proto/cline/common"
import { getWorkspacePath } from "@utils/path"
import * as path from "path"
import { Controller } from ".."
/**
@@ -20,7 +20,12 @@ export async function openFileRelativePath(_controller: Controller, request: Str
if (request.value) {
// Resolve the relative path to absolute path
const absolutePath = path.resolve(workspacePath, request.value)
const resolvedPath = workspaceResolver.resolveWorkspacePath(
workspacePath,
request.value,
"Controller.openFileRelativePath",
)
const absolutePath = typeof resolvedPath === "string" ? resolvedPath : resolvedPath.absolutePath
// Open the file using the existing integration
openFileIntegration(absolutePath)
+2 -9
View File
@@ -1,4 +1,4 @@
import path from "node:path"
import { getWorkspaceBasename } from "@core/workspace"
import type { ToggleClineRuleRequest } from "@shared/proto/cline/file"
import { ToggleClineRules } from "@shared/proto/cline/file"
import { telemetryService } from "@/services/telemetry"
@@ -36,14 +36,7 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
// Track rule toggle telemetry with current task context
if (controller.task?.ulid) {
// Extract just the filename for privacy (no full paths)
const ruleFileName = path.basename(rulePath)
telemetryService.captureClineRuleToggled(controller.task.ulid, ruleFileName, enabled, isGlobal)
}
// Track rule toggle telemetry with current task context
if (controller.task?.ulid) {
// Extract just the filename for privacy (no full paths)
const ruleFileName = path.basename(rulePath)
const ruleFileName = getWorkspaceBasename(rulePath, "Controller.toggleClineRule")
telemetryService.captureClineRuleToggled(controller.task.ulid, ruleFileName, enabled, isGlobal)
}
@@ -0,0 +1,33 @@
import { GrpcRecorder, GrpcRecorderNoops, IRecorder } from "@/core/controller/grpc-recorder/grpc-recorder"
import { LogFileHandler, LogFileHandlerNoops } from "@/core/controller/grpc-recorder/log-file-handler"
/**
* A builder class for constructing a gRPC recorder instance.
*
* This class follows the Builder pattern, allowing consumers
* to configure logging behavior and control whether recording
* is enabled or disabled before creating a final `IRecorder`.
*/
export class GrpcRecorderBuilder {
private fileHandler: LogFileHandler | null = null
private enabled: boolean = true
public withLogFileHandler(handler: LogFileHandler): this {
this.fileHandler = handler
return this
}
public enableIf(condition: boolean): this {
this.enabled = condition
return this
}
public build(): IRecorder {
if (!this.enabled) {
return new GrpcRecorderNoops()
}
const handler = this.fileHandler ?? new LogFileHandlerNoops()
return new GrpcRecorder(handler)
}
}
@@ -0,0 +1,106 @@
import { GrpcRecorder, IRecorder } from "@core/controller/grpc-recorder/grpc-recorder"
import { expect } from "chai"
import { ExtensionMessage } from "@/shared/ExtensionMessage"
import { GrpcRequest } from "@/shared/WebviewMessage"
describe("grpc-recorder", () => {
let recorder: IRecorder
before(async () => {
recorder = GrpcRecorder.builder().enableIf(true).build()
})
describe("GrpcRecorder", () => {
it("matches multiple request, response and stats", async () => {
interface UseCase {
request: GrpcRequest
response: ExtensionMessage["grpc_response"]
expectedStatus: string
}
const requestResponseUseCases: UseCase[] = [
{
request: {
service: "the-service",
method: "the-method",
message: "the-message",
request_id: "request-id-1",
is_streaming: false,
},
response: {
request_id: "request-id-1",
message: "the-message-response",
error: "",
},
expectedStatus: "completed",
},
{
request: {
service: "streaming-service",
method: "stream-method",
message: { data: "streaming-data", count: 42 },
request_id: "request-id-2",
is_streaming: true,
},
response: {
request_id: "request-id-2",
message: { streamData: "chunk-1" },
error: "",
is_streaming: true,
sequence_number: 1,
},
expectedStatus: "completed",
},
{
request: {
service: "another-service",
method: "another-method",
message: { complex: { nested: "object", array: [1, 2, 3] } },
request_id: "request-id-3",
is_streaming: false,
},
response: {
request_id: "request-id-3",
message: "",
error: "Something went wrong",
},
expectedStatus: "error",
},
]
const initialExpectedStatus = "pending"
requestResponseUseCases.forEach((us: UseCase, index: number) => {
recorder.recordRequest(us.request)
let sessionLog = recorder.getSessionLog()
expect(sessionLog.entries).length(index + 1)
expect(sessionLog.entries[index]).to.include({
service: us.request.service,
method: us.request.method,
isStreaming: us.request.is_streaming,
requestId: us.request.request_id,
status: initialExpectedStatus,
})
if (us.response) {
recorder.recordResponse(us.request.request_id, us.response)
}
sessionLog = recorder.getSessionLog()
expect(sessionLog.entries[index].status).equal(us.expectedStatus)
expect(sessionLog.entries[index].response).to.deep.include({
error: us.response?.error,
})
})
const sessionLog = recorder.getSessionLog()
expect(sessionLog.stats).to.include({
totalRequests: 3,
pendingRequests: 0,
completedRequests: 2,
errorRequests: 1,
})
})
})
})
@@ -0,0 +1,179 @@
import { GrpcResponse } from "@shared/ExtensionMessage"
import { GrpcRequest } from "@shared/WebviewMessage"
import { GrpcRecorderBuilder } from "@/core/controller/grpc-recorder/grpc-recorder.builder"
import { ILogFileHandler } from "@/core/controller/grpc-recorder/log-file-handler"
import { GrpcLogEntry, GrpcSessionLog, SessionStats } from "@/core/controller/grpc-recorder/types"
export class GrpcRecorderNoops implements IRecorder {
recordRequest(_request: GrpcRequest): void {}
recordResponse(_requestId: string, _response: GrpcResponse): void {}
recordError(_requestId: string, _error: string): void {}
getSessionLog(): GrpcSessionLog {
return {
startTime: "",
entries: [],
}
}
}
export interface IRecorder {
recordRequest(request: GrpcRequest): void
recordResponse(requestId: string, response: GrpcResponse): void
recordError(requestId: string, error: string): void
getSessionLog(): GrpcSessionLog
}
/**
* Default implementation of a gRPC recorder.
*
* Responsibilities:
* - Records requests, responses, and errors.
* - Tracks request/response lifecycle, including duration and status.
* - Maintains a session log of all recorded entries.
* - Persists logs asynchronously through a file handler.
*/
export class GrpcRecorder implements IRecorder {
private sessionLog: GrpcSessionLog
private pendingRequests: Map<string, { entry: GrpcLogEntry; startTime: number }> = new Map()
constructor(private fileHandler: ILogFileHandler) {
this.sessionLog = {
startTime: new Date().toISOString(),
entries: [],
}
this.fileHandler.initialize(this.sessionLog).catch((error) => {
console.error("Failed to initialize gRPC log file:", error)
})
}
public static builder(): GrpcRecorderBuilder {
return new GrpcRecorderBuilder()
}
/**
* Records a gRPC request.
*
* - Stores the request as a "pending" log entry.
* - Tracks the request start time for later duration calculation.
* - Persists the log asynchronously.
*
* @param request - The incoming gRPC request.
*/
public recordRequest(request: GrpcRequest): void {
const entry: GrpcLogEntry = {
requestId: request.request_id,
service: request.service,
method: request.method,
isStreaming: request.is_streaming || false,
request: {
message: request.message,
},
status: "pending",
}
this.pendingRequests.set(request.request_id, {
entry,
startTime: Date.now(),
})
this.sessionLog.entries.push(entry)
this.flushLogAsync()
}
public getSessionLog(): GrpcSessionLog {
return this.sessionLog
}
/**
* Records a gRPC response for a given request.
*
* - Looks up the pending request entry.
* - Updates the entry with response data, status, and duration.
* - Removes the request from pending if it's not streaming.
* - Recomputes session stats.
* - Persists the log asynchronously.
*
* @param requestId - The ID of the request being responded to.
* @param response - The corresponding gRPC response.
*/
public recordResponse(requestId: string, response: GrpcResponse): void {
const pendingRequest = this.pendingRequests.get(requestId)
if (!pendingRequest) {
console.warn(`No pending request found for response with ID: ${requestId}`)
return
}
const { entry, startTime } = pendingRequest
entry.response = {
message: response?.message ? response.message : undefined,
error: response?.error,
isStreaming: response?.is_streaming,
sequenceNumber: response?.sequence_number,
}
entry.duration = Date.now() - startTime
entry.status = response?.error ? "error" : "completed"
if (!response?.is_streaming) {
this.pendingRequests.delete(requestId)
}
this.sessionLog.stats = this.getStats()
this.flushLogAsync()
}
/**
* Records an error for a given request.
*
* - Marks the request as failed.
* - Records the error message and request duration.
* - Removes it from the pending requests.
* - Persists the log asynchronously.
*
* @param requestId - The ID of the request that errored.
* @param error - Error message.
*/
public recordError(requestId: string, error: string): void {
const pendingRequest = this.pendingRequests.get(requestId)
if (!pendingRequest) {
console.warn(`No pending request found for error with ID: ${requestId}`)
return
}
const { entry, startTime } = pendingRequest
entry.response = {
error: error,
}
entry.duration = Date.now() - startTime
entry.status = "error"
this.pendingRequests.delete(requestId)
this.flushLogAsync()
}
private flushLogAsync(): void {
setImmediate(() => {
this.fileHandler.write(this.sessionLog).catch((error) => {
console.error("Failed to flush gRPC log:", error)
})
})
}
public getStats(): SessionStats {
const totalRequests = this.sessionLog.entries.length
const pendingRequests = this.sessionLog.entries.filter((e) => e.status === "pending").length
const completedRequests = this.sessionLog.entries.filter((e) => e.status === "completed").length
const errorRequests = this.sessionLog.entries.filter((e) => e.status === "error").length
return {
totalRequests,
pendingRequests,
completedRequests,
errorRequests,
}
}
}
@@ -0,0 +1,19 @@
import { expect } from "chai"
import { before, describe, it } from "mocha"
import { LogFileHandler } from "@/core/controller/grpc-recorder/log-file-handler"
describe("log-file-handler", () => {
let logHandler: LogFileHandler
before(async () => {
logHandler = new LogFileHandler()
expect(logHandler.getFilePath()).not.empty
})
describe("LogFileHandler", () => {
it("returns file name with timestamp when env var not set", () => {
const result = logHandler.getFileName()
expect(result).to.contains("grpc_recorded_session")
})
})
})
@@ -0,0 +1,57 @@
import { writeFile } from "@utils/fs"
import fs from "fs/promises"
import * as path from "path"
import { GrpcSessionLog } from "@/core/controller/grpc-recorder/types"
const LOG_FILE_PREFIX = "grpc_recorded_session"
export class LogFileHandlerNoops implements ILogFileHandler {
async initialize(_initialData: GrpcSessionLog): Promise<void> {}
async write(_sessionLog: GrpcSessionLog): Promise<void> {}
}
export interface ILogFileHandler {
initialize(initialData: GrpcSessionLog): Promise<void>
write(sessionLog: GrpcSessionLog): Promise<void>
}
/**
* Default implementation of `ILogFileHandler` that persists logs to disk.
*
* - Creates a log file inside the workspace `tests/specs` folder.
* - Uses a timestamped filename by default, unless overridden by an env var.
* - Saves logs in JSON format.
*/
export class LogFileHandler implements ILogFileHandler {
private logFilePath: string
constructor() {
const fileName = this.getFileName()
const workspaceFolder = process.env.DEV_WORKSPACE_FOLDER ?? process.cwd()
const folderPath = path.join(workspaceFolder, "tests", "specs")
this.logFilePath = path.join(folderPath, fileName)
}
public getFilePath(): string {
return this.logFilePath
}
public getFileName(): string {
const envFileName = path.basename(process.env.GRPC_RECORDER_FILE_NAME || "").replace(/[^a-zA-Z0-9-_]/g, "_")
if (envFileName && envFileName.trim().length > 0) {
return `${LOG_FILE_PREFIX}_${envFileName}.json`
}
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
return `${LOG_FILE_PREFIX}_${timestamp}.json`
}
public async initialize(initialData: GrpcSessionLog): Promise<void> {
await fs.mkdir(path.dirname(this.logFilePath), { recursive: true })
await writeFile(this.logFilePath, JSON.stringify(initialData, null, 2), "utf8")
}
public async write(sessionLog: GrpcSessionLog): Promise<void> {
await writeFile(this.logFilePath, JSON.stringify(sessionLog, null, 2), "utf8")
}
}
@@ -0,0 +1,30 @@
export interface GrpcLogEntry {
requestId: string
service: string
method: string
isStreaming: boolean
request: {
message: any
}
response?: {
message?: any
error?: string
isStreaming?: boolean
sequenceNumber?: number
}
duration?: number
status: "pending" | "completed" | "error"
}
export interface SessionStats {
totalRequests: number
pendingRequests: number
completedRequests: number
errorRequests: number
}
export interface GrpcSessionLog {
startTime: string
stats?: SessionStats
entries: GrpcLogEntry[]
}
+22 -15
View File
@@ -26,6 +26,7 @@ import { telemetryService } from "@/services/telemetry"
import { ShowMessageType } from "@/shared/proto/host/window"
import { getLatestAnnouncementId } from "@/utils/announcements"
import { getCwd, getDesktopDir } from "@/utils/path"
import { PromptRegistry } from "../prompts/system-prompt"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import { PersistenceErrorEvent, StateManager } from "../storage/StateManager"
import { Task } from "../task"
@@ -40,7 +41,6 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
export class Controller {
readonly id: string
private disposables: vscode.Disposable[] = []
task?: Task
mcpHub: McpHub
@@ -53,7 +53,7 @@ export class Controller {
id: string,
) {
this.id = id
PromptRegistry.getInstance() // Ensure prompts and tools are registered
HostProvider.get().logToChannel("ClineProvider instantiated")
this.accountService = ClineAccountService.getInstance()
this.stateManager = new StateManager(context)
@@ -91,6 +91,10 @@ export class Controller {
}
}
this.stateManager.onSyncExternalChange = async () => {
await this.postStateToWebview()
}
this.mcpHub = new McpHub(
() => ensureMcpServersDirectoryExists(),
() => ensureSettingsDirectoryExists(this.context),
@@ -115,12 +119,6 @@ export class Controller {
*/
async dispose() {
await this.clearTask()
while (this.disposables.length) {
const x = this.disposables.pop()
if (x) {
x.dispose()
}
}
this.mcpHub.dispose()
console.error("Controller disposed")
@@ -195,10 +193,19 @@ export class Controller {
}
this.stateManager.setGlobalState("autoApprovalSettings", updatedAutoApprovalSettings)
}
// Apply remote feature flag gate to focus chain settings
// Apply remote feature flag gate to focus chain settings. Respect if user has disabled it.
let focusChainEnabled: boolean
if (focusChainSettings?.enabled === false) {
focusChainEnabled = false
} else if (focusChainFeatureFlagEnabled === false) {
focusChainEnabled = false
} else {
focusChainEnabled = Boolean(focusChainSettings?.enabled)
}
const effectiveFocusChainSettings = {
...(focusChainSettings || { enabled: true, remindClineInterval: 6 }),
enabled: Boolean(focusChainSettings?.enabled) && Boolean(focusChainFeatureFlagEnabled),
enabled: focusChainEnabled,
}
this.task = new Task(
@@ -629,8 +636,8 @@ export class Controller {
const workflowToggles = this.stateManager.getWorkspaceStateKey("workflowToggles")
const currentTaskItem = this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined
const checkpointTrackerErrorMessage = this.task?.taskState.checkpointTrackerErrorMessage
const clineMessages = this.task?.messageStateHandler.getClineMessages() || []
const checkpointManagerErrorMessage = this.task?.taskState.checkpointManagerErrorMessage
const processedTaskHistory = (taskHistory || [])
.filter((item) => item.ts && item.task)
@@ -649,12 +656,9 @@ export class Controller {
apiConfiguration,
uriScheme,
currentTaskItem,
checkpointTrackerErrorMessage,
clineMessages,
currentFocusChainChecklist: this.task?.taskState.currentFocusChainChecklist || null,
taskHistory: processedTaskHistory,
shouldShowAnnouncement,
platform,
checkpointManagerErrorMessage,
autoApprovalSettings,
browserSettings,
focusChainSettings,
@@ -685,6 +689,9 @@ export class Controller {
mcpResponsesCollapsed,
terminalOutputLineLimit,
customPrompt,
taskHistory: processedTaskHistory,
platform,
shouldShowAnnouncement,
}
}
@@ -1,5 +1,5 @@
import axios from "axios"
import { SapAiCoreModelsRequest, SapAiCoreModelsResponse } from "@/shared/proto/cline/models"
import { SapAiCoreModelDeployment, SapAiCoreModelsRequest, SapAiCoreModelsResponse } from "@/shared/proto/cline/models"
import { Controller } from ".."
interface Token {
@@ -11,6 +11,11 @@ interface Token {
expires_at: number
}
interface Deployment {
id: string
name: string
}
/**
* Authenticates with SAP AI Core and returns an access token
* @param clientId SAP AI Core client ID
@@ -35,19 +40,19 @@ async function getToken(clientId: string, clientSecret: string, tokenUrl: string
}
/**
* Fetches model names and orchestration availability from SAP AI Core deployments
* Fetches deployments and orchestration availability from SAP AI Core deployments
* @param accessToken Access token for authentication
* @param baseUrl SAP AI Core base URL
* @param resourceGroup SAP AI Core resource group
* @returns Promise<{modelNames: string[], orchestrationAvailable: boolean}> Model names and orchestration availability
* @returns Promise<{deployments: Deployment[], orchestrationAvailable: boolean}> Deployments and orchestration availability
*/
async function fetchAiCoreModelsAndOrchestration(
async function fetchAiCoreDeploymentsAndOrchestration(
accessToken: string,
baseUrl: string,
resourceGroup: string,
): Promise<{ modelNames: string[]; orchestrationAvailable: boolean }> {
): Promise<{ deployments: Deployment[]; orchestrationAvailable: boolean }> {
if (!accessToken) {
return { modelNames: ["ai-core-not-configured"], orchestrationAvailable: false }
return { deployments: [], orchestrationAvailable: false }
}
const headers = {
@@ -61,26 +66,29 @@ async function fetchAiCoreModelsAndOrchestration(
try {
const response = await axios.get(url, { headers })
const deployments = response.data.resources
const allDeployments = response.data.resources
// Filter running deployments
const runningDeployments = deployments.filter((deployment: any) => deployment.targetStatus === "RUNNING")
const runningDeployments = allDeployments.filter((deployment: any) => deployment.targetStatus === "RUNNING")
// Check for orchestration deployment
const orchestrationAvailable = runningDeployments.some((deployment: any) => deployment.scenarioId === "orchestration")
// Extract model names from deployments
const modelNames = runningDeployments
// Extract deployments with model names and IDs
const deployments = runningDeployments
.map((deployment: any) => {
const model = deployment.details?.resources?.backend_details?.model
if (!model?.name || !model?.version) {
return null // Skip this row
}
return `${model.name}:${model.version}`
return {
id: deployment.id,
name: `${model.name}:${model.version}`,
}
})
.filter((modelName: string | null) => modelName !== null)
.filter((deployment: any) => deployment !== null)
return { modelNames, orchestrationAvailable }
return { deployments, orchestrationAvailable }
} catch (error) {
console.error("Error fetching deployments:", error)
throw new Error("Failed to fetch deployments")
@@ -91,7 +99,7 @@ async function fetchAiCoreModelsAndOrchestration(
* Fetches available models from SAP AI Core deployments and orchestration availability
* @param controller The controller instance
* @param request The request containing SAP AI Core configuration
* @returns SapAiCoreModelsResponse with model names and orchestration availability
* @returns SapAiCoreModelsResponse with deployments and orchestration availability
*/
export async function getSapAiCoreModels(
controller: Controller,
@@ -102,30 +110,38 @@ export async function getSapAiCoreModels(
if (!request.clientId || !request.clientSecret || !request.baseUrl) {
// Return empty response if configuration is incomplete
return SapAiCoreModelsResponse.create({
modelNames: [],
deployments: [],
orchestrationAvailable: false,
})
}
// Direct authentication and model/orchestration fetching
// Direct authentication and deployment/orchestration fetching
const token = await getToken(request.clientId, request.clientSecret, request.tokenUrl)
const { modelNames, orchestrationAvailable } = await fetchAiCoreModelsAndOrchestration(
const { deployments, orchestrationAvailable } = await fetchAiCoreDeploymentsAndOrchestration(
token.access_token,
request.baseUrl,
request.resourceGroup,
)
// Extract base model names (without version) and sort
const baseModelNames = modelNames.map((modelName: string) => modelName.split(":")[0].toLowerCase()).sort()
// Create model-deployment pairs
const modelDeployments = deployments
.map((deployment) => {
const modelName = deployment.name.split(":")[0].toLowerCase()
return SapAiCoreModelDeployment.create({
modelName: modelName,
deploymentId: deployment.id,
})
})
.sort((a, b) => a.modelName.localeCompare(b.modelName))
return SapAiCoreModelsResponse.create({
modelNames: baseModelNames,
deployments: modelDeployments,
orchestrationAvailable,
})
} catch (error) {
console.error("Error fetching SAP AI Core models:", error)
return SapAiCoreModelsResponse.create({
modelNames: [],
deployments: [],
orchestrationAvailable: false,
})
}
@@ -1,55 +0,0 @@
import { HostProvider } from "@/hosts/host-provider"
import { TerminalInfo } from "@/integrations/terminal/TerminalRegistry"
import * as proto from "@/shared/proto"
import { ShowMessageType } from "@/shared/proto/host/window"
import { Controller } from "../index"
export async function updateDefaultTerminalProfile(
controller: Controller,
request: proto.cline.StringRequest,
): Promise<proto.cline.TerminalProfileUpdateResponse> {
const profileId = request.value
// Update the terminal profile in the state
controller.stateManager.setGlobalState("defaultTerminalProfile", profileId)
let closedCount = 0
let busyTerminals: TerminalInfo[] = []
// Update the terminal manager of the current task if it exists
if (controller.task) {
// Call the updated setDefaultTerminalProfile method that returns closed terminal info
const result = controller.task.terminalManager.setDefaultTerminalProfile(profileId)
closedCount = result.closedCount
busyTerminals = result.busyTerminals
// Show information message if terminals were closed
if (closedCount > 0) {
const message = `Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
}
// Show warning if there are busy terminals that couldn't be closed
if (busyTerminals.length > 0) {
const message =
`${busyTerminals.length} busy ${busyTerminals.length === 1 ? "terminal has" : "terminals have"} a different profile. ` +
`Close ${busyTerminals.length === 1 ? "it" : "them"} to use the new profile for all commands.`
HostProvider.window.showMessage({
type: ShowMessageType.WARNING,
message,
})
}
}
// Broadcast state update to all webviews
await controller.postStateToWebview()
return proto.cline.TerminalProfileUpdateResponse.create({
closedCount,
busyTerminalsCount: busyTerminals.length,
hasBusyTerminals: busyTerminals.length > 0,
})
}
@@ -9,8 +9,12 @@ import {
import { convertProtoApiConfigurationToApiConfiguration } from "@shared/proto-conversions/state/settings-conversion"
import { OpenaiReasoningEffort } from "@shared/storage/types"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { HostProvider } from "@/hosts/host-provider"
import { TerminalInfo } from "@/integrations/terminal/TerminalRegistry"
import { McpDisplayMode } from "@/shared/McpDisplayMode"
import { ShowMessageType } from "@/shared/proto/host/window"
import { telemetryService } from "../../../services/telemetry"
import { BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings"
import { Controller } from ".."
/**
@@ -98,6 +102,9 @@ export async function updateSettings(controller: Controller, request: UpdateSett
case ProtoOpenaiReasoningEffort.HIGH:
reasoningEffort = "high"
break
case ProtoOpenaiReasoningEffort.MINIMAL:
reasoningEffort = "minimal"
break
default:
throw new Error(`Invalid OpenAI reasoning effort value: ${request.openaiReasoningEffort}`)
}
@@ -176,6 +183,91 @@ export async function updateSettings(controller: Controller, request: UpdateSett
controller.stateManager.setGlobalState("customPrompt", value)
}
// Update browser settings
if (request.browserSettings !== undefined) {
// Get current browser settings to preserve fields not in the request
const currentSettings = controller.stateManager.getGlobalStateKey("browserSettings")
// Convert from protobuf format to shared format, merging with existing settings
const newBrowserSettings: SharedBrowserSettings = {
...currentSettings, // Start with existing settings (and defaults)
viewport: {
// Apply updates from request
width: request.browserSettings.viewport?.width || currentSettings.viewport.width,
height: request.browserSettings.viewport?.height || currentSettings.viewport.height,
},
// Explicitly handle optional boolean and string fields from the request
remoteBrowserEnabled:
request.browserSettings.remoteBrowserEnabled === undefined
? currentSettings.remoteBrowserEnabled
: request.browserSettings.remoteBrowserEnabled,
remoteBrowserHost:
request.browserSettings.remoteBrowserHost === undefined
? currentSettings.remoteBrowserHost
: request.browserSettings.remoteBrowserHost,
chromeExecutablePath:
// If chromeExecutablePath is explicitly in the request (even as ""), use it.
// Otherwise, fall back to mergedWithDefaults.
"chromeExecutablePath" in request.browserSettings
? request.browserSettings.chromeExecutablePath
: currentSettings.chromeExecutablePath,
disableToolUse:
request.browserSettings.disableToolUse === undefined
? currentSettings.disableToolUse
: request.browserSettings.disableToolUse,
customArgs:
"customArgs" in request.browserSettings ? request.browserSettings.customArgs : currentSettings.customArgs,
}
// Update global state with new settings
controller.stateManager.setGlobalState("browserSettings", newBrowserSettings)
// Update task browser settings if task exists
if (controller.task) {
controller.task.browserSettings = newBrowserSettings
controller.task.browserSession.browserSettings = newBrowserSettings
}
}
// Update default terminal profile
if (request.defaultTerminalProfile !== undefined) {
const profileId = request.defaultTerminalProfile
// Update the terminal profile in the state
controller.stateManager.setGlobalState("defaultTerminalProfile", profileId)
let closedCount = 0
let busyTerminals: TerminalInfo[] = []
// Update the terminal manager of the current task if it exists
if (controller.task) {
// Call the updated setDefaultTerminalProfile method that returns closed terminal info
const result = controller.task.terminalManager.setDefaultTerminalProfile(profileId)
closedCount = result.closedCount
busyTerminals = result.busyTerminals
// Show information message if terminals were closed
if (closedCount > 0) {
const message = `Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
}
// Show warning if there are busy terminals that couldn't be closed
if (busyTerminals.length > 0) {
const message =
`${busyTerminals.length} busy ${busyTerminals.length === 1 ? "terminal has" : "terminals have"} a different profile. ` +
`Close ${busyTerminals.length === 1 ? "it" : "them"} to use the new profile for all commands.`
HostProvider.window.showMessage({
type: ShowMessageType.WARNING,
message,
})
}
}
}
// Post updated state to webview
await controller.postStateToWebview()
@@ -138,7 +138,11 @@ async function cleanupTaskFiles(controller: Controller, preserveTaskIds: string[
// Delete only non-preserved task directories
for (const dir of taskDirs) {
if (!preserveTaskIds.includes(dir)) {
await fs.rm(path.join(taskDirPath, dir), { recursive: true, force: true })
// Task dir path is not workspace specific
await fs.rm(path.join(taskDirPath, dir), {
recursive: true,
force: true,
})
}
}
}
@@ -10,7 +10,7 @@ import { Controller } from ".."
export async function taskCompletionViewChanges(controller: Controller, request: Int64Request): Promise<Empty> {
try {
if (request.value && controller.task) {
await controller.task.presentMultifileDiff(request.value, true)
await controller.task.checkpointManager?.presentMultifileDiff(request.value, true)
}
return Empty.create()
} catch (error) {
+1 -1
View File
@@ -218,7 +218,7 @@ Perform your research without commentary or narration. Execute commands and read
You must use the read_file tool to examine relevant source files, configuration files, and documentation. You must use terminal commands to gather information about the codebase structure and patterns. All terminal output must be piped to cat for visibility.
### Essential Terminal Commands
Execute these commands to build your understanding. You must tailor them to the codebase and ensure the output is not overly verbose. These are only examples, the exact commands will differ depending on the codebase.
First, determine the language(s) used in the codebase, then execute these commands to build your understanding. You must tailor them to the codebase and ensure the output is not overly verbose. For example, you should exclude dependency folders such as node_modules, venv or php vendor, etc. These are only examples, the exact commands will differ depending on the codebase.
${
isPowerShell
@@ -59,6 +59,7 @@ describe("PromptRegistry", () => {
{ id: "google/gemini", expected: ModelFamily.GENERIC },
{ id: "claude-sonnet-4", expected: ModelFamily.NEXT_GEN },
{ id: "gpt-5", expected: ModelFamily.GPT_5 },
{ id: "openai/gpt-5", expected: ModelFamily.GPT_5 },
{ id: "unknown-model", expected: ModelFamily.GENERIC },
]
@@ -5,10 +5,10 @@
* model families and context configurations using snapshot testing.
*
* Usage:
* - Run tests normally: `npm test` or `yarn test`
* - Run tests normally: `npm run test:unit -- --update-snapshots`
* Tests will fail if generated prompts don't match existing snapshots
*
* - Update snapshots: `npm test -- --update-snapshots` or `yarn test --update-snapshots`
* - Update snapshots: `npm run test:unit -- --update-snapshots`
* This will regenerate all snapshot files with current prompt output
*
* When tests fail:
@@ -48,9 +48,7 @@ ${differences}
- ❌ Unexpected changes (bugs, regressions)
3. 🔄 If changes are correct, update snapshots:
npm test -- --update-snapshots
# or
yarn test --update-snapshots
npm run test:unit -- --update-snapshots
4. 🐛 If changes are unintentional, investigate:
- Check recent changes to prompt generation logic
@@ -80,15 +78,19 @@ const compareStrings = (expected: string, actual: string): string | null => {
if (differences.length < 10) {
// Limit to first 10 differences for readability
differences.push(`Line ${i + 1}:`)
if (expectedLine)
if (expectedLine) {
differences.push(` - Expected: ${expectedLine.substring(0, 100)}${expectedLine.length > 100 ? "..." : ""}`)
if (actualLine)
}
if (actualLine) {
differences.push(` + Actual: ${actualLine.substring(0, 100)}${actualLine.length > 100 ? "..." : ""}`)
}
}
}
}
if (differences.length === 0) return null
if (differences.length === 0) {
return null
}
const summary = [
`Expected length: ${expected.length} characters`,
@@ -164,7 +164,7 @@ export class PromptBuilder {
const description = [`Description: ${config.description}`]
if (!config.parameters?.length) {
return [title, description.join("\n")].join("\n")
config.parameters = []
}
// Clone parameters to avoid mutating original
@@ -197,7 +197,7 @@ export class PromptBuilder {
private static buildParametersSection(params: any[]): string {
if (!params.length) {
return ""
return "Parameters: None"
}
const paramList = params.map((p) => {
@@ -17,11 +17,7 @@ const generic: ClineToolSpec = {
id,
variant: ModelFamily.GENERIC,
name: "load_mcp_documentation",
description: `Load documentation about creating MCP servers. This tool should be used when the user requests to create or install an MCP server (the user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with \`use_mcp_tool\` and \`access_mcp_resource\`). The documentation provides detailed information about the MCP server creation process, including setup instructions, best practices, and examples.
Parameters: None
Usage:
<load_mcp_documentation>
</load_mcp_documentation>`,
description: `Load documentation about creating MCP servers. This tool should be used when the user requests to create or install an MCP server (the user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with \`use_mcp_tool\` and \`access_mcp_resource\`). The documentation provides detailed information about the MCP server creation process, including setup instructions, best practices, and examples.`,
contextRequirements: (context) => context.mcpHub !== undefined && context.mcpHub !== null,
}
@@ -11,6 +11,7 @@ export { config as gpt5Config, type GPT5VariantConfig } from "./gpt-5/config"
export { config as nextGenConfig, type NextGenVariantConfig } from "./next-gen/config"
export { config as xsConfig, type XsVariantConfig } from "./xs/config"
import { ModelFamily } from "@/shared/prompts"
import { config as genericConfig } from "./generic/config"
import { config as gpt5Config } from "./gpt-5/config"
import { config as nextGenConfig } from "./next-gen/config"
@@ -26,20 +27,21 @@ export const VARIANT_CONFIGS = {
* Generic variant - Fallback for all model types
* Optimized for broad compatibility and stable performance
*/
generic: genericConfig,
[ModelFamily.GENERIC]: genericConfig,
/**
* Next-gen variant - Advanced models with enhanced capabilities
* Includes additional features like feedback loops and web fetching
*/
"next-gen": nextGenConfig,
gpt5: gpt5Config,
[ModelFamily.NEXT_GEN]: nextGenConfig,
/**
* GPT-5 variant
*/
[ModelFamily.GPT_5]: gpt5Config,
/**
* XS variant - Compact models with limited context windows
* Streamlined for efficiency with essential tools only
*/
xs: xsConfig,
[ModelFamily.XS]: xsConfig,
} as const
/**
+70 -1
View File
@@ -1,6 +1,7 @@
import { ApiConfiguration, fireworksDefaultModelId } from "@shared/api"
import chokidar, { FSWatcher } from "chokidar"
import type { ExtensionContext } from "vscode"
import { writeTaskHistoryToState } from "./disk"
import { getTaskHistoryStateFilePath, readTaskHistoryFromState, writeTaskHistoryToState } from "./disk"
import { STATE_MANAGER_NOT_INITIALIZED } from "./error-messages"
import { GlobalState, GlobalStateKey, LocalState, LocalStateKey, SecretKey, Secrets } from "./state-keys"
import { readGlobalStateFromDisk, readSecretsFromDisk, readWorkspaceStateFromDisk } from "./utils/state-helpers"
@@ -29,10 +30,14 @@ export class StateManager {
private pendingWorkspaceState = new Set<LocalStateKey>()
private persistenceTimeout: NodeJS.Timeout | null = null
private readonly PERSISTENCE_DELAY_MS = 500
private taskHistoryWatcher: FSWatcher | null = null
// Callback for persistence errors
onPersistenceError?: (event: PersistenceErrorEvent) => void
// Callback to sync external state changes with the UI client
onSyncExternalChange?: () => void | Promise<void>
constructor(context: ExtensionContext) {
this.context = context
}
@@ -52,6 +57,9 @@ export class StateManager {
this.populateCache(globalState, secrets, workspaceState)
this.isInitialized = true
// Start watcher for taskHistory.json so external edits update cache (no persist loop)
await this.setupTaskHistoryWatcher()
} catch (error) {
console.error("[StateManager] Failed to initialize:", error)
throw error
@@ -163,6 +171,56 @@ export class StateManager {
this.scheduleDebouncedPersistence()
}
/**
* Initialize chokidar watcher for the taskHistory.json file
* Updates in-memory cache on external changes without writing back to disk.
*/
private async setupTaskHistoryWatcher(): Promise<void> {
try {
const historyFile = await getTaskHistoryStateFilePath(this.context)
// Close any existing watcher before creating a new one
if (this.taskHistoryWatcher) {
await this.taskHistoryWatcher.close()
this.taskHistoryWatcher = null
}
this.taskHistoryWatcher = chokidar.watch(historyFile, {
persistent: true,
ignoreInitial: true,
atomic: true,
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 },
})
const syncTaskHistoryFromDisk = async () => {
try {
if (!this.isInitialized) {
return
}
const onDisk = await readTaskHistoryFromState(this.context)
const cached = this.globalStateCache["taskHistory"]
if (JSON.stringify(onDisk) !== JSON.stringify(cached)) {
this.globalStateCache["taskHistory"] = onDisk
await this.onSyncExternalChange?.()
}
} catch (err) {
console.error("[StateManager] Failed to reload task history on change:", err)
}
}
this.taskHistoryWatcher
.on("add", () => syncTaskHistoryFromDisk())
.on("change", () => syncTaskHistoryFromDisk())
.on("unlink", async () => {
this.globalStateCache["taskHistory"] = []
await this.onSyncExternalChange?.()
})
.on("error", (error) => console.error("[StateManager] TaskHistory watcher error:", error))
} catch (err) {
console.error("[StateManager] Failed to set up taskHistory watcher:", err)
}
}
/**
* Convenience method for getting API configuration
* Ensures cache is initialized if not already done
@@ -277,6 +335,7 @@ export class StateManager {
planModeTogetherModelId,
planModeFireworksModelId,
planModeSapAiCoreModelId,
planModeSapAiCoreDeploymentId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeBasetenModelId,
@@ -308,6 +367,7 @@ export class StateManager {
actModeTogetherModelId,
actModeFireworksModelId,
actModeSapAiCoreModelId,
actModeSapAiCoreDeploymentId,
actModeGroqModelId,
actModeGroqModelInfo,
actModeBasetenModelId,
@@ -343,6 +403,7 @@ export class StateManager {
planModeTogetherModelId,
planModeFireworksModelId,
planModeSapAiCoreModelId,
planModeSapAiCoreDeploymentId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeBasetenModelId,
@@ -375,6 +436,7 @@ export class StateManager {
actModeTogetherModelId,
actModeFireworksModelId,
actModeSapAiCoreModelId,
actModeSapAiCoreDeploymentId,
actModeGroqModelId,
actModeGroqModelInfo,
actModeBasetenModelId,
@@ -515,6 +577,11 @@ export class StateManager {
clearTimeout(this.persistenceTimeout)
this.persistenceTimeout = null
}
// Close file watcher if active
if (this.taskHistoryWatcher) {
this.taskHistoryWatcher.close()
this.taskHistoryWatcher = null
}
this.pendingGlobalState.clear()
this.pendingSecrets.clear()
@@ -729,6 +796,7 @@ export class StateManager {
planModeTogetherModelId: this.globalStateCache["planModeTogetherModelId"],
planModeFireworksModelId: this.globalStateCache["planModeFireworksModelId"] || fireworksDefaultModelId,
planModeSapAiCoreModelId: this.globalStateCache["planModeSapAiCoreModelId"],
planModeSapAiCoreDeploymentId: this.globalStateCache["planModeSapAiCoreDeploymentId"],
planModeGroqModelId: this.globalStateCache["planModeGroqModelId"],
planModeGroqModelInfo: this.globalStateCache["planModeGroqModelInfo"],
planModeBasetenModelId: this.globalStateCache["planModeBasetenModelId"],
@@ -761,6 +829,7 @@ export class StateManager {
actModeTogetherModelId: this.globalStateCache["actModeTogetherModelId"],
actModeFireworksModelId: this.globalStateCache["actModeFireworksModelId"] || fireworksDefaultModelId,
actModeSapAiCoreModelId: this.globalStateCache["actModeSapAiCoreModelId"],
actModeSapAiCoreDeploymentId: this.globalStateCache["actModeSapAiCoreDeploymentId"],
actModeGroqModelId: this.globalStateCache["actModeGroqModelId"],
actModeGroqModelInfo: this.globalStateCache["actModeGroqModelInfo"],
actModeBasetenModelId: this.globalStateCache["actModeBasetenModelId"],
+5 -1
View File
@@ -201,7 +201,11 @@ export async function readTaskHistoryFromState(context: vscode.ExtensionContext)
try {
const filePath = await getTaskHistoryStateFilePath(context)
if (await fileExistsAtPath(filePath)) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
const contents = await fs.readFile(filePath, "utf8")
if (contents.trim() === "") {
return []
}
return JSON.parse(contents)
}
return []
} catch (error) {
+2
View File
@@ -105,6 +105,7 @@ export interface GlobalState {
planModeTogetherModelId: string | undefined
planModeFireworksModelId: string | undefined
planModeSapAiCoreModelId: string | undefined
planModeSapAiCoreDeploymentId: string | undefined
planModeGroqModelId: string | undefined
planModeGroqModelInfo: ModelInfo | undefined
planModeBasetenModelId: string | undefined
@@ -134,6 +135,7 @@ export interface GlobalState {
actModeTogetherModelId: string | undefined
actModeFireworksModelId: string | undefined
actModeSapAiCoreModelId: string | undefined
actModeSapAiCoreDeploymentId: string | undefined
actModeGroqModelId: string | undefined
actModeGroqModelInfo: ModelInfo | undefined
actModeBasetenModelId: string | undefined
+30 -12
View File
@@ -1,6 +1,7 @@
import fs from "fs/promises"
import path from "path"
import * as vscode from "vscode"
import { HistoryItem } from "@/shared/HistoryItem"
import { ensureRulesDirectoryExists, readTaskHistoryFromState, writeTaskHistoryToState } from "./disk"
import { StateManager } from "./StateManager"
@@ -68,20 +69,37 @@ export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionC
export async function migrateTaskHistoryToFile(context: vscode.ExtensionContext) {
try {
// If the old taskHistory vs code global state is undefined, do nothing
const vscodeGlobalStateTaskHistory = await context.globalState.get("taskHistory")
if (vscodeGlobalStateTaskHistory === undefined) {
// Get data from old location
const vscodeGlobalStateTaskHistory = context.globalState.get<HistoryItem[] | undefined>("taskHistory")
// Normalize old location data to array (empty array if undefined/null/not-array)
const oldLocationData = Array.isArray(vscodeGlobalStateTaskHistory) ? vscodeGlobalStateTaskHistory : []
// Early return if no migration needed
if (oldLocationData.length === 0) {
console.log("[Storage Migration] No task history to migrate")
return
}
// Read legacy from VS Code globalState, default to []
console.log("[Storage Migration] taskHistory from vscode global state: ", vscodeGlobalStateTaskHistory)
// Always create the file, even when empty
await writeTaskHistoryToState(context, Array.isArray(vscodeGlobalStateTaskHistory) ? vscodeGlobalStateTaskHistory : [])
// Don't remove the old taskHistory yet, while this version is not in production, for better dev experience.
// This is because the old version of the code (still in prod) is still reading taskHistory from the vs code global state, so it will appear as if all the user's tasks have been deleted.
// await context.globalState.update("taskHistory", undefined)
console.log("[Storage Migration] taskHistory file in new location: ", await readTaskHistoryFromState(context))
console.log("[Storage Migration] old vscode global state: ", await context.globalState.get("taskHistory"))
let finalData: HistoryItem[]
let migrationAction: string
const newLocationData = await readTaskHistoryFromState(context)
if (newLocationData.length === 0) {
// Move old data to new location
finalData = oldLocationData
migrationAction = "Migrated task history from old location to new location"
} else {
// Merge old data (more recent) with new data
finalData = [...newLocationData, ...oldLocationData]
migrationAction = "Merged task history from old and new locations"
}
// Perform migration operations sequentially - only clear old data if write succeeds
await writeTaskHistoryToState(context, finalData)
void context.globalState.update("taskHistory", undefined)
console.log(`[Storage Migration] ${migrationAction}`)
} catch (error) {
console.error("[Storage Migration] Failed to migrate task history to file:", error)
}
+4
View File
@@ -239,6 +239,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
const planModeTogetherModelId = context.globalState.get("planModeTogetherModelId") as string | undefined
const planModeFireworksModelId = context.globalState.get("planModeFireworksModelId") as string | undefined
const planModeSapAiCoreModelId = context.globalState.get("planModeSapAiCoreModelId") as string | undefined
const planModeSapAiCoreDeploymentId = context.globalState.get("planModeSapAiCoreDeploymentId") as string | undefined
const planModeGroqModelId = context.globalState.get("planModeGroqModelId") as string | undefined
const planModeGroqModelInfo = context.globalState.get("planModeGroqModelInfo") as ModelInfo | undefined
const planModeHuggingFaceModelId = context.globalState.get("planModeHuggingFaceModelId") as string | undefined
@@ -278,6 +279,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
const actModeTogetherModelId = context.globalState.get("actModeTogetherModelId") as string | undefined
const actModeFireworksModelId = context.globalState.get("actModeFireworksModelId") as string | undefined
const actModeSapAiCoreModelId = context.globalState.get("actModeSapAiCoreModelId") as string | undefined
const actModeSapAiCoreDeploymentId = context.globalState.get("actModeSapAiCoreDeploymentId") as string | undefined
const actModeGroqModelId = context.globalState.get("actModeGroqModelId") as string | undefined
const actModeGroqModelInfo = context.globalState.get("actModeGroqModelInfo") as ModelInfo | undefined
const actModeHuggingFaceModelId = context.globalState.get("actModeHuggingFaceModelId") as string | undefined
@@ -380,6 +382,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
planModeTogetherModelId,
planModeFireworksModelId,
planModeSapAiCoreModelId,
planModeSapAiCoreDeploymentId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeHuggingFaceModelId,
@@ -411,6 +414,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
actModeTogetherModelId,
actModeFireworksModelId,
actModeSapAiCoreModelId,
actModeSapAiCoreDeploymentId,
actModeGroqModelId,
actModeGroqModelInfo,
actModeHuggingFaceModelId,
+1 -1
View File
@@ -43,7 +43,7 @@ export class TaskState {
// Error tracking
consecutiveMistakeCount: number = 0
didAutomaticallyRetryFailedApiRequest = false
checkpointTrackerErrorMessage?: string
checkpointManagerErrorMessage?: string
// Task Initialization
isInitialized = false
+15 -9
View File
@@ -10,10 +10,11 @@ import { BrowserSettings } from "@shared/BrowserSettings"
import { ClineAsk, ClineSay } from "@shared/ExtensionMessage"
import { FocusChainSettings } from "@shared/FocusChainSettings"
import { Mode } from "@shared/storage/types"
import { ClineDefaultTool } from "@shared/tools"
import { ClineAskResponse } from "@shared/WebviewMessage"
import * as vscode from "vscode"
import { modelDoesntSupportWebp } from "@/utils/model-utils"
import { ToolUse, ToolUseName } from "../assistant-message"
import { ToolUse } from "../assistant-message"
import { ContextManager } from "../context/context-management/ContextManager"
import { formatResponse } from "../prompts/responses"
import { StateManager } from "../storage/StateManager"
@@ -51,12 +52,12 @@ export class ToolExecutor {
private coordinator: ToolExecutorCoordinator
// Auto-approval methods using the AutoApprove class
private shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
private shouldAutoApproveTool(toolName: ClineDefaultTool): boolean | [boolean, boolean] {
return this.autoApprover.shouldAutoApproveTool(toolName)
}
private async shouldAutoApproveToolWithPath(
blockname: ToolUseName,
blockname: ClineDefaultTool,
autoApproveActionpath: string | undefined,
): Promise<boolean> {
return this.autoApprover.shouldAutoApproveToolWithPath(blockname, autoApproveActionpath)
@@ -106,7 +107,7 @@ export class ToolExecutor {
files?: string[]
}>,
private saveCheckpoint: (isAttemptCompletionMessage?: boolean, completionMessageTs?: number) => Promise<void>,
private sayAndCreateMissingParamError: (toolName: ToolUseName, paramName: string, relPath?: string) => Promise<any>,
private sayAndCreateMissingParamError: (toolName: ClineDefaultTool, paramName: string, relPath?: string) => Promise<any>,
private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>,
private executeCommandTool: (command: string) => Promise<[boolean, any]>,
private doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>,
@@ -186,9 +187,9 @@ export class ToolExecutor {
// Register WriteToFileToolHandler for all three file tools with proper typing
const writeHandler = new WriteToFileToolHandler(validator)
this.coordinator.register(writeHandler) // registers as "write_to_file"
this.coordinator.register(new SharedToolHandler("replace_in_file", writeHandler))
this.coordinator.register(new SharedToolHandler("new_rule", writeHandler))
this.coordinator.register(writeHandler) // registers as "write_to_file" (ClineDefaultTool.FILE_NEW)
this.coordinator.register(new SharedToolHandler(ClineDefaultTool.FILE_EDIT, writeHandler))
this.coordinator.register(new SharedToolHandler(ClineDefaultTool.NEW_RULE, writeHandler))
this.coordinator.register(new ListCodeDefinitionNamesToolHandler(validator))
this.coordinator.register(new SearchFilesToolHandler(validator))
@@ -246,6 +247,7 @@ export class ToolExecutor {
* Handles errors during tool execution
*/
private async handleError(action: string, error: Error, block: ToolUse): Promise<void> {
console.log(error)
const errorString = `Error ${action}: ${error.message}`
await this.say("error", errorString)
@@ -272,7 +274,11 @@ export class ToolExecutor {
/**
* Tools that are restricted in plan mode and can only be used in act mode
*/
private static readonly PLAN_MODE_RESTRICTED_TOOLS: ToolUseName[] = ["write_to_file", "replace_in_file", "new_rule"]
private static readonly PLAN_MODE_RESTRICTED_TOOLS: ClineDefaultTool[] = [
ClineDefaultTool.FILE_NEW,
ClineDefaultTool.FILE_EDIT,
ClineDefaultTool.NEW_RULE,
]
/**
* Execute a tool through the coordinator if it's registered
@@ -337,7 +343,7 @@ export class ToolExecutor {
/**
* Check if a tool is restricted in plan mode
*/
private isPlanModeToolRestricted(toolName: ToolUseName): boolean {
private isPlanModeToolRestricted(toolName: ClineDefaultTool): boolean {
return ToolExecutor.PLAN_MODE_RESTRICTED_TOOLS.includes(toolName)
}
+8 -2
View File
@@ -26,7 +26,7 @@ export interface FocusChainDependencies {
context: vscode.ExtensionContext
stateManager: StateManager
postStateToWebview: () => Promise<void>
say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean) => Promise<undefined>
say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean) => Promise<number | undefined>
focusChainSettings: FocusChainSettings
}
@@ -37,7 +37,13 @@ export class FocusChainManager {
private context: vscode.ExtensionContext
private stateManager: StateManager
private postStateToWebview: () => Promise<void>
private say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean) => Promise<undefined>
private say: (
type: ClineSay,
text?: string,
images?: string[],
files?: string[],
partial?: boolean,
) => Promise<number | undefined>
private focusChainFileWatcherCancel?: () => void
private hasTrackedFirstProgress = false
private focusChainSettings: FocusChainSettings
+161 -521
View File
@@ -2,7 +2,7 @@ import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandler, ApiProviderInfo, buildApiHandler } from "@core/api"
import { ApiStream } from "@core/api/transform/stream"
import { parseAssistantMessageV2, ToolUseName } from "@core/assistant-message"
import { parseAssistantMessageV2 } from "@core/assistant-message"
import { ContextManager } from "@core/context/context-management/ContextManager"
import { checkContextWindowExceededError } from "@core/context/context-management/context-error-handling"
import { getContextWindowInfo } from "@core/context/context-management/context-window-utils"
@@ -19,7 +19,6 @@ import {
refreshExternalRulesToggles,
} from "@core/context/instructions/user-instructions/external-rules"
import { sendPartialMessageEvent } from "@core/controller/ui/subscribeToPartialMessage"
import { sendRelinquishControlEvent } from "@core/controller/ui/subscribeToRelinquishControl"
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
import { parseMentions } from "@core/mentions"
import { summarizeTask } from "@core/prompts/contextManagement"
@@ -32,7 +31,7 @@ import {
getSavedApiConversationHistory,
getSavedClineMessages,
} from "@core/storage/disk"
import CheckpointTracker from "@integrations/checkpoints/CheckpointTracker"
import { createTaskCheckpointManager, TaskCheckpointManager } from "@integrations/checkpoints"
import { DiffViewProvider } from "@integrations/editor/DiffViewProvider"
import { formatContentBlockToMarkdown } from "@integrations/misc/export-markdown"
import { processFilesIntoText } from "@integrations/misc/extract-text"
@@ -51,12 +50,12 @@ import { combineApiRequests } from "@shared/combineApiRequests"
import { combineCommandSequences } from "@shared/combineCommandSequences"
import { ClineApiReqCancelReason, ClineApiReqInfo, ClineAsk, ClineMessage, ClineSay } from "@shared/ExtensionMessage"
import { FocusChainSettings } from "@shared/FocusChainSettings"
import { getApiMetrics } from "@shared/getApiMetrics"
import { HistoryItem } from "@shared/HistoryItem"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@shared/Languages"
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
import { Mode, OpenaiReasoningEffort } from "@shared/storage/types"
import { ClineAskResponse, ClineCheckpointRestore } from "@shared/WebviewMessage"
import { ClineDefaultTool } from "@shared/tools"
import { ClineAskResponse } from "@shared/WebviewMessage"
import { getGitRemoteUrls, getLatestGitCommitHash } from "@utils/git"
import { arePathsEqual, getDesktopDir } from "@utils/path"
import cloneDeep from "clone-deep"
@@ -70,7 +69,7 @@ import type { SystemPromptContext } from "@/core/prompts/system-prompt"
import { getSystemPrompt } from "@/core/prompts/system-prompt"
import { HostProvider } from "@/hosts/host-provider"
import { errorService } from "@/services/error"
import { telemetryService } from "@/services/telemetry"
import { TerminalHangStage, TerminalUserInterventionAction, telemetryService } from "@/services/telemetry"
import { ShowMessageType } from "@/shared/proto/index.host"
import { isInTestMode } from "../../services/test/TestMode"
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
@@ -80,7 +79,6 @@ import { isNextGenModelFamily } from "../prompts/system-prompt/utils"
import { StateManager } from "../storage/StateManager"
import { FocusChainManager } from "./focus-chain"
import { MessageStateHandler } from "./message-state"
import { showChangedFilesDiff } from "./multifile-diff"
import { TaskState } from "./TaskState"
import { ToolExecutor } from "./ToolExecutor"
import { updateApiReqMsg } from "./utils"
@@ -112,7 +110,7 @@ export class Task {
browserSession: BrowserSession
contextManager: ContextManager
private diffViewProvider: DiffViewProvider
private checkpointTracker?: CheckpointTracker
public checkpointManager?: TaskCheckpointManager
private clineIgnoreController: ClineIgnoreController
private toolExecutor: ToolExecutor
@@ -227,8 +225,8 @@ export class Task {
this.ulid = historyItem.ulid ?? ulid()
this.taskIsFavorited = historyItem.isFavorited
this.taskState.conversationHistoryDeletedRange = historyItem.conversationHistoryDeletedRange
if (historyItem.checkpointTrackerErrorMessage) {
this.taskState.checkpointTrackerErrorMessage = historyItem.checkpointTrackerErrorMessage
if (historyItem.checkpointManagerErrorMessage) {
this.taskState.checkpointManagerErrorMessage = historyItem.checkpointManagerErrorMessage
}
} else if (task || images || files) {
this.taskId = Date.now().toString()
@@ -264,6 +262,44 @@ export class Task {
})
}
// Initialize checkpoint manager
try {
this.checkpointManager = createTaskCheckpointManager(
{
taskId: this.taskId,
},
{
enableCheckpoints: enableCheckpointsSetting,
},
{
context: controller.context,
diffViewProvider: this.diffViewProvider,
messageStateHandler: this.messageStateHandler,
fileContextTracker: this.fileContextTracker,
taskState: this.taskState,
},
{
updateTaskHistory: this.updateTaskHistory,
say: this.say.bind(this),
cancelTask: this.cancelTask,
postStateToWebview: this.postStateToWebview,
},
{
conversationHistoryDeletedRange: this.taskState.conversationHistoryDeletedRange,
checkpointManagerErrorMessage: this.taskState.checkpointManagerErrorMessage,
},
)
} catch (error) {
console.error("Failed to initialize checkpoint manager:", error)
if (enableCheckpointsSetting) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to initialize checkpoint manager: ${errorMessage}`,
})
}
}
// Prepare effective API configuration
const effectiveApiConfiguration: ApiConfiguration = {
...apiConfiguration,
@@ -364,11 +400,11 @@ export class Task {
strictPlanModeEnabled,
this.say.bind(this),
this.ask.bind(this),
this.saveCheckpoint.bind(this),
this.saveCheckpointCallback.bind(this),
this.sayAndCreateMissingParamError.bind(this),
this.removeLastPartialMessageIfExistsWithType.bind(this),
this.executeCommandTool.bind(this),
this.doesLatestTaskCompletionHaveNewChanges.bind(this),
() => this.checkpointManager?.doesLatestTaskCompletionHaveNewChanges() ?? Promise.resolve(false),
this.FocusChainManager?.updateFCListFromToolResponse.bind(this.FocusChainManager) || (async () => {}),
)
}
@@ -419,310 +455,6 @@ export class Task {
}
}
async restoreCheckpoint(messageTs: number, restoreType: ClineCheckpointRestore, offset?: number) {
const clineMessages = this.messageStateHandler.getClineMessages()
const messageIndex = clineMessages.findIndex((m) => m.ts === messageTs) - (offset || 0)
// Find the last message before messageIndex that has a lastCheckpointHash
const lastHashIndex = findLastIndex(clineMessages.slice(0, messageIndex), (m) => m.lastCheckpointHash !== undefined)
const message = clineMessages[messageIndex]
const lastMessageWithHash = clineMessages[lastHashIndex]
if (!message) {
console.error("Message not found", clineMessages)
return
}
let didWorkspaceRestoreFail = false
switch (restoreType) {
case "task":
break
case "taskAndWorkspace":
case "workspace":
if (!this.enableCheckpoints) {
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Checkpoints are disabled in settings.",
})
didWorkspaceRestoreFail = true
break
}
if (!this.checkpointTracker && !this.taskState.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.controller.context.globalStorageUri.fsPath,
this.enableCheckpoints,
)
this.messageStateHandler.setCheckpointTracker(this.checkpointTracker)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
this.taskState.checkpointTrackerErrorMessage = errorMessage
await this.postStateToWebview()
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: errorMessage,
})
didWorkspaceRestoreFail = true
}
}
if (message.lastCheckpointHash && this.checkpointTracker) {
try {
await this.checkpointTracker.resetHead(message.lastCheckpointHash)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to restore checkpoint: " + errorMessage,
})
didWorkspaceRestoreFail = true
}
} else if (offset && lastMessageWithHash.lastCheckpointHash && this.checkpointTracker) {
try {
await this.checkpointTracker.resetHead(lastMessageWithHash.lastCheckpointHash)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to restore offsetcheckpoint: " + errorMessage,
})
didWorkspaceRestoreFail = true
}
} else if (!offset && lastMessageWithHash.lastCheckpointHash && this.checkpointTracker) {
// Fallback: restore to most recent checkpoint when target message has no checkpoint hash
console.warn(`Message ${messageTs} has no checkpoint hash, falling back to previous checkpoint`)
try {
await this.checkpointTracker.resetHead(lastMessageWithHash.lastCheckpointHash)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to restore checkpoint: " + errorMessage,
})
didWorkspaceRestoreFail = true
}
} else {
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to restore checkpoint",
})
}
break
}
if (!didWorkspaceRestoreFail) {
switch (restoreType) {
case "task":
case "taskAndWorkspace": {
this.taskState.conversationHistoryDeletedRange = message.conversationHistoryDeletedRange
const apiConversationHistory = this.messageStateHandler.getApiConversationHistory()
const newConversationHistory = apiConversationHistory.slice(0, (message.conversationHistoryIndex || 0) + 2) // +1 since this index corresponds to the last user message, and another +1 since slice end index is exclusive
await this.messageStateHandler.overwriteApiConversationHistory(newConversationHistory)
// update the context history state
await this.contextManager.truncateContextHistory(
message.ts,
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
)
// aggregate deleted api reqs info so we don't lose costs/tokens
const clineMessages = this.messageStateHandler.getClineMessages()
const deletedMessages = clineMessages.slice(messageIndex + 1)
const deletedApiReqsMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(deletedMessages)))
// Detect files edited after this message timestamp for file context warning
// Only needed for task-only restores when a user edits a message or restores the task context, but not the files.
if (restoreType === "task") {
const filesEditedAfterMessage = await this.fileContextTracker.detectFilesEditedAfterMessage(
messageTs,
deletedMessages,
)
if (filesEditedAfterMessage.length > 0) {
await this.fileContextTracker.storePendingFileContextWarning(filesEditedAfterMessage)
}
}
const newClineMessages = clineMessages.slice(0, messageIndex + 1)
await this.messageStateHandler.overwriteClineMessages(newClineMessages) // calls saveClineMessages which saves historyItem
await this.say(
"deleted_api_reqs",
JSON.stringify({
tokensIn: deletedApiReqsMetrics.totalTokensIn,
tokensOut: deletedApiReqsMetrics.totalTokensOut,
cacheWrites: deletedApiReqsMetrics.totalCacheWrites,
cacheReads: deletedApiReqsMetrics.totalCacheReads,
cost: deletedApiReqsMetrics.totalCost,
} satisfies ClineApiReqInfo),
)
break
}
case "workspace":
break
}
switch (restoreType) {
case "task":
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: "Task messages have been restored to the checkpoint",
})
break
case "workspace":
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: "Workspace files have been restored to the checkpoint",
})
break
case "taskAndWorkspace":
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: "Task and workspace have been restored to the checkpoint",
})
break
}
if (restoreType !== "task") {
// Set isCheckpointCheckedOut flag on the message
// Find all checkpoint messages before this one
const checkpointMessages = this.messageStateHandler
.getClineMessages()
.filter((m) => m.say === "checkpoint_created")
const currentMessageIndex = checkpointMessages.findIndex((m) => m.ts === messageTs)
// Set isCheckpointCheckedOut to false for all checkpoint messages
checkpointMessages.forEach((m, i) => {
m.isCheckpointCheckedOut = i === currentMessageIndex
})
}
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
this.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
} else {
sendRelinquishControlEvent()
}
}
async presentMultifileDiff(messageTs: number, seeNewChangesSinceLastTaskCompletion: boolean) {
try {
if (!this.enableCheckpoints) {
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: "Checkpoints are disabled in settings. Cannot show diff.",
})
return
}
// TODO: handle if this is called from outside original workspace, in which case we need to
// show user error message we can't show diff outside of workspace?
if (!this.checkpointTracker && !this.taskState.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.controller.context.globalStorageUri.fsPath,
this.enableCheckpoints,
)
this.messageStateHandler.setCheckpointTracker(this.checkpointTracker)
} catch (error) {
console.error("Failed to initialize checkpoint tracker:", error)
const errorMessage = error instanceof Error ? error.message : "Unknown error"
this.taskState.checkpointTrackerErrorMessage = errorMessage
await this.postStateToWebview()
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: errorMessage,
})
return
}
}
if (!this.checkpointTracker) {
return
}
showChangedFilesDiff(
this.messageStateHandler,
this.checkpointTracker,
messageTs,
seeNewChangesSinceLastTaskCompletion,
)
} finally {
sendRelinquishControlEvent()
}
}
async doesLatestTaskCompletionHaveNewChanges() {
if (!this.enableCheckpoints) {
return false
}
const clineMessages = this.messageStateHandler.getClineMessages()
const messageIndex = findLastIndex(clineMessages, (m) => m.say === "completion_result")
const message = clineMessages[messageIndex]
if (!message) {
console.error("Completion message not found")
return false
}
const hash = message.lastCheckpointHash
if (!hash) {
console.error("No checkpoint hash found")
return false
}
if (this.enableCheckpoints && !this.checkpointTracker && !this.taskState.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.controller.context.globalStorageUri.fsPath,
this.enableCheckpoints,
)
this.messageStateHandler.setCheckpointTracker(this.checkpointTracker)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
return false
}
}
// Get last task completed
const lastTaskCompletedMessage = findLast(
this.messageStateHandler.getClineMessages().slice(0, messageIndex),
(m) => m.say === "completion_result",
)
try {
// Get last task completed
const lastTaskCompletedMessageCheckpointHash = lastTaskCompletedMessage?.lastCheckpointHash // ask is only used to relinquish control, its the last say we care about
// if undefined, then we get diff from beginning of git
// if (!lastTaskCompletedMessage) {
// console.error("No previous task completion message found")
// return
// }
// This value *should* always exist
const firstCheckpointMessageCheckpointHash = this.messageStateHandler
.getClineMessages()
.find((m) => m.say === "checkpoint_created")?.lastCheckpointHash
const previousCheckpointHash = lastTaskCompletedMessageCheckpointHash || firstCheckpointMessageCheckpointHash // either use the diff between the first checkpoint and the task completion, or the diff between the latest two task completions
if (!previousCheckpointHash) {
return false
}
// Get count of changed files between current state and commit
const changedFilesCount = (await this.checkpointTracker?.getDiffCount(previousCheckpointHash, hash)) || 0
if (changedFilesCount > 0) {
return true
}
} catch (error) {
console.error("Failed to get diff set:", error)
return false
}
return false
}
// Communicate with webview
// partial has three valid states true (partial message), false (completion of partial message), undefined (individual complete message)
@@ -865,7 +597,13 @@ export class Task {
this.taskState.askResponseFiles = files
}
async say(type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean): Promise<undefined> {
async say(
type: ClineSay,
text?: string,
images?: string[],
files?: string[],
partial?: boolean,
): Promise<number | undefined> {
if (this.taskState.abort) {
throw new Error("Cline instance aborted")
}
@@ -883,6 +621,7 @@ export class Task {
lastMessage.partial = partial
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
return undefined
} else {
// this is a new partial message, so add it with partial state
const sayTs = Date.now()
@@ -897,6 +636,7 @@ export class Task {
partial,
})
await this.postStateToWebview()
return sayTs
}
} else {
// partial=false means its a complete version of a previously partial message
@@ -914,6 +654,7 @@ export class Task {
// await this.postStateToWebview()
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage) // more performant than an entire postStateToWebview
return undefined
} else {
// this is a new partial=false message, so add it like normal
const sayTs = Date.now()
@@ -927,6 +668,7 @@ export class Task {
files,
})
await this.postStateToWebview()
return sayTs
}
}
} else {
@@ -942,10 +684,11 @@ export class Task {
files,
})
await this.postStateToWebview()
return sayTs
}
}
async sayAndCreateMissingParamError(toolName: ToolUseName, paramName: string, relPath?: string) {
async sayAndCreateMissingParamError(toolName: ClineDefaultTool, paramName: string, relPath?: string) {
await this.say(
"error",
`Cline tried to use ${toolName}${
@@ -964,6 +707,10 @@ export class Task {
}
}
private async saveCheckpointCallback(isAttemptCompletionMessage?: boolean, completionMessageTs?: number): Promise<void> {
return this.checkpointManager?.saveCheckpoint(isAttemptCompletionMessage, completionMessageTs) ?? Promise.resolve()
}
// Task lifecycle
private async startTask(task?: string, images?: string[], files?: string[]): Promise<void> {
@@ -1014,12 +761,6 @@ export class Task {
console.error("Failed to initialize ClineIgnoreController:", error)
// Optionally, inform the user or handle the error appropriately
}
// UPDATE: we don't need this anymore since most tasks are now created with checkpoints enabled
// right now we let users init checkpoints for old tasks, assuming they're continuing them from the same workspace (which we never tied to tasks, so no way for us to know if it's opened in the right workspace)
// const doesShadowGitExist = await CheckpointTracker.doesShadowGitExist(this.taskId, this.controllerRef.deref())
// if (!doesShadowGitExist) {
// this.checkpointTrackerErrorMessage = "Checkpoints are only available for new tasks"
// }
const savedClineMessages = await getSavedClineMessages(this.getContext(), this.taskId)
@@ -1077,9 +818,7 @@ export class Task {
let responseFiles: string[] | undefined
if (response === "messageResponse") {
await this.say("user_feedback", text, images, files)
if (!this.taskState.checkpointTrackerErrorMessage?.includes("Checkpoints initialization timed out.")) {
await this.saveCheckpoint()
}
await this.checkpointManager?.saveCheckpoint()
responseText = text
responseImages = images
responseFiles = files
@@ -1244,147 +983,6 @@ export class Task {
}
}
// Checkpoints
async saveCheckpoint(isAttemptCompletionMessage: boolean = false) {
if (
!this.enableCheckpoints ||
this.taskState.checkpointTrackerErrorMessage?.includes("Checkpoints initialization timed out.")
) {
// If checkpoints are disabled or previously encountered a timeout error, do nothing.
return
}
// Set isCheckpointCheckedOut to false for all checkpoint_created messages
this.messageStateHandler.getClineMessages().forEach((message) => {
if (message.say === "checkpoint_created") {
message.isCheckpointCheckedOut = false
}
})
if (!isAttemptCompletionMessage) {
// ensure we aren't creating a duplicate checkpoint
const lastMessage = this.messageStateHandler.getClineMessages().at(-1)
if (lastMessage?.say === "checkpoint_created") {
return
}
// Initialize checkpoint tracker if it doesn't exist
if (!this.checkpointTracker && !this.taskState.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.controller.context.globalStorageUri.fsPath,
this.enableCheckpoints,
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
this.taskState.checkpointTrackerErrorMessage = errorMessage
await this.postStateToWebview()
return
}
}
// Create a checkpoint commit and update clineMessages with a commitHash
if (this.checkpointTracker) {
// We are letting this run in a non-blocking way so that the UI doesn't freeze when creating checkpoints.
// We show that a checkpoint is created in the chatview, then in the background run the git operation (which can take multiple seconds for large shadow git repos), and once that's been completed update the previous checkpoint message with the newly created hash to be associated with.
// NOTE: the attempt completion flow is different in that it requires the latest checkpoint hash to be present before determining if it can present the 'see new changes' button. In ToolExecutor, when we call saveCheckpoint(true), we must make sure that the checkpoint hash is present in the last completion_result message before returning, since it is always followed by a addNewChangesFlagToLastCompletionResultMessage(), which calls doesLatestTaskCompletionHaveNewChanges() that uses the latest message hash to determine if there any changes since the last attempt_completion checkpoint.
await this.say("checkpoint_created")
this.checkpointTracker.commit().then(async (commitHash) => {
if (commitHash) {
const lastCheckpointMessageIndex = findLastIndex(
this.messageStateHandler.getClineMessages(),
(m) => m.say === "checkpoint_created",
)
if (lastCheckpointMessageIndex !== -1) {
await this.messageStateHandler.updateClineMessage(lastCheckpointMessageIndex, {
lastCheckpointHash: commitHash,
})
}
}
})
} // silently fails for now
//
} else {
// attempt completion requires checkpoint to be sync so that we can present button after attempt_completion
// Check if checkpoint tracker exists, if not, create it. Skip if there was a previous checkpoints initialization timeout error.
if (
!this.checkpointTracker &&
!this.taskState.checkpointTrackerErrorMessage?.includes("Checkpoints initialization timed out.")
) {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.controller.context.globalStorageUri.fsPath,
this.enableCheckpoints,
)
this.messageStateHandler.setCheckpointTracker(this.checkpointTracker)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker for attempt completion:", errorMessage)
return
}
}
if (
this.checkpointTracker &&
!this.taskState.checkpointTrackerErrorMessage?.includes("Checkpoints initialization timed out.")
) {
const commitHash = await this.checkpointTracker.commit()
// For attempt_completion, find the last completion_result message and set its checkpoint hash. This will be used to present the 'see new changes' button
const lastCompletionResultMessage = findLast(
this.messageStateHandler.getClineMessages(),
(m) => m.say === "completion_result" || m.ask === "completion_result",
)
if (lastCompletionResultMessage) {
lastCompletionResultMessage.lastCheckpointHash = commitHash
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
}
} else {
console.error("Checkpoint tracker does not exist and could not be initialized for attempt completion")
}
}
// if (commitHash) {
// Previously we checkpointed every message, but this is excessive and unnecessary.
// // Start from the end and work backwards until we find a tool use or another message with a hash
// for (let i = this.clineMessages.length - 1; i >= 0; i--) {
// const message = this.clineMessages[i]
// if (message.lastCheckpointHash) {
// // Found a message with a hash, so we can stop
// break
// }
// // Update this message with a hash
// message.lastCheckpointHash = commitHash
// // We only care about adding the hash to the last tool use (we don't want to add this hash to every prior message ie for tasks pre-checkpoint)
// const isToolUse =
// message.say === "tool" ||
// message.ask === "tool" ||
// message.say === "command" ||
// message.ask === "command" ||
// message.say === "completion_result" ||
// message.ask === "completion_result" ||
// message.ask === "followup" ||
// message.say === "use_mcp_server" ||
// message.ask === "use_mcp_server" ||
// message.say === "browser_action" ||
// message.say === "browser_action_launch" ||
// message.ask === "browser_action_launch"
// if (isToolUse) {
// break
// }
// }
// // Save the updated messages
// await this.saveClineMessagesAndUpdateHistory()
// }
}
// Tools
/**
@@ -1491,6 +1089,10 @@ export class Task {
let chunkTimer: NodeJS.Timeout | null = null
let chunkEnroute = false
// Track if buffer gets stuck
let bufferStuckTimer: NodeJS.Timeout | null = null
const BUFFER_STUCK_TIMEOUT_MS = 6000 // 6 seconds
const flushBuffer = async (force = false) => {
if (chunkEnroute || outputBuffer.length === 0) {
if (force && !chunkEnroute && outputBuffer.length > 0) {
@@ -1503,9 +1105,18 @@ export class Task {
outputBuffer = []
outputBufferSize = 0
chunkEnroute = true
// Start timer to detect if buffer gets stuck
bufferStuckTimer = setTimeout(() => {
telemetryService.captureTerminalHang(TerminalHangStage.BUFFER_STUCK)
bufferStuckTimer = null
}, BUFFER_STUCK_TIMEOUT_MS)
try {
const { response, text, images, files } = await this.ask("command_output", chunk)
if (response === "yesButtonClicked") {
// Track when user clicks "Process while Running"
telemetryService.captureTerminalUserIntervention(TerminalUserInterventionAction.PROCESS_WHILE_RUNNING)
// proceed while running - but still capture user feedback if provided
if (text || (images && images.length > 0) || (files && files.length > 0)) {
userFeedback = { text, images, files }
@@ -1517,7 +1128,13 @@ export class Task {
process.continue()
} catch {
Logger.error("Error while asking for command output")
telemetryService.captureTerminalHang(TerminalHangStage.STREAM_TIMEOUT)
} finally {
// Clear the stuck timer
if (bufferStuckTimer) {
clearTimeout(bufferStuckTimer)
bufferStuckTimer = null
}
chunkEnroute = false
// If more output accumulated while chunkEnroute, flush again
if (outputBuffer.length > 0) {
@@ -1552,8 +1169,24 @@ export class Task {
})
let completed = false
let completionTimer: NodeJS.Timeout | null = null
const COMPLETION_TIMEOUT_MS = 6000 // 6 seconds
// Start timer to detect if waiting for completion takes too long
completionTimer = setTimeout(() => {
if (!completed) {
telemetryService.captureTerminalHang(TerminalHangStage.WAITING_FOR_COMPLETION)
completionTimer = null
}
}, COMPLETION_TIMEOUT_MS)
process.once("completed", async () => {
completed = true
// Clear the completion timer
if (completionTimer) {
clearTimeout(completionTimer)
completionTimer = null
}
// Flush any remaining buffered output
if (!didContinue && outputBuffer.length > 0) {
if (chunkTimer) {
@@ -1570,6 +1203,12 @@ export class Task {
await process
// Clear timer if process completes normally
if (completionTimer) {
clearTimeout(completionTimer)
completionTimer = null
}
// Wait for a short delay to ensure all messages are sent to the webview
// This delay allows time for non-awaited promises to be created and
// for their associated messages to be sent to the webview, maintaining
@@ -1581,7 +1220,7 @@ export class Task {
if (userFeedback) {
await this.say("user_feedback", userFeedback.text, userFeedback.images, userFeedback.files)
await this.saveCheckpoint()
await this.checkpointManager?.saveCheckpoint()
let fileContentString = ""
if (userFeedback.files && userFeedback.files.length > 0) {
@@ -1634,6 +1273,14 @@ export class Task {
return { model, providerId, customPrompt }
}
private getApiRequestIdSafe(): string | undefined {
const apiLike = this.api as Partial<{
getLastRequestId: () => string | undefined
lastGenerationId?: string
}>
return apiLike.getLastRequestId?.() ?? apiLike.lastGenerationId
}
private async handleContextWindowExceededError(): Promise<void> {
const apiConversationHistory = this.messageStateHandler.getApiConversationHistory()
@@ -1973,6 +1620,9 @@ export class Task {
: "Cline uses complex prompts and iterative task execution that may be challenging for less capable models. For best results, it's recommended to use Claude 4 Sonnet for its advanced agentic coding capabilities.",
)
if (response === "messageResponse") {
// Display the user's message in the chat UI
await this.say("user_feedback", text, images, files)
// This userContent is for the *next* API call.
const feedbackUserContent: UserContent = []
feedbackUserContent.push({
@@ -2063,63 +1713,34 @@ export class Task {
}),
)
// Initialize checkpoint tracker first if enabled and it's the first request
// Initialize checkpointManager first if enabled and it's the first request
if (
isFirstRequest &&
this.enableCheckpoints &&
!this.checkpointTracker &&
!this.taskState.checkpointTrackerErrorMessage
this.checkpointManager && // TODO REVIEW: may be able to implement a replacement for the 15s timer
!this.taskState.checkpointManagerErrorMessage
) {
try {
// Warning Timer - If checkpoints take a while to to initialize, show a warning message
let checkpointsWarningTimer: NodeJS.Timeout | null = null
let checkpointsWarningShown = false
checkpointsWarningTimer = setTimeout(async () => {
if (!checkpointsWarningShown) {
checkpointsWarningShown = true
this.taskState.checkpointTrackerErrorMessage =
"Checkpoints are taking longer than expected to initialize. Working in a large repository? Consider re-opening Cline in a project that uses git, or disabling checkpoints."
await this.postStateToWebview()
}
}, 7_000)
// Timeout - If checkpoints take too long to initialize, warn user and disable checkpoints for the task
this.checkpointTracker = await pTimeout(
CheckpointTracker.create(
this.taskId,
this.controller.context.globalStorageUri.fsPath,
this.enableCheckpoints,
),
{
milliseconds: 15_000,
message:
"Checkpoints taking too long to initialize. Consider re-opening Cline in a project that uses git, or disabling checkpoints.",
},
)
if (checkpointsWarningTimer) {
clearTimeout(checkpointsWarningTimer)
checkpointsWarningTimer = null
}
await pTimeout(this.checkpointManager.checkpointTrackerCheckAndInit(), {
milliseconds: 15_000,
message:
"Checkpoints taking too long to initialize. Consider re-opening Cline in a project that uses git, or disabling checkpoints.",
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
// If the error was a timeout, we disabled all checkpoint operations for the rest of the task
if (errorMessage.includes("Checkpoints taking too long to initialize")) {
this.taskState.checkpointTrackerErrorMessage =
"Checkpoints initialization timed out. Consider re-opening Cline in a project that uses git, or disabling checkpoints."
await this.postStateToWebview()
} else {
this.taskState.checkpointTrackerErrorMessage = errorMessage // will be displayed right away since we saveClineMessages next which posts state to webview
}
console.error("Failed to initialize checkpoint manager:", errorMessage)
this.taskState.checkpointManagerErrorMessage = errorMessage // will be displayed right away since we saveClineMessages next which posts state to webview
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `Checkpoint initialization timed out: ${errorMessage}`,
})
}
}
// Now, if it's the first request AND checkpoints are enabled AND tracker was successfully initialized,
// then say "checkpoint_created" and perform the commit.
if (isFirstRequest && this.enableCheckpoints && this.checkpointTracker) {
const commitHash = await this.checkpointTracker.commit() // Actual commit
if (isFirstRequest && this.enableCheckpoints && this.checkpointManager) {
const commitHash = await this.checkpointManager.commit() // Actual commit
await this.say("checkpoint_created") // Now this is conditional
const lastCheckpointMessageIndex = findLastIndex(
this.messageStateHandler.getClineMessages(),
@@ -2136,11 +1757,11 @@ export class Task {
} else if (
isFirstRequest &&
this.enableCheckpoints &&
!this.checkpointTracker &&
this.taskState.checkpointTrackerErrorMessage
!this.checkpointManager &&
this.taskState.checkpointManagerErrorMessage
) {
// Checkpoints are enabled, but tracker failed to initialize.
// checkpointTrackerErrorMessage is already set and will be part of the state.
// checkpointManagerErrorMessage is already set and will be part of the state.
// No explicit UI message here, error message will be in ExtensionState.
}
@@ -2534,10 +2155,29 @@ export class Task {
didEndLoop = recDidEndLoop
} else {
// if there's no assistant_responses, that means we got no text or tool_use content blocks from API which we should assume is an error
await this.say(
"error",
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output.",
)
const { model, providerId } = this.getCurrentProviderInfo()
const reqId = this.getApiRequestIdSafe()
// Minimal diagnostics: structured log and telemetry
console.error("[EmptyAssistantMessage]", {
ulid: this.ulid,
providerId,
modelId: model.id,
requestId: reqId,
})
telemetryService.captureProviderApiError({
ulid: this.ulid,
model: model.id,
provider: providerId,
errorMessage: "empty_assistant_message",
requestId: reqId,
})
const baseErrorMessage =
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output."
const errorText = reqId ? `${baseErrorMessage} (reqId: ${reqId})` : baseErrorMessage
await this.say("error", errorText)
await this.messageStateHandler.addToApiConversationHistory({
role: "assistant",
content: [
+4 -4
View File
@@ -19,7 +19,7 @@ interface MessageStateHandlerParams {
taskIsFavorited?: boolean
updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>
taskState: TaskState
checkpointTrackerErrorMessage?: string
checkpointManagerErrorMessage?: string
}
export class MessageStateHandler {
@@ -27,7 +27,7 @@ export class MessageStateHandler {
private clineMessages: ClineMessage[] = []
private taskIsFavorited: boolean
private checkpointTracker: CheckpointTracker | undefined
private checkpointTrackerErrorMessage: string | undefined
private checkpointManagerErrorMessage: string | undefined
private updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>
private context: vscode.ExtensionContext
private taskId: string
@@ -41,7 +41,7 @@ export class MessageStateHandler {
this.taskState = params.taskState
this.taskIsFavorited = params.taskIsFavorited ?? false
this.updateTaskHistory = params.updateTaskHistory
this.checkpointTrackerErrorMessage = this.taskState.checkpointTrackerErrorMessage
this.checkpointManagerErrorMessage = this.taskState.checkpointManagerErrorMessage
}
setCheckpointTracker(tracker: CheckpointTracker | undefined) {
@@ -103,7 +103,7 @@ export class MessageStateHandler {
cwdOnTaskInitialization: cwd,
conversationHistoryDeletedRange: this.taskState.conversationHistoryDeletedRange,
isFavorited: this.taskIsFavorited,
checkpointTrackerErrorMessage: this.taskState.checkpointTrackerErrorMessage,
checkpointManagerErrorMessage: this.taskState.checkpointManagerErrorMessage,
})
} catch (error) {
console.error("Failed to save cline messages:", error)
@@ -1,10 +1,11 @@
import type { ToolUse } from "@core/assistant-message"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../index"
import type { TaskConfig } from "./types/TaskConfig"
import type { StronglyTypedUIHelpers } from "./types/UIHelpers"
export interface IToolHandler {
readonly name: string
readonly name: ClineDefaultTool
execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse>
getDescription(block: ToolUse): string
}
@@ -23,7 +24,7 @@ export interface IFullyManagedTool extends IToolHandler, IPartialBlockHandler {
*/
export class SharedToolHandler implements IFullyManagedTool {
constructor(
public readonly name: string,
public readonly name: ClineDefaultTool,
private baseHandler: IFullyManagedTool,
) {}
+22 -19
View File
@@ -1,7 +1,7 @@
import { ToolUseName } from "@core/assistant-message"
import { resolveWorkspacePath } from "@core/workspace"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import * as path from "path"
import { getCwd, getDesktopDir } from "@/utils/path"
import { ClineDefaultTool } from "@shared/tools"
import { getCwd, getDesktopDir, isLocatedInPath } from "@/utils/path"
export class AutoApprove {
autoApprovalSettings: AutoApprovalSettings
@@ -12,35 +12,35 @@ export class AutoApprove {
// Check if the tool should be auto-approved based on the settings
// Returns bool for most tools, and tuple for tools with nested settings
shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
shouldAutoApproveTool(toolName: ClineDefaultTool): boolean | [boolean, boolean] {
if (this.autoApprovalSettings.enabled) {
switch (toolName) {
case "read_file":
case "list_files":
case "list_code_definition_names":
case "search_files":
case ClineDefaultTool.FILE_READ:
case ClineDefaultTool.LIST_FILES:
case ClineDefaultTool.LIST_CODE_DEF:
case ClineDefaultTool.SEARCH:
return [
this.autoApprovalSettings.actions.readFiles,
this.autoApprovalSettings.actions.readFilesExternally ?? false,
]
case "new_rule":
case "write_to_file":
case "replace_in_file":
case ClineDefaultTool.NEW_RULE:
case ClineDefaultTool.FILE_NEW:
case ClineDefaultTool.FILE_EDIT:
return [
this.autoApprovalSettings.actions.editFiles,
this.autoApprovalSettings.actions.editFilesExternally ?? false,
]
case "execute_command":
case ClineDefaultTool.BASH:
return [
this.autoApprovalSettings.actions.executeSafeCommands ?? false,
this.autoApprovalSettings.actions.executeAllCommands ?? false,
]
case "browser_action":
case ClineDefaultTool.BROWSER:
return this.autoApprovalSettings.actions.useBrowser
case "web_fetch":
case ClineDefaultTool.WEB_FETCH:
return this.autoApprovalSettings.actions.useBrowser
case "access_mcp_resource":
case "use_mcp_tool":
case ClineDefaultTool.MCP_ACCESS:
case ClineDefaultTool.MCP_USE:
return this.autoApprovalSettings.actions.useMcp
}
}
@@ -50,12 +50,15 @@ export class AutoApprove {
// Check if the tool should be auto-approved based on the settings
// and the path of the action. Returns true if the tool should be auto-approved
// based on the user's settings and the path of the action.
async shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): Promise<boolean> {
async shouldAutoApproveToolWithPath(
blockname: ClineDefaultTool,
autoApproveActionpath: string | undefined,
): Promise<boolean> {
let isLocalRead: boolean = false
if (autoApproveActionpath) {
const cwd = await getCwd(getDesktopDir())
const absolutePath = path.resolve(cwd, autoApproveActionpath)
isLocalRead = absolutePath.startsWith(cwd)
const absolutePath = resolveWorkspacePath(cwd, autoApproveActionpath, "AutoApprove.shouldAutoApproveToolWithPath")
isLocalRead = isLocatedInPath(cwd, absolutePath)
} else {
// If we do not get a path for some reason, default to a (safer) false return
isLocalRead = false
@@ -2,6 +2,7 @@ import type { ToolUse } from "@core/assistant-message"
import { formatResponse } from "@core/prompts/responses"
import { ClineAsk, ClineAskUseMcpServer } from "@shared/ExtensionMessage"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
@@ -10,9 +11,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
export class AccessMcpResourceHandler implements IFullyManagedTool {
readonly name = "access_mcp_resource"
constructor() {}
readonly name = ClineDefaultTool.MCP_ACCESS
getDescription(block: ToolUse): string {
return `[${block.name} for '${block.params.server_name}']`
@@ -23,7 +22,7 @@ export class AccessMcpResourceHandler implements IFullyManagedTool {
const uri = block.params.uri
const partialMessage = JSON.stringify({
type: "access_mcp_resource",
type: this.name,
serverName: uiHelpers.removeClosingTag(block, "server_name", server_name),
toolName: undefined,
uri: uiHelpers.removeClosingTag(block, "uri", uri),
@@ -49,12 +48,12 @@ export class AccessMcpResourceHandler implements IFullyManagedTool {
// Validate required parameters
if (!server_name) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError("access_mcp_resource", "server_name")
return await config.callbacks.sayAndCreateMissingParamError(ClineDefaultTool.MCP_ACCESS, "server_name")
}
if (!uri) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError("access_mcp_resource", "uri")
return await config.callbacks.sayAndCreateMissingParamError(ClineDefaultTool.MCP_ACCESS, "uri")
}
config.taskState.consecutiveMistakeCount = 0
@@ -2,8 +2,9 @@ import { processFilesIntoText } from "@integrations/misc/extract-text"
import { showSystemNotification } from "@integrations/notifications"
import { findLast, parsePartialArrayString } from "@shared/array"
import { ClineAsk, ClineAskQuestion } from "@shared/ExtensionMessage"
import { ClineDefaultTool } from "@shared/tools"
import { telemetryService } from "@/services/telemetry"
import { ToolUse, ToolUseName } from "../../../assistant-message"
import { ToolUse } from "../../../assistant-message"
import { formatResponse } from "../../../prompts/responses"
import { ToolResponse } from "../.."
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
@@ -11,8 +12,7 @@ import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class AskFollowupQuestionToolHandler implements IToolHandler, IPartialBlockHandler {
name = "ask_followup_question"
supportedTools: ToolUseName[] = ["ask_followup_question"]
readonly name = ClineDefaultTool.ASK
getDescription(block: ToolUse): string {
return `[${block.name} for '${block.params.question}']`
@@ -36,7 +36,7 @@ export class AskFollowupQuestionToolHandler implements IToolHandler, IPartialBlo
// Validate required parameter
if (!question) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError("ask_followup_question", "question")
return await config.callbacks.sayAndCreateMissingParamError(this.name, "question")
}
config.taskState.consecutiveMistakeCount = 0
@@ -6,6 +6,7 @@ import { showSystemNotification } from "@integrations/notifications"
import { findLastIndex } from "@shared/array"
import { COMPLETION_RESULT_CHANGES_FLAG } from "@shared/ExtensionMessage"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
import type { TaskConfig } from "../types/TaskConfig"
@@ -13,9 +14,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
export class AttemptCompletionHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "attempt_completion"
constructor() {}
readonly name = ClineDefaultTool.ATTEMPT
getDescription(block: ToolUse): string {
return `[${block.name}]`
@@ -48,7 +47,7 @@ export class AttemptCompletionHandler implements IToolHandler, IPartialBlockHand
// Validate required parameters
if (!result) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError("attempt_completion", "result")
return await config.callbacks.sayAndCreateMissingParamError(this.name, "result")
}
config.taskState.consecutiveMistakeCount = 0
@@ -96,6 +95,11 @@ export class AttemptCompletionHandler implements IToolHandler, IPartialBlockHand
await config.callbacks.saveCheckpoint(true)
}
// Attempt completion is a special tool where we want to update the focus chain list before the user provides response
if (!block.partial && config.focusChainSettings.enabled) {
await config.callbacks.updateFCListFromToolResponse(block.params.task_progress)
}
// complete command message - need to ask for approval
const didApprove = await ToolResultUtils.askApprovalAndPushFeedback("command", command, config)
if (!didApprove) {
@@ -122,6 +126,11 @@ export class AttemptCompletionHandler implements IToolHandler, IPartialBlockHand
if (config.messageState.getClineMessages().at(-1)?.ask === "command_output") {
await config.callbacks.say("command_output", "")
}
if (!block.partial && config.focusChainSettings.enabled) {
await config.callbacks.updateFCListFromToolResponse(block.params.task_progress)
}
const { response, text, images, files: completionFiles } = await config.callbacks.ask("completion_result", "", false)
if (response === "yesButtonClicked") {
return "" // signals to recursive loop to stop (for now this never happens since yesButtonClicked will trigger a new task)
@@ -1,4 +1,5 @@
import { BrowserAction, BrowserActionResult, browserActions, ClineSayBrowserAction } from "@shared/ExtensionMessage"
import { ClineDefaultTool } from "@/shared/tools"
import { ToolUse } from "../../../assistant-message"
import { formatResponse } from "../../../prompts/responses"
import { ToolResponse } from "../.."
@@ -9,7 +10,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
export class BrowserToolHandler implements IFullyManagedTool {
readonly name = "browser_action"
readonly name = ClineDefaultTool.BROWSER
getDescription(block: ToolUse): string {
return `[${block.name} for '${block.params.action}']`
@@ -45,7 +46,7 @@ export class BrowserToolHandler implements IFullyManagedTool {
}
} else {
await uiHelpers.say(
"browser_action",
this.name,
JSON.stringify({
action: action as BrowserAction,
coordinate: uiHelpers.removeClosingTag(block, "coordinate", coordinate),
@@ -68,7 +69,7 @@ export class BrowserToolHandler implements IFullyManagedTool {
if (!action || !browserActions.includes(action)) {
// if the block is complete and we don't have a valid action this is a mistake
config.taskState.consecutiveMistakeCount++
const errorResult = await config.callbacks.sayAndCreateMissingParamError("browser_action", "action")
const errorResult = await config.callbacks.sayAndCreateMissingParamError(this.name, "action")
await config.services.browserSession.closeBrowser()
return errorResult
}
@@ -80,7 +81,7 @@ export class BrowserToolHandler implements IFullyManagedTool {
if (action === "launch") {
if (!url) {
config.taskState.consecutiveMistakeCount++
const errorResult = await config.callbacks.sayAndCreateMissingParamError("browser_action", "url")
const errorResult = await config.callbacks.sayAndCreateMissingParamError(this.name, "url")
await config.services.browserSession.closeBrowser()
return errorResult
}
@@ -119,7 +120,7 @@ export class BrowserToolHandler implements IFullyManagedTool {
if (action === "click") {
if (!coordinate) {
config.taskState.consecutiveMistakeCount++
const errorResult = await config.callbacks.sayAndCreateMissingParamError("browser_action", "coordinate")
const errorResult = await config.callbacks.sayAndCreateMissingParamError(this.name, "coordinate")
await config.services.browserSession.closeBrowser()
return errorResult
}
@@ -127,7 +128,7 @@ export class BrowserToolHandler implements IFullyManagedTool {
if (action === "type") {
if (!text) {
config.taskState.consecutiveMistakeCount++
const errorResult = await config.callbacks.sayAndCreateMissingParamError("browser_action", "text")
const errorResult = await config.callbacks.sayAndCreateMissingParamError(this.name, "text")
await config.services.browserSession.closeBrowser()
return errorResult
}
@@ -136,7 +137,7 @@ export class BrowserToolHandler implements IFullyManagedTool {
// Send browser action message
await config.callbacks.say(
"browser_action",
this.name,
JSON.stringify({
action: action as BrowserAction,
coordinate,
@@ -4,13 +4,14 @@ import { ensureTaskDirectoryExists } from "@core/storage/disk"
import { processFilesIntoText } from "@integrations/misc/extract-text"
import { showSystemNotification } from "@integrations/notifications"
import { ClineAsk } from "@shared/ExtensionMessage"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class CondenseHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "condense"
readonly name = ClineDefaultTool.CONDENSE
constructor() {}
@@ -5,6 +5,7 @@ import { COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
import { ClineAsk } from "@shared/ExtensionMessage"
import { fixModelHtmlEscaping } from "@utils/string"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
@@ -14,7 +15,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
export class ExecuteCommandToolHandler implements IFullyManagedTool {
readonly name = "execute_command"
readonly name = ClineDefaultTool.BASH
constructor(_validator: ToolValidator) {}
@@ -26,7 +27,7 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
const command = block.params.command
// Check if this should be auto-approved to determine UI flow
const shouldAutoApprove = uiHelpers.shouldAutoApproveTool("execute_command")
const shouldAutoApprove = uiHelpers.shouldAutoApproveTool(this.name)
if (shouldAutoApprove) {
// For auto-approved commands, we can't partially stream a say prematurely
@@ -48,12 +49,12 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
// Validate required parameters
if (!command) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError("execute_command", "command")
return await config.callbacks.sayAndCreateMissingParamError(this.name, "command")
}
if (!requiresApprovalRaw) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError("execute_command", "requires_approval")
return await config.callbacks.sayAndCreateMissingParamError(this.name, "requires_approval")
}
config.taskState.consecutiveMistakeCount = 0
@@ -1,9 +1,10 @@
import type { ToolUse } from "@core/assistant-message"
import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
import { parseSourceCodeForDefinitionsTopLevel } from "@services/tree-sitter"
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
import * as path from "path"
import { formatResponse } from "@/core/prompts/responses"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
@@ -13,7 +14,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
readonly name = "list_code_definition_names"
readonly name = ClineDefaultTool.LIST_CODE_DEF
constructor(private validator: ToolValidator) {}
@@ -53,11 +54,11 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
const pathValidation = this.validator.assertRequiredParams(block, "path")
if (!pathValidation.ok) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError("list_code_definition_names", "path")
return await config.callbacks.sayAndCreateMissingParamError(this.name, "path")
}
config.taskState.consecutiveMistakeCount = 0
const absolutePath = path.resolve(config.cwd, relDirPath!)
const absolutePath = resolveWorkspacePath(config.cwd, relDirPath!, "ListCodeDefinitionNamesToolHandler.execute")
// Execute the actual parse source code operation
const result = await parseSourceCodeForDefinitionsTopLevel(absolutePath, config.services.clineIgnoreController)
@@ -81,7 +82,7 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
} else {
// Manual approval flow
const notificationMessage = `Cline wants to analyze code definitions in ${path.basename(absolutePath)}`
const notificationMessage = `Cline wants to analyze code definitions in ${getWorkspaceBasename(absolutePath, "ListCodeDefinitionNamesToolHandler.notification")}`
// Show notification
showNotificationForApprovalIfAutoApprovalEnabled(
@@ -1,9 +1,10 @@
import type { ToolUse } from "@core/assistant-message"
import { formatResponse } from "@core/prompts/responses"
import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
import { listFiles } from "@services/glob/list-files"
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
import * as path from "path"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
@@ -13,7 +14,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
export class ListFilesToolHandler implements IFullyManagedTool {
readonly name = "list_files"
readonly name = ClineDefaultTool.LIST_FILES
constructor(private validator: ToolValidator) {}
@@ -58,11 +59,11 @@ export class ListFilesToolHandler implements IFullyManagedTool {
const pathValidation = this.validator.assertRequiredParams(block, "path")
if (!pathValidation.ok) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError("list_files", "path")
return await config.callbacks.sayAndCreateMissingParamError(this.name, "path")
}
config.taskState.consecutiveMistakeCount = 0
const absolutePath = path.resolve(config.cwd, relDirPath!)
const absolutePath = resolveWorkspacePath(config.cwd, relDirPath!, "ListFilesToolHandler.execute")
// Execute the actual list files operation
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
@@ -89,7 +90,7 @@ export class ListFilesToolHandler implements IFullyManagedTool {
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
} else {
// Manual approval flow
const notificationMessage = `Cline wants to view directory ${path.basename(absolutePath)}/`
const notificationMessage = `Cline wants to view directory ${getWorkspaceBasename(absolutePath, "ListFilesToolHandler.notification")}/`
// Show notification
showNotificationForApprovalIfAutoApprovalEnabled(
@@ -1,12 +1,13 @@
import type { ToolUse } from "@core/assistant-message"
import { loadMcpDocumentation } from "@core/prompts/loadMcpDocumentation"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class LoadMcpDocumentationHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "load_mcp_documentation"
readonly name = ClineDefaultTool.MCP_DOCS
constructor() {}
@@ -16,12 +17,12 @@ export class LoadMcpDocumentationHandler implements IToolHandler, IPartialBlockH
async handlePartialBlock(_block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
// Show loading message for partial blocks (though this tool probably won't have partials)
await uiHelpers.say("load_mcp_documentation", "", undefined, undefined, true)
await uiHelpers.say(this.name, "", undefined, undefined, true)
}
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
async execute(config: TaskConfig, _block: ToolUse): Promise<ToolResponse> {
// Show loading message at start of execution (self-managed now)
await config.callbacks.say("load_mcp_documentation", "", undefined, undefined, false)
await config.callbacks.say(this.name, "", undefined, undefined, false)
config.taskState.consecutiveMistakeCount = 0
@@ -2,14 +2,14 @@ import type { ToolUse } from "@core/assistant-message"
import { formatResponse } from "@core/prompts/responses"
import { processFilesIntoText } from "@integrations/misc/extract-text"
import { showSystemNotification } from "@integrations/notifications"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class NewTaskHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "new_task"
readonly name = ClineDefaultTool.NEW_TASK
constructor() {}
getDescription(block: ToolUse): string {
@@ -21,7 +21,7 @@ export class NewTaskHandler implements IToolHandler, IPartialBlockHandler {
*/
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
const context = uiHelpers.removeClosingTag(block, "context", block.params.context)
await uiHelpers.ask("new_task", context, true).catch(() => {})
await uiHelpers.ask(this.name, context, true).catch(() => {})
}
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
@@ -44,7 +44,7 @@ export class NewTaskHandler implements IToolHandler, IPartialBlockHandler {
}
// Ask user for response
const { text, images, files: newTaskFiles } = await config.callbacks.ask("new_task", context, false)
const { text, images, files: newTaskFiles } = await config.callbacks.ask(this.name, context, false)
// If the user provided a response, treat it as feedback
if (text || (images && images.length > 0) || (newTaskFiles && newTaskFiles.length > 0)) {
@@ -3,13 +3,14 @@ import { formatResponse } from "@core/prompts/responses"
import { findLast, parsePartialArrayString } from "@shared/array"
import { telemetryService } from "@/services/telemetry"
import { ClinePlanModeResponse } from "@/shared/ExtensionMessage"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class PlanModeRespondHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "plan_mode_respond"
readonly name = ClineDefaultTool.PLAN_MODE
constructor() {}
@@ -29,7 +30,7 @@ export class PlanModeRespondHandler implements IToolHandler, IPartialBlockHandle
options: parsePartialArrayString(uiHelpers.removeClosingTag(block, "options", optionsRaw)),
} satisfies ClinePlanModeResponse
await uiHelpers.ask("plan_mode_respond", JSON.stringify(sharedMessage), true).catch(() => {})
await uiHelpers.ask(this.name, JSON.stringify(sharedMessage), true).catch(() => {})
}
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
@@ -68,7 +69,7 @@ export class PlanModeRespondHandler implements IToolHandler, IPartialBlockHandle
text,
images,
files: planResponseFiles,
} = await config.callbacks.ask("plan_mode_respond", JSON.stringify(sharedMessage), false)
} = await config.callbacks.ask(this.name, JSON.stringify(sharedMessage), false)
config.taskState.isAwaitingPlanResponse = false
@@ -82,7 +83,7 @@ export class PlanModeRespondHandler implements IToolHandler, IPartialBlockHandle
telemetryService.captureOptionSelected(config.ulid, options.length, "plan")
// Valid option selected, don't show user message in UI
// Update last plan message with selected option
const lastPlanMessage = findLast(config.messageState.getClineMessages(), (m: any) => m.ask === "plan_mode_respond")
const lastPlanMessage = findLast(config.messageState.getClineMessages(), (m: any) => m.ask === this.name)
if (lastPlanMessage) {
lastPlanMessage.text = JSON.stringify({
...sharedMessage,
@@ -1,10 +1,11 @@
import type { ToolUse } from "@core/assistant-message"
import { formatResponse } from "@core/prompts/responses"
import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
import { extractFileContent } from "@integrations/misc/extract-file-content"
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
import * as path from "path"
import { telemetryService } from "@/services/telemetry"
import { ClineSayTool } from "@/shared/ExtensionMessage"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
@@ -14,7 +15,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
export class ReadFileToolHandler implements IFullyManagedTool {
readonly name = "read_file"
readonly name = ClineDefaultTool.FILE_READ
constructor(private validator: ToolValidator) {}
@@ -54,7 +55,7 @@ export class ReadFileToolHandler implements IFullyManagedTool {
const pathValidation = this.validator.assertRequiredParams(block, "path")
if (!pathValidation.ok) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError("read_file", "path")
return await config.callbacks.sayAndCreateMissingParamError(this.name, "path")
}
// Check clineignore access
@@ -65,7 +66,7 @@ export class ReadFileToolHandler implements IFullyManagedTool {
}
config.taskState.consecutiveMistakeCount = 0
const absolutePath = path.resolve(config.cwd, relPath!)
const absolutePath = resolveWorkspacePath(config.cwd, relPath!, "ReadFileToolHandler.execute")
// Handle approval flow
const sharedMessageProps = {
@@ -87,7 +88,7 @@ export class ReadFileToolHandler implements IFullyManagedTool {
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
} else {
// Manual approval flow
const notificationMessage = `Cline wants to read ${path.basename(absolutePath)}`
const notificationMessage = `Cline wants to read ${getWorkspaceBasename(absolutePath, "ReadFileToolHandler.notification")}`
// Show notification
showNotificationForApprovalIfAutoApprovalEnabled(
@@ -2,17 +2,17 @@ import type { ToolUse } from "@core/assistant-message"
import { formatResponse } from "@core/prompts/responses"
import { processFilesIntoText } from "@integrations/misc/extract-text"
import { showSystemNotification } from "@integrations/notifications"
import { ClineAsk } from "@shared/ExtensionMessage"
import { createAndOpenGitHubIssue } from "@utils/github-url-utils"
import * as os from "os"
import * as vscode from "vscode"
import { HostProvider } from "@/hosts/host-provider"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class ReportBugHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "report_bug"
readonly name = ClineDefaultTool.REPORT_BUG
constructor() {}
@@ -29,7 +29,7 @@ export class ReportBugHandler implements IToolHandler, IPartialBlockHandler {
additional_context: uiHelpers.removeClosingTag(block, "additional_context", block.params.additional_context),
})
await uiHelpers.ask("report_bug" as ClineAsk, partialMessage, block.partial).catch(() => {})
await uiHelpers.ask(this.name, partialMessage, block.partial).catch(() => {})
}
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
@@ -73,9 +73,10 @@ export class ReportBugHandler implements IToolHandler, IPartialBlockHandler {
// Derive system information values algorithmically
const operatingSystem = os.platform() + " " + os.release()
const clineVersion = vscode.extensions.getExtension("saoudrizwan.claude-dev")?.packageJSON.version || "Unknown"
const systemInfo = `VSCode: ${vscode.version}, Node.js: ${process.version}, Architecture: ${os.arch()}`
const currentMode = config.mode
const clineVersion = config.context.extension.packageJSON.version
const host = await HostProvider.env.getHostVersion({})
const systemInfo = `${host.platform}: ${host.version}, Node.js: ${process.version}, Architecture: ${os.arch()}`
const apiConfig = config.services.stateManager.getApiConfiguration()
const apiProvider = currentMode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider
const providerAndModel = `${apiProvider} / ${config.api.getModel().id}`
@@ -94,7 +95,7 @@ export class ReportBugHandler implements IToolHandler, IPartialBlockHandler {
cline_version: clineVersion,
})
const { text, images, files: reportBugFiles } = await config.callbacks.ask("report_bug", bugReportData, false)
const { text, images, files: reportBugFiles } = await config.callbacks.ask(this.name, bugReportData, false)
// If the user provided a response, treat it as feedback
if (text || (images && images.length > 0) || (reportBugFiles && reportBugFiles.length > 0)) {
@@ -1,10 +1,11 @@
import type { ToolUse } from "@core/assistant-message"
import { regexSearchFiles } from "@services/ripgrep"
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
import * as path from "path"
import { formatResponse } from "@/core/prompts/responses"
import { resolveWorkspacePath } from "@/core/workspace/WorkspaceResolver"
import { telemetryService } from "@/services/telemetry"
import { ClineSayTool } from "@/shared/ExtensionMessage"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
@@ -14,7 +15,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
export class SearchFilesToolHandler implements IFullyManagedTool {
readonly name = "search_files"
readonly name = ClineDefaultTool.SEARCH
constructor(private validator: ToolValidator) {}
@@ -63,16 +64,17 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
const pathValidation = this.validator.assertRequiredParams(block, "path")
if (!pathValidation.ok) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError("search_files", "path")
return await config.callbacks.sayAndCreateMissingParamError(this.name, "path")
}
if (!regex) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError("search_files", "regex")
return await config.callbacks.sayAndCreateMissingParamError(this.name, "regex")
}
config.taskState.consecutiveMistakeCount = 0
const absolutePath = path.resolve(config.cwd, relDirPath!)
const absolutePath = resolveWorkspacePath(config.cwd, relDirPath!, "SearchFilesTool.execute")
// Execute the actual regex search operation
const results = await regexSearchFiles(
config.cwd,
@@ -4,13 +4,14 @@ import { formatResponse } from "@core/prompts/responses"
import { ensureTaskDirectoryExists } from "@core/storage/disk"
import { ClineSayTool } from "@shared/ExtensionMessage"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class SummarizeTaskHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "summarize_task"
readonly name = ClineDefaultTool.SUMMARIZE_TASK
constructor() {}
@@ -25,7 +26,7 @@ export class SummarizeTaskHandler implements IToolHandler, IPartialBlockHandler
// Validate required parameters
if (!context) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError("summarize_task", "context")
return await config.callbacks.sayAndCreateMissingParamError(this.name, "context")
}
config.taskState.consecutiveMistakeCount = 0
@@ -2,6 +2,7 @@ import type { ToolUse } from "@core/assistant-message"
import { formatResponse } from "@core/prompts/responses"
import { ClineAsk, ClineAskUseMcpServer } from "@shared/ExtensionMessage"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
@@ -10,9 +11,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
export class UseMcpToolHandler implements IFullyManagedTool {
readonly name = "use_mcp_tool"
constructor() {}
readonly name = ClineDefaultTool.MCP_USE
getDescription(block: ToolUse): string {
return `[${block.name} for '${block.params.server_name}']`
@@ -1,7 +1,8 @@
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
import { ClineAsk, ClineSayTool } from "@shared/ExtensionMessage"
import { ClineDefaultTool } from "@shared/tools"
import { telemetryService } from "@/services/telemetry"
import { ToolUse, ToolUseName } from "../../../assistant-message"
import { ToolUse } from "../../../assistant-message"
import { formatResponse } from "../../../prompts/responses"
import { ToolResponse } from "../.."
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
@@ -11,8 +12,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
export class WebFetchToolHandler implements IFullyManagedTool {
name = "web_fetch"
supportedTools: ToolUseName[] = ["web_fetch"]
readonly name = ClineDefaultTool.WEB_FETCH
getDescription(block: ToolUse): string {
return `[${block.name} for '${block.params.url}']`
@@ -42,7 +42,7 @@ export class WebFetchToolHandler implements IFullyManagedTool {
// Validate required parameter
if (!url) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError("web_fetch", "url")
return await config.callbacks.sayAndCreateMissingParamError(this.name, "url")
}
config.taskState.consecutiveMistakeCount = 0
@@ -55,7 +55,7 @@ export class WebFetchToolHandler implements IFullyManagedTool {
}
const completeMessage = JSON.stringify(sharedMessageProps)
if (config.callbacks.shouldAutoApproveTool("web_fetch" as ToolUseName)) {
if (config.callbacks.shouldAutoApproveTool(this.name)) {
// Auto-approve flow
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
@@ -2,13 +2,14 @@ import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import type { ToolUse } from "@core/assistant-message"
import { constructNewFileContent } from "@core/assistant-message/diff"
import { formatResponse } from "@core/prompts/responses"
import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
import { processFilesIntoText } from "@integrations/misc/extract-text"
import { ClineSayTool } from "@shared/ExtensionMessage"
import { fileExistsAtPath } from "@utils/fs"
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
import * as path from "path"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
@@ -19,21 +20,12 @@ import { ToolDisplayUtils } from "../utils/ToolDisplayUtils"
import { ToolResultUtils } from "../utils/ToolResultUtils"
export class WriteToFileToolHandler implements IFullyManagedTool {
readonly name = "write_to_file" // This handler supports write_to_file, replace_in_file, and new_rule
readonly name = ClineDefaultTool.FILE_NEW // This handler supports write_to_file, replace_in_file, and new_rule
constructor(private validator: ToolValidator) {}
getDescription(block: ToolUse): string {
switch (block.name) {
case "write_to_file":
return `[${block.name} for '${block.params.path}']`
case "replace_in_file":
return `[${block.name} for '${block.params.path}']`
case "new_rule":
return `[${block.name} for '${block.params.path}']`
default:
return `[${block.name} for '${block.params.path}']`
}
return `[${block.name} for '${block.params.path}']`
}
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
@@ -49,12 +41,13 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
const config = uiHelpers.getConfig()
try {
const result = await this.validateAndPrepareFileOperation(config, block, rawRelPath, rawDiff, rawContent)
if (!result) {
return
}
// Creates file if it doesn't exist, and opens editor to stream content in. We don't want to handle this in the try/catch below since the error handler for it resets the diff view, which wouldn't be open if this failed.
const result = await this.validateAndPrepareFileOperation(config, block, rawRelPath, rawDiff, rawContent)
if (!result) {
return
}
try {
const { relPath, fileExists, diff, content, newContent } = result
// Create and show partial UI message
@@ -176,7 +169,7 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
await setTimeoutPromise(3_500)
} else {
// Manual approval flow with detailed feedback handling
const notificationMessage = `Cline wants to ${fileExists ? "edit" : "create"} ${path.basename(relPath)}`
const notificationMessage = `Cline wants to ${fileExists ? "edit" : "create"} ${getWorkspaceBasename(relPath, "WriteToFile.notification")}`
// Show notification
showNotificationForApprovalIfAutoApprovalEnabled(
@@ -331,7 +324,11 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
if (config.services.diffViewProvider.editType !== undefined) {
fileExists = config.services.diffViewProvider.editType === "modify"
} else {
const absolutePath = path.resolve(config.cwd, relPath)
const absolutePath = resolveWorkspacePath(
config.cwd,
relPath,
"WriteToFileToolHandler.validateAndPrepareFileOperation",
)
fileExists = await fileExistsAtPath(absolutePath)
config.services.diffViewProvider.editType = fileExists ? "modify" : "create"
}
+4 -4
View File
@@ -10,9 +10,9 @@ import type { BrowserSettings } from "@shared/BrowserSettings"
import type { ClineAsk, ClineSay } from "@shared/ExtensionMessage"
import type { FocusChainSettings } from "@shared/FocusChainSettings"
import type { Mode } from "@shared/storage/types"
import type { ClineDefaultTool } from "@shared/tools"
import type { ClineAskResponse } from "@shared/WebviewMessage"
import * as vscode from "vscode"
import type { ToolUseName } from "../../../assistant-message"
import type { ContextManager } from "../../../context/context-management/ContextManager"
import type { StateManager } from "../../../storage/StateManager"
import type { MessageStateHandler } from "../../message-state"
@@ -87,7 +87,7 @@ export interface TaskCallbacks {
saveCheckpoint: (isAttemptCompletionMessage?: boolean, completionMessageTs?: number) => Promise<void>
sayAndCreateMissingParamError: (toolName: ToolUseName, paramName: string, relPath?: string) => Promise<any>
sayAndCreateMissingParamError: (toolName: ClineDefaultTool, paramName: string, relPath?: string) => Promise<any>
removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>
@@ -97,8 +97,8 @@ export interface TaskCallbacks {
updateFCListFromToolResponse: (taskProgress: string | undefined) => Promise<void>
shouldAutoApproveTool: (toolName: ToolUseName) => boolean | [boolean, boolean]
shouldAutoApproveToolWithPath: (toolName: ToolUseName, path?: string) => Promise<boolean>
shouldAutoApproveTool: (toolName: ClineDefaultTool) => boolean | [boolean, boolean]
shouldAutoApproveToolWithPath: (toolName: ClineDefaultTool, path?: string) => Promise<boolean>
// Additional callbacks for task management
postStateToWebview: () => Promise<void>
+7 -6
View File
@@ -1,7 +1,8 @@
import type { ClineAsk, ClineSay } from "@shared/ExtensionMessage"
import type { ClineDefaultTool } from "@shared/tools"
import type { ClineAskResponse } from "@shared/WebviewMessage"
import { telemetryService } from "@/services/telemetry"
import type { ToolParamName, ToolUse, ToolUseName } from "../../../assistant-message"
import type { ToolParamName, ToolUse } from "../../../assistant-message"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
import { removeClosingTag } from "../utils/ToolConstants"
import type { TaskConfig } from "./TaskConfig"
@@ -29,12 +30,12 @@ export interface StronglyTypedUIHelpers {
removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>
// Approval methods
shouldAutoApproveTool: (toolName: ToolUseName) => boolean | [boolean, boolean]
shouldAutoApproveToolWithPath: (toolName: ToolUseName, path?: string) => Promise<boolean>
shouldAutoApproveTool: (toolName: ClineDefaultTool) => boolean | [boolean, boolean]
shouldAutoApproveToolWithPath: (toolName: ClineDefaultTool, path?: string) => Promise<boolean>
askApproval: (messageType: ClineAsk, message: string) => Promise<boolean>
// Telemetry and notifications
captureTelemetry: (toolName: ToolUseName, autoApproved: boolean, approved: boolean) => void
captureTelemetry: (toolName: ClineDefaultTool, autoApproved: boolean, approved: boolean) => void
showNotificationIfEnabled: (message: string) => void
// Config access - returns the proper typed config
@@ -50,13 +51,13 @@ export function createUIHelpers(config: TaskConfig): StronglyTypedUIHelpers {
ask: config.callbacks.ask,
removeClosingTag: (block: ToolUse, tag: ToolParamName, text?: string) => removeClosingTag(block, tag, text),
removeLastPartialMessageIfExistsWithType: config.callbacks.removeLastPartialMessageIfExistsWithType,
shouldAutoApproveTool: (toolName: ToolUseName) => config.autoApprover.shouldAutoApproveTool(toolName),
shouldAutoApproveTool: (toolName: ClineDefaultTool) => config.autoApprover.shouldAutoApproveTool(toolName),
shouldAutoApproveToolWithPath: config.callbacks.shouldAutoApproveToolWithPath,
askApproval: async (messageType: ClineAsk, message: string): Promise<boolean> => {
const { response } = await config.callbacks.ask(messageType, message, false)
return response === "yesButtonClicked"
},
captureTelemetry: (toolName: ToolUseName, autoApproved: boolean, approved: boolean) => {
captureTelemetry: (toolName: ClineDefaultTool, autoApproved: boolean, approved: boolean) => {
telemetryService.captureToolUsage(config.ulid, toolName, config.api.getModel().id, autoApproved, approved)
},
showNotificationIfEnabled: (message: string) => {
+167
View File
@@ -0,0 +1,167 @@
/**
* Handles formatting and presentation of workspace migration reports
*
* Separated from WorkspaceResolver to follow Single Responsibility Principle.
* This class focuses purely on report generation and formatting.
*/
/**
* Tracks path resolution usage for migration planning
*/
export interface UsageStats {
count: number
examples: string[]
lastUsed: Date
}
/**
* Configuration options for report generation
*/
interface ReportOptions {
includeExamples?: boolean
includeHighUsage?: boolean
highUsageThreshold?: number
sortByUsage?: boolean
}
/**
* Handles generation and formatting of migration reports
*/
export class MigrationReporter {
private readonly defaultOptions: Required<ReportOptions> = {
includeExamples: true,
includeHighUsage: true,
highUsageThreshold: 100,
sortByUsage: true,
}
/**
* Generate a comprehensive migration report from usage statistics
* @param usageMap - Map of component names to their usage statistics
* @param traceEnabled - Whether tracing is currently enabled
* @param options - Optional configuration for report generation
* @returns Formatted migration report string
*/
generateReport(usageMap: Map<string, UsageStats>, traceEnabled: boolean, options: ReportOptions = {}): string {
const config = { ...this.defaultOptions, ...options }
const entries = this.prepareEntries(usageMap, config.sortByUsage)
let report = this.generateHeader(entries.length, traceEnabled)
report += this.generateComponentDetails(entries, config)
report += this.generateSummary(entries)
if (config.includeHighUsage) {
report += this.generateHighUsageSection(entries, config.highUsageThreshold)
}
return report
}
/**
* Generate a simplified summary report
* @param usageMap - Map of component names to their usage statistics
* @returns Brief summary string
*/
generateSummary(entries: Array<[string, UsageStats]>): string {
const totalCalls = entries.reduce((sum, [_, stats]) => sum + stats.count, 0)
let summary = `\n=== Summary ===\n`
summary += `Total path resolution calls: ${totalCalls}\n`
return summary
}
/**
* Prepare and optionally sort the usage entries
*/
private prepareEntries(usageMap: Map<string, UsageStats>, sortByUsage: boolean): Array<[string, UsageStats]> {
const entries = Array.from(usageMap.entries())
if (sortByUsage) {
return entries.sort((a, b) => b[1].count - a[1].count)
}
return entries
}
/**
* Generate the report header section
*/
private generateHeader(componentCount: number, traceEnabled: boolean): string {
let header = "=== Multi-Root Migration Report ===\n"
header += `Total components using single-root: ${componentCount}\n`
header += `Trace enabled: ${traceEnabled}\n\n`
return header
}
/**
* Generate detailed component usage information
*/
private generateComponentDetails(entries: Array<[string, UsageStats]>, config: Required<ReportOptions>): string {
let details = ""
entries.forEach(([context, stats]) => {
details += `${context}:\n`
details += ` Calls: ${stats.count}\n`
details += ` Last used: ${stats.lastUsed.toISOString()}\n`
if (config.includeExamples && stats.examples.length > 0) {
details += ` Example paths:\n`
stats.examples.forEach((ex) => {
details += ` - "${ex}"\n`
})
}
details += "\n"
})
return details
}
/**
* Generate high-usage components section
*/
private generateHighUsageSection(entries: Array<[string, UsageStats]>, threshold: number): string {
const highUsageComponents = entries
.filter(([_, stats]) => stats.count > threshold)
.map(([context, stats]) => ({ context, count: stats.count }))
if (highUsageComponents.length === 0) {
return ""
}
let section = `\n=== High-Usage Components ===\n`
section += `(Operations with >${threshold} calls)\n`
highUsageComponents.forEach((h) => {
section += ` - ${h.context}: ${h.count} calls\n`
})
return section
}
/**
* Generate a JSON representation of the usage data
* @param usageMap - Map of component names to their usage statistics
* @returns JSON string representation
*/
generateJsonReport(usageMap: Map<string, UsageStats>): string {
const data = Object.fromEntries(usageMap)
return JSON.stringify(data, null, 2)
}
/**
* Generate a CSV representation of the usage data
* @param usageMap - Map of component names to their usage statistics
* @returns CSV string representation
*/
generateCsvReport(usageMap: Map<string, UsageStats>): string {
const entries = Array.from(usageMap.entries())
let csv = "Component,Calls,LastUsed,ExamplePaths\n"
entries.forEach(([context, stats]) => {
const examples = stats.examples.join("; ")
csv += `"${context}",${stats.count},"${stats.lastUsed.toISOString()}","${examples}"\n`
})
return csv
}
}
+275
View File
@@ -0,0 +1,275 @@
/**
* Workspace path resolution with migration tracing for multi-workspace support
*
* Phase 0: Acts as a tracer to identify all single-root path operations
* Phase 1+: Will handle multi-root path resolution
*/
import { Logger } from "@services/logging/Logger"
import * as path from "path"
import { MigrationReporter, type UsageStats } from "./MigrationReporter"
import { WorkspaceRoot } from "./WorkspaceRoot"
/**
* Maximum number of example paths to store per component for debugging purposes.
* This limit prevents excessive memory usage while providing enough examples
* to understand usage patterns during migration analysis.
*/
const MAX_EXAMPLE_PATHS = 5
export class WorkspaceResolver {
private usageMap = new Map<string, UsageStats>()
private traceEnabled = process.env.MULTI_ROOT_TRACE === "true" || process.env.NODE_ENV === "development"
/**
* Track usage statistics for a given context and path
* @param context - Component/handler name for tracking usage
* @param examplePath - The path to track as an example
*/
private trackUsage(context: string, examplePath: string): void {
const stats = this.usageMap.get(context) || {
count: 0,
examples: [],
lastUsed: new Date(),
}
stats.count++
stats.lastUsed = new Date()
// Keep up to MAX_EXAMPLE_PATHS example paths for debugging
if (stats.examples.length < MAX_EXAMPLE_PATHS && !stats.examples.includes(examplePath)) {
stats.examples.push(examplePath)
}
this.usageMap.set(context, stats)
}
/**
* Phase 0: Traces single-root path resolution for migration planning
* Phase 1+: Will resolve path against multiple workspace roots
*
* @param cwdOrRoots - Current working directory (Phase 0) or array of workspace roots (Phase 1+)
* @param relativePath - The relative path to resolve
* @param context - Component/handler name for tracking usage
* @returns Absolute path (Phase 0) or object with path and root (Phase 1+)
*/
resolveWorkspacePath(
cwdOrRoots: string | WorkspaceRoot[],
relativePath: string,
context?: string,
): string | { absolutePath: string; root: WorkspaceRoot } {
// Phase 0: Single-root tracer mode
if (typeof cwdOrRoots === "string") {
return this.resolveSingleRootPath(cwdOrRoots, relativePath, context)
}
// Phase 1+: Multi-root resolution
return this.resolveMultiRootPath(cwdOrRoots, relativePath)
}
/**
* Resolves a path against a single workspace root (Phase 0)
*
* @param cwd - Current working directory
* @param relativePath - The relative path to resolve
* @param context - Component/handler name for tracking usage
* @returns Absolute path
*/
private resolveSingleRootPath(cwd: string, relativePath: string, context?: string): string {
// Track usage for migration planning
if (context) {
this.trackUsage(context, relativePath)
if (this.traceEnabled) {
Logger.debug(`[MULTI-ROOT-TRACE] ${context}: resolving "${relativePath}" against "${cwd}"`)
}
}
return path.resolve(cwd, relativePath)
}
/**
* Resolves a path against multiple workspace roots (Phase 1+)
*
* @param workspaceRoots - Array of workspace roots
* @param relativePath - The relative path to resolve
* @returns Object with absolute path and matching root
*/
private resolveMultiRootPath(
workspaceRoots: WorkspaceRoot[],
relativePath: string,
): { absolutePath: string; root: WorkspaceRoot } {
// Handle absolute paths
if (path.isAbsolute(relativePath)) {
return this.resolveAbsolutePath(workspaceRoots, relativePath)
}
// Handle relative paths
return this.resolveRelativePath(workspaceRoots, relativePath)
}
/**
* Resolves an absolute path against workspace roots
*
* @param workspaceRoots - Array of workspace roots
* @param absolutePath - The absolute path to resolve
* @returns Object with absolute path and matching root
*/
private resolveAbsolutePath(
workspaceRoots: WorkspaceRoot[],
absolutePath: string,
): { absolutePath: string; root: WorkspaceRoot } {
const matchingRoot = workspaceRoots.find((root) => absolutePath.startsWith(root.path))
return {
absolutePath,
root: matchingRoot || workspaceRoots[0], // fallback to primary
}
}
/**
* Resolves a relative path against workspace roots
*
* @param workspaceRoots - Array of workspace roots
* @param relativePath - The relative path to resolve
* @returns Object with absolute path and matching root
*/
private resolveRelativePath(
workspaceRoots: WorkspaceRoot[],
relativePath: string,
): { absolutePath: string; root: WorkspaceRoot } {
// Check which roots have this relative path
const candidateRoots: WorkspaceRoot[] = []
for (const root of workspaceRoots) {
// const testPath = path.join(root.path, relativePath)
// In Phase 1, check if path exists
// For now, just add all roots as candidates
candidateRoots.push(root)
}
return this.selectBestRoot(workspaceRoots, candidateRoots, relativePath)
}
/**
* Selects the best root from candidate roots using disambiguation logic
*
* @param workspaceRoots - All available workspace roots
* @param candidateRoots - Candidate roots that could contain the path
* @param relativePath - The relative path being resolved
* @returns Object with absolute path and selected root
*/
private selectBestRoot(
workspaceRoots: WorkspaceRoot[],
candidateRoots: WorkspaceRoot[],
relativePath: string,
): { absolutePath: string; root: WorkspaceRoot } {
// Disambiguation logic (simplified for Phase 0)
if (candidateRoots.length === 0) {
// Path doesn't exist in any root, use primary
return {
absolutePath: path.resolve(workspaceRoots[0].path, relativePath),
root: workspaceRoots[0],
}
}
if (candidateRoots.length === 1) {
// Unambiguous
return {
absolutePath: path.resolve(candidateRoots[0].path, relativePath),
root: candidateRoots[0],
}
}
// Multiple matches - need disambiguation
// Phase 2: This will trigger UI picker
// For now, use primary root if it's a candidate, otherwise first match
const primaryRoot = workspaceRoots[0]
const selectedRoot = candidateRoots.find((r) => r.path === primaryRoot.path) || candidateRoots[0]
return {
absolutePath: path.resolve(selectedRoot.path, relativePath),
root: selectedRoot,
}
}
/**
* Get migration report showing all single-root usage patterns
* Currently this function is mainly called by the vscode debugger
*/
getMigrationReport(): string {
const reporter = new MigrationReporter()
return reporter.generateReport(this.usageMap, this.traceEnabled)
}
/**
* Get raw usage statistics for external analysis
* @returns Map of component names to their usage statistics
*/
getUsageStats(): Map<string, UsageStats> {
return new Map(this.usageMap)
}
/**
* Clear usage statistics (useful for testing)
*/
clearUsageStats(): void {
this.usageMap.clear()
}
/**
* Export usage data as JSON for analysis
*/
exportUsageData(): Record<string, UsageStats> {
return Object.fromEntries(this.usageMap)
}
/**
* Phase 0: Instance method for getting basename with tracking
* Phase 1+: Will handle basename for multi-workspace paths
*
* @param filePath - The file path to get basename from
* @param context - Component/handler name for tracking usage
* @returns The basename of the path
*/
getBasename(filePath: string, context?: string): string {
// Track usage for migration planning
if (!context?.length) {
return path.basename(filePath)
}
this.trackUsage(context, filePath)
// Phase 0: Just wrap existing behavior
const result = path.basename(filePath)
if (this.traceEnabled) {
Logger.debug(`[MULTI-ROOT-TRACE] ${context}: getting basename for "${filePath}"`)
}
return result
}
}
// Export singleton instance
export const workspaceResolver = new WorkspaceResolver()
/**
* Phase 0: Convenience function for easy migration from path.resolve()
* This is what we'll use to replace existing path.resolve(cwd, ...) calls
*/
export function resolveWorkspacePath(cwd: string, relativePath: string, context?: string): string {
return workspaceResolver.resolveWorkspacePath(cwd, relativePath, context) as string
}
/**
* Helper to check if we're in trace mode
*/
export function isWorkspaceTraceEnabled(): boolean {
return process.env.MULTI_ROOT_TRACE === "true" || process.env.NODE_ENV === "development"
}
/**
* Phase 0: Convenience function for path.basename with tracking
* This is what we'll use to replace existing path.basename() calls
*/
export function getWorkspaceBasename(filePath: string, context?: string): string {
return workspaceResolver.getBasename(filePath, context)
}
+32
View File
@@ -0,0 +1,32 @@
/**
* Workspace root types and interfaces for multi-workspace support
*/
export enum VcsType {
None = "none",
Git = "git",
Mercurial = "mercurial",
}
export interface WorkspaceRoot {
path: string // Absolute path to the workspace root
name?: string // Optional display name for the workspace (auto-derived from path if not provided)
vcs: VcsType // Version control system type for this root
commitHash?: string // Optional latest commit hash/changeset ID for VCS tracking
}
// Example usage:
// const workspaceRoots: WorkspaceRoot[] = [
// {
// path: "/Users/dev/frontend",
// name: "frontend",
// vcs: VcsType.Git,
// commitHash: "a1b2c3d4e5f6789"
// },
// {
// path: "/Users/dev/backend",
// name: "backend",
// vcs: VcsType.Git,
// commitHash: "f6e5d4c3b2a1987"
// }
// ]
+261
View File
@@ -0,0 +1,261 @@
/**
* WorkspaceRootManager - Central manager for multi-workspace operations
* This class handles workspace root resolution, path mapping, and workspace context
*/
import { execa } from "execa"
import * as path from "path"
import { getLatestGitCommitHash } from "../../utils/git"
import { VcsType, WorkspaceRoot } from "./WorkspaceRoot"
export interface WorkspaceContext {
workspaceRoots: WorkspaceRoot[]
primaryRoot: WorkspaceRoot
currentRoot?: WorkspaceRoot
}
export class WorkspaceRootManager {
private roots: WorkspaceRoot[] = []
private primaryIndex: number = 0
constructor(roots: WorkspaceRoot[] = [], primaryIndex: number = 0) {
this.roots = roots
this.primaryIndex = Math.min(primaryIndex, Math.max(0, roots.length - 1))
}
/**
* Initialize from a single cwd for backward compatibility
*/
static async fromLegacyCwd(cwd: string): Promise<WorkspaceRootManager> {
const vcs = await WorkspaceRootManager.detectVcs(cwd)
const gitHash = vcs === VcsType.Git ? await getLatestGitCommitHash(cwd) : null
const commitHash = gitHash === null ? undefined : gitHash
const root: WorkspaceRoot = {
path: cwd,
name: path.basename(cwd),
vcs,
commitHash,
}
return new WorkspaceRootManager([root], 0)
}
/**
* Detect version control system for a directory
*/
private static async detectVcs(dirPath: string): Promise<VcsType> {
try {
// Check for Git
await execa("git", ["rev-parse", "--git-dir"], { cwd: dirPath })
return VcsType.Git
} catch {
// Not a git repo
}
try {
// Check for Mercurial
await execa("hg", ["root"], { cwd: dirPath })
return VcsType.Mercurial
} catch {
// Not a mercurial repo
}
return VcsType.None
}
/**
* Add a new workspace root
*/
async addRoot(rootPath: string, name?: string): Promise<void> {
const vcs = await WorkspaceRootManager.detectVcs(rootPath)
const gitHash = vcs === VcsType.Git ? await getLatestGitCommitHash(rootPath) : null
const commitHash = gitHash === null ? undefined : gitHash
const root: WorkspaceRoot = {
path: rootPath,
name: name || path.basename(rootPath),
vcs,
commitHash,
}
this.roots.push(root)
}
/**
* Remove a workspace root by path
*/
removeRoot(path: string): boolean {
const index = this.roots.findIndex((r) => r.path === path)
if (index === -1) {
return false
}
this.roots.splice(index, 1)
// Adjust primary index if needed
if (this.primaryIndex >= this.roots.length) {
this.primaryIndex = Math.max(0, this.roots.length - 1)
}
return true
}
/**
* Get all workspace roots
*/
getRoots(): WorkspaceRoot[] {
return [...this.roots]
}
/**
* Get the primary workspace root
*/
getPrimaryRoot(): WorkspaceRoot | undefined {
return this.roots[this.primaryIndex]
}
/**
* Set the primary workspace root by index
*/
setPrimaryIndex(index: number): void {
if (index >= 0 && index < this.roots.length) {
this.primaryIndex = index
}
}
/**
* Find the workspace root that contains the given absolute path
*/
resolvePathToRoot(absolutePath: string): WorkspaceRoot | undefined {
// Sort roots by path length (longest first) to handle nested workspaces
const sortedRoots = [...this.roots].sort((a, b) => b.path.length - a.path.length)
for (const root of sortedRoots) {
if (absolutePath.startsWith(root.path)) {
return root
}
}
return undefined
}
/**
* Find workspace root by name
*/
getRootByName(name: string): WorkspaceRoot | undefined {
return this.roots.find((r) => r.name === name)
}
/**
* Get workspace root by index
*/
getRootByIndex(index: number): WorkspaceRoot | undefined {
return this.roots[index]
}
/**
* Check if a path is within any workspace root
*/
isPathInWorkspace(absolutePath: string): boolean {
return this.resolvePathToRoot(absolutePath) !== undefined
}
/**
* Get relative path from workspace root
*/
getRelativePathFromRoot(absolutePath: string, root?: WorkspaceRoot): string | undefined {
const targetRoot = root || this.resolvePathToRoot(absolutePath)
if (!targetRoot) {
return undefined
}
return path.relative(targetRoot.path, absolutePath)
}
/**
* Create workspace context for tool execution
*/
createContext(currentRoot?: WorkspaceRoot): WorkspaceContext {
return {
workspaceRoots: this.getRoots(),
primaryRoot: this.getPrimaryRoot()!,
currentRoot: currentRoot || this.getPrimaryRoot(),
}
}
/**
* Serialize for storage
*/
toJSON(): { roots: WorkspaceRoot[]; primaryIndex: number } {
return {
roots: this.roots,
primaryIndex: this.primaryIndex,
}
}
/**
* Deserialize from storage
*/
static fromJSON(data: { roots: WorkspaceRoot[]; primaryIndex: number }): WorkspaceRootManager {
return new WorkspaceRootManager(data.roots, data.primaryIndex)
}
/**
* Get a summary string for display
*/
getSummary(): string {
if (this.roots.length === 0) {
return "No workspace roots configured"
}
if (this.roots.length === 1) {
return `Single workspace: ${this.roots[0].name || this.roots[0].path}`
}
const primary = this.getPrimaryRoot()
return `Multi-workspace (${this.roots.length} roots)\nPrimary: ${primary?.name || primary?.path}\nAdditional: ${this.roots
.filter((_, i) => i !== this.primaryIndex)
.map((r) => r.name || path.basename(r.path))
.join(", ")}`
}
/**
* Check if this is a single-root workspace (for backward compatibility)
*/
isSingleRoot(): boolean {
return this.roots.length === 1
}
/**
* Get the single root if this is a single-root workspace
* Throws if multiple roots exist
*/
getSingleRoot(): WorkspaceRoot {
if (this.roots.length !== 1) {
throw new Error(`Expected single root, but found ${this.roots.length} roots`)
}
return this.roots[0]
}
/**
* Update commit hashes for all Git repositories
*/
async updateCommitHashes(): Promise<void> {
for (const root of this.roots) {
if (root.vcs === VcsType.Git) {
const gitHash = await getLatestGitCommitHash(root.path)
root.commitHash = gitHash === null ? undefined : gitHash
}
}
}
}
// Export for use in Task and Controller
export function createLegacyWorkspaceRoot(cwd: string): WorkspaceRoot {
return {
path: cwd,
name: path.basename(cwd),
vcs: VcsType.None, // Will be detected properly during initialization
}
}
@@ -0,0 +1,206 @@
/**
* Unit tests for WorkspaceResolver
* These tests ensure behavior preservation during refactoring
*/
import { expect } from "chai"
import { afterEach, beforeEach, describe, it } from "mocha"
import * as path from "path"
import * as sinon from "sinon"
import { Logger } from "../../../services/logging/Logger"
import { WorkspaceResolver } from "../WorkspaceResolver"
import { VcsType, WorkspaceRoot } from "../WorkspaceRoot"
describe("WorkspaceResolver", () => {
let resolver: WorkspaceResolver
let loggerStub: sinon.SinonStub
let originalEnv: string | undefined
beforeEach(() => {
resolver = new WorkspaceResolver()
loggerStub = sinon.stub(Logger, "debug")
originalEnv = process.env.MULTI_ROOT_TRACE
})
afterEach(() => {
loggerStub.restore()
process.env.MULTI_ROOT_TRACE = originalEnv
resolver.clearUsageStats()
})
describe("resolveWorkspacePath - Single Root Mode", () => {
const testCwd = "/test/workspace"
const testRelativePath = "src/file.ts"
const expectedAbsolutePath = path.resolve(testCwd, testRelativePath)
it("should resolve path without context", () => {
const result = resolver.resolveWorkspacePath(testCwd, testRelativePath)
expect(result).to.equal(expectedAbsolutePath)
})
it("should resolve path with context and track usage", () => {
const context = "TestComponent"
const result = resolver.resolveWorkspacePath(testCwd, testRelativePath, context)
expect(result).to.equal(expectedAbsolutePath)
// Verify usage tracking
const usageStats = resolver.getUsageStats()
expect(usageStats.has(context)).to.be.true
const stats = usageStats.get(context)!
expect(stats.count).to.equal(1)
expect(stats.examples).to.include(testRelativePath)
})
it("should track multiple calls to same context", () => {
const context = "TestComponent"
resolver.resolveWorkspacePath(testCwd, "file1.ts", context)
resolver.resolveWorkspacePath(testCwd, "file2.ts", context)
resolver.resolveWorkspacePath(testCwd, "file1.ts", context) // duplicate
const usageStats = resolver.getUsageStats()
const stats = usageStats.get(context)!
expect(stats.count).to.equal(3)
expect(stats.examples).to.have.length(2) // no duplicates in examples
expect(stats.examples).to.include("file1.ts")
expect(stats.examples).to.include("file2.ts")
})
it("should not log when tracing is disabled", () => {
process.env.MULTI_ROOT_TRACE = "false"
process.env.NODE_ENV = "production"
resolver.resolveWorkspacePath(testCwd, testRelativePath, "TestComponent")
expect(loggerStub.called).to.be.false
})
it("should handle absolute paths correctly", () => {
const absolutePath = "/absolute/path/file.ts"
const result = resolver.resolveWorkspacePath(testCwd, absolutePath)
expect(result).to.equal(path.resolve(testCwd, absolutePath))
})
it("should handle empty relative path", () => {
const result = resolver.resolveWorkspacePath(testCwd, "")
expect(result).to.equal(path.resolve(testCwd))
})
it("should handle relative paths with .. navigation", () => {
const relativePath = "../other/file.ts"
const result = resolver.resolveWorkspacePath(testCwd, relativePath)
expect(result).to.equal(path.resolve(testCwd, relativePath))
})
})
describe("resolveWorkspacePath - Multi Root Mode", () => {
const workspaceRoots: WorkspaceRoot[] = [
{ path: "/workspace/primary", name: "primary", vcs: VcsType.Git },
{ path: "/workspace/secondary", name: "secondary", vcs: VcsType.Git },
]
it("should handle absolute paths in multi-root mode", () => {
const absolutePath = "/workspace/primary/src/file.ts"
const result = resolver.resolveWorkspacePath(workspaceRoots, absolutePath)
expect(result).to.be.an("object")
expect((result as any).absolutePath).to.equal(absolutePath)
expect((result as any).root).to.equal(workspaceRoots[0])
})
it("should fallback to primary root for unmatched absolute paths", () => {
const absolutePath = "/other/path/file.ts"
const result = resolver.resolveWorkspacePath(workspaceRoots, absolutePath)
expect(result).to.be.an("object")
expect((result as any).absolutePath).to.equal(absolutePath)
expect((result as any).root).to.equal(workspaceRoots[0])
})
it("should resolve relative paths against primary root", () => {
const relativePath = "src/file.ts"
const result = resolver.resolveWorkspacePath(workspaceRoots, relativePath)
expect(result).to.be.an("object")
expect((result as any).absolutePath).to.equal(path.resolve(workspaceRoots[0].path, relativePath))
expect((result as any).root).to.equal(workspaceRoots[0])
})
it("should handle empty workspace roots array", () => {
// This should throw an error or handle gracefully
expect(() => {
resolver.resolveWorkspacePath([], "src/file.ts")
}).to.throw()
})
})
describe("getBasename", () => {
const testFilePath = "/path/to/file.ts"
const expectedBasename = "file.ts"
it("should return basename without context", () => {
const result = resolver.getBasename(testFilePath)
expect(result).to.equal(expectedBasename)
})
it("should return basename with context and track usage", () => {
const context = "TestComponent"
const result = resolver.getBasename(testFilePath, context)
expect(result).to.equal(expectedBasename)
// Verify usage tracking
const usageStats = resolver.getUsageStats()
expect(usageStats.has(context)).to.be.true
const stats = usageStats.get(context)!
expect(stats.count).to.equal(1)
expect(stats.examples).to.include(testFilePath)
})
})
describe("Usage Statistics Management", () => {
it("should track usage statistics correctly", () => {
resolver.resolveWorkspacePath("/test", "file1.ts", "Component1")
resolver.resolveWorkspacePath("/test", "file2.ts", "Component1")
resolver.getBasename("/test/file3.ts", "Component2")
const usageStats = resolver.getUsageStats()
expect(usageStats.size).to.equal(2)
const component1Stats = usageStats.get("Component1")!
expect(component1Stats.count).to.equal(2)
expect(component1Stats.examples).to.have.length(2)
const component2Stats = usageStats.get("Component2")!
expect(component2Stats.count).to.equal(1)
expect(component2Stats.examples).to.have.length(1)
})
})
describe("Edge Cases", () => {
it("should handle null/undefined inputs gracefully", () => {
// These should not throw
expect(() => resolver.resolveWorkspacePath("/test", "")).to.not.throw()
expect(() => resolver.getBasename("")).to.not.throw()
})
it("should handle special characters in paths", () => {
const specialPath = "src/file with spaces & symbols!.ts"
const result = resolver.resolveWorkspacePath("/test", specialPath, "Component")
expect(result).to.equal(path.resolve("/test", specialPath))
})
it("should handle very long paths", () => {
const longPath = "a/".repeat(100) + "file.ts"
const result = resolver.resolveWorkspacePath("/test", longPath, "Component")
expect(result).to.equal(path.resolve("/test", longPath))
})
})
})

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