Compare commits

...
Author SHA1 Message Date
pashpashpash 4e21a466ae sync cli cline core changes with main 2025-09-29 14:21:10 -07:00
Saoud Rizwan cda3eb8236 fix: task timeline showing text and reasoning items 2025-09-29 12:31:41 -07:00
John Costa 3c21d2be1f fix: returning undefined when URL is not valid (#6377)
* fix: returning undefined when URL is not valid

This can happen when the user is typing, and once it was set there was
no way of changing it.

* adding change set
2025-09-29 12:19:23 -07:00
Saoud Rizwan f3adf68775 v3.32.3 Release Notes 2025-09-29 11:50:11 -07:00
Saoud Rizwan 6bd3181133 Add Sonnet 4.5 to Bedrock; add model banner announcing Sonnet 4.5; modify bug report 2025-09-29 11:38:02 -07:00
dd3a234a69 v3.32.2 Release Notes (#6511)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-29 10:53:49 -07:00
Saoud Rizwan c496f8a90d Fix OpenRouter model id 2025-09-29 10:47:00 -07:00
Saoud Rizwan 25b1cf91fc Add fixed Sonnet 4.5 support 2025-09-29 10:40:15 -07:00
Saoud Rizwan b54e2043fe Revert "Add Claude Sonnet 4.5 (#6544)"
This reverts commit 98e5ccc547.
2025-09-29 10:26:31 -07:00
Saoud Rizwan 98e5ccc547 Add Claude Sonnet 4.5 (#6544) 2025-09-29 10:10:04 -07:00
Sarah Fortune 813a9589d0 Replace VSCode API with the host bridge. (#6529)
Add a method to the host bridge to open and focus the terminal panel.
2025-09-28 19:12:09 -07:00
Sarah Fortune 64eb66d49a Move getGlobalStorageDir out of the HostProvider into disk.ts (#6530) 2025-09-28 19:11:59 -07:00
Sarah Fortune 8c17d864c8 Replace VSCode API with Host Provider (#6527)
VSCode API doesn't work cross-platform, this should use the host provider instead.
2025-09-28 18:18:55 -07:00
Sarah Fortune 4f931c2d9d Handle quotes and other special chars in the curl request (#6525)
This workflow didn't work if the PR title contained quotes, e.g.
https://github.com/cline/cline/pull/6523 - Don't show VS Code LM provider on non-VSCode platforms
2025-09-28 17:50:46 -07:00
Sarah Fortune 5836db3093 [disk.ts] Replace uses of context.globalStorageUri with HostProvider.get().globalStorageFsPath (#6507)
* Fix ellipsis warning

# Conflicts:
#	src/services/test/TestServer.ts

* [disk.ts] Replace uses of context.globalStorageUri with HostProvider.get().globalStorageFsPath

This is part of removing dependencies on the VSCode API fom the codebase except for in platform specific code in src/hosts/vscode and src/extension.ts.

Remove unused vscode context param.

* Remove constructors that just call super()
2025-09-28 17:39:19 -07:00
Sarah Fortune 18879edf9f Don't include VS Code LM API for non-VSCode platforms (#6523) 2025-09-28 17:38:52 -07:00
celestial-vault 1cc702c8b9 remove getCurrentMode function in favor of state manager (#6418) 2025-09-28 15:34:52 -07:00
Saoud Rizwan c8caa6f9b9 fix: command execution not sending output to webview and incorrectly showing 'Proceed while Running' when finished (#6522) 2025-09-28 15:29:21 -07:00
Saoud Rizwan 01f61b6765 Make first checkpoint async to unblock UI 2025-09-28 12:20:47 -07:00
Saoud Rizwan b8dd6abe61 Add /task deep link handler (#6513)
* Add /task deep link handler

* Add deep link handler for /task
2025-09-28 02:23:09 -07:00
fb7f0e36fe v3.32.1 Release Notes (#6488)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-28 00:00:26 -07:00
Saoud Rizwan 403a32b69c Disable TaskFeedbackButtons 2025-09-27 23:18:22 -07:00
Saoud Rizwan ee6daed7bb Change task timeline items border radius 2025-09-27 23:13:30 -07:00
Saoud Rizwan 55ffe9e5dd Update task timeline colors 2025-09-27 23:07:16 -07:00
Saoud Rizwan c2ec5fd17d Update Settings design and 'About' section 2025-09-27 22:59:07 -07:00
Saoud Rizwan 5a84cb1145 Use SlidersHorizontal icon for API Configuration tab in Settings 2025-09-27 22:41:09 -07:00
Saoud Rizwan aec2fd5a7b fix: planActSeparateModelsSetting defaulting to true for new users after reload 2025-09-27 22:33:57 -07:00
Tomás Barreiro 5826cb584e fix: cline accounts using stale id token at refresh response (#6509) 2025-09-27 22:16:25 -07:00
Sarah Fortune 76232639c3 [Tasks directory] Replace uses of context.globalStorageUri with HostProvider.get().globalStorageFsPath (#6420)
* Replace uses of context.globalStorageUri with HostProvider.get().globalStorageFsPath for task directory

This is part of removing dependencies on the VSCode API fom the codebase except for in platform specific code in src/hosts/vscode and src/extension.ts.

Remove unused vscode context param.

* Fix test ModelContextTracker.test.ts

* Fix test FileContextTracker.test.ts
2025-09-27 17:47:27 -07:00
celestial-vault 3173483c0f remove task class enable checkpoints variable and directly use statemanager (#6491) 2025-09-27 16:38:23 -07:00
Sarah Fortune 8caf37600b Fix problem compiling integration tests (#6498)
Fixes the error below. The integraion tests are compiled to CommonJS and cannot import an .mjs directly.

```
> claude-dev@3.32.0 compile-tests
> node ./scripts/build-tests.js

node:child_process:957
    throw err;
    ^

Error: Command failed: tsc -p ./tsconfig.test.json --outDir out
    at genericNodeError (node:internal/errors:983:15)
    at wrappedFn (node:internal/errors:537:14)
    at checkExecSyncError (node:child_process:882:11)
    at execSync (node:child_process:954:15)
    at Object.<anonymous> (/Users/sjf/cline/scripts/build-tests.js:56:1)
    at Module._compile (node:internal/modules/cjs/loader:1734:14)
    at Object..js (node:internal/modules/cjs/loader:1899:10)
    at Module.load (node:internal/modules/cjs/loader:1469:32)
    at Function._load (node:internal/modules/cjs/loader:1286:12)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14) {
  status: 2,
  signal: null,
  output: [
    null,
    "src/core/task/index.ts(3,65): error TS7016: Could not find a declaration file for module '@anthropic-ai/sdk/resources/index.mjs'. '/Users/sjf/cline/node_modules/@anthropic-ai/sdk/resources/index.mjs' implicitly has an 'any' type.\n" +
      "  There are types at '/Users/sjf/cline/node_modules/@anthropic-ai/sdk/resources/index.d.ts', but this result could not be resolved under your current 'moduleResolution' setting. Consider updating to 'node16', 'nodenext', or 'bundler'.\n",
    ''
  ],
  pid: 22680,
  stdout: "src/core/task/index.ts(3,65): error TS7016: Could not find a declaration file for module '@anthropic-ai/sdk/resources/index.mjs'. '/Users/sjf/cline/node_modules/@anthropic-ai/sdk/resources/index.mjs' implicitly has an 'any' type.\n" +
    "  There are types at '/Users/sjf/cline/node_modules/@anthropic-ai/sdk/resources/index.d.ts', but this result could not be resolved under your current 'moduleResolution' setting. Consider updating to 'node16', 'nodenext', or 'bundler'.\n",
  stderr: ''
}

Node.js v23.11.0
```
2025-09-27 13:07:29 -07:00
Igor Tceglevskii 58b14c69b1 CLINE_ACTIVE usage (#6471) 2025-09-27 10:02:05 -07:00
Saoud RizwanandCopilot b831100f20 feat: preserve reasoning traces for cline/openrouter/anthropic providers to maintain conversation integrity (#6495)
* feat: preserve reasoning traces for cline/openrouter/anthropic providers to maintain conversation integrity

* Add reasoning trace preservation for providers

This change introduces a feature to preserve reasoning traces for specific providers, enhancing conversation integrity.

* Update src/core/api/transform/openai-format.ts

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-27 06:17:10 -07:00
Saoud Rizwan 23fa305eac Enable thinking by default for models that support it to reduce text verbosity in chat (#6493)
* Enable thinking by default for models that support it to reduce text verbosity in chat

* Refactor
2025-09-27 05:42:57 -07:00
Saoud Rizwan 0762e6406d fix: sending message during interactive command would show double checkpoints 2025-09-27 00:55:15 -07:00
Sarah Fortune 94da9f6669 Only show the info banner messages about the right sidebar in VSCode (#6492)
Add a `type` field to the PlatConfig with type of the IDE: VSCode, standalone, etc.
Make the info banner conditional on the type.
Quiet the gRPC logs on startup.
2025-09-26 19:37:58 -07:00
Saoud Rizwan fdeef9cece fix: uses homedir instead of hardcoded tilde for MCP path 2025-09-26 17:51:54 -07:00
lcs-bdr dd055b3327 fix: add retry logic to SAP AI Core provider (#6453)
* Use retry behavior for SAP AI Core provider

* add changeset
2025-09-26 12:47:08 -07:00
Igor Tceglevskii e852c6953b force no proxy for hostbridge connection (#6474) 2025-09-26 12:06:02 -07:00
github-actions[bot]andArafatkatze aef27eb1c4 v3.32.0 Release Notes (#6487)
* Added the new code-supernova-1-million stealth model, available for free and delivering a 1 million token context window
* Changes to inform Cline about commands that are available on your system

Updated version to 3.32.0 

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2025-09-26 10:32:37 -07:00
Ara 863572031f Empty PR to bump changeset (#6485) 2025-09-26 09:45:12 -07:00
65eee1ac6a add code-supernova-1m (#6458)
* add code-supernova-1m

* Update src/core/api/providers/cline.ts

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

* fixing cache price

* wording

* Update webview-ui/src/components/chat/Announcement.tsx

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

* Fix announcement content

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-09-26 08:19:39 -07:00
Bee 8213b0b910 Update E2E Test for Chat input (#6470)
* Update E2E Test for Chat input

* update

* typo
2025-09-26 10:00:32 +08:00
Bee f5fc3fed6f test: skip API request failure check to prevent Windows timeout (#6468)
Remove API request failure assertions that cause test timeouts on Windows due to longer API request failure times, while preserving other test functionality
2025-09-25 17:51:33 -07:00
celestial-vault 42df03177f feat: add HeroTooltip to conversation history button (#6466)
- Rename OpenDiskTaskHistoryButton to OpenDiskConversationHistoryButton
- Add HeroTooltip for consistent UI experience
- Update protobuf service and backend handler
2025-09-25 17:22:43 -07:00
Bee 648ae1b1fd feat(chat): add scroll to top functionality (#6423)
* feat(chat): add scroll to top functionality

- Add scroll to top button when action buttons are not visible
- Pass virtuosoRef to ActionButtons component for scroll control
- Enhance scroll button logic to handle both up and down directions

* Update aria-label
2025-09-25 17:10:25 -07:00
Saoud Rizwan d3cff6ac47 fix: search tool long regex string causing overflow in chatview 2025-09-24 23:38:25 -07:00
13af435103 v3.31.1 Release Notes (#6451)
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for version 3.31.1

Added details about installed CLI tools and renamed MCP tab.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-24 23:08:59 -07:00
Saoud Rizwan 3b1477bc24 Rename MCP tab 'Installed' to 'Configure' (#5966)
* Rename MCP tab 'Installed' to 'Configure'

* Fix component name

* Fix errors; update docs

* Create green-wasps-brush.md
2025-09-24 23:05:23 -07:00
Saoud Rizwan 688f93db6d Show ToS update for cline account users 2025-09-24 23:00:41 -07:00
Saoud Rizwan a702270e85 Fix import linter error 2025-09-24 21:57:30 -07:00
Saoud Rizwan 03acada1b9 Fix padding in checkpoints error 2025-09-24 21:56:43 -07:00
adam jonesandSaoud Rizwan 95cca05f5e Add CLI tools detection to environment details (#5471)
- Auto-detect available CLI tools in system PATH
- Add detected tools to environment context for AI models
- Include comprehensive list of common developer tools (gh, docker, aws, etc.)
- Cross-platform support using 'which' on Unix and 'where' on Windows

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-24 21:49:25 -07:00
Saoud Rizwan 33d77eb095 Copy changes 2025-09-24 21:16:25 -07:00
3e0c39acbb v3.31.0 Release Notes (#6379)
* changeset version bump

* Updating CHANGELOG.md format

* Update package.json

* Update changelog for version 3.31.0

Updated version to 3.31.0 and added new features and improvements.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-24 18:33:47 -07:00
Saoud Rizwan 366d8a5411 Fix failing integration test due to duplicate test id 2025-09-24 18:25:13 -07:00
Saoud Rizwan d22596e5c2 Update announcement banner content (#6448) 2025-09-24 18:06:25 -07:00
Saoud Rizwan 7cc612dbce Fix right sidebar banner and docs (#6446) 2025-09-24 18:02:44 -07:00
Araandfrostbournesb f0cab63a43 Fixing icons and positioning for UI for voice mode (#6445)
* Minor UX fixes for voice mode

* Minor UX fixes for voice mode

* Minor UX fixes for voice mode

* Fix alignment of stop icon

---------

Co-authored-by: frostbournesb <frostbournesb@protonmail.com>
2025-09-24 16:20:27 -07:00
Saoud Rizwan 3c1b670b73 fix: cline not knowing plan/act mode if compact mode is enabled (#6443) 2025-09-24 15:31:42 -07:00
Ara 5de05d68ef Restricting voice mode to mac os only (#6442) 2025-09-24 15:15:44 -07:00
Saoud Rizwan 9b6b1c376c Revert focus chain design changes (#6441)
This reverts commit e5e26f45fa.

Revert "Update colors and width of focus chain (#6437)"

This reverts commit b794583b7a.

Revert "Update task header and focus chain UI design (#6415)"

This reverts commit 011b19225e.

Fix

Fix
2025-09-24 14:50:30 -07:00
Saoud Rizwan e5e26f45fa Decrease focus chain height 2025-09-24 13:55:31 -07:00
Saoud Rizwan b794583b7a Update colors and width of focus chain (#6437) 2025-09-24 13:44:54 -07:00
celestial-vault ca96bd8f62 add infobanner (#6294)
* add infobanner

* fix type and update docs url

* restructure test to not include brittle elements; it is not clear why this banner was even added to auth tests

* move update info banner logic into rpc
2025-09-24 13:37:40 -07:00
Bee 0ecdf8d0cc update CompactTaskButton tooltip content and styling (#6424) 2025-09-24 13:10:44 -07:00
Jose Castelli 5be6ba68a3 Testing Platform - Support partial response validation via meta.expected (#6399)
Testing Platform - Support partial response validation via meta.expected
2025-09-24 20:03:32 +02:00
celestial-vaultandJuan Pablo 1bdd1e943f Add docs for opening cline in right sidebar (#6292)
* Add docs for opening cline in right sidebar

* use frame tags

* change to gifs

* docs(customization): refactor sidebar instructions with Steps component

Updated opening-cline-in-sidebar.mdx to replace numbered lists with structured <Steps> components for improved readability. Also updated image sources and added a link for Cursor alignment guidance.

---------

Co-authored-by: Juan Pablo <juan@cline.bot>
2025-09-24 09:22:02 -07:00
Saoud Rizwan c85a4abeb4 Fix bg color of API configuration in settings (#6422) 2025-09-24 02:13:29 -07:00
Saoud Rizwan 011b19225e Update task header and focus chain UI design (#6415)
* Fix chatfield buttons positioning

* Fix task header icons being cut off at bottom

* Fix copy for focus chain

* Fix focus chain text overflowing and being hidden

* Remove transition animation

* Fix token stats

* Tweak task header styles

* Fix focus chain progress bar direction

* Hide checkpoint text unless hovered

* Make copy button smaller and muted

* Use new focus chain design

* Make timeline blocks circles

* Move focus chain down and fix pencil icon positioning
2025-09-24 02:06:06 -07:00
Sarah Fortune a3945dce7f Replace vscode context.globalStoragePath with HostProvider.globalStorageFsPath (#6419) 2025-09-24 01:19:06 -07:00
celestial-vault 8daca03996 add a script to reconstruct taskHistory (#6403)
* add a script to reconstruct taskHistory

* remove format setting unrelated
2025-09-23 23:23:44 -07:00
Bee 777b8576f2 fix: update fontsource import path to resolve 401 errors (#6414)
* fix: update fontsource import path to resolve 401 errors

Replace specific weight imports with node_modules path reference to fix accessibility issues with @fontsource/azeret-mono font files in webview

* update path
2025-09-23 18:47:56 -07:00
Ara 880755ec89 feat: add multi-root workspace support for auto-approve file reads (#6412)
* feat: add multi-root workspace support for auto-approve file reads

Add logic to handle auto-approval of file read operations in multi-root workspace scenarios. When multi-root is enabled and multiple workspaces are present, the system now checks if a file is located in any workspace rather than just the current working directory. This ensures proper auto-approval behavior across all workspace roots while maintaining backward compatibility for single-root workspaces.

* Fixing pulsing border
2025-09-23 17:57:12 -07:00
Bee 1c70089521 initialize compact mode to prevent UI glitching on mount (#6413)
- Set isCompactMode initial state to true instead of false
- Replace CSS variable with Tailwind class for settings title
- Prevents layout shifts and UI glitching during component initialization
2025-09-23 17:05:48 -07:00
BeeandSaoud Rizwan fe2a8a9477 dev: disable auto-condense threshold configuration in task header (#6407)
Remove autoCondenseThreshold prop and hardcode useAutoCondense to false in TaskHeader component to temporarily disable the configurable auto-condense threshold from UI

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-23 16:41:57 -07:00
nihar-oracle eb2550ec3d fix(oca): add auth guard, unify axios config, and rotate default IDCS client id (#6410)
- refreshOcaModels:

  - Add explicit auth guard: show a user-facing error if no OCA access token is present and return a typed error via OcaCompatibleModelInfo.
  - Switch axios invocation to use getAxiosSettings() (fetch adapter) instead of proxy agents.
  - Replace getProxyAgents import with getAxiosSettings.

- OcaAuthProvider:

  - Migrate all axios calls (discovery + token POSTs) to use getAxiosSettings().
  - Add explicit error when no id_token is returned from OCA during the auth code exchange to improve diagnostics.

- utils/constants:
  - Rotate DEFAULT_IDCS_CLIENT_ID to a new value.

- utils/utils:

  - Create getAxiosSettings() helper (uses axios fetch adapter) and remove proxy agent–specific helpers.

  - Revise createOcaHeaders to avoid direct vscode + package.json coupling:

    - Use HostProvider.env.getHostVersion for host/IDE details.
    - Use ExtensionRegistryInfo.version for the extension version.
    - Set headers: client=Cline, client-version, client-ide, client-ide-version, opc-request-id.

  - Note: import of HttpsProxyAgent remains but is now unused; consider removing to avoid lint/TS warnings.

Rationale

- Reliability/UX: Users now receive a clear error when attempting to refresh OCA models without being authenticated.
- Portability: Replacing direct vscode and package.json usage in headers with HostProvider + ExtensionRegistryInfo reduces coupling and makes code host-agnostic.
- Network config simplification: Standardize axios setup through a single getAxiosSettings() helper and the fetch adapter.
- Auth robustness: Explicitly surface the absence of id_token to speed up troubleshooting OIDC flows.

Potential behavior changes

- Proxy handling: getProxyAgents() was removed in favor of the axios fetch adapter via getAxiosSettings(). If explicit HTTP(S)_PROXY env-based proxying is required, follow-up work may be needed to reintroduce agent support or configure fetch-compatible proxying.

Files touched

- src/core/controller/models/refreshOcaModels.ts
- src/services/auth/oca/providers/OcaAuthProvider.ts
- src/services/auth/oca/utils/constants.ts
- src/services/auth/oca/utils/utils.ts
2025-09-23 15:36:31 -07:00
Bee 9c3ac14cab feat: add multi-root workspace setting with feature flag support (#6409)
* feat: add multi-root workspace setting with feature flag support

Add user-configurable multi-root workspace setting that works in conjunction with feature flags. Includes proto definition, state management, and UI toggle in settings panel.

- Add multi_root_enabled field to UpdateSettingsRequest proto
- Implement multiRootSetting with user preference and feature flag state
- Add ClineFeatureSetting interface for feature flag + user setting pattern
- Create settings UI toggle with feature flag override indication
- Update state helpers to properly handle boolean conversion

* remove docs

* remove env vars
2025-09-23 15:34:50 -07:00
canvrnoandKevin Bond 6a8f900d75 REmove VCS requirement from multiroot checkpoints (#6405)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-23 13:09:46 -07:00
Bee 407e472322 fix: poll feature flags for all users (#6404)
* fix: poll feature flags for all users instead of just authenticated users

Move feature flags polling outside the authenticated user check to ensure all users (logged in and anonymous) receive up-to-date feature flags. Reset flags only for authenticated users to maintain proper user-specific configuration.

* add changeset
2025-09-23 12:59:55 -07:00
Saoud Rizwan 580b2e35e2 Update CODEOWNERS to remove dcbartlett (#6408) 2025-09-23 12:54:50 -07:00
Sarah Fortune 30c121509f Add a workflow that will trigger the Jetbrains tests for PRs in the cline repo (#6402)
Changes in the cline repo have the potential to cause breakages in the JetBrains repo. The creator of the PR might not found out until some time later that they have made a change that causes problems for JetBrains.

This workflow will trigger the workflow .github/workflows/test-changes-from-cline-repo.yml in the JetBrains repo, that runs the JetBrains integration tests. When the tests complete, the workflow will leave a comment on the PR with the results of the tests.
2025-09-23 10:13:38 -07:00
BeeandJose R. Perez b3aee3857c feat: new Task Header UI with configurable auto condense threshold (#6049)
* Update TaskHeader UI

- Create new FocusChainContainer component for todo list management
- Add onSendMessage prop to TaskHeader and TaskSection components
- Refactor TaskHeader to use new FocusChainContainer
- Update tooltip styling and Tailwind configuration
- Improve task progress visualization and interaction handling

* use tailwind

* Remove edit thread btn

* Add interactive auto-compact marker to context window bar

- Add state management for auto-compact marker position (default 75%)
- Make context window bar clickable to reposition marker
- Wrap entire bar in tooltip instead of just marker
- Add click handler to calculate percentage from mouse position
- Fix cost display conditional and remove redundant CSS property

* Prevent event bubbling in task header button clicks

Add preventDefault and stopPropagation to all task header button click handlers to prevent unintended parent element interactions. Also standardize styling by replacing inline styles with Tailwind classes and update text color class for consistency.

* Clean up

* Add CheckpointError component and clean up task header UI

- Add new CheckpointError component for error handling
- Remove commented code from FocusChainContainer
- Add rounded corners to focus chain container
- Refactor formatLargeNumber function with default parameter handling
- Clean up token display formatting in TaskHeader

* Add configurable auto-condense threshold for context window management

- Add autoCondenseThreshold setting to control when context window compaction occurs
- Update ContextManager to accept threshold percentage parameter (0-100%)
- Add ContextWindowDetails component to display context usage in task header
- Extend protobuf schema and state management for new threshold setting
- Default threshold set to 75% when auto-condense is enabled

* v2 style

* Dynamic marker & remove HeroTooltip

* Update styles

* feat: enhance task header UX with expanded progress bar hitbox smart boundary detection timeline block hover dimming effects optimized spacing and standard confirmation dialog styling

* Extract TaskHeader styles to CSS module

Move inline styles from TaskHeader component to external CSS module file for better maintainability and separation of concerns.

* migrate to tailwind

* migrate inline styes to tailwind for FocusChain

* fix(ui): fix useEffect cleanup and add key prop to InfoRow

- Return cleanup function from useEffect instead of nested setTimeout
- Add key prop to InfoRow component to ensure proper re-rendering
- Improves component lifecycle management and prevents memory leaks

* price tag and warning positioning

* clean up & persist expand state

* debounce

* add keyboard navigation for auto condense threshold slider

Add arrow key controls to adjust auto condense threshold with 5% steps (10% with Shift). Include focus management, accessibility attributes, and click-outside handling for improved UX.

* feat: small vertical positioning adjustment

* add changeset

* Update Task Action Button Text

* clean up

* Update storybook

* Apply styling feedback

* clean up

* feat: accordian style metadata for context window bar tooltip

* fix: notch slide fix

* fix: add cleanup for animation frames and timeouts in AutoCondenseMarker

Add proper cleanup functions to useEffect hooks to prevent memory leaks by canceling animation frames and clearing timeouts when component unmounts or dependencies change.

* Fix animation on mount

* remove handleBlur

* fix: change the order of the instructional text

* fix: remove the color from the tooltip percentage value

* clean up

* simplify

* useAutoCondense

* remove highlights

* set maxAllowedSize

* Auto Compact

* remove fork button

* feat: add configurable auto-condense threshold setting

Add auto_condense_threshold parameter to control when context window compaction occurs. The threshold is configurable as a percentage (0-1 range) of the total context window size, allowing users to customize when automatic condensing triggers instead of using a fixed maximum size.

Changes:
- Add autoCondenseThreshold field to protobuf UpdateSettingsRequest
- Update ApiProviderInfo interface to include autoCondenseThreshold
- Modify shouldCompactContextWindow to accept threshold percentage parameter
- Add threshold validation and state management in updateSettings
- Include autoCondenseThreshold in controller state and UI data flow

* autoCondenseThreshold

* fix package

---------

Co-authored-by: Jose R. Perez <trupix@gmail.com>
2025-09-23 09:21:07 -07:00
BeeandTomás Barreiro 10cbbb2c6b feat: Cline Auth Provider (#6131)
* Add ClineAuthProvider and integrate with AuthService

- Add new ClineAuthProvider class for Cline-specific authentication
- Update AuthService to support both Firebase and Cline auth providers
- Switch default provider from Firebase to Cline
- Add dynamic auth URL fetching for Cline provider
- Update type definitions to support multiple provider types

* Add API auth URL configuration and update Cline auth flow

- Add apiAuthUrl to environment configs for all environments
- Update ClineAuthProvider to use new token exchange API endpoint
- Add shared Cline API utilities and types
- Refactor auth service to handle access tokens with expiration
- Update mock auth service and test fixtures for new auth flow

* add changeset

* wip

* refactor authentication service and improve token handling

- Add null check for auth token in ClineAccountService
- Update authorization header format to use 'workos:' prefix
- Replace hardcoded API endpoints with CLINE_API_ENDPOINT constants
- Refactor ClineAuthInfo interface to use accessToken terminology
- Remove Firebase auth provider dependency
- Simplify auth callback handling and token storage
- Improve error handling for missing authentication tokens

* refactor(auth): standardize workos token prefix handling

Centralize workos: prefix application in AuthService.getAuthToken() method instead of duplicating across multiple API call sites. This ensures consistent authentication token formatting and simplifies maintenance by having a single source of truth for token prefixing.

* Remove unused code

* update mock responses

* Set Firebase Auth Provider as default

* Update src/shared/cline/api.ts

Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>

* remove docs

* refactor(auth): replace type union with IAuthProvider interface

- Replace AvailableAuthProvider type union with IAuthProvider interface for better extensibility
- Add ServiceConfig type for provider configuration
- Remove hardcoded providerName field in favor of provider.name property
- Update method signatures to use IAuthProvider interface
- Improve error messages and code comments for clarity
- Add TODO for mock auth provider implementation

---------

Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
2025-09-23 08:23:22 -07:00
Bee 08365b3e0b feat: add configurable auto-condense threshold setting (#6391)
* feat: add configurable auto-condense threshold setting

Add auto_condense_threshold parameter to control when context window compaction occurs. The threshold is configurable as a percentage (0-1 range) of the total context window size, allowing users to customize when automatic condensing triggers instead of using a fixed maximum size.

Changes:
- Add autoCondenseThreshold field to protobuf UpdateSettingsRequest
- Update ApiProviderInfo interface to include autoCondenseThreshold
- Modify shouldCompactContextWindow to accept threshold percentage parameter
- Add threshold validation and state management in updateSettings
- Include autoCondenseThreshold in controller state and UI data flow

* updateSettings
2025-09-23 06:48:12 -07:00
canvrnoandKevin Bond cb4c61b1ca Dependency changes (#6393)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-22 23:36:50 -07:00
canvrnoandKevin Bond 2d9ff863b7 Update dependencies (#6390)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-22 20:16:33 -07:00
Ara ae9b20a12b Adding Search tool for multi root workspace (#6288)
* Adding multi search

* Adding multi search

* Adding multi search

* Adding multi search

* feat: Cleanup

* feat: Cleanup

* feat: Cleanup

* feat: Cleanup

* Fixing search and workspace path stuff
2025-09-22 19:17:40 -07:00
Ara c16e271c14 Adding voice mode to Cline (#6208)
* Adding voice mode
2025-09-22 18:50:56 -07:00
Bee b940cef0e4 refactor: append stealth models on readOpenRouterModels & refreshOpenRouterModels (#6384)
* refactor: extract stealth models to reusable function

Move hardcoded stealth model addition from inline code to a dedicated `appendStealthModels` function. This improves code organization by centralizing stealth model management and ensures consistent application across both fresh API responses and cached model data.

* refactor(controller): integrate stealth models into OpenRouter handling

- Added import and integrated appendClineStealthModels in readOpenRouterModels
- Improved error handling with try-catch in readOpenRouterModels
- Refactored refreshOpenRouterModels to use controller method and renamed functions
- Renamed STEALTH_MODELS to CLINE_STEALTH_MODELS and made appendClineStealthModels exportable

* await cached models for immediate UI availability

Changed the initialization to synchronously await and post last cached OpenRouter models, improving UI responsiveness by making them available as soon as possible instead of relying on a promise chain.
2025-09-22 18:47:02 -07:00
tjandy98 bc228de20e Update SAP AI Core Provider Anthropic input token calculation (#6363)
* Update anthropic input token usage calculation

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>

* changeset

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>

---------

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>
2025-09-22 18:41:21 -07:00
Ara a9cac3206a feat: Fixing dictation settings (#6385) 2025-09-22 17:38:31 -07:00
Jose Castelli a9bc4c7d67 Testing platform coverage (#6332)
Testing platform coverage
2025-09-22 21:14:54 +02:00
celestial-vault 737dce09d1 Remove duplicate settings references in Task class (#6336)
* read settings from statemanager instead of keeping stale references in Task class and child classes

* removed unused vars
2025-09-22 12:02:17 -07:00
ZeroAurora a10d778bee fix: remove temperature settings in z.ai models (#6311)
To use the default temperature. (#6223 comment)
2025-09-22 12:00:13 -07:00
Jose Castelli 58b0ea9afa Run Testing platform within Test workflow [shadow] (#6273)
Run Testing platform within Test workflow [shadow]
2025-09-22 20:15:22 +02:00
canvrnoandKevin Bond 90fa3d7336 Dependency updates (#6096)
* Dependency updates

* package-lock

* Updated packages

---------

Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-22 10:39:11 -07:00
canvrnoandKevin Bond d0da22d5a9 Modify checkpoints to accept an array of workspaces (#6320)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-22 10:06:02 -07:00
nihar-oracle f2ce0b46a3 fix: Fixing oca provider utils (#6359) 2025-09-20 22:14:54 -07:00
52ab767e44 v3.30.3 Release Notes (#6352)
* changeset version bump

* Updating CHANGELOG.md format

* Update version from 3.31.0 to 3.30.3

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-19 17:57:08 -07:00
Ara d858fb9360 Empty PR to bump changeset (#6351)
* Empty PR to bump changeset

* feat: Cleanup
2025-09-19 17:49:33 -07:00
nihar-oracle 428cdb670a feat(oca): add OCA provider with model picker and auth integration (#6339)
- Add webview UI components:

  - webview-ui/src/components/settings/providers/OcaProvider.tsx
  - webview-ui/src/components/settings/providers/OcaModelPicker.tsx

- Update settings to surface OCA:
  - webview-ui/src/components/settings/ApiOptions.tsx

- Wire backend for OCA auth and controller:

  - src/services/auth/oca/OcaAuthService.ts
  - src/services/auth/oca/providers/OcaAuthProvider.ts
  - src/core/controller/index.ts
2025-09-19 17:27:45 -07:00
Toshii aab002fe65 adding yolo mode telemetry which is triggered in grpc call updated (#6348) 2025-09-19 16:45:52 -07:00
Saoud Rizwan cc540d8158 Remove comment on cacheWritesPrice in clineCodeSupernovaModelInfo 2025-09-19 15:05:22 -07:00
Saoud Rizwan adc15c79d7 Fix copy 2025-09-19 14:57:54 -07:00
Saoud Rizwan af05d3497a v3.30.2 Release Notes 2025-09-19 14:51:19 -07:00
Saoud Rizwan 23fa1cb481 Fix Announcement banner UI tests 2025-09-19 14:50:33 -07:00
800967d851 v3.30.1 Release Notes (#6343)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-19 14:25:10 -07:00
Saoud Rizwan 91deede3c3 fix: model list not being updated in time for user to use shortcut button to update model to stealth model (#6347)
* fix: model list not being updated in time for user to use shortcut button to update model to stealth model

* Create tame-rabbits-travel.md
2025-09-19 14:21:32 -07:00
pashpashpash b11e6171ff YOLO MODE (#6340)
* add yolo mode setting

* more explicit warning

* changeset
2025-09-19 13:43:50 -07:00
Bee 42666d9ca7 fix: use webview dependencies (#6344) 2025-09-19 13:40:32 -07:00
Bee 07f944b668 fix: update SectionHeader to prevent content overlap (#6337)
Remove sticky positioning and z-index styling from SectionHeader component to fix overlapping content issues during scroll. Also clean up unused imports and update description text styling to use semantic class.
2025-09-19 13:28:20 -07:00
canvrnoandKevin Bond c6e5b1509c Fix flicker issue when switching modes (#6341)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-19 13:04:29 -07:00
6787dedf47 v3.30.0 Release Notes (#6338)
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for Oracle Code Assist integration

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-19 12:04:04 -07:00
Saoud Rizwanandpashpashpash d07648746a Add code-supernova stealth model (#6327)
* Update announcement banner

* Fix banner buttons

---------

Co-authored-by: pashpashpash <nik@cline.bot>
2025-09-19 11:53:07 -07:00
nihar-oracle 07b49baa2a feat: OCA provider (#5075)
* feat(oca): add Oracle Code Assist provider; auth, models, settings

- Implement OCA API handler (src/core/api/providers/oca.ts)

  - OpenAI-compatible chat.completions with custom fetch injecting OCA headers
  - Optional reasoning (thinking) budget and ephemeral prompt caching
  - LiteLLM session tracking and usage streaming; cost via /spend/calculate
  - Guarded client init; clear error when OCA access token is missing

- Wire provider into core API and controller

  - Register provider (src/core/api/index.ts)
  - Controller flows for OCA account login/logout and auth status subscription
  - refreshOcaModels command and model config plumbing

- Add protobuf surfaces

  - proto/cline/models.proto and proto/cline/ocaAccount.proto
  - Extend proto/cline/state.proto for settings/state

- Persist settings/state and helpers
  - Update StateManager, state-keys, state-helpers, updateSettings

- Misc

  - Add changeset entry
  - Minor .gitignore and commit-message generator tweak

feat: New redirect server

feat(oca): add Oracle Code Assist provider; auth, models, settings

- Implement OCA API handler (src/core/api/providers/oca.ts)

  - OpenAI-compatible chat.completions with custom fetch injecting OCA headers
  - Optional reasoning (thinking) budget and ephemeral prompt caching
  - LiteLLM session tracking and usage streaming; cost via /spend/calculate
  - Guarded client init; clear error when OCA access token is missing

- Wire provider into core API and controller

  - Register provider (src/core/api/index.ts)
  - Controller flows for OCA account login/logout and auth status subscription
  - refreshOcaModels command and model config plumbing

- Add protobuf surfaces

  - proto/cline/models.proto and proto/cline/ocaAccount.proto
  - Extend proto/cline/state.proto for settings/state

- Persist settings/state and helpers
  - Update StateManager, state-keys, state-helpers, updateSettings

- Misc

  - Add changeset entry
  - Minor .gitignore and commit-message generator tweak

feat: New redirect server

update UI and add NPS survey link

papercuts

fix model dropdown height

fix: Fix 1

fix: removing ui

feat(AuthManager): Adding an AuthManager

fix: fixing rebase

* fix: Removing AuthManager, simplifyng auth service initialization
2025-09-19 00:33:24 -07:00
Alex KerandAlexKer 2668bcdbe0 added baseten link to docs.json (#6312)
* added baseten link to docs.json

* fixed docs formatting

---------

Co-authored-by: AlexKer <AlexKer@users.noreply.github.com>
2025-09-18 16:25:55 -07:00
e5e293c32b v3.29.2 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for v3.29.2 release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-18 15:56:16 -07:00
Bee fea8313695 Revert "fix: configure HeroUI setup (#6279)" (#6323)
* Revert "fix: configure HeroUI setup (#6279)"

This reverts commit 5c2d93617f.

* changeset
2025-09-18 15:44:01 -07:00
canvrnoandKevin Bond 4b450f4488 Pass max_tokens to moonshot provider (#6316)
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-18 14:21:26 -07:00
Sarah Fortune 2687ae149f Set telemetry env vars for JetBrains builds [esbuild] (#6299) 2025-09-18 20:45:05 +00:00
celestial-vault 3b93871b50 use consolidated type parameters for getter functions in state-helpers (#6262)
* use consolidated type parameters for getter functions in state-helpers

* fix types

* fix types
2025-09-18 12:55:43 -07:00
Jose Castelli 767b81b22b Improve standalone startup times (#6272)
Improve standalone startup times
2025-09-18 21:38:46 +02:00
Jose Castelli 5db4970c7d Enhance Testing Framework - Improve non-deterministic scenarios + fix flag (#6244)
Enhance Testing Framework - Improve non-deterministic scenarios + fix flag
2025-09-18 21:27:41 +02:00
Sarah Fortune 4f66126a8c Consolidate duplicated code in BrowserSession and UrlContextFetcher (#6289)
Move duplicated code to utils.ts

Replace context.globalStorageUri with the HostProvider.globalStorageFsPath.
This is part of removing dependencies on the VSCode API fom the codebase except for in platform specific code in src/hosts/vscode and src/extension.ts.
2025-09-18 11:40:44 -07:00
Sarah Fortune 43006ca401 Update runclinecore.sh script (#6300) 2025-09-18 10:12:56 -07:00
1064c631c5 v3.29.1 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-17 20:15:56 -07:00
canvrnoandKevin Bond 981fe9cf09 Changeset bump and announcement update (#6290)
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-17 20:01:36 -07:00
Sarah Fortune 90ab59d8f5 Replace context.globalStorageUri with the HostProvider in CheckpointTracker (#6277)
* Replace context.globalStorageUri with the HostProvider in CheckpointTracker

Replace context.globalStorageUri with the HostProvider.globalStorageFsPath.

Remove globalStoragePath param, it doesn't need to be passed around anymore.

Remove unused param taskId from shadowGit

* Remove globalStoragePath param from checkpoint manager.

Now tht the global storage path is available from the HostProvider anywhere, it doesn't need to be passed around.
2025-09-17 17:03:30 -07:00
kvyb f237dda413 fix: test e2e click “Show Code Actions” button then wait for listbox on macOS (#6287)
* fix: test e2e click “Show Code Actions” button then wait for listbox on macOS

* fix: trigger code actions during test with shortcut

* fix: add reasonable timeout to the menu and listbox call in test
2025-09-18 02:30:54 +03:00
Bee f08b0499d5 test: refactor chat e2e tests to support multi-roots workspace types (#6282)
* test: refactor chat e2e tests to support multiple workspace types

- Convert single test functions to parameterized test suite using describe blocks
- Add workspace type iteration for both chat messaging and slash command tests
- Consolidate test structure to run against different workspace configurations
- Maintain existing test logic while improving test coverage and organization
- consolidate chat input tests into single comprehensive test

Merged three separate chat input tests (slash commands, @ mentions, and partial completion) into one comprehensive test to reduce test setup time and improve test efficiency. Simplified test structure while maintaining all original functionality checks.

* update keybindings
2025-09-17 16:00:56 -07:00
Bee 5c2d93617f fix: configure HeroUI setup (#6279)
* fix: configure HeroUI setup

- Simplify Storybook stories pattern to only include TypeScript files
- Update HeroUI to v2.8.4 with dependencies
- Refactor HeroUI configuration with VSCode theme integration
- Streamline Tailwind CSS imports and plugin setup

* darkmode

* Theme

* remove workflow
2025-09-17 14:35:22 -07:00
celestial-vault 99201f9944 split up global state and settings types; split apart methods for fetching state and settings (#6263) 2025-09-17 14:11:56 -07:00
Sarah Fortune 0983a8a4b9 Replace context.globalStorageUri with HostProvider.globalStorageFsPath (#6276) 2025-09-17 21:05:10 +00:00
Jose Castelli 683096aed7 Interactive playwright script (#6222)
Interactive playwright script
2025-09-17 21:16:54 +02:00
7189b224fc v3.29.0 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Updated changelog and announcement banner for release

* Swapped grok button for jetbrains button

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
Co-authored-by: canvrno <46584286+canvrno@users.noreply.github.com>
2025-09-17 12:06:05 -07:00
Sarah Fortune fe3a75309d Don't use workspace resolver for tasks directory. (#6275) 2025-09-17 17:50:12 +00:00
Nick Baumann 7a00be2d55 Update JetBrains installation docs for GA release (#6264) 2025-09-17 10:39:50 -07:00
Ara 4e7af6eb5d Adding Multi Root Workspace Support for a few different tool calls (#6228)
* Adding multi root workspace support for different tools

* fixing spacing

* fixing spacing

* fixing spacing
2025-09-17 10:04:30 -07:00
Sarah Fortune e9c8f67822 Add global storage path to the Host Provider (#6248)
Add a the field `globalStorageFsPath` to replace the VSCode `context.globalStorageUri`
Remove duplicated code for getting the cache directory.
Add a util function to the HostProvider to get sub-directories of the global storage dir, and ensure that the sub-directory is created.
2025-09-17 00:13:12 -07:00
Sarah Fortune bef7d2c75a Clean up cline-core (#6255)
Move all the logic for waiting for the host bridge into `hostbridge-client.ts`
Add some comments
Use try/finally, remove unused param, log the error message when the health check fails.
2025-09-17 00:13:02 -07:00
Jose Castelli dfc660e73c Setup QLTY Initial Coverage Metrics (#6167)
Setup QLTY Initial Coverage Metrics
2025-09-17 08:39:08 +02:00
Bee af0bc4dc6c fix: CreditLimitError should use server returned url when available (#6261)
* fix: CreditLimitError should use server returned url when available

- Add useMemo hook for dashboard URL computation
- Display credit balance, spent, and promotions when available
- Replace inline styles with Tailwind CSS classes
- Simplify TaskServiceClient call by removing unnecessary properties
- Add proper conditional rendering for credit information
- Improve code organization and readability

* changeset added

* props

* Update storybook
2025-09-16 20:30:55 -07:00
Alex KerandAlexKer f37962bbf2 Updated Baseten models to use dynamic fetching, added docs (#6148)
* Updated Baseten models to use dynamic fetching, added docs

* removed unused function, supportsImages

* added parsePrice into model-utils.ts

* updated toolcalling comment

* updated tools to fetch from API

* fixed conflicts and updated supportTools

* applied npm run format:fix

* added bedrock stuff back in that was breaking test

---------

Co-authored-by: AlexKer <AlexKer@users.noreply.github.com>
2025-09-16 19:52:35 -07:00
celestial-vault e82597e069 move apihandler secrets to own type and fix some type errors related to the handlers (#6249) 2025-09-16 18:44:34 -07:00
Bee d1db8f747a docs: add comprehensive E2E testing documentation (#6259)
Add detailed documentation for end-to-end testing with Playwright including:
- Test execution commands and debug mode usage
- Test structure overview with file descriptions
- Writing test guidelines with fixtures and best practices
- Mock API server setup and workspace configuration
- Interactive debugging features and troubleshooting tips

Updates both CONTRIBUTING.md and E2E README to provide complete guidance for contributors working with the test suite.
2025-09-16 17:31:46 -07:00
BeeandJose R. Perez e8ba3c34fb feat: add Storybook configuration (#6256)
* feat: add Storybook configuration

- Bump package version from 3.28.3 to 3.28.4
- Add comprehensive Storybook setup with React-Vite framework and TypeScript support
- Configure Storybook with custom viewport settings and environment variables
- Add extensive story files for chat components, MCP displays, and browser automation
- Update gitignore files to exclude Storybook build artifacts and logs

* Add docs and changeset

* feat: refactor Storybook decorator to support state overrides and custom styling

- Extract ExtensionStateProviderWithOverrides component to safely use useExtensionState within provider context
- Add optional classNames parameter to createStorybookDecorator for custom styling
- Import cn utility from @heroui/react for className merging
- Fix margin class from m-x-auto to mx-auto

* Add message to mock history

---------

Co-authored-by: Jose R. Perez <trupix@gmail.com>
2025-09-16 17:27:55 -07:00
yuvalman fadcd7e3ad fix: sap provider - set sapAiCoreUseOrchestrationMode in state-helper instead of in sap provider components (#6221)
* fix: sap provider - handle undefined apiConfiguration.sapAiCoreUseOrchestrationMode

* fix: sap provider - handle undefined apiConfiguration.sapAiCoreUseOrchestrationMode

* fix: sap provider - handle undefined apiConfiguration.sapAiCoreUseOrchestrationMode
2025-09-16 14:57:30 -07:00
celestial-vault 81299d26d8 refactor: move favorited model IDs to dedicated state management (#6251)
Removed favorited_model_ids field from API configuration models in proto files and migrated storage handling to global state key management.

Cleans up configuration separation by removing non-API-related data from model definitions. Enables more consistent state handling across the application.
2025-09-16 14:56:14 -07:00
Sarah Fortune 832025c8cc Use the correct version number in cline-core (#6254)
* Use the correct version number in cline-core

cline-core should use the same version number as the extension because they share the same code for the core functionality.
Use the version number from the ExtensionRegistryInfo instead of from the standalone package.json

* Use node:fs instead of fs
2025-09-16 14:15:47 -07:00
celestial-vault f8a7a1cc58 [Cleanup] Remove API Configuration Conversion in updateSettings (#6196)
* Refactor API configuration to use proto enums and structured types

- Replace JSON string fields with proper proto message types
- Convert snake_case field names to camelCase for consistency
- Use ApiProvider enum instead of string for provider fields
- Add structured types for model info and selectors
- Remove deprecated settings conversion module
- Update proto conversions to handle new structured format

* fix types errors caused by changing to generic string
2025-09-16 10:15:32 -07:00
Sarah Fortune 3723288250 Add a field to the HostProvider for extension install dir (#6239)
* Add a field to the HostProvider `extensionFsPath`

Replace vscode.ExtensionContext `extensionUri` with `HostProvider.extensionFsPath`.

This part of removing uses of the VSCode API fom the codebase except for in platform specific code (these are src/hosts/vscode and src/extension.ts).

* Remove extra leading slash from webview URL on JetBrains

* Remove vscode.Uri from the WebviewProvider

Replace `vscode.Uri` with URL strings.
Replace Uri with Url in variable names.
2025-09-16 09:49:32 -07:00
Jose Castelli 0b5e8f5c37 Moving grpc recorder unit test (#6214)
Moving grpc recorder unit test
2025-09-16 12:54:39 +02:00
kvyb a04f6050ee fix standalone: isolate workspaceState per project via WORKSPACE_STORAGE_DIR (#6215)
* fix standalone: isolate workspaceState per project via WORKSPACE_STORAGE_DIR

* fix: keep logs global; default workspace storage dir; no try/catch on mkdir
2025-09-16 10:30:02 +03:00
307 changed files with 18741 additions and 11077 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Screen readers now properly announce buttons in history preview and history view screens.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Adding Workspace path adapter and workspace hints logic for tools
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Screen readers properly announce buttons in the MCP servers / Cline rules / workflows screens. Reflects the current selection of Cline rules / workflows tab to screen readers.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add environment variable injection to esbuild config
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix crashing bug when custom url was incorrect
+2 -2
View File
@@ -1,3 +1,3 @@
/docs/
/.github/ @saoudrizwan @dcbartlett
/README.md @saoudrizwan @nickbaumann98
/.github/ @saoudrizwan
/README.md @saoudrizwan @nickbaumann98
+2 -2
View File
@@ -5,7 +5,7 @@ body:
- 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.
**Important:** All bug reports must be reproducible using Claude Sonnet 4.5. Cline uses complex prompts so less capable models may not work as expected.
- type: dropdown
id: plugin-type
attributes:
@@ -49,7 +49,7 @@ body:
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'
placeholder: 'e.g., cline:anthropic/claude-sonnet-4.5, gemini:gemini-2.5-pro-exp-03-25'
validations:
required: false
- type: textarea
+121 -2
View File
@@ -90,8 +90,14 @@ jobs:
- name: Build Tests and Extension
run: npm run pretest
- name: Unit Tests
run: npm run test:unit
- name: Unit Tests (with coverage on Linux)
run: |
if [ "${{ runner.os }}" = "Linux" ]; then
npm install --no-save nyc
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
else
npm run test:unit
fi
# Run extension tests with coverage
- name: Extension Integration Tests with Coverage
@@ -125,6 +131,8 @@ jobs:
path: |
extension_coverage.txt
webview-ui/webview_coverage.txt
coverage-unit/lcov.info
webview-ui/coverage/lcov.info
# Set the check as failed if any of the tests failed
- name: Check for test failures
@@ -141,6 +149,69 @@ jobs:
exit 1
fi
test-platform-integration:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
# Cache testing-platform dependencies
- name: Cache testing-platform dependencies
uses: actions/cache@v4
id: testing-platform-cache
with:
path: testing-platform/node_modules
key: ${{ runner.os }}-npm-testing-platform-${{ hashFiles('testing-platform/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Compile standalone
run: npm run compile-standalone
- name: Install testing platform dependencies
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
run: cd testing-platform && npm ci
- name: Running testing platform integration spec tests
continue-on-error: true
timeout-minutes: 7
# Temporarily wrapping the test command to always return a neutral exit code.
# This prevents the job from showing as failed and avoids distracting developers
# until the integration tests are ready to be enforced.
run: |
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage || true
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
with:
name: test-platform-integration-core-coverage
path: coverage/**/lcov.info
coverage:
needs: test
runs-on: ubuntu-latest
@@ -219,3 +290,51 @@ jobs:
--verbose
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
qlty:
needs: [test, test-platform-integration]
runs-on: ubuntu-latest
# Run on PRs to main, pushes to main, and manual dispatches
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for accurate comparison
- name: Download unit tests coverage reports
uses: actions/download-artifact@v4
with:
name: pr-coverage-reports
path: .
- name: Upload core unit tests coverage to Qlty
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
# we can merge multiple files if necessary
files: |
coverage-unit/lcov.info
tag: unit:core
- name: Upload webview-ui unit tests coverage to Qlty
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
# we can merge multiple files if necessary
files: |
webview-ui/coverage/lcov.info
tag: unit:webview-ui
add-prefix: webview-ui/
- name: Download test platform integration core coverage artifact
uses: actions/download-artifact@v4
with:
name: test-platform-integration-core-coverage
path: integration-core-coverage-reports
- name: Upload core integration tests coverage to Qlty
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
files: integration-core-coverage-reports/**/lcov.info
tag: integration:core
@@ -0,0 +1,53 @@
name: Trigger Jetbrains Plugin <-> Cline Tests
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
concurrency:
group: jetbrains-trigger-${{ github.event.number }}
cancel-in-progress: true
jobs:
trigger-integration-test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: 1998650
private-key: ${{ secrets.CLINE_JETBRAINS_WORKFLOW_KEY }}
owner: cline
repositories: intellij-plugin
- name: Trigger IntelliJ Plugin Integration Test
run: |
curl -X POST \
-H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \
-H "Accept: application/vnd.github.v3+json" \
-H "User-Agent: cline-pr-trigger" \
-H "Content-Type: application/json" \
https://api.github.com/repos/cline/intellij-plugin/dispatches \
-d @- <<EOF
{
"event_type": "cline-pr-check",
"client_payload": {
"pr_number": "${{ github.event.number }}",
"branch_name": "${{ github.head_ref }}",
"action": "${{ github.event.action }}",
"sha": "${{ github.event.pull_request.head.sha }}",
"pr_title": ${{ toJSON(github.event.pull_request.title) }},
"pr_url": "${{ github.event.pull_request.html_url }}"
}
}
EOF
- name: Log trigger details
run: |
echo "Triggered IntelliJ Plugin integration test for:"
echo " PR #${{ github.event.number }}: ${{ github.event.pull_request.title }}"
echo " Branch: ${{ github.head_ref }}"
echo " Action: ${{ github.event.action }}"
echo " SHA: ${{ github.event.pull_request.head.sha }}"
+2
View File
@@ -20,6 +20,8 @@ webview-ui/src/**/*.js.map
# Ignore coverage directories and files
coverage
coverage-unit
.nyc_output
# But don't ignore the coverage scripts in .github/scripts/
!.github/scripts/coverage/
+48
View File
@@ -0,0 +1,48 @@
{
"all": true,
"check-coverage": false,
"reporter": [
"text",
"lcov"
],
"include": [
"src/**/*.ts"
],
"exclude": [
"**/*.d.ts",
"**/*.{test,spec}.{js,jsx,ts,tsx,mjs,cjs}",
"**/__tests__/**",
"**/test/**",
"**/tests/**",
"**/.nyc_output/**",
"**/.vscode-test/**",
"**/tests-results/**",
"src/test/**",
"src/generated/**",
"**/node_modules/**",
"**/dist/**",
"**/out/**",
"**/build/**",
"**/coverage/**",
"**/coverage-unit/**",
"**/proto/**",
"**/*.{config,setup}.{js,ts,mjs,cjs}",
"**/vite-env.d.ts",
"**/*.{css,scss,sass,less,styl}",
"**/*.{svg,png,jpg,jpeg,gif,ico}",
"**/*.{json,yaml,yml}"
],
"extension": [
".ts",
".js"
],
"cache": true,
"sourceMap": true,
"instrument": true,
"report-dir": "./coverage-unit"
}
+6
View File
@@ -64,3 +64,9 @@ old_docs/**
e2e-build.mjs
e2e.vsix
test-results/
# Ignore Storybook files
**/*.stories.tsx
*storybook.log
storybook-static
**/StorybookDecorator.tsx
+67
View File
@@ -1,5 +1,72 @@
# Changelog
## [3.32.3]
- Add Claude Sonnet 4.5 to Bedrock provider
- Add Alert banner for new Claude Sonnet 4.5 model
## [3.32.2]
- Add Claude Sonnet 4.5 to Cline/OpenRouter/Anthropic providers
- Add /task deep link handler
## [3.32.1]
- Preserve reasoning traces for Cline/OpenRouter/Anthropic providers to maintain conversation integrity
- Add automatically retry on rate limit errors with SAP AI Core provider
- Fix Cline accounts using stale id token at refresh response
- Minor UI improvements to Settings and Task Header
## [3.32.0]
- Added the new code-supernova-1-million stealth model, available for free and delivering a 1 million token context window
- Changes to inform Cline about commands that are available on your system
## [3.31.1]
- Version bump
## [3.31.0]
- UI Improvements: New task header and focus chain design to take up less space for a cleaner experience
- Voice Mode: Experimental feature that must be enabled in settings for hands-free coding
- YOLO Mode: Enable in settings to let Cline approve all actions and automatically switch between plan/act mode
- Fix Oracle Code Assist provider issues
## [3.30.3]
- Add Oracle Code Assist provider
## [3.30.2]
- Fix UI tests
## [3.30.1]
- Fix model list not being updated in time for user to use shortcut button to update model to stealth model
- Fix flicker issue when switching modes
- Fix Sticky header in settings view overlaping with content on scroll
- Add experimental yolo mode feature that disables all user approvals and automatically executes a task and navigates through plan to act mode until the task is complete
## [3.30.0]
- Add code-supernova stealth model
## [3.29.2]
- Fix: Reverted change that caused formatting issues
- Fix: Moonshot - Pass max_tokens value to provider
## [3.29.1]
- Changeset bump + Announcement banner update
## [3.29.0]
- Updated Baseten provider to fetch models from server
- Fix: Updated insufficient balance URL for easy Cline balance top-ups
- Accessibility: Improvements to screen readers in MCP, Cline Rules, workflows, and history views.
## [3.28.4]
- Fix bug where some Windows machines had API request hanging
+30
View File
@@ -157,6 +157,36 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
- Update existing tests if your changes affect them
- Include both unit tests and integration tests where appropriate
**End-to-End (E2E) Testing**
Cline includes comprehensive E2E tests using Playwright that simulate real user interactions with the extension in VS Code:
- **Running E2E tests:**
```bash
npm run test:e2e # Build and run all E2E tests
npm run e2e # Run tests without rebuilding
npm run test:e2e -- --debug # Run with interactive debugger
```
- **Writing E2E tests:**
- Tests are located in `src/test/e2e/`
- Use the `e2e` fixture for single-root workspace tests
- Use `e2eMultiRoot` fixture for multi-root workspace tests
- Follow existing patterns in `auth.test.ts`, `chat.test.ts`, `diff.test.ts`, and `editor.test.ts`
- See `src/test/e2e/README.md` for detailed documentation
- **Debug mode features:**
- Interactive Playwright Inspector for step-by-step debugging
- Record new interactions and generate test code automatically
- Visual VS Code instance for manual testing
- Element inspection and selector validation
- **Test environment:**
- Automated VS Code setup with Cline extension loaded
- Mock API server for backend testing
- Temporary workspaces with test fixtures
- Video recording for failed tests
4. **Version Management with Changesets**
- Create a changeset for any user-facing changes using `npm run changeset`
+10 -1
View File
@@ -91,6 +91,7 @@
"features/focus-chain",
"features/auto-compact",
"features/editing-messages",
"features/dictation",
{
"group": "@ Mentions",
"pages": [
@@ -121,6 +122,13 @@
"features/commands-and-shortcuts/git-integration",
"features/commands-and-shortcuts/keyboard-shortcuts"
]
},
{
"group": "Customization",
"pages": [
"features/customization/opening-cline-in-sidebar",
"features/customization/disable-terminal-pagers"
]
}
]
},
@@ -183,7 +191,8 @@
"provider-config/openrouter",
"provider-config/sap-aicore",
"provider-config/vercel-ai-gateway",
"provider-config/requesty"
"provider-config/requesty",
"provider-config/baseten"
]
},
{
@@ -0,0 +1,79 @@
---
title: "Disable Terminal Pagers During Cline Sessions"
description: "Make CLI output non-interactive when Cline runs commands by detecting the CLINE_ACTIVE environment variable and disabling pagers like less."
---
Many CLI tools (like Git) use a pager such as `less` for interactive, scrollable output. When Cline runs commands in your terminal, that interactivity gets in the way — the pager can pause on the first page and block progress. You can configure your shell so that when a terminal is spawned by Cline, pagers are disabled and output streams through normally.
## How it works
Cline sets an environment variable for terminals it opens to run commands:
- `CLINE_ACTIVE` — non-empty when the shell is running under Cline
You can detect this variable in your shell startup file and adjust environment variables or aliases only for Cline-run sessions. This keeps your normal interactive terminals unchanged.
## Quick setup (Zsh/Bash)
Add the following to your `~/.zshrc`, `~/.bashrc`, or `~/.bash_profile`:
```bash
# Disable pagers when the terminal is launched by Cline
if [[ -n "$CLINE_ACTIVE" ]]; then
export PAGER=cat
export GIT_PAGER=cat
export SYSTEMD_PAGER=cat
export LESS="-FRX"
fi
```
<Note>
- `PAGER=cat` ensures generic pager-aware tools print directly to stdout
- `GIT_PAGER=cat` prevents Git from invoking `less`
- `SYSTEMD_PAGER=cat` disables paging in systemd tools (if present)
- `LESS="-FRX"` makes `less` behave more like streaming output if a tool still calls it
</Note>
This configuration only applies when `CLINE_ACTIVE` is set, so your normal terminals keep their usual interactive behavior.
## Verify
- Open a task in Cline that runs terminal commands and check:
- `echo "$CLINE_ACTIVE"` prints a non-empty value
- `git log` or other long outputs should stream without pausing
- If changes don't take effect:
- Make sure you updated the correct startup file for your shell
- Restart VS Code/Cursor so integrated terminals reload your shell config
- Confirm your terminal profile sources your `~/.zshrc` or `~/.bashrc`
## Optional tweaks
- Prefer command-line options when you don't want to rely on env vars:
```bash
# One-off usage (no aliases)
git --no-pager log -n 50 --decorate --oneline
systemctl --no-pager status nginx
journalctl --no-pager -u nginx -n 200
less -FRX README.md
```
- You can also override paging via shell aliases scoped to Cline sessions using options rather than env vars:
```bash
if [[ -n "$CLINE_ACTIVE" ]]; then
# Make 'less' non-interactive by default
alias less='less -FRX'
# Disable paging for common tools via CLI flags
alias git='command git --no-pager'
alias systemctl='command systemctl --no-pager'
alias journalctl='command journalctl --no-pager'
fi
```
- If you prefer environment variables, many CLIs also respect a generic or tool-specific pager variable:
- Git: `GIT_PAGER=cat`
- Systemd: `SYSTEMD_PAGER=cat`
- Man pages: `MANPAGER=cat` (not typically needed for Cline-driven commands)
- Aliases affect the current interactive shell, while environment variables propagate to child processes. Choose the approach that best fits your workflow.
@@ -0,0 +1,60 @@
---
title: "Opening Cline in the Right Sidebar"
description: "Learn how to open Cline in the right sidebar in VS Code and Cursor"
---
By default, when you first install Cline, it appears in VS Code's left sidebar alongside your file explorer and other extensions. However, for a better coding experience, we recommend moving Cline to the right sidebar. This allows you to keep your project files visible in the left sidebar while chatting with Cline on the right, giving you full visibility of your codebase as Cline works on your project.
## VS Code
To open Cline in the right sidebar:
<Steps>
<Step title="Align Extension View">
Make sure your extension view is aligned vertically to the left
</Step>
<Step title="Open Right Side View">
Click the button that opens the right side panel in VS Code (typically used to open GitHub Copilot chat). Optionally use the `Option + CMD/Ctrl + B` shortcut.
</Step>
<Step title="Drag Cline Icon">
Drag the Cline icon over to the nav panel at the top of that right view
</Step>
</Steps>
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/vscode_right_view.gif"
alt="VS Code Right Sidebar Setup"
/>
</Frame>
## Cursor
To open Cline in the right sidebar:
<Steps>
<Step title="Align Extensions">
Cursor uses a horizontal activity bar by default to optimize space for the AI chat interface ([see here for details](https://cursor.com/docs/configuration/migrations/vscode#activity-bar-orientation)). To switch to vertical:
1. Open the Command Palette (`CMD/Ctrl + Shift + P`)
2. Search for "Preferences: Open Settings (UI)"
3. Search for `workbench.activityBar.orientation`
4. Set the value to `vertical`
5. Restart Cursor for the changes to take effect
</Step>
<Step title="Open Agent Panel">
Click the Cursor cube icon button that opens Cursor's agent (right side view panel)
</Step>
<Step title="Drag to Three Dots">
Drag the Cline icon directly onto the three dots button - it doesn't work if you just drag it to the top, it has to be the three dots
</Step>
</Steps>
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/cursor-side-bar.gif"
alt="Cursor Right Sidebar Setup"
/>
</Frame>
Once set up, Cline will load on the right side and you can use it as normal.
+60
View File
@@ -0,0 +1,60 @@
---
title: Dictation
description:
---
Cline lets you transcribe speech to text in an easy, built-in service
## Get Started
1. **Enable Dictation** in Feature Settings.
2. **Click the microphone** in the chat input area.
3. **Speak** - the button turns red while recording.
4. **Click Stop Recording** when done.
5. **Wait for transcription** - "[Transcribing...]" will appear and then the finished transcription will appear.
## Settings
Enable or disable dictation in Feature Settings by toggling "Enable Dictation." You can also change the transcription language to one of the supported languages.
## Requirements
Cline uses FFmpeg to capture your voice across all platforms:
- **macOS**: FFmpeg (via Homebrew: `brew install ffmpeg`)
- **Linux**: FFmpeg (via apt: `sudo apt-get install ffmpeg`)
- **Windows**: FFmpeg (via winget: `winget install Gyan.FFmpeg`)
If you don't have FFmpeg installed, Cline will automatically detect this and prompt you to install it with a single click.
## Technical Details
### Independent from Chat Provider
The voice transcription feature works completely independently from whatever chat provider you're using. You can use Claude, GPT-4, or any other model for your conversations, and voice transcription will always use Cline's own transcription service. As long as you have a valid Cline account with enough credits, dictation will work regardless of your chat model choice.
### Audio Format
Voice recordings are captured in WebM format using the Opus codec for optimal compression. The system records in mono (single channel) at a 16kHz sample rate, which is specifically optimized for voice recognition. The bitrate is set to 32kbps to keep file sizes efficient while maintaining good audio quality.
### Privacy & Security
Your audio is recorded locally on your machine and only the audio file itself is sent to Cline's transcription service for processing. No audio is stored anywhere after transcription is complete, and all temporary files are automatically cleaned up to protect your privacy.
## Troubleshooting
`Failed to start recording` - Audio recording tools aren't installed. Cline will prompt you to install FFmpeg - just follow the chat instructions.
`Invalid audio format or request data` - Usually an audio recording issue. Make sure FFmpeg is properly installed and working.
`Authentication failed` - You need to reauthenticate your Cline account. Sign out and back in, then check your internet connection.
`Insufficient credits for transcription service` - Your Cline account doesn't have enough credits. Check your balance and purchase more if needed.
`Cannot connect to transcription service` - Connection issue. Check your internet connection and firewall settings aren't blocking Cline's servers.
## API Usage
Voice transcription uses Cline's transcription service, which requires credits from your Cline account. Currently, voice transcription is billed at $0.006 per minute of audio.
**Note:** We are still experimenting with this feature and pricing may change in the future.
@@ -1,6 +1,6 @@
---
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."
title: "Installing Cline for JetBrains"
description: "Install Cline in your favorite JetBrains IDE with the same powerful AI assistance you know from VSCode."
---
<Frame>
@@ -20,109 +20,103 @@ Cline for JetBrains works almost identically to Cline in VSCode. All the core fe
/>
</Frame>
<Note>Cline for JetBrains is in early access. All core features are functional, with ongoing improvements based on user feedback.</Note>
Cline is now available on the [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/28247-cline). Works in IntelliJ IDEA, PyCharm, WebStorm, DataSpell, PhpStorm, and all other JetBrains IDEs.
## Installation
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:
**Method 1: Direct from your IDE**
### Manual Installation from Disk
1. **Download the Plugin:**
- 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
1. Open your JetBrains IDE
2. Press `Ctrl+Alt+S` (Windows/Linux) or `Cmd+,` (macOS)
3. Go to Plugins → Marketplace tab
4. Search "Cline" and click Install
5. Restart your IDE
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-marketplace-download.png"
alt="JetBrains plugin marketplace showing Cline download page"
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-marketplace-search.png"
alt="JetBrains marketplace showing Cline plugin search results"
/>
</Frame>
2. **Install from Disk:**
- Open your JetBrains IDE
- Go to **IntelliJ IDEA** (or whichever IDE you are in) → **Settings**
**Method 2: Browser install**
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-settings.png"
alt="JetBrains IDE settings dialog"
/>
</Frame>
Visit [plugins.jetbrains.com/plugin/28247-cline](https://plugins.jetbrains.com/plugin/28247-cline) and click the "Install to IDE" button. Your IDE will open and prompt you to install.
- Select **Plugins** from the left sidebar
- Click the gear icon ⚙️ and select **Install Plugin from Disk...**
<details>
<summary>Method 3: Manual installation</summary>
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-install-disk.png"
alt="JetBrains IDE settings showing Install Plugin from Disk option"
/>
</Frame>
Download the plugin from the [marketplace page](https://plugins.jetbrains.com/plugin/28247-cline), then:
- Select the downloaded `.zip` file
1. Go to Settings → Plugins
2. Click the gear icon → Install Plugin from Disk
3. Select the downloaded `.zip` file
4. Restart your IDE
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-zip-file.png"
alt="File selection dialog showing Cline plugin zip file"
/>
</Frame>
- Restart your IDE when prompted
</details>
## Getting Started with Cline
After installation, you'll find Cline in your IDE:
After installation, you'll find Cline in your IDE. Look for the Cline tool window (usually on the right side) or go to **View** → **Tool Windows** → **Cline**.
1. **Open Cline:**
- Look for the Cline tool window (usually on the right side)
- Or go to **View** → **Tool Windows** → **Cline**
Sign in is optional - you can also bring your own API key. If you want to sign in, click **Sign In** in the Cline panel. You'll be taken to [app.cline.bot](https://app.cline.bot) to create your account. No credit card needed to get started with free credits.
2. **Sign In (optional, BYOK is also available):**
- Click **Sign In** in the Cline panel
- You'll be taken to [app.cline.bot](https://app.cline.bot) to create your account
- No credit card needed to get started with free credits
3. **Start Coding:**
- Try this first prompt: "Hey Cline! Can you help me create a simple Hello World program in this project?"
Try this first prompt: "Hey Cline! Can you help me create a simple Hello World program in this project?"
## Key Differences from VSCode
While Cline for JetBrains includes all the same powerful features, there's one important difference to be aware of:
**Terminal Integration:** The terminal inside JetBrains isn't integrated with Cline the same way it is in VSCode. Cline can execute commands, but the output will only appear in the webview if you expand the **Command Output** section.
This means:
- Commands still run successfully
- You can see the output by clicking to expand Command Output in the chat
- Terminal commands work the same way, just with a different display
The terminal integration works differently in JetBrains. Unlike VSCode where terminal output streams directly to the chat, JetBrains shows command output in a collapsible section. Commands still execute successfully - you just need to expand the **Command Output** section to see results.
## What Works
Everything else works exactly like VSCode:
Everything else works exactly like VSCode. Cline can read, write, and edit files with the same precision. All his tools work identically - file operations, web browsing, you name it.
- **Diff Editing:** Cline can read, write, and edit files with the same precision
- **Tool Usage:** All of Cline's tools (file operations, web browsing, etc.) work identically
- **API Providers:** Connect to Anthropic, OpenAI, local models, and more
- **MCP Servers:** Full support for Model Context Protocol servers
- **Cline Rules:** Custom instructions and workflows work the same way
- **@ Mentions:** Reference files, folders, problems, and more
- **Drag & Drop:** Add files and images to conversations
You get full support for:
- API providers (Anthropic, OpenAI, local models)
- MCP servers and custom tools
- Cline rules and workflows
- @ mentions for files, folders, and problems
- Drag & drop for files and images
## Tips for JetBrains Users
- **Project Context:** Cline automatically understands your project structure, just like in VSCode
- **Language Support:** Cline works with any language your JetBrains IDE supports
- **Debugging Help:** Share error messages and stack traces directly in the chat
- **Code Review:** Ask Cline to review your code changes before committing
Cline automatically understands your project structure, just like in VSCode. He works with any language your JetBrains IDE supports - Java, Python, JavaScript, Go, whatever you're building.
I find it helpful to share error messages and stack traces directly in the chat when debugging. You can also ask him to review your code changes before committing.
## Troubleshooting
### Plugin Installation Issues
If you can't find Cline in the marketplace:
- Make sure you're searching in the **Marketplace** tab (not Installed)
- Try searching for "Cline AI" or just "Cline"
- Check that your IDE version is compatible (2023.1 or later recommended)
If installation fails:
- Restart your IDE and try again
- Check your internet connection
- Try installing from disk as an alternative
### Plugin Not Appearing
If you don't see the Cline tool window after installation:
- Restart your IDE completely
- Restart your IDE completely (File → Exit and reopen)
- Check **View** → **Tool Windows** → **Cline**
- Ensure the plugin is enabled in **Settings** → **Plugins**
- Verify the plugin is enabled in **Settings** → **Plugins** → **Installed** tab
- Look for the Cline icon in your IDE's tool window bar (usually on the right side)
### Common Issues
**Plugin appears to be installed but doesn't work:**
- Ensure you've restarted your IDE after installation
- Check if there are any error messages in the IDE's event log
- Try disabling and re-enabling the plugin in Settings
**Performance or compatibility issues:**
- Make sure you're using a supported JetBrains IDE version
- Check for IDE updates that might improve compatibility
- Consider allocating more memory to your IDE if needed
Having other issues? Join our [Discord community](https://discord.gg/cline) for help from the team and other users.
+21 -5
View File
@@ -1,13 +1,17 @@
---
title: "Installing Cline"
description: "Cline is a VS Code extension that brings AI-powered coding assistance directly
to your editor. Install using one of these methods:"
description: "Cline brings AI-powered coding assistance to your editor. Available for VS Code and JetBrains IDEs."
---
### Installation Options
## Choose Your Editor
- **VS Code Marketplace (Recommended):** Fastest method for standard VS Code and Cursor users.
- **Open VSX Registry:** For VS Code-compatible editors like VSCodium.
Cline works across multiple development environments:
- **VS Code/Cursor:** Install from VS Code Marketplace (most popular)
- **JetBrains IDEs:** Install from JetBrains Marketplace - works in IntelliJ IDEA, PyCharm, WebStorm, and more
- **VSCodium/Windsurf:** Install from Open VSX Registry
## VS Code Installation
### VS Code Marketplace: Step-by-Step Setup
@@ -46,6 +50,18 @@ For VS Code-compatible editors without Marketplace access (like VSCodium and Win
4. Select "Cline" by saoudrizwan and click **Install**.
5. Reload if prompted.
## JetBrains Installation
For IntelliJ IDEA, PyCharm, WebStorm, DataSpell, and other JetBrains IDEs:
1. Open your JetBrains IDE
2. Press `Ctrl+Alt+S` (Windows/Linux) or `Cmd+,` (macOS) to open Settings
3. Go to Plugins → Marketplace tab
4. Search "Cline" and click Install
5. Restart your IDE
**Need more help?** See our [complete JetBrains installation guide](/getting-started/installing-cline-jetbrains) for screenshots and troubleshooting.
### Creating Your Cline Account
Now that you have Cline installed, let's get you set up with your account:
+2 -2
View File
@@ -7,7 +7,7 @@ title: "Configuring MCP Servers"
Utilizing MCP servers will increase your token usage. Cline offers the ability to restrict or disable MCP server functionality as desired.
1. Click the "MCP Servers" icon in the top navigation bar of the Cline extension.
2. Select the "Installed" tab, and then Click the "Advanced MCP Settings" link at the bottom of that pane.
2. Select the "Configure" tab, and then Click the "Advanced MCP Settings" link at the bottom of that pane.
3. Cline will open a new settings window. find `Cline>Mcp:Mode` and make your selection from the dropdown menu.
<Frame>
@@ -56,7 +56,7 @@ To set the maximum time to wait for a response after a tool call to the MCP serv
Settings for all installed MCP servers are located in the `cline_mcp_settings.json` file:
1. Click the MCP Servers icon at the top navigation bar of the Cline pane.
2. Select the "Installed" tab.
2. Select the "Configure" tab.
3. Click the "Configure MCP Servers" button at the bottom of the pane.
The file uses a JSON format with a `mcpServers` object containing named server configurations:
+2 -2
View File
@@ -54,7 +54,7 @@ Please note: Smithery is maintained independently and is not affiliated with our
### Managing Installed MCP Servers
Once added, your MCP servers appear in the "Installed" tab where you can:
Once added, your MCP servers appear in the "Configure" tab where you can:
#### View Server Status
@@ -98,7 +98,7 @@ If a server fails to connect:
For advanced users, Cline stores MCP server configurations in a JSON file that can be modified:
1. In the "Installed" tab, click "Configure MCP Servers" to access the settings file
1. In the "Configure" tab, click "Configure MCP Servers" to access the settings file
2. The configuration for each server follows this format:
```json
+4 -4
View File
@@ -1386,7 +1386,7 @@
"integrity": "sha512-/uR4hAwpcJW9+zbmZL48kKFnWLkOxhIqoGWvZzjg0CniVhR4emtQJAps80WqLAhz0iJgCQxg/axtA7leaznDzQ==",
"license": "Elastic-2.0",
"dependencies": {
"axios": "^1.8.3",
"axios": "^1.12.0",
"openapi-types": "^12.0.0"
},
"engines": {
@@ -2688,9 +2688,9 @@
}
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
+115
View File
@@ -0,0 +1,115 @@
---
title: "Baseten"
description: "Learn how to configure and use Baseten's Model APIs with Cline. Access frontier open-source models with enterprise-grade performance, reliability, and competitive pricing."
---
Baseten provides on-demand frontier model APIs designed for production applications, not just experimentation. Built on the Baseten Inference Stack, these APIs deliver enterprise-grade performance and reliability with optimized inference for leading open-source models from OpenAI, DeepSeek, Meta, Moonshot AI, and Alibaba Cloud.
**Website:** [https://www.baseten.co/products/model-apis/](https://www.baseten.co/products/model-apis/)
### Getting an API Key
1. **Sign Up/Sign In:** Go to [Baseten](https://www.baseten.co/) and create an account or sign in.
2. **Navigate to API Keys:** Access your dashboard and go to the API Keys section.
3. **Create a Key:** Generate a new API key. Give it a descriptive name (e.g., "Cline").
4. **Copy the Key:** Copy the API key immediately and store it securely.
### Supported Models
Cline supports all current models under Baseten Model APIs, including:
For the most updated pricing, please visit: https://www.baseten.co/products/model-apis/
**Reasoning Models:**
- `deepseek-ai/DeepSeek-R1` - DeepSeek's first-generation reasoning model (163K context) - \$2.55/\$5.95 per 1M tokens
- `deepseek-ai/DeepSeek-R1-0528` - Latest revision of DeepSeek's reasoning model (163K context) - \$2.55/\$5.95 per 1M tokens
- `deepseek-ai/DeepSeek-V3.1` - Hybrid reasoning with advanced tool calling (163K context) - \$0.50/\$1.50 per 1M tokens
- `deepseek-ai/DeepSeek-V3-0324` - Fast general-purpose with enhanced reasoning (163K context) - \$0.77/\$0.77 per 1M tokens
**Flagship Models:**
- `openai/gpt-oss-120b` (OpenAI) - 120B MoE with strong reasoning capabilities (128K context) - \$0.10/\$0.50 per 1M tokens
- `moonshotai/Kimi-K2-Instruct` (Moonshot AI) - 1 trillion parameter model for agentic tasks (131K context) - \$0.60/\$2.50 per 1M tokens
- `moonshotai/Kimi-K2-Instruct-0905` (Moonshot AI) - September update with enhanced capabilities (262K context) - \$0.60/\$2.50 per 1M tokens
**Meta Llama 4 Series:**
- `meta-llama/Llama-4-Maverick-17B-128E-Instruct` - High-efficiency processing (1M context!) - \$0.19/\$0.72 per 1M tokens
- `meta-llama/Llama-4-Scout-17B-16E-Instruct` - Precise context understanding (1M context!) - \$0.13/\$0.50 per 1M tokens
**Coding Specialists:**
- `Qwen/Qwen3-Coder-480B-A35B-Instruct`- Advanced coding and reasoning (262K context) - \$0.38/\$1.53 per 1M tokens
- `Qwen/Qwen3-235B-A22B-Instruct-2507` - Math and reasoning expert (262K context) - \$0.22/\$0.80 per 1M tokens
### Configuration in Cline
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
2. **Select Provider:** Choose "Baseten" from the "API Provider" dropdown.
3. **Enter API Key:** Paste your Baseten API key into the "Baseten API Key" field.
4. **Select Model:** Choose your desired model from the "Model" dropdown.
### Production-First Architecture
Baseten's Model APIs are built for production environments with several key advantages:
#### Enterprise-Grade Reliability
- **Four nines of uptime** (99.99%) through active-active redundancy
- **Cloud-agnostic, multi-cluster autoscaling** for consistent availability
- **SOC 2 Type II certified** and **HIPAA compliant** for security requirements
#### Optimized Performance
- **Pre-optimized models** shipped with the Baseten Inference Stack
- **Latest-generation GPUs** with multi-cloud infrastructure
- **Ultra-fast inference** optimized from the bottom up for production workloads
#### Cost Efficiency
- **5-10x less expensive** than closed alternatives
- **Optimized multi-cloud infrastructure** for efficient resource utilization
- **Transparent pricing** with no hidden costs or rate limit surprises
#### Developer Experience
- **OpenAI compatible API** - migrate by swapping a single URL
- **Drop-in replacement** for closed models with comprehensive observability
- **Seamless scaling** from Model APIs to dedicated deployments
### Special Features
#### Function Calling & Tool Use
All Baseten models support structured outputs, function calling, and tool use as part of the Baseten Inference Stack, making them ideal for agentic applications.
#### Reasoning Capabilities
DeepSeek models offer enhanced reasoning with step-by-step thought processes, while maintaining production-ready performance.
#### Long Context Support
- **Up to 1 million tokens** for Llama 4 models (Maverick and Scout)
- **262K tokens** for Qwen3 models
- **163K tokens** for DeepSeek models
- **Perfect for code repositories** and complex multi-turn conversations
#### Quantization Optimizations
Models are deployed with advanced quantization techniques (fp4, fp8, fp16) for optimal performance while maintaining quality.
### Migration from Other Providers
Baseten's OpenAI compatibility makes migration straightforward:
**From OpenAI:**
- Swap `api.openai.com` with `inference.baseten.co/v1`
- Keep existing request/response formats
- Benefit from significant cost savings
**From Other Providers:**
- Use standard OpenAI SDK format
- Maintain existing prompting strategies
- Access to newer open-source models
### Tips and Notes
- **Model Selection:** Choose models based on your specific use case - reasoning models for complex tasks, coding models for development work, and flagship models for general applications.
- **Cost Optimization:** Baseten offers some of the most competitive pricing in the market, especially for open-source models.
- **Context Windows:** Take advantage of large context windows (up to 1M tokens) for including substantial codebases and documentation.
- **Enterprise Ready:** Baseten is designed for production use with enterprise-grade security, compliance, and reliability.
- **Dynamic Model Updates:** Cline automatically fetches the latest model list from Baseten, ensuring access to new models as they're released.
- **Multi-Cloud Capacity Management (MCM):** Baseten's multi-cloud infrastructure ensures high availability and low latency globally.
- **Support:** Baseten provides dedicated support for production deployments and can work with you on dedicated resources as you scale.
### Pricing Information
Current pricing is highly competitive and transparent. For the most up-to-date pricing, visit the [Baseten Model APIs page](https://www.baseten.co/products/model-apis/). Prices typically range from \$0.10-\$6.00 per million tokens, making Baseten significantly more cost-effective than many closed-model alternatives while providing access to state-of-the-art open-source models.
+18 -13
View File
@@ -6,7 +6,7 @@ import * as esbuild from "esbuild"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const production = process.argv.includes("--production")
const production = process.argv.includes("--production") || process.env["IS_DEBUG_BUILD"] === "false"
const watch = process.argv.includes("--watch")
const standalone = process.argv.includes("--standalone")
const e2eBuild = process.argv.includes("--e2e-build")
@@ -123,24 +123,29 @@ const copyWasmFiles = {
},
}
const buildEnvVars = { "import.meta.url": "_importMetaUrl" }
if (production) {
// IS_DEV is always disable in production builds.
buildEnvVars["process.env.IS_DEV"] = "false"
}
// Set the environment and telemetry env vars. The API key env vars need to be populated in the GitHub
// workflows from the secrets.
if (process.env.CLINE_ENVIRONMENT) {
buildEnvVars["process.env.CLINE_ENVIRONMENT"] = JSON.stringify(process.env.CLINE_ENVIRONMENT)
}
if (process.env.TELEMETRY_SERVICE_API_KEY) {
buildEnvVars["process.env.TELEMETRY_SERVICE_API_KEY"] = JSON.stringify(process.env.TELEMETRY_SERVICE_API_KEY)
}
if (process.env.ERROR_SERVICE_API_KEY) {
buildEnvVars["process.env.ERROR_SERVICE_API_KEY"] = JSON.stringify(process.env.ERROR_SERVICE_API_KEY)
}
// Base configuration shared between extension and standalone builds
const baseConfig = {
bundle: true,
minify: production,
sourcemap: !production,
logLevel: "silent",
define: production
? {
"import.meta.url": "_importMetaUrl",
"process.env.IS_DEV": JSON.stringify(!production),
...(process.env.TELEMETRY_SERVICE_API_KEY && process.env.ERROR_SERVICE_API_KEY
? {
"process.env.TELEMETRY_SERVICE_API_KEY": JSON.stringify(process.env.TELEMETRY_SERVICE_API_KEY),
"process.env.ERROR_SERVICE_API_KEY": JSON.stringify(process.env.ERROR_SERVICE_API_KEY),
}
: {}),
}
: { "import.meta.url": "_importMetaUrl" },
define: buildEnvVars,
tsconfig: path.resolve(__dirname, "tsconfig.json"),
plugins: [
copyWasmFiles,
+82 -31
View File
@@ -9,9 +9,9 @@
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"axios": "^1.8.2",
"axios": "^1.12.0",
"better-sqlite3": "^11.10.0",
"chalk": "^4.1.2",
"chalk": "5.6.2",
"commander": "^9.4.1",
"dotenv": "^16.5.0",
"execa": "^5.1.1",
@@ -200,12 +200,13 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -292,15 +293,12 @@
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": ">=10"
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
@@ -607,9 +605,10 @@
}
},
"node_modules/form-data": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -708,6 +707,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -843,6 +843,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-symbols/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -1003,6 +1019,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -1251,6 +1283,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -1635,12 +1668,12 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -1695,13 +1728,9 @@
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="
},
"chownr": {
"version": "1.1.4",
@@ -1909,9 +1938,9 @@
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
},
"form-data": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -2054,6 +2083,17 @@
"requires": {
"chalk": "^4.1.0",
"is-unicode-supported": "^0.1.0"
},
"dependencies": {
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
}
}
},
"make-error": {
@@ -2164,6 +2204,17 @@
"log-symbols": "^4.1.0",
"strip-ansi": "^6.0.0",
"wcwidth": "^1.0.1"
},
"dependencies": {
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
}
}
},
"path-key": {
+2 -2
View File
@@ -19,9 +19,9 @@
"author": "",
"license": "MIT",
"dependencies": {
"axios": "^1.8.2",
"axios": "^1.12.0",
"better-sqlite3": "^11.10.0",
"chalk": "^4.1.2",
"chalk": "5.6.2",
"dotenv": "^16.5.0",
"commander": "^9.4.1",
"execa": "^5.1.1",
+826 -415
View File
File diff suppressed because it is too large Load Diff
+56 -8
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.28.4",
"version": "3.32.3",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -200,6 +200,11 @@
"command": "cline.openWalkthrough",
"title": "Open Walkthrough",
"category": "Cline"
},
{
"command": "cline.reconstructTaskHistory",
"title": "Reconstruct Task History",
"category": "Cline"
}
],
"keybindings": [
@@ -360,8 +365,10 @@
"test:sca-server": "npx tsx watch scripts/test-standalone-core-api-server.ts",
"test:tp-orchestrator": "npx tsx scripts/testing-platform-orchestrator.ts",
"e2e": "playwright test -c playwright.config.ts",
"test:e2e": "playwright install && vsce package --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test",
"test:e2e:optimal": "vsce package --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test",
"test:e2e:build": "vsce package --allow-package-secrets sendgrid --out dist/e2e.vsix",
"test:e2e": "playwright install && npm run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
"test:e2e:optimal": "npm run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
"test:e2e:ui": "npx tsx scripts/interactive-playwright.ts",
"install:all": "npm install && cd webview-ui && npm install",
"dev:webview": "cd webview-ui && npm run dev",
"build:webview": "cd webview-ui && npm run build",
@@ -402,13 +409,16 @@
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.6.0",
"archiver": "^7.0.1",
"c8": "^10.1.3",
"chai": "^4.3.10",
"chalk": "^5.3.0",
"chalk": "5.6.2",
"esbuild": "^0.25.0",
"glob": "^11.0.3",
"grpc-tools": "^1.13.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
"minimatch": "^3.0.3",
"minimatch": "^3.1.2",
"npm-run-all": "^4.1.5",
"prebuild-install": "^7.1.3",
"protoc-gen-ts": "^0.8.7",
@@ -416,6 +426,8 @@
"rimraf": "^6.0.1",
"should": "^13.2.3",
"sinon": "^19.0.2",
"tar": "^7.5.1",
"tree-kill": "^1.2.2",
"ts-node": "^10.9.2",
"ts-proto": "^2.6.1",
"tsconfig-paths": "^4.2.0",
@@ -447,8 +459,8 @@
"@streamparser/json": "^0.0.22",
"@types/uuid": "^10.0.0",
"@vscode/codicons": "^0.0.36",
"archiver": "^7.0.1",
"axios": "^1.8.2",
"axios": "^1.12.0",
"better-sqlite3": "^12.4.1",
"cheerio": "^1.0.0",
"chokidar": "^4.0.1",
"chrome-launcher": "^1.1.2",
@@ -463,6 +475,7 @@
"get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"grpc-health-check": "^2.0.2",
"https-proxy-agent": "^7.0.6",
"iconv-lite": "^0.6.3",
"ignore": "^7.0.3",
"image-size": "^2.0.2",
@@ -485,7 +498,7 @@
"reconnecting-eventsource": "^1.6.4",
"serialize-error": "^11.0.3",
"simple-git": "^3.27.0",
"strip-ansi": "^7.1.0",
"strip-ansi": "^7.1.2",
"tree-sitter-wasms": "^0.1.11",
"ts-morph": "^25.0.1",
"turndown": "^7.2.0",
@@ -494,5 +507,40 @@
"vscode-uri": "^3.1.0",
"web-tree-sitter": "^0.22.6",
"zod": "^3.24.2"
},
"c8": {
"reporter": [
"lcov",
"html"
],
"exclude": [
"**/testing-platform/**",
"**/webview-ui/**",
"**/.vscode-test/**",
"**/node_modules/**",
"node_modules",
"**/dist-standalone/src/**",
"**/dist-standalone/vsce-extension/https:/**",
"**/dist-standalone/vsce-extension/**",
"**/dist-standalone/https:/**",
"**/dist-standalone/LIB/src/**",
"**/dist-standalone/pdfjs-dist/**",
"**/*.d.ts",
"**/*.{test,spec}.{js,jsx,ts,tsx,mjs,cjs}",
"**/__tests__/**",
"**/test/**",
"**/tests/**",
"**/.nyc_output/**",
"**/tests-results/**",
"src/test/**",
"**/src/xml/**",
"**/standalone/**",
"**/src/generated/**",
"**/evals/cli/dist/**",
"**/evals/cli/src/**",
"dist"
],
"all": true,
"exclude-after-remap": true
}
}
+41
View File
@@ -0,0 +1,41 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
service DictationService {
rpc startRecording(EmptyRequest) returns (RecordingResult);
rpc stopRecording(EmptyRequest) returns (RecordedAudio);
rpc cancelRecording(EmptyRequest) returns (RecordingResult);
rpc getRecordingStatus(EmptyRequest) returns (RecordingStatus);
rpc transcribeAudio(TranscribeAudioRequest) returns (Transcription);
}
message TranscribeAudioRequest {
string audio_base64 = 2;
string language = 3;
}
message RecordingResult {
bool success = 1;
string error = 2;
}
message RecordedAudio {
bool success = 1;
string audio_base64 = 2;
string error = 3;
}
message RecordingStatus {
bool is_recording = 1;
double duration_seconds = 2;
string error = 3;
}
message Transcription {
string text = 1;
string error = 2;
}
+1 -1
View File
@@ -50,7 +50,7 @@ service FileService {
rpc refreshRules(EmptyRequest) returns (RefreshedRules);
// Opens a task's conversation history file on disk
rpc openTaskHistory(StringRequest) returns (Empty);
rpc openDiskConversationHistory(StringRequest) returns (Empty);
// Toggles a workflow on or off
rpc toggleWorkflow(ToggleWorkflowRequest) returns (ClineRulesToggles);
+52 -3
View File
@@ -33,6 +33,8 @@ service ModelsService {
rpc refreshBasetenModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Fetches available models from SAP AI Core
rpc getSapAiCoreModels(SapAiCoreModelsRequest) returns (SapAiCoreModelsResponse);
// Fetches available models from OCA
rpc refreshOcaModels(StringRequest) returns (OcaCompatibleModelInfo);
}
// List of VS Code LM models
@@ -127,6 +129,48 @@ message UpdateApiConfigurationRequest {
ModelsApiConfiguration api_configuration = 2;
}
// Model info for OCA (OpenAI-compatible) models exposed by the OCA provider
message OcaModelInfo {
// Maximum completion tokens per request supported by this model
optional int64 max_tokens = 1;
// Total context window in tokens (input + output)
optional int64 context_window = 2;
// Whether the model supports image inputs
optional bool supports_images = 3;
// Whether prompt caching is supported for this model
bool supports_prompt_cache = 4;
// Price per million input tokens (USD unless otherwise specified by provider)
optional double input_price = 5;
// Price per million output tokens (USD unless otherwise specified by provider)
optional double output_price = 6;
// Thinking/reasoning configuration if the model supports it
optional ThinkingConfig thinking_config = 7;
// Price per million tokens for prompt cache writes
optional double cache_writes_price = 9;
// Price per million tokens for prompt cache reads
optional double cache_reads_price = 10;
// Human-readable model description
optional string description = 11;
// Recommended default temperature for this model
optional double temperature = 13;
// Optional survey content to display in the UI
optional string survey_content = 14;
// Identifier for the survey associated with this model
optional string survey_id = 15;
// Optional banner content (e.g., deprecation or promotion notes)
optional string banner = 16;
// Canonical model identifier as reported by OCA
string model_name = 17;
}
// Aggregated OCA model catalog keyed by model identifier
message OcaCompatibleModelInfo {
// key: canonical model id as reported by OCA (e.g., "openai/gpt-4o-mini")
// value: OcaModelInfo describing that model
map<string, OcaModelInfo> models = 1;
optional string error = 2;
}
// API Provider enumeration
enum ApiProvider {
ANTHROPIC = 0;
@@ -164,6 +208,7 @@ enum ApiProvider {
VERCEL_AI_GATEWAY = 32;
QWEN_CODE = 33;
DIFY = 34;
OCA = 35;
}
// Model info for OpenAI-compatible models
@@ -276,6 +321,9 @@ message ModelsApiConfiguration {
optional string qwen_code_oauth_path = 70;
optional string dify_api_key = 71;
optional string dify_base_url = 72;
optional string oca_base_url = 73;
optional string oca_api_key = 74;
optional string oca_refresh_token = 75;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
@@ -309,6 +357,8 @@ message ModelsApiConfiguration {
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;
optional string plan_mode_oca_model_id = 131;
optional OcaModelInfo plan_mode_oca_model_info = 132;
// Act mode configurations
@@ -343,7 +393,6 @@ message ModelsApiConfiguration {
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;
optional string act_mode_oca_model_id = 231;
optional OcaModelInfo act_mode_oca_model_info = 232;
}
+36
View File
@@ -0,0 +1,36 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
// Service for account-related operations
service OcaAccountService {
// Handles the user clicking the login link in the UI.
// Generates a secure nonce for state validation, stores it in secrets,
// and opens the authentication URL in the external browser.
rpc ocaAccountLoginClicked(EmptyRequest) returns (String);
// Handles the user clicking the logout button in the UI.
// Clears API keys and user state.
rpc ocaAccountLogoutClicked(EmptyRequest) returns (Empty);
// Subscribe to auth status update events (when authentication state changes)
rpc ocaSubscribeToAuthStatusUpdate(EmptyRequest)
returns (stream OcaAuthState);
}
message OcaAuthState {
optional OcaUserInfo user = 1;
optional string api_key = 2;
}
// User's information
message OcaUserInfo {
string uid = 1;
optional string display_name = 2;
optional string email = 3;
}
+51 -30
View File
@@ -1,6 +1,7 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
import "cline/models.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
@@ -17,8 +18,14 @@ service StateService {
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
rpc updateInfoBannerVersion(Int64Request) returns (Empty);
rpc getProcessInfo(EmptyRequest) returns (ProcessInfo);
}
message DictationSettings {
bool feature_enabled = 1;
bool dictation_enabled = 2;
string dictation_language = 3;
}
message State {
string state_json = 1;
}
@@ -139,6 +146,9 @@ message UpdateSettingsRequest {
optional BrowserSettingsUpdate browser_settings = 20;
optional string default_terminal_profile = 21;
optional bool yolo_mode_toggled = 22;
optional DictationSettings dictation_settings = 23;
optional int32 auto_condense_threshold = 24;
optional bool multi_root_enabled = 25;
}
// Complete API Configuration message
@@ -150,10 +160,10 @@ message ApiConfiguration {
optional string lite_llm_base_url = 4;
optional string lite_llm_api_key = 5;
optional bool lite_llm_use_prompt_cache = 6;
optional string openai_headers = 7; // JSON string
map<string, string> open_ai_headers = 7;
optional string anthropic_base_url = 8;
optional string openrouter_api_key = 9;
optional string openrouter_provider_sorting = 10;
optional string open_router_api_key = 9;
optional string open_router_provider_sorting = 10;
optional string aws_access_key = 11;
optional string aws_secret_key = 12;
optional string aws_session_token = 13;
@@ -166,14 +176,14 @@ message ApiConfiguration {
optional string claude_code_path = 20;
optional string vertex_project_id = 21;
optional string vertex_region = 22;
optional string openai_base_url = 23;
optional string openai_api_key = 24;
optional string open_ai_base_url = 23;
optional string open_ai_api_key = 24;
optional string ollama_base_url = 25;
optional string ollama_api_options_ctx_num = 26;
optional string lm_studio_base_url = 27;
optional string gemini_api_key = 28;
optional string gemini_base_url = 29;
optional string openai_native_api_key = 30;
optional string open_ai_native_api_key = 30;
optional string deep_seek_api_key = 31;
optional string requesty_api_key = 32;
optional string requesty_base_url = 33;
@@ -209,61 +219,65 @@ message ApiConfiguration {
optional string qwen_code_oauth_path = 63;
optional string dify_api_key = 64;
optional string dify_base_url = 65;
optional string oca_base_url = 66;
optional string oca_api_key = 67;
optional string oca_refresh_token = 68;
// Plan mode configurations
optional string plan_mode_api_provider = 100;
optional ApiProvider plan_mode_api_provider = 100;
optional string plan_mode_api_model_id = 101;
optional int32 plan_mode_thinking_budget_tokens = 102;
optional string plan_mode_reasoning_effort = 103;
optional string plan_mode_vscode_lm_model_selector = 104; // JSON string
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 104;
optional bool plan_mode_aws_bedrock_custom_selected = 105;
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
optional string plan_mode_openrouter_model_id = 107;
optional string plan_mode_openrouter_model_info = 108; // JSON string
optional string plan_mode_openai_model_id = 109;
optional string plan_mode_openai_model_info = 110; // JSON string
optional string plan_mode_open_router_model_id = 107;
optional OpenRouterModelInfo plan_mode_open_router_model_info = 108;
optional string plan_mode_open_ai_model_id = 109;
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 110;
optional string plan_mode_ollama_model_id = 111;
optional string plan_mode_lm_studio_model_id = 112;
optional string plan_mode_lite_llm_model_id = 113;
optional string plan_mode_lite_llm_model_info = 114; // JSON string
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 114;
optional string plan_mode_requesty_model_id = 115;
optional string plan_mode_requesty_model_info = 116; // JSON string
optional OpenRouterModelInfo plan_mode_requesty_model_info = 116;
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_huawei_cloud_maas_model_id = 120;
optional string plan_mode_huawei_cloud_maas_model_info = 121;
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 121;
optional string plan_mode_vercel_ai_gateway_model_id = 122;
optional string plan_mode_vercel_ai_gateway_model_info = 123;
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 123;
optional string plan_mode_oca_model_id = 124;
optional OcaModelInfo plan_mode_oca_model_info = 125;
// Act mode configurations
optional string act_mode_api_provider = 200;
optional ApiProvider act_mode_api_provider = 200;
optional string act_mode_api_model_id = 201;
optional int32 act_mode_thinking_budget_tokens = 202;
optional string act_mode_reasoning_effort = 203;
optional string act_mode_vscode_lm_model_selector = 204; // JSON string
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 204;
optional bool act_mode_aws_bedrock_custom_selected = 205;
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
optional string act_mode_openrouter_model_id = 207;
optional string act_mode_openrouter_model_info = 208; // JSON string
optional string act_mode_openai_model_id = 209;
optional string act_mode_openai_model_info = 210; // JSON string
optional string act_mode_open_router_model_id = 207;
optional OpenRouterModelInfo act_mode_open_router_model_info = 208;
optional string act_mode_open_ai_model_id = 209;
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 210;
optional string act_mode_ollama_model_id = 211;
optional string act_mode_lm_studio_model_id = 212;
optional string act_mode_lite_llm_model_id = 213;
optional string act_mode_lite_llm_model_info = 214; // JSON string
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 214;
optional string act_mode_requesty_model_id = 215;
optional string act_mode_requesty_model_info = 216; // JSON string
optional OpenRouterModelInfo act_mode_requesty_model_info = 216;
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_huawei_cloud_maas_model_id = 220;
optional string act_mode_huawei_cloud_maas_model_info = 221;
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 221;
optional string act_mode_vercel_ai_gateway_model_id = 222;
optional string act_mode_vercel_ai_gateway_model_info = 223;
// Favorited model IDs
repeated string favorited_model_ids = 300;
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 223;
optional string act_mode_oca_model_id = 224;
optional OcaModelInfo act_mode_oca_model_info = 225;
// Extension fields for Bedrock Api Keys
optional string aws_authentication = 301;
@@ -289,3 +303,10 @@ message Viewport {
message UpdateTerminalConnectionTimeoutResponse {
optional int32 timeout_ms = 1;
}
message ProcessInfo {
int32 process_id = 1;
optional string version = 2;
optional int64 uptime_ms = 3;
}
Binary file not shown.
+3
View File
@@ -31,6 +31,9 @@ service EnvService {
// Returns events when the telemetry settings change.
rpc subscribeToTelemetrySettings(cline.EmptyRequest) returns (stream TelemetrySettingsEvent);
// Initiates a graceful shutdown of the host bridge service.
rpc shutdown(cline.EmptyRequest) returns (cline.Empty);
}
message GetHostVersionResponse {
+5 -3
View File
@@ -27,6 +27,9 @@ service WorkspaceService {
// Opens and focuses the Cline sidebar panel in the host IDE.
rpc openClineSidebarPanel(OpenClineSidebarPanelRequest) returns (OpenClineSidebarPanelResponse);
// Opens and focuses the terminal panel.
rpc openTerminalPanel(OpenTerminalRequest) returns (OpenTerminalResponse);
}
message GetWorkspacePathsRequest {
@@ -82,12 +85,11 @@ message SearchWorkspaceItemsResponse {
message OpenProblemsPanelRequest {}
message OpenProblemsPanelResponse {}
message OpenInFileExplorerPanelRequest {
string path = 1;
}
message OpenInFileExplorerPanelResponse {}
// Request/response for opening the Cline sidebar
message OpenClineSidebarPanelRequest {}
message OpenClineSidebarPanelResponse {}
message OpenTerminalRequest {}
message OpenTerminalResponse {}
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env npx tsx
/**
* Interactive Playwright launcher for the Cline VS Code extension.
*
* Overview:
* - Starts the mock Cline API server (from the e2e test fixtures).
* - Downloads a stable build of VS Code (via @vscode/test-electron).
* - Creates a temporary VS Code user profile directory.
* - Installs and links the Cline extension (from dist/e2e.vsix and the dev path).
* - Opens a test workspace and automatically reveals the Cline sidebar.
* - Records **all gRPC calls** during the session for later inspection.
* - Keeps VS Code running for manual interactive testing until the window is closed or Ctrl+C is pressed.
* - Cleans up all resources (mock server, temp profile, Electron process) on exit.
*
* Usage:
* 1. (Optional) Build and install the e2e extension:
* npm run test:e2e:build
*
* 2. From the repo root, start the interactive session:
* npm run test:playwright:interactive
*
* 3. VS Code will launch with the Cline extension loaded and gRPC recording enabled.
*
* 4. Interact with the extension manually.
*
* 5. Close the VS Code window or press Ctrl+C to end the session and trigger cleanup.
*/
import { downloadAndUnzipVSCode, SilentReporter } from "@vscode/test-electron"
import { mkdtempSync } from "fs"
import os from "os"
import path from "path"
import { _electron } from "playwright"
import { ClineApiServerMock } from "../src/test/e2e/fixtures/server"
import { E2ETestHelper } from "../src/test/e2e/utils/helpers"
async function main() {
await ClineApiServerMock.startGlobalServer()
const userDataDir = mkdtempSync(path.join(os.tmpdir(), "vsce-interactive"))
const executablePath = await downloadAndUnzipVSCode("stable", undefined, new SilentReporter())
// launch VSCode
const app = await _electron.launch({
executablePath,
env: {
...process.env,
TEMP_PROFILE: "true",
E2E_TEST: "true",
CLINE_ENVIRONMENT: "local",
GRPC_RECORDER_ENABLED: "true",
GRPC_RECORDER_TESTS_FILTERS_ENABLED: "true",
},
args: [
"--no-sandbox",
"--disable-updates",
"--disable-workspace-trust",
"--disable-extensions",
"--skip-welcome",
"--skip-release-notes",
`--user-data-dir=${userDataDir}`,
`--install-extension=${path.join(E2ETestHelper.CODEBASE_ROOT_DIR, "dist", "e2e.vsix")}`,
`--extensionDevelopmentPath=${E2ETestHelper.CODEBASE_ROOT_DIR}`,
path.join(E2ETestHelper.E2E_TESTS_DIR, "fixtures", "workspace"),
],
})
const page = await app.firstWindow()
await E2ETestHelper.openClineSidebar(page)
console.log("VSCode with Cline extension is now running!")
console.log(`Temporary data directory on: ${userDataDir}`)
console.log("You can manually interact with the extension.")
console.log("Press Ctrl+C to close when done.")
async function teardown() {
console.log("Cleaning up resources...")
try {
await app?.close()
await ClineApiServerMock.stopGlobalServer?.()
await E2ETestHelper.rmForRetries(userDataDir, { recursive: true })
} catch (e) {
console.log(`We could teardown interactive playwright properly, error:${e}`)
}
console.log("Finished cleaning up resources...")
}
process.on("SIGINT", async () => {
await teardown()
process.exit(0)
})
process.on("SIGTERM", async () => {
await teardown()
process.exit(0)
})
const win = await app.firstWindow()
win.on("close", async () => {
console.log("VS Code window closed.")
await teardown()
process.exit(0)
})
process.stdin.resume()
}
main().catch((err) => {
console.error("Failed to start:", err)
process.exit(1)
})
+3 -1
View File
@@ -9,7 +9,7 @@ if [[ "${1:-}" == "-h" ]]; then
fi
CORE_DIR=~/.cline/core
INSTALL_DIR=$CORE_DIR/0.0.1
INSTALL_DIR=$CORE_DIR/dev-instance/
LOG_FILE=~/.cline/cline-core-service.log
ZIP_FILE=standalone.zip
@@ -25,4 +25,6 @@ unp $ZIP_FILE > /dev/null
pkill -f cline-core.js || true
echo pwd: $(pwd)
set -x
NODE_PATH=./node_modules DEV_WORKSPACE_FOLDER=/tmp/ node cline-core.js 2>&1 | tee $LOG_FILE
+40 -35
View File
@@ -28,19 +28,19 @@
* Ideal for local development, testing, or lightweight E2E scenarios.
*/
import * as fs from "node:fs"
import { mkdtempSync, rmSync } from "node:fs"
import * as os from "node:os"
import { ChildProcess, execSync, 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"
const USE_C8 = process.env.USE_C8 === "true"
// Locate the standalone build directory and core file with flexible path resolution
const projectRoot = process.env.PROJECT_ROOT || path.resolve(__dirname, "..")
@@ -48,8 +48,11 @@ const distDir = process.env.CLINE_DIST_DIR || path.join(projectRoot, "dist-stand
const clineCoreFile = process.env.CLINE_CORE_FILE || "cline-core.js"
const coreFile = path.join(distDir, clineCoreFile)
const childProcesses: ChildProcess[] = []
async function main(): Promise<void> {
console.log("Starting Simple Cline gRPC Server...")
console.log(`Project Root: ${projectRoot}`)
console.log(`Workspace: ${WORKSPACE_DIR}`)
console.log(`ProtoBus Port: ${PROTOBUS_PORT}`)
console.log(`HostBridge Port: ${HOSTBRIDGE_PORT}`)
@@ -75,27 +78,24 @@ async function main(): Promise<void> {
process.exit(1)
}
// Create temporary directories like e2e tests
const extensionsDir = path.join(distDir, "vsce-extension")
const userDataDir = mkdtempSync(path.join(os.tmpdir(), "vsce"))
const extensionsDir = mkdtempSync(path.join(os.tmpdir(), "vsce"))
const clineTestWorkspace = mkdtempSync(path.join(os.tmpdir(), "cline-test-workspace-"))
// 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,
env: {
...process.env,
TEST_HOSTBRIDGE_WORKSPACE_DIR: clineTestWorkspace,
HOST_BRIDGE_ADDRESS: `127.0.0.1:${HOSTBRIDGE_PORT}`,
},
})
childProcesses.push(hostbridge)
console.log(`Temp user data dir: ${userDataDir}`)
console.log(`Temp extensions dir: ${extensionsDir}`)
// Extract standalone.zip to the extensions directory
// Extract standalone.zip if needed
const standaloneZipPath = path.join(distDir, "standalone.zip")
if (!fs.existsSync(standaloneZipPath)) {
console.error(`standalone.zip not found at: ${standaloneZipPath}`)
@@ -104,47 +104,56 @@ async function main(): Promise<void> {
console.log("Extracting standalone.zip to extensions directory...")
try {
execSync(`unzip -q "${standaloneZipPath}" -d "${extensionsDir}"`, { stdio: "inherit" })
if (!fs.existsSync(extensionsDir)) {
execSync(`unzip -q "${standaloneZipPath}" -d "${extensionsDir}"`, { stdio: "inherit" })
}
console.log(`Successfully extracted standalone.zip to: ${extensionsDir}`)
} catch (error) {
console.error("Failed to extract standalone.zip:", error)
process.exit(1)
}
// 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,
const covDir = path.join(projectRoot, `coverage/coverage-core-${PROTOBUS_PORT}`)
const baseArgs = ["--enable-source-maps", path.join(distDir, "cline-core.js")]
const spawnArgs = USE_C8 ? ["c8", "--report-dir", covDir, "node", ...baseArgs] : ["node", ...baseArgs]
console.log(`Starting Cline Core Service... (useC8=${USE_C8})`)
const coreService: ChildProcess = spawn("npx", spawnArgs, {
cwd: projectRoot,
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,
E2E_TEST,
CLINE_ENVIRONMENT,
CLINE_DIR: userDataDir,
INSTALL_DIR: extensionsDir,
},
stdio: "inherit",
})
childProcesses.push(coreService)
const shutdown = async () => {
console.log("\nShutting down services...")
while (childProcesses.length > 0) {
const child = childProcesses.pop()
if (child && !child.killed) child.kill("SIGINT")
}
// Handle graceful shutdown
const shutdown = async (): Promise<void> => {
console.log(`\n Shutting down services...\n${userDataDir}\n${extensionsDir}\n${clineTestWorkspace}\n`)
hostbridge.kill()
coreService.kill()
await ClineApiServerMock.stopGlobalServer()
// Cleanup temp directories
try {
rmSync(userDataDir, { recursive: true, force: true })
rmSync(extensionsDir, { recursive: true, force: true })
rmSync(clineTestWorkspace, { recursive: true, force: true })
console.log("Cleaned up temporary directories")
} catch (error) {
console.warn("Failed to cleanup temp directories:", error)
} catch (err) {
console.warn("Failed to cleanup temp directories:", err)
}
process.exit(0)
@@ -155,24 +164,20 @@ async function main(): Promise<void> {
coreService.on("exit", (code) => {
console.log(`Core service exited with code ${code}`)
hostbridge.kill()
process.exit(code || 0)
shutdown()
})
hostbridge.on("exit", (code) => {
console.log(`HostBridge exited with code ${code}`)
coreService.kill()
process.exit(code || 0)
shutdown()
})
console.log("Cline gRPC Server is running!")
console.log(`Connect to: 127.0.0.1:${PROTOBUS_PORT}`)
console.log(`Cline gRPC Server is running on 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)
main().catch((err) => {
console.error("Failed to start simple Cline server:", err)
process.exit(1)
})
}
+102 -36
View File
@@ -14,61 +14,124 @@
* Flags:
* --server-logs Show server logs (hidden by default)
* --count=<number> Repeat execution N times (default: 1)
* --fix Automatically update spec files with actual responses
* --coverage Generate integration test coverage information
*
* Environment Variables:
* HOSTBRIDGE_PORT gRPC server port (default: 26040)
* SERVER_BOOT_DELAY Server startup delay in ms (default: 3000)
*/
import { ChildProcess, spawn } from "child_process"
import fs from "fs"
import minimist from "minimist"
import net from "net"
import path from "path"
const STANDALONE_GRPC_SERVER_PORT = process.env.STANDALONE_GRPC_SERVER_PORT || "26040"
const SERVER_BOOT_DELAY = Number(process.env.SERVER_BOOT_DELAY) || 3000
import kill from "tree-kill"
let showServerLogs = false
let fix = false
let coverage = false
const WAIT_SERVER_DEFAULT_TIMEOUT = 15000
const usedPorts = new Set<number>()
function startServer(): Promise<ChildProcess> {
return new Promise((resolve, reject) => {
const server = spawn("npx", ["tsx", "scripts/test-standalone-core-api-server.ts"], {
stdio: showServerLogs ? "inherit" : "ignore",
})
server.once("error", reject)
setTimeout(() => {
if (server.killed) {
reject(new Error("Server died during startup"))
} else {
resolve(server)
/**
* Find an available TCP port within the given range [min, max].
*
* - Ports are allocated sequentially (starting at `min`) rather than randomly,
* which avoids accidental reuse when running hundreds of tests in a row.
* - Each successfully allocated port is tracked in `usedPorts` to guarantee
* it is never handed out again within the lifetime of this orchestrator.
* - Before returning, the function binds a temporary server to the port to
* verify that the OS really considers it available, then immediately closes it.
*
* This approach makes the orchestrator much more robust on CI (e.g. GitHub Actions),
* where a just-terminated server may leave its socket in TIME_WAIT and cause
* flakiness if the same port is reallocated too soon.
*/
async function getAvailablePort(min = 20000, max = 49151): Promise<number> {
return new Promise((resolve, _) => {
const tryPort = (candidate?: number) => {
const port = candidate ?? Math.floor(Math.random() * (max - min + 1)) + min
if (usedPorts.has(port)) {
// already allocated in this run
return tryPort()
}
}, SERVER_BOOT_DELAY)
const server = net.createServer()
server.once("error", () => tryPort())
server.once("listening", () => {
server.close(() => {
usedPorts.add(port) // mark reserved
resolve(port)
})
})
server.listen(port, "127.0.0.1")
}
tryPort()
})
}
// Poll until a given TCP port on a host is accepting connections.
async function waitForPort(port: number, host = "127.0.0.1", timeout = 10000): Promise<void> {
const start = Date.now()
const waitForPortSleepMs = 100
while (Date.now() - start < timeout) {
await new Promise((res) => setTimeout(res, waitForPortSleepMs))
try {
await new Promise<void>((resolve, reject) => {
const socket = net.connect(port, host, () => {
socket.destroy()
resolve()
})
socket.on("error", reject)
})
return
} catch {
// try again
}
}
throw new Error(`Timeout waiting for ${host}:${port}`)
}
async function startServer(): Promise<{ server: ChildProcess; grpcPort: string }> {
const grpcPort = (await getAvailablePort()).toString()
const hostbridgePort = (await getAvailablePort()).toString()
const server = spawn("npx", ["tsx", "scripts/test-standalone-core-api-server.ts"], {
stdio: showServerLogs ? "inherit" : "pipe",
env: {
...process.env,
PROTOBUS_PORT: grpcPort,
HOSTBRIDGE_PORT: hostbridgePort,
USE_C8: coverage ? "true" : "false",
},
})
// Wait for either the server to become ready or fail on spawn error
await Promise.race([
waitForPort(Number(grpcPort), "127.0.0.1", WAIT_SERVER_DEFAULT_TIMEOUT),
new Promise((_, reject) => server.once("error", reject)),
])
return { server, grpcPort }
}
function stopServer(server: ChildProcess): Promise<void> {
return new Promise((resolve) => {
server.once("exit", () => resolve())
server.kill("SIGINT")
setTimeout(() => {
if (!server.killed) {
server.kill("SIGKILL")
resolve()
}
}, 5000)
if (!server.pid) return resolve()
kill(server.pid, "SIGINT", (err) => {
if (err) console.warn("Failed to kill server process:", err)
server.once("exit", () => resolve())
})
})
}
function runTestingPlatform(specFile: string): Promise<void> {
function runTestingPlatform(specFile: string, grpcPort: string): Promise<void> {
return new Promise((resolve, reject) => {
const testProcess = spawn("npx", ["ts-node", "index.ts", specFile], {
const testProcess = spawn("npx", ["ts-node", "index.ts", specFile, ...(fix ? ["--fix"] : [])], {
cwd: path.join(process.cwd(), "testing-platform"),
stdio: "inherit",
env: {
...process.env,
HOSTBRIDGE_PORT: STANDALONE_GRPC_SERVER_PORT,
STANDALONE_GRPC_SERVER_PORT: grpcPort,
},
})
@@ -80,9 +143,9 @@ function runTestingPlatform(specFile: string): Promise<void> {
}
async function runSpec(specFile: string): Promise<void> {
const server = await startServer()
const { server, grpcPort } = await startServer()
try {
await runTestingPlatform(specFile)
await runTestingPlatform(specFile, grpcPort)
console.log(`${path.basename(specFile)} passed`)
} finally {
await stopServer(server)
@@ -133,8 +196,7 @@ async function runAll(inputPath: string, count: number) {
console.log(`✅ Passed: ${success}`)
if (failure > 0) console.log(`❌ Failed: ${failure}`)
console.log(`📋 Total specs: ${specFiles.length} Total runs: ${specFiles.length * count}`)
const totalElapsed = ((Date.now() - totalStart) / 1000).toFixed(2)
console.log(`\n🏁 All runs completed in ${totalElapsed}s`)
console.log(`🏁 All runs completed in ${((Date.now() - totalStart) / 1000).toFixed(2)}s`)
}
async function main() {
@@ -142,9 +204,13 @@ async function main() {
const inputPath = args._[0]
const count = Number(args.count)
showServerLogs = Boolean(args["server-logs"])
fix = Boolean(args["fix"])
coverage = Boolean(args["coverage"])
if (!inputPath) {
console.error("Usage: npx tsx scripts/testing-platform-orchestrator.ts <spec-file-or-folder> [--count=N] [--server-logs]")
console.error(
"Usage: npx tsx scripts/testing-platform-orchestrator.ts <spec-file-or-folder> [--count=N] [--server-logs] [--fix] [--coverage]",
)
process.exit(1)
}
+8 -3
View File
@@ -1,15 +1,20 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandlerOptions, ModelInfo } from "@shared/api"
import { ModelInfo } from "@shared/api"
import { ApiHandler } from "../../core/api/index"
import { ApiStream } from "../../core/api/transform/stream"
interface DifyHandlerOptions {
difyApiKey?: string
difyBaseUrl?: string
}
export class DifyHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: DifyHandlerOptions
private baseUrl: string
private apiKey: string
private conversationId: string | null = null
constructor(options: ApiHandlerOptions) {
constructor(options: DifyHandlerOptions) {
this.options = options
this.apiKey = options.difyApiKey || ""
this.baseUrl = options.difyBaseUrl || ""
+4
View File
@@ -13,6 +13,7 @@ import "./utils/path" // necessary to have access to String.prototype.toPosix
import { HostProvider } from "@/hosts/host-provider"
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
import { ExtensionRegistryInfo } from "./registry"
import { audioRecordingService } from "./services/dictation/AudioRecordingService"
import { ErrorService } from "./services/error"
import { featureFlagsService } from "./services/feature-flags"
import { initializeDistinctId } from "./services/logging/distinctId"
@@ -99,6 +100,9 @@ async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
* Performs cleanup when Cline is deactivated that is common to all platforms.
*/
export async function tearDown(): Promise<void> {
// Clean up audio recording service to ensure no orphaned processes
audioRecordingService.cleanup()
PostHogClientProvider.getInstance().dispose()
telemetryService.dispose()
ErrorService.get().dispose()
+1 -1
View File
@@ -4,7 +4,7 @@ export enum Environment {
local = "local",
}
interface EnvironmentConfig {
export interface EnvironmentConfig {
appBaseUrl: string
apiBaseUrl: string
mcpBaseUrl: string
+15 -4
View File
@@ -21,6 +21,7 @@ import { LmStudioHandler } from "./providers/lmstudio"
import { MistralHandler } from "./providers/mistral"
import { MoonshotHandler } from "./providers/moonshot"
import { NebiusHandler } from "./providers/nebius"
import { OcaHandler } from "./providers/oca"
import { OllamaHandler } from "./providers/ollama"
import { OpenAiHandler } from "./providers/openai"
import { OpenAiNativeHandler } from "./providers/openai-native"
@@ -57,6 +58,7 @@ export interface ApiProviderInfo {
providerId: string
model: ApiHandlerModel
customPrompt?: string // "compact"
autoCondenseThreshold?: number // 0-1 range
}
export interface SingleCompletionHandler {
@@ -352,10 +354,6 @@ function createHandlerForProvider(
mode === "plan" ? options.planModeHuaweiCloudMaasModelInfo : options.actModeHuaweiCloudMaasModelInfo,
})
case "dify": // Add Dify.ai handler
console.log("[DIFY DEBUG] Instantiating DifyHandler with options:", {
difyApiKeyPresent: !!options.difyApiKey,
difyBaseUrl: options.difyBaseUrl,
})
return new DifyHandler({
difyApiKey: options.difyApiKey,
difyBaseUrl: options.difyBaseUrl,
@@ -376,6 +374,19 @@ function createHandlerForProvider(
zaiApiKey: options.zaiApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "oca":
return new OcaHandler({
ocaBaseUrl: options.ocaBaseUrl,
ocaModelId: mode === "plan" ? options.planModeOcaModelId : options.actModeOcaModelId,
ocaModelInfo: mode === "plan" ? options.planModeOcaModelInfo : options.actModeOcaModelInfo,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
ocaUsePromptCache:
mode === "plan"
? options.planModeOcaModelInfo?.supportsPromptCache
: options.actModeOcaModelInfo?.supportsPromptCache,
taskId: options.ulid,
})
default:
return new AnthropicHandler({
onRetryAttempt: options.onRetryAttempt,
@@ -1,7 +1,7 @@
import "should"
import { ConverseStreamCommand } from "@aws-sdk/client-bedrock-runtime"
import { ApiHandlerOptions } from "@shared/api"
import { Readable } from "stream"
import type { AwsBedrockHandlerOptions } from "../bedrock"
import { AwsBedrockHandler } from "../bedrock"
describe("AwsBedrockHandler", () => {
@@ -202,8 +202,8 @@ describe("AwsBedrockHandler", () => {
})
})
const mockOptions: ApiHandlerOptions = {
actModeApiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
const mockOptions: AwsBedrockHandlerOptions = {
apiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
awsRegion: "us-east-1",
awsAccessKey: "test-key",
awsSecretKey: "test-secret",
@@ -214,9 +214,9 @@ describe("AwsBedrockHandler", () => {
awsBedrockUsePromptCache: false,
awsUseCrossRegionInference: false,
awsBedrockEndpoint: "",
actModeAwsBedrockCustomSelected: false,
actModeAwsBedrockCustomModelBaseId: undefined,
actModeThinkingBudgetTokens: 1600,
awsBedrockCustomSelected: false,
awsBedrockCustomModelBaseId: undefined,
thinkingBudgetTokens: 1600,
}
const mockModelInfo = {
+34 -7
View File
@@ -50,10 +50,14 @@ export class AnthropicHandler implements ApiHandler {
const enable1mContextWindow = model.id.endsWith(CLAUDE_SONNET_4_1M_SUFFIX)
const budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = !!((modelId.includes("3-7") || modelId.includes("4-")) && budget_tokens !== 0)
const reasoningOn = !!(
(modelId.includes("3-7") || modelId.includes("4-") || modelId.includes("4-5")) &&
budget_tokens !== 0
)
switch (modelId) {
// 'latest' alias does not support cache_control
case "claude-sonnet-4-5-20250929":
case "claude-sonnet-4-20250514":
case "claude-3-7-sonnet-20250219":
case "claude-3-5-sonnet-20241022":
@@ -150,6 +154,8 @@ export class AnthropicHandler implements ApiHandler {
}
}
let thinkingDeltaAccumulator = ""
for await (const chunk of stream) {
switch (chunk?.type) {
case "message_start":
@@ -182,14 +188,26 @@ export class AnthropicHandler implements ApiHandler {
type: "reasoning",
reasoning: chunk.content_block.thinking || "",
}
const thinking = chunk.content_block.thinking
const signature = chunk.content_block.signature
if (thinking && signature) {
yield {
type: "ant_thinking",
thinking,
signature,
}
}
break
case "redacted_thinking":
// Handle redacted thinking blocks - we still mark it as reasoning
// but note that the content is encrypted
// Content is encrypted, and we don't to pass placeholder text back to the API
yield {
type: "reasoning",
reasoning: "[Redacted thinking block]",
}
yield {
type: "ant_redacted_thinking",
data: chunk.content_block.data,
}
break
case "text":
// we may receive multiple text blocks, in which case just insert a line break between them
@@ -209,10 +227,23 @@ export class AnthropicHandler implements ApiHandler {
case "content_block_delta":
switch (chunk.delta.type) {
case "thinking_delta":
// 'reasoning' type just displays in the UI, but ant_thinking will be used to send the thinking traces back to the API
yield {
type: "reasoning",
reasoning: chunk.delta.thinking,
}
thinkingDeltaAccumulator += chunk.delta.thinking
break
case "signature_delta":
// It's used when sending the thinking block back to the API
// API expects this in completed form, not as array of deltas
if (thinkingDeltaAccumulator && chunk.delta.signature) {
yield {
type: "ant_thinking",
thinking: thinkingDeltaAccumulator,
signature: chunk.delta.signature,
}
}
break
case "text_delta":
yield {
@@ -220,10 +251,6 @@ export class AnthropicHandler implements ApiHandler {
text: chunk.delta.text,
}
break
case "signature_delta":
// We don't need to do anything with the signature in the client
// It's used when sending the thinking block back to the API
break
}
break
case "content_block_stop":
+6 -11
View File
@@ -146,20 +146,15 @@ export class BasetenHandler implements ApiHandler {
}
}
/**
* Checks if the current model supports vision/images
*/
supportsImages(): boolean {
const model = this.getModel()
return model.info.supportsImages === true
}
/**
* Checks if the current model supports tools
*/
supportsTools(): boolean {
const _model = this.getModel()
// Baseten models support tools via OpenAI-compatible API
return true
const model = this.getModel()
const modelInfo = model.info as any
// Use dynamic API data when available, fallback to true since all current Baseten models support tools
// (as of 2025-09-16 - could change if Baseten add non-tool models in future, currently no plans to do so)
return modelInfo.supportedFeatures ? modelInfo.supportedFeatures.includes("tools") : true
}
}
+7 -4
View File
@@ -16,7 +16,7 @@ import { withRetry } from "../retry"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
interface AwsBedrockHandlerOptions extends CommonApiHandlerOptions {
export interface AwsBedrockHandlerOptions extends CommonApiHandlerOptions {
apiModelId?: string
awsAccessKey?: string
awsSecretKey?: string
@@ -30,7 +30,7 @@ interface AwsBedrockHandlerOptions extends CommonApiHandlerOptions {
awsProfile?: string
awsBedrockEndpoint?: string
awsBedrockCustomSelected?: boolean
awsBedrockCustomModelBaseId?: BedrockModelId
awsBedrockCustomModelBaseId?: string
thinkingBudgetTokens?: number
}
@@ -170,7 +170,7 @@ export class AwsBedrockHandler implements ApiHandler {
if (baseModel && baseModel in bedrockModels) {
return {
id: modelId,
info: bedrockModels[baseModel],
info: bedrockModels[baseModel as BedrockModelId],
}
}
// For custom models without valid base model in bedrock model list, use default model's capabilities
@@ -741,7 +741,10 @@ export class AwsBedrockHandler implements ApiHandler {
*/
private shouldEnableReasoning(baseModelId: string, budgetTokens: number): boolean {
return (
(baseModelId.includes("3-7") || baseModelId.includes("sonnet-4") || baseModelId.includes("opus-4")) &&
(baseModelId.includes("3-7") ||
baseModelId.includes("sonnet-4") ||
baseModelId.includes("opus-4") ||
baseModelId.includes("sonnet-4-5")) &&
budgetTokens !== 0
)
}
+2 -3
View File
@@ -162,7 +162,7 @@ export class ClineHandler implements ApiHandler {
// @ts-ignore-next-line
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
if (this.getModel().id === "cline/sonic") {
if (this.getModel().id === "cline/code-supernova-1-million") {
totalCost = 0
}
@@ -200,14 +200,13 @@ export class ClineHandler implements ApiHandler {
async getApiStreamUsage(): Promise<ApiStreamUsageChunk | undefined> {
if (this.lastGenerationId) {
try {
// TODO: replace this with firebase auth
// TODO: use global API Host
const clineAccountAuthToken = await this._authService.getAuthToken()
if (!clineAccountAuthToken) {
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE)
}
const response = await axios.get(`${this.clineAccountService.baseUrl}/generation?id=${this.lastGenerationId}`, {
headers: {
// Align with backend auth expectations
Authorization: `Bearer ${clineAccountAuthToken}`,
},
timeout: 15_000, // this request hangs sometimes
+8 -3
View File
@@ -1,8 +1,13 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandlerOptions, ModelInfo } from "../../../shared/api"
import { ModelInfo } from "../../../shared/api"
import { ApiHandler } from "../index"
import { ApiStream } from "../transform/stream"
interface DifyHandlerOptions {
difyApiKey?: string
difyBaseUrl?: string
}
// Dify API Response Types
export interface DifyFileResponse {
id: string
@@ -66,14 +71,14 @@ interface DifyConversationResponse {
}
export class DifyHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: DifyHandlerOptions
private baseUrl: string
private apiKey: string
private conversationId: string | null = null
private currentTaskId: string | null = null
private abortController: AbortController | null = null
constructor(options: ApiHandlerOptions) {
constructor(options: DifyHandlerOptions) {
this.options = options
this.apiKey = options.difyApiKey || ""
this.baseUrl = options.difyBaseUrl || ""
+1
View File
@@ -49,6 +49,7 @@ export class MoonshotHandler implements ApiHandler {
model: model.id,
messages: openAiMessages,
temperature: 0,
max_tokens: model.info.maxTokens,
stream: true,
stream_options: { include_usage: true },
})
+261
View File
@@ -0,0 +1,261 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
import OpenAI, { APIError, OpenAIError } from "openai"
import type { FinalRequestOptions, Headers as OpenAIHeaders } from "openai/core"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
import { DEFAULT_OCA_BASE_URL, OCI_HEADER_OPC_REQUEST_ID } from "@/services/auth/oca/utils/constants"
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
import { Logger } from "@/services/logging/Logger"
import { ApiHandler, type CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
export interface OcaHandlerOptions extends CommonApiHandlerOptions {
ocaBaseUrl?: string
ocaModelId?: string
ocaModelInfo?: LiteLLMModelInfo
thinkingBudgetTokens?: number
ocaUsePromptCache?: boolean
taskId?: string
}
export class OcaHandler implements ApiHandler {
protected options: OcaHandlerOptions
protected client: OpenAI | undefined
constructor(options: OcaHandlerOptions) {
this.options = options
}
protected initializeClient(options: OcaHandlerOptions) {
return new (class OCIOpenAI extends OpenAI {
protected override async prepareOptions(opts: FinalRequestOptions<unknown>): Promise<void> {
const token = await OcaAuthService.getInstance().getAuthToken()
if (!token) {
throw new OpenAIError("Unable to handle auth, Oracle Code Assist (OCA) access token is not available")
}
opts.headers ??= {}
// OCA Headers
const ociHeaders = await createOcaHeaders(token, options.taskId!)
opts.headers = { ...opts.headers, ...ociHeaders }
Logger.log(`Making request with customer opc-request-id: ${opts.headers?.["opc-request-id"]}`)
return super.prepareOptions(opts)
}
protected override makeStatusError(
status: number | undefined,
error: Object | undefined,
message: string | undefined,
headers: OpenAIHeaders | undefined,
): APIError {
interface OciError {
code?: string
message?: string
}
let ociErrorMessage = message
if (typeof error === "object" && error !== null) {
try {
ociErrorMessage = JSON.stringify(error)
const ociErr = error as OciError
if (ociErr.code !== undefined && ociErr.message !== undefined) {
ociErrorMessage = `${ociErr.code}: ${ociErr.message}`
}
} catch {}
}
const opcRequestId = headers?.[OCI_HEADER_OPC_REQUEST_ID]
if (opcRequestId) {
ociErrorMessage += `\n(${OCI_HEADER_OPC_REQUEST_ID}: ${opcRequestId})`
}
return super.makeStatusError(status, error, ociErrorMessage, headers)
}
})({
baseURL: options.ocaBaseUrl || DEFAULT_OCA_BASE_URL,
apiKey: "noop",
})
}
protected ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.ocaModelId) {
throw new Error("Oracle Code Assist (OCA) model is not selected")
}
try {
this.client = this.initializeClient(this.options)
} catch (error) {
throw new Error(`Error creating Oracle Code Assist (OCA) client: ${error.message}`)
}
}
return this.client
}
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
const client = this.ensureClient()
const modelId = this.options.ocaModelId || liteLlmDefaultModelId
const token = await OcaAuthService.getInstance().getAuthToken()
if (!token) {
throw new OpenAIError("Unable to handle auth, Oracle Code Assist (OCA) access token is not available")
}
const ociHeaders = await createOcaHeaders(token, this.options.taskId!)
Logger.log(`Making calculate cost request with customer opc-request-id: ${ociHeaders["opc-request-id"]}`)
try {
const response = await fetch(`${client.baseURL}/spend/calculate`, {
method: "POST",
headers: ociHeaders,
body: JSON.stringify({
completion_response: {
model: modelId,
usage: {
prompt_tokens,
completion_tokens,
},
},
}),
})
if (response.ok) {
const data: { cost: number } = await response.json()
return data.cost
} else {
console.error("Error calculating spend:", response.statusText)
return undefined
}
} catch (error) {
console.error("Error calculating spend:", error)
return undefined
}
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
role: "system",
content: systemPrompt,
}
const modelId = this.options.ocaModelId || liteLlmDefaultModelId
const isOminiModel = modelId.includes("o1-mini") || modelId.includes("o3-mini") || modelId.includes("o4-mini")
// Configuration for extended thinking
const budgetTokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = budgetTokens !== 0 ? true : false
const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined
let temperature: number | undefined = this.options.ocaModelInfo?.temperature ?? 0
const maxTokens: number | undefined = this.options.ocaModelInfo?.maxTokens
if (isOminiModel && reasoningOn) {
temperature = undefined // Thinking mode doesn't support temperature
}
// Define cache control object if prompt caching is enabled
const cacheControl = this.options.ocaUsePromptCache ? { cache_control: { type: "ephemeral" } } : undefined
// Add cache_control to system message if enabled
const enhancedSystemMessage = {
...systemMessage,
...(cacheControl && cacheControl),
}
// Find the last two user messages to apply caching
const userMsgIndices = formattedMessages.reduce((acc, msg, index) => {
if (msg.role === "user") {
acc.push(index)
}
return acc
}, [] as number[])
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastUserMsgIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
// Apply cache_control to the last two user messages if enabled
const enhancedMessages = formattedMessages.map((message, index) => {
if ((index === lastUserMsgIndex || index === secondLastUserMsgIndex) && cacheControl) {
return {
...message,
...cacheControl,
}
}
return message
})
const stream = await client.chat.completions.create({
model: this.options.ocaModelId || liteLlmDefaultModelId,
messages: [enhancedSystemMessage, ...enhancedMessages],
temperature,
stream: true,
max_completion_tokens: maxTokens,
max_tokens: maxTokens,
stream_options: { include_usage: true },
...(thinkingConfig && { thinking: thinkingConfig }), // Add thinking configuration when applicable
...(this.options.taskId && {
litellm_session_id: `cline-${this.options.taskId}`,
}), // Add session ID for LiteLLM tracking
})
const inputCost = (await this.calculateCost(1e6, 0)) || 0
const outputCost = (await this.calculateCost(0, 1e6)) || 0
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
// Handle normal text content
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// Handle reasoning events (thinking)
// Thinking is not in the standard types but may be in the response
interface ThinkingDelta {
thinking?: string
}
if ((delta as ThinkingDelta)?.thinking) {
yield {
type: "reasoning",
reasoning: (delta as ThinkingDelta).thinking || "",
}
}
// Handle token usage information
if (chunk.usage) {
const totalCost =
(inputCost * chunk.usage.prompt_tokens) / 1e6 + (outputCost * chunk.usage.completion_tokens) / 1e6
// Extract cache-related information if available
// Need to use type assertion since these properties are not in the standard OpenAI types
const usage = chunk.usage as {
prompt_tokens: number
completion_tokens: number
cache_creation_input_tokens?: number
prompt_cache_miss_tokens?: number
cache_read_input_tokens?: number
prompt_cache_hit_tokens?: number
}
const cacheWriteTokens = usage.cache_creation_input_tokens || usage.prompt_cache_miss_tokens || 0
const cacheReadTokens = usage.cache_read_input_tokens || usage.prompt_cache_hit_tokens || 0
yield {
type: "usage",
inputTokens: usage.prompt_tokens || 0,
outputTokens: usage.completion_tokens || 0,
cacheWriteTokens: cacheWriteTokens > 0 ? cacheWriteTokens : undefined,
cacheReadTokens: cacheReadTokens > 0 ? cacheReadTokens : undefined,
totalCost,
}
}
}
}
getModel() {
return {
id: this.options.ocaModelId || liteLlmDefaultModelId,
info: this.options.ocaModelInfo || liteLlmModelInfoSaneDefaults,
}
}
}
+15
View File
@@ -122,6 +122,21 @@ export class OpenRouterHandler implements ApiHandler {
}
}
// OpenRouter passes reasoning details that we can pass back unmodified in api requests to preserve reasoning traces for model
// See: https://openrouter.ai/docs/use-cases/reasoning-tokens#preserving-reasoning-blocks
if (
"reasoning_details" in delta &&
delta.reasoning_details &&
// @ts-ignore-next-line
delta.reasoning_details.length && // exists and non-0
!shouldSkipReasoningForModel(this.options.openRouterModelId)
) {
yield {
type: "reasoning_details",
reasoning_details: delta.reasoning_details,
}
}
if (!didOutputUsage && chunk.usage) {
yield {
type: "usage",
+6 -7
View File
@@ -9,6 +9,7 @@ import { ModelInfo, SapAiCoreModelId, sapAiCoreDefaultModelId, sapAiCoreModels }
import axios from "axios"
import OpenAI from "openai"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
@@ -454,8 +455,9 @@ export class SapAiCoreHandler implements ApiHandler {
return this.deployments?.some((d) => d.name.split(":")[0].toLowerCase() === modelId.split(":")[0].toLowerCase()) ?? false
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
if (this.options.sapAiCoreUseOrchestrationMode ?? true) {
if (this.options.sapAiCoreUseOrchestrationMode) {
yield* this.createMessageWithOrchestration(systemPrompt, messages)
} else {
yield* this.createMessageWithDeployments(systemPrompt, messages)
@@ -823,16 +825,13 @@ export class SapAiCoreHandler implements ApiHandler {
// Handle metadata (token usage)
if (data.metadata?.usage) {
// inputTokens does not include cached write/read tokens
let inputTokens = data.metadata.usage.inputTokens || 0
const outputTokens = data.metadata.usage.outputTokens || 0
// calibrate input token
const totalTokens = data.metadata.usage.totalTokens || 0
const cacheReadInputTokens = data.metadata.usage.cacheReadInputTokens || 0
const cacheWriteOutputTokens = data.metadata.usage.cacheWriteOutputTokens || 0
if (inputTokens + outputTokens + cacheReadInputTokens + cacheWriteOutputTokens !== totalTokens) {
inputTokens = totalTokens - outputTokens - cacheReadInputTokens - cacheWriteOutputTokens
}
const cacheWriteInputTokens = data.metadata.usage.cacheWriteInputTokens || 0
inputTokens = inputTokens + cacheReadInputTokens + cacheWriteInputTokens
yield {
type: "usage",
-1
View File
@@ -85,7 +85,6 @@ export class ZAiHandler implements ApiHandler {
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature: 0,
})
for await (const chunk of stream) {
+10
View File
@@ -115,7 +115,15 @@ export function convertToOpenAiMessages(
// Process non-tool messages
let content: string | undefined
const reasoningDetails: any[] = []
if (nonToolMessages.length > 0) {
nonToolMessages.forEach((part) => {
// @ts-ignore-next-line
if (part.type === "text" && part.reasoning_details) {
// @ts-ignore-next-line
reasoningDetails.push(part.reasoning_details)
}
})
content = nonToolMessages
.map((part) => {
if (part.type === "image") {
@@ -142,6 +150,8 @@ export function convertToOpenAiMessages(
content,
// Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty
tool_calls: tool_calls.length > 0 ? tool_calls : undefined,
// @ts-ignore-next-line
reasoning_details: reasoningDetails,
})
}
}
+3 -4
View File
@@ -29,6 +29,7 @@ export async function createOpenRouterStream(
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
// handles direct model.id match logic
switch (model.id) {
case "anthropic/claude-sonnet-4.5":
case "anthropic/claude-sonnet-4":
case "anthropic/claude-opus-4.1":
case "anthropic/claude-opus-4":
@@ -88,6 +89,7 @@ export async function createOpenRouterStream(
// (models usually default to max tokens allowed)
let maxTokens: number | undefined
switch (model.id) {
case "anthropic/claude-sonnet-4.5":
case "anthropic/claude-sonnet-4":
case "anthropic/claude-opus-4.1":
case "anthropic/claude-opus-4":
@@ -124,6 +126,7 @@ export async function createOpenRouterStream(
let reasoning: { max_tokens: number } | undefined
switch (model.id) {
case "anthropic/claude-sonnet-4.5":
case "anthropic/claude-sonnet-4":
case "anthropic/claude-opus-4.1":
case "anthropic/claude-opus-4":
@@ -139,10 +142,6 @@ export async function createOpenRouterStream(
reasoning = { max_tokens: budget_tokens }
}
break
case "cline/sonic":
temperature = 0.7
topP = 0.95
break
default:
if (thinkingBudgetTokens && model.info?.thinkingConfig && thinkingBudgetTokens > 0) {
temperature = undefined // extended thinking does not support non-1 temperature
+23 -1
View File
@@ -1,5 +1,11 @@
export type ApiStream = AsyncGenerator<ApiStreamChunk>
export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamReasoningChunk | ApiStreamUsageChunk
export type ApiStreamChunk =
| ApiStreamTextChunk
| ApiStreamReasoningChunk
| ApiStreamReasoningDetailsChunk
| ApiStreamAnthropicThinkingChunk
| ApiStreamAnthropicRedactedThinkingChunk
| ApiStreamUsageChunk
export interface ApiStreamTextChunk {
type: "text"
@@ -11,6 +17,22 @@ export interface ApiStreamReasoningChunk {
reasoning: string
}
export interface ApiStreamReasoningDetailsChunk {
type: "reasoning_details"
reasoning_details: any // openrouter has various properties that we can pass back unmodified in api requests to preserve reasoning traces
}
export interface ApiStreamAnthropicThinkingChunk {
type: "ant_thinking"
thinking: string
signature: string
}
export interface ApiStreamAnthropicRedactedThinkingChunk {
type: "ant_redacted_thinking"
data: string
}
export interface ApiStreamUsageChunk {
type: "usage"
inputTokens: number
+275
View File
@@ -0,0 +1,275 @@
import { getSavedClineMessages, getTaskMetadata, readTaskHistoryFromState, writeTaskHistoryToState } from "@core/storage/disk"
import { HostProvider } from "@hosts/host-provider"
import { ClineMessage } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { ShowMessageType } from "@shared/proto/host/window"
import { fileExistsAtPath } from "@utils/fs"
import * as path from "path"
import { ulid } from "ulid"
interface TaskReconstructionResult {
totalTasks: number
reconstructedTasks: number
skippedTasks: number
errors: string[]
}
/**
* Reconstructs task history from existing task folders
*/
export async function reconstructTaskHistory(): Promise<void> {
try {
// Show confirmation dialog using HostProvider
const proceed = await HostProvider.window.showMessage({
type: ShowMessageType.WARNING,
message:
"This will rebuild your task history from existing task data. This operation will backup your current task history and attempt to reconstruct it from task folders. Continue?",
options: {
items: ["Yes, Reconstruct", "Cancel"],
},
})
if (proceed?.selectedOption !== "Yes, Reconstruct") {
return
}
// Show initial progress message
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: "Reconstructing task history...",
})
const result = await performTaskHistoryReconstruction()
// Show results
if (result.errors.length > 0) {
const errorMessage = `Reconstruction completed with warnings:\n- Reconstructed: ${result.reconstructedTasks} tasks\n- Skipped: ${result.skippedTasks} tasks\n- Errors: ${result.errors.length}\n\nFirst few errors:\n${result.errors.slice(0, 3).join("\n")}`
HostProvider.window.showMessage({
type: ShowMessageType.WARNING,
message: errorMessage,
})
} else {
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: `Task history successfully reconstructed! Found and restored ${result.reconstructedTasks} tasks.`,
})
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to reconstruct task history: ${errorMessage}`,
})
}
}
async function performTaskHistoryReconstruction(): Promise<TaskReconstructionResult> {
const result: TaskReconstructionResult = {
totalTasks: 0,
reconstructedTasks: 0,
skippedTasks: 0,
errors: [],
}
// Backup existing task history
await backupExistingTaskHistory()
// Get tasks directory
const tasksDir = path.join(HostProvider.get().globalStorageFsPath, "tasks")
// Check if tasks directory exists
if (!(await fileExistsAtPath(tasksDir))) {
throw new Error("No tasks directory found. Nothing to reconstruct.")
}
// Scan for task directories
const taskIds = await scanTaskDirectories(tasksDir)
result.totalTasks = taskIds.length
if (taskIds.length === 0) {
throw new Error("No task directories found. Nothing to reconstruct.")
}
// Process each task
const reconstructedItems: HistoryItem[] = []
for (const taskId of taskIds) {
try {
const historyItem = await reconstructTaskHistoryItem(taskId)
if (historyItem) {
reconstructedItems.push(historyItem)
result.reconstructedTasks++
} else {
result.skippedTasks++
}
} catch (error) {
result.skippedTasks++
const errorMsg = error instanceof Error ? error.message : String(error)
result.errors.push(`Task ${taskId}: ${errorMsg}`)
}
}
// Sort by timestamp (newest first)
reconstructedItems.sort((a, b) => b.ts - a.ts)
// Write reconstructed history
await writeTaskHistoryToState(reconstructedItems)
return result
}
async function backupExistingTaskHistory(): Promise<void> {
try {
const existingHistory = await readTaskHistoryFromState()
if (existingHistory.length > 0) {
const backupPath = path.join(HostProvider.get().globalStorageFsPath, "state", `taskHistory.backup.${Date.now()}.json`)
// Ensure state directory exists
const fs = await import("fs/promises")
await fs.mkdir(path.dirname(backupPath), { recursive: true })
await fs.writeFile(backupPath, JSON.stringify(existingHistory, null, 2))
}
} catch (error) {
// Non-fatal error, just log it
console.warn("Failed to backup existing task history:", error)
}
}
async function scanTaskDirectories(tasksDir: string): Promise<string[]> {
const fs = await import("fs/promises")
try {
const entries = await fs.readdir(tasksDir, { withFileTypes: true })
return entries
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.filter((name) => /^\d+$/.test(name)) // Only numeric task IDs
} catch (error) {
throw new Error(`Failed to scan tasks directory: ${error}`)
}
}
async function reconstructTaskHistoryItem(taskId: string): Promise<HistoryItem | null> {
try {
// Load UI messages to extract task info
const clineMessages = await getSavedClineMessages(taskId)
if (clineMessages.length === 0) {
return null // Skip empty tasks
}
// Load task metadata for token usage
const metadata = await getTaskMetadata(taskId)
// Extract task information
const taskInfo = extractTaskInformation(clineMessages, metadata)
// Create HistoryItem
const historyItem: HistoryItem = {
id: taskId,
ulid: taskInfo.ulid || ulid(), // Generate new ULID if missing
ts: taskInfo.timestamp,
task: taskInfo.taskDescription,
tokensIn: taskInfo.tokensIn,
tokensOut: taskInfo.tokensOut,
cacheWrites: taskInfo.cacheWrites,
cacheReads: taskInfo.cacheReads,
totalCost: taskInfo.totalCost,
size: taskInfo.size,
isFavorited: taskInfo.isFavorited,
conversationHistoryDeletedRange: taskInfo.conversationHistoryDeletedRange,
}
return historyItem
} catch (error) {
throw new Error(`Failed to reconstruct task ${taskId}: ${error}`)
}
}
interface TaskInfo {
ulid?: string
timestamp: number
taskDescription: string
tokensIn: number
tokensOut: number
cacheWrites?: number
cacheReads?: number
totalCost: number
size?: number
isFavorited?: boolean
conversationHistoryDeletedRange?: [number, number]
}
function extractTaskInformation(clineMessages: ClineMessage[], metadata: any): TaskInfo {
// Find the first user message (task description)
const firstUserMessage = clineMessages.find((msg) => msg.type === "say" && msg.say === "text" && msg.text)
// Extract timestamp from first message or use task ID as fallback
const timestamp = clineMessages.length > 0 ? clineMessages[0].ts : Date.now()
// Extract task description
let taskDescription = "Untitled Task"
if (firstUserMessage?.text) {
// Clean up the task description
const cleanText = firstUserMessage.text
.replace(/<task>\s*/g, "")
.replace(/\s*<\/task>/g, "")
.trim()
const firstLine = cleanText.split("\n")[0]
if (firstLine) {
taskDescription = firstLine.substring(0, 100) // Limit length
}
}
// Calculate token usage from API request messages
let tokensIn = 0
let tokensOut = 0
let cacheWrites = 0
let cacheReads = 0
let totalCost = 0
// Look for api_req_started messages with token info
const apiReqMessages = clineMessages.filter((msg) => msg.type === "say" && msg.say === "api_req_started" && msg.text)
for (const msg of apiReqMessages) {
try {
if (msg.text) {
const apiInfo = JSON.parse(msg.text)
if (apiInfo.tokensIn) tokensIn += apiInfo.tokensIn
if (apiInfo.tokensOut) tokensOut += apiInfo.tokensOut
if (apiInfo.cacheWrites) cacheWrites += apiInfo.cacheWrites
if (apiInfo.cacheReads) cacheReads += apiInfo.cacheReads
if (apiInfo.cost) totalCost += apiInfo.cost
}
} catch {
// Ignore parsing errors
}
}
// Use metadata if available and no tokens found in messages
if (tokensIn === 0 && tokensOut === 0 && metadata.model_usage) {
for (const usage of metadata.model_usage) {
tokensIn += usage.tokensIn || 0
tokensOut += usage.tokensOut || 0
cacheWrites += usage.cacheWrites || 0
cacheReads += usage.cacheReads || 0
totalCost += usage.totalCost || 0
}
}
// Calculate approximate size (rough estimate)
const messageSize = JSON.stringify(clineMessages).length
const size = Math.floor(messageSize / 1024) // KB
return {
timestamp,
taskDescription,
tokensIn,
tokensOut,
cacheWrites: cacheWrites > 0 ? cacheWrites : undefined,
cacheReads: cacheReads > 0 ? cacheReads : undefined,
totalCost,
size,
}
}
@@ -108,15 +108,22 @@ export class ContextManager {
/**
* Determine whether we should compact context window, based on token counts
*/
shouldCompactContextWindow(clineMessages: ClineMessage[], api: ApiHandler, previousApiReqIndex: number): boolean {
shouldCompactContextWindow(
clineMessages: ClineMessage[],
api: ApiHandler,
previousApiReqIndex: number,
thresholdPercentage?: number,
): boolean {
if (previousApiReqIndex >= 0) {
const previousRequest = clineMessages[previousApiReqIndex]
if (previousRequest && previousRequest.text) {
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
const { maxAllowedSize } = getContextWindowInfo(api)
return totalTokens >= maxAllowedSize
const { contextWindow, maxAllowedSize } = getContextWindowInfo(api)
const roundedThreshold = thresholdPercentage ? Math.floor(contextWindow * thresholdPercentage) : maxAllowedSize
const thresholdTokens = Math.min(roundedThreshold, maxAllowedSize)
return totalTokens >= thresholdTokens
}
}
return false
@@ -6,19 +6,19 @@ import * as path from "path"
import * as sinon from "sinon"
import * as vscode from "vscode"
import { Controller } from "@/core/controller"
import { HostProvider } from "@/hosts/host-provider"
import { setVscodeHostProviderMock } from "@/test/host-provider-test-utils"
import type { FileMetadataEntry, TaskMetadata } from "./ContextTrackerTypes"
import { FileContextTracker } from "./FileContextTracker"
describe("FileContextTracker", () => {
const filePath = "src/test-file.ts"
const taskId = "test-task-id"
let sandbox: sinon.SinonSandbox
let mockController: Controller
let _mockWorkspace: sinon.SinonStub
let mockFileSystemWatcher: any
let chokidarWatchStub: sinon.SinonStub
let tracker: FileContextTracker
let taskId: string
let mockTaskMetadata: TaskMetadata
let getTaskMetadataStub: sinon.SinonStub
let saveTaskMetadataStub: sinon.SinonStub
@@ -46,11 +46,6 @@ describe("FileContextTracker", () => {
// Stub chokidar.watch to return our mock watcher
chokidarWatchStub = sandbox.stub(chokidar, "watch").returns(mockFileSystemWatcher as any)
// Mock controller and context
mockController = {
context: { globalStorageUri: { fsPath: "/mock/storage" } } as vscode.ExtensionContext,
} as unknown as Controller
// Mock disk module functions
mockTaskMetadata = { files_in_context: [], model_usage: [] }
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
@@ -59,29 +54,24 @@ describe("FileContextTracker", () => {
setVscodeHostProviderMock()
// Create tracker instance
taskId = "test-task-id"
tracker = new FileContextTracker(mockController, taskId)
tracker = new FileContextTracker({} as Controller, taskId)
})
afterEach(() => {
sandbox.restore()
// Reset HostProvider after each test to ensure clean state
HostProvider.reset()
})
it("should add a record when a file is read by a tool", async () => {
const filePath = "src/test-file.ts"
await tracker.trackFileContext(filePath, "read_tool")
// Verify getTaskMetadata was called
expect(getTaskMetadataStub.calledOnce).to.be.true
expect(getTaskMetadataStub.firstCall.args[1]).to.equal(taskId)
expect(getTaskMetadataStub.firstCall.args[0]).to.equal(taskId)
// Verify saveTaskMetadata was called with the correct data
expect(saveTaskMetadataStub.calledOnce).to.be.true
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
const savedMetadata = saveTaskMetadataStub.firstCall.args[1]
expect(savedMetadata.files_in_context.length).to.equal(1)
const fileEntry = savedMetadata.files_in_context[0]
@@ -93,13 +83,11 @@ describe("FileContextTracker", () => {
})
it("should add a record when a file is edited by Cline", async () => {
const filePath = "src/test-file.ts"
await tracker.trackFileContext(filePath, "cline_edited")
// Verify saveTaskMetadata was called with the correct data
expect(saveTaskMetadataStub.calledOnce).to.be.true
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
const savedMetadata = saveTaskMetadataStub.firstCall.args[1]
// Check that we have at least one entry in files_in_context
expect(savedMetadata.files_in_context).to.be.an("array").that.is.not.empty
@@ -121,12 +109,10 @@ describe("FileContextTracker", () => {
})
it("should add a record when a file is mentioned", async () => {
const filePath = "src/test-file.ts"
await tracker.trackFileContext(filePath, "file_mentioned")
// Verify saveTaskMetadata was called with the correct data
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
const savedMetadata = saveTaskMetadataStub.firstCall.args[1]
const fileEntry = savedMetadata.files_in_context[0]
expect(fileEntry.path).to.equal(filePath)
@@ -137,12 +123,10 @@ describe("FileContextTracker", () => {
})
it("should add a record when a file is edited by the user", async () => {
const filePath = "src/test-file.ts"
await tracker.trackFileContext(filePath, "user_edited")
// Verify saveTaskMetadata was called with the correct data
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
const savedMetadata = saveTaskMetadataStub.firstCall.args[1]
const fileEntry = savedMetadata.files_in_context[0]
expect(fileEntry.path).to.equal(filePath)
@@ -156,8 +140,6 @@ describe("FileContextTracker", () => {
})
it("should mark existing entries as stale when adding a new entry for the same file", async () => {
const filePath = "src/test-file.ts"
// Add an initial entry
mockTaskMetadata.files_in_context = [
{
@@ -174,7 +156,7 @@ describe("FileContextTracker", () => {
await tracker.trackFileContext(filePath, "cline_edited")
// Verify the metadata now has two entries - one stale and one active
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
const savedMetadata = saveTaskMetadataStub.firstCall.args[1]
expect(savedMetadata.files_in_context.length).to.equal(2)
// First entry should be marked as stale
@@ -187,8 +169,6 @@ describe("FileContextTracker", () => {
})
it("should setup a file watcher for tracked files", async () => {
const filePath = "src/test-file.ts"
await tracker.trackFileContext(filePath, "read_tool")
// Verify chokidar.watch was called
@@ -199,8 +179,6 @@ describe("FileContextTracker", () => {
})
it("should track user edits when file watcher detects changes", async () => {
const filePath = "src/test-file.ts"
// First track the file to set up the watcher
await tracker.trackFileContext(filePath, "read_tool")
@@ -226,8 +204,6 @@ describe("FileContextTracker", () => {
})
it("should not track Cline edits as user edits", async () => {
const filePath = "src/test-file.ts"
// First track the file to set up the watcher
await tracker.trackFileContext(filePath, "read_tool")
@@ -256,8 +232,6 @@ describe("FileContextTracker", () => {
})
it("should dispose file watchers when dispose is called", async () => {
const filePath = "src/test-file.ts"
// Track a file to set up the watcher
await tracker.trackFileContext(filePath, "read_tool")
@@ -90,7 +90,7 @@ export class FileContextTracker {
}
// Add file to metadata
await this.addFileToFileContextTracker(this.controller.context, this.taskId, filePath, operation)
await this.addFileToFileContextTracker(this.taskId, filePath, operation)
// Set up file watcher for this file
await this.setupFileWatcher(filePath)
@@ -104,14 +104,9 @@ export class FileContextTracker {
* This handles the business logic of determining if the file is new, stale, or active.
* It also updates the metadata with the latest read/edit dates.
*/
async addFileToFileContextTracker(
context: vscode.ExtensionContext,
taskId: string,
filePath: string,
source: FileMetadataEntry["record_source"],
) {
async addFileToFileContextTracker(taskId: string, filePath: string, source: FileMetadataEntry["record_source"]) {
try {
const metadata = await getTaskMetadata(context, taskId)
const metadata = await getTaskMetadata(taskId)
const now = Date.now()
// Mark existing entries for this file as stale
@@ -160,7 +155,7 @@ export class FileContextTracker {
}
metadata.files_in_context.push(newEntry)
await saveTaskMetadata(context, taskId, metadata)
await saveTaskMetadata(taskId, metadata)
} catch (error) {
console.error("Failed to add file to metadata:", error)
}
@@ -200,7 +195,7 @@ export class FileContextTracker {
try {
// Check task metadata for files that were edited by Cline or users after the message timestamp
const taskMetadata = await getTaskMetadata(this.controller.context, this.taskId)
const taskMetadata = await getTaskMetadata(this.taskId)
if (taskMetadata?.files_in_context) {
for (const fileEntry of taskMetadata.files_in_context) {
@@ -285,7 +280,7 @@ export class FileContextTracker {
static async cleanupOrphanedWarnings(context: vscode.ExtensionContext): Promise<void> {
const startTime = Date.now()
try {
const taskHistory = await readTaskHistoryFromState(context)
const taskHistory = await readTaskHistoryFromState()
const existingTaskIds = new Set(taskHistory.map((task) => task.id))
const allStateKeys = context.workspaceState.keys()
const pendingWarningKeys = allStateKeys.filter((key) => key.startsWith("pendingFileContextWarning_"))
@@ -2,15 +2,13 @@ import * as diskModule from "@core/storage/disk"
import { expect } from "chai"
import { afterEach, beforeEach, describe, it } from "mocha"
import * as sinon from "sinon"
import * as vscode from "vscode"
import type { TaskMetadata } from "./ContextTrackerTypes"
import { ModelContextTracker } from "./ModelContextTracker"
describe("ModelContextTracker", () => {
const taskId = "test-task-id"
let sandbox: sinon.SinonSandbox
let mockContext: vscode.ExtensionContext
let tracker: ModelContextTracker
let taskId: string
let mockTaskMetadata: TaskMetadata
let getTaskMetadataStub: sinon.SinonStub
let saveTaskMetadataStub: sinon.SinonStub
@@ -18,19 +16,13 @@ describe("ModelContextTracker", () => {
beforeEach(() => {
sandbox = sinon.createSandbox()
// Mock controller and context
mockContext = {
globalStorageUri: { fsPath: "/mock/storage" },
} as unknown as vscode.ExtensionContext
// Mock disk module functions
mockTaskMetadata = { files_in_context: [], model_usage: [] }
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
// Create tracker instance
taskId = "test-task-id"
tracker = new ModelContextTracker(mockContext, taskId)
tracker = new ModelContextTracker(taskId)
})
afterEach(() => {
@@ -53,13 +45,13 @@ describe("ModelContextTracker", () => {
// Verify getTaskMetadata was called with correct parameters
expect(getTaskMetadataStub.calledOnce).to.be.true
expect(getTaskMetadataStub.firstCall.args[1]).to.equal(taskId)
expect(getTaskMetadataStub.firstCall.args[0]).to.equal(taskId)
// Verify saveTaskMetadata was called with the correct data
expect(saveTaskMetadataStub.calledOnce).to.be.true
// Extract the saved metadata from the call arguments
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
const savedMetadata = saveTaskMetadataStub.firstCall.args[1]
// Verify model_usage array has one entry
expect(savedMetadata.model_usage.length).to.equal(1)
@@ -105,7 +97,7 @@ describe("ModelContextTracker", () => {
expect(saveTaskMetadataStub.calledOnce).to.be.true
// Extract the saved metadata
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
const savedMetadata = saveTaskMetadataStub.firstCall.args[1]
// Verify model_usage array now has two entries
expect(savedMetadata.model_usage.length).to.equal(2)
@@ -166,7 +158,7 @@ describe("ModelContextTracker", () => {
expect(saveTaskMetadataStub.calledOnce).to.be.true
// Get the saved metadata
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
const savedMetadata = saveTaskMetadataStub.firstCall.args[1]
// Since we reset the array for each call, we should always have 1 entry
expect(savedMetadata.model_usage.length).to.equal(1)
@@ -1,17 +1,14 @@
import { getTaskMetadata, saveTaskMetadata } from "@core/storage/disk"
import * as vscode from "vscode"
export class ModelContextTracker {
readonly taskId: string
private context: vscode.ExtensionContext
constructor(context: vscode.ExtensionContext, taskId: string) {
this.context = context
constructor(taskId: string) {
this.taskId = taskId
}
async recordModelUsage(apiProviderId: string, modelId: string, mode: string) {
const metadata = await getTaskMetadata(this.context, this.taskId)
const metadata = await getTaskMetadata(this.taskId)
if (!metadata.model_usage) {
metadata.model_usage = []
@@ -35,6 +32,6 @@ export class ModelContextTracker {
mode: mode,
})
await saveTaskMetadata(this.context, this.taskId, metadata)
await saveTaskMetadata(this.taskId, metadata)
}
}
@@ -74,7 +74,7 @@ export async function refreshClineRulesToggles(
localToggles: ClineRulesToggles
}> {
// Global toggles
const globalClineRulesToggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
const globalClineRulesToggles = controller.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
const updatedGlobalToggles = await synchronizeRuleToggles(globalClineRulesFilePath, globalClineRulesToggles)
controller.stateManager.setGlobalState("globalClineRulesToggles", updatedGlobalToggles)
@@ -247,11 +247,11 @@ export async function deleteRuleFile(
// Update the appropriate toggles
if (isGlobal) {
if (type === "workflow") {
const toggles = controller.stateManager.getGlobalStateKey("globalWorkflowToggles")
const toggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
delete toggles[rulePath]
controller.stateManager.setGlobalState("globalWorkflowToggles", toggles)
} else {
const toggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
const toggles = controller.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
delete toggles[rulePath]
controller.stateManager.setGlobalState("globalClineRulesToggles", toggles)
}
@@ -15,7 +15,7 @@ export async function refreshWorkflowToggles(
localWorkflowToggles: ClineRulesToggles
}> {
// Global workflows
const globalWorkflowToggles = controller.stateManager.getGlobalStateKey("globalWorkflowToggles")
const globalWorkflowToggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
const globalClineWorkflowsFilePath = await ensureWorkflowsDirectoryExists()
const updatedGlobalWorkflowToggles = await synchronizeRuleToggles(globalClineWorkflowsFilePath, globalWorkflowToggles)
controller.stateManager.setGlobalState("globalWorkflowToggles", updatedGlobalWorkflowToggles)
@@ -19,8 +19,7 @@ export async function discoverBrowser(controller: Controller, _request: EmptyReq
// This way we don't override the user's preference
// Test the connection to get the endpoint
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
const browserSession = new BrowserSession(controller.context, browserSettings)
const browserSession = new BrowserSession(controller.stateManager)
const result = await browserSession.testConnection(discoveredHost)
return BrowserConnection.create({
@@ -11,7 +11,7 @@ import { Controller } from "../index"
export async function getBrowserConnectionInfo(controller: Controller, _: EmptyRequest): Promise<BrowserConnectionInfo> {
try {
// Get browser settings from extension state
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
const browserSettings = controller.stateManager.getGlobalSettingsKey("browserSettings")
// Check if there's an active browser session by using the controller's handleWebviewMessage approach
// This is similar to what's done in controller/index.ts for the "getBrowserConnectionInfo" message
@@ -11,8 +11,7 @@ import { Controller } from "../index"
*/
export async function getDetectedChromePath(controller: Controller, _: EmptyRequest): Promise<ChromePath> {
try {
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
const browserSession = new BrowserSession(controller.context, browserSettings)
const browserSession = new BrowserSession(controller.stateManager)
const result = await browserSession.getDetectedChromePath()
return ChromePath.create({
@@ -10,8 +10,7 @@ import { Controller } from "../index"
*/
export async function relaunchChromeDebugMode(controller: Controller, _: EmptyRequest): Promise<StringMessage> {
try {
const { browserSettings } = await controller.getStateToPostToWebview()
const browserSession = new BrowserSession(controller.context, browserSettings)
const browserSession = new BrowserSession(controller.stateManager)
// Relaunch Chrome in debug mode
await browserSession.relaunchChromeDebugMode(controller)
@@ -12,8 +12,7 @@ import { Controller } from "../index"
*/
export async function testBrowserConnection(controller: Controller, request: StringRequest): Promise<BrowserConnection> {
try {
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
const browserSession = new BrowserSession(controller.context, browserSettings)
const browserSession = new BrowserSession(controller.stateManager)
const text = request.value || ""
// If no text is provided, try auto-discovery
@@ -0,0 +1,32 @@
import { RecordingResult } from "@shared/proto/cline/dictation"
import { audioRecordingService } from "@/services/dictation/AudioRecordingService"
import { telemetryService } from "@/services/telemetry"
import { Controller } from ".."
/**
* Cancels audio recording without saving or transcribing the audio
* @param controller The controller instance
* @returns RecordingResult indicating success or failure
*/
export const cancelRecording = async (controller: Controller): Promise<RecordingResult> => {
const taskId = controller.task?.taskId
const recordingStatus = audioRecordingService.getRecordingStatus()
const recordingDuration = recordingStatus.durationSeconds * 1000 // Convert to milliseconds
let errorMessage = ""
let isSuccess = true
try {
const result = await audioRecordingService.cancelRecording()
isSuccess = !!result?.success
errorMessage = result?.error ?? ""
} catch (error) {
console.error("Error canceling recording:", error)
isSuccess = false
errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
}
telemetryService.captureVoiceRecordingStopped(taskId, recordingDuration, false, process.platform)
return RecordingResult.create({
success: isSuccess,
error: errorMessage ?? "",
})
}
@@ -0,0 +1,25 @@
import { RecordingStatus } from "@shared/proto/cline/dictation"
import { audioRecordingService } from "@/services/dictation/AudioRecordingService"
/**
* Gets the current recording status
* @returns RecordingStatus with current status
*/
export const getRecordingStatus = async (): Promise<RecordingStatus> => {
try {
const status = audioRecordingService.getRecordingStatus()
return RecordingStatus.create({
isRecording: status.isRecording,
durationSeconds: status.durationSeconds,
error: status.error ?? "",
})
} catch (error) {
console.error("Error getting recording status:", error)
return RecordingStatus.create({
isRecording: false,
durationSeconds: 0,
error: error instanceof Error ? error.message : "Unknown error occurred",
})
}
}
@@ -0,0 +1,163 @@
import { RecordingResult } from "@shared/proto/cline/dictation"
import * as os from "os"
import { HostProvider } from "@/hosts/host-provider"
import { audioRecordingService } from "@/services/dictation/AudioRecordingService"
import { telemetryService } from "@/services/telemetry"
import { AUDIO_PROGRAM_CONFIG } from "@/shared/audioProgramConstants"
import { ShowMessageType } from "@/shared/proto/host/window"
import { Controller } from ".."
/**
* Handles the installation of missing dependencies with Cline
*/
async function handleInstallWithCline(
controller: Controller,
dependencyName: string,
installCommand: string,
platform: string,
): Promise<void> {
const platformName = platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : "Linux"
const installTask = `Please install ${dependencyName} for voice recording on ${platformName}.\n\nRun this command:\n\`\`\`bash\n${installCommand}\n\`\`\`\n\nThis will enable voice recording functionality in Cline.`
// Clear any existing task and start the installation task
await controller.clearTask()
await controller.postStateToWebview()
await controller.initTask(installTask)
HostProvider.get().logToChannel(`Started task to install ${dependencyName}`)
}
/**
* Handles copying the installation command to clipboard
*/
async function handleCopyCommand(installCommand: string): Promise<void> {
await HostProvider.env.clipboardWriteText({ value: installCommand })
await HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: `Installation command copied to clipboard: ${installCommand}`,
options: { items: [] },
})
}
/**
* Handles missing dependency notification and user action
*/
async function handleMissingDependency(
controller: Controller,
platform: string,
config: (typeof AUDIO_PROGRAM_CONFIG)[keyof typeof AUDIO_PROGRAM_CONFIG],
): Promise<void> {
const installWithCline = "Install with Cline"
const installManually = "Copy Command"
const dismiss = "Dismiss"
const action = await HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: `${config.dependencyName} is required for voice recording. ${config.installDescription}`,
options: { items: [installWithCline, installManually, dismiss] },
})
if (action.selectedOption === installWithCline) {
await handleInstallWithCline(controller, config.dependencyName, config.installCommand, platform)
} else if (action.selectedOption === installManually) {
await handleCopyCommand(config.installCommand)
}
// If dismiss, do nothing
}
/**
* Handles sign-in errors for dictation
*/
async function handleSignInError(controller: Controller, errorMessage: string): Promise<void> {
const signInAction = "Sign in to Cline"
const action = await HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `Voice recording error: ${errorMessage}`,
options: { items: [signInAction] },
})
if (action.selectedOption === signInAction) {
await controller.authService.createAuthRequest()
}
}
/**
* Shows a generic error message
*/
async function showGenericError(errorMessage: string): Promise<void> {
await HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `Voice recording error: ${errorMessage}`,
options: { items: [] },
})
}
/**
* Checks if the recording error is due to missing dependencies
*/
function isMissingDependencyError(
error: string | undefined,
config: (typeof AUDIO_PROGRAM_CONFIG)[keyof typeof AUDIO_PROGRAM_CONFIG] | undefined,
): boolean {
return !!(error && config && error.includes(config.error))
}
/**
* Starts audio recording using the Extension Host
* @param controller The controller instance
* @returns RecordingResult with success status
*/
export const startRecording = async (controller: Controller): Promise<RecordingResult> => {
const taskId = controller.task?.taskId
try {
// Verify user authentication
const userInfo = controller.authService.getInfo()
if (!userInfo?.user?.uid) {
throw new Error("Please sign in to your Cline Account to use Dictation.")
}
// Attempt to start recording
const result = await audioRecordingService.startRecording()
// Handle successful recording start
if (result.success) {
telemetryService.captureVoiceRecordingStarted(taskId, process.platform)
return RecordingResult.create({
success: true,
error: "",
})
}
// Check if the error is due to missing dependencies
const platform = os.platform() as keyof typeof AUDIO_PROGRAM_CONFIG
const config = AUDIO_PROGRAM_CONFIG[platform]
if (isMissingDependencyError(result.error, config)) {
// Don't await - show dialog asynchronously so frontend gets immediate response
handleMissingDependency(controller, platform, config)
}
return RecordingResult.create({
success: false,
error: result.error || "",
})
} catch (error) {
console.error("Error starting recording:", error)
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
// Handle different error types
if (errorMessage.includes("sign in")) {
// Don't await - show dialog asynchronously so frontend gets immediate response
handleSignInError(controller, errorMessage)
} else {
// Don't await - show dialog asynchronously so frontend gets immediate response
showGenericError(errorMessage)
}
return RecordingResult.create({
success: false,
error: errorMessage,
})
}
}
@@ -0,0 +1,37 @@
import { RecordedAudio } from "@shared/proto/cline/dictation"
import { audioRecordingService } from "@/services/dictation/AudioRecordingService"
import { telemetryService } from "@/services/telemetry"
import { Controller } from ".."
/**
* Stops audio recording and returns the recorded audio
* @param controller The controller instance
* @returns RecordedAudio with audio data
*/
export const stopRecording = async (controller: Controller): Promise<RecordedAudio> => {
const taskId = controller.task?.taskId
const recordingStatus = audioRecordingService.getRecordingStatus()
const recordingDuration = recordingStatus.durationSeconds * 1000 // Convert to milliseconds
try {
const result = await audioRecordingService.stopRecording()
telemetryService.captureVoiceRecordingStopped(taskId, recordingDuration, result.success, process.platform)
return RecordedAudio.create({
success: result.success,
audioBase64: result.audioBase64 ?? "",
error: result.error ?? "",
})
} catch (error) {
console.error("Error stopping recording:", error)
telemetryService.captureVoiceRecordingStopped(taskId, recordingDuration, false, process.platform)
return RecordedAudio.create({
success: false,
audioBase64: "",
error: error instanceof Error ? error.message : "Unknown error occurred",
})
}
}
@@ -0,0 +1,73 @@
import { TranscribeAudioRequest, Transcription } from "@shared/proto/cline/dictation"
import { HostProvider } from "@/hosts/host-provider"
import { getVoiceTranscriptionService } from "@/services/dictation/VoiceTranscriptionService"
import { telemetryService } from "@/services/telemetry"
import { ShowMessageType } from "@/shared/proto/host/window"
import { Controller } from ".."
/**
* Transcribes audio using Cline transcription service
* @param controller The controller instance
* @param request TranscribeAudioRequest containing base64 audio data
* @returns Transcription with transcribed text or error
*/
export const transcribeAudio = async (controller: Controller, request: TranscribeAudioRequest): Promise<Transcription> => {
const taskId = controller.task?.taskId
const startTime = Date.now()
// Capture telemetry for transcription start
telemetryService.captureVoiceTranscriptionStarted(taskId, request.language ?? "en")
try {
// Transcribe the audio
const result = await getVoiceTranscriptionService().transcribeAudio(request.audioBase64, request.language ?? "en")
const durationMs = Date.now() - startTime
if (result.error) {
let errorType = "api_error"
if (result.error.includes("Authentication failed")) {
errorType = "invalid_jwt_token"
} else if (result.error.includes("Insufficient credits")) {
errorType = "insufficient_credits"
} else if (result.error.includes("Invalid audio format")) {
errorType = "invalid_audio_format"
} else if (result.error.includes("No internet connection")) {
errorType = "no_internet"
} else if (result.error.includes("Cannot connect")) {
errorType = "connection_error"
} else if (result.error.includes("Connection timed out")) {
errorType = "timeout_error"
} else if (result.error.includes("Network error")) {
errorType = "network_error"
}
telemetryService.captureVoiceTranscriptionError(taskId, errorType, result.error, durationMs)
// Use the error message directly from the service as it's already user-friendly
const errorMessage = result.error
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: errorMessage,
})
} else if (result.text) {
telemetryService.captureVoiceTranscriptionCompleted(taskId, result.text.length, durationMs, request.language ?? "en")
}
return Transcription.create({
text: result.text ?? "",
error: result.error ?? "",
})
} catch (error) {
console.error("Error transcribing audio:", error)
const durationMs = Date.now() - startTime
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
telemetryService.captureVoiceTranscriptionError(taskId, "unexpected_error", errorMessage, durationMs)
return Transcription.create({
text: "",
error: errorMessage,
})
}
}
@@ -1,6 +1,7 @@
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
import { Empty, StringRequest } from "@shared/proto/cline/common"
import path from "path"
import { HostProvider } from "@/hosts/host-provider"
import { Controller } from ".."
/**
* Opens a file in the editor
@@ -8,11 +9,11 @@ import { Controller } from ".."
* @param request The request message containing the file path in the 'value' field
* @returns Empty response
*/
export async function openTaskHistory(controller: Controller, request: StringRequest): Promise<Empty> {
const globalStoragePath = controller.context.globalStorageUri.fsPath
const taskHistoryPath = path.join(globalStoragePath, "tasks", request.value, "api_conversation_history.json")
export async function openDiskConversationHistory(_controller: Controller, request: StringRequest): Promise<Empty> {
const globalStoragePath = HostProvider.get().globalStorageFsPath
const taskConversationHistoryPath = path.join(globalStoragePath, "tasks", request.value, "api_conversation_history.json")
if (request.value) {
openFileIntegration(taskHistoryPath)
openFileIntegration(taskConversationHistoryPath)
}
return Empty.create()
}
@@ -32,7 +32,7 @@ export async function openFocusChainFile(controller: Controller, request: String
}
}
const focusChainFilePath = await ensureFocusChainFile(controller.context, taskId, initialFocusChainContent)
const focusChainFilePath = await ensureFocusChainFile(taskId, initialFocusChainContent)
telemetryService.captureFocusChainListOpened(taskId)
await openFileIntegration(focusChainFilePath)
+2 -2
View File
@@ -24,7 +24,7 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
// This is the same core logic as in the original handler
if (isGlobal) {
const toggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
const toggles = controller.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
toggles[rulePath] = enabled
controller.stateManager.setGlobalState("globalClineRulesToggles", toggles)
} else {
@@ -41,7 +41,7 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
}
// Get the current state to return in the response
const globalToggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
const globalToggles = controller.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
const localToggles = controller.stateManager.getWorkspaceStateKey("localClineRulesToggles")
return ToggleClineRules.create({
+1 -1
View File
@@ -21,7 +21,7 @@ export async function toggleWorkflow(controller: Controller, request: ToggleWork
// Update the toggles based on isGlobal flag
if (isGlobal) {
// Global workflows
const toggles = controller.stateManager.getGlobalStateKey("globalWorkflowToggles")
const toggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
toggles[workflowPath] = enabled
controller.stateManager.setGlobalState("globalWorkflowToggles", toggles)
await controller.postStateToWebview()
@@ -1,11 +1,11 @@
import { afterEach, beforeEach, describe, it } from "mocha"
import "should"
import { Controller } from "@core/controller"
import { IRecorder } from "@core/controller/grpc-recorder/grpc-recorder"
import { GrpcRecorderBuilder } from "@core/controller/grpc-recorder/grpc-recorder.builder"
import { testHooks } from "@core/controller/grpc-recorder/test-hooks"
import { GrpcLogEntry } from "@core/controller/grpc-recorder/types"
import * as sinon from "sinon"
import { Controller } from ".."
import { IRecorder } from "./grpc-recorder"
import { GrpcRecorderBuilder } from "./grpc-recorder.builder"
import { testHooks } from "./test-hooks"
import { GrpcLogEntry } from "./types"
describe("test-hooks", () => {
let cleanupSyntheticEntriesStub: sinon.SinonStub
@@ -91,7 +91,15 @@ function testFilters(): GrpcRequestFilter[] {
return [
(req) => req.is_streaming,
(req) => ["cline.UiService", "cline.McpService", "cline.WebService"].includes(req.service),
(req) => ["refreshOpenRouterModels", "getAvailableTerminalProfiles"].includes(req.method),
(req) =>
[
"refreshOpenRouterModels",
"getAvailableTerminalProfiles",
"showTaskWithId",
"deleteTasksWithIds",
"getTotalTasksSize",
"cancelTask",
].includes(req.method),
]
}
+145 -83
View File
@@ -25,17 +25,26 @@ import { clineEnvConfig } from "@/config"
import { HostProvider } from "@/hosts/host-provider"
import { ExtensionRegistryInfo } from "@/registry"
import { AuthService } from "@/services/auth/AuthService"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
import { featureFlagsService } from "@/services/feature-flags"
import { getDistinctId } from "@/services/logging/distinctId"
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 {
ensureCacheDirectoryExists,
ensureMcpServersDirectoryExists,
ensureSettingsDirectoryExists,
GlobalFileNames,
} from "../storage/disk"
import { PersistenceErrorEvent, StateManager } from "../storage/StateManager"
import { Task } from "../task"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { appendClineStealthModels } from "./models/refreshOpenRouterModels"
import { sendStateUpdate } from "./state/subscribeToState"
import { sendChatButtonClickedEvent } from "./ui/subscribeToChatButtonClicked"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -50,6 +59,7 @@ export class Controller {
mcpHub: McpHub
accountService: ClineAccountService
authService: AuthService
ocaAuthService: OcaAuthService
readonly stateManager: StateManager
// NEW: Add workspace manager (optional initially)
@@ -62,9 +72,10 @@ export class Controller {
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)
this.authService = AuthService.getInstance(this)
this.ocaAuthService = OcaAuthService.initialize(this)
this.accountService = ClineAccountService.getInstance()
// Initialize cache service asynchronously - critical for extension functionality
this.stateManager
@@ -108,21 +119,17 @@ export class Controller {
this.mcpHub = new McpHub(
() => ensureMcpServersDirectoryExists(),
() => ensureSettingsDirectoryExists(this.context),
() => ensureSettingsDirectoryExists(),
ExtensionRegistryInfo.version,
telemetryService,
)
// Clean up legacy checkpoints
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath).catch((error) => {
cleanupLegacyCheckpoints().catch((error) => {
console.error("Failed to cleanup legacy checkpoints:", error)
})
}
async getCurrentMode(): Promise<Mode> {
return this.stateManager.getGlobalStateKey("mode")
}
/*
VSCode extensions use the disposable pattern to clean up resources when the sidebar/editor tab is closed by the user or system. This applies to event listening, commands, interacting with the UI, etc.
- https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/
@@ -164,6 +171,23 @@ export class Controller {
}
}
// Oca Auth methods
async handleOcaSignOut() {
try {
await this.ocaAuthService.handleDeauth()
await this.postStateToWebview()
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: "Successfully logged out of OCA",
})
} catch (_error) {
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: "OCA Logout failed",
})
}
}
async setUserInfo(info?: UserInfo) {
this.stateManager.setGlobalState("userInfo", info)
}
@@ -171,23 +195,13 @@ export class Controller {
async initTask(task?: string, images?: string[], files?: string[], historyItem?: HistoryItem) {
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
const apiConfiguration = this.stateManager.getApiConfiguration()
const autoApprovalSettings = this.stateManager.getGlobalStateKey("autoApprovalSettings")
const browserSettings = this.stateManager.getGlobalStateKey("browserSettings")
const focusChainSettings = this.stateManager.getGlobalStateKey("focusChainSettings")
const preferredLanguage = this.stateManager.getGlobalStateKey("preferredLanguage")
const openaiReasoningEffort = this.stateManager.getGlobalStateKey("openaiReasoningEffort")
const mode = this.stateManager.getGlobalStateKey("mode")
const shellIntegrationTimeout = this.stateManager.getGlobalStateKey("shellIntegrationTimeout")
const autoApprovalSettings = this.stateManager.getGlobalSettingsKey("autoApprovalSettings")
const shellIntegrationTimeout = this.stateManager.getGlobalSettingsKey("shellIntegrationTimeout")
const terminalReuseEnabled = this.stateManager.getGlobalStateKey("terminalReuseEnabled")
const terminalOutputLineLimit = this.stateManager.getGlobalStateKey("terminalOutputLineLimit")
const defaultTerminalProfile = this.stateManager.getGlobalStateKey("defaultTerminalProfile")
const enableCheckpointsSetting = this.stateManager.getGlobalStateKey("enableCheckpointsSetting")
const terminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
const defaultTerminalProfile = this.stateManager.getGlobalSettingsKey("defaultTerminalProfile")
const isNewUser = this.stateManager.getGlobalStateKey("isNewUser")
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
const strictPlanModeEnabled = this.stateManager.getGlobalStateKey("strictPlanModeEnabled")
const yoloModeToggled = this.stateManager.getGlobalStateKey("yoloModeToggled")
const useAutoCondense = this.stateManager.getGlobalStateKey("useAutoCondense")
const NEW_USER_TASK_COUNT_THRESHOLD = 10
@@ -204,18 +218,6 @@ export class Controller {
}
this.stateManager.setGlobalState("autoApprovalSettings", updatedAutoApprovalSettings)
}
// 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 {
focusChainEnabled = Boolean(focusChainSettings?.enabled)
}
const effectiveFocusChainSettings = {
...(focusChainSettings || { enabled: true, remindClineInterval: 6 }),
enabled: focusChainEnabled,
}
// Initialize and persist the workspace manager (multi-root or single-root) with telemetry + fallback
this.workspaceManager = await setupWorkspaceManager({
@@ -232,21 +234,10 @@ export class Controller {
() => this.postStateToWebview(),
(taskId) => this.reinitExistingTaskFromId(taskId),
() => this.cancelTask(),
apiConfiguration,
autoApprovalSettings,
browserSettings,
effectiveFocusChainSettings,
preferredLanguage,
openaiReasoningEffort,
mode,
strictPlanModeEnabled ?? true,
yoloModeToggled,
useAutoCondense ?? false,
shellIntegrationTimeout,
terminalReuseEnabled ?? true,
terminalOutputLineLimit ?? 500,
defaultTerminalProfile ?? "default",
enableCheckpointsSetting ?? true,
cwd,
this.stateManager,
this.workspaceManager,
@@ -292,7 +283,6 @@ export class Controller {
// Additional safety
if (this.task) {
this.task.updateMode(modeToSwitchTo)
return true
}
return false
@@ -316,7 +306,6 @@ export class Controller {
await this.postStateToWebview()
if (this.task) {
this.task.updateMode(modeToSwitchTo)
if (this.task.taskState.isAwaitingPlanResponse && didSwitchToActMode) {
this.task.taskState.didRespondToPlanAskBySwitchingMode = true
// Use chatContent if provided, otherwise use default message
@@ -374,9 +363,9 @@ export class Controller {
const clineProvider: ApiProvider = "cline"
// Get current settings to determine how to update providers
const planActSeparateModelsSetting = this.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
const planActSeparateModelsSetting = this.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
const currentMode = await this.getCurrentMode()
const currentMode = this.stateManager.getGlobalSettingsKey("mode")
// Get current API configuration from cache
const currentApiConfiguration = this.stateManager.getApiConfiguration()
@@ -418,6 +407,62 @@ export class Controller {
}
}
async handleOcaAuthCallback(code: string, state: string) {
try {
await this.ocaAuthService.handleAuthCallback(code, state)
const ocaProvider: ApiProvider = "oca"
// Get current settings to determine how to update providers
const planActSeparateModelsSetting = this.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
const currentMode = this.stateManager.getGlobalSettingsKey("mode")
// Get current API configuration from cache
const currentApiConfiguration = this.stateManager.getApiConfiguration()
const updatedConfig = { ...currentApiConfiguration }
if (planActSeparateModelsSetting) {
// Only update the current mode's provider
if (currentMode === "plan") {
updatedConfig.planModeApiProvider = ocaProvider
} else {
updatedConfig.actModeApiProvider = ocaProvider
}
} else {
// Update both modes to keep them in sync
updatedConfig.planModeApiProvider = ocaProvider
updatedConfig.actModeApiProvider = ocaProvider
}
// Update the API configuration through cache service
this.stateManager.setApiConfiguration(updatedConfig)
// Mark welcome view as completed since user has successfully logged in
this.stateManager.setGlobalState("welcomeViewCompleted", true)
if (this.task) {
this.task.api = buildApiHandler({ ...updatedConfig, ulid: this.task.ulid }, currentMode)
}
await this.postStateToWebview()
} catch (error) {
console.error("Failed to handle auth callback:", error)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to log in to OCA",
})
// Even on login failure, we preserve any existing tokens
// Only clear tokens on explicit logout
}
}
async handleTaskCreation(prompt: string) {
await sendChatButtonClickedEvent(this.id)
await this.initTask(prompt)
}
// MCP Marketplace
private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise<McpMarketplaceCatalog | undefined> {
try {
@@ -533,7 +578,7 @@ export class Controller {
}
const openrouter: ApiProvider = "openrouter"
const currentMode = await this.getCurrentMode()
const currentMode = this.stateManager.getGlobalSettingsKey("mode")
// Update API configuration through cache service
const currentApiConfiguration = this.stateManager.getApiConfiguration()
@@ -552,29 +597,25 @@ export class Controller {
// Dont send settingsButtonClicked because its bad ux if user is on welcome
}
private async ensureCacheDirectoryExists(): Promise<string> {
const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
// Read OpenRouter models from disk cache
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
if (fileExists) {
const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8")
return JSON.parse(fileContents)
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
try {
if (await fileExistsAtPath(openRouterModelsFilePath)) {
const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8")
const models = JSON.parse(fileContents)
// Append stealth models
return appendClineStealthModels(models)
}
} catch (error) {
console.error("Error reading cached OpenRouter models:", error)
}
return undefined
}
// Read Vercel AI Gateway models from disk cache
async readVercelAiGatewayModels(): Promise<Record<string, ModelInfo> | undefined> {
const vercelAiGatewayModelsFilePath = path.join(
await this.ensureCacheDirectoryExists(),
GlobalFileNames.vercelAiGatewayModels,
)
const vercelAiGatewayModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.vercelAiGatewayModels)
const fileExists = await fileExistsAtPath(vercelAiGatewayModelsFilePath)
if (fileExists) {
const fileContents = await fs.readFile(vercelAiGatewayModelsFilePath, "utf8")
@@ -597,7 +638,7 @@ export class Controller {
const history = this.stateManager.getGlobalStateKey("taskHistory")
const historyItem = history.find((item) => item.id === id)
if (historyItem) {
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
const taskDirPath = path.join(HostProvider.get().globalStorageFsPath, "tasks", id)
const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
const contextHistoryFilePath = path.join(taskDirPath, GlobalFileNames.contextHistory)
@@ -649,36 +690,42 @@ export class Controller {
const apiConfiguration = this.stateManager.getApiConfiguration()
const lastShownAnnouncementId = this.stateManager.getGlobalStateKey("lastShownAnnouncementId")
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
const autoApprovalSettings = this.stateManager.getGlobalStateKey("autoApprovalSettings")
const browserSettings = this.stateManager.getGlobalStateKey("browserSettings")
const focusChainSettings = this.stateManager.getGlobalStateKey("focusChainSettings")
const preferredLanguage = this.stateManager.getGlobalStateKey("preferredLanguage")
const openaiReasoningEffort = this.stateManager.getGlobalStateKey("openaiReasoningEffort")
const mode = this.stateManager.getGlobalStateKey("mode")
const strictPlanModeEnabled = this.stateManager.getGlobalStateKey("strictPlanModeEnabled")
const useAutoCondense = this.stateManager.getGlobalStateKey("useAutoCondense")
const autoApprovalSettings = this.stateManager.getGlobalSettingsKey("autoApprovalSettings")
const browserSettings = this.stateManager.getGlobalSettingsKey("browserSettings")
const focusChainSettings = this.stateManager.getGlobalSettingsKey("focusChainSettings")
const dictationSettings = this.stateManager.getGlobalSettingsKey("dictationSettings")
const preferredLanguage = this.stateManager.getGlobalSettingsKey("preferredLanguage")
const openaiReasoningEffort = this.stateManager.getGlobalSettingsKey("openaiReasoningEffort")
const mode = this.stateManager.getGlobalSettingsKey("mode")
const strictPlanModeEnabled = this.stateManager.getGlobalSettingsKey("strictPlanModeEnabled")
const yoloModeToggled = this.stateManager.getGlobalSettingsKey("yoloModeToggled")
const useAutoCondense = this.stateManager.getGlobalSettingsKey("useAutoCondense")
const userInfo = this.stateManager.getGlobalStateKey("userInfo")
const mcpMarketplaceEnabled = this.stateManager.getGlobalStateKey("mcpMarketplaceEnabled")
const mcpDisplayMode = this.stateManager.getGlobalStateKey("mcpDisplayMode")
const telemetrySetting = this.stateManager.getGlobalStateKey("telemetrySetting")
const planActSeparateModelsSetting = this.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
const enableCheckpointsSetting = this.stateManager.getGlobalStateKey("enableCheckpointsSetting")
const globalClineRulesToggles = this.stateManager.getGlobalStateKey("globalClineRulesToggles")
const globalWorkflowToggles = this.stateManager.getGlobalStateKey("globalWorkflowToggles")
const shellIntegrationTimeout = this.stateManager.getGlobalStateKey("shellIntegrationTimeout")
const telemetrySetting = this.stateManager.getGlobalSettingsKey("telemetrySetting")
const planActSeparateModelsSetting = this.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
const enableCheckpointsSetting = this.stateManager.getGlobalSettingsKey("enableCheckpointsSetting")
const globalClineRulesToggles = this.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
const globalWorkflowToggles = this.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
const shellIntegrationTimeout = this.stateManager.getGlobalSettingsKey("shellIntegrationTimeout")
const terminalReuseEnabled = this.stateManager.getGlobalStateKey("terminalReuseEnabled")
const defaultTerminalProfile = this.stateManager.getGlobalStateKey("defaultTerminalProfile")
const defaultTerminalProfile = this.stateManager.getGlobalSettingsKey("defaultTerminalProfile")
const isNewUser = this.stateManager.getGlobalStateKey("isNewUser")
const welcomeViewCompleted = Boolean(
this.stateManager.getGlobalStateKey("welcomeViewCompleted") || this.authService.getInfo()?.user?.uid,
)
const customPrompt = this.stateManager.getGlobalStateKey("customPrompt")
const customPrompt = this.stateManager.getGlobalSettingsKey("customPrompt")
const mcpResponsesCollapsed = this.stateManager.getGlobalStateKey("mcpResponsesCollapsed")
const terminalOutputLineLimit = this.stateManager.getGlobalStateKey("terminalOutputLineLimit")
const terminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
const favoritedModelIds = this.stateManager.getGlobalStateKey("favoritedModelIds")
const lastDismissedInfoBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedInfoBannerVersion") || 0
const localClineRulesToggles = this.stateManager.getWorkspaceStateKey("localClineRulesToggles")
const localWindsurfRulesToggles = this.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
const localCursorRulesToggles = this.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
const workflowToggles = this.stateManager.getWorkspaceStateKey("workflowToggles")
const autoCondenseThreshold = this.stateManager.getGlobalSettingsKey("autoCondenseThreshold")
const currentTaskItem = this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined
const clineMessages = this.task?.messageStateHandler.getClineMessages() || []
@@ -695,6 +742,12 @@ export class Controller {
const distinctId = getDistinctId()
const version = ExtensionRegistryInfo.version
// Set feature flag in dictation settings based on platform
const updatedDictationSettings = {
...dictationSettings,
featureEnabled: process.platform === "darwin", // Enable dictation only on macOS
}
return {
version,
apiConfiguration,
@@ -705,10 +758,12 @@ export class Controller {
autoApprovalSettings,
browserSettings,
focusChainSettings,
dictationSettings: updatedDictationSettings,
preferredLanguage,
openaiReasoningEffort,
mode,
strictPlanModeEnabled,
yoloModeToggled,
useAutoCondense,
userInfo,
mcpMarketplaceEnabled,
@@ -734,10 +789,17 @@ export class Controller {
taskHistory: processedTaskHistory,
platform,
shouldShowAnnouncement,
favoritedModelIds,
autoCondenseThreshold,
// NEW: Add workspace information
workspaceRoots: this.workspaceManager?.getRoots() ?? [],
primaryRootIndex: this.workspaceManager?.getPrimaryIndex() ?? 0,
isMultiRootWorkspace: (this.workspaceManager?.getRoots().length ?? 0) > 1,
multiRootSetting: {
user: this.stateManager.getGlobalStateKey("multiRootEnabled"),
featureFlag: featureFlagsService.getMultiRootEnabled(),
},
lastDismissedInfoBannerVersion,
}
}
@@ -1,7 +1,8 @@
import { GlobalFileNames } from "@core/storage/disk"
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import { EmptyRequest } from "@shared/proto/cline/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "@shared/proto/cline/models"
import { fileExistsAtPath } from "@utils/fs"
import { parsePrice } from "@utils/model-utils"
import axios from "axios"
import fs from "fs/promises"
import path from "path"
@@ -19,16 +20,16 @@ export async function refreshBasetenModels(
_request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
console.log("=== refreshBasetenModels called ===")
const basetenModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.basetenModels)
const basetenModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.basetenModels)
// Get the Baseten API key from the controller's state
const basetenApiKey = controller.stateManager.getSecretKey("basetenApiKey")
const models: Record<string, Partial<OpenRouterModelInfo>> = {}
const models: Record<string, Partial<OpenRouterModelInfo> & { supportedFeatures?: string[] }> = {}
try {
if (!basetenApiKey) {
console.log("No Baseten API key found, using static models as fallback")
// Don't throw an error, just use static models
// Don't throw an error, just use static models, althought this might be slightly out of date
for (const [modelId, modelInfo] of Object.entries(basetenModels)) {
models[modelId] = {
maxTokens: modelInfo.maxTokens,
@@ -69,25 +70,20 @@ export async function refreshBasetenModels(
continue
}
// Only include models that are listed in the static basetenModels
if (!(rawModel.id in basetenModels)) {
console.log(`Skipping model ${rawModel.id} - not in static basetenModels list`)
continue
}
// Check if we have static pricing information for this model
const staticModelInfo = basetenModels[rawModel.id as keyof typeof basetenModels]
const modelInfo: Partial<OpenRouterModelInfo> = {
maxTokens: staticModelInfo?.maxTokens || 8192,
contextWindow: staticModelInfo?.contextWindow || 8192,
supportsImages: staticModelInfo?.supportsImages || false,
const modelInfo: Partial<OpenRouterModelInfo> & { supportedFeatures?: string[] } = {
maxTokens: rawModel.max_completion_tokens || staticModelInfo?.maxTokens,
contextWindow: rawModel.context_length || staticModelInfo?.contextWindow,
supportsImages: false, // Baseten model APIs does not support image input
supportsPromptCache: staticModelInfo?.supportsPromptCache || false,
inputPrice: staticModelInfo?.inputPrice || 0,
outputPrice: staticModelInfo?.outputPrice || 0,
inputPrice: parsePrice(rawModel.pricing?.prompt) || staticModelInfo?.inputPrice || 0,
outputPrice: parsePrice(rawModel.pricing?.completion) || staticModelInfo?.outputPrice || 0,
cacheWritesPrice: staticModelInfo?.cacheWritesPrice || 0,
cacheReadsPrice: staticModelInfo?.cacheReadsPrice || 0,
description: generateModelDescription(rawModel, staticModelInfo),
supportedFeatures: rawModel.supported_features || [],
}
models[rawModel.id] = modelInfo
@@ -122,14 +118,12 @@ export async function refreshBasetenModels(
console.error("Baseten API Error:", errorMessage)
// If we failed to fetch models, try to read cached models first
const cachedModels = await readBasetenModels(controller)
const cachedModels = await readBasetenModels()
if (cachedModels && Object.keys(cachedModels).length > 0) {
console.log("Using cached Baseten models")
// Filter cached models to only include those in static basetenModels
// Use all cached models (no filtering)
for (const [modelId, modelInfo] of Object.entries(cachedModels)) {
if (modelId in basetenModels) {
models[modelId] = modelInfo
}
models[modelId] = modelInfo
}
} else {
// Fall back to static models from shared/api.ts
@@ -165,26 +159,18 @@ export async function refreshBasetenModels(
cacheReadsPrice: model.cacheReadsPrice ?? 0,
description: model.description ?? "",
tiers: model.tiers ?? [],
// Note: supportedFeatures is preserved as custom property but not part of OpenRouterModelInfo proto
}
}
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
}
/**
* Ensures the cache directory exists and returns its path
*/
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
/**
* Reads cached Baseten models from disk
*/
async function readBasetenModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
const basetenModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.basetenModels)
async function readBasetenModels(): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
const basetenModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.basetenModels)
const fileExists = await fileExistsAtPath(basetenModelsFilePath)
if (fileExists) {
try {
@@ -219,14 +205,47 @@ function isValidChatModel(rawModel: any): boolean {
* Generates a descriptive name for the model
*/
function generateModelDescription(rawModel: any, staticModelInfo?: any): string {
// Use static description if available
// Use static description if available and preferred
if (staticModelInfo?.description) {
return staticModelInfo.description
}
// Generate description based on model characteristics
const modelId = rawModel.id
const ownedBy = rawModel.owned_by || "Unknown"
// Use API description if available
if (rawModel.description) {
const contextWindow = rawModel.context_length
const quantization = rawModel.quantization
const features = rawModel.supported_features || []
return `${ownedBy} model: ${modelId}`
let description = rawModel.description
// Add technical details if available
const technicalDetails = []
if (contextWindow) {
technicalDetails.push(`${contextWindow.toLocaleString()} token context`)
}
if (quantization) {
technicalDetails.push(`${quantization} precision`)
}
if (features.length > 0) {
const featureList = features.join(", ")
technicalDetails.push(`supports ${featureList}`)
}
if (technicalDetails.length > 0) {
description += ` (${technicalDetails.join(", ")})`
}
return description
}
// Fallback: use name or model ID
const modelName = rawModel.name || rawModel.id
const contextWindow = rawModel.context_length
const ownedBy = rawModel.owned_by || "Baseten"
if (contextWindow) {
return `${ownedBy} ${modelName} with ${contextWindow.toLocaleString()} token context window`
}
return `${ownedBy} model: ${modelName}`
}
@@ -1,4 +1,4 @@
import { GlobalFileNames } from "@core/storage/disk"
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import { EmptyRequest } from "@shared/proto/cline/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "@shared/proto/cline/models"
import { fileExistsAtPath } from "@utils/fs"
@@ -16,7 +16,7 @@ import { Controller } from ".."
* @returns Response containing the Groq models
*/
export async function refreshGroqModels(controller: Controller, _request: EmptyRequest): Promise<OpenRouterCompatibleModelInfo> {
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.groqModels)
const groqApiKey = controller.stateManager.getSecretKey("groqApiKey")
@@ -165,7 +165,7 @@ export async function refreshGroqModels(controller: Controller, _request: EmptyR
* Reads cached Groq models from disk
*/
async function readGroqModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.groqModels)
const fileExists = await fileExistsAtPath(groqModelsFilePath)
if (fileExists) {
try {
@@ -246,12 +246,3 @@ function generateModelDescription(rawModel: any, staticModelInfo?: any): string
return `${ownedBy} model with ${contextWindow.toLocaleString()} token context window`
}
/**
* Ensures the cache directory exists and returns its path
*/
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
@@ -5,21 +5,9 @@ import { fileExistsAtPath } from "@utils/fs"
import axios from "axios"
import fs from "fs/promises"
import path from "path"
import { ensureCacheDirectoryExists } from "@/core/storage/disk"
import { Controller } from ".."
/**
* Ensures the cache directory exists and returns its path
*/
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
try {
await fs.mkdir(cacheDir, { recursive: true })
} catch (_error) {
// Directory might already exist
}
return cacheDir
}
/**
* Refreshes the Hugging Face models and returns the updated model list
* @param controller The controller instance
@@ -27,10 +15,10 @@ async function ensureCacheDirectoryExists(controller: Controller): Promise<strin
* @returns Response containing the Hugging Face models
*/
export async function refreshHuggingFaceModels(
controller: Controller,
_controller: Controller,
_request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const huggingFaceModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), "huggingface_models.json")
const huggingFaceModelsFilePath = path.join(await ensureCacheDirectoryExists(), "huggingface_models.json")
let models: Record<string, OpenRouterModelInfo> = {}
@@ -0,0 +1,142 @@
import { StringRequest } from "@shared/proto/cline/common"
import { OcaCompatibleModelInfo, OcaModelInfo } from "@shared/proto/cline/models"
import axios from "axios"
import { HostProvider } from "@/hosts/host-provider"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
import { DEFAULT_OCA_BASE_URL } from "@/services/auth/oca/utils/constants"
import { createOcaHeaders, getAxiosSettings } from "@/services/auth/oca/utils/utils"
import { Logger } from "@/services/logging/Logger"
import { ShowMessageType } from "@/shared/proto/index.host"
import { Controller } from ".."
/**
* Refreshes the Oca models and returns the updated model list
* @param controller The controller instance
* @param request Empty request object
* @returns Response containing the Oca models
*/
export async function refreshOcaModels(controller: Controller, request: StringRequest): Promise<OcaCompatibleModelInfo> {
const parsePrice = (price: any) => {
if (price) {
return parseFloat(price) * 1_000_000
}
return undefined
}
const models: Record<string, OcaModelInfo> = {}
let defaultModelId: string | undefined
const ocaAccessToken = await OcaAuthService.getInstance().getAuthToken()
if (!ocaAccessToken) {
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Not authenticated with OCA. Please sign in first.",
})
return OcaCompatibleModelInfo.create({ error: "Not authenticated with OCA" })
}
const baseUrl = request.value || DEFAULT_OCA_BASE_URL
const modelsUrl = `${baseUrl}/v1/model/info`
const headers = await createOcaHeaders(ocaAccessToken!, "models-refresh")
try {
Logger.log(`Making refresh oca model request with customer opc-request-id: ${headers["opc-request-id"]}`)
const response = await axios.get(modelsUrl, { headers, ...getAxiosSettings() })
if (response.data?.data) {
if (response.data.data.length === 0) {
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "No models found. Did you set up your OCA access (possibly through entitlements)?",
})
}
for (const model of response.data.data) {
const modelId = model.litellm_params?.model
if (typeof modelId !== "string" || !modelId) {
continue
}
if (!defaultModelId) {
defaultModelId = modelId
}
const modelInfo = model.model_info
models[modelId] = OcaModelInfo.create({
maxTokens: model.litellm_params?.max_tokens || -1,
contextWindow: modelInfo.context_window,
supportsImages: modelInfo.supports_vision || false,
supportsPromptCache: modelInfo.supports_caching || false,
inputPrice: parsePrice(modelInfo.input_price) || 0,
outputPrice: parsePrice(modelInfo.output_price) || 0,
cacheWritesPrice: parsePrice(modelInfo.caching_price) || 0,
cacheReadsPrice: parsePrice(modelInfo.cached_price) || 0,
description: modelInfo.description,
thinkingConfig: modelInfo.thinking_config,
surveyContent: modelInfo.survey_content,
surveyId: modelInfo.survey_id,
temperature: modelInfo.temperature || 0,
banner: modelInfo.banner,
modelName: modelId,
})
}
console.log("OCA models fetched", models)
// Fetch current config
const apiConfiguration = controller.stateManager.getApiConfiguration()
const updatedConfig = { ...apiConfiguration }
// Which mode(s) to update?
const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
const planModeSelectedModelId =
apiConfiguration?.planModeOcaModelId && models[apiConfiguration.planModeOcaModelId]
? apiConfiguration.planModeOcaModelId
: defaultModelId!
const actModeSelectedModelId =
apiConfiguration?.actModeOcaModelId && models[apiConfiguration.actModeOcaModelId]
? apiConfiguration.actModeOcaModelId
: defaultModelId!
// Save new model selection(s) to configuration object, per plan/act mode setting
if (planActSeparateModelsSetting) {
if (currentMode === "plan") {
updatedConfig.planModeOcaModelId = planModeSelectedModelId
updatedConfig.planModeOcaModelInfo = models[planModeSelectedModelId]
} else {
updatedConfig.actModeOcaModelId = actModeSelectedModelId
updatedConfig.actModeOcaModelInfo = models[actModeSelectedModelId]
}
} else {
updatedConfig.planModeOcaModelId = planModeSelectedModelId
updatedConfig.planModeOcaModelInfo = models[planModeSelectedModelId]
updatedConfig.actModeOcaModelId = actModeSelectedModelId
updatedConfig.actModeOcaModelInfo = models[actModeSelectedModelId]
}
controller.stateManager.setApiConfiguration(updatedConfig)
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
message: `Refreshed OCA models from ${baseUrl}`,
})
await controller.postStateToWebview?.()
} else {
console.error("Invalid response from OCA API")
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to fetch OCA models. Please check your configuration from ${baseUrl}`,
})
}
} catch (err) {
let userMsg
if (err.response) {
// The request was made and the server responded with a status code that falls out of the range of 2xx
userMsg = `Did you set up your OCA access (possibly through entitlements)? OCA service returned ${err.response.status} ${err.response.statusText}.`
} else if (err.request) {
// The request was made but no response was received
userMsg = `Unable to access the OCA backend. Is your endpoint and proxy configured properly? Please see the troubleshooting guide.`
} else {
userMsg = err.message
console.error(userMsg, err)
}
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `Error refreshing OCA models. ` + userMsg + ` opc-request-id: ${headers["opc-request-id"]}`,
})
return OcaCompatibleModelInfo.create({ error: userMsg })
}
return OcaCompatibleModelInfo.create({ models })
}
@@ -1,12 +1,11 @@
import { GlobalFileNames } from "@core/storage/disk"
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import { EmptyRequest } from "@shared/proto/cline/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "@shared/proto/cline/models"
import { fileExistsAtPath } from "@utils/fs"
import axios from "axios"
import cloneDeep from "clone-deep"
import fs from "fs/promises"
import path from "path"
import { CLAUDE_SONNET_4_1M_TIERS, clineMicrowaveAlphaModelInfo, openRouterClaudeSonnet41mModelId } from "@/shared/api"
import { CLAUDE_SONNET_4_1M_TIERS, clineCodeSupernovaModelInfo, openRouterClaudeSonnet41mModelId } from "@/shared/api"
import { Controller } from ".."
type OpenRouterSupportedParams =
@@ -77,9 +76,9 @@ export async function refreshOpenRouterModels(
controller: Controller,
_request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
let models: Record<string, OpenRouterModelInfo> = {}
const models: Record<string, OpenRouterModelInfo> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models")
@@ -109,6 +108,7 @@ export async function refreshOpenRouterModels(
})
switch (rawModel.id) {
case "anthropic/claude-sonnet-4.5":
case "anthropic/claude-sonnet-4":
// NOTE: we artificially restrict the context window to 200k to keep costs low for users, and have a :1m model variant created below for users that want to use the full 1m.
modelInfo.contextWindow = 200_000
@@ -221,22 +221,6 @@ export async function refreshOpenRouterModels(
models[openRouterClaudeSonnet41mModelId] = claudeSonnet41mModelInfo
}
}
// Add hardcoded cline/sonic model
models["cline/sonic"] = OpenRouterModelInfo.create({
maxTokens: clineMicrowaveAlphaModelInfo.maxTokens ?? 0,
contextWindow: clineMicrowaveAlphaModelInfo.contextWindow ?? 0,
supportsImages: clineMicrowaveAlphaModelInfo.supportsImages ?? false,
supportsPromptCache: clineMicrowaveAlphaModelInfo.supportsPromptCache ?? false,
inputPrice: clineMicrowaveAlphaModelInfo.inputPrice ?? 0,
outputPrice: clineMicrowaveAlphaModelInfo.outputPrice ?? 0,
cacheWritesPrice: clineMicrowaveAlphaModelInfo.cacheWritesPrice ?? 0,
cacheReadsPrice: clineMicrowaveAlphaModelInfo.cacheReadsPrice ?? 0,
description: clineMicrowaveAlphaModelInfo.description ?? "",
thinkingConfig: clineMicrowaveAlphaModelInfo.thinkingConfig ?? undefined,
supportsGlobalEndpoint: clineMicrowaveAlphaModelInfo.supportsGlobalEndpoint ?? undefined,
tiers: clineMicrowaveAlphaModelInfo.tiers ?? [],
})
} else {
console.error("Invalid response from OpenRouter API")
}
@@ -246,38 +230,45 @@ export async function refreshOpenRouterModels(
console.error("Error fetching OpenRouter models:", error)
// If we failed to fetch models, try to read cached models
const cachedModels = await readOpenRouterModels(controller)
const cachedModels = await controller.readOpenRouterModels()
if (cachedModels) {
models = cachedModels
return OpenRouterCompatibleModelInfo.create({ models: cachedModels })
}
}
return OpenRouterCompatibleModelInfo.create({ models })
// Append stealth models if any
return OpenRouterCompatibleModelInfo.create({ models: appendClineStealthModels(models) })
}
/**
* Reads cached OpenRouter models from disk
* Stealth models are models that are compatible with the OpenRouter API but not listed on the OpenRouter website or API.
*/
async function readOpenRouterModels(controller: Controller): Promise<Record<string, OpenRouterModelInfo> | undefined> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
if (fileExists) {
try {
const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8")
return JSON.parse(fileContents)
} catch (error) {
console.error("Error reading cached OpenRouter models:", error)
return undefined
}
}
return undefined
const CLINE_STEALTH_MODELS: Record<string, OpenRouterModelInfo> = {
"cline/code-supernova-1-million": OpenRouterModelInfo.create({
maxTokens: clineCodeSupernovaModelInfo.maxTokens ?? 0,
contextWindow: clineCodeSupernovaModelInfo.contextWindow ?? 0,
supportsImages: clineCodeSupernovaModelInfo.supportsImages ?? false,
supportsPromptCache: clineCodeSupernovaModelInfo.supportsPromptCache ?? false,
inputPrice: clineCodeSupernovaModelInfo.inputPrice ?? 0,
outputPrice: clineCodeSupernovaModelInfo.outputPrice ?? 0,
cacheWritesPrice: clineCodeSupernovaModelInfo.cacheWritesPrice ?? 0,
cacheReadsPrice: clineCodeSupernovaModelInfo.cacheReadsPrice ?? 0,
description: clineCodeSupernovaModelInfo.description ?? "",
thinkingConfig: clineCodeSupernovaModelInfo.thinkingConfig ?? undefined,
supportsGlobalEndpoint: clineCodeSupernovaModelInfo.supportsGlobalEndpoint ?? undefined,
tiers: clineCodeSupernovaModelInfo.tiers ?? [],
}),
// Add more stealth models here as needed
}
/**
* Ensures the cache directory exists and returns its path
*/
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
export function appendClineStealthModels(
currentModels: Record<string, OpenRouterModelInfo>,
): Record<string, OpenRouterModelInfo> {
// Create a shallow clone of the current models to avoid mutating the original object
const cloned = { ...currentModels }
for (const [modelId, modelInfo] of Object.entries(CLINE_STEALTH_MODELS)) {
if (!cloned[modelId]) {
cloned[modelId] = modelInfo
}
}
return cloned
}
@@ -21,10 +21,14 @@ export async function refreshRequestyModels(controller: Controller, _: EmptyRequ
const models: Record<string, OpenRouterModelInfo> = {}
try {
const apiKey = controller.stateManager.getSecretKey("requestyApiKey")
const baseUrl = controller.stateManager.getGlobalStateKey("requestyBaseUrl")
const baseUrl = controller.stateManager.getGlobalSettingsKey("requestyBaseUrl")
const resolvedUrl = toRequestyServiceUrl(baseUrl)
const url = new URL(`${resolvedUrl.pathname}/models`, resolvedUrl).toString()
const url = resolvedUrl != null ? new URL(`${resolvedUrl.pathname}/models`, resolvedUrl).toString() : undefined
if (url == null) {
throw new Error("URL is not valid.")
}
const headers = {
Authorization: `Bearer ${apiKey}`,
@@ -1,4 +1,4 @@
import { GlobalFileNames } from "@core/storage/disk"
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import { EmptyRequest } from "@shared/proto/cline/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "@shared/proto/cline/models"
import { fileExistsAtPath } from "@utils/fs"
@@ -14,13 +14,10 @@ import { Controller } from ".."
* @returns Response containing Vercel AI Gateway models
*/
export async function refreshVercelAiGatewayModels(
controller: Controller,
_controller: Controller,
_request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const vercelAiGatewayModelsFilePath = path.join(
await ensureCacheDirectoryExists(controller),
GlobalFileNames.vercelAiGatewayModels,
)
const vercelAiGatewayModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.vercelAiGatewayModels)
let models: Record<string, OpenRouterModelInfo> = {}
@@ -65,7 +62,7 @@ export async function refreshVercelAiGatewayModels(
console.error("Error fetching Vercel AI Gateway models:", error)
// If we failed to fetch models, try to read cached models
const cachedModels = await readVercelAiGatewayModels(controller)
const cachedModels = await readVercelAiGatewayModels()
if (cachedModels) {
models = cachedModels
}
@@ -77,11 +74,8 @@ export async function refreshVercelAiGatewayModels(
/**
* Reads cached Vercel AI Gateway models from disk
*/
async function readVercelAiGatewayModels(controller: Controller): Promise<Record<string, OpenRouterModelInfo> | undefined> {
const vercelAiGatewayModelsFilePath = path.join(
await ensureCacheDirectoryExists(controller),
GlobalFileNames.vercelAiGatewayModels,
)
async function readVercelAiGatewayModels(): Promise<Record<string, OpenRouterModelInfo> | undefined> {
const vercelAiGatewayModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.vercelAiGatewayModels)
const fileExists = await fileExistsAtPath(vercelAiGatewayModelsFilePath)
if (fileExists) {
try {
@@ -94,12 +88,3 @@ async function readVercelAiGatewayModels(controller: Controller): Promise<Record
}
return undefined
}
/**
* Ensures the cache directory exists and returns its path
*/
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
@@ -28,7 +28,7 @@ export async function updateApiConfigurationProto(
// Update the task's API handler if there's an active task
if (controller.task) {
const currentMode = await controller.getCurrentMode()
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
controller.task.api = buildApiHandler({ ...appApiConfiguration, ulid: controller.task.ulid }, currentMode)
}
@@ -0,0 +1,15 @@
import { EmptyRequest, String as ProtoString } from "@shared/proto/cline/common"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
import { Controller } from "../index"
/**
* Handles the user clicking the login link in the UI.
* Generates a secure nonce for state validation, stores it in secrets,
* and opens the authentication URL in the external browser.
*
* @param controller The controller instance.
* @returns The login URL as a string.
*/
export async function ocaAccountLoginClicked(_controller: Controller, _: EmptyRequest): Promise<ProtoString> {
return await OcaAuthService.getInstance().createAuthRequest()
}
@@ -0,0 +1,14 @@
import type { EmptyRequest } from "@shared/proto/cline/common"
import { Empty } from "@shared/proto/cline/common"
import type { Controller } from "../index"
/**
* Handles the account logout action
* @param controller The controller instance
* @param _request The empty request object
* @returns Empty response
*/
export async function ocaAccountLogoutClicked(controller: Controller, _request: EmptyRequest): Promise<Empty> {
await controller.handleOcaSignOut()
return Empty.create({})
}
@@ -0,0 +1,14 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { OcaAuthState } from "@shared/proto/cline/oca_account"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
import { Controller } from ".."
import { StreamingResponseHandler } from "../grpc-handler"
export async function ocaSubscribeToAuthStatusUpdate(
_controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler<OcaAuthState>,
requestId?: string,
): Promise<void> {
return OcaAuthService.getInstance().subscribeToAuthStatusUpdate(request, responseStream, requestId)
}
@@ -0,0 +1,20 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { ProcessInfo } from "@shared/proto/cline/state"
import { Controller } from ".."
/**
* Gets process information including PID, version, and uptime
* @param controller The controller instance
* @param request Empty request
* @returns ProcessInfo with process details
*/
export async function getProcessInfo(controller: Controller, request: EmptyRequest): Promise<ProcessInfo> {
// Get the current state to access the version (same source as webview)
const state = await controller.getStateToPostToWebview()
return ProcessInfo.create({
processId: process.pid,
version: state.version || "unknown",
uptimeMs: Math.floor(process.uptime() * 1000), // Convert seconds to milliseconds
})
}

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