Compare commits

..
Author SHA1 Message Date
abeatrix ba464b2845 refactor: add parallel command execution architecture documentation
Add comprehensive documentation for the parallel command execution system that enables multiple `execute_command` tools to run concurrently without conflicts.

**Key Changes:**
- Document queue-based ask/response system using `PendingAskQueue` to handle concurrent ask operations with unique IDs
- Explain `ConcurrentCommandOrchestrator` that streams output via `say()` instead of blocking on `ask()` calls
- Detail orchestration flow with `isExecutingInParallel` flag and parallel-safe command execution
- List key implementation files and their responsibilities

**Why:**
The previous single-state approach (`askResponse`/`askResponseText`) caused conflicts when multiple commands tried to use `ask()` simultaneously. This new architecture uses a queue with unique ask IDs to safely handle concurrent interactive operations during parallel tool execution.

**Implementation:**
- `PendingAskQueue` manages concurrent asks with FIFO response resolution
- `ConcurrentCommandOrchestrator` provides non-blocking output streaming
- Flags (`isExecutingInParallel`, `setParallelExecution`) control orchestrator selection
- Proper cleanup in finally blocks ensures state consistency
<response_metadata>
{
  "conventional_commit_type": "docs",
  "scope": "parallel-execution",
  "breaking_change": false
}
</response_metadata>
2026-01-26 12:53:54 -08:00
Bee df1d33c751 feat: add auto-generation of state proto (#8555)
* feat:  add auto-generation of state proto

Add lint-staged hook to automatically regenerate proto/cline/state.proto
when src/shared/storage/state-keys.ts changes. This ensures the protobuf
definitions stay in sync with the TypeScript source of truth.

Changes:
- Add generate-state-proto.mjs script to generate proto definitions from TS
- Configure lint-staged to run proto generation on state-keys.ts changes
- Update state.proto with regenerated field numbers and new OpenTelemetry fields

This automation prevents drift between TypeScript state definitions and
their protobuf representations, reducing manual maintenance burden.

* PlanActMode

* feat(proto): change thinking budget token fields to int64

Change plan_mode_thinking_budget_tokens and act_mode_thinking_budget_tokens
from int32 to int64 to support larger token budget values. Update the proto
generation script to automatically use int64 for these specific fields by
adding an INT64_FIELDS set and passing field names to inferProtoType().

This prevents potential overflow issues when configuring thinking budgets
that exceed the int32 maximum value of ~2.1 billion tokens.

* feat(proto): change auto_condense_threshold type from int32 to double

Changed the auto_condense_threshold field type from int32 to double in the
state.proto file to support decimal values. Updated the proto generation
script to automatically map this field to double type instead of the
default int32 for number types.

* add documentation for proto field generation

Add inline documentation to state.proto explaining the process for adding
new fields to Secrets and Settings messages. Also add a note in state-keys.ts
clarifying that the generate-state-proto.mjs script runs automatically on
commit. Remove redundant sync comment from API_HANDLER_SETTINGS_FIELDS.

* fix comment format

* open_ai_headers
2026-01-15 14:28:23 -08:00
361494d18f refactor: History View UI (#8563)
* refactor: History UI Renew

* update

* udpate styles

* Create wild-ears-poke.md

* Update webview-ui/src/components/history/HistoryView.tsx

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

* clean up

* remove unused styles

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
2026-01-15 13:02:07 -08:00
Tomás Barreiro dca0a8fa3e Refactor fetching remote config to reduce the number of requests (#8115)
* Remove the remote config auth listener

* Introduce a throttle RemoteConfigService

* Add changeset

* Change the interval to an hour

* Refactor

* Reintroduce comment and remove await

* Move the fetchRemoteConfig to the initTask function
2026-01-15 21:12:24 +01:00
MaxandMax Paulus 🥪 6d7213dc6a gpt 5.2 codex banner fix and version bump (#8642)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-01-15 11:57:36 -08:00
Bee 97a35d3868 fix: remove error_retry when duplicate or after retry succeeds (#8614)
* fix: remove error_retry when duplicate or retry succeeds

Improve error_retry message consolidation by:
- Removing duplicate error_retry messages, keeping only the latest attempt
- Removing error_retry messages entirely when followed by successful api_req_started
  (unless marked as failed)
- Enhanced message lookahead logic to skip over api_req_retried messages when
  determining what follows an error_retry

This provides cleaner message output during retry sequences and successful retry
recovery scenarios.

* add changeset

* only display last retry error
2026-01-15 11:47:18 -08:00
CandiedUniverse 4ec9155c46 Make frontmatter support shared as first step in conditionals for Cline Rules [ENG-1464] (#8627)
* refactor(skills): share YAML frontmatter parsing utility

* docs(frontmatter): explain parse result fields
2026-01-15 10:33:52 -08:00
tjandy98andMax bffca989a1 Add claude 4.5 haiku (#8057)
* Add claude 4.5 haiku

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

* Create big-cows-ring.md

* Update maxTokens

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

---------

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>
Co-authored-by: Max <maxpaulus43@gmail.com>
2026-01-15 10:03:35 -08:00
Lize Cai 0133b5d030 Sap add claude opus 4.5 to SAP AI Core Provider (#8421)
* add anthropic--claude-4.5-opus into sap provider.

Signed-off-by: Lize Cai <lize.cai@sap.com>

* add changeset

Signed-off-by: Lize Cai <lize.cai@sap.com>

---------

Signed-off-by: Lize Cai <lize.cai@sap.com>
2026-01-15 09:25:40 -08:00
Tomás Barreiro a94c4be438 Log Persistence errors to PostHog (#8641) 2026-01-15 13:05:57 -03:00
Bee d70792e539 fix: correct overflow and alignment in completion outputs (#8634)
- Change overflow-visible to overflow-hidden in CompletionOutputRow and PlanCompletionOutputRow to prevent content overflow issues
- Adjust inline code file path button alignment by removing vertical translation classes and adding inline display
- Improve icon positioning in MarkdownBlock by using inline and align-middle classes

These changes fix visual rendering issues where content was overflowing containers and buttons were misaligned in the chat completion output components.
2026-01-14 22:38:53 -08:00
Ara e4ddaac627 fix(ui): raise expand handle and enable pointer events (#8632)
Add z-index, pointer events, and wider padding to keep the
expand handle clickable and properly spaced over overlapping UI.
2026-01-14 21:59:08 -08:00
Tomás Barreiro 9478b600aa Remove spammy banners log (#8631) 2026-01-15 06:57:09 +01:00
Antonio Di Monaco d194e47bf6 Fix: StreamableHttp MCP reconnection (#8367) (#8611) 2026-01-14 16:04:54 -08:00
github-actions[bot]andArafatkatze c9ff9cf1d5 v3.50.0 Release Notes (#8574)
- Add gpt-5.2-codex OpenAI model support
- Add create-pull-request skill

- Fix the selection of remotely configured providers
- Fix act_mode_respond to prevent consecutive calls
- Fix invalid tool call IDs when switching between model formats

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-14 15:23:28 -08:00
d7fa6b33c1 fix: normalize tool call IDs for OpenAI messages (#8623)
* fix: normalize tool call IDs for OpenAI messages

Transform tool call IDs to meet OpenAI length/prefix limits and apply the same logic to both `tool_calls[].id` and `tool_call_id` so they always match, preventing invalid parameter errors. Also enforce 53-char `fc_` IDs for the Responses API and add a helper to detect that format.

Ensure that whatever ID is produced for the tool_calls[].id in the assistant message matches what's produced for tool_call_id in the tool result message.

* add changeset

* refactor: move isOpenAIResponseToolId and fix tool ID truncation

- Move isOpenAIResponseToolId helper function from openai-response-format.ts
  to openai-format.ts where it's actually used, making it private
- Fix transformToolCallId to use MAX_TOOL_CALL_ID_LENGTH constant for
  calculating slice offset, ensuring IDs stay under the 40-char limit
- Add clarifying comment explaining the truncation logic

* fix: correct function call ID prefix check in OpenAI response format

Fix startsWith check to use "fc_" instead of "fc" to properly detect

* Fix tool call length

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

---------

Co-authored-by: Robin Newhouse <robin@cline.bot>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2026-01-14 15:14:44 -08:00
Robin Newhouse 8f521e7ea3 Add gpt-5.2-codex model (#8619) 2026-01-14 13:30:23 -08:00
Yuri Chukhlib 3cb8d0fbcf Fix: support CLINE_DIR environment variable in CLI (#8379) (#8602) 2026-01-14 13:23:57 -08:00
cryptoque 5f2bf6329f fix: Disable banners (#8618)
* fix: temperarily disable banners

* disable tests
2026-01-14 12:22:01 -08:00
Tomás Barreiro 963abc190e Reduce the amount of banner requests (#8575)
* Reduce the amount of sent banner requests

* Revert not fetching if no token is provided

* Remove redundant null

* Make a single call

* Make another request if forceRefresh is true

* Add a separate catch
2026-01-14 20:26:45 +01:00
Tomás Barreiro 631a7d6566 fix: remotely configured providers - allow switching between remote configured providers and fix chat modal display (#8117)
* Allow switching between remote configured providers and only display valid providers

* Add changeset

* Return the provider set by the remote config

* Address comments

* Address comment

* Validate when updating settings

* Refactor

* Revert

* Use a more descriptive name

* Fix types

* Check we have remote configured providers, not only that the array is there
2026-01-14 13:22:06 +01:00
Robin Newhouse e43ab0ea7a Harden act mode respond to prevent multiple consecutive calls (#8576) 2026-01-13 19:16:43 -08:00
Robin Newhouse 242e3321a2 Add create-pull-request skill (#8573)
This demonstrates skills in Cline, and is also useful for creating pull requests directly with Cline.
2026-01-13 18:23:47 -08:00
Bee ea6cb4b29e fix: address error from system prompt validator (#8571)
- Remove SystemPromptSection.MCP from Gemini-3 component order
- Disable feedback section in XS variant component overrides
- Update variant validator to allow disabled overrides without requiring them in componentOrder/tools list

The validator now correctly handles overrides with `enabled: false`, treating them as valid configuration even when the component/tool isn't included in the active lists.
2026-01-13 17:17:52 -08:00
Ara 2c75285566 Update Package lock.json for release (#8569) 2026-01-13 14:27:17 -08:00
github-actions[bot]andArafatkatze 8279f2e145 Release notes for v3.49.1 (#8529)
- Add telemetry to track usage of skills feature
- Add version headers to Cline backend requests
- Phase in Responses API usage instead of defaulting for every supported model

- Fix workflow slash command search to be case-insensitive
- Fix model display in ModelPickerModal when using LiteLLM
- Fix LiteLLM model fetching with default base URL
- Fix crash when OpenAI-compatible APIs send usage chunks with empty or null choices arrays at end of streaming
- Fix model ID for Kat Coder Pro Free model

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-13 12:24:30 -08:00
Ara 5b94ba3ef9 Fix the model id for KatCoder Pro free models (#8558)
* Fix the model id for KatCoder Pro free models

* Fix the model id for KatCoder Pro free models

* Fix the model id for KatCoder Pro free models
2026-01-13 09:47:59 -08:00
MaxandMax Paulus 🥪 703146182a add cline pr review as a github workflow (#8434)
fix pr review workflow

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-01-13 09:04:47 -08:00
BeeandSaoud Rizwan 9603643b77 refactor: Chat Streaming UI (#8264)
* feat(ui): add new row components and unify ChatRow styling with tailwinds and lucid icons

- Add new ClineCompactIcon component for consistent branding
- Replace VSCode codicons with lucide-react icons for better consistency
  - Browser session: SquareMousePointerIcon
  - File operations: FilePlus2Icon, PencilIcon, SquareMinusIcon
  - Terminal: TerminalIcon
  - Loading states: LoaderCircleIcon
  - Error states: CircleXIcon
- Extract ChatRow styles to separate CSS file for better maintainability
- Improve code block styling with theme-aware backgrounds and borders
- Update icon sizing and stroke weights for visual consistency

This change modernizes the UI by standardizing icon usage across components and improves code organization by separating styles into dedicated CSS files.

* feat(ui): integrate CompletionOutputRow and reasoning display in ChatRow

Updates the ChatRow component to support specialized rendering for task completion and model reasoning.

- Integrates `CompletionOutputRow` and `PlanCompletionOutputRow` for structured completion states.
- Adds `ThinkingRow` integration and props for handling `reasoningContent`.
- Updates `ChatRowProps` to include mode and request status tracking.
- Refines Storybook mocks to demonstrate reasoning steps and detailed completion results.

* feat(webview): group low-stakes tool executions in chat view

- Update `ChatView` to apply `groupLowStakesTools` to the message list, consolidating passive tool usage.
- Overhaul `MessageRenderer` to support rendering grouped tool messages with specific display info (icons, labels) for actions like `readFile`, `listFiles`, and `searchFiles`.
- Add logic to format search regex patterns for better readability.
- Implement utility checks for calculating costs and pending states within tool groups.
- This change reduces UI clutter by visually collapsing repetitive information-gathering steps.

* refactor(ui): update checkpoint control UI and restore menu

- Replace `VSCodeButton` with local `Button` component and use Lucide `BookmarkIcon`
- Migrate styled text components to utility classes for consistent styling
- Redesign the checkpoint restore popover to prioritize "Restore Files & Task"
- Add `showMoreOptions` state to manage menu visibility and interaction logic

* refactor(chat): rename CSS file for CompletionOutputRow

Renames `ChatRow.css` to `CompletionOutputRow.css` to align with the component naming convention. This change includes updating the import in `CompletionOutputRow.tsx` to reference the correctly named stylesheet.

* clean up PlanCompletionOutputRow

* clean up

* clean up

* update e2e

* update displayName

* fix blinking cursor position

* use classnames

* Completion notch

* clean up header class

* Move Command Output component to CommandOutputRow

* Fix shimmering animation

* update TypewriterText story title

* clean up notch style

* Seperate ToolGroupRenderer into individual component. Clean up styles and message utils.

* fix truncation display

* update styles for open file links

* apply feedback - fix CompletionOutputRow & ThinkingRow

* Display old Ask block for tools

* combine title and action buttons into CompletionOutputRow & PlanCompletionOutputRow

* remove animation from Cline icon

* update styles and animation

* adjust spacing

* Fix shimmering animation

* clean up

* clean up and simplify component styles

* clean up import names

* fix markdown block and use tailwind styles

* clean up spacing

* hide scrollbar

* remove expand handler

* cline logo position

* fix(chat): align logo to top in request progress indicator

Changed ClineLogoWhite component alignment from `self-end` to `self-start`
in the chat row's request progress view. This ensures the logo aligns to
the top rather than the bottom when displaying in-progress requests,
improving visual consistency with the adjacent message content.

* fix DiffEditRow title truncation

* Keep Cline logo for output text

* fix(chat): add invisible spacer for non-rendered rows

Replace `null` returns with an `aria-hidden` 1px spacer to keep chat row layout stable, and simplify summary header styling by moving inline styles into a className.

* update activity indicators and button styling for tool group

- Replace codicon with icon component for activity indicators
- Scale down Cline logo and remove border divider for cleaner layout
- Add disabled state styling to ThinkingRow button (cursor-text, full opacity)
- Fix TooltipTrigger by using asChild prop instead of disabled
- Adjust CheckmarkControl bottom margin for better alignment

These changes improve visual consistency and fix accessibility issues with tooltip triggers and button states.
<budget:token_budget>200000</budget:token_budget>

* revert: show cline logo during stream only

* remove streaming thinking title

* spacing

* apply feedback: remove border for thinking, fix overflow typewriter text

* fix(ui): align thinking text and reasoning content positions

- Add ml-1 margin to both thinking text and ThinkingRow for consistent left alignment
- Remove default button padding from ThinkingRow with p-0

* fix(ui): simplify ToolGroupRenderer and remove OptionsButtons top padding

- Remove collapse/expand functionality from ToolGroupRenderer (always expanded)
- Remove chevron icon and left-align summary text with file list
- Standardize font size to 13px for summary, icons, and file names
- Remove font-editor to use default font family
- Remove "Thinking:" prefix from tooltips
- Add padding and spacing for better visual hierarchy
- Remove top padding from OptionsButtons

* fix(ui): restore CodeAccordian padding and overflow

* fix(ui): reduce spacing between header and content text

* fix(ui): restore task completion buttons to original style

- Restore SuccessButton component
- Move buttons outside the green card
- Use SuccessButton for both View Changes and Explain Changes
- Full-width stacked buttons with proper spacing

* fix(ui): polish Task Completed and Plan Created card styling

- Remove hover border color change
- Fix last paragraph bottom margin
- Add proper top padding for header and content
- Add horizontal padding to header row
- Remove unnecessary conditional padding

* fix(ui): style tweaks for copy button and checkpoint label

- Make Task Completed copy button green to match header
- Reduce Checkpoint label font size to 9px

* fix(ui): prevent TypewriterText from jumping on completion

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-01-12 22:11:37 -08:00
Bee c6dce7fb17 feat: force dependency pre-bundling in vite config (#8550)
Add optimizeDeps configuration with force flag to ensure Vite
re-optimizes dependencies on every build. This resolves potential
issues with stale or inconsistent dependency resolution in the
webview build process.
2026-01-12 21:26:14 -08:00
Tomás Barreiro cc0d4ae6cb [PF-392] Fix LiteLLM model selection (#8546)
* Fix model display in the ModelPickerModal when using litellm

* Add changeset

* Cleanup

* Fix model selection
2026-01-13 05:00:57 +01:00
BeeandTomás Barreiro a9365e30e9 refactor: simplify API configuration management and state handling (#8415)
* refactor: simplify API configuration management and state handling

Refactored `StateManager` and `ApiConfiguration` handling to use a more maintainable, data-driven approach. Replaced manual key mapping in `setApiConfiguration` with automated categorization based on static definitions.

- Updated `buildApiHandler` and `createHandlerForProvider` to accept `Partial<ApiConfiguration>`, improving flexibility.
- Introduced `categorizeApiConfigurationKeys` and other helpers to separate settings from secrets automatically.
- Centralized secret key definitions in `state-keys.ts` to reduce boilerplate and potential for errors when adding new providers.
- Cleaned up redundant imports and type definitions across the core API and storage modules.

* apply feedback

* clean up

* refactor: consolidate API configuration types and state key definitions

- Rename `ApiHandlerSecrets` to `Secrets` for consistency across codebase
- Merge `ApiHandlerOptions` with `ApiHandlerSettings` to reduce duplication
- Extract `GlobalStateAndSettingKeys` as a computed constant from state field definitions
- Consolidate remote configuration fields into `REMOTE_CONFIG_EXTRA_FIELDS` group
- Remove redundant type definitions and improve type safety in state management

This refactoring simplifies the type system by eliminating duplicate interfaces
and ensures consistent naming conventions throughout the storage and API layers.

* Clean up

* rename type with default

* type safe

* add unit test

* Apply suggestions from code review

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

* apply feedback

---------

Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
2026-01-12 19:45:47 -08:00
canvrno efe468d9b1 Add telemetry for skills feature (#8548)
* Added telemetry for skills feature

* Use safeCapture for skills telemetry capture

* Include skill source in skill telemetry
2026-01-12 19:38:36 -08:00
Tomás Barreiro 1b6202604d Fix remote config check (#8549) 2026-01-13 04:11:17 +01:00
cryptoqueandSarah Fortune ea1dbd8bea feat: When remote config is enabled, add logic for enterprise to control local MCPs via remote config (#8175)
* add requirements

* add requirements checklist

* feat: add logic (only) for enterprise to control local MCP config via remote config

* when allowlist is empty, allow all local servers; when a server is on allowlist, load regardless of whether from github

* fix comment, use Object.keys(remoteConfig).length to check if remote config is on or not

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2026-01-12 18:35:42 -08:00
Saoud Rizwan e43517519e fix(e2e): increase getSidebar timeout for slower macOS CI runners (#8547) 2026-01-12 17:15:53 -08:00
520d08c5f2 Send basic telemetry headers when making requests to the Cline backend (#8413)
* Send basic telemetry headers when making requests to the Cline backend

* Add changeset

* Update src/services/account/ClineAccountService.ts

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2026-01-13 01:23:56 +01:00
Chaitanya Erankiandcelestial-vault 31a55ac87e fix: modelInfo for Oracle Code Assist provider not being saved in Cline CLI and removing extra log statements (#8447)
* Fixed bugs within cline cli and removed extra console.log

* Removed old models from using Responses API

* Revertred last commit'

* Added changeset

---------

Co-authored-by: celestial-vault <58194240+celestial-vault@users.noreply.github.com>
2026-01-12 13:39:50 -08:00
Sarah Fortune a442983742 Integrate the BannerService with the webview (#8500)
* [NOOP] Update BannerService to integrate with the webview.

Update the BannerService to convert the banners to the BannerCardData format for the webview.

Add a field for the banners in the `ExtensionState`.

In the WelcomeSection, get the banners from the extension state and show them in the webview.

NOOP- this is a currently a no-op because the controller is not yet populating the `banners` field in the extension state. I will submit that in a second PR because we need the handlers for the dismissal logic before we can start displaying the banners.

# Conflicts:
#	src/shared/ExtensionMessage.ts

* Update tests

* Validate the banner action type before sending it to the webview

* Handle dimiss for API banners

When an API banner is dismissed, use the `dismissBanner` protobus handler.

Add warning comments saying not to use the old banner version system. This not scalable as it requires a different protobus handlers for each type of banner. You can get the same effect by using the banner ID and appending a version number to the ID.

* Send the banners from the extension to the webview

The controllers populates the banners in the extension state.
Add a check for buttons with empty titles because they don't render properly and this is an error in the banner configuration if it happens.

* Add handler to Link action button in the webview.

* Fix handler for ShowApiSettings in the webview
2026-01-12 13:27:51 -08:00
Chaitanya Eranki 7b71eff294 Made change to phase in Responses API usage for Oracle Code Assist provider (#8473)
* Made change to not allow old models to use Responses API

* Added changeset

* Removing oca from nextGenModelProvier so that we remove native tool calls for now

* Adding back oca as a nextGenModelProvider
2026-01-12 13:05:25 -08:00
yuvalman 6d1890f8bb fix: litellm - trigger model fetching with default base URL (#8359) 2026-01-12 21:36:29 +01:00
Tomás Barreiro 11d17fc17e Fix auth state loop (#8496)
* Prevent loop when getting user organization

* Do not restore user info if the org he is switching to is already active

* Add changeset

* Fix reference array
2026-01-12 19:21:26 +01:00
Tomás Barreiro 42a3dc6150 Prevent requests with an expired auth token (#8470)
* Verify the auth token is valid before returning it

* Add changeset

* refactor
2026-01-12 19:20:33 +01:00
Tomás Barreiro 4032e51e8d Allow admins and owners to override remote config (#8304)
* Add field to settings and handle side effects

* Avoid fetching and applying remote config if it's disabled

* Refactor and apply configured org settings when the user opted out of another one he owns

* Refactor

Fix check

* Add toggle to the account view

* Add changeset

* Fix can disable remote config

* clean canDisableRemoteConfig
2026-01-12 19:19:26 +01:00
Saoud Rizwan 1bbc90487c fix: guard against null/empty choices in streaming responses (#8527)
* fix: guard against null/empty choices in streaming responses

Some OpenAI-compatible APIs (DeepSeek, Groq, OpenWebUI, etc.) send
usage chunks at the end of streaming with empty or null choices arrays.
This was causing crashes with 'Cannot read properties of undefined'.

Added optional chaining on chunk.choices across all 24 affected
provider files to safely handle these usage-only chunks.

Fixes #8384

* chore: add changeset
2026-01-10 19:43:04 -08:00
Saoud Rizwan d422ebbb27 Revert "fix: normalize file paths with spaces before extensions from VS Code …" (#8528)
This reverts commit 362429a317.
2026-01-10 18:18:51 -08:00
Yuri ChukhlibandYuri Chukhlib 7470d234ef feat: add image support for Claude 3.5 Haiku
Update Claude 3.5 Haiku model to support image processing as per 
Anthropic API release notes.

Fixes #2009

Co-authored-by: Yuri Chukhlib <yurii.chukhlib@viber.com>
2026-01-10 18:13:12 -08:00
Yuri ChukhlibandYuri Chukhlib 21b81f1844 fix: close context menu when pressing Escape key
When the @ mention context menu shows "No results found" and the user
presses Escape, the menu was not closing because setShowContextMenu(false)
was not being called.

Fixes #5532

Co-authored-by: Yuri Chukhlib <yurii.chukhlib@viber.com>
2026-01-10 18:12:31 -08:00
Yuri ChukhlibandYuri Chukhlib 362429a317 fix: normalize file paths with spaces before extensions from VS Code LM API
Some LLM providers (notably Claude Sonnet 4.5 via VS Code LM API) insert
spurious spaces before file extensions (e.g., "file .ts" instead of "file.ts").

This fix adds heuristic normalization to remove spaces immediately before
file extensions while preserving legitimate spaces in filenames.

Fixes #7827

Co-authored-by: Yuri Chukhlib <yurii.chukhlib@viber.com>
2026-01-10 18:11:33 -08:00
Yuri ChukhlibandYuri Chukhlib 09cb9ac9ac fix: make workflow slash command search case-insensitive
Users can now find workflows regardless of letter casing (e.g., searching "/testhook" finds "Testhook").

Fixes #7834

Co-authored-by: Yuri Chukhlib <yurii.chukhlib@viber.com>
2026-01-10 18:09:51 -08:00
Ara 94160faeef package update (#8499) 2026-01-09 20:18:44 -08:00
Ara f526f70e3a package update (#8498) 2026-01-09 20:03:33 -08:00
github-actions[bot]andArafatkatze d9b47378c6 v3.49.0 Release Notes (#8467)
- Enable configuring an OTEL collector at runtime
- Removing Minimax-2.1 from free model list as the free trial has ended
- Improved image display in MCP responses
- Auto-sync remote MCP servers from remote config to local settings

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-09 19:46:23 -08:00
Saoud Rizwan 0671c59e6d feat(mcp): improve image display in MCP responses (#8412)
* feat(mcp): improve image display in MCP responses

- Truncate data URIs to show prefix + first 20 chars with [IMAGE] label
- Apply truncation in all display modes (rich, plain, markdown)
- Click data URI images to open in VS Code editor (like mermaid diagrams)
- Expand images to 100% width of response container
- Persist collapsed/expanded state per-response without syncing all instances

* fix(settings): remove Collapse MCP Responses setting from UI

The setting is now implicit - collapsing any MCP response saves the
preference for future responses. Removes confusing sync behavior
between the Settings toggle and individual response toggles.
2026-01-09 18:20:55 -08:00
cryptoqueandSarah Fortune bf87887501 feat: Auto-sync remote MCP servers from remote config to local settings (#8146)
* feat: add remote config sync with extension mcp marketplace for new remote servers

* refactor: extract getMcpSettingsFilePath into disk.ts to be reused

* refactor: rename helper method to avoid ambiguity

* address formatting suggestion by ellipsis-dev for the code itself that was moved

* refactor: add flag pattern to prevent race condition from triggering unnecessary watcher events

* fix: do not re-throw error

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2026-01-09 16:35:28 -08:00
celestial-vault 66a81a6efa remove unused param from pushToolResult (#8474) 2026-01-09 14:59:13 -08:00
Sarah Fortune eee64c5204 Remove unused react banners (#8463) 2026-01-09 14:03:14 -08:00
Tomás Barreiro 748ba99c1c Remove the IAuthProvider (#8469)
* Remove the IAuthProvider

* Remove the comment

* Remove optional chaining
2026-01-09 20:39:39 +01:00
Juan Pablo FloresandTony Loehr 82b1a01644 Adds mcp server support (#8177)
Co-authored-by: Tony Loehr <turingxo@gmail.com>
2026-01-09 11:06:43 -08:00
Tomás Barreiro 1c6307e8ad Remove other references to OTEL_TELEMETRY_ENABLED=1 (#8468) 2026-01-09 10:22:36 -08:00
Tomás Barreiro 58d9c0af18 Enable configuring an OTEL collector at runtime (#8350)
* Replace process.env usage with a BUILD_CONSTANTS variable

* Update import

* revert doc update

* Enable configuring an OTEL collector at runtime

* Refactor

* Refactor

* Add changeset

* Do not build IS_STANDALONE

* Add comment

* Update the `.env.example` file

* Remove `true` from the selected options and revert env.example

* Use `true` for runtime variables
2026-01-09 18:57:24 +01:00
Sarah Fortune bf213c24ea Refactoring (#8462) 2026-01-08 22:35:35 -08:00
github-actions[bot]andArafatkatze 80cceaa3ae v3.48.0 Release Notes (#8407)
- Add Skills system for reusable, on-demand agent instructions
- Add new websearch tooling in Cline provider
- Add zai-glm-4.7 to Cerebras model list
- Add model refresh and improve reasoning support for Vercel AI Gateway

- Revert #8341 due to regressions in diff view/document truncation (see #8423, #8429)
- Fixed extension crash when using context menu selector

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-08 18:42:53 -08:00
Ara 359e088eb6 Gemini thinking + Katcoder support (#8459)
* Fix: Support for gemini thinking

* Fix: Katcoder

* Fix: Katcoder

* Fix: Katcoder
2026-01-08 18:20:05 -08:00
185 changed files with 8366 additions and 5320 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add Skills system for reusable, on-demand agent instructions.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add zai-glm-4.7 to Cerebras model list
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add claude 4.5 haiku
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Adding support for responses api to OCA provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Log Persistence errors to PostHog
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix issue where tool call ids are invalid when switching between models using the chat completion format and the responses api format.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
add bash command permission system to cline
-6
View File
@@ -1,6 +0,0 @@
---
"claude-dev": patch
---
Revert #8341 (0d04205dc) due to regressions in diff view/document truncation (see #8423, #8429).
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
feat(vercel-ai-gateway): add model refresh and improve reasoning support
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: removes retry message from UI after retry succeeds
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
show cline command permission denials in the CLI
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
add claude 4.5 opus into sap provider.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Throttle the remote config fetch
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Improve history view filter menu
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Reduce the number of network requests for the users profile
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: Verify selected index is not -1 when checking if an option is selectable in the context menu
+196
View File
@@ -0,0 +1,196 @@
---
name: create-pull-request
description: Create a GitHub pull request following project conventions. Use when the user asks to create a PR, submit changes for review, or open a pull request. Handles commit analysis, branch management, and PR creation using the gh CLI tool.
---
# Create Pull Request
This skill guides you through creating a well-structured GitHub pull request that follows project conventions and best practices.
## Prerequisites Check
Before proceeding, verify the following:
### 1. Check if `gh` CLI is installed
```bash
gh --version
```
If not installed, inform the user:
> The GitHub CLI (`gh`) is required but not installed. Please install it:
> - macOS: `brew install gh`
> - Other: https://cli.github.com/
### 2. Check if authenticated with GitHub
```bash
gh auth status
```
If not authenticated, guide the user to run `gh auth login`.
### 3. Verify clean working directory
```bash
git status
```
If there are uncommitted changes, ask the user whether to:
- Commit them as part of this PR
- Stash them temporarily
- Discard them (with caution)
## Gather Context
### 1. Identify the current branch
```bash
git branch --show-current
```
Ensure you're not on `main` or `master`. If so, ask the user to create or switch to a feature branch.
### 2. Find the base branch
```bash
git remote show origin | grep "HEAD branch"
```
This is typically `main` or `master`.
### 3. Analyze recent commits relevant to this PR
```bash
git log origin/main..HEAD --oneline --no-decorate
```
Review these commits to understand:
- What changes are being introduced
- The scope of the PR (single feature/fix or multiple changes)
- Whether commits should be squashed or reorganized
### 4. Review the diff
```bash
git diff origin/main..HEAD --stat
```
This shows which files changed and helps identify the type of change.
## Information Gathering
Before creating the PR, you need the following information. Check if it can be inferred from:
- Commit messages
- Branch name (e.g., `fix/issue-123`, `feature/new-login`)
- Changed files and their content
If any critical information is missing, use `ask_followup_question` to ask the user:
### Required Information
1. **Related Issue Number**: Look for patterns like `#123`, `fixes #123`, or `closes #123` in commit messages
2. **Description**: What problem does this solve? Why were these changes made?
3. **Type of Change**: Bug fix, new feature, breaking change, refactor, cosmetic, documentation, or workflow
4. **Test Procedure**: How was this tested? What could break?
### Example clarifying question
If the issue number is not found:
> I couldn't find a related issue number in the commit messages or branch name. What GitHub issue does this PR address? (Enter the issue number, e.g., "123" or "N/A" for small fixes)
## Git Best Practices
Before creating the PR, consider these best practices:
### Commit Hygiene
1. **Atomic commits**: Each commit should represent a single logical change
2. **Clear commit messages**: Follow conventional commit format when possible
3. **No merge commits**: Prefer rebasing over merging to keep history clean
### Branch Management
1. **Rebase on latest main** (if needed):
```bash
git fetch origin
git rebase origin/main
```
2. **Squash if appropriate**: If there are many small "WIP" commits, consider interactive rebase:
```bash
git rebase -i origin/main
```
Only suggest this if commits appear messy and the user is comfortable with rebasing.
### Push Changes
Ensure all commits are pushed:
```bash
git push origin HEAD
```
If the branch was rebased, you may need:
```bash
git push origin HEAD --force-with-lease
```
## Create the Pull Request
**IMPORTANT**: Read and use the PR template at `.github/pull_request_template.md`. The PR body format must **strictly match** the template structure. Do not deviate from the template format.
When filling out the template:
- Replace `#XXXX` with the actual issue number, or keep as `#XXXX` if no issue exists (for small fixes)
- Fill in all sections with relevant information gathered from commits and context
- Mark the appropriate "Type of Change" checkbox(es)
- Complete the "Pre-flight Checklist" items that apply
### Create PR with gh CLI
```bash
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main
```
Alternatively, create as draft if the user wants review before marking ready:
```bash
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main --draft
```
## Post-Creation
After creating the PR:
1. **Display the PR URL** so the user can review it
2. **Remind about CI checks**: Tests and linting will run automatically
3. **Suggest next steps**:
- Add reviewers if needed: `gh pr edit --add-reviewer USERNAME`
- Add labels if needed: `gh pr edit --add-label "bug"`
## Error Handling
### Common Issues
1. **No commits ahead of main**: The branch has no changes to submit
- Ask if the user meant to work on a different branch
2. **Branch not pushed**: Remote doesn't have the branch
- Push the branch first: `git push -u origin HEAD`
3. **PR already exists**: A PR for this branch already exists
- Show the existing PR: `gh pr view`
- Ask if they want to update it instead
4. **Merge conflicts**: Branch conflicts with base
- Guide user through resolving conflicts or rebasing
## Summary Checklist
Before finalizing, ensure:
- [ ] `gh` CLI is installed and authenticated
- [ ] Working directory is clean
- [ ] All commits are pushed
- [ ] Branch is up-to-date with base branch
- [ ] Related issue number is identified, or placeholder is used
- [ ] PR description follows the template exactly
- [ ] Appropriate type of change is selected
- [ ] Pre-flight checklist items are addressed
+3 -3
View File
@@ -72,12 +72,12 @@ POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: tru
# Example configurations:
#
# Console debugging (logs only):
# OTEL_TELEMETRY_ENABLED=1
# OTEL_TELEMETRY_ENABLED=true
# OTEL_LOGS_EXPORTER=console
# TEL_DEBUG_DIAGNOSTICS=true
#
# OTLP with gRPC (insecure, for local testing):
# OTEL_TELEMETRY_ENABLED=1
# OTEL_TELEMETRY_ENABLED=true
# OTEL_LOGS_EXPORTER=otlp
# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
# OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
@@ -85,7 +85,7 @@ POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: tru
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token
#
# OTLP with HTTP/JSON (production):
# OTEL_TELEMETRY_ENABLED=1
# OTEL_TELEMETRY_ENABLED=true
# OTEL_LOGS_EXPORTER=otlp
# OTEL_EXPORTER_OTLP_PROTOCOL=http/json
# OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
+312
View File
@@ -0,0 +1,312 @@
name: Cline PR Code Review
on:
pull_request:
types:
[opened, ready_for_review]
# Manual trigger for backfilling existing PRs. Run from terminal:
# gh workflow run cline-pr-review.yml -f pr_number=1234
# Or batch process open PRs:
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
# gh workflow run cline-pr-review.yml -f pr_number=$num
# sleep 60
# done
workflow_dispatch:
inputs:
pr_number:
description: "PR number to review"
required: true
type: string
concurrency:
group: pr-review-${{ github.event.pull_request.number || inputs.pr_number }}
cancel-in-progress: true
jobs:
cline-pr-review:
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
if: |
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 60
# SECURITY: These permissions are intentionally restrictive.
# - contents: read -> cline can read the codebase but CANNOT write/push any code
# - pull-requests: write -> cline can post reviews and inline suggestions
# - issues: read -> cline can search for related issues
# NOTE: Even with pull-requests: write, cline CANNOT merge PRs because branch protection
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
permissions:
contents: read
pull-requests: write
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
- name: Install and Verify Cline CLI
run: |
npx cline version # verify installation
- name: Configure Cline with Anthropic
run: |
npx cline auth --provider anthropic \
--apikey "${{ secrets.ANTHROPIC_API_KEY }}" \
--modelid claude-opus-4-5-20251101
- name: Get PR number
id: pr
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
else
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
fi
- name: Review PR with Cline
env:
PR_NUMBER: ${{ steps.pr.outputs.number }}
GITHUB_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
CLINE_COMMAND_PERMISSIONS: |
{
"allow": [
"gh pr diff *",
"gh pr view *",
"gh pr checks *",
"gh pr list *",
"gh label list *",
"gh issue list *",
"gh issue view *",
"git log *",
"gh pr comment ${{ steps.pr.outputs.number }} *",
"gh pr edit ${{ steps.pr.outputs.number }} *",
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments *",
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews *"
]
}
run: |
npx cline --yolo 'You'\''re a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
PR: #'"${PR_NUMBER}"'
## Gather context
```bash
# Get full PR details
gh pr view '"${PR_NUMBER}"' --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
# Get the diff
gh pr diff '"${PR_NUMBER}"'
# Check CI status
gh pr checks '"${PR_NUMBER}"'
# Get existing review comments (to understand context and your previous feedback)
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/comments --jq '\''.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'\''
# Get conversation comments
gh pr view '"${PR_NUMBER}"' --comments
```
If this is a re-review (workflow_dispatch event):
Read your previous comments carefully. Understand what you asked for before.
Check if new commits or comments address your previous feedback.
## Check contributing guidelines
Flag (but don'\''t block) if:
- Missing changeset - For user-facing changes, check if there'\''s a `.changeset/` file:
```bash
gh pr diff '"${PR_NUMBER}"' --name-only | grep '\''.changeset/'\'' || echo '\''No changeset found'\''
```
If missing, ask them to run `npm run changeset`
- Missing tests - New features should have tests
## Find related issues and PRs
Search thoroughly for context that might help with the review:
```bash
# Find related issues for context
gh issue list --search '\''<keywords from the PR>'\'' --state all --limit 30
gh issue list --search '\''<error messages or feature names>'\'' --state all --limit 20
# Find similar PRs for reference
gh pr list --search '\''<keywords>'\'' --state all --limit 30
```
For each relevant issue or PR you find, read it including comments:
```bash
gh issue view <number> --comments
gh pr view <number> --comments
```
Look for:
- Open issues this PR might fix that weren'\''t linked in the description
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
- Context from maintainer discussions that could inform your review
## Find subject matter experts
For files changed in this PR, find who knows the code best:
```bash
# Get files changed
gh pr diff '"${PR_NUMBER}"' --name-only
# For each relevant path, find contributors
git log --since='\''6 months ago'\'' --format='\''%an'\'' -- <path> | sort | uniq -c | sort -rn | head -5
```
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
| SME | Reason |
|-----|--------|
| @username1 | Authored PR #X which modified this area |
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
| @username3 | Reviewed similar PR #Y with extensive feedback |
## Bash command usage
Don'\''t use operators like `|`, `&&`, or `;` - run each command separately and analyze the output.
When referencing command outputs, quote them properly to avoid formatting issues.
## Deep code review
This is the most important part. Don'\''t just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
Step 1: Understand the intent
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
Step 2: Form your own opinion first
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
Step 3: Compare approaches
Now look at their implementation. How does it compare to what you would have done?
- Is their approach better in some ways? Note what they did well.
- Is their approach missing something? Be specific about what and why.
- Are there edge cases they haven'\''t considered?
- Does it follow the patterns established in similar parts of the codebase?
Step 4: Look at the bigger picture
- What other files or systems does this change interact with?
- Could this break anything else?
- Is there additional work needed beyond this PR to complete the feature?
- Does this fit well with the overall architecture?
Step 5: Find reference implementations
Look for similar changes in the codebase:
```bash
git log --oneline --all --grep='\''<relevant keywords>'\'' | head -20
git log --oneline -- <similar files> | head -20
```
If this is adding a new API provider, look at how other providers are implemented.
If this is adding a new feature, look at how similar features were added.
Note where their implementation aligns with or diverges from established patterns.
Step 6: Standard code review checks
- DRY: Is there duplicated code that could be extracted?
- Error handling: Are errors handled appropriately?
- Security: Any injection risks, credential exposure, unsafe dependencies?
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
- Types: Is TypeScript used correctly? Any unsafe type assertions?
- Naming: Are variables and functions named clearly?
- Comments: Is complex logic explained? Are there outdated comments?
## Inline code suggestions
For specific code improvements, use GitHub'\''s suggestion syntax via `gh api`.
This creates suggestions the author can commit with one click.
Single-line suggestion:
```bash
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
-X POST \
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
-f event='\''COMMENT'\'' \
-f body='\'''\'' \
-F comments='\''[
{
"path": "src/example.ts",
"line": 42,
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
}
]'\''
```
Multi-line suggestion (replacing lines 40-45):
```bash
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
-X POST \
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
-f event='\''COMMENT'\'' \
-f body='\'''\'' \
-F comments='\''[
{
"path": "src/example.ts",
"start_line": 40,
"line": 45,
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
}
]'\''
```
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
## Post your review
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
Start with a warm thank you for their contribution. Be conversational, not robotic.
Include what'\''s relevant:
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author'\''s intent, why they made the changes, how they implemented it, and what files/systems are affected. Don'\''t just summarize - explain.
- Related issues/PRs you found that provide useful context (link to them)
- Your review findings (issues to address, suggestions, etc.)
- Clear next steps for the author
Include a '\''For Maintainers'\'' section with:
- Anything else useful to help the maintainer resolve this PR
- Related issues/PRs with context on why they'\''re relevant
- Open issues this PR might fix that weren'\''t linked in the description
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
- SME table - who should review this and why
For the SME table:
| SME | Reason |
|-----|--------|
| @username | Primary contributor to affected files |
## Update labels
Add appropriate labels based on your analysis:
```bash
gh label list --json name,description --limit 100
gh pr edit '"${PR_NUMBER}"' --add-label '\''label1,label2'\''
```
When done, add the reviewed label:
```bash
gh pr edit '"${PR_NUMBER}"' --add-label '\''Bot Reviewed'\''
```
## Remember
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like '\''let me know if you have questions'\'', '\''I can help you with'\'', or '\''feel free to ask'\'' - you won'\''t be there to follow up. Frame any questions for the maintainers who will follow up.
- Be helpful and welcoming - Many contributors are new to the project
- Be specific - Point to exact lines and suggest fixes, don'\''t give vague feedback
- Think deeply - Don'\''t just surface-level review, understand the intent and evaluate the approach
- Use inline suggestions - Make it easy for authors to accept changes
- You'\''re a first-pass reviewer - A human maintainer will do final approval'
+5
View File
@@ -41,3 +41,8 @@ webview-ui/src/services/grpc-client.ts
# E2E Tests
test-results
/.github/act
/pkg
.secrets
+58 -1
View File
@@ -1,8 +1,65 @@
# Changelog
## [3.51.0]
### Added
- Adding OpenAI gpt-5.2-codex model to the model picker
## [3.50.0]
### Added
- Add gpt-5.2-codex OpenAI model support
- Add create-pull-request skill
### Fixed
- Fix the selection of remotely configured providers
- Fix act_mode_respond to prevent consecutive calls
- Fix invalid tool call IDs when switching between model formats
## [3.49.1]
### Added
- Add telemetry to track usage of skills feature
- Add version headers to Cline backend requests
- Phase in Responses API usage instead of defaulting for every supported model
### Fixed
- Fix workflow slash command search to be case-insensitive
- Fix model display in ModelPickerModal when using LiteLLM
- Fix LiteLLM model fetching with default base URL
- Fix crash when OpenAI-compatible APIs send usage chunks with empty or null choices arrays at end of streaming
- Fix model ID for Kat Coder Pro Free model
## [3.49.0]
- Enable configuring an OTEL collector at runtime
- Removing Minimax-2.1 from free model list as the free trial has ended
- Improved image display in MCP responses
- Auto-sync remote MCP servers from remote config to local settings
## [3.48.0]
### Added
- Add Skills system for reusable, on-demand agent instructions
- Add new websearch tooling in Cline provider
- Add zai-glm-4.7 to Cerebras model list
- Add model refresh and improve reasoning support for Vercel AI Gateway
### Fixed
- Revert #8341 due to regressions in diff view/document truncation (see #8423, #8429)
- Fixed extension crash when using context menu selector
## [3.47.0]
### Added
- Added experimental support for Background Edits (allows editing files in background without opening the diff view)
- Updated free model to MiniMax M2.1 (replacing MiniMax M2)
- Added support for Azure based identity authentication in OpenAI Compatible provider and Azure OpenAI
@@ -1680,4 +1737,4 @@ Add Opus 4.1 through Claude Code
## [0.0.6]
- Initial release
- Initial release
+29
View File
@@ -127,3 +127,32 @@ const isGenerating = explanationInfo.status === "generating" && !wasCancelled
**See also:** `BrowserSessionRow.tsx` uses similar pattern with `isLastApiReqInterrupted` and `isLastMessageResume`.
**Backend side:** When streaming is cancelled, clean up properly (close tabs, clear comments, etc.) by checking `taskState.abort` after the streaming function returns.
## Parallel Command Execution
When multiple `execute_command` tools run in parallel (via `PARALLEL_SAFE_TOOLS`), commands cannot use `ask()` for interactive output handling—multiple concurrent `ask()` calls cause conflicts because they share single state variables in TaskState.
**The Solution - Queue-Based Ask/Response System:**
- `PendingAskQueue` (`src/core/task/PendingAskQueue.ts`) - Manages concurrent ask operations with unique IDs per ask
- `TaskState.pendingAskQueue` - Replaces single `askResponse`/`askResponseText`/`askResponseImages`/`askResponseFiles`
- `Task.ask()` - Refactored to create unique ask IDs and wait for specific responses
- `Task.handleWebviewAskResponse()` - Resolves asks in FIFO order (first pending ask gets the response)
**Concurrent Command Orchestration:**
- `ConcurrentCommandOrchestrator` (`src/integrations/terminal/ConcurrentCommandOrchestrator.ts`) - Alternative to `CommandOrchestrator`
- Does NOT call `ask()` on each output chunk—streams via `say()` instead
- No "Proceed While Running" button (not needed for parallel execution)
- Used when `taskState.isExecutingInParallel = true`
**How It Works:**
1. When parallel tools execute, flags are set: `taskState.isExecutingInParallel = true` and `commandExecutor.setParallelExecution(true)`
2. Each command's orchestrator is selected based on these flags
3. In parallel mode, output is streamed directly without asking for user input on each chunk
4. Single user response (if needed) is queued and distributed to waiting asks via `PendingAskQueue`
5. Flags are cleared in a `finally` block to ensure proper cleanup
**Key Files:**
- `src/core/task/PendingAskQueue.ts` - Queue implementation
- `src/integrations/terminal/ConcurrentCommandOrchestrator.ts` - Parallel-safe orchestrator
- `src/core/task/TaskState.ts` - Added `pendingAskQueue` and `isExecutingInParallel`
- `src/core/task/index.ts` - Refactored `ask()`, `handleWebviewAskResponse()`, and parallel execution logic
- `src/integrations/terminal/CommandExecutor.ts` - Added `setParallelExecution()` and orchestrator selection
@@ -358,6 +358,9 @@ func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cli
if openRouterInfo, ok := modelInfo.(*cline.OpenRouterModelInfo); ok {
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
} else if ocaInfo, ok := modelInfo.(*cline.OcaModelInfo); ok {
apiConfig.PlanModeOcaModelInfo = ocaInfo
apiConfig.ActModeOcaModelInfo = ocaInfo
}
}
@@ -426,6 +429,9 @@ func UpdateProviderPartial(ctx context.Context, manager *task.Manager, provider
if openRouterInfo, ok := updates.ModelInfo.(*cline.OpenRouterModelInfo); ok {
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
} else if ocaInfo, ok := updates.ModelInfo.(*cline.OcaModelInfo); ok {
apiConfig.PlanModeOcaModelInfo = ocaInfo
apiConfig.ActModeOcaModelInfo = ocaInfo
}
}
+2 -2
View File
@@ -190,7 +190,7 @@ func (pw *ProviderWizard) handleAddOcaProvider() error {
}
// Step 3: Select model
modelID, _, err := pw.selectModel(cline.ApiProvider_OCA, "")
modelID, modelInfo, err := pw.selectModel(cline.ApiProvider_OCA, "")
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
@@ -198,7 +198,7 @@ func (pw *ProviderWizard) handleAddOcaProvider() error {
// Step 4: Apply the OCA model configuration and set as active
updates := ProviderUpdatesPartial{
ModelID: &modelID,
ModelInfo: nil,
ModelInfo: modelInfo,
}
if err := UpdateProviderPartial(pw.ctx, pw.manager, cline.ApiProvider_OCA, updates, true); err != nil {
+9 -4
View File
@@ -37,11 +37,16 @@ var (
func InitializeGlobalConfig(cfg *GlobalConfig) error {
if cfg.ConfigPath == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
// Check CLINE_DIR environment variable first
if clineDir := os.Getenv("CLINE_DIR"); clineDir != "" {
cfg.ConfigPath = clineDir
} else {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
cfg.ConfigPath = filepath.Join(homeDir, ".cline")
}
cfg.ConfigPath = filepath.Join(homeDir, ".cline")
}
// Ensure .cline directory exists
@@ -58,59 +58,59 @@ Enable OpenTelemetry and configure an OTLP endpoint:
```bash
# Enable OpenTelemetry
export OTEL_TELEMETRY_ENABLED=1
export CLINE_OTEL_TELEMETRY_ENABLED=true
# Configure metrics and logs export
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export CLINE_OTEL_METRICS_EXPORTER=otlp
export CLINE_OTEL_LOGS_EXPORTER=otlp
# Set your OTLP endpoint
export OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4317
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4317
# Optional: Set protocol (default is grpc)
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export CLINE_OTEL_EXPORTER_OTLP_PROTOCOL=grpc
```
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `OTEL_TELEMETRY_ENABLED` | Enable OpenTelemetry (`1` or `true`) | Disabled |
| `OTEL_METRICS_EXPORTER` | Metrics exporter type (`console`, `otlp`, or both) | None |
| `OTEL_LOGS_EXPORTER` | Logs exporter type (`console`, `otlp`, or both) | None |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL | None |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol (`grpc`, `http/protobuf`, `http/json`) | `grpc` |
| `OTEL_EXPORTER_OTLP_INSECURE` | Allow insecure connections | `false` |
| `OTEL_EXPORTER_OTLP_HEADERS` | Custom headers (comma-separated `key=value` pairs) | None |
| `CLINE_OTEL_TELEMETRY_ENABLED` | Enable OpenTelemetry (`true`) | Disabled |
| `CLINE_OTEL_METRICS_EXPORTER` | Metrics exporter type (`console`, `otlp`, or both) | None |
| `CLINE_OTEL_LOGS_EXPORTER` | Logs exporter type (`console`, `otlp`, or both) | None |
| `CLINE_OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL | None |
| `CLINE_OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol (`grpc`, `http/protobuf`, `http/json`) | `grpc` |
| `CLINE_OTEL_EXPORTER_OTLP_INSECURE` | Allow insecure connections | `false` |
| `CLINE_OTEL_EXPORTER_OTLP_HEADERS` | Custom headers (comma-separated `key=value` pairs) | None |
### Advanced Configuration
**Separate endpoints for metrics and logs:**
```bash
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://metrics-collector:4317
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs-collector:4317
export CLINE_OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://metrics-collector:4317
export CLINE_OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs-collector:4317
```
**Custom headers for authentication:**
```bash
export OTEL_EXPORTER_OTLP_HEADERS="api-key=your-key,x-custom-header=value"
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="api-key=your-key,x-custom-header=value"
```
**Multiple exporters (console + OTLP):**
```bash
export OTEL_METRICS_EXPORTER=console,otlp
export OTEL_LOGS_EXPORTER=console,otlp
export CLINE_OTEL_METRICS_EXPORTER=console,otlp
export CLINE_OTEL_LOGS_EXPORTER=console,otlp
```
**Export intervals:**
```bash
# Metrics export interval in milliseconds (default: 60000)
export OTEL_METRIC_EXPORT_INTERVAL=30000
export CLINE_OTEL_METRIC_EXPORT_INTERVAL=30000
# Logs batch size and timeout
export OTEL_LOG_BATCH_SIZE=512
export OTEL_LOG_BATCH_TIMEOUT=5000
export OTEL_LOG_MAX_QUEUE_SIZE=2048
export CLINE_OTEL_LOG_BATCH_SIZE=512
export CLINE_OTEL_LOG_BATCH_TIMEOUT=5000
export CLINE_OTEL_LOG_MAX_QUEUE_SIZE=2048
```
## Integration Examples
@@ -120,11 +120,11 @@ export OTEL_LOG_MAX_QUEUE_SIZE=2048
Export to Datadog using their OTLP endpoint:
```bash
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.datadoghq.com
export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
export CLINE_OTEL_TELEMETRY_ENABLED=true
export CLINE_OTEL_METRICS_EXPORTER=otlp
export CLINE_OTEL_LOGS_EXPORTER=otlp
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://api.datadoghq.com
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
```
### New Relic
@@ -132,11 +132,11 @@ export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
Export to New Relic:
```bash
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4317
export OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
export CLINE_OTEL_TELEMETRY_ENABLED=true
export CLINE_OTEL_METRICS_EXPORTER=otlp
export CLINE_OTEL_LOGS_EXPORTER=otlp
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4317
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
```
### Grafana Cloud
@@ -144,11 +144,11 @@ export OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
Export to Grafana Cloud:
```bash
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic YOUR_BASE64_CREDENTIALS"
export CLINE_OTEL_TELEMETRY_ENABLED=true
export CLINE_OTEL_METRICS_EXPORTER=otlp
export CLINE_OTEL_LOGS_EXPORTER=otlp
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic YOUR_BASE64_CREDENTIALS"
```
@@ -158,9 +158,9 @@ Test your configuration with console output before sending to a real endpoint:
```bash
# Enable console output to see what data would be exported
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=console
export OTEL_LOGS_EXPORTER=console
export CLINE_OTEL_TELEMETRY_ENABLED=true
export CLINE_OTEL_METRICS_EXPORTER=console
export CLINE_OTEL_LOGS_EXPORTER=console
```
Then launch Cline and check the console output for metrics and logs.
@@ -171,20 +171,20 @@ Then launch Cline and check the console output for metrics and logs.
1. **Verify OpenTelemetry is enabled:**
```bash
echo $OTEL_TELEMETRY_ENABLED
echo $CLINE_OTEL_TELEMETRY_ENABLED
```
Should output `1` or `true`
Should output `true`
2. **Check exporters are configured:**
```bash
echo $OTEL_METRICS_EXPORTER
echo $OTEL_LOGS_EXPORTER
echo $CLINE_OTEL_METRICS_EXPORTER
echo $CLINE_OTEL_LOGS_EXPORTER
```
3. **Test with console exporter first:**
```bash
export OTEL_METRICS_EXPORTER=console
export OTEL_LOGS_EXPORTER=console
export CLINE_OTEL_METRICS_EXPORTER=console
export CLINE_OTEL_LOGS_EXPORTER=console
```
### Connection Errors
@@ -196,7 +196,7 @@ Then launch Cline and check the console output for metrics and logs.
2. **Check if insecure mode is needed:**
```bash
export OTEL_EXPORTER_OTLP_INSECURE=true
export CLINE_OTEL_EXPORTER_OTLP_INSECURE=true
```
3. **Verify authentication headers:**
@@ -120,8 +120,38 @@ Controls a built-in browser to interact with websites or local servers. Useful f
</browser_action>
```
### Leverage MCP Tools
You can use Model Context Protocol (MCP) tools within your workflows to interact with external services like GitHub, Slack, or databases. This allows you to create powerful end-to-end automations.
### Leveraging MCP Tools
MCP tools allow Cline to interact with external services like GitHub, Slack, or databases. You can reference them in your workflows using natural language or explicit XML tags for deterministic control.
#### Natural Language (Heuristic)
Most of the time, the simplest way to use an MCP tool is to describe the action you want Cline to take.
```markdown
1. Fetch the latest issues from the github-repo MCP server.
2. Summarize the critical bugs.
3. Post the summary to the #engineering channel using the slack-notifications MCP.
```
#### Explicit XML Tag (Deterministic)
For critical automation where you need exact control over parameters, use the `use_mcp_tool` tag.
```xml
<use_mcp_tool>
<server_name>github-repo-manager</server_name>
<tool_name>create_issue</tool_name>
<arguments>
{
"owner": "cline",
"repo": "cline",
"title": "Automated Bug Report",
"body": "Found a regression in the latest build."
}
</arguments>
</use_mcp_tool>
```
### Manage Context Window
Be mindful of Cline's context window. If a workflow is too long or processes too much data, it might exceed the token limit.
+8 -11
View File
@@ -16,20 +16,17 @@ Cline supports accessing models directly through the official OpenAI API.
### Supported Models
Cline is compatible with a variety of OpenAI models, including but not limited to:
Cline is compatible with a variety of OpenAI models, including common choices from OpenAI's featured/frontier lists:
- 'o3'
- `o3-mini` (medium reasoning effort)
- 'o4-mini'
- `o3-mini-high` (high reasoning effort)
- `o3-mini-low` (low reasoning effort)
- `o1`
- `o1-preview`
- `o1-mini`
- `gpt-5.2`
- `gpt-5.2-codex`
- `gpt-5-mini`
- `gpt-5-nano`
- `gpt-4.1`
- `gpt-4o`
- `gpt-4o-mini`
- 'gpt-4.1'
- 'gpt-4.1-mini'
- `o3`
- `o4-mini`
For the most current list of available models and their capabilities, please refer to the official [OpenAI Models documentation](https://platform.openai.com/docs/models).
+57 -39
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.47.0",
"version": "3.51.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.47.0",
"version": "3.51.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -1182,7 +1182,6 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2645,7 +2644,6 @@
"node_modules/@grpc/grpc-js": {
"version": "1.9.15",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@grpc/proto-loader": "^0.7.8",
"@types/node": ">=12.12.47"
@@ -3229,7 +3227,6 @@
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@hono/node-server": "^1.19.7",
"ajv": "^8.17.1",
@@ -3298,7 +3295,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -4914,7 +4910,8 @@
"optional": true,
"os": [
"android"
]
],
"peer": true
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.4",
@@ -4927,7 +4924,8 @@
"optional": true,
"os": [
"android"
]
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.4",
@@ -4940,7 +4938,8 @@
"optional": true,
"os": [
"darwin"
]
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.4",
@@ -4953,7 +4952,8 @@
"optional": true,
"os": [
"darwin"
]
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.4",
@@ -4966,7 +4966,8 @@
"optional": true,
"os": [
"freebsd"
]
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.4",
@@ -4979,7 +4980,8 @@
"optional": true,
"os": [
"freebsd"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.4",
@@ -4992,7 +4994,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.4",
@@ -5005,7 +5008,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.4",
@@ -5018,7 +5022,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.4",
@@ -5031,7 +5036,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.4",
@@ -5044,7 +5050,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.4",
@@ -5057,7 +5064,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.4",
@@ -5070,7 +5078,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.4",
@@ -5083,7 +5092,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.4",
@@ -5096,7 +5106,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.4",
@@ -5109,7 +5120,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.4",
@@ -5122,7 +5134,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.4",
@@ -5135,7 +5148,8 @@
"optional": true,
"os": [
"openharmony"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.4",
@@ -5148,7 +5162,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.4",
@@ -5161,7 +5176,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.4",
@@ -5174,7 +5190,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.4",
@@ -5187,7 +5204,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@sap-ai-sdk/ai-api": {
"version": "2.1.0",
@@ -6750,7 +6768,8 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/get-folder-size": {
"version": "3.0.4",
@@ -6782,7 +6801,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz",
"integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -7486,7 +7504,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -8253,7 +8270,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -9467,8 +9483,7 @@
},
"node_modules/devtools-protocol": {
"version": "0.0.1342118",
"license": "BSD-3-Clause",
"peer": true
"license": "BSD-3-Clause"
},
"node_modules/diff": {
"version": "5.2.0",
@@ -12444,7 +12459,6 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"license": "MIT",
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -12678,7 +12692,6 @@
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
"license": "MPL-2.0",
"peer": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
@@ -15559,6 +15572,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -15579,6 +15593,7 @@
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -16246,6 +16261,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -17671,6 +17687,7 @@
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
@@ -17687,6 +17704,7 @@
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -18050,7 +18068,6 @@
"version": "5.5.3",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -18310,6 +18327,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -18384,6 +18402,7 @@
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -19079,7 +19098,6 @@
"node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -19094,4 +19112,4 @@
}
}
}
}
}
+5 -1
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.47.0",
"version": "3.51.0",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -403,6 +403,10 @@
"storybook": "cd webview-ui && npm run storybook"
},
"lint-staged": {
"src/shared/storage/state-keys.ts": [
"node scripts/generate-state-proto.mjs",
"git add proto/cline/state.proto"
],
"*": [
"biome check --write --staged --no-errors-on-unmatched --files-ignore-unknown=true"
]
+217 -170
View File
@@ -54,183 +54,229 @@ message AutoApprovalSettings {
optional bool enable_notifications = 3;
}
// NOTE: Add the new secret fields under SECRETS_KEYS in src/shared/storage/state-keys.ts
// and use the scripts/generate-state-proto.mjs script to regenerate this list.
message Secrets {
optional string api_key = 1;
optional string open_router_api_key = 4;
optional string aws_access_key = 5;
optional string aws_secret_key = 6;
optional string aws_session_token = 7;
optional string aws_bedrock_api_key = 8;
optional string open_ai_api_key = 9;
optional string gemini_api_key = 10;
optional string open_ai_native_api_key = 11;
optional string ollama_api_key = 12;
optional string deep_seek_api_key = 13;
optional string requesty_api_key = 14;
optional string together_api_key = 15;
optional string fireworks_api_key = 16;
optional string qwen_api_key = 17;
optional string doubao_api_key = 18;
optional string mistral_api_key = 19;
optional string lite_llm_api_key = 20;
optional string auth_nonce = 21;
optional string asksage_api_key = 22;
optional string xai_api_key = 23;
optional string moonshot_api_key = 24;
optional string zai_api_key = 25;
optional string hugging_face_api_key = 26;
optional string nebius_api_key = 27;
optional string sambanova_api_key = 28;
optional string cerebras_api_key = 29;
optional string sap_ai_core_client_id = 30;
optional string sap_ai_core_client_secret = 31;
optional string groq_api_key = 32;
optional string huawei_cloud_maas_api_key = 33;
optional string baseten_api_key = 34;
optional string vercel_ai_gateway_api_key = 35;
optional string dify_api_key = 36;
optional string oca_api_key = 37;
optional string oca_refresh_token = 38;
optional string hicap_api_key = 39;
optional string mcp_oauth_secrets = 40;
optional string cline_account_id = 2;
optional string open_router_api_key = 3;
optional string aws_access_key = 4;
optional string aws_secret_key = 5;
optional string aws_session_token = 6;
optional string aws_bedrock_api_key = 7;
optional string open_ai_api_key = 8;
optional string gemini_api_key = 9;
optional string open_ai_native_api_key = 10;
optional string ollama_api_key = 11;
optional string deep_seek_api_key = 12;
optional string requesty_api_key = 13;
optional string together_api_key = 14;
optional string fireworks_api_key = 15;
optional string qwen_api_key = 16;
optional string doubao_api_key = 17;
optional string mistral_api_key = 18;
optional string lite_llm_api_key = 19;
optional string auth_nonce = 20;
optional string asksage_api_key = 21;
optional string xai_api_key = 22;
optional string moonshot_api_key = 23;
optional string zai_api_key = 24;
optional string hugging_face_api_key = 25;
optional string nebius_api_key = 26;
optional string sambanova_api_key = 27;
optional string cerebras_api_key = 28;
optional string sap_ai_core_client_id = 29;
optional string sap_ai_core_client_secret = 30;
optional string groq_api_key = 31;
optional string huawei_cloud_maas_api_key = 32;
optional string baseten_api_key = 33;
optional string vercel_ai_gateway_api_key = 34;
optional string dify_api_key = 35;
optional string minimax_api_key = 36;
optional string hicap_api_key = 37;
optional string aihubmix_api_key = 38;
optional string nous_research_api_key = 39;
optional string remote_lite_llm_api_key = 40;
optional string oca_api_key = 41;
optional string oca_refresh_token = 42;
optional string mcp_o_auth_secrets = 43;
}
// NOTE: Add new fields under API_HANDLER_SETTINGS_FIELDS or USER_SETTINGS_FIELDS
// in src/shared/storage/state-keys.ts and use the scripts/generate-state-proto.mjs
// script to regenerate this list.
message Settings {
optional string aws_region = 1;
optional bool aws_use_cross_region_inference = 2;
optional bool aws_bedrock_use_prompt_cache = 3;
optional string aws_bedrock_endpoint = 4;
optional string aws_profile = 5;
optional string aws_authentication = 6;
optional bool aws_use_profile = 7;
optional string vertex_project_id = 8;
optional string vertex_region = 9;
optional string requesty_base_url = 10;
optional string open_ai_base_url = 11;
// map<string, string> open_ai_headers = 12;
optional string ollama_base_url = 13;
optional string ollama_api_options_ctx_num = 14;
optional string lm_studio_base_url = 15;
optional string lm_studio_max_tokens = 16;
optional string anthropic_base_url = 17;
optional string gemini_base_url = 18;
optional string azure_api_version = 19;
optional string open_router_provider_sorting = 20;
optional AutoApprovalSettings auto_approval_settings = 21;
optional BrowserSettings browser_settings = 24;
optional string lite_llm_base_url = 25;
optional bool lite_llm_use_prompt_cache = 26;
optional int32 fireworks_model_max_completion_tokens = 27;
optional int32 fireworks_model_max_tokens = 28;
optional string lite_llm_base_url = 1;
optional bool lite_llm_use_prompt_cache = 2;
map<string, string> open_ai_headers = 3;
optional string anthropic_base_url = 4;
optional string open_router_provider_sorting = 5;
optional string aws_region = 6;
optional bool aws_use_cross_region_inference = 7;
optional bool aws_use_global_inference = 8;
optional bool aws_bedrock_use_prompt_cache = 9;
optional string aws_authentication = 10;
optional bool aws_use_profile = 11;
optional string aws_profile = 12;
optional string aws_bedrock_endpoint = 13;
optional string claude_code_path = 14;
optional string vertex_project_id = 15;
optional string vertex_region = 16;
optional string open_ai_base_url = 17;
optional string ollama_base_url = 18;
optional string ollama_api_options_ctx_num = 19;
optional string lm_studio_base_url = 20;
optional string lm_studio_max_tokens = 21;
optional string gemini_base_url = 22;
optional string requesty_base_url = 23;
optional int32 fireworks_model_max_completion_tokens = 24;
optional int32 fireworks_model_max_tokens = 25;
optional string qwen_code_oauth_path = 26;
optional string azure_api_version = 27;
optional bool azure_identity = 28;
optional string qwen_api_line = 29;
optional string moonshot_api_line = 30;
optional string zai_api_line = 31;
optional string telemetry_setting = 32;
optional string asksage_api_url = 33;
optional bool plan_act_separate_models_setting = 34;
optional bool enable_checkpoints_setting = 35;
optional int32 request_timeout_ms = 36;
optional int32 shell_integration_timeout = 37;
optional string default_terminal_profile = 38;
optional int32 terminal_output_line_limit = 39;
optional string sap_ai_core_token_url = 40;
optional string sap_ai_core_base_url = 41;
optional string sap_ai_resource_group = 42;
optional bool sap_ai_core_use_orchestration_mode = 43;
optional string claude_code_path = 44;
optional string qwen_code_oauth_path = 45;
optional bool strict_plan_mode_enabled = 46;
optional bool yolo_mode_toggled = 47;
optional bool use_auto_condense = 48;
optional string preferred_language = 49;
optional OpenaiReasoningEffort openai_reasoning_effort = 50;
optional PlanActMode mode = 51;
optional DictationSettings dictation_settings = 52;
optional FocusChainSettings focus_chain_settings = 53;
optional string custom_prompt = 54;
optional string dify_base_url = 55;
optional double auto_condense_threshold = 56;
optional string oca_base_url = 57;
optional ApiProvider plan_mode_api_provider = 58;
optional string plan_mode_api_model_id = 59;
optional int64 plan_mode_thinking_budget_tokens = 60;
optional string plan_mode_reasoning_effort = 61;
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 62;
optional bool plan_mode_aws_bedrock_custom_selected = 63;
optional string plan_mode_aws_bedrock_custom_model_base_id = 64;
optional string plan_mode_open_router_model_id = 65;
optional OpenRouterModelInfo plan_mode_open_router_model_info = 66;
optional string plan_mode_open_ai_model_id = 67;
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 68;
optional string plan_mode_ollama_model_id = 69;
optional string plan_mode_lm_studio_model_id = 70;
optional string plan_mode_lite_llm_model_id = 71;
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 72;
optional string plan_mode_requesty_model_id = 73;
optional OpenRouterModelInfo plan_mode_requesty_model_info = 74;
optional string plan_mode_together_model_id = 75;
optional string plan_mode_fireworks_model_id = 76;
optional string plan_mode_sap_ai_core_model_id = 77;
optional string plan_mode_sap_ai_core_deployment_id = 78;
optional string plan_mode_groq_model_id = 79;
optional OpenRouterModelInfo plan_mode_groq_model_info = 80;
optional string plan_mode_baseten_model_id = 81;
optional OpenRouterModelInfo plan_mode_baseten_model_info = 82;
optional string plan_mode_hugging_face_model_id = 83;
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 84;
optional string plan_mode_huawei_cloud_maas_model_id = 85;
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 86;
optional string plan_mode_oca_model_id = 87;
optional OcaModelInfo plan_mode_oca_model_info = 88;
optional ApiProvider act_mode_api_provider = 89;
optional string act_mode_api_model_id = 90;
optional int64 act_mode_thinking_budget_tokens = 91;
optional string act_mode_reasoning_effort = 92;
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 93;
optional bool act_mode_aws_bedrock_custom_selected = 94;
optional string act_mode_aws_bedrock_custom_model_base_id = 95;
optional string act_mode_open_router_model_id = 96;
optional OpenRouterModelInfo act_mode_open_router_model_info = 97;
optional string act_mode_open_ai_model_id = 98;
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 99;
optional string act_mode_ollama_model_id = 100;
optional string act_mode_lm_studio_model_id = 101;
optional string act_mode_lite_llm_model_id = 102;
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 103;
optional string act_mode_requesty_model_id = 104;
optional OpenRouterModelInfo act_mode_requesty_model_info = 105;
optional string act_mode_together_model_id = 106;
optional string act_mode_fireworks_model_id = 107;
optional string act_mode_sap_ai_core_model_id = 108;
optional string act_mode_sap_ai_core_deployment_id = 109;
optional string act_mode_groq_model_id = 110;
optional OpenRouterModelInfo act_mode_groq_model_info = 111;
optional string act_mode_baseten_model_id = 112;
optional OpenRouterModelInfo act_mode_baseten_model_info = 113;
optional string act_mode_hugging_face_model_id = 114;
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 115;
optional string act_mode_huawei_cloud_maas_model_id = 116;
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 117;
optional string plan_mode_vercel_ai_gateway_model_id = 118;
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 119;
optional string act_mode_vercel_ai_gateway_model_id = 120;
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 121;
optional string act_mode_oca_model_id = 122;
optional OcaModelInfo act_mode_oca_model_info = 123;
optional int32 max_consecutive_mistakes = 124;
optional bool subagents_enabled = 125;
optional int32 subagent_terminal_output_line_limit = 126;
optional string aihubmix_api_key = 127;
optional string aihubmix_base_url = 128;
optional string aihubmix_app_code = 129;
optional string plan_mode_aihubmix_model_id = 130;
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 131;
optional string act_mode_aihubmix_model_id = 132;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 133;
optional bool cline_web_tools_enabled = 134;
optional bool hooks_enabled = 135;
optional bool azure_identity = 136;
optional bool skills_enabled = 137;
optional string asksage_api_url = 31;
optional int32 request_timeout_ms = 32;
optional string sap_ai_resource_group = 33;
optional string sap_ai_core_token_url = 34;
optional string sap_ai_core_base_url = 35;
optional bool sap_ai_core_use_orchestration_mode = 36;
optional string dify_base_url = 37;
optional string zai_api_line = 38;
optional string oca_base_url = 39;
optional string minimax_api_line = 40;
optional string oca_mode = 41;
optional string aihubmix_base_url = 42;
optional string aihubmix_app_code = 43;
optional string plan_mode_api_model_id = 44;
optional int64 plan_mode_thinking_budget_tokens = 45;
optional string gemini_plan_mode_thinking_level = 46;
optional string plan_mode_reasoning_effort = 47;
optional string plan_mode_verbosity = 48;
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 49;
optional bool plan_mode_aws_bedrock_custom_selected = 50;
optional string plan_mode_aws_bedrock_custom_model_base_id = 51;
optional string plan_mode_open_router_model_id = 52;
optional OpenRouterModelInfo plan_mode_open_router_model_info = 53;
optional string plan_mode_open_ai_model_id = 54;
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 55;
optional string plan_mode_ollama_model_id = 56;
optional string plan_mode_lm_studio_model_id = 57;
optional string plan_mode_lite_llm_model_id = 58;
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 59;
optional string plan_mode_requesty_model_id = 60;
optional OpenRouterModelInfo plan_mode_requesty_model_info = 61;
optional string plan_mode_together_model_id = 62;
optional string plan_mode_fireworks_model_id = 63;
optional string plan_mode_sap_ai_core_model_id = 64;
optional string plan_mode_sap_ai_core_deployment_id = 65;
optional string plan_mode_groq_model_id = 66;
optional OpenRouterModelInfo plan_mode_groq_model_info = 67;
optional string plan_mode_baseten_model_id = 68;
optional OpenRouterModelInfo plan_mode_baseten_model_info = 69;
optional string plan_mode_hugging_face_model_id = 70;
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 71;
optional string plan_mode_huawei_cloud_maas_model_id = 72;
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 73;
optional string plan_mode_oca_model_id = 74;
optional OcaModelInfo plan_mode_oca_model_info = 75;
optional string plan_mode_oca_reasoning_effort = 76;
optional string plan_mode_aihubmix_model_id = 77;
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 78;
optional string plan_mode_hicap_model_id = 79;
optional OpenRouterModelInfo plan_mode_hicap_model_info = 80;
optional string plan_mode_nous_research_model_id = 81;
optional string plan_mode_vercel_ai_gateway_model_id = 82;
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 83;
optional string act_mode_api_model_id = 84;
optional int64 act_mode_thinking_budget_tokens = 85;
optional string gemini_act_mode_thinking_level = 86;
optional string act_mode_reasoning_effort = 87;
optional string act_mode_verbosity = 88;
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 89;
optional bool act_mode_aws_bedrock_custom_selected = 90;
optional string act_mode_aws_bedrock_custom_model_base_id = 91;
optional string act_mode_open_router_model_id = 92;
optional OpenRouterModelInfo act_mode_open_router_model_info = 93;
optional string act_mode_open_ai_model_id = 94;
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 95;
optional string act_mode_ollama_model_id = 96;
optional string act_mode_lm_studio_model_id = 97;
optional string act_mode_lite_llm_model_id = 98;
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 99;
optional string act_mode_requesty_model_id = 100;
optional OpenRouterModelInfo act_mode_requesty_model_info = 101;
optional string act_mode_together_model_id = 102;
optional string act_mode_fireworks_model_id = 103;
optional string act_mode_sap_ai_core_model_id = 104;
optional string act_mode_sap_ai_core_deployment_id = 105;
optional string act_mode_groq_model_id = 106;
optional OpenRouterModelInfo act_mode_groq_model_info = 107;
optional string act_mode_baseten_model_id = 108;
optional OpenRouterModelInfo act_mode_baseten_model_info = 109;
optional string act_mode_hugging_face_model_id = 110;
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 111;
optional string act_mode_huawei_cloud_maas_model_id = 112;
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 113;
optional string act_mode_oca_model_id = 114;
optional OcaModelInfo act_mode_oca_model_info = 115;
optional string act_mode_oca_reasoning_effort = 116;
optional string act_mode_aihubmix_model_id = 117;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 118;
optional string act_mode_hicap_model_id = 119;
optional OpenRouterModelInfo act_mode_hicap_model_info = 120;
optional string act_mode_nous_research_model_id = 121;
optional string act_mode_vercel_ai_gateway_model_id = 122;
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 123;
optional ApiProvider plan_mode_api_provider = 124;
optional ApiProvider act_mode_api_provider = 125;
optional string hicap_model_id = 126;
optional string lm_studio_model_id = 127;
optional AutoApprovalSettings auto_approval_settings = 128;
optional string global_cline_rules_toggles = 129;
optional string global_workflow_toggles = 130;
optional string global_skills_toggles = 131;
optional BrowserSettings browser_settings = 132;
optional string telemetry_setting = 133;
optional bool plan_act_separate_models_setting = 134;
optional bool enable_checkpoints_setting = 135;
optional int32 shell_integration_timeout = 136;
optional string default_terminal_profile = 137;
optional int32 terminal_output_line_limit = 138;
optional int32 max_consecutive_mistakes = 139;
optional int32 subagent_terminal_output_line_limit = 140;
optional bool strict_plan_mode_enabled = 141;
optional bool yolo_mode_toggled = 142;
optional bool use_auto_condense = 143;
optional bool cline_web_tools_enabled = 144;
optional string preferred_language = 145;
optional OpenaiReasoningEffort openai_reasoning_effort = 146;
optional PlanActMode mode = 147;
optional DictationSettings dictation_settings = 148;
optional FocusChainSettings focus_chain_settings = 149;
optional string custom_prompt = 150;
optional double auto_condense_threshold = 151;
optional bool hooks_enabled = 152;
optional bool subagents_enabled = 153;
optional bool enable_parallel_tool_calling = 154;
optional bool background_edit_enabled = 155;
optional bool skills_enabled = 156;
optional bool opt_out_of_remote_config = 157;
optional bool open_telemetry_enabled = 158;
optional string open_telemetry_metrics_exporter = 159;
optional string open_telemetry_logs_exporter = 160;
optional string open_telemetry_otlp_protocol = 161;
optional string open_telemetry_otlp_endpoint = 162;
optional string open_telemetry_otlp_metrics_protocol = 163;
optional string open_telemetry_otlp_metrics_endpoint = 164;
optional string open_telemetry_otlp_logs_protocol = 165;
optional string open_telemetry_otlp_logs_endpoint = 166;
optional int32 open_telemetry_metric_export_interval = 167;
optional bool open_telemetry_otlp_insecure = 168;
optional int32 open_telemetry_log_batch_size = 169;
optional int32 open_telemetry_log_batch_timeout = 170;
optional int32 open_telemetry_log_max_queue_size = 171;
}
message DictationSettings {
@@ -374,6 +420,7 @@ message UpdateSettingsRequest {
optional bool background_edit_enabled = 36;
optional string oca_reasoning_effort = 37;
optional bool skills_enabled = 38;
optional bool opt_out_of_remote_config = 39;
}
message UpdateTerminalConnectionTimeoutRequest {
+413
View File
@@ -0,0 +1,413 @@
#!/usr/bin/env node
/**
* Generates proto message definitions from TypeScript source of truth.
*
* This script reads the field definitions from src/shared/storage/state-keys.ts
* and generates the corresponding proto message definitions for Secrets and Settings.
*
* Usage: node scripts/generate-state-proto.mjs
*
* The generated proto content is written to proto/cline/state.proto,
* replacing only the Secrets and Settings messages while preserving
* the rest of the file (services, enums, other messages).
*/
import * as fs from "node:fs/promises"
import { Project, SyntaxKind } from "ts-morph"
const STATE_KEYS_PATH = "src/shared/storage/state-keys.ts"
const STATE_PROTO_PATH = "proto/cline/state.proto"
/**
* Convert camelCase to snake_case for proto field names
*/
function camelToSnake(str) {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
}
// Fields that should use int64 instead of int32
const INT64_FIELDS = new Set(["planModeThinkingBudgetTokens", "actModeThinkingBudgetTokens"])
// Fields that should use double instead of int32
const DOUBLE_FIELDS = new Set(["autoCondenseThreshold"])
/**
* Infer proto type from TypeScript type expression
* @param {string} typeText - The TypeScript type expression
* @param {string} [fieldName] - Optional field name for field-specific overrides
*/
function inferProtoType(typeText, fieldName) {
// Remove 'undefined' from union types
const cleanType = typeText
.replace(/\s*\|\s*undefined/g, "")
.replace(/undefined\s*\|\s*/g, "")
.trim()
// Handle common types
if (cleanType === "string") {
return "string"
}
if (cleanType === "boolean") {
return "bool"
}
if (cleanType === "number") {
// Some number fields need specific numeric types
if (fieldName && INT64_FIELDS.has(fieldName)) {
return "int64"
}
if (fieldName && DOUBLE_FIELDS.has(fieldName)) {
return "double"
}
return "int32"
}
// Handle Record<string, string> as map<string, string>
if (/Record\s*<\s*string\s*,\s*string\s*>/.test(cleanType)) {
return "map<string, string>"
}
// Handle specific known types that map to proto messages/enums
// Order matters! More specific types must come before generic ones
// (e.g., OpenAiCompatibleModelInfo before ModelInfo)
// Check known types BEFORE string literals, since types like `"act" as Mode`
// contain quotes but should map to proto enums
const knownTypes = [
// Specific model info types first
["OpenAiCompatibleModelInfo", "OpenAiCompatibleModelInfo"],
["LiteLLMModelInfo", "LiteLLMModelInfo"],
["OcaModelInfo", "OcaModelInfo"],
// Generic ModelInfo last (catches OpenRouterModelInfo, etc.)
["ModelInfo", "OpenRouterModelInfo"],
// Other types - order matters for substring matching
["AutoApprovalSettings", "AutoApprovalSettings"],
["BrowserSettings", "BrowserSettings"],
["DictationSettings", "DictationSettings"],
["FocusChainSettings", "FocusChainSettings"],
["OpenaiReasoningEffort", "OpenaiReasoningEffort"],
["PlanActMode", "PlanActMode"],
["ApiProvider", "ApiProvider"],
["LanguageModelChatSelector", "LanguageModelChatSelector"], // Must come before "Mode" check
]
for (const [tsType, protoType] of knownTypes) {
if (cleanType.includes(tsType)) {
return protoType
}
}
// Check for Mode type separately with word boundary to avoid matching "VsCodeLmModelSelector"
// This handles TS `Mode` type which maps to proto `PlanActMode`
if (/\bMode\b/.test(cleanType)) {
return "PlanActMode"
}
// Handle specific string literal unions (treat as string)
// This comes after known types check since some types like `"act" as Mode` contain quotes
if (cleanType.includes('"') || cleanType.includes("'")) {
return "string"
}
// Default to string for complex types we can't map
return "string"
}
/**
* Parse the SECRETS_KEYS array from state-keys.ts
*/
function parseSecretsKeys(sourceFile) {
const secretsDecl = sourceFile.getVariableDeclaration("SECRETS_KEYS")
if (!secretsDecl) {
throw new Error("Could not find SECRETS_KEYS declaration")
}
let initializer = secretsDecl.getInitializer()
if (!initializer) {
throw new Error("SECRETS_KEYS has no initializer")
}
// Handle 'as const' expression
if (initializer.getKind() === SyntaxKind.AsExpression) {
initializer = initializer.getExpression()
}
if (initializer.getKind() !== SyntaxKind.ArrayLiteralExpression) {
throw new Error(`SECRETS_KEYS is not an array literal (got ${SyntaxKind[initializer.getKind()]})`)
}
const keys = []
for (const element of initializer.getElements()) {
const text = element.getText()
// Remove quotes and handle special prefixes
const key = text.replace(/^['"]|['"]$/g, "")
// Skip prefixed keys like "cline:clineAccountId"
if (!key.includes(":")) {
keys.push(key)
}
}
return keys
}
/**
* Parse field definitions from an object literal in state-keys.ts
*/
function parseFieldDefinitions(sourceFile, variableName) {
const decl = sourceFile.getVariableDeclaration(variableName)
if (!decl) {
throw new Error(`Could not find ${variableName} declaration`)
}
const initializer = decl.getInitializer()
if (!initializer) {
throw new Error(`${variableName} has no initializer`)
}
// Handle 'satisfies' expression
let objectLiteral = initializer
if (initializer.getKind() === SyntaxKind.SatisfiesExpression) {
objectLiteral = initializer.getExpression()
}
if (objectLiteral.getKind() !== SyntaxKind.ObjectLiteralExpression) {
throw new Error(`${variableName} is not an object literal`)
}
const fields = []
for (const prop of objectLiteral.getProperties()) {
if (prop.getKind() !== SyntaxKind.PropertyAssignment) {
continue
}
const name = prop.getName()
const propInit = prop.getInitializer()
if (!propInit || propInit.getKind() !== SyntaxKind.ObjectLiteralExpression) {
continue
}
// Get the 'default' property to infer the type
const defaultProp = propInit.getProperty("default")
if (!defaultProp) {
continue
}
let typeText = "string"
const defaultInit = defaultProp.getInitializer()
if (defaultInit) {
// Check for 'as' expression to get the type
if (defaultInit.getKind() === SyntaxKind.AsExpression) {
const typeNode = defaultInit.getTypeNode()
if (typeNode) {
typeText = typeNode.getText()
}
} else {
// Infer from literal
const text = defaultInit.getText()
if (text === "true" || text === "false") {
typeText = "boolean"
} else if (/^\d+$/.test(text)) {
typeText = "number"
} else if (/^\d+\.\d+$/.test(text)) {
typeText = "number"
}
}
}
fields.push({
name,
tsType: typeText,
protoType: inferProtoType(typeText, name),
})
}
return fields
}
/**
* Convert snake_case to camelCase for mapping proto fields back to TS keys
*/
function snakeToCamel(str) {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
}
/**
* Parse field numbers from an existing proto message definition
* Returns a map of camelCase field names to their field numbers
*/
function parseProtoMessageFieldNumbers(protoContent, messageName) {
const fieldNumbers = {}
// Match the message block (handles single-level nesting for now)
const messageRegex = new RegExp(`message\\s+${messageName}\\s*\\{([^}]*(?:\\{[^}]*\\}[^}]*)*)\\}`, "s")
const match = protoContent.match(messageRegex)
if (!match) {
return fieldNumbers
}
const messageBody = match[1]
// Match field definitions: optional/required/repeated type name = number;
const fieldRegex = /(?:optional|required|repeated)?\s*\w+\s+(\w+)\s*=\s*(\d+)\s*;/g
const matches = messageBody.matchAll(fieldRegex)
for (const fieldMatch of matches) {
const snakeName = fieldMatch[1]
const fieldNum = parseInt(fieldMatch[2], 10)
const camelName = snakeToCamel(snakeName)
fieldNumbers[camelName] = fieldNum
}
return fieldNumbers
}
/**
* Load field number mappings from existing proto file
*/
async function loadFieldNumbersFromProto() {
try {
const protoContent = await fs.readFile(STATE_PROTO_PATH, "utf-8")
const secrets = parseProtoMessageFieldNumbers(protoContent, "Secrets")
const settings = parseProtoMessageFieldNumbers(protoContent, "Settings")
console.log(` Found ${Object.keys(secrets).length} existing Secrets fields`)
console.log(` Found ${Object.keys(settings).length} existing Settings fields`)
return { Secrets: secrets, Settings: settings }
} catch {
// Proto file doesn't exist, start fresh
return { Secrets: {}, Settings: {} }
}
}
/**
* Assign field numbers, preserving existing assignments and adding new ones
*/
function assignFieldNumbers(fields, existingNumbers, startNumber = 1) {
const result = {}
let nextNumber = startNumber
// Find the highest existing number
for (const num of Object.values(existingNumbers)) {
if (num >= nextNumber) {
nextNumber = num + 1
}
}
// Preserve existing assignments
for (const field of fields) {
if (existingNumbers[field.name] !== undefined) {
result[field.name] = existingNumbers[field.name]
}
}
// Assign new numbers for new fields
for (const field of fields) {
if (result[field.name] === undefined) {
result[field.name] = nextNumber++
}
}
return result
}
/**
* Generate proto message definition
*/
function generateProtoMessage(messageName, fields, fieldNumbers) {
const lines = [`message ${messageName} {`]
// Sort fields by field number for consistent output
const sortedFields = [...fields].sort((a, b) => fieldNumbers[a.name] - fieldNumbers[b.name])
for (const field of sortedFields) {
const snakeName = camelToSnake(field.name)
const fieldNum = fieldNumbers[field.name]
// Map types cannot have the 'optional' modifier in proto3
const prefix = field.protoType.startsWith("map<") ? "" : "optional "
lines.push(` ${prefix}${field.protoType} ${snakeName} = ${fieldNum};`)
}
lines.push("}")
return lines.join("\n")
}
/**
* Generate Secrets message from SECRETS_KEYS
*/
function generateSecretsMessage(secretsKeys, fieldNumbers) {
const fields = secretsKeys.map((key) => ({
name: key,
protoType: "string",
}))
return generateProtoMessage("Secrets", fields, fieldNumbers)
}
/**
* Replace a message in the proto file content
*/
function replaceMessage(protoContent, messageName, newMessageContent) {
// Match the message definition including nested braces
const messageRegex = new RegExp(`message\\s+${messageName}\\s*\\{[^}]*(?:\\{[^}]*\\}[^}]*)*\\}`, "g")
if (messageRegex.test(protoContent)) {
return protoContent.replace(messageRegex, newMessageContent)
} else {
// Message doesn't exist, append before the first message or at end
console.warn(`Warning: ${messageName} message not found in proto file, appending`)
return protoContent + "\n\n" + newMessageContent
}
}
async function main() {
console.log("Generating proto definitions from TypeScript source...")
// Parse TypeScript source
const project = new Project({
tsConfigFilePath: "tsconfig.json",
})
const sourceFile = project.addSourceFileAtPath(STATE_KEYS_PATH)
// Parse definitions
const secretsKeys = parseSecretsKeys(sourceFile)
console.log(`Found ${secretsKeys.length} secret keys`)
const apiHandlerFields = parseFieldDefinitions(sourceFile, "API_HANDLER_SETTINGS_FIELDS")
const userSettingsFields = parseFieldDefinitions(sourceFile, "USER_SETTINGS_FIELDS")
const settingsFields = [...apiHandlerFields, ...userSettingsFields]
console.log(`Found ${settingsFields.length} settings fields`)
// Load existing field numbers from proto file
const existingFieldNumbers = await loadFieldNumbersFromProto()
// Assign field numbers (preserving existing, adding new ones)
const secretsFieldNumbers = assignFieldNumbers(
secretsKeys.map((k) => ({ name: k })),
existingFieldNumbers.Secrets,
1,
)
const settingsFieldNumbers = assignFieldNumbers(settingsFields, existingFieldNumbers.Settings, 1)
// Generate messages
const secretsMessage = generateSecretsMessage(secretsKeys, secretsFieldNumbers)
const settingsMessage = generateProtoMessage("Settings", settingsFields, settingsFieldNumbers)
// Read existing proto file
let protoContent = await fs.readFile(STATE_PROTO_PATH, "utf-8")
// Replace messages
protoContent = replaceMessage(protoContent, "Secrets", secretsMessage)
protoContent = replaceMessage(protoContent, "Settings", settingsMessage)
// Write updated proto file
await fs.writeFile(STATE_PROTO_PATH, protoContent)
console.log(`Updated ${STATE_PROTO_PATH}`)
console.log("\nGeneration complete! Run 'npm run protos' to regenerate TypeScript from protos.")
}
main().catch((error) => {
console.error("Error:", error)
process.exit(1)
})
+3 -3
View File
@@ -2,12 +2,12 @@
/**
* NPM Package Builder for Cline CLI
*
*
* This script builds the Cline CLI NPM package (dist-standalone/).
* It is completely independent from package-standalone.mjs (JetBrains build).
*
*
* Usage: node scripts/package-npm.mjs
*
*
* Prerequisites:
* - npm run protos && npm run protos-go
* - npm run compile-cli
+2 -12
View File
@@ -76,19 +76,9 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
await showVersionUpdateAnnouncement(context)
// Initialize banner service
// Initialize banner service (TEMPORARILY DISABLED - not fetching banners to prevent API hammering)
BannerService.initialize(webview.controller)
BannerService.get()
.fetchActiveBanners()
.then((banners) => {
if (banners.length > 0) {
Logger.log(`BannerService: ${banners.length} active banner(s) fetched.`)
// Banners are now cached and can be accessed by the frontend when needed
}
})
.catch((error) => {
Logger.error("BannerService: Failed to fetch banners on startup", error)
})
// DISABLED: .getActiveBanners(true)
telemetryService.captureExtensionActivated()
+1 -1
View File
@@ -259,7 +259,7 @@ export class AIhubmixHandler implements ApiHandler {
const stream = await client.chat.completions.create(fixedRequestBody)
for await (const chunk of stream as any) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -199,7 +199,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 (["x-ai/grok-code-fast-1", "minimax/minimax-m2.1"].includes(this.getModel().id)) {
if (["x-ai/grok-code-fast-1", "kwaipilot/kat-coder-pro"].includes(this.getModel().id)) {
totalCost = 0
}
+1 -1
View File
@@ -104,7 +104,7 @@ export class DeepSeekHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -67,7 +67,7 @@ export class DoubaoHandler implements ApiHandler {
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -60,7 +60,7 @@ export class FireworksHandler implements ApiHandler {
let reasoning: string | null = null
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (reasoning || delta?.content?.includes("<think>")) {
reasoning = (reasoning || "") + (delta.content ?? "")
}
+1 -1
View File
@@ -229,7 +229,7 @@ export class GroqHandler implements ApiHandler {
const stream = await client.chat.completions.create(requestParams)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
// Handle reasoning field if present (for reasoning models with parsed output)
if ((delta as any)?.reasoning) {
+1 -1
View File
@@ -66,7 +66,7 @@ export class HicapHandler implements ApiHandler {
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -86,7 +86,7 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
// Handle reasoning content detection
if (delta?.content) {
+1 -1
View File
@@ -97,7 +97,7 @@ export class HuggingFaceHandler implements ApiHandler {
for await (const chunk of stream) {
_chunkCount++
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
_totalContent += delta.content
+1 -1
View File
@@ -307,7 +307,7 @@ export class LiteLlmHandler implements ApiHandler {
} as LiteLlmChatCompletionCreateParams)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
// Handle normal text content
if (delta?.content) {
+1 -1
View File
@@ -60,7 +60,7 @@ export class LmStudioHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const choice = chunk.choices[0]
const choice = chunk.choices?.[0]
const delta = choice?.delta
if (delta?.content) {
yield {
+1 -1
View File
@@ -62,7 +62,7 @@ export class MoonshotHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -57,7 +57,7 @@ export class NebiusHandler implements ApiHandler {
})
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -55,7 +55,7 @@ export class NousResearchHandler implements ApiHandler {
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -2
View File
@@ -241,7 +241,7 @@ export class OcaHandler implements ApiHandler {
const stream = await client.chat.completions.create(chatCompletionsParams)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
// Handle normal text content
if (delta?.content) {
@@ -303,7 +303,6 @@ export class OcaHandler implements ApiHandler {
}
async *createMessageResponsesApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
console.log("Uses Responses API")
const client = this.ensureClient()
// Convert messages to Responses API input format
+1 -1
View File
@@ -123,7 +123,7 @@ export class OpenAiNativeHandler implements ApiHandler {
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -132,7 +132,7 @@ export class OpenAiHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -113,7 +113,7 @@ export class OpenRouterHandler implements ApiHandler {
this.lastGenerationId = chunk.id
}
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -122,7 +122,7 @@ export class QwenHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -99,7 +99,7 @@ export class RequestyHandler implements ApiHandler {
let lastUsage: any
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -68,7 +68,7 @@ export class SambanovaHandler implements ApiHandler {
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+6
View File
@@ -601,6 +601,8 @@ export class SapAiCoreHandler implements ApiHandler {
}
const anthropicModels = [
"anthropic--claude-4.5-haiku",
"anthropic--claude-4.5-opus",
"anthropic--claude-4.5-sonnet",
"anthropic--claude-4-sonnet",
"anthropic--claude-4-opus",
@@ -649,7 +651,9 @@ export class SapAiCoreHandler implements ApiHandler {
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
if (
model.id === "anthropic--claude-4.5-opus" ||
model.id === "anthropic--claude-4.5-sonnet" ||
model.id === "anthropic--claude-4.5-haiku" ||
model.id === "anthropic--claude-4-sonnet" ||
model.id === "anthropic--claude-4-opus" ||
model.id === "anthropic--claude-3.7-sonnet"
@@ -779,7 +783,9 @@ export class SapAiCoreHandler implements ApiHandler {
} else if (openAIModels.includes(model.id) || perplexityModels.includes(model.id)) {
yield* this.streamCompletionGPT(response.data, model)
} else if (
model.id === "anthropic--claude-4.5-opus" ||
model.id === "anthropic--claude-4.5-sonnet" ||
model.id === "anthropic--claude-4.5-haiku" ||
model.id === "anthropic--claude-4-sonnet" ||
model.id === "anthropic--claude-4-opus" ||
model.id === "anthropic--claude-3.7-sonnet"
+1 -1
View File
@@ -66,7 +66,7 @@ export class TogetherHandler implements ApiHandler {
})
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -71,7 +71,7 @@ export class VercelAIGatewayHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
+1 -1
View File
@@ -69,7 +69,7 @@ export class XAIHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -95,7 +95,7 @@ export class ZAiHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+50 -5
View File
@@ -10,6 +10,43 @@ import {
ClineUserToolResultContentBlock,
} from "@/shared/messages/content"
// OpenAI API has a maximum tool call ID length of 40 characters
const MAX_TOOL_CALL_ID_LENGTH = 40
/**
* Determines if a given tool ID follows the OpenAI Responses API format for tool calls.
* OpenAI tool call IDs start with "fc_" and are exactly 53 characters long.
*
* @param callId - The tool ID to check
* @returns True if the tool ID matches the OpenAI Responses API format, false otherwise
*/
function isOpenAIResponseToolId(callId: string): boolean {
return callId.startsWith("fc_") && callId.length === 53
}
/**
* Transforms a tool ID to a consistent format for OpenAI's Chat Completions API.
* This function MUST be used for both tool_calls[].id (assistant) and tool_call_id (tool result)
* to ensure they match - otherwise OpenAI will reject the request with:
* "Invalid parameter: 'tool_call_id' of 'xxx' not found in 'tool_calls' of previous message."
*
* @param toolId - The original tool ID from Cline/Anthropic format
* @returns The transformed ID suitable for OpenAI API
*/
function transformToolCallId(toolId: string): string {
// OpenAI Responses API uses "fc_" prefix with 53 char length
// Convert these to "call_" prefix format for Chat Completions API
if (isOpenAIResponseToolId(toolId)) {
// Use the last 33 chars + "call_" (5 chars) to stay under the 40-char limit.
return `call_${toolId.slice(toolId.length - (MAX_TOOL_CALL_ID_LENGTH - 5))}`
}
// Ensure ID doesn't exceed max length
if (toolId.length > MAX_TOOL_CALL_ID_LENGTH) {
return toolId.slice(0, MAX_TOOL_CALL_ID_LENGTH)
}
return toolId
}
/**
* Converts an array of ClineStorageMessage objects to OpenAI's Completions API format.
*
@@ -80,7 +117,9 @@ export function convertToOpenAiMessages(
}
openAiMessages.push({
role: "tool",
tool_call_id: toolMessage.tool_use_id,
// The tool_call_id must match the id used in the assistant's tool_calls array.
// Use the same transformation logic as tool_calls to ensure IDs match.
tool_call_id: transformToolCallId(toolMessage.tool_use_id),
content: content,
})
})
@@ -171,23 +210,29 @@ export function convertToOpenAiMessages(
// Process tool use messages
const tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => {
const toolDetails = toolMessage.reasoning_details
const toolId = toolMessage.id
if (toolDetails) {
if (Array.isArray(toolDetails)) {
// For Gemini: reasoning details must be linkable back to the tool call.
// Sometimes OpenRouter/Gemini returns entries without `id`; those poison the next request.
// Keep only entries with an id matching the tool call id.
// See: https://github.com/cline/cline/issues/8214
const validDetails = toolDetails.filter((detail: any) => detail?.id === toolMessage.id)
if (validDetails.length > 0) reasoningDetails.push(...validDetails)
const validDetails = toolDetails.filter((detail: any) => detail?.id === toolId)
if (validDetails.length > 0) {
reasoningDetails.push(...validDetails)
}
} else {
// Single reasoning detail - only include if it has matching id
const detail = toolDetails as any
if (detail?.id === toolMessage.id) reasoningDetails.push(toolDetails)
if (detail?.id === toolId) {
reasoningDetails.push(toolDetails)
}
}
}
return {
id: toolMessage.id,
// Use the same transformation as tool_call_id to ensure IDs match
id: transformToolCallId(toolId),
type: "function",
function: {
name: toolMessage.name,
@@ -170,7 +170,8 @@ export function convertToOpenAIResponsesInput(messages: ClineStorageMessage[]):
assistantItems.push({
type: "function_call",
call_id,
id: part.id,
// MAX 53 characters for OpenAI Responses API tool IDs
id: !part.id.startsWith("fc_") ? `fc_${part.id.slice(0, 50)}` : part.id,
name: part.name,
arguments: JSON.stringify(part.input ?? {}),
})
+2 -2
View File
@@ -180,7 +180,7 @@ export async function createOpenRouterStream(
thinkingBudgetTokens &&
model.info?.thinkingConfig &&
thinkingBudgetTokens > 0 &&
!(model.id.includes("gemini") && geminiThinkingLevel)
!(model.id.includes("gemini-3") && geminiThinkingLevel)
) {
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: thinkingBudgetTokens }
@@ -212,7 +212,7 @@ export async function createOpenRouterStream(
...(providerPreferences ? { provider: providerPreferences } : {}),
...(isClaudeSonnet1m ? { provider: { order: ["anthropic", "google-vertex/global"], allow_fallbacks: false } } : {}),
...getOpenAIToolParams(tools),
...(model.id.includes("gemini") && geminiThinkingLevel
...(model.id.includes("gemini-3") && geminiThinkingLevel
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
: {}),
})
@@ -139,7 +139,7 @@ export async function createVercelAIGatewayStream(
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
...(reasoning ? { reasoning } : {}),
...getOpenAIToolParams(tools),
...(model.id.includes("gemini") && geminiThinkingLevel
...(model.id.includes("gemini-3") && geminiThinkingLevel
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
: {}),
})
@@ -0,0 +1,30 @@
import { expect } from "chai"
import { parseYamlFrontmatter } from "../frontmatter"
describe("parseYamlFrontmatter", () => {
it("returns original content when no frontmatter", () => {
const input = "Just text"
const result = parseYamlFrontmatter(input)
expect(result.hadFrontmatter).to.equal(false)
expect(result.data).to.deep.equal({})
expect(result.body).to.equal(input)
})
it("parses valid YAML frontmatter", () => {
const input = `---\npaths:\n - "src/**"\n---\n\nHello`
const result = parseYamlFrontmatter(input)
expect(result.hadFrontmatter).to.equal(true)
expect(result.parseError).to.equal(undefined)
expect(result.data).to.deep.equal({ paths: ["src/**"] })
expect(result.body.trim()).to.equal("Hello")
})
it("fails open on malformed YAML", () => {
const input = `---\npaths: [invalid\n---\nBody`
const result = parseYamlFrontmatter(input)
expect(result.hadFrontmatter).to.equal(true)
expect(result.data).to.deep.equal({})
expect(result.body).to.equal(input)
expect(result.parseError).to.be.a("string")
})
})
@@ -0,0 +1,53 @@
import * as yaml from "js-yaml"
export type FrontmatterParseResult = {
data: Record<string, unknown>
/**
* The markdown content after stripping the `--- frontmatter ---` block.
*
* Named `body` (rather than `content`) to make it clear this is the remaining
* document body and to keep this helper generic for multiple consumers.
*/
body: string
/**
* True when the input contained a frontmatter block, even if parsing failed.
*
* This allows callers to distinguish:
* - "no frontmatter provided" (baseline behavior), vs
* - "frontmatter was provided" (may have semantic meaning in future consumers).
*/
hadFrontmatter: boolean
/**
* Present only when YAML frontmatter was detected but failed to parse.
*
* This helper is intentionally fail-open and does not log. Returning `parseError`
* lets each caller decide whether to log, surface diagnostics, etc.
*/
parseError?: string
}
/**
* Parse YAML frontmatter from markdown content.
*
* Behavior is intentionally fail-open:
* - If YAML fails to parse, returns data={} and body=original markdown.
* - If no frontmatter exists, returns data={} and body=original markdown.
*/
export function parseYamlFrontmatter(markdown: string): FrontmatterParseResult {
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/
const match = markdown.match(frontmatterRegex)
if (!match) {
return { data: {}, body: markdown, hadFrontmatter: false }
}
const [, yamlContent, body] = match
try {
const data = (yaml.load(yamlContent) as Record<string, unknown>) || {}
return { data, body, hadFrontmatter: true }
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return { data: {}, body: markdown, hadFrontmatter: true, parseError: message }
}
}
@@ -2,28 +2,16 @@ import { ensureSkillsDirectoryExists, GlobalFileNames } from "@core/storage/disk
import type { SkillContent, SkillMetadata } from "@shared/skills"
import { fileExistsAtPath, isDirectory } from "@utils/fs"
import * as fs from "fs/promises"
import * as yaml from "js-yaml"
import * as path from "path"
import { parseYamlFrontmatter } from "./frontmatter"
/**
* Parse YAML frontmatter from markdown content.
*/
/** Parse YAML frontmatter from markdown content (shared helper). */
function parseFrontmatter(fileContent: string): { data: Record<string, unknown>; content: string } {
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/
const match = fileContent.match(frontmatterRegex)
if (!match) {
return { data: {}, content: fileContent }
}
const [, yamlContent, body] = match
try {
const data = yaml.load(yamlContent) as Record<string, unknown>
return { data: data || {}, content: body }
} catch (error) {
console.warn("Failed to parse YAML frontmatter:", error)
return { data: {}, content: fileContent }
const result = parseYamlFrontmatter(fileContent)
if (result.parseError) {
console.warn("Failed to parse YAML frontmatter:", result.parseError)
}
return { data: result.data, content: result.body }
}
/**
+20 -63
View File
@@ -30,14 +30,16 @@ import { ExtensionRegistryInfo } from "@/registry"
import { AuthService } from "@/services/auth/AuthService"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
import { LogoutReason } from "@/services/auth/types"
import { BannerService } from "@/services/banner/BannerService"
import { featureFlagsService } from "@/services/feature-flags"
import { getDistinctId } from "@/services/logging/distinctId"
import { Logger } from "@/services/logging/Logger"
import { telemetryService } from "@/services/telemetry"
import { BannerCardData } from "@/shared/cline/banner"
import { getAxiosSettings } from "@/shared/net"
import { ShowMessageType } from "@/shared/proto/host/window"
import { getLatestAnnouncementId } from "@/utils/announcements"
import { getCwd, getDesktopDir } from "@/utils/path"
import { BannerService } from "../../services/banner/BannerService"
import { PromptRegistry } from "../prompts/system-prompt"
import {
ensureCacheDirectoryExists,
@@ -47,6 +49,7 @@ import {
writeMcpMarketplaceCatalogToCache,
} from "../storage/disk"
import { fetchRemoteConfig } from "../storage/remote-config/fetch"
import { clearRemoteConfig } from "../storage/remote-config/utils"
import { type PersistenceErrorEvent, StateManager } from "../storage/StateManager"
import { Task } from "../task"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
@@ -104,13 +107,13 @@ export class Controller {
/**
* Starts the periodic remote config fetching timer
* Fetches immediately and then every 30 seconds
* Fetches immediately and then every hour
*/
private startRemoteConfigTimer() {
// Initial fetch
fetchRemoteConfig(this)
// Set up 30-second interval
this.remoteConfigTimer = setInterval(() => fetchRemoteConfig(this), 30000) // 30 seconds
// Set up 1-hour interval
this.remoteConfigTimer = setInterval(() => fetchRemoteConfig(this), 3600000) // 1 hour
}
constructor(readonly context: vscode.ExtensionContext) {
@@ -119,7 +122,7 @@ export class Controller {
this.stateManager = StateManager.get()
StateManager.get().registerCallbacks({
onPersistenceError: async ({ error }: PersistenceErrorEvent) => {
console.error("[Controller] Cache persistence failed, recovering:", error)
Logger.error("[Controller] Cache persistence failed, recovering:", error)
try {
await StateManager.get().reInitialize(this.task?.taskId)
await this.postStateToWebview()
@@ -128,7 +131,7 @@ export class Controller {
message: "Saving settings to storage failed.",
})
} catch (recoveryError) {
console.error("[Controller] Cache recovery failed:", recoveryError)
Logger.error("[Controller] Cache recovery failed:", recoveryError)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to save settings. Please restart the extension.",
@@ -186,6 +189,7 @@ export class Controller {
try {
// AuthService now handles its own storage cleanup in handleDeauth()
this.stateManager.setGlobalState("userInfo", undefined)
clearRemoteConfig()
// Update API providers through cache service
const apiConfiguration = this.stateManager.getApiConfiguration()
@@ -535,6 +539,8 @@ export class Controller {
// Mark welcome view as completed since user has successfully logged in
this.stateManager.setGlobalState("welcomeViewCompleted", true)
await fetchRemoteConfig(this)
if (this.task) {
this.task.api = buildApiHandler({ ...updatedConfig, ulid: this.task.ulid }, currentMode)
}
@@ -877,6 +883,7 @@ export class Controller {
const distinctId = getDistinctId()
const version = ExtensionRegistryInfo.version
const environment = ClineEnv.config().environment
const banners = await this.getBanners()
// Set feature flag in dictation settings based on platform
const updatedDictationSettings = {
@@ -961,6 +968,8 @@ export class Controller {
enableParallelToolCalling: this.stateManager.getGlobalSettingsKey("enableParallelToolCalling"),
backgroundEditEnabled: this.stateManager.getGlobalSettingsKey("backgroundEditEnabled"),
skillsEnabled,
optOutOfRemoteConfig: this.stateManager.getGlobalSettingsKey("optOutOfRemoteConfig"),
banners,
}
}
@@ -1003,64 +1012,12 @@ export class Controller {
return history
}
/**
* Initializes the BannerService if not already initialized
*/
private async ensureBannerService() {
if (!BannerService.isInitialized()) {
try {
BannerService.initialize(this)
} catch (error) {
console.error("Failed to initialize BannerService:", error)
}
}
}
/**
* Fetches non-dismissed banners for display
* @returns Array of banners that haven't been dismissed
*/
async fetchBannersForDisplay(): Promise<any[]> {
async getBanners(): Promise<BannerCardData[]> {
try {
await this.ensureBannerService()
if (BannerService.isInitialized()) {
return await BannerService.get().getNonDismissedBanners()
}
} catch (error) {
console.error("Failed to fetch banners:", error)
}
return []
}
/**
* Dismisses a banner and sends telemetry
* @param bannerId The ID of the banner to dismiss
*/
async dismissBanner(bannerId: string): Promise<void> {
try {
await this.ensureBannerService()
if (BannerService.isInitialized()) {
await BannerService.get().dismissBanner(bannerId)
await this.postStateToWebview()
}
} catch (error) {
console.error("Failed to dismiss banner:", error)
}
}
/**
* Sends a banner event for telemetry tracking
* @param bannerId The ID of the banner
* @param eventType The type of event (seen, dismiss, click)
*/
async trackBannerEvent(bannerId: string, eventType: "dismiss"): Promise<void> {
try {
await this.ensureBannerService()
if (BannerService.isInitialized()) {
await BannerService.get().sendBannerEvent(bannerId, eventType)
}
} catch (error) {
console.error("Failed to track banner event:", error)
return BannerService.get().getActiveBanners()
} catch (err) {
console.log(err)
return []
}
}
}
@@ -18,7 +18,7 @@ export async function refreshLiteLlmModels(): Promise<Record<string, ModelInfo>>
try {
// Get the LiteLLM configuration
const apiConfiguration = stateManager.getApiConfiguration()
const baseUrl = apiConfiguration.liteLlmBaseUrl || ""
const baseUrl = apiConfiguration.liteLlmBaseUrl || "http://localhost:4000"
const apiKey = apiConfiguration.liteLlmApiKey
if (!apiKey) {
@@ -63,10 +63,10 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
}
const modelInfo = model.model_info
const supportedApiList = modelInfo.supported_api_list ?? [CHAT_COMPLETIONS_API]
const apiFormat: ApiFormat = supportedApiList.includes(RESPONSES_API)
? ApiFormat.OPENAI_RESPONSES
: ApiFormat.OPENAI_CHAT
console.log(modelId, supportedApiList)
const apiFormat: ApiFormat =
supportedApiList.includes(RESPONSES_API) && !supportedApiList.includes(CHAT_COMPLETIONS_API)
? ApiFormat.OPENAI_RESPONSES
: ApiFormat.OPENAI_CHAT
models[modelId] = OcaModelInfo.create({
maxTokens: model.litellm_params?.max_tokens || -1,
contextWindow: modelInfo.context_window,
@@ -1,8 +1,9 @@
import { Empty } from "@shared/proto/cline/common"
import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-configuration-conversion"
import { buildApiHandler } from "@/core/api"
import { ApiHandlerOptions, ApiHandlerSecrets, ApiProvider } from "@/shared/api"
import { ApiHandlerOptions, ApiProvider } from "@/shared/api"
import { UpdateApiConfigurationRequestNew } from "@/shared/proto/index.cline"
import { Secrets } from "@/shared/storage/state-keys"
import type { Controller } from "../index"
/**
@@ -69,7 +70,7 @@ export async function updateApiConfiguration(controller: Controller, request: Up
const { options: maskOptionsFields, secrets: maskSecretsFields } = parseFieldMask(updateMask)
// Process secrets based on field mask
const secrets: Partial<ApiHandlerSecrets> = {}
const secrets: Partial<Secrets> = {}
if (protoSecrets && maskSecretsFields.size > 0) {
// Validate all masked fields exist
@@ -81,7 +82,7 @@ export async function updateApiConfiguration(controller: Controller, request: Up
// Process entries that are in the mask
for (const [key, value] of Object.entries(protoSecrets)) {
if (maskSecretsFields.has(key)) {
secrets[key as keyof ApiHandlerSecrets] = value
secrets[key as keyof Secrets] = value
}
}
}
+11 -5
View File
@@ -1,9 +1,10 @@
import { BannerService } from "@/services/banner/BannerService"
import type { StringRequest } from "@/shared/proto/cline/common"
import { Empty } from "@/shared/proto/cline/common"
import type { Controller } from ".."
/**
* Dismisses a banner by ID
* Dismisses a banner and sends telemetry
* @param controller The controller instance
* @param request The request containing the banner ID to dismiss
* @returns Empty response
@@ -11,9 +12,14 @@ import type { Controller } from ".."
export async function dismissBanner(controller: Controller, request: StringRequest): Promise<Empty> {
const bannerId = request.value
if (bannerId) {
await controller.dismissBanner(bannerId)
if (!bannerId) {
return {}
}
return Empty.create()
try {
await BannerService.get().dismissBanner(bannerId)
await controller.postStateToWebview()
} catch (error) {
console.error("Failed to dismiss banner:", error)
}
return {}
}
+14 -6
View File
@@ -1,3 +1,4 @@
import { BannerService } from "@/services/banner/BannerService"
import { Empty } from "@/shared/proto/cline/common"
import type { TrackBannerEventRequest } from "@/shared/proto/cline/state"
import type { Controller } from ".."
@@ -8,12 +9,19 @@ import type { Controller } from ".."
* @param request The request containing banner ID and event type
* @returns Empty response
*/
export async function trackBannerEvent(controller: Controller, request: TrackBannerEventRequest): Promise<Empty> {
export async function trackBannerEvent(_controller: Controller, request: TrackBannerEventRequest): Promise<Empty> {
const { bannerId, eventType } = request
if (bannerId && eventType) {
await controller.trackBannerEvent(bannerId, eventType as "dismiss")
if (!bannerId) {
return {}
}
return Empty.create()
if (eventType !== "dismiss") {
console.error("Unsupported event type ", eventType)
return {}
}
try {
await BannerService.get().sendBannerEvent(bannerId, eventType)
} catch (error) {
console.error("Failed to track banner event:", error)
}
return {}
}
@@ -11,6 +11,8 @@ import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-
import { OpenaiReasoningEffort } from "@shared/storage/types"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { ClineEnv } from "@/config"
import { fetchRemoteConfig } from "@/core/storage/remote-config/fetch"
import { clearRemoteConfig } from "@/core/storage/remote-config/utils"
import { HostProvider } from "@/hosts/host-provider"
import { McpDisplayMode } from "@/shared/McpDisplayMode"
import { ShowMessageType } from "@/shared/proto/host/window"
@@ -383,6 +385,25 @@ export async function updateSettings(controller: Controller, request: UpdateSett
controller.stateManager.setGlobalState("enableParallelToolCalling", !!request.enableParallelToolCalling)
}
if (request.optOutOfRemoteConfig !== undefined) {
const hadOptedOut = controller.stateManager.getGlobalSettingsKey("optOutOfRemoteConfig")
const isOptingOut = !!request.optOutOfRemoteConfig
const isReenablingRemoteConfig = !isOptingOut && hadOptedOut
// Update now so any subsequent function can access the updated value
controller.stateManager.setGlobalState("optOutOfRemoteConfig", isOptingOut)
if (isOptingOut && !hadOptedOut) {
clearRemoteConfig()
} else if (isReenablingRemoteConfig) {
// Fire-and-forget: We don't need to await here
// The function catches any errors and posts the updated state to the webview
// The immediate state update below shows the user's intent (opted-in),
// and we apply the actual config afterwards without blocking the settings update
fetchRemoteConfig(controller)
}
}
// Post updated state to webview
await controller.postStateToWebview()
@@ -338,7 +338,7 @@
"type": "function",
"function": {
"name": "act_mode_respond",
"description": "Provide a progress update or preamble to the user during ACT MODE execution. This tool allows you to communicate your thought process and planned actions without interrupting the execution flow. After displaying your message, execution automatically continues, allowing you to proceed with subsequent tool calls immediately. This tool is only available in ACT MODE. This tool may not be called immediately after a previous act_mode_respond call.\n\nIMPORTANT: Use this tool frequently to create a better user experience. Since it's non-blocking, there's no performance penalty for frequent use.\n\nUse this tool when:\n- After reading files and before making any edits - explain your analysis and what changes you plan to make\n- When starting a new phase of work (e.g., transitioning from backend to frontend, or from one feature to another)\n- During long sequences of operations to provide progress updates\n- When your approach or strategy changes mid-task\n- Before executing complex or potentially risky operations\n- To explain why you're choosing one approach over another\n\nDo NOT use this tool when you have completed all required actions and are ready to present the final output; in that case, use the attempt_completion tool instead.\n\nCRITICAL CONSTRAINT: You MUST NOT call this tool more than once in a row. After using act_mode_respond, your next assistant message MUST either call a different tool or perform additional work without using act_mode_respond again. If you attempt to call act_mode_respond consecutively, the tool call will fail with an explicit error.",
"description": "Provide a progress update or preamble to the user during ACT MODE execution. This tool allows you to communicate your thought process and planned actions without interrupting the execution flow. After displaying your message, execution automatically continues, allowing you to proceed with subsequent tool calls immediately. This tool is only available in ACT MODE. This tool may not be called immediately after a previous act_mode_respond call.\n\nIMPORTANT: Use this tool when it adds value to the user experience, but always follow it with an actual tool call - never call it twice in a row.\n\nUse this tool when:\n- After reading files and before making any edits - explain your analysis and what changes you plan to make\n- When starting a new phase of work (e.g., transitioning from backend to frontend, or from one feature to another)\n- During long sequences of operations to provide progress updates\n- When your approach or strategy changes mid-task\n- Before executing complex or potentially risky operations\n- To explain why you're choosing one approach over another\n\nDo NOT use this tool when you have completed all required actions and are ready to present the final output; in that case, use the attempt_completion tool instead.\n\nCRITICAL CONSTRAINT: You MUST NOT call this tool more than once in a row. After using act_mode_respond, your next assistant message MUST either call a different tool or perform additional work without using act_mode_respond again. If you attempt to call act_mode_respond consecutively, the tool call will fail with an explicit error.",
"strict": false,
"parameters": {
"type": "object",
@@ -296,7 +296,7 @@
},
{
"name": "act_mode_respond",
"description": "Provide a progress update or preamble to the user during ACT MODE execution. This tool allows you to communicate your thought process and planned actions without interrupting the execution flow. After displaying your message, execution automatically continues, allowing you to proceed with subsequent tool calls immediately. This tool is only available in ACT MODE. This tool may not be called immediately after a previous act_mode_respond call.\n\nIMPORTANT: Use this tool frequently to create a better user experience. Since it's non-blocking, there's no performance penalty for frequent use.\n\nUse this tool when:\n- After reading files and before making any edits - explain your analysis and what changes you plan to make\n- When starting a new phase of work (e.g., transitioning from backend to frontend, or from one feature to another)\n- During long sequences of operations to provide progress updates\n- When your approach or strategy changes mid-task\n- Before executing complex or potentially risky operations\n- To explain why you're choosing one approach over another\n\nDo NOT use this tool when you have completed all required actions and are ready to present the final output; in that case, use the attempt_completion tool instead.\n\nCRITICAL CONSTRAINT: You MUST NOT call this tool more than once in a row. After using act_mode_respond, your next assistant message MUST either call a different tool or perform additional work without using act_mode_respond again. If you attempt to call act_mode_respond consecutively, the tool call will fail with an explicit error.",
"description": "Provide a progress update or preamble to the user during ACT MODE execution. This tool allows you to communicate your thought process and planned actions without interrupting the execution flow. After displaying your message, execution automatically continues, allowing you to proceed with subsequent tool calls immediately. This tool is only available in ACT MODE. This tool may not be called immediately after a previous act_mode_respond call.\n\nIMPORTANT: Use this tool when it adds value to the user experience, but always follow it with an actual tool call - never call it twice in a row.\n\nUse this tool when:\n- After reading files and before making any edits - explain your analysis and what changes you plan to make\n- When starting a new phase of work (e.g., transitioning from backend to frontend, or from one feature to another)\n- During long sequences of operations to provide progress updates\n- When your approach or strategy changes mid-task\n- Before executing complex or potentially risky operations\n- To explain why you're choosing one approach over another\n\nDo NOT use this tool when you have completed all required actions and are ready to present the final output; in that case, use the attempt_completion tool instead.\n\nCRITICAL CONSTRAINT: You MUST NOT call this tool more than once in a row. After using act_mode_respond, your next assistant message MUST either call a different tool or perform additional work without using act_mode_respond again. If you attempt to call act_mode_respond consecutively, the tool call will fail with an explicit error.",
"parameters": {
"type": "OBJECT",
"properties": {
@@ -28,7 +28,7 @@ const NATIVE_GPT_5: ClineToolSpec = {
name: "act_mode_respond",
description: `Provide a progress update or preamble to the user during ACT MODE execution. This tool allows you to communicate your thought process and planned actions without interrupting the execution flow. After displaying your message, execution automatically continues, allowing you to proceed with subsequent tool calls immediately. This tool is only available in ACT MODE. This tool may not be called immediately after a previous act_mode_respond call.
IMPORTANT: Use this tool frequently to create a better user experience. Since it's non-blocking, there's no performance penalty for frequent use.
IMPORTANT: Use this tool when it adds value to the user experience, but always follow it with an actual tool call - never call it twice in a row.
Use this tool when:
- After reading files and before making any edits - explain your analysis and what changes you plan to make
@@ -38,7 +38,6 @@ export const config = createVariant(ModelFamily.GEMINI_3)
SystemPromptSection.EDITING_FILES,
SystemPromptSection.FEEDBACK,
SystemPromptSection.TODO,
SystemPromptSection.MCP,
SystemPromptSection.TASK_PROGRESS,
SystemPromptSection.SYSTEM_INFO,
SystemPromptSection.OBJECTIVE,
@@ -125,9 +125,14 @@ export class VariantValidator {
// Check component overrides reference valid components
if (variant.componentOverrides) {
const invalidOverrides = Object.keys(variant.componentOverrides).filter(
(key) => !variant.componentOrder.includes(key as SystemPromptSection),
)
const invalidOverrides = Object.keys(variant.componentOverrides).filter((key) => {
const override = variant.componentOverrides[key as SystemPromptSection]
// Skip overrides that explicitly disable a component - these are valid even without being in componentOrder
if (override?.enabled === false) {
return false
}
return !variant.componentOrder.includes(key as SystemPromptSection)
})
if (invalidOverrides.length > 0) {
warnings.push(`Component overrides for unused components: ${invalidOverrides.join(", ")}`)
}
@@ -147,7 +152,14 @@ export class VariantValidator {
// Check tool overrides reference valid tools
if (variant.toolOverrides) {
const invalidOverrides = Object.keys(variant.toolOverrides).filter((key) => !variant.tools?.includes(key as any))
const invalidOverrides = Object.keys(variant.toolOverrides).filter((key) => {
const override = variant.toolOverrides![key as keyof typeof variant.toolOverrides]
// Skip overrides that explicitly disable a tool - these are valid even without being in tools list
if (override?.enabled === false) {
return false
}
return !variant.tools?.includes(key as any)
})
if (invalidOverrides.length > 0) {
warnings.push(`Tool overrides for unused tools: ${invalidOverrides.join(", ")}`)
}
@@ -94,6 +94,6 @@ export const xsComponentOverrides: PromptVariant["componentOverrides"] = {
enabled: true, // Use default user instructions
},
[SystemPromptSection.FEEDBACK]: {
enabled: true, // Use default feedback section
enabled: false,
},
}
+81 -618
View File
@@ -1,13 +1,17 @@
import { ApiConfiguration, ModelInfo } from "@shared/api"
import {
ApiHandlerSettingsKeys,
GlobalState,
GlobalStateAndSettings,
GlobalStateAndSettingsKey,
GlobalStateKey,
isSecretKey,
isSettingsKey,
LocalState,
LocalStateKey,
RemoteConfigFields,
SecretKey,
SecretKeys,
Secrets,
Settings,
SettingsKey,
@@ -15,6 +19,7 @@ import {
import chokidar, { FSWatcher } from "chokidar"
import type { ExtensionContext } from "vscode"
import { HostProvider } from "@/hosts/host-provider"
import { Logger } from "@/services/logging/Logger"
import { ShowMessageType } from "@/shared/proto/index.host"
import {
getTaskHistoryStateFilePath,
@@ -24,6 +29,7 @@ import {
writeTaskSettingsToStorage,
} from "./disk"
import { STATE_MANAGER_NOT_INITIALIZED } from "./error-messages"
import { filterAllowedRemoteConfigFields } from "./remote-config/utils"
import { readGlobalStateFromDisk, readSecretsFromDisk, readWorkspaceStateFromDisk } from "./utils/state-helpers"
export interface PersistenceErrorEvent {
error: Error
@@ -180,6 +186,18 @@ export class StateManager {
this.scheduleDebouncedPersistence()
}
private setRemoteConfigState(updates: Partial<GlobalStateAndSettings>): void {
if (!this.isInitialized) {
throw new Error(STATE_MANAGER_NOT_INITIALIZED)
}
// Update cache in one go
this.remoteConfigCache = {
...this.remoteConfigCache,
...filterAllowedRemoteConfigFields(updates),
}
}
/**
* Set method for task settings keys - updates cache immediately and schedules debounced persistence
*/
@@ -477,353 +495,43 @@ export class StateManager {
/**
* Convenience method for setting API configuration
* Automatically categorizes keys based on STATE_DEFINITION and SecretKeys
*/
setApiConfiguration(apiConfiguration: ApiConfiguration): void {
if (!this.isInitialized) {
throw new Error(STATE_MANAGER_NOT_INITIALIZED)
}
const {
apiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
awsUseGlobalInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsBedrockApiKey,
awsProfile,
awsUseProfile,
awsAuthentication,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiHeaders,
ollamaBaseUrl,
ollamaApiKey,
ollamaApiOptionsCtxNum,
lmStudioBaseUrl,
lmStudioMaxTokens,
anthropicBaseUrl,
geminiApiKey,
geminiBaseUrl,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyBaseUrl,
togetherApiKey,
qwenApiKey,
doubaoApiKey,
mistralApiKey,
azureApiVersion,
azureIdentity,
openRouterProviderSorting,
liteLlmBaseUrl,
liteLlmApiKey,
liteLlmUsePromptCache,
qwenApiLine,
moonshotApiLine,
zaiApiLine,
asksageApiKey,
asksageApiUrl,
xaiApiKey,
clineAccountId,
sambanovaApiKey,
cerebrasApiKey,
groqApiKey,
moonshotApiKey,
nebiusApiKey,
fireworksApiKey,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
sapAiCoreClientId,
sapAiCoreClientSecret,
sapAiCoreBaseUrl,
sapAiCoreTokenUrl,
sapAiResourceGroup,
sapAiCoreUseOrchestrationMode,
claudeCodePath,
qwenCodeOauthPath,
basetenApiKey,
huggingFaceApiKey,
huaweiCloudMaasApiKey,
difyApiKey,
difyBaseUrl,
vercelAiGatewayApiKey,
zaiApiKey,
minimaxApiKey,
minimaxApiLine,
nousResearchApiKey,
requestTimeoutMs,
ocaBaseUrl,
ocaMode,
hicapApiKey,
hicapModelId,
aihubmixApiKey,
aihubmixBaseUrl,
aihubmixAppCode,
// Plan mode configurations
planModeApiProvider,
planModeApiModelId,
planModeThinkingBudgetTokens,
planModeReasoningEffort,
planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId,
planModeOpenRouterModelId,
planModeOpenRouterModelInfo,
planModeOpenAiModelId,
planModeOpenAiModelInfo,
planModeOllamaModelId,
planModeLmStudioModelId,
planModeLiteLlmModelId,
planModeLiteLlmModelInfo,
planModeRequestyModelId,
planModeRequestyModelInfo,
planModeTogetherModelId,
planModeFireworksModelId,
planModeSapAiCoreModelId,
planModeSapAiCoreDeploymentId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeBasetenModelId,
planModeBasetenModelInfo,
planModeHuggingFaceModelId,
planModeHuggingFaceModelInfo,
planModeHuaweiCloudMaasModelId,
planModeHuaweiCloudMaasModelInfo,
planModeOcaModelId,
planModeOcaModelInfo,
planModeOcaReasoningEffort,
planModeHicapModelId,
planModeHicapModelInfo,
planModeAihubmixModelId,
planModeAihubmixModelInfo,
planModeNousResearchModelId,
planModeVercelAiGatewayModelId,
planModeVercelAiGatewayModelInfo,
geminiPlanModeThinkingLevel,
// Act mode configurations
actModeApiProvider,
actModeApiModelId,
actModeThinkingBudgetTokens,
actModeReasoningEffort,
actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId,
actModeOpenRouterModelId,
actModeOpenRouterModelInfo,
actModeOpenAiModelId,
actModeOpenAiModelInfo,
actModeOllamaModelId,
actModeLmStudioModelId,
actModeLiteLlmModelId,
actModeLiteLlmModelInfo,
actModeRequestyModelId,
actModeRequestyModelInfo,
actModeTogetherModelId,
actModeFireworksModelId,
actModeSapAiCoreModelId,
actModeSapAiCoreDeploymentId,
actModeGroqModelId,
actModeGroqModelInfo,
actModeBasetenModelId,
actModeBasetenModelInfo,
actModeHuggingFaceModelId,
actModeHuggingFaceModelInfo,
actModeHuaweiCloudMaasModelId,
actModeHuaweiCloudMaasModelInfo,
actModeOcaModelId,
actModeOcaModelInfo,
actModeOcaReasoningEffort,
actModeHicapModelId,
actModeHicapModelInfo,
actModeAihubmixModelId,
actModeAihubmixModelInfo,
actModeNousResearchModelId,
actModeVercelAiGatewayModelId,
actModeVercelAiGatewayModelInfo,
geminiActModeThinkingLevel,
} = apiConfiguration
// Automatically categorize the API configuration keys
const { settingsUpdates, secretsUpdates } = Object.entries(apiConfiguration).reduce(
(acc, [key, value]) => {
if (key === undefined || value === undefined) {
return acc // Skip undefined values
}
// Batch update global state keys
this.setGlobalStateBatch({
// Plan mode configuration updates
planModeApiProvider,
planModeApiModelId,
planModeThinkingBudgetTokens,
planModeReasoningEffort,
planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId,
planModeOpenRouterModelId,
planModeOpenRouterModelInfo,
planModeOpenAiModelId,
planModeOpenAiModelInfo,
planModeOllamaModelId,
planModeLmStudioModelId,
planModeLiteLlmModelId,
planModeLiteLlmModelInfo,
planModeRequestyModelId,
planModeRequestyModelInfo,
planModeTogetherModelId,
planModeFireworksModelId,
planModeSapAiCoreModelId,
planModeSapAiCoreDeploymentId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeBasetenModelId,
planModeBasetenModelInfo,
planModeHuggingFaceModelId,
planModeHuggingFaceModelInfo,
planModeHuaweiCloudMaasModelId,
planModeHuaweiCloudMaasModelInfo,
planModeOcaModelId,
planModeOcaModelInfo,
planModeOcaReasoningEffort,
planModeHicapModelId,
planModeHicapModelInfo,
planModeAihubmixModelId,
planModeAihubmixModelInfo,
planModeNousResearchModelId,
planModeVercelAiGatewayModelId,
planModeVercelAiGatewayModelInfo,
geminiPlanModeThinkingLevel,
if (isSecretKey(key)) {
// This is a secret key
acc.secretsUpdates[key as keyof Secrets] = value as any
} else if (isSettingsKey(key)) {
// This is a settings key
acc.settingsUpdates[key as keyof Settings] = value as any
}
// Act mode configuration updates
actModeApiProvider,
actModeApiModelId,
actModeThinkingBudgetTokens,
actModeReasoningEffort,
actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId,
actModeOpenRouterModelId,
actModeOpenRouterModelInfo,
actModeOpenAiModelId,
actModeOpenAiModelInfo,
actModeOllamaModelId,
actModeLmStudioModelId,
actModeLiteLlmModelId,
actModeLiteLlmModelInfo,
actModeRequestyModelId,
actModeRequestyModelInfo,
actModeTogetherModelId,
actModeFireworksModelId,
actModeSapAiCoreModelId,
actModeSapAiCoreDeploymentId,
actModeGroqModelId,
actModeGroqModelInfo,
actModeBasetenModelId,
actModeBasetenModelInfo,
actModeHuggingFaceModelId,
actModeHuggingFaceModelInfo,
actModeHuaweiCloudMaasModelId,
actModeHuaweiCloudMaasModelInfo,
actModeOcaModelId,
actModeOcaModelInfo,
actModeOcaReasoningEffort,
actModeHicapModelId,
actModeHicapModelInfo,
actModeAihubmixModelId,
actModeAihubmixModelInfo,
actModeNousResearchModelId,
actModeVercelAiGatewayModelId,
actModeVercelAiGatewayModelInfo,
geminiActModeThinkingLevel,
return acc
},
{ settingsUpdates: {} as Partial<Settings>, secretsUpdates: {} as Partial<Secrets> },
)
// Global state updates
awsRegion,
awsUseCrossRegionInference,
awsUseGlobalInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
awsAuthentication,
vertexProjectId,
vertexRegion,
requestyBaseUrl,
openAiBaseUrl,
openAiHeaders,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioBaseUrl,
lmStudioMaxTokens,
anthropicBaseUrl,
geminiBaseUrl,
azureApiVersion,
azureIdentity,
openRouterProviderSorting,
liteLlmBaseUrl,
liteLlmUsePromptCache,
qwenApiLine,
moonshotApiLine,
zaiApiLine,
asksageApiUrl,
requestTimeoutMs,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
sapAiCoreBaseUrl,
sapAiCoreTokenUrl,
sapAiResourceGroup,
sapAiCoreUseOrchestrationMode,
claudeCodePath,
difyBaseUrl,
qwenCodeOauthPath,
ocaBaseUrl,
minimaxApiLine,
ocaMode,
hicapModelId,
aihubmixBaseUrl,
aihubmixAppCode,
})
// Batch update settings (stored in global state)
if (Object.keys(settingsUpdates).length > 0) {
this.setRemoteConfigState(settingsUpdates)
this.setGlobalStateBatch(settingsUpdates)
}
// Batch update secrets
this.setSecretsBatch({
apiKey,
openRouterApiKey,
clineAccountId,
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsBedrockApiKey,
openAiApiKey,
ollamaApiKey,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
togetherApiKey,
qwenApiKey,
doubaoApiKey,
mistralApiKey,
liteLlmApiKey,
fireworksApiKey,
asksageApiKey,
xaiApiKey,
sambanovaApiKey,
cerebrasApiKey,
groqApiKey,
moonshotApiKey,
nebiusApiKey,
sapAiCoreClientId,
sapAiCoreClientSecret,
basetenApiKey,
huggingFaceApiKey,
huaweiCloudMaasApiKey,
difyApiKey,
vercelAiGatewayApiKey,
zaiApiKey,
minimaxApiKey,
hicapApiKey,
aihubmixApiKey,
nousResearchApiKey,
})
if (Object.keys(secretsUpdates).length > 0) {
this.setSecretsBatch(secretsUpdates)
}
}
/**
@@ -984,7 +692,7 @@ export class StateManager {
await this.persistPendingState()
this.persistenceTimeout = null
} catch (error) {
console.error("[StateManager] Failed to persist pending changes:", error)
Logger.error("[StateManager] Failed to persist pending changes:", error)
this.persistenceTimeout = null
// Call persistence error callback for error recovery
@@ -1091,292 +799,47 @@ export class StateManager {
Object.assign(this.workspaceStateCache, workspaceState)
}
/**
* Helper to get a setting value with override support
* Precedence: remote config > task settings > global settings
*/
private getSettingWithOverride<K extends keyof Settings>(key: K): Settings[K] {
const remoteValue = this.remoteConfigCache[key]
if (remoteValue !== undefined) {
return remoteValue
}
const taskValue = this.taskStateCache[key]
if (taskValue !== undefined) {
return taskValue
}
return this.globalStateCache[key]
}
/**
* Helper to get a secret value
*/
private getSecret<K extends keyof Secrets>(key: K): Secrets[K] {
return this.secretsCache[key]
}
/**
* Construct API configuration from cached component keys
*/
private constructApiConfigurationFromCache(): ApiConfiguration {
return {
// Secrets
apiKey: this.secretsCache["apiKey"],
openRouterApiKey: this.secretsCache["openRouterApiKey"],
clineAccountId: this.secretsCache["clineAccountId"],
awsAccessKey: this.secretsCache["awsAccessKey"],
awsSecretKey: this.secretsCache["awsSecretKey"],
awsSessionToken: this.secretsCache["awsSessionToken"],
awsBedrockApiKey: this.secretsCache["awsBedrockApiKey"],
openAiApiKey: this.secretsCache["openAiApiKey"],
ollamaApiKey: this.secretsCache["ollamaApiKey"],
geminiApiKey: this.secretsCache["geminiApiKey"],
openAiNativeApiKey: this.secretsCache["openAiNativeApiKey"],
deepSeekApiKey: this.secretsCache["deepSeekApiKey"],
requestyApiKey: this.secretsCache["requestyApiKey"],
togetherApiKey: this.secretsCache["togetherApiKey"],
qwenApiKey: this.secretsCache["qwenApiKey"],
doubaoApiKey: this.secretsCache["doubaoApiKey"],
mistralApiKey: this.secretsCache["mistralApiKey"],
liteLlmApiKey: this.secretsCache["remoteLiteLlmApiKey"] || this.secretsCache["liteLlmApiKey"],
fireworksApiKey: this.secretsCache["fireworksApiKey"],
asksageApiKey: this.secretsCache["asksageApiKey"],
xaiApiKey: this.secretsCache["xaiApiKey"],
sambanovaApiKey: this.secretsCache["sambanovaApiKey"],
cerebrasApiKey: this.secretsCache["cerebrasApiKey"],
groqApiKey: this.secretsCache["groqApiKey"],
basetenApiKey: this.secretsCache["basetenApiKey"],
moonshotApiKey: this.secretsCache["moonshotApiKey"],
nebiusApiKey: this.secretsCache["nebiusApiKey"],
sapAiCoreClientId: this.secretsCache["sapAiCoreClientId"],
sapAiCoreClientSecret: this.secretsCache["sapAiCoreClientSecret"],
huggingFaceApiKey: this.secretsCache["huggingFaceApiKey"],
huaweiCloudMaasApiKey: this.secretsCache["huaweiCloudMaasApiKey"],
difyApiKey: this.secretsCache["difyApiKey"],
vercelAiGatewayApiKey: this.secretsCache["vercelAiGatewayApiKey"],
zaiApiKey: this.secretsCache["zaiApiKey"],
minimaxApiKey: this.secretsCache["minimaxApiKey"],
hicapApiKey: this.secretsCache["hicapApiKey"],
aihubmixApiKey: this.secretsCache["aihubmixApiKey"],
// Build secrets object
const secrets = Object.fromEntries(SecretKeys.map((key) => [key, this.getSecret(key)])) as Secrets
// Global state (with remote config precedence for applicable fields)
awsRegion:
this.remoteConfigCache["awsRegion"] || this.taskStateCache["awsRegion"] || this.globalStateCache["awsRegion"],
awsUseCrossRegionInference:
this.remoteConfigCache["awsUseCrossRegionInference"] ||
this.taskStateCache["awsUseCrossRegionInference"] ||
this.globalStateCache["awsUseCrossRegionInference"],
awsUseGlobalInference:
this.remoteConfigCache["awsUseGlobalInference"] ||
this.taskStateCache["awsUseGlobalInference"] ||
this.globalStateCache["awsUseGlobalInference"],
awsBedrockUsePromptCache:
this.remoteConfigCache["awsBedrockUsePromptCache"] ||
this.taskStateCache["awsBedrockUsePromptCache"] ||
this.globalStateCache["awsBedrockUsePromptCache"],
awsBedrockEndpoint:
this.remoteConfigCache["awsBedrockEndpoint"] ||
this.taskStateCache["awsBedrockEndpoint"] ||
this.globalStateCache["awsBedrockEndpoint"],
awsProfile: this.taskStateCache["awsProfile"] || this.globalStateCache["awsProfile"],
awsUseProfile: this.taskStateCache["awsUseProfile"] || this.globalStateCache["awsUseProfile"],
awsAuthentication: this.taskStateCache["awsAuthentication"] || this.globalStateCache["awsAuthentication"],
vertexProjectId:
this.remoteConfigCache["vertexProjectId"] ||
this.taskStateCache["vertexProjectId"] ||
this.globalStateCache["vertexProjectId"],
vertexRegion:
this.remoteConfigCache["vertexRegion"] ||
this.taskStateCache["vertexRegion"] ||
this.globalStateCache["vertexRegion"],
requestyBaseUrl: this.taskStateCache["requestyBaseUrl"] || this.globalStateCache["requestyBaseUrl"],
openAiBaseUrl:
this.remoteConfigCache["openAiBaseUrl"] ||
this.taskStateCache["openAiBaseUrl"] ||
this.globalStateCache["openAiBaseUrl"],
openAiHeaders:
this.remoteConfigCache["openAiHeaders"] ||
this.taskStateCache["openAiHeaders"] ||
this.globalStateCache["openAiHeaders"] ||
{},
ollamaBaseUrl: this.taskStateCache["ollamaBaseUrl"] || this.globalStateCache["ollamaBaseUrl"],
ollamaApiOptionsCtxNum:
this.taskStateCache["ollamaApiOptionsCtxNum"] || this.globalStateCache["ollamaApiOptionsCtxNum"],
lmStudioBaseUrl: this.taskStateCache["lmStudioBaseUrl"] || this.globalStateCache["lmStudioBaseUrl"],
lmStudioMaxTokens: this.taskStateCache["lmStudioMaxTokens"] || this.globalStateCache["lmStudioMaxTokens"],
anthropicBaseUrl: this.taskStateCache["anthropicBaseUrl"] || this.globalStateCache["anthropicBaseUrl"],
geminiBaseUrl: this.taskStateCache["geminiBaseUrl"] || this.globalStateCache["geminiBaseUrl"],
azureApiVersion:
this.remoteConfigCache["azureApiVersion"] ||
this.taskStateCache["azureApiVersion"] ||
this.globalStateCache["azureApiVersion"],
azureIdentity:
this.remoteConfigCache["azureIdentity"] ||
this.taskStateCache["azureIdentity"] ||
this.globalStateCache["azureIdentity"],
openRouterProviderSorting:
this.taskStateCache["openRouterProviderSorting"] || this.globalStateCache["openRouterProviderSorting"],
liteLlmBaseUrl:
this.remoteConfigCache["liteLlmBaseUrl"] ||
this.taskStateCache["liteLlmBaseUrl"] ||
this.globalStateCache["liteLlmBaseUrl"],
liteLlmUsePromptCache: this.taskStateCache["liteLlmUsePromptCache"] || this.globalStateCache["liteLlmUsePromptCache"],
qwenApiLine: this.taskStateCache["qwenApiLine"] || this.globalStateCache["qwenApiLine"],
moonshotApiLine: this.taskStateCache["moonshotApiLine"] || this.globalStateCache["moonshotApiLine"],
zaiApiLine: this.taskStateCache["zaiApiLine"] || this.globalStateCache["zaiApiLine"],
asksageApiUrl: this.taskStateCache["asksageApiUrl"] || this.globalStateCache["asksageApiUrl"],
requestTimeoutMs: this.taskStateCache["requestTimeoutMs"] || this.globalStateCache["requestTimeoutMs"],
fireworksModelMaxCompletionTokens:
this.taskStateCache["fireworksModelMaxCompletionTokens"] ||
this.globalStateCache["fireworksModelMaxCompletionTokens"],
fireworksModelMaxTokens:
this.taskStateCache["fireworksModelMaxTokens"] || this.globalStateCache["fireworksModelMaxTokens"],
sapAiCoreBaseUrl: this.taskStateCache["sapAiCoreBaseUrl"] || this.globalStateCache["sapAiCoreBaseUrl"],
sapAiCoreTokenUrl: this.taskStateCache["sapAiCoreTokenUrl"] || this.globalStateCache["sapAiCoreTokenUrl"],
sapAiResourceGroup: this.taskStateCache["sapAiResourceGroup"] || this.globalStateCache["sapAiResourceGroup"],
sapAiCoreUseOrchestrationMode:
this.taskStateCache["sapAiCoreUseOrchestrationMode"] || this.globalStateCache["sapAiCoreUseOrchestrationMode"],
claudeCodePath: this.taskStateCache["claudeCodePath"] || this.globalStateCache["claudeCodePath"],
qwenCodeOauthPath: this.taskStateCache["qwenCodeOauthPath"] || this.globalStateCache["qwenCodeOauthPath"],
difyBaseUrl: this.taskStateCache["difyBaseUrl"] || this.globalStateCache["difyBaseUrl"],
ocaBaseUrl: this.globalStateCache["ocaBaseUrl"],
minimaxApiLine: this.taskStateCache["minimaxApiLine"] || this.globalStateCache["minimaxApiLine"],
ocaMode: this.globalStateCache["ocaMode"],
hicapModelId: this.globalStateCache["hicapModelId"],
aihubmixBaseUrl: this.taskStateCache["aihubmixBaseUrl"] || this.globalStateCache["aihubmixBaseUrl"],
aihubmixAppCode: this.taskStateCache["aihubmixAppCode"] || this.globalStateCache["aihubmixAppCode"],
// Plan mode configurations
planModeApiProvider:
this.remoteConfigCache["planModeApiProvider"] ||
this.taskStateCache["planModeApiProvider"] ||
this.globalStateCache["planModeApiProvider"],
planModeApiModelId: this.taskStateCache["planModeApiModelId"] || this.globalStateCache["planModeApiModelId"],
planModeThinkingBudgetTokens:
this.taskStateCache["planModeThinkingBudgetTokens"] || this.globalStateCache["planModeThinkingBudgetTokens"],
planModeReasoningEffort:
this.taskStateCache["planModeReasoningEffort"] || this.globalStateCache["planModeReasoningEffort"],
planModeVsCodeLmModelSelector:
this.taskStateCache["planModeVsCodeLmModelSelector"] || this.globalStateCache["planModeVsCodeLmModelSelector"],
planModeAwsBedrockCustomSelected:
this.taskStateCache["planModeAwsBedrockCustomSelected"] ||
this.globalStateCache["planModeAwsBedrockCustomSelected"],
planModeAwsBedrockCustomModelBaseId:
this.taskStateCache["planModeAwsBedrockCustomModelBaseId"] ||
this.globalStateCache["planModeAwsBedrockCustomModelBaseId"],
planModeOpenRouterModelId:
this.taskStateCache["planModeOpenRouterModelId"] || this.globalStateCache["planModeOpenRouterModelId"],
planModeOpenRouterModelInfo:
this.taskStateCache["planModeOpenRouterModelInfo"] || this.globalStateCache["planModeOpenRouterModelInfo"],
planModeOpenAiModelId: this.taskStateCache["planModeOpenAiModelId"] || this.globalStateCache["planModeOpenAiModelId"],
planModeOpenAiModelInfo:
this.taskStateCache["planModeOpenAiModelInfo"] || this.globalStateCache["planModeOpenAiModelInfo"],
planModeOllamaModelId: this.taskStateCache["planModeOllamaModelId"] || this.globalStateCache["planModeOllamaModelId"],
planModeLmStudioModelId:
this.taskStateCache["planModeLmStudioModelId"] || this.globalStateCache["planModeLmStudioModelId"],
planModeLiteLlmModelId:
this.taskStateCache["planModeLiteLlmModelId"] || this.globalStateCache["planModeLiteLlmModelId"],
planModeLiteLlmModelInfo:
this.taskStateCache["planModeLiteLlmModelInfo"] || this.globalStateCache["planModeLiteLlmModelInfo"],
planModeRequestyModelId:
this.taskStateCache["planModeRequestyModelId"] || this.globalStateCache["planModeRequestyModelId"],
planModeRequestyModelInfo:
this.taskStateCache["planModeRequestyModelInfo"] || this.globalStateCache["planModeRequestyModelInfo"],
planModeTogetherModelId:
this.taskStateCache["planModeTogetherModelId"] || this.globalStateCache["planModeTogetherModelId"],
planModeFireworksModelId:
this.taskStateCache["planModeFireworksModelId"] || this.globalStateCache["planModeFireworksModelId"],
planModeSapAiCoreModelId:
this.taskStateCache["planModeSapAiCoreModelId"] || this.globalStateCache["planModeSapAiCoreModelId"],
planModeSapAiCoreDeploymentId:
this.taskStateCache["planModeSapAiCoreDeploymentId"] || this.globalStateCache["planModeSapAiCoreDeploymentId"],
planModeGroqModelId: this.taskStateCache["planModeGroqModelId"] || this.globalStateCache["planModeGroqModelId"],
planModeGroqModelInfo: this.taskStateCache["planModeGroqModelInfo"] || this.globalStateCache["planModeGroqModelInfo"],
planModeBasetenModelId:
this.taskStateCache["planModeBasetenModelId"] || this.globalStateCache["planModeBasetenModelId"],
planModeBasetenModelInfo:
this.taskStateCache["planModeBasetenModelInfo"] || this.globalStateCache["planModeBasetenModelInfo"],
planModeHuggingFaceModelId:
this.taskStateCache["planModeHuggingFaceModelId"] || this.globalStateCache["planModeHuggingFaceModelId"],
planModeHuggingFaceModelInfo:
this.taskStateCache["planModeHuggingFaceModelInfo"] || this.globalStateCache["planModeHuggingFaceModelInfo"],
planModeHuaweiCloudMaasModelId:
this.taskStateCache["planModeHuaweiCloudMaasModelId"] || this.globalStateCache["planModeHuaweiCloudMaasModelId"],
planModeHuaweiCloudMaasModelInfo:
this.taskStateCache["planModeHuaweiCloudMaasModelInfo"] ||
this.globalStateCache["planModeHuaweiCloudMaasModelInfo"],
planModeOcaModelId: this.globalStateCache["planModeOcaModelId"],
planModeOcaModelInfo: this.globalStateCache["planModeOcaModelInfo"],
planModeOcaReasoningEffort: this.globalStateCache["planModeOcaReasoningEffort"],
planModeHicapModelId: this.taskStateCache["planModeHicapModelId"] || this.globalStateCache["planModeHicapModelId"],
planModeHicapModelInfo:
this.taskStateCache["planModeHicapModelInfo"] || this.globalStateCache["planModeHicapModelInfo"],
planModeAihubmixModelId:
this.taskStateCache["planModeAihubmixModelId"] || this.globalStateCache["planModeAihubmixModelId"],
planModeAihubmixModelInfo:
this.taskStateCache["planModeAihubmixModelInfo"] || this.globalStateCache["planModeAihubmixModelInfo"],
planModeNousResearchModelId:
this.taskStateCache["planModeNousResearchModelId"] || this.globalStateCache["planModeNousResearchModelId"],
planModeVercelAiGatewayModelId:
this.taskStateCache["planModeVercelAiGatewayModelId"] || this.globalStateCache["planModeVercelAiGatewayModelId"],
planModeVercelAiGatewayModelInfo:
this.taskStateCache["planModeVercelAiGatewayModelInfo"] ||
this.globalStateCache["planModeVercelAiGatewayModelInfo"],
geminiPlanModeThinkingLevel:
this.taskStateCache["geminiPlanModeThinkingLevel"] || this.globalStateCache["geminiPlanModeThinkingLevel"],
// Act mode configurations
actModeApiProvider:
this.remoteConfigCache["actModeApiProvider"] ||
this.taskStateCache["actModeApiProvider"] ||
this.globalStateCache["actModeApiProvider"],
actModeApiModelId: this.taskStateCache["actModeApiModelId"] || this.globalStateCache["actModeApiModelId"],
actModeThinkingBudgetTokens:
this.taskStateCache["actModeThinkingBudgetTokens"] || this.globalStateCache["actModeThinkingBudgetTokens"],
actModeReasoningEffort:
this.taskStateCache["actModeReasoningEffort"] || this.globalStateCache["actModeReasoningEffort"],
actModeVsCodeLmModelSelector:
this.taskStateCache["actModeVsCodeLmModelSelector"] || this.globalStateCache["actModeVsCodeLmModelSelector"],
actModeAwsBedrockCustomSelected:
this.taskStateCache["actModeAwsBedrockCustomSelected"] ||
this.globalStateCache["actModeAwsBedrockCustomSelected"],
actModeAwsBedrockCustomModelBaseId:
this.taskStateCache["actModeAwsBedrockCustomModelBaseId"] ||
this.globalStateCache["actModeAwsBedrockCustomModelBaseId"],
actModeOpenRouterModelId:
this.taskStateCache["actModeOpenRouterModelId"] || this.globalStateCache["actModeOpenRouterModelId"],
actModeOpenRouterModelInfo:
this.taskStateCache["actModeOpenRouterModelInfo"] || this.globalStateCache["actModeOpenRouterModelInfo"],
actModeOpenAiModelId: this.taskStateCache["actModeOpenAiModelId"] || this.globalStateCache["actModeOpenAiModelId"],
actModeOpenAiModelInfo:
this.taskStateCache["actModeOpenAiModelInfo"] || this.globalStateCache["actModeOpenAiModelInfo"],
actModeOllamaModelId: this.taskStateCache["actModeOllamaModelId"] || this.globalStateCache["actModeOllamaModelId"],
actModeLmStudioModelId:
this.taskStateCache["actModeLmStudioModelId"] || this.globalStateCache["actModeLmStudioModelId"],
actModeLiteLlmModelId: this.taskStateCache["actModeLiteLlmModelId"] || this.globalStateCache["actModeLiteLlmModelId"],
actModeLiteLlmModelInfo:
this.taskStateCache["actModeLiteLlmModelInfo"] || this.globalStateCache["actModeLiteLlmModelInfo"],
actModeRequestyModelId:
this.taskStateCache["actModeRequestyModelId"] || this.globalStateCache["actModeRequestyModelId"],
actModeRequestyModelInfo:
this.taskStateCache["actModeRequestyModelInfo"] || this.globalStateCache["actModeRequestyModelInfo"],
actModeTogetherModelId:
this.taskStateCache["actModeTogetherModelId"] || this.globalStateCache["actModeTogetherModelId"],
actModeFireworksModelId:
this.taskStateCache["actModeFireworksModelId"] || this.globalStateCache["actModeFireworksModelId"],
actModeSapAiCoreModelId:
this.taskStateCache["actModeSapAiCoreModelId"] || this.globalStateCache["actModeSapAiCoreModelId"],
actModeSapAiCoreDeploymentId:
this.taskStateCache["actModeSapAiCoreDeploymentId"] || this.globalStateCache["actModeSapAiCoreDeploymentId"],
actModeGroqModelId: this.taskStateCache["actModeGroqModelId"] || this.globalStateCache["actModeGroqModelId"],
actModeGroqModelInfo: this.taskStateCache["actModeGroqModelInfo"] || this.globalStateCache["actModeGroqModelInfo"],
actModeBasetenModelId: this.taskStateCache["actModeBasetenModelId"] || this.globalStateCache["actModeBasetenModelId"],
actModeBasetenModelInfo:
this.taskStateCache["actModeBasetenModelInfo"] || this.globalStateCache["actModeBasetenModelInfo"],
actModeHuggingFaceModelId:
this.taskStateCache["actModeHuggingFaceModelId"] || this.globalStateCache["actModeHuggingFaceModelId"],
actModeHuggingFaceModelInfo:
this.taskStateCache["actModeHuggingFaceModelInfo"] || this.globalStateCache["actModeHuggingFaceModelInfo"],
actModeHuaweiCloudMaasModelId:
this.taskStateCache["actModeHuaweiCloudMaasModelId"] || this.globalStateCache["actModeHuaweiCloudMaasModelId"],
actModeHuaweiCloudMaasModelInfo:
this.taskStateCache["actModeHuaweiCloudMaasModelInfo"] ||
this.globalStateCache["actModeHuaweiCloudMaasModelInfo"],
actModeOcaModelId: this.globalStateCache["actModeOcaModelId"],
actModeOcaModelInfo: this.globalStateCache["actModeOcaModelInfo"],
actModeOcaReasoningEffort: this.globalStateCache["actModeOcaReasoningEffort"],
actModeHicapModelId: this.globalStateCache["actModeHicapModelId"],
actModeHicapModelInfo: this.globalStateCache["actModeHicapModelInfo"],
actModeAihubmixModelId:
this.taskStateCache["actModeAihubmixModelId"] || this.globalStateCache["actModeAihubmixModelId"],
actModeAihubmixModelInfo:
this.taskStateCache["actModeAihubmixModelInfo"] || this.globalStateCache["actModeAihubmixModelInfo"],
actModeNousResearchModelId:
this.taskStateCache["actModeNousResearchModelId"] || this.globalStateCache["actModeNousResearchModelId"],
actModeVercelAiGatewayModelId:
this.taskStateCache["actModeVercelAiGatewayModelId"] || this.globalStateCache["actModeVercelAiGatewayModelId"],
actModeVercelAiGatewayModelInfo:
this.taskStateCache["actModeVercelAiGatewayModelInfo"] ||
this.globalStateCache["actModeVercelAiGatewayModelInfo"],
geminiActModeThinkingLevel:
this.taskStateCache["geminiActModeThinkingLevel"] || this.globalStateCache["geminiActModeThinkingLevel"],
nousResearchApiKey: this.secretsCache["nousResearchApiKey"],
// Preserve legacy fallback behavior for LiteLLM API key:
// if a remoteLiteLlmApiKey is set (via remote config), it should
// take precedence over the local liteLlmApiKey.
const remoteLiteLlmApiKey = this.secretsCache["remoteLiteLlmApiKey"]
if (remoteLiteLlmApiKey !== undefined && remoteLiteLlmApiKey !== null && remoteLiteLlmApiKey !== "") {
secrets.liteLlmApiKey = remoteLiteLlmApiKey
}
// Build API handler settings object with task override support
const settings = Object.fromEntries(ApiHandlerSettingsKeys.map((key) => [key, this.getSettingWithOverride(key)]))
return { ...secrets, ...settings } satisfies ApiConfiguration
}
}
+14
View File
@@ -180,6 +180,20 @@ export async function ensureSettingsDirectoryExists(): Promise<string> {
return getGlobalStorageDir("settings")
}
/**
* Gets the path to the MCP settings file, creating it if it doesn't exist
* @param settingsDirectoryPath Path to the settings directory
* @returns Path to the MCP settings file
*/
export async function getMcpSettingsFilePath(settingsDirectoryPath: string): Promise<string> {
const mcpSettingsFilePath = path.join(settingsDirectoryPath, GlobalFileNames.mcpSettings)
const fileExists = await fileExistsAtPath(mcpSettingsFilePath)
if (!fileExists) {
await fs.writeFile(mcpSettingsFilePath, JSON.stringify({ mcpServers: {} }, null, 2))
}
return mcpSettingsFilePath
}
export async function getSavedApiConversationHistory(taskId: string): Promise<Anthropic.MessageParam[]> {
const filePath = path.join(await ensureTaskDirectoryExists(taskId), GlobalFileNames.apiConversationHistory)
const fileExists = await fileExistsAtPath(filePath)
+13 -4
View File
@@ -1,13 +1,13 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios"
import { Controller } from "@/core/controller"
import { buildBasicClineHeaders } from "@/services/EnvUtils"
import { getAxiosSettings } from "@/shared/net"
import { ClineEnv } from "../../../config"
import { AuthService } from "../../../services/auth/AuthService"
import { CLINE_API_ENDPOINT } from "../../../shared/cline/api"
import { APIKeySchema, type APIKeySettings, RemoteConfig, RemoteConfigSchema } from "../../../shared/remote-config/schema"
import { deleteRemoteConfigFromCache, readRemoteConfigFromCache, writeRemoteConfigToCache } from "../disk"
import { StateManager } from "../StateManager"
import { applyRemoteConfig } from "./utils"
import { applyRemoteConfig, clearRemoteConfig, isRemoteConfigEnabled } from "./utils"
/**
* Parses API keys from a JSON string response
@@ -51,6 +51,7 @@ async function makeAuthenticatedRequest<T>(endpoint: string, organizationId: str
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
...(await buildBasicClineHeaders()),
},
...getAxiosSettings(),
}
@@ -170,6 +171,10 @@ async function findOrganizationWithRemoteConfig(): Promise<{ organizationId: str
// Scan each organization for remote config
for (const org of userOrganizations) {
if (!isRemoteConfigEnabled(org.organizationId)) {
continue
}
const remoteConfig = await fetchRemoteConfigForOrganization(org.organizationId)
if (remoteConfig) {
@@ -198,7 +203,7 @@ async function ensureUserInOrgWithRemoteConfig(controller: Controller): Promise<
const result = await findOrganizationWithRemoteConfig()
if (!result) {
StateManager.get().clearRemoteConfig()
clearRemoteConfig()
controller.postStateToWebview()
return undefined
}
@@ -230,7 +235,11 @@ async function ensureUserInOrgWithRemoteConfig(controller: Controller): Promise<
// Cache and apply the remote config
await writeRemoteConfigToCache(organizationId, config)
await applyRemoteConfig(config)
if (isRemoteConfigEnabled(organizationId)) {
await applyRemoteConfig(config, undefined, controller.mcpHub)
} else {
clearRemoteConfig()
}
controller.postStateToWebview()
return config
@@ -0,0 +1,92 @@
import { getMcpSettingsFilePath } from "@core/storage/disk"
import { StateManager } from "@core/storage/StateManager"
import { RemoteMCPServer } from "@shared/remote-config/schema"
import * as fs from "fs/promises"
import type { McpHub } from "@/services/mcp/McpHub"
/**
* Synchronizes remote MCP servers from remote config to the local MCP settings file
* This allows admins to centrally configure MCP servers that are automatically deployed to users
*
* Handles:
* - Removing servers that were previously from remote config but are no longer present
* - Adding new servers from remote config
* - Preventing duplicates when re-adding servers
*
* @param remoteMCPServers Array of remote MCP servers from remote config
* @param settingsDirectoryPath Path to the settings directory
* @param mcpHub Optional McpHub instance to set flag preventing watcher triggers
*/
export async function syncRemoteMcpServersToSettings(
remoteMCPServers: RemoteMCPServer[],
settingsDirectoryPath: string,
mcpHub?: McpHub,
): Promise<void> {
try {
// Get or create the MCP settings file
const settingsPath = await getMcpSettingsFilePath(settingsDirectoryPath)
// Read current settings
const content = await fs.readFile(settingsPath, "utf-8")
const config = JSON.parse(content)
// Ensure mcpServers object exists
if (!config.mcpServers || typeof config.mcpServers !== "object") {
config.mcpServers = {}
}
// Get previous remote servers from cache
const stateManager = StateManager.get()
const previousRemoteServers = (stateManager.getRemoteConfigSettings().previousRemoteMCPServers as RemoteMCPServer[]) || []
// Remove old remote servers that are no longer in the new list
for (const prevServer of previousRemoteServers) {
// Check if this server exists in current settings with same name and URL
const existingServer = config.mcpServers[prevServer.name]
if (existingServer && existingServer.url === prevServer.url) {
// Check if it's still in the new remote config
const stillInRemoteConfig = remoteMCPServers.some(
(newServer) => newServer.name === prevServer.name && newServer.url === prevServer.url,
)
if (!stillInRemoteConfig) {
// Remove it from settings
delete config.mcpServers[prevServer.name]
}
}
}
// Add/update servers from new remote config
for (const server of remoteMCPServers) {
// Check if server with same name and URL already exists to skip duplicates
const existingServer = config.mcpServers[server.name]
if (existingServer && existingServer.url === server.url) {
continue
}
// Add or update the server
config.mcpServers[server.name] = {
url: server.url,
type: "streamableHttp",
disabled: false,
autoApprove: [],
}
}
// Set flag to prevent watcher from triggering
if (mcpHub) {
mcpHub.setIsUpdatingFromRemoteConfig(true)
}
try {
// Write back to file
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
} finally {
// Always clear flag, even if write fails
if (mcpHub) {
mcpHub.setIsUpdatingFromRemoteConfig(false)
}
}
} catch (error) {
console.error("[RemoteConfig] Failed to sync remote MCP servers:", error)
}
}
+109 -12
View File
@@ -1,12 +1,17 @@
import { synchronizeRemoteRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
import { RemoteConfig } from "@shared/remote-config/schema"
import { RemoteConfigFields } from "@shared/storage/state-keys"
import { getTelemetryService } from "@/services/telemetry"
import { GlobalStateAndSettings, RemoteConfigFields } from "@shared/storage/state-keys"
import { AuthService } from "@/services/auth/AuthService"
import { Logger } from "@/services/logging/Logger"
import { getTelemetryService, telemetryService } from "@/services/telemetry"
import { OpenTelemetryClientProvider } from "@/services/telemetry/providers/opentelemetry/OpenTelemetryClientProvider"
import { OpenTelemetryTelemetryProvider } from "@/services/telemetry/providers/opentelemetry/OpenTelemetryTelemetryProvider"
import { type TelemetryService } from "@/services/telemetry/TelemetryService"
import { ApiProvider } from "@/shared/api"
import { isOpenTelemetryConfigValid, remoteConfigToOtelConfig } from "@/shared/services/config/otel-config"
import { ensureSettingsDirectoryExists } from "../disk"
import { StateManager } from "../StateManager"
import { syncRemoteMcpServersToSettings } from "./syncRemoteMcpServers"
/**
* Transforms RemoteConfig schema to RemoteConfigFields shape
@@ -88,7 +93,7 @@ export function transformRemoteConfigToStateShape(remoteConfig: RemoteConfig): P
// Map provider settings
const providers: string[] = []
const providers: ApiProvider[] = []
// Map OpenAiCompatible provider settings
const openAiSettings = remoteConfig.providerSettings?.OpenAiCompatible
@@ -206,24 +211,46 @@ async function applyRemoteOTELConfig(transformed: Partial<RemoteConfigFields>, t
}
}
/**
* Applies remote config to the StateManager's remote config cache
* @param remoteConfig The remote configuration object to apply
*/
export async function applyRemoteConfig(remoteConfig?: RemoteConfig): Promise<void> {
const stateManager = StateManager.get()
const telemetryService = await getTelemetryService()
export function clearRemoteConfig() {
try {
const stateManager = StateManager.get()
// If no remote config provided, clear the cache and relevant state
if (!remoteConfig) {
stateManager.clearRemoteConfig()
telemetryService.removeProvider(REMOTE_CONFIG_OTEL_PROVIDER_ID)
// the remote config cline rules toggle state is stored in global state
stateManager.setGlobalState("remoteRulesToggles", {})
stateManager.setGlobalState("remoteWorkflowToggles", {})
// clear secrets
stateManager.setSecret("remoteLiteLlmApiKey", undefined)
} catch (err) {
Logger.error("[REMOTE CONFIG] Failed to clear remote config", err)
}
}
/**
* Applies remote config to the StateManager's remote config cache
* @param remoteConfig The remote configuration object to apply
* @param settingsDirectoryPath Path to the settings directory
* @param mcpHub Optional McpHub instance to prevent watcher triggers during sync
*/
export async function applyRemoteConfig(
remoteConfig?: RemoteConfig,
settingsDirectoryPath?: string,
mcpHub?: any,
): Promise<void> {
const stateManager = StateManager.get()
const telemetryService = await getTelemetryService()
// If no remote config provided, clear the cache and relevant state
if (!remoteConfig) {
clearRemoteConfig()
return
}
// Save previousRemoteMCPServers before clearing cache, this is needed for next sync to detect removals)
const previousRemoteMCPServers = stateManager.getRemoteConfigSettings().previousRemoteMCPServers
// Transform remote config to state shape
// These are then set to the remote config cache in the StateManager
// We need to ensure the cache is checked for new fields
@@ -248,5 +275,75 @@ export async function applyRemoteConfig(remoteConfig?: RemoteConfig): Promise<vo
stateManager.setRemoteConfigField(key as keyof RemoteConfigFields, value)
}
// Restore previousRemoteMCPServers across cache clears
if (previousRemoteMCPServers !== undefined) {
stateManager.setRemoteConfigField("previousRemoteMCPServers", previousRemoteMCPServers)
}
// Sync remote MCP servers to settings file (AFTER cache is populated, so sync can read previous state)
if (remoteConfig.remoteMCPServers !== undefined) {
try {
// Get settings directory path - use provided path or get it from disk helper
const settingsPath = settingsDirectoryPath || (await ensureSettingsDirectoryExists())
await syncRemoteMcpServersToSettings(remoteConfig.remoteMCPServers, settingsPath, mcpHub)
// Store current remote servers list for next sync to detect removals
stateManager.setRemoteConfigField("previousRemoteMCPServers", remoteConfig.remoteMCPServers)
} catch (error) {
console.error("[RemoteConfig] Failed to sync remote MCP servers to settings:", error)
// Continue with other config application even if MCP sync fails
}
}
await applyRemoteOTELConfig(transformed, telemetryService)
}
const isProviderValid = (provider?: ApiProvider) => {
const remoteConfiguredProviders = StateManager.get().getRemoteConfigSettings().remoteConfiguredProviders
if (!remoteConfiguredProviders || !remoteConfiguredProviders.length) {
return true
}
return provider && remoteConfiguredProviders.includes(provider)
}
/**
* Receives a config and returns the subset of fields that can be overriden in the cache
*/
export function filterAllowedRemoteConfigFields(config: Partial<GlobalStateAndSettings>): Partial<GlobalStateAndSettings> {
const updatedFields: Partial<GlobalStateAndSettings> = {}
const actModeApiProvider = config.actModeApiProvider
if (isProviderValid(actModeApiProvider)) {
updatedFields.actModeApiProvider = actModeApiProvider
}
const planModeApiProvider = config.planModeApiProvider
if (isProviderValid(planModeApiProvider)) {
updatedFields.planModeApiProvider = planModeApiProvider
}
return updatedFields
}
const canDisableRemoteConfig = (orgId: string) => {
// Check if they're an admin/owner
const authService = AuthService.getInstance()
const userOrgs = authService.getUserOrganizations()
if (!userOrgs) {
return false
}
const org = userOrgs.find((org) => org.organizationId === orgId)
const isAdminOrOwner = org?.roles?.some((role) => role === "admin" || role === "owner")
return isAdminOrOwner
}
export const isRemoteConfigEnabled = (orgId: string) => {
const stateManager = StateManager.get()
const hasOptedOut = stateManager.getGlobalSettingsKey("optOutOfRemoteConfig")
const isDisabled = hasOptedOut && canDisableRemoteConfig(orgId)
return !isDisabled
}
+99 -773
View File
@@ -1,759 +1,125 @@
import { ANTHROPIC_MIN_THINKING_BUDGET, ApiProvider, fireworksDefaultModelId, type OcaModelInfo } from "@shared/api"
import { GlobalStateAndSettings, LocalState, SecretKey, Secrets } from "@shared/storage/state-keys"
import { ApiProvider } from "@shared/api"
import {
applyTransform,
GlobalStateAndSettingKeys,
GlobalStateAndSettings,
getDefaultValue,
isAsyncProperty,
isComputedProperty,
LocalState,
LocalStateKeys,
SecretKeys,
Secrets,
} from "@shared/storage/state-keys"
import { ExtensionContext } from "vscode"
import { Controller } from "@/core/controller"
import { getHooksEnabledSafe } from "@/core/hooks/hooks-utils"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@/shared/AutoApprovalSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@/shared/BrowserSettings"
import { ClineRulesToggles } from "@/shared/cline-rules"
import { DEFAULT_DICTATION_SETTINGS, DictationSettings } from "@/shared/DictationSettings"
import { DEFAULT_FOCUS_CHAIN_SETTINGS } from "@/shared/FocusChainSettings"
import { DEFAULT_MCP_DISPLAY_MODE } from "@/shared/McpDisplayMode"
import { OpenaiReasoningEffort } from "@/shared/storage/types"
import { readTaskHistoryFromState } from "../disk"
export async function readSecretsFromDisk(context: ExtensionContext): Promise<Secrets> {
const [
apiKey,
openRouterApiKey,
firebaseClineAccountId,
clineAccountId,
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsBedrockApiKey,
openAiApiKey,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
togetherApiKey,
qwenApiKey,
doubaoApiKey,
mistralApiKey,
fireworksApiKey,
liteLlmApiKey,
remoteLiteLlmApiKey,
asksageApiKey,
xaiApiKey,
sambanovaApiKey,
cerebrasApiKey,
groqApiKey,
moonshotApiKey,
nebiusApiKey,
huggingFaceApiKey,
sapAiCoreClientId,
sapAiCoreClientSecret,
huaweiCloudMaasApiKey,
basetenApiKey,
zaiApiKey,
ollamaApiKey,
vercelAiGatewayApiKey,
difyApiKey,
authNonce,
ocaApiKey,
ocaRefreshToken,
minimaxApiKey,
hicapApiKey,
aihubmixApiKey,
mcpOAuthSecrets,
nousResearchApiKey,
] = await Promise.all([
context.secrets.get("apiKey") as Promise<Secrets["apiKey"]>,
context.secrets.get("openRouterApiKey") as Promise<Secrets["openRouterApiKey"]>,
context.secrets.get("clineAccountId") as Promise<Secrets["clineAccountId"]>,
context.secrets.get("cline:clineAccountId") as Promise<Secrets["cline:clineAccountId"]>,
context.secrets.get("awsAccessKey") as Promise<Secrets["awsAccessKey"]>,
context.secrets.get("awsSecretKey") as Promise<Secrets["awsSecretKey"]>,
context.secrets.get("awsSessionToken") as Promise<Secrets["awsSessionToken"]>,
context.secrets.get("awsBedrockApiKey") as Promise<Secrets["awsBedrockApiKey"]>,
context.secrets.get("openAiApiKey") as Promise<Secrets["openAiApiKey"]>,
context.secrets.get("geminiApiKey") as Promise<Secrets["geminiApiKey"]>,
context.secrets.get("openAiNativeApiKey") as Promise<Secrets["openAiNativeApiKey"]>,
context.secrets.get("deepSeekApiKey") as Promise<Secrets["deepSeekApiKey"]>,
context.secrets.get("requestyApiKey") as Promise<Secrets["requestyApiKey"]>,
context.secrets.get("togetherApiKey") as Promise<Secrets["togetherApiKey"]>,
context.secrets.get("qwenApiKey") as Promise<Secrets["qwenApiKey"]>,
context.secrets.get("doubaoApiKey") as Promise<Secrets["doubaoApiKey"]>,
context.secrets.get("mistralApiKey") as Promise<Secrets["mistralApiKey"]>,
context.secrets.get("fireworksApiKey") as Promise<Secrets["fireworksApiKey"]>,
context.secrets.get("liteLlmApiKey") as Promise<Secrets["liteLlmApiKey"]>,
context.secrets.get("remoteLiteLlmApiKey") as Promise<Secrets["remoteLiteLlmApiKey"]>,
context.secrets.get("asksageApiKey") as Promise<Secrets["asksageApiKey"]>,
context.secrets.get("xaiApiKey") as Promise<Secrets["xaiApiKey"]>,
context.secrets.get("sambanovaApiKey") as Promise<Secrets["sambanovaApiKey"]>,
context.secrets.get("cerebrasApiKey") as Promise<Secrets["cerebrasApiKey"]>,
context.secrets.get("groqApiKey") as Promise<Secrets["groqApiKey"]>,
context.secrets.get("moonshotApiKey") as Promise<Secrets["moonshotApiKey"]>,
context.secrets.get("nebiusApiKey") as Promise<Secrets["nebiusApiKey"]>,
context.secrets.get("huggingFaceApiKey") as Promise<Secrets["huggingFaceApiKey"]>,
context.secrets.get("sapAiCoreClientId") as Promise<Secrets["sapAiCoreClientId"]>,
context.secrets.get("sapAiCoreClientSecret") as Promise<Secrets["sapAiCoreClientSecret"]>,
context.secrets.get("huaweiCloudMaasApiKey") as Promise<Secrets["huaweiCloudMaasApiKey"]>,
context.secrets.get("basetenApiKey") as Promise<Secrets["basetenApiKey"]>,
context.secrets.get("zaiApiKey") as Promise<Secrets["zaiApiKey"]>,
context.secrets.get("ollamaApiKey") as Promise<Secrets["ollamaApiKey"]>,
context.secrets.get("vercelAiGatewayApiKey") as Promise<Secrets["vercelAiGatewayApiKey"]>,
context.secrets.get("difyApiKey") as Promise<Secrets["difyApiKey"]>,
context.secrets.get("authNonce") as Promise<Secrets["authNonce"]>,
context.secrets.get("ocaApiKey") as Promise<string | undefined>,
context.secrets.get("ocaRefreshToken") as Promise<string | undefined>,
context.secrets.get("minimaxApiKey") as Promise<Secrets["minimaxApiKey"]>,
context.secrets.get("hicapApiKey") as Promise<Secrets["hicapApiKey"]>,
context.secrets.get("aihubmixApiKey") as Promise<Secrets["aihubmixApiKey"]>,
context.secrets.get("mcpOAuthSecrets") as Promise<Secrets["mcpOAuthSecrets"]>,
context.secrets.get("nousResearchApiKey") as Promise<Secrets["nousResearchApiKey"]>,
])
return {
authNonce,
apiKey,
openRouterApiKey,
clineAccountId: firebaseClineAccountId,
"cline:clineAccountId": clineAccountId,
huggingFaceApiKey,
huaweiCloudMaasApiKey,
basetenApiKey,
zaiApiKey,
ollamaApiKey,
vercelAiGatewayApiKey,
difyApiKey,
sapAiCoreClientId,
sapAiCoreClientSecret,
xaiApiKey,
sambanovaApiKey,
cerebrasApiKey,
groqApiKey,
moonshotApiKey,
nebiusApiKey,
asksageApiKey,
fireworksApiKey,
liteLlmApiKey,
remoteLiteLlmApiKey,
doubaoApiKey,
mistralApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
togetherApiKey,
qwenApiKey,
geminiApiKey,
openAiApiKey,
awsBedrockApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
ocaApiKey,
ocaRefreshToken,
minimaxApiKey,
hicapApiKey,
aihubmixApiKey,
mcpOAuthSecrets,
nousResearchApiKey,
}
export async function readSecretsFromDisk(context: ExtensionContext): Promise<Secrets> {
const secrets = await Promise.all(SecretKeys.map((key) => context.secrets.get(key)))
return SecretKeys.reduce((acc, key, index) => {
acc[key] = secrets[index]
return acc
}, {} as Secrets)
}
export async function readWorkspaceStateFromDisk(context: ExtensionContext): Promise<LocalState> {
const localClineRulesToggles = context.workspaceState.get("localClineRulesToggles") as ClineRulesToggles | undefined
const localWindsurfRulesToggles = context.workspaceState.get("localWindsurfRulesToggles") as ClineRulesToggles | undefined
const localCursorRulesToggles = context.workspaceState.get("localCursorRulesToggles") as ClineRulesToggles | undefined
const localAgentsRulesToggles = context.workspaceState.get("localAgentsRulesToggles") as ClineRulesToggles | undefined
const localWorkflowToggles = context.workspaceState.get("workflowToggles") as ClineRulesToggles | undefined
const localSkillsToggles = context.workspaceState.get("localSkillsToggles") as ClineRulesToggles | undefined
const states = LocalStateKeys.map((key) => context.workspaceState.get<ClineRulesToggles | undefined>(key))
return {
localClineRulesToggles: localClineRulesToggles || {},
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
localCursorRulesToggles: localCursorRulesToggles || {},
localAgentsRulesToggles: localAgentsRulesToggles || {},
workflowToggles: localWorkflowToggles || {},
localSkillsToggles: localSkillsToggles || {},
}
return LocalStateKeys.reduce((acc, key, index) => {
acc[key] = states[index] || {}
return acc
}, {} as LocalState)
}
export async function readGlobalStateFromDisk(context: ExtensionContext): Promise<GlobalStateAndSettings> {
try {
// Get all global state values
const strictPlanModeEnabled =
context.globalState.get<GlobalStateAndSettings["strictPlanModeEnabled"]>("strictPlanModeEnabled")
const yoloModeToggled = context.globalState.get<GlobalStateAndSettings["yoloModeToggled"]>("yoloModeToggled")
const useAutoCondense = context.globalState.get<GlobalStateAndSettings["useAutoCondense"]>("useAutoCondense")
const clineWebToolsEnabled =
context.globalState.get<GlobalStateAndSettings["clineWebToolsEnabled"]>("clineWebToolsEnabled")
const isNewUser = context.globalState.get<GlobalStateAndSettings["isNewUser"]>("isNewUser")
const welcomeViewCompleted =
context.globalState.get<GlobalStateAndSettings["welcomeViewCompleted"]>("welcomeViewCompleted")
const awsRegion = context.globalState.get<GlobalStateAndSettings["awsRegion"]>("awsRegion")
const awsUseCrossRegionInference =
context.globalState.get<GlobalStateAndSettings["awsUseCrossRegionInference"]>("awsUseCrossRegionInference")
const awsUseGlobalInference =
context.globalState.get<GlobalStateAndSettings["awsUseGlobalInference"]>("awsUseGlobalInference")
const awsBedrockUsePromptCache =
context.globalState.get<GlobalStateAndSettings["awsBedrockUsePromptCache"]>("awsBedrockUsePromptCache")
const awsBedrockEndpoint = context.globalState.get<GlobalStateAndSettings["awsBedrockEndpoint"]>("awsBedrockEndpoint")
const awsProfile = context.globalState.get<GlobalStateAndSettings["awsProfile"]>("awsProfile")
const awsUseProfile = context.globalState.get<GlobalStateAndSettings["awsUseProfile"]>("awsUseProfile")
const awsAuthentication = context.globalState.get<GlobalStateAndSettings["awsAuthentication"]>("awsAuthentication")
const vertexProjectId = context.globalState.get<GlobalStateAndSettings["vertexProjectId"]>("vertexProjectId")
const vertexRegion = context.globalState.get<GlobalStateAndSettings["vertexRegion"]>("vertexRegion")
const openAiBaseUrl = context.globalState.get<GlobalStateAndSettings["openAiBaseUrl"]>("openAiBaseUrl")
const requestyBaseUrl = context.globalState.get<GlobalStateAndSettings["requestyBaseUrl"]>("requestyBaseUrl")
const openAiHeaders = context.globalState.get<GlobalStateAndSettings["openAiHeaders"]>("openAiHeaders")
const ollamaBaseUrl = context.globalState.get<GlobalStateAndSettings["ollamaBaseUrl"]>("ollamaBaseUrl")
const ollamaApiOptionsCtxNum =
context.globalState.get<GlobalStateAndSettings["ollamaApiOptionsCtxNum"]>("ollamaApiOptionsCtxNum")
const lmStudioBaseUrl = context.globalState.get<GlobalStateAndSettings["lmStudioBaseUrl"]>("lmStudioBaseUrl")
const lmStudioMaxTokens = context.globalState.get<GlobalStateAndSettings["lmStudioMaxTokens"]>("lmStudioMaxTokens")
const anthropicBaseUrl = context.globalState.get<GlobalStateAndSettings["anthropicBaseUrl"]>("anthropicBaseUrl")
const geminiBaseUrl = context.globalState.get<GlobalStateAndSettings["geminiBaseUrl"]>("geminiBaseUrl")
const azureApiVersion = context.globalState.get<GlobalStateAndSettings["azureApiVersion"]>("azureApiVersion")
const azureIdentity = context.globalState.get<GlobalStateAndSettings["azureIdentity"]>("azureIdentity")
const openRouterProviderSorting =
context.globalState.get<GlobalStateAndSettings["openRouterProviderSorting"]>("openRouterProviderSorting")
const lastShownAnnouncementId =
context.globalState.get<GlobalStateAndSettings["lastShownAnnouncementId"]>("lastShownAnnouncementId")
const autoApprovalSettings =
context.globalState.get<GlobalStateAndSettings["autoApprovalSettings"]>("autoApprovalSettings")
const browserSettings = context.globalState.get<GlobalStateAndSettings["browserSettings"]>("browserSettings")
const liteLlmBaseUrl = context.globalState.get<GlobalStateAndSettings["liteLlmBaseUrl"]>("liteLlmBaseUrl")
const liteLlmUsePromptCache =
context.globalState.get<GlobalStateAndSettings["liteLlmUsePromptCache"]>("liteLlmUsePromptCache")
const fireworksModelMaxCompletionTokens = context.globalState.get<
GlobalStateAndSettings["fireworksModelMaxCompletionTokens"]
>("fireworksModelMaxCompletionTokens")
const fireworksModelMaxTokens =
context.globalState.get<GlobalStateAndSettings["fireworksModelMaxTokens"]>("fireworksModelMaxTokens")
const userInfo = context.globalState.get<GlobalStateAndSettings["userInfo"]>("userInfo")
const qwenApiLine = context.globalState.get<GlobalStateAndSettings["qwenApiLine"]>("qwenApiLine")
const moonshotApiLine = context.globalState.get<GlobalStateAndSettings["moonshotApiLine"]>("moonshotApiLine")
const zaiApiLine = context.globalState.get<GlobalStateAndSettings["zaiApiLine"]>("zaiApiLine")
const minimaxApiLine = context.globalState.get<GlobalStateAndSettings["minimaxApiLine"]>("minimaxApiLine")
const telemetrySetting = context.globalState.get<GlobalStateAndSettings["telemetrySetting"]>("telemetrySetting")
const asksageApiUrl = context.globalState.get<GlobalStateAndSettings["asksageApiUrl"]>("asksageApiUrl")
const planActSeparateModelsSettingRaw =
context.globalState.get<GlobalStateAndSettings["planActSeparateModelsSetting"]>("planActSeparateModelsSetting")
const favoritedModelIds = context.globalState.get<GlobalStateAndSettings["favoritedModelIds"]>("favoritedModelIds")
const globalClineRulesToggles =
context.globalState.get<GlobalStateAndSettings["globalClineRulesToggles"]>("globalClineRulesToggles")
const requestTimeoutMs = context.globalState.get<GlobalStateAndSettings["requestTimeoutMs"]>("requestTimeoutMs")
const shellIntegrationTimeout =
context.globalState.get<GlobalStateAndSettings["shellIntegrationTimeout"]>("shellIntegrationTimeout")
const enableCheckpointsSettingRaw =
context.globalState.get<GlobalStateAndSettings["enableCheckpointsSetting"]>("enableCheckpointsSetting")
const mcpMarketplaceEnabledRaw =
context.globalState.get<GlobalStateAndSettings["mcpMarketplaceEnabled"]>("mcpMarketplaceEnabled")
const mcpDisplayMode = context.globalState.get<GlobalStateAndSettings["mcpDisplayMode"]>("mcpDisplayMode")
const mcpResponsesCollapsedRaw =
context.globalState.get<GlobalStateAndSettings["mcpResponsesCollapsed"]>("mcpResponsesCollapsed")
const globalWorkflowToggles =
context.globalState.get<GlobalStateAndSettings["globalWorkflowToggles"]>("globalWorkflowToggles")
const globalSkillsToggles = context.globalState.get<GlobalStateAndSettings["globalSkillsToggles"]>("globalSkillsToggles")
const terminalReuseEnabled =
context.globalState.get<GlobalStateAndSettings["terminalReuseEnabled"]>("terminalReuseEnabled")
const vscodeTerminalExecutionMode =
context.globalState.get<GlobalStateAndSettings["vscodeTerminalExecutionMode"]>("vscodeTerminalExecutionMode")
const terminalOutputLineLimit =
context.globalState.get<GlobalStateAndSettings["terminalOutputLineLimit"]>("terminalOutputLineLimit")
const maxConsecutiveMistakes =
context.globalState.get<GlobalStateAndSettings["maxConsecutiveMistakes"]>("maxConsecutiveMistakes")
const subagentTerminalOutputLineLimit = context.globalState.get<
GlobalStateAndSettings["subagentTerminalOutputLineLimit"]
>("subagentTerminalOutputLineLimit")
const defaultTerminalProfile =
context.globalState.get<GlobalStateAndSettings["defaultTerminalProfile"]>("defaultTerminalProfile")
const sapAiCoreBaseUrl = context.globalState.get<GlobalStateAndSettings["sapAiCoreBaseUrl"]>("sapAiCoreBaseUrl")
const sapAiCoreTokenUrl = context.globalState.get<GlobalStateAndSettings["sapAiCoreTokenUrl"]>("sapAiCoreTokenUrl")
const sapAiResourceGroup = context.globalState.get<GlobalStateAndSettings["sapAiResourceGroup"]>("sapAiResourceGroup")
const claudeCodePath = context.globalState.get<GlobalStateAndSettings["claudeCodePath"]>("claudeCodePath")
const difyBaseUrl = context.globalState.get<GlobalStateAndSettings["difyBaseUrl"]>("difyBaseUrl")
const ocaBaseUrl = context.globalState.get("ocaBaseUrl") as string | undefined
const ocaMode = context.globalState.get("ocaMode") as string | undefined
const openaiReasoningEffort =
context.globalState.get<GlobalStateAndSettings["openaiReasoningEffort"]>("openaiReasoningEffort")
const preferredLanguage = context.globalState.get<GlobalStateAndSettings["preferredLanguage"]>("preferredLanguage")
const focusChainSettings = context.globalState.get<GlobalStateAndSettings["focusChainSettings"]>("focusChainSettings")
const dictationSettings = context.globalState.get<GlobalStateAndSettings["dictationSettings"]>("dictationSettings") as
| DictationSettings
| undefined
const lastDismissedInfoBannerVersion =
context.globalState.get<GlobalStateAndSettings["lastDismissedInfoBannerVersion"]>("lastDismissedInfoBannerVersion")
const lastDismissedModelBannerVersion = context.globalState.get<
GlobalStateAndSettings["lastDismissedModelBannerVersion"]
>("lastDismissedModelBannerVersion")
const lastDismissedCliBannerVersion =
context.globalState.get<GlobalStateAndSettings["lastDismissedCliBannerVersion"]>("lastDismissedCliBannerVersion")
const dismissedBanners = context.globalState.get<GlobalStateAndSettings["dismissedBanners"]>("dismissedBanners")
const qwenCodeOauthPath = context.globalState.get<GlobalStateAndSettings["qwenCodeOauthPath"]>("qwenCodeOauthPath")
const customPrompt = context.globalState.get<GlobalStateAndSettings["customPrompt"]>("customPrompt")
const autoCondenseThreshold =
context.globalState.get<GlobalStateAndSettings["autoCondenseThreshold"]>("autoCondenseThreshold") // number from 0 to 1
const hooksEnabled = context.globalState.get<GlobalStateAndSettings["hooksEnabled"]>("hooksEnabled")
const enableParallelToolCalling =
context.globalState.get<GlobalStateAndSettings["enableParallelToolCalling"]>("enableParallelToolCalling")
const hicapModelId = context.globalState.get<GlobalStateAndSettings["hicapModelId"]>("hicapModelId")
const aihubmixBaseUrl = context.globalState.get<GlobalStateAndSettings["aihubmixBaseUrl"]>("aihubmixBaseUrl")
const aihubmixAppCode = context.globalState.get<GlobalStateAndSettings["aihubmixAppCode"]>("aihubmixAppCode")
// OpenTelemetry configuration
const openTelemetryEnabled =
context.globalState.get<GlobalStateAndSettings["openTelemetryEnabled"]>("openTelemetryEnabled")
const openTelemetryMetricsExporter =
context.globalState.get<GlobalStateAndSettings["openTelemetryMetricsExporter"]>("openTelemetryMetricsExporter")
const openTelemetryLogsExporter =
context.globalState.get<GlobalStateAndSettings["openTelemetryLogsExporter"]>("openTelemetryLogsExporter")
const openTelemetryOtlpProtocol =
context.globalState.get<GlobalStateAndSettings["openTelemetryOtlpProtocol"]>("openTelemetryOtlpProtocol")
const openTelemetryOtlpEndpoint =
context.globalState.get<GlobalStateAndSettings["openTelemetryOtlpEndpoint"]>("openTelemetryOtlpEndpoint")
const openTelemetryOtlpMetricsProtocol = context.globalState.get<
GlobalStateAndSettings["openTelemetryOtlpMetricsProtocol"]
>("openTelemetryOtlpMetricsProtocol")
const openTelemetryOtlpMetricsEndpoint = context.globalState.get<
GlobalStateAndSettings["openTelemetryOtlpMetricsEndpoint"]
>("openTelemetryOtlpMetricsEndpoint")
const openTelemetryOtlpLogsProtocol =
context.globalState.get<GlobalStateAndSettings["openTelemetryOtlpLogsProtocol"]>("openTelemetryOtlpLogsProtocol")
const openTelemetryOtlpLogsEndpoint =
context.globalState.get<GlobalStateAndSettings["openTelemetryOtlpLogsEndpoint"]>("openTelemetryOtlpLogsEndpoint")
const openTelemetryMetricExportInterval = context.globalState.get<
GlobalStateAndSettings["openTelemetryMetricExportInterval"]
>("openTelemetryMetricExportInterval")
const openTelemetryOtlpInsecure =
context.globalState.get<GlobalStateAndSettings["openTelemetryOtlpInsecure"]>("openTelemetryOtlpInsecure")
const openTelemetryLogBatchSize =
context.globalState.get<GlobalStateAndSettings["openTelemetryLogBatchSize"]>("openTelemetryLogBatchSize")
const openTelemetryLogBatchTimeout =
context.globalState.get<GlobalStateAndSettings["openTelemetryLogBatchTimeout"]>("openTelemetryLogBatchTimeout")
const openTelemetryLogMaxQueueSize =
context.globalState.get<GlobalStateAndSettings["openTelemetryLogMaxQueueSize"]>("openTelemetryLogMaxQueueSize")
const subagentsEnabled = context.globalState.get<GlobalStateAndSettings["subagentsEnabled"]>("subagentsEnabled")
const skillsEnabled = context.globalState.get<GlobalStateAndSettings["skillsEnabled"]>("skillsEnabled")
const backgroundEditEnabled =
context.globalState.get<GlobalStateAndSettings["backgroundEditEnabled"]>("backgroundEditEnabled")
// Get mode-related configurations
const mode = context.globalState.get<GlobalStateAndSettings["mode"]>("mode")
// Plan mode configurations
const planModeApiProvider = context.globalState.get<GlobalStateAndSettings["planModeApiProvider"]>("planModeApiProvider")
const planModeApiModelId = context.globalState.get<GlobalStateAndSettings["planModeApiModelId"]>("planModeApiModelId")
const planModeThinkingBudgetTokens =
context.globalState.get<GlobalStateAndSettings["planModeThinkingBudgetTokens"]>("planModeThinkingBudgetTokens")
const geminiPlanModeThinkingLevel =
context.globalState.get<GlobalStateAndSettings["geminiPlanModeThinkingLevel"]>("geminiPlanModeThinkingLevel")
const planModeReasoningEffort =
context.globalState.get<GlobalStateAndSettings["planModeReasoningEffort"]>("planModeReasoningEffort")
const planModeVsCodeLmModelSelector =
context.globalState.get<GlobalStateAndSettings["planModeVsCodeLmModelSelector"]>("planModeVsCodeLmModelSelector")
const planModeAwsBedrockCustomSelected = context.globalState.get<
GlobalStateAndSettings["planModeAwsBedrockCustomSelected"]
>("planModeAwsBedrockCustomSelected")
const planModeAwsBedrockCustomModelBaseId = context.globalState.get<
GlobalStateAndSettings["planModeAwsBedrockCustomModelBaseId"]
>("planModeAwsBedrockCustomModelBaseId")
const planModeOpenRouterModelId =
context.globalState.get<GlobalStateAndSettings["planModeOpenRouterModelId"]>("planModeOpenRouterModelId")
const planModeOpenRouterModelInfo =
context.globalState.get<GlobalStateAndSettings["planModeOpenRouterModelInfo"]>("planModeOpenRouterModelInfo")
const planModeOpenAiModelId =
context.globalState.get<GlobalStateAndSettings["planModeOpenAiModelId"]>("planModeOpenAiModelId")
const planModeOpenAiModelInfo =
context.globalState.get<GlobalStateAndSettings["planModeOpenAiModelInfo"]>("planModeOpenAiModelInfo")
const planModeOllamaModelId =
context.globalState.get<GlobalStateAndSettings["planModeOllamaModelId"]>("planModeOllamaModelId")
const planModeLmStudioModelId =
context.globalState.get<GlobalStateAndSettings["planModeLmStudioModelId"]>("planModeLmStudioModelId")
const planModeLiteLlmModelId =
context.globalState.get<GlobalStateAndSettings["planModeLiteLlmModelId"]>("planModeLiteLlmModelId")
const planModeLiteLlmModelInfo =
context.globalState.get<GlobalStateAndSettings["planModeLiteLlmModelInfo"]>("planModeLiteLlmModelInfo")
const planModeRequestyModelId =
context.globalState.get<GlobalStateAndSettings["planModeRequestyModelId"]>("planModeRequestyModelId")
const planModeRequestyModelInfo =
context.globalState.get<GlobalStateAndSettings["planModeRequestyModelInfo"]>("planModeRequestyModelInfo")
const planModeTogetherModelId =
context.globalState.get<GlobalStateAndSettings["planModeTogetherModelId"]>("planModeTogetherModelId")
const planModeFireworksModelId =
context.globalState.get<GlobalStateAndSettings["planModeFireworksModelId"]>("planModeFireworksModelId")
const planModeSapAiCoreModelId =
context.globalState.get<GlobalStateAndSettings["planModeSapAiCoreModelId"]>("planModeSapAiCoreModelId")
const planModeSapAiCoreDeploymentId =
context.globalState.get<GlobalStateAndSettings["planModeSapAiCoreDeploymentId"]>("planModeSapAiCoreDeploymentId")
const planModeGroqModelId = context.globalState.get<GlobalStateAndSettings["planModeGroqModelId"]>("planModeGroqModelId")
const planModeGroqModelInfo =
context.globalState.get<GlobalStateAndSettings["planModeGroqModelInfo"]>("planModeGroqModelInfo")
const planModeHuggingFaceModelId =
context.globalState.get<GlobalStateAndSettings["planModeHuggingFaceModelId"]>("planModeHuggingFaceModelId")
const planModeHuggingFaceModelInfo =
context.globalState.get<GlobalStateAndSettings["planModeHuggingFaceModelInfo"]>("planModeHuggingFaceModelInfo")
const planModeHuaweiCloudMaasModelId =
context.globalState.get<GlobalStateAndSettings["planModeHuaweiCloudMaasModelId"]>("planModeHuaweiCloudMaasModelId")
const planModeHuaweiCloudMaasModelInfo = context.globalState.get<
GlobalStateAndSettings["planModeHuaweiCloudMaasModelInfo"]
>("planModeHuaweiCloudMaasModelInfo")
const planModeBasetenModelId =
context.globalState.get<GlobalStateAndSettings["planModeBasetenModelId"]>("planModeBasetenModelId")
const planModeBasetenModelInfo =
context.globalState.get<GlobalStateAndSettings["planModeBasetenModelInfo"]>("planModeBasetenModelInfo")
const planModeOcaModelId = context.globalState.get("planModeOcaModelId") as string | undefined
const planModeOcaModelInfo = context.globalState.get("planModeOcaModelInfo") as OcaModelInfo | undefined
const planModeOcaReasoningEffort = context.globalState.get("planModeOcaReasoningEffort") as string | undefined
const planModeHicapModelId =
context.globalState.get<GlobalStateAndSettings["planModeHicapModelId"]>("planModeHicapModelId")
const planModeHicapModelInfo =
context.globalState.get<GlobalStateAndSettings["planModeHicapModelInfo"]>("planModeHicapModelInfo")
const planModeAihubmixModelId =
context.globalState.get<GlobalStateAndSettings["planModeAihubmixModelId"]>("planModeAihubmixModelId")
const planModeAihubmixModelInfo =
context.globalState.get<GlobalStateAndSettings["planModeAihubmixModelInfo"]>("planModeAihubmixModelInfo")
const planModeNousResearchModelId =
context.globalState.get<GlobalStateAndSettings["planModeNousResearchModelId"]>("planModeNousResearchModelId")
const planModeVercelAiGatewayModelId =
context.globalState.get<GlobalStateAndSettings["planModeVercelAiGatewayModelId"]>("planModeVercelAiGatewayModelId")
const planModeVercelAiGatewayModelInfo = context.globalState.get<
GlobalStateAndSettings["planModeVercelAiGatewayModelInfo"]
>("planModeVercelAiGatewayModelInfo")
// Act mode configurations
const actModeApiProvider = context.globalState.get<GlobalStateAndSettings["actModeApiProvider"]>("actModeApiProvider")
const actModeApiModelId = context.globalState.get<GlobalStateAndSettings["actModeApiModelId"]>("actModeApiModelId")
const actModeThinkingBudgetTokens =
context.globalState.get<GlobalStateAndSettings["actModeThinkingBudgetTokens"]>("actModeThinkingBudgetTokens")
const geminiActModeThinkingLevel =
context.globalState.get<GlobalStateAndSettings["geminiActModeThinkingLevel"]>("geminiActModeThinkingLevel")
const actModeReasoningEffort =
context.globalState.get<GlobalStateAndSettings["actModeReasoningEffort"]>("actModeReasoningEffort")
const actModeVsCodeLmModelSelector =
context.globalState.get<GlobalStateAndSettings["actModeVsCodeLmModelSelector"]>("actModeVsCodeLmModelSelector")
const actModeAwsBedrockCustomSelected = context.globalState.get<
GlobalStateAndSettings["actModeAwsBedrockCustomSelected"]
>("actModeAwsBedrockCustomSelected")
const actModeAwsBedrockCustomModelBaseId = context.globalState.get<
GlobalStateAndSettings["actModeAwsBedrockCustomModelBaseId"]
>("actModeAwsBedrockCustomModelBaseId")
const actModeOpenRouterModelId =
context.globalState.get<GlobalStateAndSettings["actModeOpenRouterModelId"]>("actModeOpenRouterModelId")
const actModeOpenRouterModelInfo =
context.globalState.get<GlobalStateAndSettings["actModeOpenRouterModelInfo"]>("actModeOpenRouterModelInfo")
const actModeOpenAiModelId =
context.globalState.get<GlobalStateAndSettings["actModeOpenAiModelId"]>("actModeOpenAiModelId")
const actModeOpenAiModelInfo =
context.globalState.get<GlobalStateAndSettings["actModeOpenAiModelInfo"]>("actModeOpenAiModelInfo")
const actModeOllamaModelId =
context.globalState.get<GlobalStateAndSettings["actModeOllamaModelId"]>("actModeOllamaModelId")
const actModeLmStudioModelId =
context.globalState.get<GlobalStateAndSettings["actModeLmStudioModelId"]>("actModeLmStudioModelId")
const actModeLiteLlmModelId =
context.globalState.get<GlobalStateAndSettings["actModeLiteLlmModelId"]>("actModeLiteLlmModelId")
const actModeLiteLlmModelInfo =
context.globalState.get<GlobalStateAndSettings["actModeLiteLlmModelInfo"]>("actModeLiteLlmModelInfo")
const actModeRequestyModelId =
context.globalState.get<GlobalStateAndSettings["actModeRequestyModelId"]>("actModeRequestyModelId")
const actModeRequestyModelInfo =
context.globalState.get<GlobalStateAndSettings["actModeRequestyModelInfo"]>("actModeRequestyModelInfo")
const actModeTogetherModelId =
context.globalState.get<GlobalStateAndSettings["actModeTogetherModelId"]>("actModeTogetherModelId")
const actModeFireworksModelId =
context.globalState.get<GlobalStateAndSettings["actModeFireworksModelId"]>("actModeFireworksModelId")
const actModeSapAiCoreModelId =
context.globalState.get<GlobalStateAndSettings["actModeSapAiCoreModelId"]>("actModeSapAiCoreModelId")
const actModeSapAiCoreDeploymentId =
context.globalState.get<GlobalStateAndSettings["actModeSapAiCoreDeploymentId"]>("actModeSapAiCoreDeploymentId")
const actModeGroqModelId = context.globalState.get<GlobalStateAndSettings["actModeGroqModelId"]>("actModeGroqModelId")
const actModeGroqModelInfo =
context.globalState.get<GlobalStateAndSettings["actModeGroqModelInfo"]>("actModeGroqModelInfo")
const actModeHuggingFaceModelId =
context.globalState.get<GlobalStateAndSettings["actModeHuggingFaceModelId"]>("actModeHuggingFaceModelId")
const actModeHuggingFaceModelInfo =
context.globalState.get<GlobalStateAndSettings["actModeHuggingFaceModelInfo"]>("actModeHuggingFaceModelInfo")
const actModeHuaweiCloudMaasModelId =
context.globalState.get<GlobalStateAndSettings["actModeHuaweiCloudMaasModelId"]>("actModeHuaweiCloudMaasModelId")
const actModeHuaweiCloudMaasModelInfo = context.globalState.get<
GlobalStateAndSettings["actModeHuaweiCloudMaasModelInfo"]
>("actModeHuaweiCloudMaasModelInfo")
const actModeBasetenModelId =
context.globalState.get<GlobalStateAndSettings["actModeBasetenModelId"]>("actModeBasetenModelId")
const actModeBasetenModelInfo =
context.globalState.get<GlobalStateAndSettings["actModeBasetenModelInfo"]>("actModeBasetenModelInfo")
const actModeOcaModelId = context.globalState.get("actModeOcaModelId") as string | undefined
const actModeOcaModelInfo = context.globalState.get("actModeOcaModelInfo") as OcaModelInfo | undefined
const actModeOcaReasoningEffort = context.globalState.get("actModeOcaReasoningEffort") as string | undefined
const actModeNousResearchModelId =
context.globalState.get<GlobalStateAndSettings["actModeNousResearchModelId"]>("actModeNousResearchModelId")
const sapAiCoreUseOrchestrationMode =
context.globalState.get<GlobalStateAndSettings["sapAiCoreUseOrchestrationMode"]>("sapAiCoreUseOrchestrationMode")
const actModeHicapModelId = context.globalState.get<GlobalStateAndSettings["actModeHicapModelId"]>("actModeHicapModelId")
const actModeHicapModelInfo =
context.globalState.get<GlobalStateAndSettings["actModeHicapModelInfo"]>("actModeHicapModelInfo")
const actModeAihubmixModelId =
context.globalState.get<GlobalStateAndSettings["actModeAihubmixModelId"]>("actModeAihubmixModelId")
const actModeAihubmixModelInfo =
context.globalState.get<GlobalStateAndSettings["actModeAihubmixModelInfo"]>("actModeAihubmixModelInfo")
const actModeVercelAiGatewayModelId =
context.globalState.get<GlobalStateAndSettings["actModeVercelAiGatewayModelId"]>("actModeVercelAiGatewayModelId")
const actModeVercelAiGatewayModelInfo = context.globalState.get<
GlobalStateAndSettings["actModeVercelAiGatewayModelInfo"]
>("actModeVercelAiGatewayModelInfo")
let apiProvider: ApiProvider
if (planModeApiProvider) {
apiProvider = planModeApiProvider
} else {
// New users should default to openrouter, since they've opted to use an API key instead of signing in
apiProvider = "openrouter"
// Batch read all state values in a single optimized pass
const stateValues = new Map<string, any>()
// Read all values at once for better performance
for (const key of GlobalStateAndSettingKeys) {
const value = context.globalState.get(key as string)
stateValues.set(key, value)
}
const mcpResponsesCollapsed = mcpResponsesCollapsedRaw ?? false
// Build result object with proper typing
const result = {} as any // Use any for assignment, but return proper type
// Plan/Act separate models setting is a boolean indicating whether the user wants to use different models for plan and act. Existing users expect this to be enabled, while we want new users to opt in to this being disabled by default.
// On win11 state sometimes initializes as empty string instead of undefined
let planActSeparateModelsSetting: boolean | undefined
if (planActSeparateModelsSettingRaw === true || planActSeparateModelsSettingRaw === false) {
planActSeparateModelsSetting = planActSeparateModelsSettingRaw
} else {
// default to false
planActSeparateModelsSetting = false
// Process each state property using optimized approach
for (const key of GlobalStateAndSettingKeys) {
const stateKey = key as keyof GlobalStateAndSettings
let value = stateValues.get(stateKey)
// Skip async properties - they need special handling
if (isAsyncProperty(stateKey)) {
continue
}
// Skip computed properties - they need special handling
if (isComputedProperty(stateKey)) {
continue
}
// Apply default value if needed
if (value === undefined) {
const defaultValue = getDefaultValue(stateKey)
if (defaultValue !== undefined) {
value = defaultValue
}
}
// Apply transformation if provided
if (value !== undefined) {
value = applyTransform(stateKey, value)
}
// Set the processed value
result[stateKey] = value
}
// Read task history from disk
// Note: If this throws (e.g., filesystem I/O error), StateManager initialization will fail
// and the extension will not start. This is intentional to prevent data loss - better to
// fail visibly than silently wipe history. The readTaskHistoryFromState function handles:
// - File doesn't exist → returns []
// - Parse errors → attempts reconstruction, returns [] only if reconstruction fails
// - I/O errors → throws (caught here, causing initialization to fail)
// Handle computed properties with special logic
await handleComputedProperties(result, stateValues)
// So, any errors thrown here are true IO errors, which should be exceptionally rare.
// The state manager tries once more to start on any failure. So if there is truly an I/O error happening twice that is not due to the file not existing or being corrupted, then something is truly wrong and it is correct to not start the application.
const taskHistory = await readTaskHistoryFromState()
// Handle async properties
await handleAsyncProperties(result)
// Multi-root workspace support
const workspaceRoots = context.globalState.get<GlobalStateAndSettings["workspaceRoots"]>("workspaceRoots")
/**
* Get primary root index from global state.
* The primary root is the main workspace folder that Cline focuses on when dealing with
* multi-root workspaces. In VS Code, you can have multiple folders open in one workspace,
* and the primary root index indicates which folder (by its position in the array, 0-based)
* should be treated as the main/default working directory for operations.
*/
const primaryRootIndex = context.globalState.get<GlobalStateAndSettings["primaryRootIndex"]>("primaryRootIndex")
const multiRootEnabled = context.globalState.get<GlobalStateAndSettings["multiRootEnabled"]>("multiRootEnabled")
const nativeToolCallEnabled =
context.globalState.get<GlobalStateAndSettings["nativeToolCallEnabled"]>("nativeToolCallEnabled")
const remoteRulesToggles = context.globalState.get<GlobalStateAndSettings["remoteRulesToggles"]>("remoteRulesToggles")
const remoteWorkflowToggles =
context.globalState.get<GlobalStateAndSettings["remoteWorkflowToggles"]>("remoteWorkflowToggles")
return {
// api configuration fields
claudeCodePath,
awsRegion,
awsUseCrossRegionInference,
awsUseGlobalInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
awsAuthentication,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
requestyBaseUrl,
openAiHeaders: openAiHeaders || {},
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioBaseUrl,
lmStudioMaxTokens,
anthropicBaseUrl,
geminiBaseUrl,
qwenApiLine,
moonshotApiLine,
zaiApiLine,
azureApiVersion,
azureIdentity,
openRouterProviderSorting,
liteLlmBaseUrl,
liteLlmUsePromptCache,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
asksageApiUrl,
favoritedModelIds: favoritedModelIds || [],
requestTimeoutMs,
sapAiCoreBaseUrl,
sapAiCoreTokenUrl,
sapAiResourceGroup,
difyBaseUrl,
sapAiCoreUseOrchestrationMode: sapAiCoreUseOrchestrationMode ?? true,
ocaBaseUrl,
minimaxApiLine,
ocaMode: ocaMode || "internal",
hicapModelId,
aihubmixBaseUrl,
aihubmixAppCode,
// Plan mode configurations
planModeApiProvider: planModeApiProvider || apiProvider,
planModeApiModelId,
// undefined means it was never modified, 0 means it was turned off
// (having this on by default ensures that <thinking> text does not pollute the user's chat and is instead rendered as reasoning)
planModeThinkingBudgetTokens: planModeThinkingBudgetTokens ?? ANTHROPIC_MIN_THINKING_BUDGET,
planModeReasoningEffort,
planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId,
planModeOpenRouterModelId,
planModeOpenRouterModelInfo,
planModeOpenAiModelId,
planModeOpenAiModelInfo,
planModeOllamaModelId,
planModeLmStudioModelId,
planModeLiteLlmModelId,
planModeLiteLlmModelInfo,
planModeRequestyModelId,
planModeRequestyModelInfo,
planModeTogetherModelId,
planModeFireworksModelId: planModeFireworksModelId || fireworksDefaultModelId,
planModeSapAiCoreModelId,
planModeSapAiCoreDeploymentId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeHuggingFaceModelId,
planModeHuggingFaceModelInfo,
planModeHuaweiCloudMaasModelId,
planModeHuaweiCloudMaasModelInfo,
planModeBasetenModelId,
planModeBasetenModelInfo,
planModeOcaModelId,
planModeOcaModelInfo,
planModeOcaReasoningEffort,
planModeHicapModelId,
planModeHicapModelInfo,
planModeAihubmixModelId,
planModeAihubmixModelInfo,
planModeNousResearchModelId,
planModeVercelAiGatewayModelId,
planModeVercelAiGatewayModelInfo,
geminiPlanModeThinkingLevel,
// Act mode configurations
actModeApiProvider: actModeApiProvider || apiProvider,
actModeApiModelId,
actModeThinkingBudgetTokens: actModeThinkingBudgetTokens ?? ANTHROPIC_MIN_THINKING_BUDGET,
actModeReasoningEffort,
actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId,
actModeOpenRouterModelId,
actModeOpenRouterModelInfo,
actModeOpenAiModelId,
actModeOpenAiModelInfo,
actModeOllamaModelId,
actModeLmStudioModelId,
actModeLiteLlmModelId,
actModeLiteLlmModelInfo,
actModeRequestyModelId,
actModeRequestyModelInfo,
actModeTogetherModelId,
actModeFireworksModelId: actModeFireworksModelId || fireworksDefaultModelId,
actModeSapAiCoreModelId,
actModeSapAiCoreDeploymentId,
actModeGroqModelId,
actModeGroqModelInfo,
actModeHuggingFaceModelId,
actModeHuggingFaceModelInfo,
actModeHuaweiCloudMaasModelId,
actModeHuaweiCloudMaasModelInfo,
actModeBasetenModelId,
actModeBasetenModelInfo,
actModeOcaModelId,
actModeOcaModelInfo,
actModeOcaReasoningEffort,
actModeHicapModelId,
actModeHicapModelInfo,
actModeAihubmixModelId,
actModeAihubmixModelInfo,
actModeNousResearchModelId,
actModeVercelAiGatewayModelId,
actModeVercelAiGatewayModelInfo,
geminiActModeThinkingLevel,
// Other global fields
focusChainSettings: focusChainSettings || DEFAULT_FOCUS_CHAIN_SETTINGS,
dictationSettings: { ...DEFAULT_DICTATION_SETTINGS, ...dictationSettings },
strictPlanModeEnabled: strictPlanModeEnabled ?? true,
yoloModeToggled: yoloModeToggled ?? false,
useAutoCondense: useAutoCondense ?? false,
clineWebToolsEnabled: clineWebToolsEnabled ?? true,
isNewUser: isNewUser ?? true,
welcomeViewCompleted,
lastShownAnnouncementId,
taskHistory: taskHistory || [],
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
globalClineRulesToggles: globalClineRulesToggles || {},
browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled)
preferredLanguage: preferredLanguage || "English",
openaiReasoningEffort: (openaiReasoningEffort as OpenaiReasoningEffort) || "medium",
mode: mode || "act",
userInfo,
mcpMarketplaceEnabled: mcpMarketplaceEnabledRaw ?? true,
mcpDisplayMode: mcpDisplayMode ?? DEFAULT_MCP_DISPLAY_MODE,
mcpResponsesCollapsed: mcpResponsesCollapsed,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting: planActSeparateModelsSetting ?? false,
enableCheckpointsSetting: enableCheckpointsSettingRaw ?? true,
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
terminalReuseEnabled: terminalReuseEnabled ?? true,
vscodeTerminalExecutionMode: vscodeTerminalExecutionMode ?? "vscodeTerminal",
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
maxConsecutiveMistakes: maxConsecutiveMistakes ?? 3,
subagentTerminalOutputLineLimit: subagentTerminalOutputLineLimit ?? 2000,
defaultTerminalProfile: defaultTerminalProfile ?? "default",
globalWorkflowToggles: globalWorkflowToggles || {},
globalSkillsToggles: globalSkillsToggles || {},
qwenCodeOauthPath,
customPrompt,
autoCondenseThreshold: autoCondenseThreshold || 0.75, // default to 0.75 if not set
backgroundEditEnabled: backgroundEditEnabled ?? false,
// Hooks require explicit user opt-in and are only supported on macOS/Linux
hooksEnabled: getHooksEnabledSafe(hooksEnabled),
subagentsEnabled: subagentsEnabled ?? false,
skillsEnabled: skillsEnabled ?? false,
enableParallelToolCalling: enableParallelToolCalling ?? false,
lastDismissedInfoBannerVersion: lastDismissedInfoBannerVersion ?? 0,
lastDismissedModelBannerVersion: lastDismissedModelBannerVersion ?? 0,
lastDismissedCliBannerVersion: lastDismissedCliBannerVersion ?? 0,
dismissedBanners: dismissedBanners || [],
nativeToolCallEnabled: nativeToolCallEnabled ?? true,
// Multi-root workspace support
workspaceRoots,
primaryRootIndex: primaryRootIndex ?? 0,
// Feature flag - defaults to false
// For now, always return false to disable multi-root support by default
multiRootEnabled: !!multiRootEnabled,
// OpenTelemetry configuration
openTelemetryEnabled: openTelemetryEnabled ?? true,
openTelemetryMetricsExporter,
openTelemetryLogsExporter,
openTelemetryOtlpProtocol: openTelemetryOtlpProtocol ?? "http/json",
openTelemetryOtlpEndpoint: openTelemetryOtlpEndpoint ?? "http://localhost:4318",
openTelemetryOtlpMetricsProtocol,
openTelemetryOtlpMetricsEndpoint,
openTelemetryOtlpLogsProtocol,
openTelemetryOtlpLogsEndpoint,
openTelemetryMetricExportInterval: openTelemetryMetricExportInterval ?? 60000,
openTelemetryOtlpInsecure: openTelemetryOtlpInsecure ?? false,
openTelemetryLogBatchSize: openTelemetryLogBatchSize ?? 512,
openTelemetryLogBatchTimeout: openTelemetryLogBatchTimeout ?? 5000,
openTelemetryLogMaxQueueSize: openTelemetryLogMaxQueueSize ?? 2048,
remoteRulesToggles: remoteRulesToggles || {},
remoteWorkflowToggles: remoteWorkflowToggles || {},
}
return result as GlobalStateAndSettings
} catch (error) {
console.error("[StateHelpers] Failed to read global state:", error)
throw error
}
}
/**
* Handle properties that require computed logic
*/
async function handleComputedProperties(result: any, stateValues: Map<string, any>): Promise<void> {
// 1. API Provider logic - set defaults based on existing values
const defaultApiProvider: ApiProvider = "openrouter"
result.planModeApiProvider = result.planModeApiProvider || defaultApiProvider
result.actModeApiProvider = result.actModeApiProvider || defaultApiProvider
// 2. Plan/Act separate models setting with special logic
const planActSeparateModelsSettingRaw = stateValues.get("planActSeparateModelsSetting")
if (planActSeparateModelsSettingRaw === true || planActSeparateModelsSettingRaw === false) {
result.planActSeparateModelsSetting = planActSeparateModelsSettingRaw
} else {
// Default to false when not explicitly set
result.planActSeparateModelsSetting = false
}
}
/**
* Handle properties that require async operations
*/
async function handleAsyncProperties(result: any): Promise<void> {
// Task history requires async disk read
result.taskHistory = await readTaskHistoryFromState()
}
export async function resetWorkspaceState(controller: Controller) {
const context = controller.context
await Promise.all(context.workspaceState.keys().map((key) => controller.context.workspaceState.update(key, undefined)))
await Promise.all(LocalStateKeys.map((key) => controller.context.workspaceState.update(key, undefined)))
await controller.stateManager.reInitialize()
}
@@ -762,49 +128,9 @@ export async function resetGlobalState(controller: Controller) {
// TODO: Reset all workspace states?
const context = controller.context
await Promise.all(context.globalState.keys().map((key) => context.globalState.update(key, undefined)))
const secretKeys: SecretKey[] = [
"apiKey",
"openRouterApiKey",
"awsAccessKey",
"awsSecretKey",
"awsSessionToken",
"awsBedrockApiKey",
"openAiApiKey",
"ollamaApiKey",
"geminiApiKey",
"openAiNativeApiKey",
"deepSeekApiKey",
"requestyApiKey",
"togetherApiKey",
"qwenApiKey",
"doubaoApiKey",
"mistralApiKey",
"clineAccountId",
"liteLlmApiKey",
"remoteLiteLlmApiKey",
"fireworksApiKey",
"asksageApiKey",
"xaiApiKey",
"sambanovaApiKey",
"cerebrasApiKey",
"groqApiKey",
"basetenApiKey",
"moonshotApiKey",
"nebiusApiKey",
"huggingFaceApiKey",
"huaweiCloudMaasApiKey",
"vercelAiGatewayApiKey",
"zaiApiKey",
"difyApiKey",
"ocaApiKey",
"ocaRefreshToken",
"minimaxApiKey",
"hicapApiKey",
"aihubmixApiKey",
"mcpOAuthSecrets",
"nousResearchApiKey",
]
await Promise.all(secretKeys.map((key) => context.secrets.delete(key)))
await Promise.all(GlobalStateAndSettingKeys.map((key) => context.globalState.update(key, undefined)))
await Promise.all(SecretKeys.map((key) => context.secrets.delete(key)))
await controller.stateManager.reInitialize()
}
+129
View File
@@ -0,0 +1,129 @@
import { ClineAskResponse } from "@shared/WebviewMessage"
/**
* Represents a pending ask operation waiting for user response
*/
interface PendingAsk {
askId: string
askTs: number
response?: ClineAskResponse
text?: string
images?: string[]
files?: string[]
resolved: boolean
}
/**
* Manages concurrent ask operations with queue-based tracking.
* Each ask gets a unique ID, allowing multiple asks to be pending simultaneously.
* Responses are matched back to their corresponding asks.
*/
export class PendingAskQueue {
private queue: Map<string, PendingAsk> = new Map()
private lastMessageTs?: number
/**
* Create a new pending ask and add it to the queue
* @returns The unique askId for this ask operation
*/
createPendingAsk(askTs: number): string {
const askId = `ask-${askTs}-${Math.random().toString(36).substr(2, 9)}`
this.queue.set(askId, {
askId,
askTs,
resolved: false,
})
this.lastMessageTs = askTs
return askId
}
/**
* Resolve a pending ask with user response
* @param askId The unique ID of the ask to resolve
* @param response The user's response
* @param text Optional text response
* @param images Optional image attachments
* @param files Optional file attachments
* @returns true if ask was found and resolved, false otherwise
*/
resolvePendingAsk(askId: string, response: ClineAskResponse, text?: string, images?: string[], files?: string[]): boolean {
const pendingAsk = this.queue.get(askId)
if (!pendingAsk) {
return false
}
pendingAsk.response = response
pendingAsk.text = text
pendingAsk.images = images
pendingAsk.files = files
pendingAsk.resolved = true
return true
}
/**
* Get the resolution status of a pending ask
* @param askId The unique ID of the ask
* @returns The ask object if found, undefined otherwise
*/
getPendingAsk(askId: string): PendingAsk | undefined {
return this.queue.get(askId)
}
/**
* Remove a resolved ask from the queue
* @param askId The unique ID of the ask
*/
removePendingAsk(askId: string): void {
this.queue.delete(askId)
}
/**
* Get all pending asks (those not yet resolved)
*/
getPendingAsks(): PendingAsk[] {
return Array.from(this.queue.values()).filter((ask) => !ask.resolved)
}
/**
* Check if an ask was interrupted (another message came after it)
* @param askId The unique ID of the ask
* @returns true if this ask is no longer the most recent, false otherwise
*/
wasAskInterrupted(askId: string, currentLastMessageTs?: number): boolean {
const pendingAsk = this.queue.get(askId)
if (!pendingAsk) {
return true // Ask was removed or never existed
}
// If currentLastMessageTs is provided and is different from this ask's ts,
// then this ask was interrupted by another message
if (currentLastMessageTs !== undefined && currentLastMessageTs !== pendingAsk.askTs) {
return true
}
return false
}
/**
* Clear all pending asks (used on task abort)
*/
clear(): void {
this.queue.clear()
this.lastMessageTs = undefined
}
/**
* Get the last message timestamp
*/
getLastMessageTs(): number | undefined {
return this.lastMessageTs
}
/**
* Set the last message timestamp (for tracking if asks were interrupted)
*/
setLastMessageTs(ts: number): void {
this.lastMessageTs = ts
}
}
+5 -6
View File
@@ -1,6 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AssistantMessageContent } from "@core/assistant-message"
import { ClineAskResponse } from "@shared/WebviewMessage"
import { PendingAskQueue } from "./PendingAskQueue"
import type { HookExecution } from "./types/HookExecution"
export class TaskState {
@@ -21,11 +21,8 @@ export class TaskState {
presentAssistantMessageLocked = false
presentAssistantMessageHasPendingUpdates = false
// Ask/Response handling
askResponse?: ClineAskResponse
askResponseText?: string
askResponseImages?: string[]
askResponseFiles?: string[]
// Ask/Response handling - now queue-based for concurrent ask support
pendingAskQueue = new PendingAskQueue()
lastMessageTs?: number
// Plan mode specific state
@@ -39,6 +36,8 @@ export class TaskState {
didRejectTool = false
didAlreadyUseTool = false
didEditFile: boolean = false
lastToolName: string = "" // Track last tool used for consecutive call detection
isExecutingInParallel = false // Track if currently executing tools in parallel
// Error tracking
consecutiveMistakeCount: number = 0
+11 -3
View File
@@ -101,11 +101,13 @@ export class ToolExecutor {
images?: string[],
files?: string[],
partial?: boolean,
call_id?: string,
) => Promise<number | undefined>,
private ask: (
type: ClineAsk,
text?: string,
partial?: boolean,
call_id?: string,
) => Promise<{
response: ClineAskResponse
text?: string
@@ -114,7 +116,11 @@ export class ToolExecutor {
}>,
private saveCheckpoint: (isAttemptCompletionMessage?: boolean, completionMessageTs?: number) => Promise<void>,
private sayAndCreateMissingParamError: (toolName: ClineDefaultTool, paramName: string, relPath?: string) => Promise<any>,
private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>,
private removeLastPartialMessageIfExistsWithType: (
type: "ask" | "say",
askOrSay: ClineAsk | ClineSay,
call_id?: string,
) => Promise<void>,
private executeCommandTool: (command: string, timeoutSeconds: number | undefined) => Promise<[boolean, any]>,
private doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>,
private updateFCListFromToolResponse: (taskProgress: string | undefined) => Promise<void>,
@@ -294,7 +300,6 @@ export class ToolExecutor {
block,
this.taskState.userMessageContent,
(block: ToolUse) => ToolDisplayUtils.getToolDescription(block),
this.api,
this.coordinator,
this.taskState.toolUseIdMap,
)
@@ -548,7 +553,7 @@ export class ToolExecutor {
// Check if handler supports partial blocks with proper typing
if (handler && "handlePartialBlock" in handler) {
const uiHelpers = createUIHelpers(config)
const uiHelpers = createUIHelpers(config, block.call_id)
const partialHandler = handler as IPartialBlockHandler
await partialHandler.handlePartialBlock(block, uiHelpers)
}
@@ -600,6 +605,9 @@ export class ToolExecutor {
toolWasExecuted = true
this.pushToolResult(toolResult, block)
// Track the last executed tool for consecutive call detection (used by act_mode_respond)
this.taskState.lastToolName = block.name
// Check abort before running PostToolUse hook (success path)
if (this.taskState.abort) {
return
+161 -51
View File
@@ -60,7 +60,7 @@ import { HistoryItem } from "@shared/HistoryItem"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@shared/Languages"
import { USER_CONTENT_TAGS } from "@shared/messages/constants"
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
import { ClineDefaultTool, READ_ONLY_TOOLS } from "@shared/tools"
import { ClineDefaultTool, PARALLEL_SAFE_TOOLS, READ_ONLY_TOOLS } from "@shared/tools"
import { ClineAskResponse } from "@shared/WebviewMessage"
import { isClaude4PlusModelFamily, isGPT5ModelFamily, isLocalModel, isNextGenModelFamily } from "@utils/model-utils"
import { arePathsEqual, getDesktopDir } from "@utils/path"
@@ -586,6 +586,8 @@ export class Task {
throw new Error("Cline instance aborted")
}
let askTs: number
let askId: string | undefined
if (partial !== undefined) {
const clineMessages = this.messageStateHandler.getClineMessages()
const lastMessage = clineMessages.at(-1)
@@ -600,18 +602,13 @@ export class Task {
text,
partial,
})
// todo be more efficient about saving and posting only new data or one whole message at a time so ignore partial for saves, and only post parts of partial message instead of whole array in new listener
// await this.saveClineMessagesAndUpdateHistory()
// await this.postStateToWebview()
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
throw new Error("Current ask promise was ignored 1")
} else {
// this is a new partial message, so add it with partial state
// this.askResponse = undefined
// this.askResponseText = undefined
// this.askResponseImages = undefined
askTs = Date.now()
askId = this.taskState.pendingAskQueue.createPendingAsk(askTs)
this.taskState.lastMessageTs = askTs
await this.messageStateHandler.addToClineMessages({
ts: askTs,
@@ -627,11 +624,6 @@ export class Task {
// partial=false means its a complete version of a previously partial message
if (isUpdatingPreviousPartial) {
// this is the complete version of a previously partial message, so replace the partial with the complete version
this.taskState.askResponse = undefined
this.taskState.askResponseText = undefined
this.taskState.askResponseImages = undefined
this.taskState.askResponseFiles = undefined
/*
Bug for the history books:
In the webview we use the ts as the chatrow key for the virtuoso list. Since we would update this ts right at the end of streaming, it would cause the view to flicker. The key prop has to be stable otherwise react has trouble reconciling items between renders, causing unmounting and remounting of components (flickering).
@@ -639,22 +631,18 @@ export class Task {
So in this case we must make sure that the message ts is never altered after first setting it.
*/
askTs = lastMessage.ts
askId = this.taskState.pendingAskQueue.createPendingAsk(askTs)
this.taskState.lastMessageTs = askTs
// lastMessage.ts = askTs
await this.messageStateHandler.updateClineMessage(lastMessageIndex, {
text,
partial: false,
})
// await this.postStateToWebview()
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
} else {
// this is a new partial=false message, so add it like normal
this.taskState.askResponse = undefined
this.taskState.askResponseText = undefined
this.taskState.askResponseImages = undefined
this.taskState.askResponseFiles = undefined
askTs = Date.now()
askId = this.taskState.pendingAskQueue.createPendingAsk(askTs)
this.taskState.lastMessageTs = askTs
await this.messageStateHandler.addToClineMessages({
ts: askTs,
@@ -667,12 +655,8 @@ export class Task {
}
} else {
// this is a new non-partial message, so add it like normal
// const lastMessage = this.clineMessages.at(-1)
this.taskState.askResponse = undefined
this.taskState.askResponseText = undefined
this.taskState.askResponseImages = undefined
this.taskState.askResponseFiles = undefined
askTs = Date.now()
askId = this.taskState.pendingAskQueue.createPendingAsk(askTs)
this.taskState.lastMessageTs = askTs
await this.messageStateHandler.addToClineMessages({
ts: askTs,
@@ -683,30 +667,52 @@ export class Task {
await this.postStateToWebview()
}
await pWaitFor(() => this.taskState.askResponse !== undefined || this.taskState.lastMessageTs !== askTs, {
interval: 100,
})
if (this.taskState.lastMessageTs !== askTs) {
// Wait for this specific ask to be resolved
if (!askId) {
throw new Error("Failed to create pending ask")
}
await pWaitFor(
() => {
const pendingAsk = this.taskState.pendingAskQueue.getPendingAsk(askId!)
return (
pendingAsk?.resolved === true ||
this.taskState.pendingAskQueue.wasAskInterrupted(askId!, this.taskState.lastMessageTs)
)
},
{
interval: 100,
},
)
const pendingAsk = this.taskState.pendingAskQueue.getPendingAsk(askId)
if (!pendingAsk || this.taskState.pendingAskQueue.wasAskInterrupted(askId, this.taskState.lastMessageTs)) {
throw new Error("Current ask promise was ignored") // could happen if we send multiple asks in a row i.e. with command_output. It's important that when we know an ask could fail, it is handled gracefully
}
const result = {
response: this.taskState.askResponse!,
text: this.taskState.askResponseText,
images: this.taskState.askResponseImages,
files: this.taskState.askResponseFiles,
response: pendingAsk.response!,
text: pendingAsk.text,
images: pendingAsk.images,
files: pendingAsk.files,
}
this.taskState.askResponse = undefined
this.taskState.askResponseText = undefined
this.taskState.askResponseImages = undefined
this.taskState.askResponseFiles = undefined
this.taskState.pendingAskQueue.removePendingAsk(askId)
return result
}
async handleWebviewAskResponse(askResponse: ClineAskResponse, text?: string, images?: string[], files?: string[]) {
this.taskState.askResponse = askResponse
this.taskState.askResponseText = text
this.taskState.askResponseImages = images
this.taskState.askResponseFiles = files
// Get all pending asks and resolve them in the order they were created
const pendingAsks = this.taskState.pendingAskQueue.getPendingAsks()
if (pendingAsks.length === 0) {
console.warn("handleWebviewAskResponse: No pending asks to resolve")
return
}
// Resolve the first unresolved ask (FIFO order)
const firstPendingAsk = pendingAsks[0]
this.taskState.pendingAskQueue.resolvePendingAsk(firstPendingAsk.askId, askResponse, text, images, files)
}
async say(
@@ -715,6 +721,7 @@ export class Task {
images?: string[],
files?: string[],
partial?: boolean,
call_id?: string,
): Promise<number | undefined> {
// Allow hook messages even when aborted to enable proper cleanup
if (this.taskState.abort && type !== "hook_status" && type !== "hook_output_stream") {
@@ -729,9 +736,14 @@ export class Task {
}
if (partial !== undefined) {
const lastMessage = this.messageStateHandler.getClineMessages().at(-1)
const clineMessages = this.messageStateHandler.getClineMessages()
const lastMessage = clineMessages.at(-1)
const isUpdatingPreviousPartial =
lastMessage && lastMessage.partial && lastMessage.type === "say" && lastMessage.say === type
lastMessage &&
lastMessage.partial &&
lastMessage.type === "say" &&
lastMessage.say === type &&
lastMessage.call_id === call_id
if (partial) {
if (isUpdatingPreviousPartial) {
// existing partial message, so update it
@@ -755,6 +767,7 @@ export class Task {
files,
partial,
modelInfo,
call_id,
})
await this.postStateToWebview()
return sayTs
@@ -788,6 +801,7 @@ export class Task {
images,
files,
modelInfo,
call_id,
})
await this.postStateToWebview()
return sayTs
@@ -805,6 +819,7 @@ export class Task {
images,
files,
modelInfo,
call_id,
})
await this.postStateToWebview()
return sayTs
@@ -821,12 +836,31 @@ export class Task {
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
}
async removeLastPartialMessageIfExistsWithType(type: "ask" | "say", askOrSay: ClineAsk | ClineSay) {
async removeLastPartialMessageIfExistsWithType(type: "ask" | "say", askOrSay: ClineAsk | ClineSay, call_id?: string) {
const clineMessages = this.messageStateHandler.getClineMessages()
const lastMessage = clineMessages.at(-1)
if (lastMessage?.partial && lastMessage.type === type && (lastMessage.ask === askOrSay || lastMessage.say === askOrSay)) {
this.messageStateHandler.setClineMessages(clineMessages.slice(0, -1))
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
// If call_id is provided, find the message with matching call_id
if (call_id) {
const messageIndex = clineMessages.findIndex(
(msg) =>
msg.partial && msg.type === type && (msg.ask === askOrSay || msg.say === askOrSay) && msg.call_id === call_id,
)
if (messageIndex >= 0) {
clineMessages.splice(messageIndex, 1)
this.messageStateHandler.setClineMessages(clineMessages)
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
}
} else {
// Old behavior: remove the last message of this type (for sequential execution)
const lastMessage = clineMessages.at(-1)
if (
lastMessage?.partial &&
lastMessage.type === type &&
(lastMessage.ask === askOrSay || lastMessage.say === askOrSay)
) {
this.messageStateHandler.setClineMessages(clineMessages.slice(0, -1))
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
}
}
}
@@ -1933,8 +1967,25 @@ export class Task {
attempt: this.taskState.autoRetryAttempts,
maxAttempts: 3,
delaySeconds: delay / 1000,
errorMessage: streamingFailedMessage,
}),
)
// Clear streamingFailedMessage now that error_retry contains it
// This prevents showing the error in both ErrorRow and error_retry
const autoRetryApiReqIndex = findLastIndex(
this.messageStateHandler.getClineMessages(),
(m) => m.say === "api_req_started",
)
if (autoRetryApiReqIndex !== -1) {
const clineMessages = this.messageStateHandler.getClineMessages()
const currentApiReqInfo: ClineApiReqInfo = JSON.parse(clineMessages[autoRetryApiReqIndex].text || "{}")
delete currentApiReqInfo.streamingFailedMessage
await this.messageStateHandler.updateClineMessage(autoRetryApiReqIndex, {
text: JSON.stringify(currentApiReqInfo),
})
}
await setTimeoutPromise(delay)
} else {
// Show error_retry with failed flag to indicate all retries exhausted (but not for insufficient credits)
@@ -1946,6 +1997,7 @@ export class Task {
maxAttempts: 3,
delaySeconds: 0,
failed: true, // Special flag to indicate retries exhausted
errorMessage: streamingFailedMessage,
}),
)
}
@@ -1991,6 +2043,34 @@ export class Task {
yield* iterator
}
/**
* Collects consecutive complete tool blocks that are safe to run in parallel.
* Stops at the first non-parallel-safe tool or streaming tool.
*/
private collectConsecutiveParallelToolBlocks(): ToolUse[] {
const toolBlocks: ToolUse[] = []
let index = this.taskState.currentStreamingContentIndex
while (index < this.taskState.assistantMessageContent.length) {
const block = this.taskState.assistantMessageContent[index]
// Only collect complete tool blocks
if (block.type !== "tool_use" || block.partial) {
break
}
// Check if this tool can run in parallel
if (!PARALLEL_SAFE_TOOLS.includes(block.name as any)) {
break
}
toolBlocks.push(block)
index++
}
return toolBlocks
}
async presentAssistantMessage() {
if (this.taskState.abort) {
throw new Error("Cline instance aborted")
@@ -2083,7 +2163,7 @@ export class Task {
await this.say("text", content, undefined, undefined, block.partial)
break
}
case "tool_use":
case "tool_use": {
// If we have a pending initial commit, we must block unsafe tools until it finishes.
// Safe tools (read-only) can run in parallel.
if (this.initialCheckpointCommitPromise) {
@@ -2092,8 +2172,35 @@ export class Task {
this.initialCheckpointCommitPromise = undefined
}
}
// Try to execute multiple parallel-safe tools concurrently
if (this.isParallelToolCallingEnabled()) {
const parallelTools = this.collectConsecutiveParallelToolBlocks()
if (parallelTools.length > 1) {
// Set flag to indicate we're in parallel execution
// This allows CommandExecutor to use ConcurrentCommandOrchestrator
this.taskState.isExecutingInParallel = true
this.commandExecutor.setParallelExecution(true)
try {
// Execute all parallel tools concurrently
await Promise.all(parallelTools.map((toolBlock) => this.toolExecutor.executeTool(toolBlock)))
} finally {
// Clear the parallel execution flag
this.taskState.isExecutingInParallel = false
this.commandExecutor.setParallelExecution(false)
}
// Advance past all executed tools
this.taskState.currentStreamingContentIndex += parallelTools.length - 1
break
}
}
// Fall back to sequential execution for non-parallel tools
await this.toolExecutor.executeTool(block)
break
}
}
/*
@@ -2672,6 +2779,7 @@ export class Task {
attempt: this.taskState.autoRetryAttempts,
maxAttempts: 3,
delaySeconds: delay / 1000,
errorMessage,
}),
)
@@ -2693,6 +2801,7 @@ export class Task {
maxAttempts: 3,
delaySeconds: 0,
failed: true, // Special flag to indicate retries exhausted
errorMessage,
}),
)
}
@@ -2914,6 +3023,8 @@ export class Task {
let response: ClineAskResponse
const noResponseErrorMessage = "No assistant message was received. Would you like to retry the request?"
if (this.taskState.autoRetryAttempts < 3) {
// Auto-retry enabled with max 3 attempts: automatically approve the retry
this.taskState.autoRetryAttempts++
@@ -2927,6 +3038,7 @@ export class Task {
attempt: this.taskState.autoRetryAttempts,
maxAttempts: 3,
delaySeconds: delay / 1000,
errorMessage: noResponseErrorMessage,
}),
)
await setTimeoutPromise(delay)
@@ -2939,12 +3051,10 @@ export class Task {
maxAttempts: 3,
delaySeconds: 0,
failed: true, // Special flag to indicate retries exhausted
errorMessage: noResponseErrorMessage,
}),
)
const askResult = await this.ask(
"api_req_failed",
"No assistant message was received. Would you like to retry the request?",
)
const askResult = await this.ask("api_req_failed", noResponseErrorMessage)
response = askResult.response
// Reset retry counter if user chooses to manually retry
if (response === "yesButtonClicked") {
@@ -39,6 +39,18 @@ export class ActModeRespondHandler implements IToolHandler, IPartialBlockHandler
)
}
// Block consecutive act_mode_respond calls to prevent narration loops
// Note: We intentionally do NOT increment consecutiveMistakeCount here to avoid
// breaking the conversation flow - we just guide the model to use proper tools
if (config.taskState.lastToolName === ClineDefaultTool.ACT_MODE) {
return formatResponse.toolResult(
`[BLOCKED] You cannot call act_mode_respond consecutively. ` +
`Your next action MUST be a different tool that performs actual work: ` +
`read_file, replace_in_file, write_to_file, execute_command, list_files, search_files, etc. ` +
`Stop explaining and start doing.`,
)
}
// Validate required parameters
if (!response) {
config.taskState.consecutiveMistakeCount++
@@ -56,8 +68,15 @@ export class ActModeRespondHandler implements IToolHandler, IPartialBlockHandler
await config.callbacks.updateFCListFromToolResponse(taskProgress)
}
// Note: lastToolName is tracked centrally by ToolExecutor after tool execution
// Return success immediately to allow LLM to continue execution
// The key difference from plan_mode_respond: no blocking for user input
return formatResponse.toolResult(`[Message displayed to user. You may now proceed with the next steps.]`)
// NOTE: We explicitly tell the model to use a different tool next to prevent narration loops
return formatResponse.toolResult(
`[Message displayed. Now proceed with your next tool call - ` +
`it must be a different tool (read_file, replace_in_file, execute_command, etc.), ` +
`not act_mode_respond again.]`,
)
}
}
@@ -1,6 +1,7 @@
import type { ToolUse } from "@core/assistant-message"
import { discoverSkills, getAvailableSkills, getSkillContent } from "@core/context/instructions/user-instructions/skills"
import type { SkillMetadata } from "@shared/skills"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
@@ -48,6 +49,13 @@ export class UseSkillToolHandler implements IToolHandler, IPartialBlockHandler {
return `Error: No skills are available. Skills may be disabled or not configured.`
}
const globalCount = availableSkills.filter((skill) => skill.source === "global").length
const projectCount = availableSkills.filter((skill) => skill.source === "project").length
const apiConfig = config.services.stateManager.getApiConfiguration()
const currentMode = config.services.stateManager.getGlobalSettingsKey("mode")
const provider = currentMode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider
// Show tool message
const message = JSON.stringify({ tool: "useSkill", path: skillName })
await config.callbacks.say("tool", message, undefined, undefined, false)
@@ -62,6 +70,20 @@ export class UseSkillToolHandler implements IToolHandler, IPartialBlockHandler {
return `Error: Skill "${skillName}" not found. Available skills: ${availableNames || "none"}`
}
telemetryService.safeCapture(
() =>
telemetryService.captureSkillUsed({
ulid: config.ulid,
skillName,
skillSource: skillContent.source === "global" ? "global" : "project",
skillsAvailableGlobal: globalCount,
skillsAvailableProject: projectCount,
provider,
modelId: config.api.getModel().id,
}),
"UseSkillToolHandler.execute",
)
return `# Skill "${skillContent.name}" is now active
${skillContent.instructions}
@@ -373,7 +373,6 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
block,
config.taskState.userMessageContent,
ToolDisplayUtils.getToolDescription,
config.api,
config.coordinator,
config.taskState.toolUseIdMap,
)
@@ -449,7 +448,6 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
block,
config.taskState.userMessageContent,
ToolDisplayUtils.getToolDescription,
config.api,
config.coordinator,
config.taskState.toolUseIdMap,
)
+13 -2
View File
@@ -84,7 +84,14 @@ export interface TaskServices {
* All callback functions available to tool handlers
*/
export interface TaskCallbacks {
say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean) => Promise<number | undefined>
say: (
type: ClineSay,
text?: string,
images?: string[],
files?: string[],
partial?: boolean,
call_id?: string,
) => Promise<number | undefined>
ask: (
type: ClineAsk,
@@ -101,7 +108,11 @@ export interface TaskCallbacks {
sayAndCreateMissingParamError: (toolName: ClineDefaultTool, paramName: string, relPath?: string) => Promise<any>
removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>
removeLastPartialMessageIfExistsWithType: (
type: "ask" | "say",
askOrSay: ClineAsk | ClineSay,
call_id?: string,
) => Promise<void>
executeCommandTool: (command: string, timeoutSeconds: number | undefined) => Promise<[boolean, any]>
+18 -5
View File
@@ -12,7 +12,14 @@ import type { TaskConfig } from "./TaskConfig"
*/
export interface StronglyTypedUIHelpers {
// Core UI methods
say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean) => Promise<number | undefined>
say: (
type: ClineSay,
text?: string,
images?: string[],
files?: string[],
partial?: boolean,
call_id?: string,
) => Promise<number | undefined>
ask: (
type: ClineAsk,
@@ -27,7 +34,11 @@ export interface StronglyTypedUIHelpers {
// Utility methods
removeClosingTag: (block: ToolUse, tag: ToolParamName, text?: string) => string
removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>
removeLastPartialMessageIfExistsWithType: (
type: "ask" | "say",
askOrSay: ClineAsk | ClineSay,
call_id?: string,
) => Promise<void>
// Approval methods
shouldAutoApproveTool: (toolName: ClineDefaultTool) => boolean | [boolean, boolean]
@@ -45,12 +56,14 @@ export interface StronglyTypedUIHelpers {
/**
* Creates strongly-typed UI helpers from a TaskConfig
*/
export function createUIHelpers(config: TaskConfig): StronglyTypedUIHelpers {
export function createUIHelpers(config: TaskConfig, callId?: string): StronglyTypedUIHelpers {
return {
say: config.callbacks.say,
say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean, call_id?: string) =>
config.callbacks.say(type, text, images, files, partial, call_id ?? callId),
ask: config.callbacks.ask,
removeClosingTag: (block: ToolUse, tag: ToolParamName, text?: string) => removeClosingTag(block, tag, text),
removeLastPartialMessageIfExistsWithType: config.callbacks.removeLastPartialMessageIfExistsWithType,
removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay, call_id?: string) =>
config.callbacks.removeLastPartialMessageIfExistsWithType(type, askOrSay, call_id ?? callId),
shouldAutoApproveTool: (toolName: ClineDefaultTool) => config.autoApprover.shouldAutoApproveTool(toolName),
shouldAutoApproveToolWithPath: config.callbacks.shouldAutoApproveToolWithPath,
askApproval: async (messageType: ClineAsk, message: string): Promise<boolean> => {
@@ -1,4 +1,3 @@
import { ApiHandler } from "@core/api"
import { ToolUse } from "@core/assistant-message"
import { formatResponse } from "@core/prompts/responses"
import { ToolResponse } from "@core/task"
@@ -20,7 +19,6 @@ export class ToolResultUtils {
block: ToolUse,
userMessageContent: any[],
toolDescription: (block: ToolUse) => string,
_api: ApiHandler,
coordinator?: ToolExecutorCoordinator,
toolUseIdMap?: Map<string, string>,
): void {
+28 -7
View File
@@ -21,6 +21,7 @@ import { telemetryService } from "@services/telemetry"
import { findLastIndex } from "@shared/array"
import { ClineToolResponseContent } from "@shared/messages"
import { orchestrateCommandExecution } from "./CommandOrchestrator"
import { orchestrateConcurrentCommandExecution } from "./ConcurrentCommandOrchestrator"
import { StandaloneTerminalManager } from "./standalone/StandaloneTerminalManager"
import type {
CommandExecutorCallbacks,
@@ -51,6 +52,9 @@ export class CommandExecutor {
// Flag to track if the current command was cancelled externally
private wasCancelledExternally = false
// Track if we're currently in parallel execution mode
private isParallelExecution = false
// Track shell integration warnings to determine when to show background terminal suggestion
private shellIntegrationWarningTracker: ShellIntegrationWarningTracker = {
timestamps: [],
@@ -89,6 +93,14 @@ export class CommandExecutor {
}
}
/**
* Set whether we're in parallel execution mode.
* In parallel mode, commands don't use ask() to wait for user input on each output chunk.
*/
setParallelExecution(isParallel: boolean): void {
this.isParallelExecution = isParallel
}
/**
* Execute a command in the terminal.
*
@@ -97,6 +109,9 @@ export class CommandExecutor {
* This ensures subagents run in hidden terminals, not cluttering the user's VSCode terminal
* 2. Regular commands Use the configured terminal manager based on terminalExecutionMode
*
* In parallel execution mode, uses ConcurrentCommandOrchestrator which streams output
* via say() instead of ask(), avoiding conflicts with multiple concurrent commands.
*
* @param command The command to execute
* @param timeoutSeconds Optional timeout in seconds
* @returns [userRejected, result] tuple
@@ -136,20 +151,26 @@ export class CommandExecutor {
process.once("completed", clearCurrentProcess)
process.once("error", clearCurrentProcess)
// Choose orchestrator based on execution mode
// In parallel mode, use ConcurrentCommandOrchestrator to avoid ask() conflicts
const orchestrator = this.isParallelExecution ? orchestrateConcurrentCommandExecution : orchestrateCommandExecution
// Use shared orchestration logic
// The StandaloneTerminalManager handles background command tracking internally
const result = await orchestrateCommandExecution(process, manager, this.callbacks, {
const result = await orchestrator(process, manager, this.callbacks, {
command,
timeoutSeconds,
// When "Proceed While Running" is triggered, track the command in the manager
// Returns the log file path so the orchestrator can send it to the UI
// existingOutput contains all output lines captured so far
onProceedWhileRunning: useStandalone
? (existingOutput: string[]) => {
const backgroundCmd = this.standaloneManager.trackBackgroundCommand(process, command, existingOutput)
return { logFilePath: backgroundCmd.logFilePath }
}
: undefined,
// (Not used in concurrent mode, but kept for compatibility)
onProceedWhileRunning:
useStandalone && !this.isParallelExecution
? (existingOutput: string[]) => {
const backgroundCmd = this.standaloneManager.trackBackgroundCommand(process, command, existingOutput)
return { logFilePath: backgroundCmd.logFilePath }
}
: undefined,
showShellIntegrationSuggestion: this.shouldShowBackgroundTerminalSuggestion(),
terminalType: useStandalone ? "standalone" : "vscode",
})
@@ -198,8 +198,16 @@ export async function orchestrateCommandExecution(
await flushBuffer()
}
}
} catch {
Logger.error("Error while asking for command output")
} catch (error) {
// Handle ask being interrupted by concurrent commands
// When multiple commands run in parallel, some asks may be ignored
// This is expected behavior - just proceed with execution
if (error instanceof Error && error.message.includes("ask promise was ignored")) {
// Silently proceed - this command's ask was superseded by another concurrent command
didContinue = true
} else {
Logger.error("Error while asking for command output", error)
}
} finally {
// Clear the stuck timer
if (bufferStuckTimer) {
@@ -0,0 +1,258 @@
/**
* ConcurrentCommandOrchestrator - Orchestration for parallel command execution.
*
* Unlike CommandOrchestrator which is designed for single commands,
* this orchestrator handles multiple commands running in parallel without
* calling ask() for each one (which would cause conflicts when parallel commands
* try to ask at the same time).
*
* Key differences from CommandOrchestrator:
* - Does NOT call ask() - output is delivered via say() directly
* - Buffers output and streams it after command completion
* - No "Proceed While Running" button (not needed for parallel execution)
* - Handles concurrent output from multiple commands safely
*/
import { Logger } from "@services/logging/Logger"
import * as fs from "fs"
import * as os from "os"
import * as path from "path"
import {
CHUNK_BYTE_SIZE,
CHUNK_DEBOUNCE_MS,
CHUNK_LINE_COUNT,
COMPLETION_TIMEOUT_MS,
MAX_BYTES_BEFORE_FILE,
MAX_LINES_BEFORE_FILE,
} from "./constants"
import type {
CommandExecutorCallbacks,
ITerminalManager,
OrchestrationOptions,
OrchestrationResult,
TerminalProcessResultPromise,
} from "./types"
/**
* Orchestrate concurrent command execution without interactive asks.
* Multiple commands can run in parallel without trying to ask for each one's output.
*/
export async function orchestrateConcurrentCommandExecution(
process: TerminalProcessResultPromise,
terminalManager: ITerminalManager,
callbacks: CommandExecutorCallbacks,
options: OrchestrationOptions,
): Promise<OrchestrationResult> {
const { timeoutSeconds, onOutputLine, terminalType = "vscode" } = options
// Track command execution state
callbacks.updateBackgroundCommandState(true)
const clearCommandState = async () => {
callbacks.updateBackgroundCommandState(false)
// Mark the command message as completed
const clineMessages = callbacks.getClineMessages()
const findLastIndex = (arr: any[], predicate: (item: any) => boolean) => {
for (let i = arr.length - 1; i >= 0; i--) {
if (predicate(arr[i])) return i
}
return -1
}
const lastCommandIndex = findLastIndex(clineMessages, (m) => m.ask === "command" || m.say === "command")
if (lastCommandIndex !== -1) {
await callbacks.updateClineMessage(lastCommandIndex, {
commandCompleted: true,
})
}
}
process.once("completed", clearCommandState)
process.once("error", clearCommandState)
process.catch(() => {
clearCommandState()
})
// Accumulated output lines
const outputLines: string[] = []
let outputBuffer: string[] = []
let outputBufferSize: number = 0
let chunkTimer: NodeJS.Timeout | null = null
let completionTimer: NodeJS.Timeout | null = null
// Large output file-based logging state
let isWritingToFile = false
let largeOutputLogPath: string | null = null
let largeOutputLogStream: fs.WriteStream | null = null
let totalOutputBytes = 0
let totalLineCount = 0
const scheduleFlush = () => {
if (chunkTimer) {
clearTimeout(chunkTimer)
}
chunkTimer = setTimeout(async () => await flushBuffer(), CHUNK_DEBOUNCE_MS)
}
const flushBuffer = async (force = false) => {
if (outputBuffer.length === 0 && !force) {
return
}
const chunk = outputBuffer.join("\n")
outputBuffer = []
outputBufferSize = 0
if (chunk) {
// In concurrent mode, we use say() directly without ask()
// This avoids conflicts when multiple commands output simultaneously
await callbacks.say("command_output", chunk)
}
}
const switchToFileBased = async () => {
if (isWritingToFile) return
isWritingToFile = true
// Flush any pending buffer to UI
if (outputBuffer.length > 0) {
const chunk = outputBuffer.join("\n")
outputBuffer = []
outputBufferSize = 0
await callbacks.say("command_output", chunk)
}
// Clear any pending flush timer
if (chunkTimer) {
clearTimeout(chunkTimer)
chunkTimer = null
}
// Set up file logging
largeOutputLogPath = path.join(os.tmpdir(), `cline-large-output-${Date.now()}.log`)
largeOutputLogStream = fs.createWriteStream(largeOutputLogPath, { flags: "a" })
// Write all existing lines to file
if (outputLines.length > 0) {
largeOutputLogStream.write(outputLines.join("\n") + "\n")
}
// Notify user
await callbacks.say(
"command_output",
`\n📋 Output is large (${outputLines.length} lines, ${Math.round(totalOutputBytes / 1024)}KB). Writing to: ${largeOutputLogPath}`,
)
}
const processLine = async (line: string) => {
outputLines.push(line)
totalLineCount++
totalOutputBytes += line.length + 1
// Check if we need to switch to file-based logging
if (totalLineCount > MAX_LINES_BEFORE_FILE || totalOutputBytes > MAX_BYTES_BEFORE_FILE) {
await switchToFileBased()
}
// If file-based logging is enabled, write to file
if (isWritingToFile && largeOutputLogStream) {
largeOutputLogStream.write(line + "\n")
} else {
// Otherwise buffer for UI delivery
outputBuffer.push(line)
outputBufferSize += line.length + 1
// Flush when buffer is full
if (outputBuffer.length >= CHUNK_LINE_COUNT || outputBufferSize >= CHUNK_BYTE_SIZE) {
if (chunkTimer) {
clearTimeout(chunkTimer)
chunkTimer = null
}
await flushBuffer()
} else {
scheduleFlush()
}
}
// Call the line output handler if provided
if (onOutputLine) {
onOutputLine(line)
}
}
const completionHandler = async () => {
// Clear timers
if (chunkTimer) {
clearTimeout(chunkTimer)
chunkTimer = null
}
if (completionTimer) {
clearTimeout(completionTimer)
completionTimer = null
}
// Final flush
await flushBuffer(true)
// Close file stream if open
if (largeOutputLogStream) {
largeOutputLogStream.end()
largeOutputLogStream = null
}
}
// Set up completion timeout
completionTimer = setTimeout(async () => {
await completionHandler()
}, COMPLETION_TIMEOUT_MS)
try {
// Listen for output lines
process.on("line", async (line: string) => {
await processLine(line)
})
// Wait for process to complete
const result = await process
await completionHandler()
// Process final output
const terminalOutput = terminalManager.processOutput(outputLines)
return {
userRejected: false,
result: terminalOutput,
completed: true,
outputLines,
}
} catch (error) {
await completionHandler()
if (error instanceof Error) {
Logger.error(`Concurrent command execution error: ${error.message}`)
if (largeOutputLogPath) {
return {
userRejected: false,
result: `Error: ${error.message}\n\nOutput was logged to: ${largeOutputLogPath}`,
completed: true,
outputLines,
}
}
return {
userRejected: false,
result: `Error: ${error.message}`,
completed: true,
outputLines,
}
}
return {
userRejected: false,
result: "Unknown error occurred",
completed: true,
outputLines,
}
}
}
+7 -1
View File
@@ -14,7 +14,7 @@ export const ClineHeaders = {
} as const
export type ClineHeaderName = (typeof ClineHeaders)[keyof typeof ClineHeaders]
export async function buildClineExtraHeaders(): Promise<Record<string, string>> {
export async function buildBasicClineHeaders(): Promise<Record<string, string>> {
const headers: Record<string, string> = {}
try {
const host = await HostProvider.env.getHostVersion(EmptyRequest.create({}))
@@ -31,6 +31,12 @@ export async function buildClineExtraHeaders(): Promise<Record<string, string>>
}
headers[ClineHeaders.CORE_VERSION] = ExtensionRegistryInfo.version
return headers
}
export async function buildClineExtraHeaders(): Promise<Record<string, string>> {
const headers = await buildBasicClineHeaders()
try {
const isMultiRoot = await isMultiRootWorkspace()
headers[ClineHeaders.IS_MULTIROOT] = isMultiRoot ? "true" : "false"

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