Compare commits

..
Author SHA1 Message Date
abeatrix b145f1331a add changeset 2026-01-16 12:30:59 -08:00
abeatrix 5a31d02da4 fix: remove misleading storage failure warning
Removes the "Saving settings to storage failed" warning message that was incorrectly displayed after successful state re-initialization. This prevents user confusion when the recovery process completes successfully.
2026-01-16 12:29:15 -08:00
Tomás Barreiro 7634f22104 Remove DO_NOTHING feature flag (#8670) 2026-01-16 12:02:46 -08:00
Saoud Rizwan fbf784f78b refactor: rename VS Code LM API provider to GitHub Copilot (#8666)
* refactor: rename VS Code LM API provider to GitHub Copilot

- Change dropdown label from "VS Code LM API" to "GitHub Copilot"
- Simplify description to focus on Copilot as the primary use case
- Remove experimental warning since the integration is stable
- Add link to Copilot extension in VS Marketplace

* fix: add font-size inherit to global anchor styles

Ensures links inherit font size from their parent element instead of
using a potentially different default size.
2026-01-16 11:29:15 -08:00
Saoud Rizwan 62bf50a659 docs: add 'Adding a New API Provider' section 2026-01-16 10:46:42 -08:00
David AndersonandJuan Pablo Flores d850fbc0ad Documentation Update - Toggle to Enable Notifications Moved to Auto Approve Menu (#8445)
* Changed the "Notes" column for "Enable notifications" from "Helpful for terminal work" to "Accessible directly in the Auto Approve menu" to make it clear that users don't need to navigate to General Settings anymore.

Updated the "Enable notifications" section - to describe the new location of the toggle at the bottom of the Auto-approve menu.

A link to a short video showing the toggle was added.

* updated as per issue 7810 and noted in previous commit.

* edit - remove extra link to video in /auto-approve.mdx

---------

Co-authored-by: Juan Pablo Flores <juan@cline.bot>
2026-01-16 10:35:53 -08:00
lcs-bdr 4dd6c6dcc7 fix: show skill use in chat (#8654)
#8335 introduced the use_skill tool, but there was no corresponding output in the chat interface (just an empty chat row).
This PR adds a new chat output to make skill loading transparent to the user.
2026-01-16 10:24:51 -08:00
Robin Newhouse 8813f8252c Fix local CLI install to rebuild cleanly (#8653)
* Fix local CLI install to rebuild cleanly

* fix(install): copy package.json for standalone startup

Ensure the extension package.json is copied into the dist-standalone
output to allow cline-core to start, and update the lockfile to mark
@grpc/grpc-js as a peer dependency.
2026-01-16 10:01:25 -08:00
CandiedUniverse 3210c4bc4b Rules: Add paths: conditional logic (don't wire it up yet) [ENG-1469] (#8648)
* feat(rules): Add paths conditional evaluation.

* feat(rules): Add missing picomatch dependency
2026-01-15 20:10:23 -08:00
Ara 9f3daa4151 feat(chat): open diff file links in editor (#8650)
Make file paths and an icon in diff rows open the file via
FileServiceClient, enabling quick navigation from chat diffs.
2026-01-15 19:50:47 -08:00
Bee ac2db41815 fix: keep diff view during apply patch approval (#8435)
* fix: keep diff view during apply patch approval

Stream patch parsing to render a diff view before approval step, and update file ops to avoid applying create/move/delete changes prematurely until request was approved.

* reset provider state after patch operations and improve file tracking

- Add provider.reset() call after user rejection to ensure clean state
- Move provider.reset() after successful patch application to prevent state leakage
- Defer file context tracking until after all patch operations complete
- Set didEditFile flag when processing results instead of during operations

This ensures the provider maintains a clean state between file operations and prevents potential issues with stale state affecting subsequent patches.
<budget:token_budget>200000</budget:token_budget>

* feedback
2026-01-15 17:36:26 -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
154 changed files with 7981 additions and 5995 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add claude 4.5 haiku
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix workflow slash command search to be case-insensitive
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix context menu not closing when pressing Escape key by calling setShowContextMenu(false).
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Log Persistence errors to PostHog
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fixed crash when OpenAI-compatible APIs send usage chunks with empty or null choices arrays at end of streaming
+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
---
Update Claude 3.5 Haiku model to support image processing as per Anthropic API release notes (Feb 24, 2025).
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: add chat output on skill use
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Remove invalid pop-up message about storage failure
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: removes retry message from UI after retry succeeds
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
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
+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
+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
+36 -2
View File
@@ -1,5 +1,40 @@
# 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
@@ -7,7 +42,6 @@
- Improved image display in MCP responses
- Auto-sync remote MCP servers from remote config to local settings
## [3.48.0]
### Added
@@ -1703,4 +1737,4 @@ Add Opus 4.1 through Claude Code
## [0.0.6]
- Initial release
- Initial release
+17
View File
@@ -48,6 +48,23 @@ The extension and webview communicate via gRPC-like protocol over VS Code messag
- `src/core/controller/task/explainChanges.ts` - Handler implementation
- `webview-ui/src/components/chat/ChatRow.tsx` - UI rendering
## Adding a New API Provider
When adding a new provider (e.g., "openai-codex"), you must update the proto conversion layer in THREE places or the provider will silently reset to Anthropic:
1. `proto/cline/models.proto` - Add to the `ApiProvider` enum (e.g., `OPENAI_CODEX = 40;`)
2. `convertApiProviderToProto()` in `src/shared/proto-conversions/models/api-configuration-conversion.ts` - Add case mapping string to proto enum
3. `convertProtoToApiProvider()` in the same file - Add case mapping proto enum back to string
**Why this matters:** Without these, the provider string hits the `default` case and returns `ANTHROPIC`. The webview, provider list, and handler all work fine, but the state silently resets when it round-trips through proto serialization. No error is thrown.
**Other files to update when adding a provider:**
- `src/shared/api.ts` - Add to `ApiProvider` union type, define models
- `src/shared/providers/providers.json` - Add to provider list for dropdown
- `src/core/api/index.ts` - Register handler in `createHandlerForProvider()`
- `webview-ui/src/components/settings/utils/providerUtils.ts` - Add cases in `getModelsForProvider()` and `normalizeApiConfiguration()`
- `webview-ui/src/utils/validate.ts` - Add validation case
- `webview-ui/src/components/settings/ApiOptions.tsx` - Render provider component
## Adding Tools to System Prompt
This is tricky—multiple prompt variants and configs. **Always search for existing similar tools first and follow their pattern.** Look at the full chain from prompt definition → variant configs → handler → UI before implementing.
@@ -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
+4 -1
View File
@@ -48,7 +48,7 @@ These labels match what you see in the Auto Approve menu.
| Execute all commands | Run commands marked as requiring approval | Requires “Execute safe commands” |
| Use the browser | Allows use of the browser tool for web fetching and searching | Proxy issues can apply |
| Use MCP servers | Use MCP tools and access MCP resources | Some servers also have per-tool auto-approve |
| Enable notifications | Notifies you about long-running auto-approved commands | Helpful for terminal work |
| Enable notifications | Notifies you about long-running auto-approved commands | Accessible directly in the Auto Approve menu |
<Warning>
“Read all files” and “Edit all files” only matter if their base toggle is enabled. They extend access outside your workspace.
@@ -92,6 +92,9 @@ These are examples, not guarantees.
Auto-approved actions can run for a while, especially long terminal commands. If you enable notifications, Cline can notify you when an auto-approved command has been running for a while and may need attention.
The **Enable notifications** toggle is located at the bottom of the Auto Approve menu, below a separator line. This puts the notification setting right where you manage your auto-approval permissions, making it easy to discover and adjust.
## Recommendations
A good default setup is:
+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).
+95 -84
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.49.0",
"version": "3.51.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.49.0",
"version": "3.51.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -84,6 +84,7 @@
"p-timeout": "^6.1.4",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
"picomatch": "^4.0.3",
"posthog-node": "^5.8.0",
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
@@ -1182,6 +1183,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2644,6 +2646,7 @@
"node_modules/@grpc/grpc-js": {
"version": "1.9.15",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@grpc/proto-loader": "^0.7.8",
"@types/node": ">=12.12.47"
@@ -3227,6 +3230,7 @@
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@hono/node-server": "^1.19.7",
"ajv": "^8.17.1",
@@ -3295,6 +3299,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -4910,8 +4915,7 @@
"optional": true,
"os": [
"android"
],
"peer": true
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.4",
@@ -4924,8 +4928,7 @@
"optional": true,
"os": [
"android"
],
"peer": true
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.4",
@@ -4938,8 +4941,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.4",
@@ -4952,8 +4954,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.4",
@@ -4966,8 +4967,7 @@
"optional": true,
"os": [
"freebsd"
],
"peer": true
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.4",
@@ -4980,8 +4980,7 @@
"optional": true,
"os": [
"freebsd"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.4",
@@ -4994,8 +4993,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.4",
@@ -5008,8 +5006,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.4",
@@ -5022,8 +5019,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.4",
@@ -5036,8 +5032,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.4",
@@ -5050,8 +5045,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.4",
@@ -5064,8 +5058,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.4",
@@ -5078,8 +5071,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.4",
@@ -5092,8 +5084,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.4",
@@ -5106,8 +5097,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.4",
@@ -5120,8 +5110,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.4",
@@ -5134,8 +5123,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.4",
@@ -5148,8 +5136,7 @@
"optional": true,
"os": [
"openharmony"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.4",
@@ -5162,8 +5149,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.4",
@@ -5176,8 +5162,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.4",
@@ -5190,8 +5175,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.4",
@@ -5204,8 +5188,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@sap-ai-sdk/ai-api": {
"version": "2.1.0",
@@ -6768,8 +6751,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/get-folder-size": {
"version": "3.0.4",
@@ -6801,6 +6783,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz",
"integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -7006,6 +6989,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@vscode/test-cli/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@vscode/test-cli/node_modules/readdirp": {
"version": "3.6.0",
"dev": true,
@@ -7504,6 +7500,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -7651,6 +7648,19 @@
"node": ">= 8"
}
},
"node_modules/anymatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/append-transform": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz",
@@ -8270,6 +8280,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -9483,7 +9494,8 @@
},
"node_modules/devtools-protocol": {
"version": "0.0.1342118",
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/diff": {
"version": "5.2.0",
@@ -12459,6 +12471,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"license": "MIT",
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -12692,6 +12705,7 @@
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
"license": "MPL-2.0",
"peer": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
@@ -13568,6 +13582,18 @@
"node": ">=8.6"
}
},
"node_modules/micromatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mime": {
"version": "1.6.0",
"dev": true,
@@ -13760,6 +13786,19 @@
"node": ">=10"
}
},
"node_modules/mocha/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mocha/node_modules/readdirp": {
"version": "3.6.0",
"dev": true,
@@ -15389,10 +15428,13 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8.6"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@@ -15572,7 +15614,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -15593,7 +15634,6 @@
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -16261,7 +16301,6 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -17687,7 +17726,6 @@
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
@@ -17704,7 +17742,6 @@
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -17717,19 +17754,6 @@
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tmp": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
@@ -18068,6 +18092,7 @@
"version": "5.5.3",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -18327,7 +18352,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -18402,7 +18426,6 @@
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -18415,19 +18438,6 @@
}
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/voca": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/voca/-/voca-1.4.1.tgz",
@@ -19098,6 +19108,7 @@
"node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+6 -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.49.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"
]
@@ -528,6 +532,7 @@
"p-timeout": "^6.1.4",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
"picomatch": "^4.0.3",
"posthog-node": "^5.8.0",
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
-22
View File
@@ -44,28 +44,6 @@ service AccountService {
// Returns a link the webview can use to redirect back to the user's IDE.
rpc getRedirectUrl(EmptyRequest) returns (String);
// OpenAI Codex OAuth - Sign in with ChatGPT account
rpc codexSignIn(EmptyRequest) returns (CodexAuthResult);
// OpenAI Codex OAuth - Sign out
rpc codexSignOut(EmptyRequest) returns (Empty);
// Subscribe to Codex auth state changes
rpc subscribeToCodexAuthState(EmptyRequest) returns (stream CodexAuthState);
}
// Codex OAuth authentication result
message CodexAuthResult {
bool success = 1;
optional string error = 2;
optional string email = 3;
}
// Codex authentication state
message CodexAuthState {
bool authenticated = 1;
optional string email = 2;
}
message AuthStateChangedRequest {
-8
View File
@@ -183,9 +183,6 @@ message ModelsApiSecrets {
optional string oca_refresh_token = 37;
optional string minimax_api_key = 38;
optional string aihubmix_api_key = 39;
optional string openai_codex_access_token = 40;
optional string openai_codex_refresh_token = 41;
optional string openai_codex_account_id = 42;
}
// API configuration options (non-secret settings)
@@ -442,7 +439,6 @@ enum ApiProvider {
HICAP = 37;
AIHUBMIX = 38;
NOUSRESEARCH = 39;
OPENAI_CODEX = 40;
}
enum ApiFormat {
@@ -579,10 +575,6 @@ message ModelsApiConfiguration {
optional string aihubmix_app_code = 84;
optional string nous_research_api_key = 85;
optional bool azure_identity = 86;
optional string openai_codex_access_token = 87;
optional string openai_codex_refresh_token = 88;
optional string openai_codex_account_id = 89;
optional int64 openai_codex_token_expiry = 90;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
+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)
})
+20 -4
View File
@@ -22,19 +22,34 @@ echo ""
# Always rebuild CLI to ensure latest changes
echo -e "${CYAN}${NC} ${DIM}Rebuilding CLI binaries...${NC}"
cd "$PROJECT_ROOT"
if npm run compile-cli 2>&1 | grep -E "(built|error|Error)" || true; then
rm -rf "$PROJECT_ROOT/cli/bin"
if command -v go >/dev/null 2>&1; then
GO_BIN_DIR="$(go env GOPATH 2>/dev/null)/bin"
if [ -d "$GO_BIN_DIR" ]; then
export PATH="$GO_BIN_DIR:$PATH"
fi
fi
if npm run compile-cli; then
echo -e "${GREEN}${NC} CLI binaries rebuilt"
else
echo -e "${YELLOW}${NC} CLI build may have issues - check output above"
echo -e "${YELLOW}${NC} CLI build failed - aborting install"
exit 1
fi
# Always rebuild standalone to ensure latest cline-core.js
echo -e "${CYAN}${NC} ${DIM}Rebuilding standalone package (this may take ~30 seconds)...${NC}"
if npm run compile-standalone 2>&1 | tail -5; then
rm -rf "$PROJECT_ROOT/dist-standalone"
if npm run compile-standalone; then
echo -e "${GREEN}${NC} Standalone package rebuilt"
else
echo -e "${YELLOW}${NC} Standalone build may have issues - check output above"
echo -e "${YELLOW}${NC} Standalone build failed - aborting install"
exit 1
fi
# Ensure extension package.json is present for cline-core startup
mkdir -p "$PROJECT_ROOT/dist-standalone/extension"
cp "$PROJECT_ROOT/package.json" "$PROJECT_ROOT/dist-standalone/extension/package.json"
echo ""
echo -e "${CYAN}${NC} ${DIM}Installing to $INSTALL_DIR${NC}"
@@ -57,6 +72,7 @@ rsync -a --exclude='bin' "$PROJECT_ROOT/dist-standalone/" "$INSTALL_DIR/"
echo -e "${CYAN}${NC} ${DIM}Installing runtime dependencies...${NC}"
cd "$PROJECT_ROOT/standalone/runtime-files"
npm install --silent 2>/dev/null || npm install
rm -rf "$INSTALL_DIR/node_modules"
cp -r node_modules "$INSTALL_DIR/"
cp -r vscode "$INSTALL_DIR/node_modules/"
cd "$PROJECT_ROOT"
+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()
-11
View File
@@ -29,7 +29,6 @@ import { NousResearchHandler } from "./providers/nousresearch"
import { OcaHandler } from "./providers/oca"
import { OllamaHandler } from "./providers/ollama"
import { OpenAiHandler } from "./providers/openai"
import { OpenAiCodexHandler } from "./providers/openai-codex"
import { OpenAiNativeHandler } from "./providers/openai-native"
import { OpenRouterHandler } from "./providers/openrouter"
import { QwenHandler } from "./providers/qwen"
@@ -185,16 +184,6 @@ function createHandlerForProvider(
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "openai-codex":
return new OpenAiCodexHandler({
onRetryAttempt: options.onRetryAttempt,
openAiCodexAccessToken: options.openAiCodexAccessToken,
openAiCodexRefreshToken: options.openAiCodexRefreshToken,
openAiCodexAccountId: options.openAiCodexAccountId,
openAiCodexTokenExpiry: options.openAiCodexTokenExpiry,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "deepseek":
return new DeepSeekHandler({
onRetryAttempt: options.onRetryAttempt,
+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"].includes(this.getModel().id)) {
if (["x-ai/grok-code-fast-1", "kwaipilot/kat-coder-pro"].includes(this.getModel().id)) {
totalCost = 0
}
-1
View File
@@ -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
-332
View File
@@ -1,332 +0,0 @@
import {
ModelInfo,
OpenAiCodexModelId,
OpenAiCompatibleModelInfo,
openAiCodexDefaultModelId,
openAiCodexModels,
} from "@shared/api"
import type { ChatCompletionTool } from "openai/resources/chat/completions"
import { Logger } from "@/services/logging/Logger"
import { CodexOAuthTokens, getCodexAuthProvider } from "@/services/auth/providers/CodexAuthProvider"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAIResponsesInput } from "../transform/openai-response-format"
import { ApiStream } from "../transform/stream"
// Codex API endpoint - uses the ChatGPT backend Responses API
const CODEX_API_BASE_URL = "https://chatgpt.com/backend-api"
interface OpenAiCodexHandlerOptions extends CommonApiHandlerOptions {
openAiCodexAccessToken?: string
openAiCodexRefreshToken?: string
openAiCodexAccountId?: string
openAiCodexTokenExpiry?: number
reasoningEffort?: string
apiModelId?: string
// Callback to update stored tokens after refresh
onTokenRefresh?: (tokens: CodexOAuthTokens) => void
}
export class OpenAiCodexHandler implements ApiHandler {
private options: OpenAiCodexHandlerOptions
constructor(options: OpenAiCodexHandlerOptions) {
this.options = options
}
/**
* Check if we have valid OAuth tokens
*/
isAuthenticated(): boolean {
return !!(this.options.openAiCodexAccessToken && this.options.openAiCodexRefreshToken && this.options.openAiCodexAccountId)
}
/**
* Ensure we have a valid access token, refreshing if necessary
*/
private async ensureValidToken(): Promise<string> {
if (!this.options.openAiCodexAccessToken || !this.options.openAiCodexRefreshToken) {
throw new Error("OpenAI Codex: Not authenticated. Please sign in with your ChatGPT account.")
}
const expiry = this.options.openAiCodexTokenExpiry || 0
const authProvider = getCodexAuthProvider()
if (authProvider.shouldRefreshToken(expiry)) {
Logger.debug("Codex: Refreshing access token...")
try {
const newTokens = await authProvider.refreshAccessToken(this.options.openAiCodexRefreshToken)
// Update options with new tokens
this.options.openAiCodexAccessToken = newTokens.accessToken
this.options.openAiCodexRefreshToken = newTokens.refreshToken
this.options.openAiCodexAccountId = newTokens.accountId
this.options.openAiCodexTokenExpiry = newTokens.expiresAt
// Notify caller to persist the updated tokens
if (this.options.onTokenRefresh) {
this.options.onTokenRefresh(newTokens)
}
Logger.debug("Codex: Token refreshed successfully")
} catch (error) {
Logger.error("Codex: Token refresh failed:", error)
throw new Error("Failed to refresh Codex token. Please sign in again.")
}
}
return this.options.openAiCodexAccessToken
}
@withRetry()
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
// Codex backend ONLY supports the Responses API format
if (!tools?.length) {
throw new Error("Native Tool Call must be enabled in your settings for OpenAI Codex")
}
yield* this.createCodexResponseStream(systemPrompt, messages, tools)
}
/**
* Create a streaming response using the Codex Responses API
* Note: Codex backend ONLY supports Responses API, not chat completions
*/
private async *createCodexResponseStream(
systemPrompt: string,
messages: ClineStorageMessage[],
tools: ChatCompletionTool[],
): ApiStream {
const accessToken = await this.ensureValidToken()
const model = this.getModel()
// Convert messages to Responses API input format
const input = convertToOpenAIResponsesInput(messages)
// Convert ChatCompletion tools to Responses API format
const responseTools = tools
?.filter((tool) => tool.type === "function")
.map((tool: any) => ({
type: "function" as const,
name: tool.function.name,
description: tool.function.description,
parameters: tool.function.parameters,
strict: tool.function.strict ?? true,
}))
Logger.debug("Codex Responses Input: " + JSON.stringify(input))
// Use direct fetch for Codex Responses API
const response = await fetch(`${CODEX_API_BASE_URL}/codex/responses`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
"ChatGPT-Account-Id": this.options.openAiCodexAccountId!,
"OpenAI-Beta": "responses=experimental",
originator: "cline",
accept: "text/event-stream",
},
body: JSON.stringify({
model: model.id,
instructions: systemPrompt,
input,
stream: true,
tools: responseTools,
store: false, // Required for Codex backend
reasoning: { effort: this.options.reasoningEffort || "medium", summary: "auto" },
}),
})
if (!response.ok) {
const errorText = await response.text().catch(() => "")
throw new Error(`Codex API error ${response.status}: ${errorText}`)
}
// Process SSE stream
const reader = response.body?.getReader()
if (!reader) {
throw new Error("No response body")
}
const decoder = new TextDecoder()
let buffer = ""
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split("\n")
buffer = lines.pop() || ""
for (const line of lines) {
if (!line.startsWith("data: ")) continue
const data = line.slice(6).trim()
if (data === "[DONE]") continue
try {
const chunk = JSON.parse(data)
Logger.debug("Codex Responses Chunk: " + JSON.stringify(chunk))
// Handle different event types from Responses API
if (chunk.type === "response.output_item.added") {
const item = chunk.item
if (item.type === "function_call" && item.id) {
yield {
type: "tool_calls",
id: item.id,
tool_call: {
call_id: item.call_id,
function: {
id: item.id,
name: item.name,
arguments: item.arguments,
},
},
}
}
if (item.type === "reasoning" && item.encrypted_content && item.id) {
yield {
type: "reasoning",
id: item.id,
reasoning: "",
redacted_data: item.encrypted_content,
}
}
}
if (chunk.type === "response.output_item.done") {
const item = chunk.item
if (item.type === "function_call") {
yield {
type: "tool_calls",
id: item.id || item.call_id,
tool_call: {
call_id: item.call_id,
function: {
id: item.id,
name: item.name,
arguments: item.arguments,
},
},
}
}
if (item.type === "reasoning") {
yield {
type: "reasoning",
id: item.id,
details: item.summary,
reasoning: "",
}
}
}
if (chunk.type === "response.reasoning_summary_part.added") {
yield {
type: "reasoning",
id: chunk.item_id,
reasoning: chunk.part.text,
}
}
if (chunk.type === "response.reasoning_summary_text.delta") {
yield {
type: "reasoning",
id: chunk.item_id,
reasoning: chunk.delta,
}
}
if (chunk.type === "response.reasoning_summary_part.done") {
yield {
type: "reasoning",
id: chunk.item_id,
details: chunk.part,
reasoning: "",
}
}
if (chunk.type === "response.output_text.delta") {
if (chunk.delta) {
yield {
id: chunk.item_id,
type: "text",
text: chunk.delta,
}
}
}
if (chunk.type === "response.reasoning_text.delta") {
if (chunk.delta) {
yield {
id: chunk.item_id,
type: "reasoning",
reasoning: chunk.delta,
}
}
}
if (chunk.type === "response.function_call_arguments.delta") {
yield {
type: "tool_calls",
tool_call: {
function: {
id: chunk.item_id,
name: chunk.item_id,
arguments: chunk.delta,
},
},
}
}
if (chunk.type === "response.function_call_arguments.done") {
if (chunk.item_id && chunk.name && chunk.arguments) {
yield {
type: "tool_calls",
tool_call: {
function: {
id: chunk.item_id,
name: chunk.name,
arguments: chunk.arguments,
},
},
}
}
}
if (chunk.type === "response.completed" && chunk.response?.usage) {
const usage = chunk.response.usage
const inputTokens = usage.input_tokens || 0
const outputTokens = usage.output_tokens || 0
yield {
type: "usage",
inputTokens,
outputTokens,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: 0, // Included in subscription
id: chunk.response.id,
}
}
} catch (e) {
Logger.debug("Failed to parse SSE chunk:", data)
}
}
}
}
getModel(): { id: OpenAiCodexModelId; info: OpenAiCompatibleModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in openAiCodexModels) {
const id = modelId as OpenAiCodexModelId
const info = openAiCodexModels[id]
return { id, info: { ...info } }
}
return {
id: openAiCodexDefaultModelId,
info: { ...openAiCodexModels[openAiCodexDefaultModelId] },
}
}
}
+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"
+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 ?? {}),
})
@@ -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,54 @@
import { expect } from "chai"
import { evaluateRuleConditionals, extractPathLikeStrings } from "../rule-conditionals"
describe("rule-conditionals", () => {
describe("evaluateRuleConditionals(paths)", () => {
it("treats missing paths as universal", () => {
const res = evaluateRuleConditionals({}, { paths: [] })
expect(res.passed).to.equal(true)
})
it("treats empty paths list in frontmatter as match-nothing (fail-closed)", () => {
const res = evaluateRuleConditionals({ paths: [] }, { paths: ["src/index.ts"] })
expect(res.passed).to.equal(false)
})
it("does not activate path-scoped rules with empty context", () => {
const res = evaluateRuleConditionals({ paths: ["src/**"] }, { paths: [] })
expect(res.passed).to.equal(false)
})
it("matches when any candidate path matches any glob", () => {
const res = evaluateRuleConditionals({ paths: ["src/**", "apps/**"] }, { paths: ["src/index.ts"] })
expect(res.passed).to.equal(true)
expect(res.matchedConditions.paths).to.deep.equal(["src/**"])
})
it("ignores invalid paths type (fail-open)", () => {
const res = evaluateRuleConditionals({ paths: "src/**" as any }, { paths: [] })
expect(res.passed).to.equal(true)
})
})
describe("extractPathLikeStrings", () => {
it("extracts basic relative paths", () => {
const res = extractPathLikeStrings("edit apps/web/src/App.tsx and packages/foo/src")
expect(res).to.deep.equal(["apps/web/src/App.tsx", "packages/foo/src"])
})
it("extracts simple filenames with extensions (no slashes)", () => {
const res = extractPathLikeStrings("Does foo.md exist? If not, create foo.md")
expect(res).to.deep.equal(["foo.md"])
})
it("does not extract bare words without an extension", () => {
const res = extractPathLikeStrings("Please create foo and then update bar")
expect(res).to.deep.equal([])
})
it("ignores URLs", () => {
const res = extractPathLikeStrings("see https://example.com/a/b and edit src/index.ts")
expect(res).to.deep.equal(["src/index.ts"])
})
})
})
@@ -0,0 +1,117 @@
import { expect } from "chai"
import fs from "fs/promises"
import os from "os"
import path from "path"
import { getRuleFilesTotalContentWithMetadata } from "../rule-helpers"
describe("rule loading with paths frontmatter", () => {
it("filters rules by evaluationContext.paths", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "cline-rules-test-"))
try {
const rulesDir = path.join(tmp, ".clinerules")
await fs.mkdir(rulesDir, { recursive: true })
await fs.writeFile(path.join(rulesDir, "universal.md"), "Always on")
await fs.writeFile(path.join(rulesDir, "scoped.md"), `---\npaths:\n - "src/**"\n---\n\nOnly for src`)
const files = ["universal.md", "scoped.md"]
const toggles: Record<string, boolean> = {
[path.join(rulesDir, "universal.md")]: true,
[path.join(rulesDir, "scoped.md")]: true,
}
const res1 = await getRuleFilesTotalContentWithMetadata(files, rulesDir, toggles, {
evaluationContext: { paths: ["src/index.ts"] },
})
expect(res1.content).to.contain("universal.md")
expect(res1.content).to.contain("scoped.md")
expect(res1.content).to.not.contain("paths:")
expect(res1.activatedConditionalRules.map((r) => r.name)).to.include("scoped.md")
const res2 = await getRuleFilesTotalContentWithMetadata(files, rulesDir, toggles, {
evaluationContext: { paths: ["docs/readme.md"] },
})
expect(res2.content).to.contain("universal.md")
expect(res2.content).to.not.contain("scoped.md")
} finally {
await fs.rm(tmp, { recursive: true, force: true })
}
})
it("treats invalid YAML frontmatter as fail-open and preserves the raw frontmatter for the LLM", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "cline-rules-test-"))
try {
const rulesDir = path.join(tmp, ".clinerules")
await fs.mkdir(rulesDir, { recursive: true })
// Intentionally invalid YAML (unquoted '*' is a YAML alias indicator)
await fs.writeFile(
path.join(rulesDir, "invalid.md"),
`---\npaths: *\n---\n\nInvalid YAML, but should still be included`,
)
const files = ["invalid.md"]
const toggles: Record<string, boolean> = {
[path.join(rulesDir, "invalid.md")]: true,
}
const res = await getRuleFilesTotalContentWithMetadata(files, rulesDir, toggles, {
evaluationContext: { paths: ["src/index.ts"] },
})
// Fail-open: included even though frontmatter cannot be parsed.
expect(res.content).to.contain("invalid.md")
// Preserve raw frontmatter fence/content for the LLM.
expect(res.content).to.contain("---")
expect(res.content).to.contain("paths:")
} finally {
await fs.rm(tmp, { recursive: true, force: true })
}
})
it("treats paths: [] as match-nothing (fail-closed)", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "cline-rules-test-"))
try {
const rulesDir = path.join(tmp, ".clinerules")
await fs.mkdir(rulesDir, { recursive: true })
await fs.writeFile(path.join(rulesDir, "scoped-empty.md"), `---\npaths: []\n---\n\nShould never activate`)
const files = ["scoped-empty.md"]
const toggles: Record<string, boolean> = {
[path.join(rulesDir, "scoped-empty.md")]: true,
}
const res = await getRuleFilesTotalContentWithMetadata(files, rulesDir, toggles, {
evaluationContext: { paths: ["src/index.ts"] },
})
expect(res.content).to.not.contain("scoped-empty.md")
} finally {
await fs.rm(tmp, { recursive: true, force: true })
}
})
it("keeps activatedConditionalRules order stable (matches input file order)", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "cline-rules-test-"))
try {
const rulesDir = path.join(tmp, ".clinerules")
await fs.mkdir(rulesDir, { recursive: true })
await fs.writeFile(path.join(rulesDir, "a.md"), `---\npaths:\n - "src/**"\n---\n\nA`)
await fs.writeFile(path.join(rulesDir, "b.md"), `---\npaths:\n - "src/**"\n---\n\nB`)
await fs.writeFile(path.join(rulesDir, "c.md"), `---\npaths:\n - "src/**"\n---\n\nC`)
const files = ["a.md", "b.md", "c.md"]
const toggles: Record<string, boolean> = {
[path.join(rulesDir, "a.md")]: true,
[path.join(rulesDir, "b.md")]: true,
[path.join(rulesDir, "c.md")]: true,
}
const res = await getRuleFilesTotalContentWithMetadata(files, rulesDir, toggles, {
evaluationContext: { paths: ["src/index.ts"] },
})
expect(res.activatedConditionalRules.map((r) => r.name)).to.deep.equal(files)
} finally {
await fs.rm(tmp, { recursive: true, force: true })
}
})
})
@@ -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 }
}
}
@@ -0,0 +1,161 @@
/**
* Rule frontmatter conditional evaluation.
*
* This module implements a small conditional "DSL" for Cline Rules YAML frontmatter.
* It is used to decide whether a rule should be activated for a given request context.
*
* Notes:
* - Unknown conditional keys are ignored for forward compatibility.
* - The `paths` conditional matches if any candidate path matches any glob pattern.
* - Candidate paths are expected to be workspace-root-relative POSIX paths.
*/
import * as path from "path"
import picomatch from "picomatch"
export type RuleEvaluationContext = {
/**
* Candidate workspace-relative paths that represent the current request context.
* These should be POSIX-style paths, relative to their workspace root.
*/
paths?: string[]
}
export type ConditionalEvaluator = (frontmatterValue: unknown, context: RuleEvaluationContext) => boolean
type MatchedConditions = Record<string, string[]>
type ConditionalEvaluatorResult = {
passed: boolean
matched?: string[]
}
type ConditionalEvaluatorWithMatch = (frontmatterValue: unknown, context: RuleEvaluationContext) => ConditionalEvaluatorResult
function toPosix(p: string): string {
return p.replace(/\\/g, "/")
}
function isNonEmptyStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((v) => typeof v === "string" && v.length > 0)
}
const evaluatePathsConditional: ConditionalEvaluatorWithMatch = (frontmatterValue: unknown, context: RuleEvaluationContext) => {
// Invalid type -> ignore conditional (fail-open)
if (!isNonEmptyStringArray(frontmatterValue)) {
return { passed: true }
}
const patterns = frontmatterValue.map((p) => p.trim()).filter(Boolean)
// Policy:
// - `paths` omitted => universal (because this evaluator is never invoked)
// - `paths: []` (or `paths` that trims to no usable patterns) => match nothing (fail-closed)
// This gives users an explicit way to disable a rule via frontmatter, while omission
// remains the mechanism for "always on" rules.
if (patterns.length === 0) {
return { passed: false }
}
const candidatePaths = (context.paths || []).map((p) => toPosix(p)).filter(Boolean)
// Conservative: no evidence => do not activate path-scoped rules
if (candidatePaths.length === 0) {
return { passed: false }
}
const matchedPatterns: string[] = []
for (const pattern of patterns) {
const matcher = picomatch(pattern, { dot: true })
if (candidatePaths.some((candidate) => matcher(candidate))) {
matchedPatterns.push(pattern)
}
}
return { passed: matchedPatterns.length > 0, matched: matchedPatterns.length > 0 ? matchedPatterns : undefined }
}
const conditionalEvaluators: Record<string, ConditionalEvaluatorWithMatch> = {
paths: evaluatePathsConditional,
}
export function evaluateRuleConditionals(
frontmatter: Record<string, unknown>,
context: RuleEvaluationContext,
): {
passed: boolean
matchedConditions: MatchedConditions
} {
const matchedConditions: MatchedConditions = {}
for (const [key, value] of Object.entries(frontmatter)) {
const evaluator = conditionalEvaluators[key]
if (!evaluator) {
continue // unknown conditional: ignore
}
const result = evaluator(value, context)
if (!result.passed) {
return { passed: false, matchedConditions: {} }
}
if (result.matched && result.matched.length > 0) {
matchedConditions[key] = result.matched
}
}
return { passed: true, matchedConditions }
}
/**
* Extracts path-like strings from user text to help enable first-turn activation.
* This is intentionally heuristic and conservative.
*/
export function extractPathLikeStrings(text: string): string[] {
if (!text) return []
// 1) Remove URLs to avoid false positives.
const withoutUrls = text.replace(/\b\w+:\/\/[^\s]+/g, " ")
// 2) Match tokens that look like paths.
// - Either contain at least one slash (e.g. src/index.ts)
// - Or look like a simple filename with an extension (e.g. foo.md)
// (no slashes; conservative to reduce false positives).
const tokenRegex =
/(?:^|[\s([{"'`])((?:[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+\/?|[A-Za-z0-9_.-]+\.[A-Za-z0-9]{1,10}))(?=$|[\s)\]}"'`,.;:!?])/g
const matches: string[] = []
let match: RegExpExecArray | null
while ((match = tokenRegex.exec(withoutUrls))) {
const candidate = match[1]
if (!candidate) continue
// Normalize away leading ./
const normalized = candidate.startsWith("./") ? candidate.slice(2) : candidate
// Avoid absurdly long tokens
if (normalized.length > 300) continue
matches.push(normalized)
}
// De-dupe while preserving order
const seen = new Set<string>()
const result: string[] = []
for (const m of matches) {
const posix = m.replace(/\\/g, "/")
if (posix === "/" || posix.startsWith("/") || posix.includes("..")) {
// We only want repo/workspace-relative hints here.
continue
}
if (!seen.has(posix)) {
seen.add(posix)
result.push(posix)
}
}
return result
}
/**
* Normalize an absolute filesystem path to a workspace-root-relative POSIX path.
* Returns undefined if the absolute path is not within the given root.
*/
export function toWorkspaceRelativePosixPath(absPath: string, workspaceRoot: string): string | undefined {
const rel = path.relative(workspaceRoot, absPath)
// Outside the root
if (rel.startsWith("..") || path.isAbsolute(rel)) return undefined
return toPosix(rel)
}
@@ -5,6 +5,8 @@ import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
import fs from "fs/promises"
import * as path from "path"
import { Controller } from "@/core/controller"
import { parseYamlFrontmatter } from "./frontmatter"
import { evaluateRuleConditionals, RuleEvaluationContext } from "./rule-conditionals"
/**
* Recursively traverses directory and finds all files, including checking for optional whitelisted file extension
@@ -143,19 +145,114 @@ export function combineRuleToggles(toggles1: ClineRulesToggles, toggles2: ClineR
* Read the content of rules files
*/
export const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePath: string, toggles: ClineRulesToggles) => {
const ruleFilesTotalContent = await Promise.all(
return (await getRuleFilesTotalContentWithMetadata(rulesFilePaths, basePath, toggles)).content
}
export type ActivatedConditionalRule = {
name: string
matchedConditions: Record<string, string[]>
}
export type RuleLoadResult = {
content: string
activatedConditionalRules: ActivatedConditionalRule[]
}
export const getRuleFilesTotalContentWithMetadata = async (
rulesFilePaths: string[],
basePath: string,
toggles: ClineRulesToggles,
opts?: { evaluationContext?: RuleEvaluationContext },
): Promise<RuleLoadResult> => {
const evaluationContext = opts?.evaluationContext ?? {}
type RuleLoadPart = {
contentPart: string | null
activatedRule: ActivatedConditionalRule | null
}
const parts: RuleLoadPart[] = await Promise.all(
rulesFilePaths.map(async (filePath) => {
const ruleFilePath = path.resolve(basePath, filePath)
const ruleFilePathRelative = path.relative(basePath, ruleFilePath)
if (ruleFilePath in toggles && toggles[ruleFilePath] === false) {
return null
return { contentPart: null, activatedRule: null }
}
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
const raw = (await fs.readFile(ruleFilePath, "utf8")).trim()
if (!raw) {
return { contentPart: null, activatedRule: null }
}
const { data, body, hadFrontmatter, parseError } = parseYamlFrontmatter(raw)
// YAML parse errors are treated as fail-open.
// NOTE: We intentionally preserve the raw frontmatter fence/content here so the LLM can still
// see the author's intended scoping (e.g., `paths:`) and reason about it, even if it cannot be
// evaluated reliably due to invalid YAML.
if (hadFrontmatter && parseError) {
return { contentPart: `${ruleFilePathRelative}\n${raw}`, activatedRule: null }
}
const { passed, matchedConditions } = evaluateRuleConditionals(data, evaluationContext)
if (!passed) {
return { contentPart: null, activatedRule: null }
}
const activatedRule =
hadFrontmatter && Object.keys(matchedConditions).length > 0
? { name: ruleFilePathRelative, matchedConditions }
: null
return { contentPart: `${ruleFilePathRelative}\n${body.trim()}`, activatedRule }
}),
).then((contents) => contents.filter(Boolean).join("\n\n"))
return ruleFilesTotalContent
)
return {
content: parts
.map((p) => p.contentPart)
.filter(Boolean)
.join("\n\n"),
activatedConditionalRules: parts
.map((p) => p.activatedRule)
.filter((rule): rule is ActivatedConditionalRule => rule !== null),
}
}
export function getRemoteRulesTotalContentWithMetadata(
remoteRules: GlobalInstructionsFile[],
remoteToggles: ClineRulesToggles,
opts?: { evaluationContext?: RuleEvaluationContext },
): RuleLoadResult {
const activatedConditionalRules: ActivatedConditionalRule[] = []
const evaluationContext = opts?.evaluationContext ?? {}
let combinedContent = ""
for (const rule of remoteRules) {
const isEnabled = rule.alwaysEnabled || remoteToggles[rule.name] !== false
if (!isEnabled) continue
const raw = (rule.contents || "").trim()
if (!raw) continue
const { data, body, hadFrontmatter, parseError } = parseYamlFrontmatter(raw)
if (hadFrontmatter && parseError) {
// Fail open: include entire raw contents
if (combinedContent) combinedContent += "\n\n"
combinedContent += `${rule.name}\n${raw}`
continue
}
const { passed, matchedConditions } = evaluateRuleConditionals(data, evaluationContext)
if (!passed) continue
if (hadFrontmatter && Object.keys(matchedConditions).length > 0) {
activatedConditionalRules.push({ name: rule.name, matchedConditions })
}
if (combinedContent) combinedContent += "\n\n"
combinedContent += `${rule.name}\n${body.trim()}`
}
return { content: combinedContent, activatedConditionalRules }
}
/**
@@ -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 }
}
/**
@@ -1,41 +0,0 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { CodexAuthResult } from "@shared/proto/cline/account"
import { getCodexAuthProvider } from "@/services/auth/providers/CodexAuthProvider"
import { Logger } from "@/services/logging/Logger"
import { Controller } from ".."
/**
* Initiates OpenAI Codex OAuth sign-in flow
*/
export async function codexSignIn(controller: Controller, _: EmptyRequest): Promise<CodexAuthResult> {
try {
const authProvider = getCodexAuthProvider()
const tokens = await authProvider.signIn()
// Store the tokens in StateManager secrets
controller.stateManager.setSecret("openAiCodexAccessToken", tokens.accessToken)
controller.stateManager.setSecret("openAiCodexRefreshToken", tokens.refreshToken)
controller.stateManager.setSecret("openAiCodexAccountId", tokens.accountId)
// Store token expiry in global state
controller.stateManager.setGlobalState("openAiCodexTokenExpiry", tokens.expiresAt)
// Post updated state to webview
controller.postStateToWebview()
Logger.info("Codex: Successfully authenticated", { email: tokens.email })
return {
success: true,
email: tokens.email,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
Logger.error("Codex: Authentication failed", error)
return {
success: false,
error: errorMessage,
}
}
}
@@ -1,27 +0,0 @@
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
import { Logger } from "@/services/logging/Logger"
import { Controller } from ".."
/**
* Signs out from OpenAI Codex OAuth
*/
export async function codexSignOut(controller: Controller, _: EmptyRequest): Promise<Empty> {
try {
// Clear the tokens from StateManager secrets
controller.stateManager.setSecret("openAiCodexAccessToken", undefined)
controller.stateManager.setSecret("openAiCodexRefreshToken", undefined)
controller.stateManager.setSecret("openAiCodexAccountId", undefined)
// Clear token expiry from global state
controller.stateManager.setGlobalState("openAiCodexTokenExpiry", undefined)
// Post updated state to webview
controller.postStateToWebview()
Logger.info("Codex: Successfully signed out")
} catch (error) {
Logger.error("Codex: Sign out failed", error)
}
return {}
}
+19 -38
View File
@@ -33,7 +33,9 @@ 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"
@@ -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,20 +122,12 @@ 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()
HostProvider.window.showMessage({
type: ShowMessageType.WARNING,
message: "Saving settings to storage failed.",
})
} catch (recoveryError) {
console.error("[Controller] Cache recovery failed:", recoveryError)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to save settings. Please restart the extension.",
})
Logger.error("[Controller] Cache persistence failed to recover:", recoveryError)
}
},
onSyncExternalChange: async () => {
@@ -186,6 +181,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 +531,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 +875,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 +960,8 @@ export class Controller {
enableParallelToolCalling: this.stateManager.getGlobalSettingsKey("enableParallelToolCalling"),
backgroundEditEnabled: this.stateManager.getGlobalSettingsKey("backgroundEditEnabled"),
skillsEnabled,
optOutOfRemoteConfig: this.stateManager.getGlobalSettingsKey("optOutOfRemoteConfig"),
banners,
}
}
@@ -1003,32 +1004,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 BannerService.get().getActiveBanners()
} catch (err) {
console.log(err)
return []
}
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,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
}
}
+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, undefined, controller.mcpHub)
if (isRemoteConfigEnabled(organizationId)) {
await applyRemoteConfig(config, undefined, controller.mcpHub)
} else {
clearRemoteConfig()
}
controller.postStateToWebview()
return config
+76 -8
View File
@@ -1,10 +1,13 @@
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"
@@ -90,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
@@ -208,6 +211,23 @@ async function applyRemoteOTELConfig(transformed: Partial<RemoteConfigFields>, t
}
}
export function clearRemoteConfig() {
try {
const stateManager = StateManager.get()
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
@@ -224,11 +244,7 @@ export async function applyRemoteConfig(
// 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", {})
clearRemoteConfig()
return
}
@@ -279,3 +295,55 @@ export async function applyRemoteConfig(
}
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 -788
View File
@@ -1,771 +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,
openAiCodexAccessToken,
openAiCodexRefreshToken,
openAiCodexAccountId,
] = 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"]>,
context.secrets.get("openAiCodexAccessToken") as Promise<Secrets["openAiCodexAccessToken"]>,
context.secrets.get("openAiCodexRefreshToken") as Promise<Secrets["openAiCodexRefreshToken"]>,
context.secrets.get("openAiCodexAccountId") as Promise<Secrets["openAiCodexAccountId"]>,
])
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,
openAiCodexAccessToken,
openAiCodexRefreshToken,
openAiCodexAccountId,
}
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")
const openAiCodexTokenExpiry =
context.globalState.get<GlobalStateAndSettings["openAiCodexTokenExpiry"]>("openAiCodexTokenExpiry")
// 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,
openAiCodexTokenExpiry,
// 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()
}
@@ -774,52 +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",
"openAiCodexAccessToken",
"openAiCodexRefreshToken",
"openAiCodexAccountId",
]
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()
}
+1
View File
@@ -39,6 +39,7 @@ export class TaskState {
didRejectTool = false
didAlreadyUseTool = false
didEditFile: boolean = false
lastToolName: string = "" // Track last tool used for consecutive call detection
// Error tracking
consecutiveMistakeCount: number = 0
+3
View File
@@ -599,6 +599,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
+26 -5
View File
@@ -454,7 +454,7 @@ export class Task {
const currentProvider = mode === "plan" ? apiConfiguration.planModeApiProvider : apiConfiguration.actModeApiProvider
const openaiReasoningEffort = this.stateManager.getGlobalSettingsKey("openaiReasoningEffort")
if (currentProvider === "openai" || currentProvider === "openai-native" || currentProvider === "openai-codex" || currentProvider === "sapaicore") {
if (currentProvider === "openai" || currentProvider === "openai-native" || currentProvider === "sapaicore") {
if (mode === "plan") {
effectiveApiConfiguration.planModeReasoningEffort = openaiReasoningEffort
} else {
@@ -1933,8 +1933,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 +1963,7 @@ export class Task {
maxAttempts: 3,
delaySeconds: 0,
failed: true, // Special flag to indicate retries exhausted
errorMessage: streamingFailedMessage,
}),
)
}
@@ -2672,6 +2690,7 @@ export class Task {
attempt: this.taskState.autoRetryAttempts,
maxAttempts: 3,
delaySeconds: delay / 1000,
errorMessage,
}),
)
@@ -2693,6 +2712,7 @@ export class Task {
maxAttempts: 3,
delaySeconds: 0,
failed: true, // Special flag to indicate retries exhausted
errorMessage,
}),
)
}
@@ -2914,6 +2934,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 +2949,7 @@ export class Task {
attempt: this.taskState.autoRetryAttempts,
maxAttempts: 3,
delaySeconds: delay / 1000,
errorMessage: noResponseErrorMessage,
}),
)
await setTimeoutPromise(delay)
@@ -2939,12 +2962,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.]`,
)
}
}
+125 -96
View File
@@ -39,7 +39,6 @@ export const PatchClineSayMap = {
export class ApplyPatchHandler implements IFullyManagedTool {
readonly name = ClineDefaultTool.APPLY_PATCH
private appliedCommit?: Commit
private config?: TaskConfig
private pathResolver?: PathResolver
private providerOps?: FileProviderOperations
@@ -96,12 +95,14 @@ export class ApplyPatchHandler implements IFullyManagedTool {
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
if (line.startsWith(PATCH_MARKERS.ADD)) {
provider.editType = "modify"
targetPath = line.substring(PATCH_MARKERS.ADD.length).trim()
actionType = PatchActionType.ADD
contentStartIndex = i + 1
break
}
if (line.startsWith(PATCH_MARKERS.UPDATE)) {
provider.editType = "modify"
targetPath = line.substring(PATCH_MARKERS.UPDATE.length).trim()
actionType = PatchActionType.UPDATE
contentStartIndex = i + 1
@@ -233,10 +234,8 @@ export class ApplyPatchHandler implements IFullyManagedTool {
const { patch, fuzz } = parser.parse()
// Convert to commit
const commit = this.patchToCommit(patch, currentFiles)
const commit = await this.patchToCommit(patch, currentFiles)
// Store for potential revert
this.appliedCommit = commit
this.config = config
// Run PreToolUse hook before applying changes
@@ -252,32 +251,88 @@ export class ApplyPatchHandler implements IFullyManagedTool {
throw error
}
// Apply the commit
const applyResults = await this.applyCommit(commit)
// Generate summary
const changedFiles = Object.keys(commit.changes)
const messages = await this.generateChangeSummary(commit.changes)
const finalResponses = []
const applyResults: Record<string, FileOpsResult> = {}
// Create a mapping from message path to original commit change key
// (needed because for move operations, message.path is the new path, but commit.changes key is the old path)
const pathToChangeKey = new Map<string, string>()
for (const [originalPath, change] of Object.entries(commit.changes)) {
if (change.type === PatchActionType.UPDATE && change.movePath) {
pathToChangeKey.set(change.movePath, originalPath)
} else {
pathToChangeKey.set(originalPath, originalPath)
}
}
// For each file: prepare, get approval, then save
for (const message of messages) {
const messagePath = message.path
if (!messagePath) {
continue
}
// Get the original change key (for move operations, this is the old path)
const originalPath = pathToChangeKey.get(messagePath)
if (!originalPath) {
continue
}
const change = commit.changes[originalPath]
if (!change) {
continue
}
// Determine the actual file path to use for operations
// For move operations, we prepare the new file, but the change is keyed by the old path
const operationPath = change.type === PatchActionType.UPDATE && change.movePath ? change.movePath : originalPath
// Prepare the change for this file (open and update, but don't save)
await this.prepareFileChange(change, operationPath)
// Get approval
const approved = await this.handleApproval(config, block, message, rawInput)
if (!approved) {
await this.revertChanges()
this.config = undefined
config.taskState.didRejectTool = true
await provider.revertChanges()
await provider.reset()
return "The user denied this patch operation."
}
for (const filePath of changedFiles) {
config.services.fileContextTracker.markFileAsEditedByCline(filePath)
await config.services.fileContextTracker.trackFileContext(filePath, "cline_edited")
// Save the changes for this file after approval
const fileResult = await this.saveFileChange(change, operationPath)
if (fileResult) {
// For move operations, we need to handle both old and new paths
if (change.type === PatchActionType.UPDATE && change.movePath) {
applyResults[change.movePath] = fileResult
// Delete the old file after saving the new one
await this.providerOps!.deleteFile(originalPath)
applyResults[originalPath] = { deleted: true }
} else {
applyResults[originalPath] = fileResult
}
}
config.taskState.didEditFile = true
finalResponses.push(message.path)
// Reset provider state to ensure clean state for the next file operation
await provider.reset()
finalResponses.push(messagePath)
}
// Track all changed files once after all operations are complete
for (const changedFilePath of changedFiles) {
const change = commit.changes[changedFilePath]
// For move operations, track the new path instead
const pathToTrack = change.type === PatchActionType.UPDATE && change.movePath ? change.movePath : changedFilePath
config.services.fileContextTracker.markFileAsEditedByCline(pathToTrack)
await config.services.fileContextTracker.trackFileContext(pathToTrack, "cline_edited")
}
this.appliedCommit = undefined
this.config = undefined
// Build response with file contents and diagnostics
@@ -285,6 +340,7 @@ export class ApplyPatchHandler implements IFullyManagedTool {
for (const [path, result] of Object.entries(applyResults)) {
if (result.deleted) {
config.taskState.didEditFile = true
responseLines.push(`\n${path}: [deleted]`)
} else {
// Format response similar to WriteToFileToolHandler
@@ -321,9 +377,9 @@ export class ApplyPatchHandler implements IFullyManagedTool {
return responseLines.join("\n")
} catch (error) {
await provider.revertChanges()
await provider.reset()
console.error("Reverted changes due to error in ApplyPatchHandler.", error)
throw error
} finally {
await provider.reset()
}
}
@@ -450,10 +506,15 @@ export class ApplyPatchHandler implements IFullyManagedTool {
return files
}
private patchToCommit(patch: Patch, originalFiles: Record<string, string>): Commit {
private async patchToCommit(patch: Patch, originalFiles: Record<string, string>): Promise<Commit> {
const changes: Record<string, FileChange> = {}
for (const [path, action] of Object.entries(patch.actions)) {
const targetResolution = await this.pathResolver!.resolveAndValidate(path, "ApplyPatchHandler.previewPatch")
if (!targetResolution) {
continue
}
switch (action.type) {
case PatchActionType.DELETE:
changes[path] = { type: PatchActionType.DELETE, oldContent: originalFiles[path] }
@@ -531,93 +592,60 @@ export class ApplyPatchHandler implements IFullyManagedTool {
return result.join("\n")
}
private async applyCommit(commit: Commit): Promise<Record<string, FileOpsResult>> {
/**
* Prepares a single file change (opens file and updates content) without saving.
* Call saveFileChange() after approval.
*/
private async prepareFileChange(change: FileChange, path: string): Promise<void> {
const ops = this.providerOps!
const results: Record<string, FileOpsResult> = {}
for (const [path, change] of Object.entries(commit.changes)) {
switch (change.type) {
case PatchActionType.DELETE:
await ops.deleteFile(path)
results[path] = { deleted: true }
break
case PatchActionType.ADD:
if (!change.newContent) {
throw new DiffError(`Cannot create ${path} with no content`)
}
const addResult = await ops.createFile(path, change.newContent)
results[path] = {
finalContent: addResult.finalContent,
newProblemsMessage: addResult.newProblemsMessage,
userEdits: addResult.userEdits,
autoFormattingEdits: addResult.autoFormattingEdits,
}
break
case PatchActionType.UPDATE:
if (!change.newContent) {
throw new DiffError(`UPDATE change for ${path} has no new content`)
}
if (change.movePath) {
const moveResult = await ops.moveFile(path, change.movePath, change.newContent)
results[change.movePath] = {
finalContent: moveResult.finalContent,
newProblemsMessage: moveResult.newProblemsMessage,
userEdits: moveResult.userEdits,
autoFormattingEdits: moveResult.autoFormattingEdits,
}
results[path] = { deleted: true }
} else {
const updateResult = await ops.modifyFile(path, change.newContent)
results[path] = {
finalContent: updateResult.finalContent,
newProblemsMessage: updateResult.newProblemsMessage,
userEdits: updateResult.userEdits,
autoFormattingEdits: updateResult.autoFormattingEdits,
}
}
break
}
switch (change.type) {
case PatchActionType.DELETE:
await ops.deleteFile(path, false)
break
case PatchActionType.ADD:
if (!change.newContent) {
throw new DiffError(`Cannot create ${path} with no content`)
}
await ops.createFile(path, change.newContent, false)
break
case PatchActionType.UPDATE:
if (!change.newContent) {
throw new DiffError(`UPDATE change for ${path} has no new content`)
}
if (change.movePath) {
// For move operations, prepare the new file (the old file will be handled separately)
await ops.createFile(change.movePath, change.newContent, false)
} else {
await ops.modifyFile(path, change.newContent, false)
}
break
}
return results
}
private async revertChanges(): Promise<void> {
if (!this.appliedCommit || !this.providerOps) {
return
}
/**
* Saves the changes for a single file after approval.
*/
private async saveFileChange(change: FileChange, path: string): Promise<FileOpsResult | undefined> {
const ops = this.providerOps!
const ops = this.providerOps
for (const [path, change] of Object.entries(this.appliedCommit.changes)) {
try {
switch (change.type) {
case PatchActionType.DELETE:
if (change.oldContent !== undefined) {
await ops.createFile(path, change.oldContent)
}
break
case PatchActionType.ADD:
await ops.deleteFile(path)
break
case PatchActionType.UPDATE:
if (change.movePath) {
await ops.deleteFile(change.movePath)
if (change.oldContent !== undefined) {
await ops.createFile(path, change.oldContent)
}
} else if (change.oldContent !== undefined) {
await ops.modifyFile(path, change.oldContent)
}
break
switch (change.type) {
case PatchActionType.DELETE:
// For delete operations, actually delete the file now (after approval)
await ops.deleteFile(path)
return { deleted: true }
case PatchActionType.ADD:
if (!change.newContent) {
throw new DiffError(`Cannot create ${path} with no content`)
}
} catch (error) {
console.error(`Failed to revert ${path}:`, error)
}
return await ops.saveChanges()
case PatchActionType.UPDATE:
if (!change.newContent) {
throw new DiffError(`UPDATE change for ${path} has no new content`)
}
// For move operations, we're saving the new file (the old file deletion is handled in the calling code)
return await ops.saveChanges()
}
this.appliedCommit = undefined
this.config = undefined
}
private async generateChangeSummary(changes: Record<string, FileChange>): Promise<ClineSayTool[]> {
@@ -703,6 +731,7 @@ export class ApplyPatchHandler implements IFullyManagedTool {
undefined,
block.isNativeToolCall,
)
return approved
}
}
@@ -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}
@@ -14,34 +14,86 @@ export interface FileOpsResult {
export class FileProviderOperations {
constructor(private provider: DiffViewProvider) {}
async createFile(path: string, content: string): Promise<FileOpsResult> {
async openFile(path: string): Promise<void> {
await this.provider.open(path)
}
/**
* Saves the current changes and returns the result.
*/
async saveChanges(): Promise<FileOpsResult> {
const result = await this.provider.saveChanges()
return result
}
/**
* Creates a file. If isFinal is false, prepares the creation without saving.
* Call saveChanges() after approval when isFinal is false.
*/
async createFile(path: string, content: string, isFinal: boolean = true): Promise<FileOpsResult | undefined> {
this.provider.editType = "create"
await this.provider.open(path)
await this.provider.update(content, true)
const result = await this.provider.saveChanges()
await this.provider.reset()
return result
await this.openFile(path)
await this.provider.update(content, isFinal)
if (isFinal) {
return await this.saveChanges()
}
return undefined
}
async modifyFile(path: string, content: string): Promise<FileOpsResult> {
/**
* Modifies a file. If isFinal is false, prepares the modification without saving.
* Call saveChanges() after approval when isFinal is false.
*/
async modifyFile(path: string, content: string, isFinal: boolean = true): Promise<FileOpsResult | undefined> {
this.provider.editType = "modify"
await this.provider.open(path)
await this.provider.update(content, true)
const result = await this.provider.saveChanges()
await this.provider.reset()
return result
await this.openFile(path)
await this.provider.update(content, isFinal)
if (isFinal) {
return await this.saveChanges()
}
return undefined
}
async deleteFile(path: string): Promise<void> {
/**
* Deletes a file. If isFinal is false, prepares the deletion without actually deleting.
* Opens the file in the diff view to show it will be deleted.
* Call deleteFile() with isFinal=true after approval when isFinal is false.
*/
async deleteFile(path: string, isFinal: boolean = true): Promise<FileOpsResult | undefined> {
this.provider.editType = "delete"
await this.provider.open(path)
await this.provider.deleteFile(path)
await this.openFile(path)
if (isFinal) {
await this.provider.deleteFile(path)
return undefined
} else {
// Update with empty content to show the file will be deleted
await this.provider.update("", isFinal)
return undefined
}
}
async moveFile(oldPath: string, newPath: string, content: string): Promise<FileOpsResult> {
const result = await this.createFile(newPath, content)
await this.deleteFile(oldPath)
return result
/**
* Moves a file from oldPath to newPath. If isFinal is false, prepares the move without saving.
* Call saveChanges() after approval when isFinal is false.
*/
async moveFile(
oldPath: string,
newPath: string,
content: string,
isFinal: boolean = true,
): Promise<FileOpsResult | undefined> {
if (isFinal) {
const result = await this.createFile(newPath, content, isFinal)
await this.deleteFile(oldPath, isFinal)
return result
} else {
await this.createFile(newPath, content, isFinal)
await this.deleteFile(oldPath, isFinal)
return undefined
}
}
async getFileContent(): Promise<string | undefined> {
+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"
+8 -3
View File
@@ -11,6 +11,7 @@ import { ClineEnv } from "@/config"
import { CLINE_API_ENDPOINT } from "@/shared/cline/api"
import { getAxiosSettings } from "@/shared/net"
import { AuthService } from "../auth/AuthService"
import { buildBasicClineHeaders } from "../EnvUtils"
export class ClineAccountService {
private static instance: ClineAccountService
@@ -58,6 +59,7 @@ export class ClineAccountService {
headers: {
Authorization: `Bearer ${clineAccountAuthToken}`,
"Content-Type": "application/json",
...(await buildBasicClineHeaders()),
...config.headers,
},
...getAxiosSettings(),
@@ -236,12 +238,15 @@ export class ClineAccountService {
organizationId: organizationId || null, // Pass organization if provided
},
})
const activeOrgId = this._authService.getActiveOrganizationId()
if (activeOrgId !== organizationId) {
// After user switches account, we will force a refresh of the id token by calling this function that restores the refresh token and retrieves new auth info
await this._authService.restoreRefreshTokenAndRetrieveAuthInfo()
}
} catch (error) {
console.error("Error switching account:", error)
throw error
} finally {
// After user switches account, we will force a refresh of the id token by calling this function that restores the refresh token and retrieves new auth info
await this._authService.restoreRefreshTokenAndRetrieveAuthInfo()
throw error
}
}
+15 -13
View File
@@ -115,7 +115,18 @@ export class AuthService {
* Refreshing it if necessary.
*/
async getAuthToken(): Promise<string | null> {
return this.internalGetAuthToken(this._provider)
const token = await this.internalGetAuthToken(this._provider)
if (!token) {
return null
}
if (this._provider.timeUntilExpiry(token) <= 0) {
// internalGetAuthToken may return stale data on network errors
// Verify the token is not expired after refresh - We have a pending larger refactor to prevent this
// This prevents 401 errors from using expired tokens
return null
}
return `workos:${token}`
}
/**
@@ -152,7 +163,7 @@ export class AuthService {
if (this._refreshPromise) {
Logger.info("Token refresh already in progress, waiting for completion")
const updatedToken = await this._refreshPromise
return updatedToken ? `workos:${updatedToken}` : null
return updatedToken || null
}
// Start a new refresh operation
@@ -162,15 +173,6 @@ export class AuthService {
try {
const updatedAuthInfo = await provider.retrieveClineAuthInfo(this._controller)
if (updatedAuthInfo) {
// retrieveClineAuthInfo may return stale data on network errors
// Verify the token is not expired after refresh
// This prevents 401 errors from using expired tokens
const nowInSeconds = Date.now() / 1000
if ((updatedAuthInfo.expiresAt ?? nowInSeconds) < nowInSeconds) {
clineAccountAuthToken = undefined
return undefined
}
this._clineAuthInfo = updatedAuthInfo
this._authenticated = true
clineAccountAuthToken = updatedAuthInfo.idToken
@@ -206,10 +208,10 @@ export class AuthService {
return clineAccountAuthToken
})()
await this._refreshPromise
clineAccountAuthToken = await this._refreshPromise
}
return clineAccountAuthToken ? `workos:${clineAccountAuthToken}` : null
return clineAccountAuthToken || null
} catch (error) {
Logger.error("Error getting auth token:", error)
return null
+2
View File
@@ -5,6 +5,7 @@ import { setWelcomeViewCompleted } from "@/core/controller/state/setWelcomeViewC
import { WebviewProvider } from "@/core/webview"
import { CLINE_API_ENDPOINT } from "@/shared/cline/api"
import { fetch } from "@/shared/net"
import { buildBasicClineHeaders } from "../EnvUtils"
import { AuthService } from "./AuthService"
export class AuthServiceMock extends AuthService {
@@ -62,6 +63,7 @@ export class AuthServiceMock extends AuthService {
method: "POST",
headers: {
"Content-Type": "application/json",
...(await buildBasicClineHeaders()),
},
body: JSON.stringify({
code: testCode,
@@ -3,6 +3,7 @@ import { type JwtPayload } from "jwt-decode"
import { ClineEnv, EnvironmentConfig } from "@/config"
import { Controller } from "@/core/controller"
import { HostProvider } from "@/hosts/host-provider"
import { buildBasicClineHeaders } from "@/services/EnvUtils"
import { AuthInvalidTokenError, AuthNetworkError } from "@/services/error/ClineError"
import { Logger } from "@/services/logging/Logger"
import { telemetryService } from "@/services/telemetry"
@@ -95,9 +96,14 @@ export class ClineAuthProvider {
/**
* Returns the time in seconds until token expiry
*/
private timeUntilExpiry(_refreshToken: string, expiresAt?: number): number {
timeUntilExpiry(jwt: string): number {
const data = this.extractTokenData(jwt)
if (!data.exp) {
return 0
}
const currentTime = Date.now() / 1000
const expirationTime = expiresAt || 0
const expirationTime = data.exp
return expirationTime - currentTime
}
@@ -108,7 +114,7 @@ export class ClineAuthProvider {
const startedAt = storedAuthData?.startedAt
const timeSinceStarted = Date.now() - (startedAt || 0)
const tokenData = this.extractTokenData(storedAuthData)
const tokenData = this.extractTokenData(storedAuthData?.idToken)
telemetryService.capture({
event: "extension_logging_user_out",
properties: {
@@ -129,7 +135,7 @@ export class ClineAuthProvider {
const startedAt = storedAuthData?.startedAt
const timeSinceStarted = Date.now() - (startedAt || 0)
const tokenData = this.extractTokenData(storedAuthData)
const tokenData = this.extractTokenData(storedAuthData?.idToken)
telemetryService.capture({
event: "extension_refresh_attempt_failed",
properties: {
@@ -142,14 +148,12 @@ export class ClineAuthProvider {
})
}
private extractTokenData(authInfo?: ClineAuthInfo): Partial<TokenData> {
if (!authInfo || !authInfo.idToken) {
private extractTokenData(token: string | undefined): Partial<TokenData> {
if (!token) {
return {}
}
const idToken = authInfo.idToken
return parseJwtPayload<TokenData>(idToken) || {}
return parseJwtPayload<TokenData>(token) || {}
}
/**
@@ -188,10 +192,7 @@ export class ClineAuthProvider {
// and it failed the first refresh attempt
// with something other than invalid token
// continue with the request
if (
this.refreshRetryCount > 0 &&
this.timeUntilExpiry(storedAuthData.refreshToken, storedAuthData.expiresAt) > 30
) {
if (this.refreshRetryCount > 0 && this.timeUntilExpiry(storedAuthData.idToken) > 30) {
this.refreshRetryCount = 0
this.lastRefreshAttempt = 0
return storedAuthData
@@ -294,7 +295,7 @@ export class ClineAuthProvider {
const endpoint = new URL(CLINE_API_ENDPOINT.REFRESH_TOKEN, this.config.apiBaseUrl)
const response = await fetch(endpoint.toString(), {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: await this.headers(),
body: JSON.stringify({
refreshToken: storedData.refreshToken,
grantType: "refresh_token",
@@ -357,10 +358,7 @@ export class ClineAuthProvider {
method: "GET",
redirect: "manual",
credentials: "include", // Important for cookies if needed
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
headers: await this.headers(),
})
// If we get a redirect status (3xx), get the Location header
@@ -397,10 +395,7 @@ export class ClineAuthProvider {
const response = await fetch(tokenUrl.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
headers: await this.headers(),
body: JSON.stringify({
grant_type: "authorization_code",
code: authorizationCode,
@@ -449,6 +444,7 @@ export class ClineAuthProvider {
const userResponse = await axios.get(`${ClineEnv.config().apiBaseUrl}/api/v1/users/me`, {
headers: {
Authorization: `Bearer workos:${tokenData.accessToken}`,
...(await this.headers()),
},
...getAxiosSettings(),
})
@@ -467,4 +463,12 @@ export class ClineAuthProvider {
}
}
}
private async headers() {
return {
Accept: "application/json",
"Content-Type": "application/json",
...(await buildBasicClineHeaders()),
}
}
}
@@ -1,483 +0,0 @@
import * as crypto from "crypto"
import * as http from "http"
import * as vscode from "vscode"
import { Logger } from "@/services/logging/Logger"
import { fetch } from "@/shared/net"
// OAuth constants (same as OpenAI Codex CLI)
export const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
export const CODEX_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize"
export const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token"
export const CODEX_OAUTH_PORT = 1455
export const CODEX_REDIRECT_URI = `http://localhost:${CODEX_OAUTH_PORT}/auth/callback`
export const CODEX_SCOPES = "openid profile email offline_access"
// JWT claim path for ChatGPT account ID
const JWT_CLAIM_PATH = "https://api.openai.com/auth"
export interface CodexOAuthTokens {
accessToken: string
refreshToken: string
expiresAt: number // Unix timestamp in milliseconds
accountId: string
email?: string
}
interface TokenResponse {
access_token: string
refresh_token: string
expires_in: number
token_type: string
}
interface PKCEPair {
verifier: string
challenge: string
}
interface AuthorizationFlow {
pkce: PKCEPair
state: string
url: string
}
/**
* Generate a cryptographically secure random string for PKCE
*/
function generateCodeVerifier(): string {
return crypto.randomBytes(32).toString("base64url")
}
/**
* Generate SHA256 hash for PKCE code challenge
*/
function generateCodeChallenge(verifier: string): string {
return crypto.createHash("sha256").update(verifier).digest("base64url")
}
/**
* Generate random state for CSRF protection
*/
function generateState(): string {
return crypto.randomBytes(16).toString("hex")
}
/**
* Decode a JWT token and extract the payload
*/
function decodeJWT(token: string): Record<string, any> | null {
try {
const parts = token.split(".")
if (parts.length !== 3) {
return null
}
const payload = Buffer.from(parts[1], "base64url").toString("utf-8")
return JSON.parse(payload)
} catch {
return null
}
}
/**
* Extract ChatGPT account ID from access token JWT
*/
function extractAccountId(accessToken: string): string | null {
const decoded = decodeJWT(accessToken)
if (!decoded) {
return null
}
// Try the official claim path
const claims = decoded[JWT_CLAIM_PATH]
if (claims?.chatgpt_account_id) {
return claims.chatgpt_account_id
}
// Fallback: try direct property
if (decoded.chatgpt_account_id) {
return decoded.chatgpt_account_id
}
return null
}
/**
* Extract email from access token JWT
*/
function extractEmail(accessToken: string): string | undefined {
const decoded = decodeJWT(accessToken)
return decoded?.email
}
export class CodexAuthProvider {
readonly name = "codex"
/**
* Create the authorization URL with PKCE parameters
*/
createAuthorizationFlow(): AuthorizationFlow {
const verifier = generateCodeVerifier()
const challenge = generateCodeChallenge(verifier)
const state = generateState()
const url = new URL(CODEX_AUTHORIZE_URL)
url.searchParams.set("response_type", "code")
url.searchParams.set("client_id", CODEX_CLIENT_ID)
url.searchParams.set("redirect_uri", CODEX_REDIRECT_URI)
url.searchParams.set("scope", CODEX_SCOPES)
url.searchParams.set("code_challenge", challenge)
url.searchParams.set("code_challenge_method", "S256")
url.searchParams.set("state", state)
url.searchParams.set("id_token_add_organizations", "true")
url.searchParams.set("codex_cli_simplified_flow", "true")
url.searchParams.set("originator", "cline")
return {
pkce: { verifier, challenge },
state,
url: url.toString(),
}
}
/**
* Start local OAuth callback server
*/
startLocalOAuthServer(
expectedState: string,
): Promise<{ server: http.Server; getAuthCode: () => Promise<string | null>; close: () => void }> {
return new Promise((resolve, reject) => {
let authCode: string | null = null
let codeReceived = false
const server = http.createServer((req, res) => {
try {
const url = new URL(req.url || "", `http://localhost:${CODEX_OAUTH_PORT}`)
if (url.pathname !== "/auth/callback") {
res.statusCode = 404
res.end("Not found")
return
}
const state = url.searchParams.get("state")
if (state !== expectedState) {
res.statusCode = 400
res.setHeader("Content-Type", "text/html; charset=utf-8")
res.end(this.getErrorHtml("State mismatch - possible CSRF attack"))
return
}
const error = url.searchParams.get("error")
if (error) {
const errorDescription = url.searchParams.get("error_description") || error
res.statusCode = 400
res.setHeader("Content-Type", "text/html; charset=utf-8")
res.end(this.getErrorHtml(errorDescription))
return
}
const code = url.searchParams.get("code")
if (!code) {
res.statusCode = 400
res.setHeader("Content-Type", "text/html; charset=utf-8")
res.end(this.getErrorHtml("Missing authorization code"))
return
}
authCode = code
codeReceived = true
res.statusCode = 200
res.setHeader("Content-Type", "text/html; charset=utf-8")
res.end(this.getSuccessHtml())
} catch (err) {
Logger.error("Error handling OAuth callback:", err)
res.statusCode = 500
res.end("Internal server error")
}
})
server.on("error", (err: NodeJS.ErrnoException) => {
if (err.code === "EADDRINUSE") {
reject(new Error(`Port ${CODEX_OAUTH_PORT} is already in use. Please close any other Codex sessions.`))
} else {
reject(err)
}
})
server.listen(CODEX_OAUTH_PORT, "127.0.0.1", () => {
resolve({
server,
getAuthCode: async () => {
// Poll for auth code with timeout
const timeout = 120000 // 2 minutes
const pollInterval = 100
const startTime = Date.now()
while (!codeReceived && Date.now() - startTime < timeout) {
await new Promise((r) => setTimeout(r, pollInterval))
}
return authCode
},
close: () => {
server.close()
},
})
})
})
}
/**
* Exchange authorization code for tokens
*/
async exchangeAuthorizationCode(code: string, codeVerifier: string): Promise<CodexOAuthTokens> {
const response = await fetch(CODEX_TOKEN_URL, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: CODEX_CLIENT_ID,
code,
code_verifier: codeVerifier,
redirect_uri: CODEX_REDIRECT_URI,
}),
})
if (!response.ok) {
const text = await response.text().catch(() => "")
Logger.error(`Codex token exchange failed: ${response.status}`, text)
throw new Error(`Failed to exchange authorization code: ${response.status}`)
}
const data = (await response.json()) as TokenResponse
if (!data.access_token || !data.refresh_token || typeof data.expires_in !== "number") {
throw new Error("Invalid token response from OpenAI")
}
const accountId = extractAccountId(data.access_token)
if (!accountId) {
throw new Error("Could not extract ChatGPT account ID from token")
}
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + data.expires_in * 1000,
accountId,
email: extractEmail(data.access_token),
}
}
/**
* Refresh access token using refresh token
*/
async refreshAccessToken(refreshToken: string): Promise<CodexOAuthTokens> {
const response = await fetch(CODEX_TOKEN_URL, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: CODEX_CLIENT_ID,
}),
})
if (!response.ok) {
const text = await response.text().catch(() => "")
Logger.error(`Codex token refresh failed: ${response.status}`, text)
throw new Error(`Failed to refresh token: ${response.status}`)
}
const data = (await response.json()) as TokenResponse
if (!data.access_token || !data.refresh_token || typeof data.expires_in !== "number") {
throw new Error("Invalid token response from OpenAI")
}
const accountId = extractAccountId(data.access_token)
if (!accountId) {
throw new Error("Could not extract ChatGPT account ID from token")
}
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + data.expires_in * 1000,
accountId,
email: extractEmail(data.access_token),
}
}
/**
* Check if the token needs to be refreshed
*/
shouldRefreshToken(expiresAt: number): boolean {
const bufferMs = 30 * 1000 // 30 seconds buffer
return Date.now() >= expiresAt - bufferMs
}
/**
* Open browser to authorization URL
*/
async openBrowser(url: string): Promise<boolean> {
try {
await vscode.env.openExternal(vscode.Uri.parse(url))
return true
} catch (err) {
Logger.error("Failed to open browser:", err)
return false
}
}
/**
* Full sign-in flow
*/
async signIn(): Promise<CodexOAuthTokens> {
// Create authorization URL with PKCE
const flow = this.createAuthorizationFlow()
// Start local server to receive callback
const { getAuthCode, close } = await this.startLocalOAuthServer(flow.state)
try {
// Open browser for user to authenticate
const opened = await this.openBrowser(flow.url)
if (!opened) {
throw new Error("Failed to open browser for authentication")
}
// Wait for authorization code
const code = await getAuthCode()
if (!code) {
throw new Error("Authentication timed out or was cancelled")
}
// Exchange code for tokens
return await this.exchangeAuthorizationCode(code, flow.pkce.verifier)
} finally {
close()
}
}
private getSuccessHtml(): string {
return `<!DOCTYPE html>
<html>
<head>
<title>Authentication Successful</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
text-align: center;
background: white;
padding: 40px 60px;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.checkmark {
width: 80px;
height: 80px;
margin: 0 auto 20px;
border-radius: 50%;
background: #10b981;
display: flex;
align-items: center;
justify-content: center;
}
.checkmark svg {
width: 40px;
height: 40px;
fill: white;
}
h1 { color: #1f2937; margin: 0 0 10px; }
p { color: #6b7280; margin: 0; }
</style>
</head>
<body>
<div class="container">
<div class="checkmark">
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</div>
<h1>Authentication Successful</h1>
<p>You can close this window and return to Cline.</p>
</div>
</body>
</html>`
}
private getErrorHtml(message: string): string {
return `<!DOCTYPE html>
<html>
<head>
<title>Authentication Failed</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
}
.container {
text-align: center;
background: white;
padding: 40px 60px;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.error-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
border-radius: 50%;
background: #ef4444;
display: flex;
align-items: center;
justify-content: center;
}
.error-icon svg {
width: 40px;
height: 40px;
fill: white;
}
h1 { color: #1f2937; margin: 0 0 10px; }
p { color: #6b7280; margin: 0; }
</style>
</head>
<body>
<div class="container">
<div class="error-icon">
<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</div>
<h1>Authentication Failed</h1>
<p>${message}</p>
</div>
</body>
</html>`
}
}
// Singleton instance
let codexAuthProvider: CodexAuthProvider | null = null
export function getCodexAuthProvider(): CodexAuthProvider {
if (!codexAuthProvider) {
codexAuthProvider = new CodexAuthProvider()
}
return codexAuthProvider
}
+254 -20
View File
@@ -1,6 +1,9 @@
/**
* Tests for BannerService
* Tests API fetching, caching, and client-side provider filtering
*
* NOTE: Tests temporarily disabled while banner API fetching is disabled
* to prevent blocking the extension. Tests will be re-enabled when API is stable.
*/
import type { BannerRules } from "@shared/ClineBanner"
@@ -12,7 +15,7 @@ import type { Controller } from "@/core/controller"
import { Logger } from "../logging/Logger"
import { BannerService } from "./BannerService"
describe("BannerService", () => {
describe.skip("BannerService (TEMPORARILY DISABLED - Banner API fetch disabled)", () => {
let sandbox: sinon.SinonSandbox
let bannerService: BannerService
let axiosGetStub: sinon.SinonStub
@@ -66,16 +69,18 @@ describe("BannerService", () => {
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.fetchActiveBanners()
const banners = await bannerService.getActiveBanners()
expect(axiosGetStub.calledOnce).to.be.true
expect(banners).to.have.lengthOf(1)
expect(banners[0].id).to.equal("bnr_test1")
expect(banners[0].title).to.equal("Test Banner")
expect(banners[0].description).to.equal("This is a test")
})
it("should handle API errors gracefully", async () => {
axiosGetStub.rejects(new Error("Network error"))
const banners = await bannerService.fetchActiveBanners()
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(0)
})
@@ -102,25 +107,25 @@ describe("BannerService", () => {
axiosGetStub.resolves(mockResponse)
// First call fetches from API
await bannerService.fetchActiveBanners()
await bannerService.getActiveBanners()
expect(axiosGetStub.callCount).to.equal(1)
// Second call within cache window uses cache (no new API call)
await bannerService.fetchActiveBanners()
await bannerService.getActiveBanners()
expect(axiosGetStub.callCount).to.equal(1)
// After 4 minutes, still uses cache
clock.tick(4 * 60 * 1000)
await bannerService.fetchActiveBanners()
await bannerService.getActiveBanners()
expect(axiosGetStub.callCount).to.equal(1)
// After 6 minutes total, cache expired, makes new API call
clock.tick(2 * 60 * 1000)
await bannerService.fetchActiveBanners()
await bannerService.getActiveBanners()
expect(axiosGetStub.callCount).to.equal(2)
// Force refresh always bypasses cache
await bannerService.fetchActiveBanners(true)
await bannerService.getActiveBanners(true)
expect(axiosGetStub.callCount).to.equal(3)
})
})
@@ -158,7 +163,7 @@ describe("BannerService", () => {
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.fetchActiveBanners()
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(1)
expect(banners[0].id).to.equal("bnr_openai")
@@ -196,7 +201,7 @@ describe("BannerService", () => {
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.fetchActiveBanners()
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(1)
expect(banners[0].id).to.equal("bnr_anthropic")
@@ -234,7 +239,7 @@ describe("BannerService", () => {
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.fetchActiveBanners()
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(0)
})
@@ -271,7 +276,7 @@ describe("BannerService", () => {
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.fetchActiveBanners()
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(1)
expect(banners[0].id).to.equal("bnr_multi")
@@ -307,7 +312,7 @@ describe("BannerService", () => {
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.fetchActiveBanners()
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(0)
})
@@ -333,7 +338,7 @@ describe("BannerService", () => {
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.fetchActiveBanners()
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(1)
})
@@ -357,7 +362,7 @@ describe("BannerService", () => {
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.fetchActiveBanners()
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(1)
expect(banners[0].id).to.equal("bnr_norules")
@@ -385,12 +390,12 @@ describe("BannerService", () => {
axiosGetStub.resolves(mockResponse)
await bannerService.fetchActiveBanners()
await bannerService.getActiveBanners()
expect(axiosGetStub.calledOnce).to.be.true
bannerService.clearCache()
await bannerService.fetchActiveBanners()
await bannerService.getActiveBanners()
expect(axiosGetStub.calledTwice).to.be.true
})
})
@@ -415,7 +420,7 @@ describe("BannerService", () => {
}
axiosGetStub.resolves(mockResponse)
await bannerService.fetchActiveBanners()
await bannerService.getActiveBanners()
expect(axiosGetStub.calledOnce).to.be.true
const call = axiosGetStub.getCall(0)
@@ -450,7 +455,7 @@ describe("BannerService", () => {
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.fetchActiveBanners()
const banners = await bannerService.getActiveBanners()
Object.defineProperty(process, "platform", {
value: originalPlatform,
@@ -501,7 +506,7 @@ describe("BannerService", () => {
// Clear cache to ensure fresh API call for each platform test
bannerService.clearCache()
await bannerService.fetchActiveBanners()
await bannerService.getActiveBanners()
expect(axiosGetStub.called).to.be.true
const call = axiosGetStub.lastCall
@@ -518,4 +523,233 @@ describe("BannerService", () => {
}
})
})
describe("Banner to BannerCardData Conversion", () => {
it("should convert banner with valid action types", async () => {
const mockResponse = {
data: {
data: {
items: [
{
id: "bnr_valid_actions",
titleMd: "Valid Actions Banner",
bodyMd: "Has valid actions",
icon: "lightbulb",
severity: "info" as const,
placement: "top" as const,
rulesJson: "{}",
actions: [
{ title: "Link", action: "link", arg: "https://example.com" },
{ title: "Settings", action: "show-api-settings" },
],
},
],
},
},
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(1)
expect(banners[0].id).to.equal("bnr_valid_actions")
expect(banners[0].title).to.equal("Valid Actions Banner")
expect(banners[0].description).to.equal("Has valid actions")
expect(banners[0].icon).to.equal("lightbulb")
expect(banners[0].actions).to.have.lengthOf(2)
expect(banners[0].actions![0].title).to.equal("Link")
expect(banners[0].actions![0].action).to.equal("link")
expect(banners[0].actions![0].arg).to.equal("https://example.com")
expect(banners[0].actions![1].title).to.equal("Settings")
expect(banners[0].actions![1].action).to.equal("show-api-settings")
})
it("should drop banner with invalid action type and log error", async () => {
const mockResponse = {
data: {
data: {
items: [
{
id: "bnr_invalid_action",
titleMd: "Invalid Action Banner",
bodyMd: "Has invalid action type",
severity: "info" as const,
placement: "top" as const,
rulesJson: "{}",
actions: [{ title: "Invalid", action: "unknown-action-type", arg: "test" }],
},
],
},
},
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(0)
})
it("should keep valid banners and drop only invalid ones", async () => {
const mockResponse = {
data: {
data: {
items: [
{
id: "bnr_valid",
titleMd: "Valid Banner",
bodyMd: "This one is valid",
severity: "info" as const,
placement: "top" as const,
rulesJson: "{}",
actions: [{ title: "Link", action: "link", arg: "https://example.com" }],
},
{
id: "bnr_invalid",
titleMd: "Invalid Banner",
bodyMd: "This one has invalid action",
severity: "info" as const,
placement: "top" as const,
rulesJson: "{}",
actions: [{ title: "Bad", action: "not-a-real-action" }],
},
{
id: "bnr_also_valid",
titleMd: "Also Valid Banner",
bodyMd: "This one is also valid",
severity: "info" as const,
placement: "top" as const,
rulesJson: "{}",
},
],
},
},
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(2)
expect(banners[0].id).to.equal("bnr_valid")
expect(banners[1].id).to.equal("bnr_also_valid")
})
it("should convert banner with no actions", async () => {
const mockResponse = {
data: {
data: {
items: [
{
id: "bnr_no_actions",
titleMd: "No Actions Banner",
bodyMd: "Has no actions",
severity: "info" as const,
placement: "top" as const,
rulesJson: "{}",
},
],
},
},
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(1)
expect(banners[0].id).to.equal("bnr_no_actions")
expect(banners[0].actions).to.have.lengthOf(0)
})
it("should convert banner with empty actions array", async () => {
const mockResponse = {
data: {
data: {
items: [
{
id: "bnr_empty_actions",
titleMd: "Empty Actions Banner",
bodyMd: "Has empty actions array",
severity: "info" as const,
placement: "top" as const,
rulesJson: "{}",
actions: [],
},
],
},
},
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(1)
expect(banners[0].id).to.equal("bnr_empty_actions")
expect(banners[0].actions).to.have.lengthOf(0)
})
it("should drop banner when action has undefined action type", async () => {
const mockResponse = {
data: {
data: {
items: [
{
id: "bnr_undefined_action",
titleMd: "Undefined Action Type",
bodyMd: "Action has no type defined",
severity: "info" as const,
placement: "top" as const,
rulesJson: "{}",
actions: [{ title: "Just a label" }],
},
],
},
},
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(0)
})
it("should accept all valid BannerActionType values", async () => {
const validActionTypes = [
"link",
"show-api-settings",
"show-feature-settings",
"show-account",
"set-model",
"install-cli",
]
const mockResponse = {
data: {
data: {
items: [
{
id: "bnr_all_valid_types",
titleMd: "All Valid Types",
bodyMd: "Has all valid action types",
severity: "info" as const,
placement: "top" as const,
rulesJson: "{}",
actions: validActionTypes.map((type, index) => ({
title: `Action ${index}`,
action: type,
})),
},
],
},
},
}
axiosGetStub.resolves(mockResponse)
const banners = await bannerService.getActiveBanners()
expect(banners).to.have.lengthOf(1)
expect(banners[0].actions).to.have.lengthOf(validActionTypes.length)
banners[0].actions!.forEach((action, index) => {
expect(action.action).to.equal(validActionTypes[index])
})
})
})
})
+69 -9
View File
@@ -1,10 +1,12 @@
import type { Banner, BannerRules, BannersResponse } from "@shared/ClineBanner"
import { BannerActionType, type BannerCardData } from "@shared/cline/banner"
import axios from "axios"
import { ClineEnv } from "@/config"
import type { Controller } from "@/core/controller"
import { HostProvider } from "@/hosts/host-provider"
import { getAxiosSettings } from "@/shared/net"
import { AuthService } from "../auth/AuthService"
import { buildBasicClineHeaders } from "../EnvUtils"
import { getDistinctId } from "../logging/distinctId"
import { Logger } from "../logging/Logger"
@@ -19,9 +21,12 @@ export class BannerService {
private readonly CACHE_DURATION_MS = 5 * 60 * 1000 // 5 minutes
private _controller: Controller
private _authService?: AuthService
private actionTypes: Set<string>
private _fetchPromise: Promise<Banner[]> | null = null
private constructor(controller: Controller) {
this._controller = controller
this.actionTypes = new Set<string>(Object.values(BannerActionType))
}
/**
@@ -78,7 +83,7 @@ export class BannerService {
* @param forceRefresh If true, bypasses cache and fetches fresh data
* @returns Array of banners that match current environment
*/
public async fetchActiveBanners(forceRefresh = false): Promise<Banner[]> {
private async internalGetActiveBanners(forceRefresh = false): Promise<Banner[]> {
try {
// Return cached banners if still valid
const now = Date.now()
@@ -87,6 +92,22 @@ export class BannerService {
return this._cachedBanners
}
if (this._fetchPromise && !forceRefresh) {
return this._fetchPromise
}
this._fetchPromise = this.fetchActiveBanners()
return this._fetchPromise
} catch (error) {
// Log error but don't throw - banner fetching shouldn't break the extension
Logger.error("BannerService: Error getting internal banners", error)
return []
}
}
private async fetchActiveBanners(): Promise<Banner[]> {
try {
const now = Date.now()
const ideType = await this.getIdeType()
const extensionVersion = await this.getExtensionVersion()
const osType = await this.getOSType()
@@ -102,13 +123,11 @@ export class BannerService {
Logger.log(`BannerService: Fetching banners from ${url}`)
const authService = this.getAuthServiceInstance()
let token: string | null = null
if (authService) {
token = await authService.getAuthToken()
}
const token: string | null = (await authService?.getAuthToken()) || null
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(await buildBasicClineHeaders()),
}
if (token) {
headers["Authorization"] = `Bearer ${token}`
@@ -136,11 +155,16 @@ export class BannerService {
this._cachedBanners = matchingBanners
this._lastFetchTime = now
if (matchingBanners.length > 0) {
Logger.log(`BannerService: ${matchingBanners.length} active banner(s) fetched.`)
}
return matchingBanners
} catch (error) {
// Log error but don't throw - banner fetching shouldn't break the extension
Logger.error("BannerService: Error fetching banners", error)
return []
} finally {
this._fetchPromise = null
}
}
@@ -325,6 +349,7 @@ export class BannerService {
timeout: 10000,
headers: {
"Content-Type": "application/json",
...(await buildBasicClineHeaders()),
},
...getAxiosSettings(),
})
@@ -379,14 +404,49 @@ export class BannerService {
}
}
/**
* Converts a Banner (API response format) to BannerCardData (UI format)
* @param banner The banner from the API
* @returns BannerCardData suitable for the carousel, or null if banner is invalid.
*/
private convertToBannerCardData(banner: Banner): BannerCardData | null {
// Validate all action types before conversion
// Each action must have a valid action type - undefined is not allowed
for (const action of banner.actions || []) {
if (!action.action || !this.actionTypes.has(action.action)) {
Logger.error(`BannerService: ${banner.id} has invalid or missing action type '${action.action ?? "undefined"}'.`)
return null
}
if (!action.title) {
Logger.error(`BannerService: ${banner.id} is missing an action title: ${JSON.stringify(action)}`)
return null
}
}
const actions = (banner.actions || []).map((action) => ({
title: action.title || "",
action: action.action as BannerActionType,
arg: action.arg,
}))
return {
id: banner.id,
title: banner.titleMd,
description: banner.bodyMd,
icon: banner.icon,
actions,
}
}
/**
* Gets banners that haven't been dismissed by the user
* @param forceRefresh If true, bypasses cache and fetches fresh data
* @returns Array of non-dismissed banners
* @returns Array of non-dismissed banners converted to BannerCardData format
*
* TEMPORARILY DISABLED: Returning empty array to prevent API calls
*/
public async getNonDismissedBanners(forceRefresh = false): Promise<Banner[]> {
const allBanners = await this.fetchActiveBanners(forceRefresh)
return allBanners.filter((banner) => !this.isBannerDismissed(banner.id))
public async getActiveBanners(forceRefresh = false): Promise<BannerCardData[]> {
// Disable all banner fetching to prevent blocking the extension
return []
}
/**
@@ -87,10 +87,6 @@ export class FeatureFlagsService {
return this.cache.get(flagName) === true
}
public getDoNothingFlag(): boolean {
return this.getBooleanFlagEnabled(FeatureFlag.DO_NOTHING)
}
public getHooksEnabled(): boolean {
return this.getBooleanFlagEnabled(FeatureFlag.HOOKS)
}
+19 -12
View File
@@ -276,25 +276,29 @@ export class McpHub {
const stateManager = StateManager.get()
const remoteConfig = stateManager.getRemoteConfigSettings()
// If marketplace is explicitly disabled by enterprise config, block all local servers
if (remoteConfig.mcpMarketplaceEnabled === false) {
return
}
// Early exit for non-enterprise users: if no remote config is set, allow all local servers
if (Object.keys(remoteConfig).length === 0) {
// No remote config restrictions - proceed with connection (default behavior for non-enterprise users)
// This ensures backwards compatibility and that regular users are not affected
} else {
// Enterprise restrictions apply
// Only apply allowlist restrictions if enterprise has configured an allowlist
if (remoteConfig.allowedMCPServers && remoteConfig.allowedMCPServers.length > 0) {
// Check if server is from GitHub marketplace
if (name.startsWith("github.com/")) {
const allowedIds = remoteConfig.allowedMCPServers.map((server: { id: string }) => server.id)
// If marketplace is explicitly disabled by enterprise config, block all local servers
if (remoteConfig.mcpMarketplaceEnabled === false) {
return
}
// If allowlist exists, only servers on the allowlist are allowed
const hasAllowlist = remoteConfig.allowedMCPServers && remoteConfig.allowedMCPServers.length > 0
if (hasAllowlist) {
const allowedIds = remoteConfig.allowedMCPServers!.map((server: { id: string }) => server.id)
if (!allowedIds.includes(name)) {
return
}
}
// Non-GitHub local servers are allowed when there's an allowlist
// (the allowlist only restricts marketplace servers)
// If marketplace is enabled with no allowlist, all local servers are allowed
}
// If no enterprise allowlist configured, allow all local servers (default behavior)
}
if (config.disabled) {
@@ -448,6 +452,9 @@ export class McpHub {
break
}
case "streamableHttp": {
// Use ReconnectingEventSource for auto-reconnection on connection drops
global.EventSource = ReconnectingEventSource
// Custom fetch wrapper that treats 404 as 405 for GET requests.
// The MCP SDK sends a GET request to check for SSE stream support.
// Per MCP spec, servers should return 405 if they don't support SSE,
@@ -409,4 +409,39 @@ describe("Telemetry system is abstracted and can easily switch between providers
await noOpProvider.dispose()
})
})
describe("Skills Telemetry", () => {
it("should capture skill used events correctly", async () => {
const noOpProvider = new NoOpTelemetryProvider()
const logSpy = sinon.spy(noOpProvider, "log")
const telemetryService = new TelemetryService([noOpProvider], MOCK_METADATA)
logSpy.resetHistory()
telemetryService.captureSkillUsed({
ulid: "task-123",
skillName: "my-skill",
skillSource: "global",
skillsAvailableGlobal: 2,
skillsAvailableProject: 3,
provider: "cline",
modelId: "anthropic/claude-sonnet-4.5",
})
assert.ok(logSpy.calledOnce, "Log should be called once")
const [eventName, properties] = logSpy.firstCall.args
assert.strictEqual(eventName, "task.skill_used", "Event name should be task.skill_used")
assert.ok(properties, "Properties should be defined")
assert.strictEqual(properties.ulid, "task-123", "Properties should include task ULID")
assert.strictEqual(properties.skillName, "my-skill", "Properties should include skillName")
assert.strictEqual(properties.skillSource, "global", "Properties should include skillSource")
assert.strictEqual(properties.skillsAvailableGlobal, 2, "Properties should include global skill count")
assert.strictEqual(properties.skillsAvailableProject, 3, "Properties should include project skill count")
assert.strictEqual(properties.provider, "cline", "Properties should include provider")
assert.strictEqual(properties.modelId, "anthropic/claude-sonnet-4.5", "Properties should include modelId")
logSpy.restore()
await noOpProvider.dispose()
})
})
})
+40 -1
View File
@@ -16,7 +16,7 @@ import { TelemetryProviderFactory } from "./TelemetryProviderFactory"
* When adding a new category, add it both here and to the initial values in telemetryCategoryEnabled
* Ensure `if (!this.isCategoryEnabled('<category_name>')` is added to the capture method
*/
type TelemetryCategory = "checkpoints" | "browser" | "focus_chain" | "dictation" | "subagents" | "hooks"
type TelemetryCategory = "checkpoints" | "browser" | "focus_chain" | "dictation" | "subagents" | "skills" | "hooks"
/**
* Terminal type for telemetry differentiation
@@ -109,6 +109,7 @@ export class TelemetryService {
["dictation", true], // Dictation telemetry enabled
["focus_chain", true], // Focus Chain telemetry enabled
["subagents", true], // CLI Subagents telemetry enabled
["skills", true], // Skills telemetry enabled
["hooks", true], // Hooks telemetry enabled
])
@@ -284,6 +285,8 @@ export class TelemetryService {
SUBAGENT_DISABLED: "task.subagent_disabled",
SUBAGENT_STARTED: "task.subagent_started",
SUBAGENT_COMPLETED: "task.subagent_completed",
// Skills telemetry events
SKILL_USED: "task.skill_used",
},
// UI interaction events for tracking user engagement
UI: {
@@ -1003,6 +1006,42 @@ export class TelemetryService {
this.recordHistogram(TelemetryService.METRICS.TOOLS.CALLS_PER_TASK, toolCallCount, toolAttributes)
}
public captureSkillUsed(args: {
ulid: string
skillName: string
skillSource: "global" | "project"
skillsAvailableGlobal: number
skillsAvailableProject: number
provider?: string
modelId?: string
}): void {
if (!this.isCategoryEnabled("skills")) {
return
}
if (!args.ulid || !args.skillName) {
return
}
const skillsAvailableGlobal = Math.max(0, args.skillsAvailableGlobal)
const skillsAvailableProject = Math.max(0, args.skillsAvailableProject)
const properties = {
ulid: args.ulid,
skillName: args.skillName,
skillSource: args.skillSource,
skillsAvailableGlobal,
skillsAvailableProject,
provider: args.provider,
modelId: args.modelId,
}
this.capture({
event: TelemetryService.EVENTS.TASK.SKILL_USED,
properties,
})
}
/**
* Records when an MCP tool is called.
* This telemetry event is designed to monitor the usage and performance of MCP tools
+13 -2
View File
@@ -9,11 +9,22 @@ export interface Banner {
id: string
titleMd: string
bodyMd: string
severity: BannerSeverity
placement: BannerPlacement
icon?: string
actions?: BannerAction[]
rulesJson: string
activeFrom?: string
activeTo?: string
// Severity and placement are not used in the extension
severity?: BannerSeverity
placement?: BannerPlacement
}
export interface BannerAction {
action?: string
arg?: string
title?: string
}
export interface BannersResponse {
+4
View File
@@ -7,6 +7,7 @@ import { AutoApprovalSettings } from "./AutoApprovalSettings"
import { ApiConfiguration } from "./api"
import { BrowserSettings } from "./BrowserSettings"
import { ClineFeatureSetting } from "./ClineFeatureSetting"
import { BannerCardData } from "./cline/banner"
import { ClineRulesToggles } from "./cline-rules"
import { DictationSettings } from "./DictationSettings"
import { FocusChainSettings } from "./FocusChainSettings"
@@ -110,6 +111,8 @@ export interface ExtensionState {
nativeToolCallSetting?: boolean
enableParallelToolCalling?: boolean
backgroundEditEnabled?: boolean
optOutOfRemoteConfig?: boolean
banners?: BannerCardData[]
}
export interface ClineMessage {
@@ -199,6 +202,7 @@ export interface ClineSayTool {
| "webFetch"
| "webSearch"
| "summarizeTask"
| "useSkill"
path?: string
diff?: string
content?: string
+33 -273
View File
@@ -1,5 +1,5 @@
import type { LanguageModelChatSelector } from "../core/api/providers/types"
import { ApiFormat } from "./proto/cline/models"
import { ApiHandlerSettings } from "./storage/state-keys"
export type ApiProvider =
| "anthropic"
@@ -42,198 +42,15 @@ export type ApiProvider =
| "minimax"
| "hicap"
| "nousResearch"
| "openai-codex"
export interface ApiHandlerSecrets {
apiKey?: string // anthropic
liteLlmApiKey?: string
awsAccessKey?: string
awsSecretKey?: string
openRouterApiKey?: string
aihubmixApiKey?: string
aihubmixBaseUrl?: string
aihubmixAppCode?: string
export const DEFAULT_API_PROVIDER = "openrouter" as ApiProvider
clineAccountId?: string
awsSessionToken?: string
awsBedrockApiKey?: string
openAiApiKey?: string
geminiApiKey?: string
openAiNativeApiKey?: string
ollamaApiKey?: string
deepSeekApiKey?: string
requestyApiKey?: string
togetherApiKey?: string
fireworksApiKey?: string
qwenApiKey?: string
doubaoApiKey?: string
mistralApiKey?: string
authNonce?: string
asksageApiKey?: string
xaiApiKey?: string
moonshotApiKey?: string
zaiApiKey?: string
huggingFaceApiKey?: string
nebiusApiKey?: string
sambanovaApiKey?: string
cerebrasApiKey?: string
sapAiCoreClientId?: string
sapAiCoreClientSecret?: string
groqApiKey?: string
huaweiCloudMaasApiKey?: string
basetenApiKey?: string
vercelAiGatewayApiKey?: string
difyApiKey?: string
minimaxApiKey?: string
hicapApiKey?: string
nousResearchApiKey?: string
openAiCodexAccessToken?: string
openAiCodexRefreshToken?: string
openAiCodexAccountId?: string
openAiCodexTokenExpiry?: number
}
export interface ApiHandlerOptions {
// Global configuration (not mode-specific)
export interface ApiHandlerOptions extends Partial<ApiHandlerSettings> {
ulid?: string // Used to identify the task in API requests
liteLlmBaseUrl?: string
liteLlmUsePromptCache?: boolean
openAiHeaders?: Record<string, string> // Custom headers for OpenAI requests
anthropicBaseUrl?: string
openRouterProviderSorting?: string
awsRegion?: string
awsUseCrossRegionInference?: boolean
awsUseGlobalInference?: boolean
awsBedrockUsePromptCache?: boolean
awsAuthentication?: string
awsUseProfile?: boolean
awsProfile?: string
awsBedrockEndpoint?: string
claudeCodePath?: string
vertexProjectId?: string
vertexRegion?: string
openAiBaseUrl?: string
ollamaBaseUrl?: string
ollamaApiOptionsCtxNum?: string
lmStudioBaseUrl?: string
lmStudioModelId?: string
lmStudioMaxTokens?: string
geminiBaseUrl?: string
requestyBaseUrl?: string
fireworksModelMaxCompletionTokens?: number
fireworksModelMaxTokens?: number
qwenCodeOauthPath?: string
azureApiVersion?: string
azureIdentity?: boolean
qwenApiLine?: string
moonshotApiLine?: string
asksageApiUrl?: string
requestTimeoutMs?: number
sapAiResourceGroup?: string
sapAiCoreTokenUrl?: string
sapAiCoreBaseUrl?: string
sapAiCoreUseOrchestrationMode?: boolean
difyBaseUrl?: string
zaiApiLine?: string
hicapApiKey?: string
hicapModelId?: string
onRetryAttempt?: (attempt: number, maxRetries: number, delay: number, error: any) => void
ocaBaseUrl?: string
minimaxApiLine?: string
ocaMode?: string
aihubmixBaseUrl?: string
aihubmixAppCode?: string
// Plan mode configurations
planModeApiModelId?: string
planModeThinkingBudgetTokens?: number
geminiPlanModeThinkingLevel?: string
planModeReasoningEffort?: string
planModeVerbosity?: string
planModeVsCodeLmModelSelector?: LanguageModelChatSelector
planModeAwsBedrockCustomSelected?: boolean
planModeAwsBedrockCustomModelBaseId?: string
planModeOpenRouterModelId?: string
planModeOpenRouterModelInfo?: ModelInfo
planModeOpenAiModelId?: string
planModeOpenAiModelInfo?: OpenAiCompatibleModelInfo
planModeOllamaModelId?: string
planModeLmStudioModelId?: string
planModeLiteLlmModelId?: string
planModeLiteLlmModelInfo?: LiteLLMModelInfo
planModeRequestyModelId?: string
planModeRequestyModelInfo?: ModelInfo
planModeTogetherModelId?: string
planModeFireworksModelId?: string
planModeSapAiCoreModelId?: string
planModeSapAiCoreDeploymentId?: string
planModeGroqModelId?: string
planModeGroqModelInfo?: ModelInfo
planModeBasetenModelId?: string
planModeBasetenModelInfo?: ModelInfo
planModeHuggingFaceModelId?: string
planModeHuggingFaceModelInfo?: ModelInfo
planModeHuaweiCloudMaasModelId?: string
planModeHuaweiCloudMaasModelInfo?: ModelInfo
planModeOcaModelId?: string
planModeOcaModelInfo?: OcaModelInfo
planModeOcaReasoningEffort?: string
planModeAihubmixModelId?: string
planModeAihubmixModelInfo?: OpenAiCompatibleModelInfo
planModeHicapModelId?: string
planModeHicapModelInfo?: ModelInfo
planModeNousResearchModelId?: string
planModeVercelAiGatewayModelId?: string
planModeVercelAiGatewayModelInfo?: ModelInfo
// Act mode configurations
actModeApiModelId?: string
actModeThinkingBudgetTokens?: number
geminiActModeThinkingLevel?: string
actModeReasoningEffort?: string
actModeVerbosity?: string
actModeVsCodeLmModelSelector?: LanguageModelChatSelector
actModeAwsBedrockCustomSelected?: boolean
actModeAwsBedrockCustomModelBaseId?: string
actModeOpenRouterModelId?: string
actModeOpenRouterModelInfo?: ModelInfo
actModeOpenAiModelId?: string
actModeOpenAiModelInfo?: OpenAiCompatibleModelInfo
actModeOllamaModelId?: string
actModeLmStudioModelId?: string
actModeLiteLlmModelId?: string
actModeLiteLlmModelInfo?: LiteLLMModelInfo
actModeRequestyModelId?: string
actModeRequestyModelInfo?: ModelInfo
actModeTogetherModelId?: string
actModeFireworksModelId?: string
actModeSapAiCoreModelId?: string
actModeSapAiCoreDeploymentId?: string
actModeGroqModelId?: string
actModeGroqModelInfo?: ModelInfo
actModeBasetenModelId?: string
actModeBasetenModelInfo?: ModelInfo
actModeHuggingFaceModelId?: string
actModeHuggingFaceModelInfo?: ModelInfo
actModeHuaweiCloudMaasModelId?: string
actModeHuaweiCloudMaasModelInfo?: ModelInfo
actModeOcaModelId?: string
actModeOcaModelInfo?: OcaModelInfo
actModeOcaReasoningEffort?: string
actModeAihubmixModelId?: string
actModeAihubmixModelInfo?: OpenAiCompatibleModelInfo
actModeHicapModelId?: string
actModeHicapModelInfo?: ModelInfo
actModeNousResearchModelId?: string
actModeVercelAiGatewayModelId?: string
actModeVercelAiGatewayModelInfo?: ModelInfo
onRetryAttempt?: (attempt: number, maxRetries: number, delay: number, error: any) => void // Callback function
}
export type ApiConfiguration = ApiHandlerOptions &
ApiHandlerSecrets & {
planModeApiProvider?: ApiProvider
actModeApiProvider?: ApiProvider
}
export type ApiConfiguration = ApiHandlerOptions
// Models
@@ -1566,6 +1383,20 @@ export const openAiNativeModels = {
supportsReasoning: true,
supportsReasoningEffort: true,
},
"gpt-5.2-codex": {
maxTokens: 8_192, // 128000 breaks context window truncation
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 1.75,
outputPrice: 14.0,
cacheReadsPrice: 0.175,
apiFormat: ApiFormat.OPENAI_RESPONSES,
temperature: 1,
systemRole: "developer",
supportsReasoning: true,
supportsReasoningEffort: true,
},
"gpt-5.1-2025-11-13": {
maxTokens: 8_192,
contextWindow: 272000,
@@ -1816,91 +1647,6 @@ export const openAiNativeModels = {
},
} as const satisfies Record<string, OpenAiCompatibleModelInfo>
// OpenAI Codex (uses ChatGPT subscription via OAuth)
// https://help.openai.com/en/articles/11369540-using-codex-with-your-chatgpt-plan
export type OpenAiCodexModelId = keyof typeof openAiCodexModels
export const openAiCodexDefaultModelId: OpenAiCodexModelId = "gpt-5.1-codex"
export const openAiCodexModels = {
"gpt-5.2-codex": {
maxTokens: 8_192,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0, // Included in ChatGPT subscription
outputPrice: 0,
apiFormat: ApiFormat.OPENAI_RESPONSES,
temperature: 1,
systemRole: "developer",
supportsReasoning: true,
supportsReasoningEffort: true,
},
"gpt-5.2": {
maxTokens: 8_192,
contextWindow: 272000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
apiFormat: ApiFormat.OPENAI_RESPONSES,
temperature: 1,
systemRole: "developer",
supportsReasoning: true,
supportsReasoningEffort: true,
},
"gpt-5.1-codex-max": {
maxTokens: 8_192,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
apiFormat: ApiFormat.OPENAI_RESPONSES,
temperature: 1,
systemRole: "developer",
supportsReasoning: true,
supportsReasoningEffort: true,
},
"gpt-5.1-codex": {
maxTokens: 8_192,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
apiFormat: ApiFormat.OPENAI_RESPONSES,
temperature: 1,
systemRole: "developer",
supportsReasoning: true,
supportsReasoningEffort: true,
},
"gpt-5.1-codex-mini": {
maxTokens: 8_192,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
apiFormat: ApiFormat.OPENAI_RESPONSES,
temperature: 1,
systemRole: "developer",
supportsReasoning: true,
supportsReasoningEffort: true,
},
"gpt-5.1": {
maxTokens: 8_192,
contextWindow: 272000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
apiFormat: ApiFormat.OPENAI_RESPONSES,
temperature: 1,
systemRole: "developer",
supportsReasoning: true,
supportsReasoningEffort: true,
},
} as const satisfies Record<string, OpenAiCompatibleModelInfo>
// Azure OpenAI
// https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
// https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#api-specs
@@ -3728,6 +3474,13 @@ export const sapAiCoreDefaultModelId: SapAiCoreModelId = "anthropic--claude-3.5-
// Pricing is calculated using Capacity Units, not directly in USD
const sapAiCoreModelDescription = "Pricing is calculated using SAP's Capacity Units rather than direct USD pricing."
export const sapAiCoreModels = {
"anthropic--claude-4.5-haiku": {
maxTokens: 64000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
description: sapAiCoreModelDescription,
},
"anthropic--claude-4.5-sonnet": {
maxTokens: 8192,
contextWindow: 200_000,
@@ -3742,6 +3495,13 @@ export const sapAiCoreModels = {
supportsPromptCache: true,
description: sapAiCoreModelDescription,
},
"anthropic--claude-4.5-opus": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
description: sapAiCoreModelDescription,
},
"anthropic--claude-4-opus": {
maxTokens: 8192,
contextWindow: 200_000,
-10
View File
@@ -17,16 +17,6 @@ export enum BannerActionType {
InstallCli = "install-cli",
}
/**
* Backend banner format returned from server API
*/
export interface BackendBanner {
id: string
titleMd: string
bodyMd: string
rulesJson: string
}
/**
* Banner data structure for backend-to-frontend communication.
* Backend constructs this JSON, frontend renders it via BannerCarousel.
+49 -8
View File
@@ -1,16 +1,21 @@
import { ClineMessage } from "./ExtensionMessage"
/**
* Consolidates error_retry messages in a retry sequence, keeping only the latest one.
* Consolidates error_retry messages in a retry sequence, keeping only the latest one,
* and removes successful retry messages entirely.
*
* When an API request fails and auto-retry is enabled, multiple error_retry messages are created
* (e.g., "Attempt 1 of 3", "Attempt 2 of 3", "Attempt 3 of 3"), interleaved with api_req_retried
* messages. This function filters out earlier retry messages, showing only the most recent one.
* messages. This function:
* 1. Filters out earlier retry messages, showing only the most recent one
* 2. Removes error_retry messages entirely when followed by a successful api_req_started
* (indicating the retry succeeded)
*
* @param messages - An array of ClineMessage objects to process.
* @returns A new array of ClineMessage objects with error_retry sequences consolidated.
*
* @example
* // During retry sequence - shows only latest attempt:
* const messages: ClineMessage[] = [
* { type: 'say', say: 'error_retry', text: '{"attempt":1,"maxAttempts":3}', ts: 1000 },
* { type: 'say', say: 'api_req_retried', ts: 1001 },
@@ -20,6 +25,16 @@ import { ClineMessage } from "./ExtensionMessage"
* ];
* const result = combineErrorRetryMessages(messages);
* // Result: [{ type: 'say', say: 'error_retry', text: '{"attempt":3,"maxAttempts":3}', ts: 1004 }]
*
* @example
* // After successful retry - removes error_retry entirely:
* const messages: ClineMessage[] = [
* { type: 'say', say: 'error_retry', text: '{"attempt":1,"maxAttempts":3}', ts: 1000 },
* { type: 'say', say: 'api_req_retried', ts: 1001 },
* { type: 'say', say: 'api_req_started', text: '{}', ts: 1002 },
* ];
* const result = combineErrorRetryMessages(messages);
* // Result: [{ type: 'say', say: 'api_req_started', text: '{}', ts: 1002 }]
*/
export function combineErrorRetryMessages(messages: ClineMessage[]): ClineMessage[] {
const result: ClineMessage[] = []
@@ -28,15 +43,41 @@ export function combineErrorRetryMessages(messages: ClineMessage[]): ClineMessag
const message = messages[i]
if (message.say === "error_retry") {
// Look ahead to see if the next non-api_req_retried message is also an error_retry
let nextMessage = messages[i + 1]
if (nextMessage?.say === "api_req_retried") {
nextMessage = messages[i + 2]
// Look ahead to find if there's another error_retry before the next api_req_started
let hasLaterErrorRetry = false
let hasApiReqStartedBefore = false
for (let j = i + 1; j < messages.length; j++) {
const laterMessage = messages[j]
if (laterMessage.say === "api_req_started") {
hasApiReqStartedBefore = true
break
}
if (laterMessage.say === "error_retry") {
hasLaterErrorRetry = true
break
}
}
if (nextMessage?.say === "error_retry") {
// Skip this message, we'll show the next one (or a later one in the sequence)
// Case 1: Another error_retry follows before api_req_started - skip this one
if (hasLaterErrorRetry) {
continue
}
// Case 2: api_req_started follows (no later error_retry) - retry succeeded
// Don't show the error_retry unless it has failed: true
if (hasApiReqStartedBefore) {
try {
const retryInfo = JSON.parse(message.text || "{}")
// Only skip if this wasn't a final failure message
if (!retryInfo.failed) {
continue
}
} catch {
// If we can't parse, still skip to be safe
continue
}
}
}
result.push(message)
@@ -259,8 +259,6 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
return ProtoApiProvider.GEMINI
case "openai-native":
return ProtoApiProvider.OPENAI_NATIVE
case "openai-codex":
return ProtoApiProvider.OPENAI_CODEX
case "requesty":
return ProtoApiProvider.REQUESTY
case "together":
@@ -349,8 +347,6 @@ export function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvid
return "gemini"
case ProtoApiProvider.OPENAI_NATIVE:
return "openai-native"
case ProtoApiProvider.OPENAI_CODEX:
return "openai-codex"
case ProtoApiProvider.REQUESTY:
return "requesty"
case ProtoApiProvider.TOGETHER:
@@ -499,10 +495,6 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
minimaxApiKey: config.minimaxApiKey,
minimaxApiLine: config.minimaxApiLine,
nousResearchApiKey: config.nousResearchApiKey,
openaiCodexAccessToken: config.openAiCodexAccessToken,
openaiCodexRefreshToken: config.openAiCodexRefreshToken,
openaiCodexAccountId: config.openAiCodexAccountId,
openaiCodexTokenExpiry: config.openAiCodexTokenExpiry,
ocaMode: config.ocaMode,
aihubmixApiKey: config.aihubmixApiKey,
aihubmixBaseUrl: config.aihubmixBaseUrl,
@@ -683,10 +675,6 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
hicapApiKey: protoConfig.hicapApiKey,
hicapModelId: protoConfig.hicapModelId,
nousResearchApiKey: protoConfig.nousResearchApiKey,
openAiCodexAccessToken: protoConfig.openaiCodexAccessToken,
openAiCodexRefreshToken: protoConfig.openaiCodexRefreshToken,
openAiCodexAccountId: protoConfig.openaiCodexAccountId,
openAiCodexTokenExpiry: protoConfig.openaiCodexTokenExpiry,
// Plan mode configurations
planModeApiProvider:
+1 -5
View File
@@ -26,7 +26,7 @@
},
{
"value": "vscode-lm",
"label": "VS Code LM API"
"label": "GitHub Copilot"
},
{
"value": "deepseek",
@@ -36,10 +36,6 @@
"value": "openai-native",
"label": "OpenAI"
},
{
"value": "openai-codex",
"label": "OpenAI Codex"
},
{
"value": "ollama",
"label": "Ollama"
@@ -4,7 +4,6 @@ export enum FeatureFlag {
CUSTOM_INSTRUCTIONS = "custom-instructions",
DICTATION = "dictation",
FOCUS_CHAIN_CHECKLIST = "focus_chain_checklist",
DO_NOTHING = "do_nothing",
HOOKS = "hooks",
WEBTOOLS = "webtools",
// Feature flag for showing the new onboarding flow or old welcome view.
@@ -12,7 +11,6 @@ export enum FeatureFlag {
}
export const FeatureFlagDefaultValue: Partial<Record<FeatureFlag, FeatureFlagPayload>> = {
[FeatureFlag.DO_NOTHING]: false,
[FeatureFlag.HOOKS]: false,
[FeatureFlag.WEBTOOLS]: false,
[FeatureFlag.ONBOARDING_MODELS]: process.env.E2E_TEST === "true" ? { models: {} } : undefined,
@@ -0,0 +1,470 @@
/**
* State Keys Type Safety Tests
*
* This test suite validates the type safety guarantees of the state-keys module,
* which uses a single-source-of-truth pattern where types are auto-generated from
* field definition objects.
*
* ## Type Safety Model
*
* The state-keys module generates TypeScript types from runtime objects using:
* 1. Field definition objects with `default` values (e.g., `{ default: true as boolean }`)
* 2. `satisfies FieldDefinitions` constraint to enforce structure
* 3. `BuildInterface<T>` mapped type to extract types from `default` values
*
* ## Known Limitations (What These Tests Catch)
*
* The `as` type assertions on default values are TRUSTED by TypeScript. This means:
* - `{ default: "foo" as number }` would compile but be wrong at runtime
* - `{ default: undefined as string }` compiles but `string` doesn't include `undefined`
*
* These tests provide runtime validation to catch such mismatches that TypeScript cannot.
*
* ## What These Tests Validate
*
* 1. **Type-Value Consistency**: Default values match their declared types at runtime
* 2. **Key Synchronization**: Generated key arrays match the source objects
* 3. **Type Guard Correctness**: `isGlobalStateKey`, `isSettingsKey`, etc. work correctly
* 4. **Default Value Retrieval**: `getDefaultValue` returns correct values
* 5. **Transform Functions**: Transforms produce values of the correct type
*
* ## Running Tests
*
* ```bash
* npm run test:unit -- --grep "State Keys"
* ```
*/
import { expect } from "chai"
import { describe, it } from "mocha"
import {
applyTransform,
GLOBAL_STATE_DEFAULTS,
type GlobalState,
GlobalStateAndSettingKeys,
type GlobalStateAndSettings,
type GlobalStateAndSettingsKey,
type GlobalStateKey,
getDefaultValue,
hasTransform,
isAsyncProperty,
isComputedProperty,
isGlobalStateKey,
isLocalStateKey,
isSecretKey,
isSettingsKey,
type LocalState,
type LocalStateKey,
LocalStateKeys,
SETTINGS_DEFAULTS,
SETTINGS_TRANSFORMS,
type SecretKey,
SecretKeys,
type Settings,
type SettingsKey,
SettingsKeys,
} from "../state-keys"
// ============================================================================
// Test Helpers
// ============================================================================
/**
* Validates that a value matches the expected TypeScript type at runtime.
* This catches cases where `as` assertions mask type mismatches.
*/
function assertTypeMatch(value: unknown, expectedType: string, key: string): void {
const actualType = value === null ? "null" : Array.isArray(value) ? "array" : typeof value
if (expectedType === "array") {
expect(Array.isArray(value), `${key}: expected array, got ${actualType}`).to.be.true
} else if (expectedType === "object") {
expect(actualType, `${key}: expected object, got ${actualType}`).to.equal("object")
expect(value, `${key}: expected object, got null`).to.not.be.null
expect(Array.isArray(value), `${key}: expected object, got array`).to.be.false
} else if (expectedType === "undefined") {
expect(value, `${key}: expected undefined, got ${actualType}`).to.be.undefined
} else {
expect(actualType, `${key}: expected ${expectedType}, got ${actualType}`).to.equal(expectedType)
}
}
/**
* Infers the expected runtime type from a default value.
* Used to validate that default values are consistent with their purpose.
*/
function inferExpectedType(value: unknown): string {
if (value === undefined) {
return "undefined"
}
if (value === null) {
return "null"
}
if (Array.isArray(value)) {
return "array"
}
return typeof value
}
// ============================================================================
// Tests
// ============================================================================
describe("State Keys Type Safety", () => {
describe("Type-Value Consistency", () => {
/**
* These tests validate that default values match their declared types.
* This catches mistakes like `{ default: "string" as number }` which
* TypeScript would accept but would cause runtime issues.
*/
it("should have GlobalState defaults with correct runtime types", () => {
const defaults = GLOBAL_STATE_DEFAULTS as Record<string, unknown>
// Validate each default value has a sensible runtime type
for (const [key, value] of Object.entries(defaults)) {
const type = inferExpectedType(value)
// Re-validate to ensure consistency
assertTypeMatch(value, type, `GLOBAL_STATE_DEFAULTS.${key}`)
}
})
it("should have Settings defaults with correct runtime types", () => {
const defaults = SETTINGS_DEFAULTS as Record<string, unknown>
for (const [key, value] of Object.entries(defaults)) {
const type = inferExpectedType(value)
assertTypeMatch(value, type, `SETTINGS_DEFAULTS.${key}`)
}
})
it("should not have undefined defaults masquerading as non-optional types", () => {
// This test catches the pattern: { default: undefined as SomeType }
// where SomeType doesn't include undefined
const allDefaults = { ...GLOBAL_STATE_DEFAULTS, ...SETTINGS_DEFAULTS } as Record<string, unknown>
const undefinedKeys = Object.entries(allDefaults)
.filter(([_, value]) => value === undefined)
.map(([key]) => key)
// These keys have undefined defaults, which is valid only if their
// declared type includes `| undefined`. This test documents which
// keys are expected to have undefined defaults.
expect(undefinedKeys).to.be.an("array")
// If a key unexpectedly becomes undefined, this test will catch it
})
it("should have array defaults that are actually arrays", () => {
const allDefaults = { ...GLOBAL_STATE_DEFAULTS, ...SETTINGS_DEFAULTS } as Record<string, unknown>
for (const [key, value] of Object.entries(allDefaults)) {
if (Array.isArray(value)) {
// Verify it's a proper array, not an array-like object
expect(value, `${key} should be a true array`).to.be.instanceOf(Array)
}
}
})
it("should have object defaults that are plain objects", () => {
const allDefaults = { ...GLOBAL_STATE_DEFAULTS, ...SETTINGS_DEFAULTS } as Record<string, unknown>
for (const [key, value] of Object.entries(allDefaults)) {
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
// Verify it's a plain object
expect(Object.getPrototypeOf(value), `${key} should be a plain object`).to.equal(Object.prototype)
}
}
})
})
describe("Key Array Synchronization", () => {
/**
* These tests ensure the generated key arrays stay in sync with
* their source objects. A mismatch could cause runtime errors.
*/
it("should have SettingsKeys match SETTINGS_DEFAULTS keys", () => {
const defaultKeys = Object.keys(SETTINGS_DEFAULTS)
const exportedKeys = new Set<string>(SettingsKeys)
// Every key in defaults should be in the exported array
for (const key of defaultKeys) {
expect(exportedKeys.has(key), `SettingsKeys missing key: ${key}`).to.be.true
}
})
it("should have GlobalStateAndSettingKeys be a superset of SettingsKeys", () => {
const combinedSet = new Set<string>(GlobalStateAndSettingKeys)
for (const key of SettingsKeys) {
expect(combinedSet.has(key), `GlobalStateAndSettingKeys missing settings key: ${key}`).to.be.true
}
})
it("should have no duplicate keys in exported arrays", () => {
expect(new Set(SettingsKeys).size, "SettingsKeys has duplicates").to.equal(SettingsKeys.length)
expect(new Set(SecretKeys).size, "SecretKeys has duplicates").to.equal(SecretKeys.length)
expect(new Set(LocalStateKeys).size, "LocalStateKeys has duplicates").to.equal(LocalStateKeys.length)
expect(new Set(GlobalStateAndSettingKeys).size, "GlobalStateAndSettingKeys has duplicates").to.equal(
GlobalStateAndSettingKeys.length,
)
})
it("should have SecretKeys as strings", () => {
for (const key of SecretKeys) {
expect(typeof key, `SecretKey ${key} should be string`).to.equal("string")
expect(key.length, `SecretKey should not be empty`).to.be.greaterThan(0)
}
})
})
describe("Type Guard Functions", () => {
/**
* Type guards narrow `string` to specific key types.
* These tests ensure they correctly identify valid keys.
*/
it("should correctly identify GlobalState keys", () => {
// Known GlobalState keys from the defaults
const knownGlobalStateKeys = Object.keys(GLOBAL_STATE_DEFAULTS)
for (const key of knownGlobalStateKeys) {
expect(isGlobalStateKey(key), `${key} should be a GlobalStateKey`).to.be.true
}
// Non-existent keys should return false
expect(isGlobalStateKey("nonExistentKey123")).to.be.false
expect(isGlobalStateKey("")).to.be.false
})
it("should correctly identify Settings keys", () => {
for (const key of SettingsKeys) {
expect(isSettingsKey(key), `${key} should be a SettingsKey`).to.be.true
}
expect(isSettingsKey("nonExistentKey123")).to.be.false
})
it("should correctly identify Secret keys", () => {
// Sample known secret keys
const knownSecretKeys = ["apiKey", "openRouterApiKey", "awsAccessKey"]
for (const key of knownSecretKeys) {
expect(isSecretKey(key), `${key} should be a SecretKey`).to.be.true
}
expect(isSecretKey("notASecretKey")).to.be.false
})
it("should correctly identify LocalState keys", () => {
for (const key of LocalStateKeys) {
expect(isLocalStateKey(key), `${key} should be a LocalStateKey`).to.be.true
}
expect(isLocalStateKey("notALocalStateKey")).to.be.false
})
it("should have mutually exclusive key categories where expected", () => {
// Secret keys should not overlap with settings keys
for (const secretKey of SecretKeys) {
// Most secret keys should not be in settings (they're stored separately)
// This is a sanity check, not a strict requirement
if (isSettingsKey(secretKey)) {
// If there is overlap, document it
console.log(`Note: ${secretKey} is both a SecretKey and SettingsKey`)
}
}
})
})
describe("Default Value Retrieval", () => {
/**
* Tests for the getDefaultValue utility function.
*/
it("should return correct default values for known keys", () => {
// Test a few known defaults
const testCases: Array<{ key: GlobalStateAndSettingsKey; expectedType: string }> = [
{ key: "autoApprovalSettings", expectedType: "object" },
{ key: "browserSettings", expectedType: "object" },
{ key: "shellIntegrationTimeout", expectedType: "number" },
{ key: "preferredLanguage", expectedType: "string" },
{ key: "yoloModeToggled", expectedType: "boolean" },
]
for (const { key, expectedType } of testCases) {
const value = getDefaultValue(key)
if (value !== undefined) {
assertTypeMatch(value, expectedType, `getDefaultValue(${key})`)
}
}
})
it("should return undefined for keys without defaults", () => {
// Keys with `undefined` as default should return undefined
const keysWithUndefinedDefaults = GlobalStateAndSettingKeys.filter((key) => {
const value = getDefaultValue(key)
return value === undefined
})
// This is expected behavior - document which keys have undefined defaults
expect(keysWithUndefinedDefaults).to.be.an("array")
})
})
describe("Transform Functions", () => {
/**
* Tests for transform functions that modify values before storage.
*/
it("should have transforms return the same type as input", () => {
// Get keys that have transforms
const keysWithTransforms = Object.keys(SETTINGS_TRANSFORMS)
expect(keysWithTransforms.length, "Should have at least one transform").to.be.greaterThan(0)
for (const key of keysWithTransforms) {
expect(hasTransform(key), `hasTransform(${key}) should be true`).to.be.true
}
})
it("should correctly identify keys without transforms", () => {
expect(hasTransform("nonExistentKey")).to.be.false
expect(hasTransform("")).to.be.false
})
it("should apply transforms without throwing", () => {
const keysWithTransforms = Object.keys(SETTINGS_TRANSFORMS)
for (const key of keysWithTransforms) {
// Transform should handle various inputs gracefully
expect(() => applyTransform(key, {})).to.not.throw()
expect(() => applyTransform(key, undefined)).to.not.throw()
expect(() => applyTransform(key, null)).to.not.throw()
}
})
it("should pass through values for keys without transforms", () => {
const testValue = { test: "value" }
const result = applyTransform("keyWithoutTransform", testValue)
expect(result).to.equal(testValue)
})
it("should merge defaults in browserSettings transform", () => {
if (hasTransform("browserSettings")) {
const partial = { viewport: { width: 800, height: 600 } }
const result = applyTransform("browserSettings", partial)
expect(result).to.be.an("object")
expect(result.viewport).to.deep.equal({ width: 800, height: 600 })
}
})
})
describe("Metadata Properties", () => {
/**
* Tests for isAsync and isComputed metadata flags.
*/
it("should correctly identify async properties", () => {
// taskHistory is known to be async
expect(isAsyncProperty("taskHistory")).to.be.true
expect(isAsyncProperty("preferredLanguage")).to.be.false
expect(isAsyncProperty("nonExistent")).to.be.false
})
it("should correctly identify computed properties", () => {
// planActSeparateModelsSetting is known to be computed
expect(isComputedProperty("planActSeparateModelsSetting")).to.be.true
expect(isComputedProperty("preferredLanguage")).to.be.false
expect(isComputedProperty("nonExistent")).to.be.false
})
})
describe("Type Exports", () => {
/**
* Compile-time tests that verify type exports work correctly.
* If these fail to compile, the types are broken.
*/
it("should export usable GlobalState type", () => {
// This is a compile-time check - if GlobalState is broken, this won't compile
const partialState: Partial<GlobalState> = {
isNewUser: true,
favoritedModelIds: [],
}
expect(partialState.isNewUser).to.equal(true)
})
it("should export usable Settings type", () => {
const partialSettings: Partial<Settings> = {
preferredLanguage: "English",
shellIntegrationTimeout: 5000,
}
expect(partialSettings.preferredLanguage).to.equal("English")
})
it("should export usable key types", () => {
// These assignments verify the key types are correctly narrowed
const globalKey: GlobalStateKey = "isNewUser"
const settingsKey: SettingsKey = "preferredLanguage"
const secretKey: SecretKey = "apiKey"
const localKey: LocalStateKey = "localClineRulesToggles"
expect(globalKey).to.be.a("string")
expect(settingsKey).to.be.a("string")
expect(secretKey).to.be.a("string")
expect(localKey).to.be.a("string")
})
it("should have GlobalStateAndSettings include both GlobalState and Settings", () => {
const combined: Partial<GlobalStateAndSettings> = {
// From GlobalState
isNewUser: true,
// From Settings
preferredLanguage: "English",
}
expect(combined.isNewUser).to.equal(true)
expect(combined.preferredLanguage).to.equal("English")
})
it("should have LocalState keys map to ClineRulesToggles", () => {
const localState: Partial<LocalState> = {
localClineRulesToggles: {},
localCursorRulesToggles: { "some-rule": true },
}
expect(localState.localClineRulesToggles).to.deep.equal({})
})
})
describe("Edge Cases", () => {
/**
* Tests for edge cases and boundary conditions.
*/
it("should handle empty string keys gracefully", () => {
expect(isGlobalStateKey("")).to.be.false
expect(isSettingsKey("")).to.be.false
expect(isSecretKey("")).to.be.false
expect(isLocalStateKey("")).to.be.false
})
it("should handle keys with special characters", () => {
// The cline:clineAccountId key has a colon
expect(SecretKeys).to.include("cline:clineAccountId")
expect(isSecretKey("cline:clineAccountId")).to.be.true
})
it("should not have keys that could cause prototype pollution", () => {
const dangerousKeys = ["__proto__", "constructor", "prototype"]
for (const key of dangerousKeys) {
expect(isGlobalStateKey(key), `${key} should not be a GlobalStateKey`).to.be.false
expect(isSettingsKey(key), `${key} should not be a SettingsKey`).to.be.false
expect(isSecretKey(key), `${key} should not be a SecretKey`).to.be.false
}
})
})
})
+425 -276
View File
@@ -1,299 +1,448 @@
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { ApiProvider, ModelInfo, type OcaModelInfo } from "@shared/api"
import { BrowserSettings } from "@shared/BrowserSettings"
import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import {
ANTHROPIC_MIN_THINKING_BUDGET,
ApiProvider,
DEFAULT_API_PROVIDER,
LiteLLMModelInfo,
ModelInfo,
type OcaModelInfo,
OpenAiCompatibleModelInfo,
} from "@shared/api"
import { BrowserSettings, DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { ClineRulesToggles } from "@shared/cline-rules"
import { DictationSettings } from "@shared/DictationSettings"
import { FocusChainSettings } from "@shared/FocusChainSettings"
import { DEFAULT_DICTATION_SETTINGS, DictationSettings } from "@shared/DictationSettings"
import { DEFAULT_FOCUS_CHAIN_SETTINGS, FocusChainSettings } from "@shared/FocusChainSettings"
import { HistoryItem } from "@shared/HistoryItem"
import { McpDisplayMode } from "@shared/McpDisplayMode"
import { DEFAULT_MCP_DISPLAY_MODE, McpDisplayMode } from "@shared/McpDisplayMode"
import { WorkspaceRoot } from "@shared/multi-root/types"
import { GlobalInstructionsFile } from "@shared/remote-config/schema"
import { Mode, OpenaiReasoningEffort } from "@shared/storage/types"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { UserInfo } from "@shared/UserInfo"
import { LanguageModelChatSelector } from "vscode"
export type SecretKey = keyof Secrets
export type GlobalStateKey = keyof GlobalState
// ============================================================================
// SINGLE SOURCE OF TRUTH FOR STORAGE KEYS
//
// Property definitions with types, default values, and metadata
// NOTE: When adding a new field, the scripts/generate-state-proto.mjs will be
// executed automatically to regenerate the proto/cline/state.proto file with the
// new fields once the file is staged and committed.
// ============================================================================
export type LocalStateKey = keyof LocalState
export type SettingsKey = keyof Settings
export type GlobalStateAndSettingsKey = keyof (GlobalState & Settings)
export type GlobalStateAndSettings = GlobalState & Settings
export interface RemoteConfigExtraFields {
remoteConfiguredProviders: string[]
allowedMCPServers: Array<{ id: string }>
remoteMCPServers?: Array<{ name: string; url: string; alwaysEnabled?: boolean }>
previousRemoteMCPServers?: Array<{ name: string; url: string }>
remoteGlobalRules?: GlobalInstructionsFile[]
remoteGlobalWorkflows?: GlobalInstructionsFile[]
blockPersonalRemoteMCPServers?: boolean
openTelemetryOtlpHeaders: Record<string, string> | undefined
/**
* Defines the shape of a field definition. Each field must have a `default` value,
* and optionally can have `isAsync`, `isComputed`, or `transform` metadata.
*
* The type casting on `default` (e.g., `true as boolean`) is necessary because
* TypeScript would otherwise infer the literal type (`true`) instead of the
* wider type (`boolean`). This ensures the generated interfaces allow any
* value of that type, not just the default literal.
*/
type FieldDefinition<T> = {
default: T // The default value for the field with proper type casting using as (e.g., `true as boolean | undefined`)
isAsync?: boolean
isComputed?: boolean
transform?: (value: any) => T
}
export type RemoteConfigFields = GlobalStateAndSettings & RemoteConfigExtraFields
type FieldDefinitions = Record<string, FieldDefinition<any>>
export interface GlobalState {
lastShownAnnouncementId: string | undefined
taskHistory: HistoryItem[]
userInfo: UserInfo | undefined
favoritedModelIds: string[]
mcpMarketplaceEnabled: boolean
mcpResponsesCollapsed: boolean
terminalReuseEnabled: boolean
vscodeTerminalExecutionMode: "vscodeTerminal" | "backgroundExec"
isNewUser: boolean
welcomeViewCompleted: boolean | undefined
mcpDisplayMode: McpDisplayMode
// Multi-root workspace support
workspaceRoots: WorkspaceRoot[] | undefined
primaryRootIndex: number
multiRootEnabled: boolean
lastDismissedInfoBannerVersion: number
lastDismissedModelBannerVersion: number
lastDismissedCliBannerVersion: number
nativeToolCallEnabled: boolean
remoteRulesToggles: ClineRulesToggles
remoteWorkflowToggles: ClineRulesToggles
dismissedBanners: Array<{ bannerId: string; dismissedAt: number }>
}
const REMOTE_CONFIG_EXTRA_FIELDS = {
remoteConfiguredProviders: { default: [] as ApiProvider[] },
allowedMCPServers: { default: [] as Array<{ id: string }> },
remoteMCPServers: { default: undefined as Array<{ name: string; url: string; alwaysEnabled?: boolean }> | undefined },
previousRemoteMCPServers: { default: undefined as Array<{ name: string; url: string }> | undefined },
remoteGlobalRules: { default: undefined as GlobalInstructionsFile[] | undefined },
remoteGlobalWorkflows: { default: undefined as GlobalInstructionsFile[] | undefined },
blockPersonalRemoteMCPServers: { default: false as boolean },
openTelemetryOtlpHeaders: { default: undefined as Record<string, string> | undefined },
} satisfies FieldDefinitions
export interface Settings {
awsRegion: string | undefined
awsUseCrossRegionInference: boolean | undefined
awsUseGlobalInference: boolean | undefined
awsBedrockUsePromptCache: boolean | undefined
awsBedrockEndpoint: string | undefined
awsProfile: string | undefined
awsAuthentication: string | undefined
awsUseProfile: boolean | undefined
vertexProjectId: string | undefined
vertexRegion: string | undefined
requestyBaseUrl: string | undefined
openAiBaseUrl: string | undefined
openAiHeaders: Record<string, string>
ollamaBaseUrl: string | undefined
ollamaApiOptionsCtxNum: string | undefined
lmStudioBaseUrl: string | undefined
lmStudioMaxTokens: string | undefined
anthropicBaseUrl: string | undefined
geminiBaseUrl: string | undefined
azureApiVersion: string | undefined
azureIdentity: boolean | undefined
openRouterProviderSorting: string | undefined
autoApprovalSettings: AutoApprovalSettings
globalClineRulesToggles: ClineRulesToggles
globalWorkflowToggles: ClineRulesToggles
browserSettings: BrowserSettings
liteLlmBaseUrl: string | undefined
liteLlmUsePromptCache: boolean | undefined
fireworksModelMaxCompletionTokens: number | undefined
fireworksModelMaxTokens: number | undefined
qwenApiLine: string | undefined
moonshotApiLine: string | undefined
zaiApiLine: string | undefined
telemetrySetting: TelemetrySetting
asksageApiUrl: string | undefined
planActSeparateModelsSetting: boolean
enableCheckpointsSetting: boolean
requestTimeoutMs: number | undefined
shellIntegrationTimeout: number
defaultTerminalProfile: string
terminalOutputLineLimit: number
maxConsecutiveMistakes: number
subagentTerminalOutputLineLimit: number
sapAiCoreTokenUrl: string | undefined
sapAiCoreBaseUrl: string | undefined
sapAiResourceGroup: string | undefined
sapAiCoreUseOrchestrationMode: boolean | undefined
claudeCodePath: string | undefined
qwenCodeOauthPath: string | undefined
strictPlanModeEnabled: boolean
yoloModeToggled: boolean
useAutoCondense: boolean
clineWebToolsEnabled: boolean
preferredLanguage: string
openaiReasoningEffort: OpenaiReasoningEffort
mode: Mode
dictationSettings: DictationSettings
focusChainSettings: FocusChainSettings
customPrompt: "compact" | undefined
difyBaseUrl: string | undefined
autoCondenseThreshold: number | undefined // number from 0 to 1
ocaBaseUrl: string | undefined
minimaxApiLine: string | undefined
ocaMode: string | undefined
aihubmixBaseUrl: string | undefined
aihubmixAppCode: string | undefined
openAiCodexTokenExpiry: number | undefined
hooksEnabled: boolean
subagentsEnabled: boolean
skillsEnabled: boolean
globalSkillsToggles: Record<string, boolean>
enableParallelToolCalling: boolean
backgroundEditEnabled: boolean
const GLOBAL_STATE_FIELDS = {
lastShownAnnouncementId: { default: undefined as string | undefined },
taskHistory: { default: [] as HistoryItem[], isAsync: true },
userInfo: { default: undefined as UserInfo | undefined },
favoritedModelIds: { default: [] as string[] },
mcpMarketplaceEnabled: { default: true as boolean },
mcpResponsesCollapsed: { default: false as boolean },
terminalReuseEnabled: { default: true as boolean },
vscodeTerminalExecutionMode: {
default: "vscodeTerminal" as "vscodeTerminal" | "backgroundExec",
},
isNewUser: { default: true as boolean },
welcomeViewCompleted: { default: undefined as boolean | undefined },
mcpDisplayMode: { default: DEFAULT_MCP_DISPLAY_MODE as McpDisplayMode },
workspaceRoots: { default: undefined as WorkspaceRoot[] | undefined },
primaryRootIndex: { default: 0 as number },
multiRootEnabled: { default: false as boolean },
lastDismissedInfoBannerVersion: { default: 0 as number },
lastDismissedModelBannerVersion: { default: 0 as number },
lastDismissedCliBannerVersion: { default: 0 as number },
nativeToolCallEnabled: { default: true as boolean },
remoteRulesToggles: { default: {} as ClineRulesToggles },
remoteWorkflowToggles: { default: {} as ClineRulesToggles },
dismissedBanners: { default: [] as Array<{ bannerId: string; dismissedAt: number }> },
} satisfies FieldDefinitions
// Fields that map directly to ApiHandlerOptions in @shared/api.ts
const API_HANDLER_SETTINGS_FIELDS = {
// Global configuration (not mode-specific)
liteLlmBaseUrl: { default: undefined as string | undefined },
liteLlmUsePromptCache: { default: undefined as boolean | undefined },
openAiHeaders: { default: {} as Record<string, string> },
anthropicBaseUrl: { default: undefined as string | undefined },
openRouterProviderSorting: { default: undefined as string | undefined },
awsRegion: { default: undefined as string | undefined },
awsUseCrossRegionInference: { default: undefined as boolean | undefined },
awsUseGlobalInference: { default: undefined as boolean | undefined },
awsBedrockUsePromptCache: { default: undefined as boolean | undefined },
awsAuthentication: { default: undefined as string | undefined },
awsUseProfile: { default: undefined as boolean | undefined },
awsProfile: { default: undefined as string | undefined },
awsBedrockEndpoint: { default: undefined as string | undefined },
claudeCodePath: { default: undefined as string | undefined },
vertexProjectId: { default: undefined as string | undefined },
vertexRegion: { default: undefined as string | undefined },
openAiBaseUrl: { default: undefined as string | undefined },
ollamaBaseUrl: { default: undefined as string | undefined },
ollamaApiOptionsCtxNum: { default: undefined as string | undefined },
lmStudioBaseUrl: { default: undefined as string | undefined },
lmStudioMaxTokens: { default: undefined as string | undefined },
geminiBaseUrl: { default: undefined as string | undefined },
requestyBaseUrl: { default: undefined as string | undefined },
fireworksModelMaxCompletionTokens: { default: undefined as number | undefined },
fireworksModelMaxTokens: { default: undefined as number | undefined },
qwenCodeOauthPath: { default: undefined as string | undefined },
azureApiVersion: { default: undefined as string | undefined },
azureIdentity: { default: undefined as boolean | undefined },
qwenApiLine: { default: undefined as string | undefined },
moonshotApiLine: { default: undefined as string | undefined },
asksageApiUrl: { default: undefined as string | undefined },
requestTimeoutMs: { default: undefined as number | undefined },
sapAiResourceGroup: { default: undefined as string | undefined },
sapAiCoreTokenUrl: { default: undefined as string | undefined },
sapAiCoreBaseUrl: { default: undefined as string | undefined },
sapAiCoreUseOrchestrationMode: { default: true as boolean },
difyBaseUrl: { default: undefined as string | undefined },
zaiApiLine: { default: undefined as string | undefined },
ocaBaseUrl: { default: undefined as string | undefined },
minimaxApiLine: { default: undefined as string | undefined },
ocaMode: { default: "internal" as string },
aihubmixBaseUrl: { default: undefined as string | undefined },
aihubmixAppCode: { default: undefined as string | undefined },
// Plan mode configurations
planModeApiModelId: { default: undefined as string | undefined },
planModeThinkingBudgetTokens: { default: ANTHROPIC_MIN_THINKING_BUDGET as number | undefined },
geminiPlanModeThinkingLevel: { default: undefined as string | undefined },
planModeReasoningEffort: { default: undefined as string | undefined },
planModeVerbosity: { default: undefined as string | undefined },
planModeVsCodeLmModelSelector: { default: undefined as LanguageModelChatSelector | undefined },
planModeAwsBedrockCustomSelected: { default: undefined as boolean | undefined },
planModeAwsBedrockCustomModelBaseId: { default: undefined as string | undefined },
planModeOpenRouterModelId: { default: undefined as string | undefined },
planModeOpenRouterModelInfo: { default: undefined as ModelInfo | undefined },
planModeOpenAiModelId: { default: undefined as string | undefined },
planModeOpenAiModelInfo: { default: undefined as OpenAiCompatibleModelInfo | undefined },
planModeOllamaModelId: { default: undefined as string | undefined },
planModeLmStudioModelId: { default: undefined as string | undefined },
planModeLiteLlmModelId: { default: undefined as string | undefined },
planModeLiteLlmModelInfo: { default: undefined as LiteLLMModelInfo | undefined },
planModeRequestyModelId: { default: undefined as string | undefined },
planModeRequestyModelInfo: { default: undefined as ModelInfo | undefined },
planModeTogetherModelId: { default: undefined as string | undefined },
planModeFireworksModelId: { default: undefined as string | undefined },
planModeSapAiCoreModelId: { default: undefined as string | undefined },
planModeSapAiCoreDeploymentId: { default: undefined as string | undefined },
planModeGroqModelId: { default: undefined as string | undefined },
planModeGroqModelInfo: { default: undefined as ModelInfo | undefined },
planModeBasetenModelId: { default: undefined as string | undefined },
planModeBasetenModelInfo: { default: undefined as ModelInfo | undefined },
planModeHuggingFaceModelId: { default: undefined as string | undefined },
planModeHuggingFaceModelInfo: { default: undefined as ModelInfo | undefined },
planModeHuaweiCloudMaasModelId: { default: undefined as string | undefined },
planModeHuaweiCloudMaasModelInfo: { default: undefined as ModelInfo | undefined },
planModeOcaModelId: { default: undefined as string | undefined },
planModeOcaModelInfo: { default: undefined as OcaModelInfo | undefined },
planModeOcaReasoningEffort: { default: undefined as string | undefined },
planModeAihubmixModelId: { default: undefined as string | undefined },
planModeAihubmixModelInfo: { default: undefined as OpenAiCompatibleModelInfo | undefined },
planModeHicapModelId: { default: undefined as string | undefined },
planModeHicapModelInfo: { default: undefined as ModelInfo | undefined },
planModeNousResearchModelId: { default: undefined as string | undefined },
planModeVercelAiGatewayModelId: { default: undefined as string | undefined },
planModeVercelAiGatewayModelInfo: { default: undefined as ModelInfo | undefined },
// Act mode configurations
actModeApiModelId: { default: undefined as string | undefined },
actModeThinkingBudgetTokens: { default: ANTHROPIC_MIN_THINKING_BUDGET as number | undefined },
geminiActModeThinkingLevel: { default: undefined as string | undefined },
actModeReasoningEffort: { default: undefined as string | undefined },
actModeVerbosity: { default: undefined as string | undefined },
actModeVsCodeLmModelSelector: { default: undefined as LanguageModelChatSelector | undefined },
actModeAwsBedrockCustomSelected: { default: undefined as boolean | undefined },
actModeAwsBedrockCustomModelBaseId: { default: undefined as string | undefined },
actModeOpenRouterModelId: { default: undefined as string | undefined },
actModeOpenRouterModelInfo: { default: undefined as ModelInfo | undefined },
actModeOpenAiModelId: { default: undefined as string | undefined },
actModeOpenAiModelInfo: { default: undefined as OpenAiCompatibleModelInfo | undefined },
actModeOllamaModelId: { default: undefined as string | undefined },
actModeLmStudioModelId: { default: undefined as string | undefined },
actModeLiteLlmModelId: { default: undefined as string | undefined },
actModeLiteLlmModelInfo: { default: undefined as LiteLLMModelInfo | undefined },
actModeRequestyModelId: { default: undefined as string | undefined },
actModeRequestyModelInfo: { default: undefined as ModelInfo | undefined },
actModeTogetherModelId: { default: undefined as string | undefined },
actModeFireworksModelId: { default: undefined as string | undefined },
actModeSapAiCoreModelId: { default: undefined as string | undefined },
actModeSapAiCoreDeploymentId: { default: undefined as string | undefined },
actModeGroqModelId: { default: undefined as string | undefined },
actModeGroqModelInfo: { default: undefined as ModelInfo | undefined },
actModeBasetenModelId: { default: undefined as string | undefined },
actModeBasetenModelInfo: { default: undefined as ModelInfo | undefined },
actModeHuggingFaceModelId: { default: undefined as string | undefined },
actModeHuggingFaceModelInfo: { default: undefined as ModelInfo | undefined },
actModeHuaweiCloudMaasModelId: { default: undefined as string | undefined },
actModeHuaweiCloudMaasModelInfo: { default: undefined as ModelInfo | undefined },
actModeOcaModelId: { default: undefined as string | undefined },
actModeOcaModelInfo: { default: undefined as OcaModelInfo | undefined },
actModeOcaReasoningEffort: { default: undefined as string | undefined },
actModeAihubmixModelId: { default: undefined as string | undefined },
actModeAihubmixModelInfo: { default: undefined as OpenAiCompatibleModelInfo | undefined },
actModeHicapModelId: { default: undefined as string | undefined },
actModeHicapModelInfo: { default: undefined as ModelInfo | undefined },
actModeNousResearchModelId: { default: undefined as string | undefined },
actModeVercelAiGatewayModelId: { default: undefined as string | undefined },
actModeVercelAiGatewayModelInfo: { default: undefined as ModelInfo | undefined },
// Model-specific settings
hicapModelId: string | undefined
// Plan mode configurations
planModeApiProvider: ApiProvider
planModeApiModelId: string | undefined
planModeThinkingBudgetTokens: number | undefined
geminiPlanModeThinkingLevel: string | undefined
planModeReasoningEffort: string | undefined
planModeVsCodeLmModelSelector: LanguageModelChatSelector | undefined
planModeAwsBedrockCustomSelected: boolean | undefined
planModeAwsBedrockCustomModelBaseId: string | undefined
planModeOpenRouterModelId: string | undefined
planModeOpenRouterModelInfo: ModelInfo | undefined
planModeOpenAiModelId: string | undefined
planModeOpenAiModelInfo: ModelInfo | undefined
planModeOllamaModelId: string | undefined
planModeLmStudioModelId: string | undefined
planModeLiteLlmModelId: string | undefined
planModeLiteLlmModelInfo: ModelInfo | undefined
planModeRequestyModelId: string | undefined
planModeRequestyModelInfo: ModelInfo | undefined
planModeTogetherModelId: string | undefined
planModeFireworksModelId: string | undefined
planModeSapAiCoreModelId: string | undefined
planModeSapAiCoreDeploymentId: string | undefined
planModeGroqModelId: string | undefined
planModeGroqModelInfo: ModelInfo | undefined
planModeBasetenModelId: string | undefined
planModeBasetenModelInfo: ModelInfo | undefined
planModeHuggingFaceModelId: string | undefined
planModeHuggingFaceModelInfo: ModelInfo | undefined
planModeHuaweiCloudMaasModelId: string | undefined
planModeHuaweiCloudMaasModelInfo: ModelInfo | undefined
planModeOcaModelId: string | undefined
planModeOcaModelInfo: OcaModelInfo | undefined
planModeOcaReasoningEffort: string | undefined
planModeHicapModelId: string | undefined
planModeHicapModelInfo: ModelInfo | undefined
planModeAihubmixModelId: string | undefined
planModeAihubmixModelInfo: ModelInfo | undefined
planModeNousResearchModelId: string | undefined
planModeVercelAiGatewayModelId: string | undefined
planModeVercelAiGatewayModelInfo: ModelInfo | undefined
// Act mode configurations
actModeApiProvider: ApiProvider
actModeApiModelId: string | undefined
actModeThinkingBudgetTokens: number | undefined
geminiActModeThinkingLevel: string | undefined
actModeReasoningEffort: string | undefined
actModeVsCodeLmModelSelector: LanguageModelChatSelector | undefined
actModeAwsBedrockCustomSelected: boolean | undefined
actModeAwsBedrockCustomModelBaseId: string | undefined
actModeOpenRouterModelId: string | undefined
actModeOpenRouterModelInfo: ModelInfo | undefined
actModeOpenAiModelId: string | undefined
actModeOpenAiModelInfo: ModelInfo | undefined
actModeOllamaModelId: string | undefined
actModeLmStudioModelId: string | undefined
actModeLiteLlmModelId: string | undefined
actModeLiteLlmModelInfo: ModelInfo | undefined
actModeRequestyModelId: string | undefined
actModeRequestyModelInfo: ModelInfo | undefined
actModeTogetherModelId: string | undefined
actModeFireworksModelId: string | undefined
actModeSapAiCoreModelId: string | undefined
actModeSapAiCoreDeploymentId: string | undefined
actModeGroqModelId: string | undefined
actModeGroqModelInfo: ModelInfo | undefined
actModeBasetenModelId: string | undefined
actModeBasetenModelInfo: ModelInfo | undefined
actModeHuggingFaceModelId: string | undefined
actModeHuggingFaceModelInfo: ModelInfo | undefined
actModeHuaweiCloudMaasModelId: string | undefined
actModeHuaweiCloudMaasModelInfo: ModelInfo | undefined
actModeOcaModelId: string | undefined
actModeOcaModelInfo: OcaModelInfo | undefined
actModeOcaReasoningEffort: string | undefined
actModeHicapModelId: string | undefined
actModeHicapModelInfo: ModelInfo | undefined
actModeAihubmixModelId: string | undefined
actModeAihubmixModelInfo: ModelInfo | undefined
actModeNousResearchModelId: string | undefined
actModeVercelAiGatewayModelId: string | undefined
actModeVercelAiGatewayModelInfo: ModelInfo | undefined
planModeApiProvider: { default: DEFAULT_API_PROVIDER as ApiProvider },
actModeApiProvider: { default: DEFAULT_API_PROVIDER as ApiProvider },
// Deprecated model settings
hicapModelId: { default: undefined as string | undefined },
lmStudioModelId: { default: undefined as string | undefined },
} satisfies FieldDefinitions
const USER_SETTINGS_FIELDS = {
// Settings that are NOT part of ApiHandlerOptions
autoApprovalSettings: {
default: DEFAULT_AUTO_APPROVAL_SETTINGS as AutoApprovalSettings,
},
globalClineRulesToggles: { default: {} as ClineRulesToggles },
globalWorkflowToggles: { default: {} as ClineRulesToggles },
globalSkillsToggles: { default: {} as Record<string, boolean> },
browserSettings: {
default: DEFAULT_BROWSER_SETTINGS as BrowserSettings,
transform: (v: any) => ({ ...DEFAULT_BROWSER_SETTINGS, ...v }),
},
telemetrySetting: { default: "unset" as TelemetrySetting },
planActSeparateModelsSetting: { default: false as boolean, isComputed: true },
enableCheckpointsSetting: { default: true as boolean },
shellIntegrationTimeout: { default: 4000 as number },
defaultTerminalProfile: { default: "default" as string },
terminalOutputLineLimit: { default: 500 as number },
maxConsecutiveMistakes: { default: 3 as number },
subagentTerminalOutputLineLimit: { default: 2000 as number },
strictPlanModeEnabled: { default: true as boolean },
yoloModeToggled: { default: false as boolean },
useAutoCondense: { default: false as boolean },
clineWebToolsEnabled: { default: true as boolean },
preferredLanguage: { default: "English" as string },
openaiReasoningEffort: { default: "medium" as OpenaiReasoningEffort },
mode: { default: "act" as Mode },
dictationSettings: {
default: DEFAULT_DICTATION_SETTINGS as DictationSettings,
transform: (v: any) => ({ ...DEFAULT_DICTATION_SETTINGS, ...v }),
},
focusChainSettings: { default: DEFAULT_FOCUS_CHAIN_SETTINGS as FocusChainSettings },
customPrompt: { default: undefined as "compact" | undefined },
autoCondenseThreshold: { default: 0.75 as number }, // number from 0 to 1
hooksEnabled: { default: false as boolean },
subagentsEnabled: { default: false as boolean },
enableParallelToolCalling: { default: false as boolean },
backgroundEditEnabled: { default: false as boolean },
skillsEnabled: { default: false as boolean },
optOutOfRemoteConfig: { default: false as boolean },
// OpenTelemetry configuration
openTelemetryEnabled: boolean
openTelemetryMetricsExporter: string | undefined
openTelemetryLogsExporter: string | undefined
openTelemetryOtlpProtocol: string
openTelemetryOtlpEndpoint: string
openTelemetryOtlpMetricsProtocol: string | undefined
openTelemetryOtlpMetricsEndpoint: string | undefined
openTelemetryOtlpLogsProtocol: string | undefined
openTelemetryOtlpLogsEndpoint: string | undefined
openTelemetryMetricExportInterval: number
openTelemetryOtlpInsecure: boolean
openTelemetryLogBatchSize: number
openTelemetryLogBatchTimeout: number
openTelemetryLogMaxQueueSize: number
openTelemetryEnabled: { default: true as boolean },
openTelemetryMetricsExporter: { default: undefined as string | undefined },
openTelemetryLogsExporter: { default: undefined as string | undefined },
openTelemetryOtlpProtocol: { default: "http/json" as string | undefined },
openTelemetryOtlpEndpoint: { default: "http://localhost:4318" as string | undefined },
openTelemetryOtlpMetricsProtocol: { default: undefined as string | undefined },
openTelemetryOtlpMetricsEndpoint: { default: undefined as string | undefined },
openTelemetryOtlpLogsProtocol: { default: undefined as string | undefined },
openTelemetryOtlpLogsEndpoint: { default: undefined as string | undefined },
openTelemetryMetricExportInterval: { default: 60000 as number | undefined },
openTelemetryOtlpInsecure: { default: false as boolean | undefined },
openTelemetryLogBatchSize: { default: 512 as number | undefined },
openTelemetryLogBatchTimeout: { default: 5000 as number | undefined },
openTelemetryLogMaxQueueSize: { default: 2048 as number | undefined },
} satisfies FieldDefinitions
const SETTINGS_FIELDS = { ...API_HANDLER_SETTINGS_FIELDS, ...USER_SETTINGS_FIELDS }
const GLOBAL_STATE_AND_SETTINGS_FIELDS = { ...GLOBAL_STATE_FIELDS, ...SETTINGS_FIELDS }
// ============================================================================
// SECRET KEYS AND LOCAL STATE - Static definitions
// ============================================================================
// Secret keys used in Api Configuration
const SECRETS_KEYS = [
"apiKey",
"clineAccountId", // Cline Account ID for Firebase
"cline:clineAccountId",
"openRouterApiKey",
"awsAccessKey",
"awsSecretKey",
"awsSessionToken",
"awsBedrockApiKey",
"openAiApiKey",
"geminiApiKey",
"openAiNativeApiKey",
"ollamaApiKey",
"deepSeekApiKey",
"requestyApiKey",
"togetherApiKey",
"fireworksApiKey",
"qwenApiKey",
"doubaoApiKey",
"mistralApiKey",
"liteLlmApiKey",
"authNonce",
"asksageApiKey",
"xaiApiKey",
"moonshotApiKey",
"zaiApiKey",
"huggingFaceApiKey",
"nebiusApiKey",
"sambanovaApiKey",
"cerebrasApiKey",
"sapAiCoreClientId",
"sapAiCoreClientSecret",
"groqApiKey",
"huaweiCloudMaasApiKey",
"basetenApiKey",
"vercelAiGatewayApiKey",
"difyApiKey",
"minimaxApiKey",
"hicapApiKey",
"aihubmixApiKey",
"nousResearchApiKey",
"remoteLiteLlmApiKey",
"ocaApiKey",
"ocaRefreshToken",
"mcpOAuthSecrets",
] as const
export const LocalStateKeys = [
"localClineRulesToggles",
"localCursorRulesToggles",
"localWindsurfRulesToggles",
"localAgentsRulesToggles",
"localSkillsToggles",
"workflowToggles",
] as const
// ============================================================================
// GENERATED TYPES - Auto-generated from property definitions
// ============================================================================
type ExtractDefault<T> = T extends { default: infer U } ? U : never
type BuildInterface<T extends Record<string, { default: any }>> = { [K in keyof T]: ExtractDefault<T[K]> }
export type GlobalState = BuildInterface<typeof GLOBAL_STATE_FIELDS>
export type Settings = BuildInterface<typeof SETTINGS_FIELDS>
type RemoteConfigExtra = BuildInterface<typeof REMOTE_CONFIG_EXTRA_FIELDS>
export type ApiHandlerOptionSettings = BuildInterface<typeof API_HANDLER_SETTINGS_FIELDS>
export type ApiHandlerSettings = ApiHandlerOptionSettings & Secrets
export type GlobalStateAndSettings = GlobalState & Settings
export type RemoteConfigFields = GlobalStateAndSettings & RemoteConfigExtra
// ============================================================================
// TYPE ALIASES
// ============================================================================
export type Secrets = { [K in (typeof SecretKeys)[number]]: string | undefined }
export type LocalState = { [K in (typeof LocalStateKeys)[number]]: ClineRulesToggles }
export type SecretKey = (typeof SecretKeys)[number]
export type GlobalStateKey = keyof GlobalState
export type LocalStateKey = keyof LocalState
export type SettingsKey = keyof Settings
export type GlobalStateAndSettingsKey = keyof GlobalStateAndSettings
// ============================================================================
// GENERATED KEYS AND LOOKUP SETS - Auto-generated from property definitions
// ============================================================================
const GlobalStateKeys = new Set(Object.keys(GLOBAL_STATE_FIELDS))
const SettingsKeysSet = new Set(Object.keys(SETTINGS_FIELDS))
const GlobalStateAndSettingsKeySet = new Set(Object.keys(GLOBAL_STATE_AND_SETTINGS_FIELDS))
const ApiHandlerSettingsKeysSet = new Set(Object.keys(API_HANDLER_SETTINGS_FIELDS))
export const SecretKeys = Array.from(SECRETS_KEYS)
export const SettingsKeys = Array.from(SettingsKeysSet) as (keyof Settings)[]
export const ApiHandlerSettingsKeys = Array.from(ApiHandlerSettingsKeysSet) as (keyof ApiHandlerOptionSettings)[]
export const GlobalStateAndSettingKeys = Array.from(GlobalStateAndSettingsKeySet) as GlobalStateAndSettingsKey[]
// GENERATED DEFAULTS - Auto-generated from property definitions
// ============================================================================
export const GLOBAL_STATE_DEFAULTS = extractDefaults(GLOBAL_STATE_FIELDS)
export const SETTINGS_DEFAULTS = extractDefaults(SETTINGS_FIELDS)
export const SETTINGS_TRANSFORMS = extractTransforms(SETTINGS_FIELDS)
export const ASYNC_PROPERTIES = extractMetadata({ ...GLOBAL_STATE_FIELDS, ...SETTINGS_FIELDS }, "isAsync")
export const COMPUTED_PROPERTIES = extractMetadata({ ...GLOBAL_STATE_FIELDS, ...SETTINGS_FIELDS }, "isComputed")
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
export const isGlobalStateKey = (key: string): key is GlobalStateKey => GlobalStateKeys.has(key)
export const isSettingsKey = (key: string): key is SettingsKey => SettingsKeysSet.has(key)
export const isSecretKey = (key: string): key is SecretKey => new Set(SECRETS_KEYS).has(key as SecretKey)
export const isLocalStateKey = (key: string): key is LocalStateKey => new Set(LocalStateKeys).has(key as LocalStateKey)
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
export const isAsyncProperty = (key: string): boolean => ASYNC_PROPERTIES.has(key)
export const isComputedProperty = (key: string): boolean => COMPUTED_PROPERTIES.has(key)
export const getDefaultValue = <K extends GlobalStateAndSettingsKey>(key: K): GlobalStateAndSettings[K] | undefined => {
return ((GLOBAL_STATE_DEFAULTS as any)[key] ?? (SETTINGS_DEFAULTS as any)[key]) as GlobalStateAndSettings[K] | undefined
}
export interface Secrets {
apiKey: string | undefined
clineAccountId: string | undefined
"cline:clineAccountId": string | undefined // Auth_Provider:AccountId
openRouterApiKey: string | undefined
awsAccessKey: string | undefined
awsSecretKey: string | undefined
awsSessionToken: string | undefined
awsBedrockApiKey: string | undefined
openAiApiKey: string | undefined
geminiApiKey: string | undefined
openAiNativeApiKey: string | undefined
ollamaApiKey: string | undefined
deepSeekApiKey: string | undefined
requestyApiKey: string | undefined
togetherApiKey: string | undefined
fireworksApiKey: string | undefined
qwenApiKey: string | undefined
doubaoApiKey: string | undefined
mistralApiKey: string | undefined
liteLlmApiKey: string | undefined
remoteLiteLlmApiKey: string | undefined
authNonce: string | undefined
asksageApiKey: string | undefined
xaiApiKey: string | undefined
moonshotApiKey: string | undefined
zaiApiKey: string | undefined
huggingFaceApiKey: string | undefined
nebiusApiKey: string | undefined
sambanovaApiKey: string | undefined
cerebrasApiKey: string | undefined
sapAiCoreClientId: string | undefined
sapAiCoreClientSecret: string | undefined
groqApiKey: string | undefined
huaweiCloudMaasApiKey: string | undefined
basetenApiKey: string | undefined
vercelAiGatewayApiKey: string | undefined
difyApiKey: string | undefined
ocaApiKey: string | undefined
ocaRefreshToken: string | undefined
minimaxApiKey: string | undefined
hicapApiKey: string | undefined
aihubmixApiKey: string | undefined
mcpOAuthSecrets: string | undefined
nousResearchApiKey: string | undefined
openAiCodexAccessToken: string | undefined
openAiCodexRefreshToken: string | undefined
openAiCodexAccountId: string | undefined
export const hasTransform = (key: string): boolean => key in SETTINGS_TRANSFORMS
export const applyTransform = <T>(key: string, value: T): T => {
const transform = SETTINGS_TRANSFORMS[key]
return transform ? transform(value) : value
}
export interface LocalState {
localClineRulesToggles: ClineRulesToggles
localCursorRulesToggles: ClineRulesToggles
localWindsurfRulesToggles: ClineRulesToggles
localAgentsRulesToggles: ClineRulesToggles
workflowToggles: ClineRulesToggles
localSkillsToggles: ClineRulesToggles
function extractDefaults<T extends Record<string, any>>(props: T): Partial<BuildInterface<T>> {
return Object.fromEntries(
Object.entries(props)
.map(([key, prop]) => [key, prop.default])
.filter(([_, value]) => value !== undefined),
) as Partial<BuildInterface<T>>
}
function extractTransforms<T extends Record<string, any>>(props: T): Record<string, (value: any) => any> {
return Object.fromEntries(
Object.entries(props)
.filter(([_, prop]) => "transform" in prop && prop.transform !== undefined)
.map(([key, prop]) => [key, prop.transform]),
)
}
function extractMetadata<T extends Record<string, any>>(props: T, field: string): Set<string> {
return new Set(
Object.entries(props)
.filter(([_, prop]) => field in prop && prop[field] === true)
.map(([key]) => key),
)
}
-3
View File
@@ -16,9 +16,6 @@ e2e.describe("Diff Editor", () => {
await sidebar.getByTestId("send-button").click()
await expect(inputbox).toHaveValue("")
// Loading State initially
await expect(sidebar.getByText("API Request...")).toBeVisible({ timeout: 10000 })
// Back to home page with history
await sidebar.getByRole("button", { name: "Start New Task" }).click()
await expect(sidebar.getByText("Recent Tasks")).toBeVisible()
+2 -1
View File
@@ -97,7 +97,8 @@ export class E2ETestHelper {
return null
}
await E2ETestHelper.waitUntil(async () => (await findSidebarFrame()) !== null)
// Use longer timeout (30s) for sidebar - macOS CI runners can be slow
await E2ETestHelper.waitUntil(async () => (await findSidebarFrame()) !== null, 30000)
return (await findSidebarFrame()) || page.mainFrame()
}
+15
View File
@@ -0,0 +1,15 @@
declare module "picomatch" {
type PicomatchOptions = {
dot?: boolean
nocase?: boolean
ignore?: string | string[]
posix?: boolean
windows?: boolean
}
type PicomatchMatcher = (input: string) => boolean
function picomatch(pattern: string | string[], options?: PicomatchOptions): PicomatchMatcher
export default picomatch
}
+1
View File
@@ -21,6 +21,7 @@
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz",
"integrity": "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
+1
View File
@@ -17,6 +17,7 @@
],
"typeRoots": [
"./node_modules/@types",
"./src/types",
"./src/test/types"
],
"outDir": "out",
+5 -1
View File
@@ -2,7 +2,11 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node"
"moduleResolution": "node",
"typeRoots": [
"./node_modules/@types",
"./src/types"
]
},
"ts-node": {
"require": [
+236 -6
View File
@@ -511,7 +511,8 @@ const createErrorMessages = () => [
"text",
"I can see there are TypeScript errors in your code. Let me examine the files and fix these issues.",
),
createMessage(3.3, "say", "tool", JSON.stringify({ tool: "readFile", path: "src/components/UserProfile.tsx" })),
createMessage(3.3, "say", "tool", JSON.stringify({ tool: "readFile", path: "src/components/UserProfile_1.tsx" })),
createMessage(3.3, "say", "tool", JSON.stringify({ tool: "readFile", path: "src/components/UserProfile_2.tsx" })),
createMessage(
3,
"say",
@@ -569,7 +570,7 @@ const createPlanModeMessages = () => [
createApiReqMessage(4.5, "Detailed planning request", { tokensIn: 20002, tokensOut: 12500, cost: 0.095 }),
createAskMessage(
"plan_mode_respond",
"Here's my comprehensive plan for refactoring your React application with TypeScript migration and performance optimization phases.",
"Here's my comprehensive plan for refactoring your React application with TypeScript migration and performance optimization phases.\n\n\n\n\nPhase 1: TypeScript Migration\n1. Set up TypeScript in the project\n2. Rename .js files to .tsx/.ts\n3. Add type definitions for components and props\n4. Fix type errors and ensure type safety\n\nPhase 2: Performance Optimization\n1. Analyze current performance bottlenecks\n2. Implement code-splitting and lazy loading\n3. Optimize rendering with React.memo and useCallback\n4. Minimize bundle size with tree-shaking and minification\n5. Test performance improvements using profiling tools",
),
]
@@ -630,7 +631,7 @@ export const BrowserAutomation: Story = {
const createToolApprovalMessages = () => [
createMessage(5, "say", "task", "Help me read the configuration file"),
createMessage(4.7, "say", "text", "I need to read a file to understand your configuration."),
createAskMessage("tool", JSON.stringify({ tool: "read_file", path: "config.json" })),
createAskMessage("tool", JSON.stringify({ tool: "readFile", path: "config.json" })),
]
export const ToolApproval: Story = {
@@ -676,6 +677,7 @@ const quickStory = (
clineMessages: [
...createLongMessages(),
createMessage(6, "say", "task", `Help with ${name.toLowerCase()}`),
createMessage(5, "say", "reasoning", `Thinking about helping user with ${name.toLowerCase()}`),
createMessage(4.7, "say", "text", `I'll help you with ${name.toLowerCase()}.`),
createAskMessage(askType, text, streamingFailedMessage),
],
@@ -725,7 +727,7 @@ export const MistakeLimitReached = quickStory(
export const CompletionResult = quickStory(
"Task Completion",
"completion_result",
"Task completed successfully! I've implemented all the requested features.",
"Task completed successfully! I've implemented all the requested features.\n\nWould you like to start a new task?\n\n- View Changes\n- Start New Task\n- Resume Previous Task HAS_CHANGES",
"Shows task completion state with Start New Task button.",
)
export const BrowserActionLaunch = quickStory(
@@ -772,7 +774,7 @@ export const ApiRequestActive: Story = {
export const PlanModeResponse = quickStory(
"Plan Mode Response",
"plan_mode_respond",
"Here's my detailed plan for creating a comprehensive testing strategy.",
"Here's my comprehensive plan for refactoring your React application with TypeScript migration and performance optimization phases.\n\n\n\n\nPhase 1: TypeScript Migration\n1. Set up TypeScript in the project\n2. Rename .js files to .tsx/.ts\n3. Add type definitions for components and props\n4. Fix type errors and ensure type safety\n\nPhase 2: Performance Optimization\n1. Analyze current performance bottlenecks\n2. Implement code-splitting and lazy loading\n3. Optimize rendering with React.memo and useCallback\n4. Minimize bundle size with tree-shaking and minification\n5. Test performance improvements using profiling tools",
"Shows plan mode response where Cline presents a detailed plan for user approval.",
)
export const CondenseConversation = quickStory(
@@ -784,7 +786,10 @@ export const CondenseConversation = quickStory(
export const ReportBug = quickStory(
"Report Bug",
"report_bug",
"Would you like to report this issue to help improve Cline?",
JSON.stringify({
steps_to_reproduce: "1. Open Cline\n2. Start a new task\n3. Observe the error",
what_happened: "Cline crashes unexpectedly",
}),
"Shows utility action to report bugs to the GitHub repository.",
)
export const ResumeCompletedTask = quickStory(
@@ -794,6 +799,231 @@ export const ResumeCompletedTask = quickStory(
"Shows Start New Task option for resume completed task.",
)
export const ShellIntegrationWarningWithSuggestion: Story = {
decorators: [
createStoryDecorator({
clineMessages: [
createMessage(5, "say", "task", "Run a command"),
createMessage(4.7, "say", "text", "I'll run the command for you."),
createMessage(4.5, "say", "shell_integration_warning_with_suggestion", ""),
],
vscodeTerminalExecutionMode: "integrated",
}),
],
parameters: {
docs: {
description: {
story: "Shows shell integration warning with suggestion to enable Background Terminal mode.",
},
},
},
}
export const ShellIntegrationWarningBackgroundEnabled: Story = {
decorators: [
createStoryDecorator({
clineMessages: [
createMessage(5, "say", "task", "Run a command"),
createMessage(4.7, "say", "text", "I'll run the command for you."),
createMessage(4.5, "say", "shell_integration_warning_with_suggestion", ""),
],
vscodeTerminalExecutionMode: "backgroundExec",
}),
],
parameters: {
docs: {
description: {
story: "Shows shell integration warning when Background Terminal mode is already enabled.",
},
},
},
}
export const ShellIntegrationWarning: Story = {
decorators: [
createStoryDecorator({
clineMessages: [
createMessage(5, "say", "task", "Run a command"),
createMessage(4.7, "say", "text", "I'll run the command for you."),
createMessage(4.5, "say", "shell_integration_warning", ""),
],
}),
],
parameters: {
docs: {
description: {
story: "Shows shell integration unavailable warning with instructions to update VSCode and select a supported shell.",
},
},
},
}
export const ErrorRetryInProgress: Story = {
decorators: [
createStoryDecorator({
clineMessages: [
createMessage(5, "say", "task", "Process a request"),
createMessage(4.7, "say", "text", "Attempting to process your request."),
createMessage(
4.5,
"say",
"error_retry",
JSON.stringify({ attempt: 2, maxAttempts: 5, delaySeconds: 10, failed: false }),
),
],
}),
],
parameters: {
docs: {
description: {
story: "Shows auto-retry in progress with attempt count and delay.",
},
},
},
}
export const ErrorRetryFailed: Story = {
decorators: [
createStoryDecorator({
clineMessages: [
createMessage(5, "say", "task", "Process a request"),
createMessage(4.7, "say", "text", "Attempting to process your request."),
createMessage(
4.5,
"say",
"error_retry",
JSON.stringify({ attempt: 5, maxAttempts: 5, delaySeconds: 0, failed: true }),
),
],
}),
],
parameters: {
docs: {
description: {
story: "Shows auto-retry failed after max attempts with manual intervention required.",
},
},
},
}
export const GenerateExplanationInProgress: Story = {
decorators: [
createStoryDecorator({
clineMessages: [
createMessage(5, "say", "task", "Explain my recent changes"),
createMessage(4.7, "say", "text", "I'll generate an explanation of your changes."),
createMessage(
4.5,
"say",
"generate_explanation",
JSON.stringify({
title: "Authentication refactor",
fromRef: "abc123def",
toRef: "working directory",
status: "generating",
}),
),
],
}),
],
parameters: {
docs: {
description: {
story: "Shows explanation generation in progress with spinner.",
},
},
},
}
export const GenerateExplanationComplete: Story = {
decorators: [
createStoryDecorator({
clineMessages: [
createMessage(5, "say", "task", "Explain my recent changes"),
createMessage(4.7, "say", "text", "I'll generate an explanation of your changes."),
createMessage(
4.5,
"say",
"generate_explanation",
JSON.stringify({
title: "Authentication refactor",
fromRef: "abc123def",
toRef: "xyz789ghi",
status: "complete",
}),
),
],
}),
],
parameters: {
docs: {
description: {
story: "Shows successfully generated explanation with git refs.",
},
},
},
}
export const GenerateExplanationError: Story = {
decorators: [
createStoryDecorator({
clineMessages: [
createMessage(5, "say", "task", "Explain my recent changes"),
createMessage(4.7, "say", "text", "I'll generate an explanation of your changes."),
createMessage(
4.5,
"say",
"generate_explanation",
JSON.stringify({
title: "Authentication refactor",
fromRef: "abc123def",
toRef: "",
status: "error",
error: "Failed to generate explanation: Git repository not found",
}),
),
],
}),
],
parameters: {
docs: {
description: {
story: "Shows explanation generation error with error message.",
},
},
},
}
export const GenerateExplanationCancelled: Story = {
decorators: [
createStoryDecorator({
clineMessages: [
createMessage(5, "say", "task", "Explain my recent changes"),
createMessage(4.7, "say", "text", "I'll generate an explanation of your changes."),
createMessage(
4.5,
"say",
"generate_explanation",
JSON.stringify({
title: "Authentication refactor",
fromRef: "abc123def",
toRef: "",
status: "generating",
}),
),
createMessage(4.3, "ask", undefined, "Task was cancelled", { ask: "resume_task" }),
],
}),
],
parameters: {
docs: {
description: {
story: "Shows explanation generation cancelled state (detected via resume_task message).",
},
},
},
}
// Diff Edit Stories - New Format
const createNewFormatMultiFileMessages = () => [
createMessage(5, "say", "task", "Help me refactor the authentication module"),
@@ -0,0 +1,10 @@
export const ClineCompactIcon = () => (
<svg height="16" viewBox="0 0 92 96" width="16">
<g fill="currentColor">
<path d="M65.4492701,16.3 C76.3374701,16.3 85.1635558,25.16479 85.1635558,36.1 L85.1635558,42.7 L90.9027661,54.1647464 C91.4694141,55.2966923 91.4668177,56.6300535 90.8957658,57.7597839 L85.1635558,69.1 L85.1635558,75.7 C85.1635558,86.63554 76.3374701,95.5 65.4492701,95.5 L26.0206986,95.5 C15.1328272,95.5 6.30641291,86.63554 6.30641291,75.7 L6.30641291,69.1 L0.448507752,57.7954874 C-0.14693501,56.6464093 -0.149634367,55.2802504 0.441262896,54.1288283 L6.30641291,42.7 L6.30641291,36.1 C6.30641291,25.16479 15.1328272,16.3 26.0206986,16.3 L65.4492701,16.3 Z M62.9301895,22 L29.189529,22 C19.8723267,22 12.3191987,29.5552188 12.3191987,38.875 L12.3191987,44.5 L7.44288578,53.9634655 C6.84794449,55.1180686 6.85066096,56.4896598 7.45017099,57.6418974 L12.3191987,67 L12.3191987,72.625 C12.3191987,81.9450625 19.8723267,89.5 29.189529,89.5 L62.9301895,89.5 C72.2476729,89.5 79.8005198,81.9450625 79.8005198,72.625 L79.8005198,67 L84.5682187,57.6061395 C85.1432011,56.473244 85.1458141,55.1345713 84.5752587,53.9994398 L79.8005198,44.5 L79.8005198,38.875 C79.8005198,29.5552188 72.2476729,22 62.9301895,22 Z" />
<ellipse cx="45.7349843" cy="11" rx="12" ry="14" />
<ellipse cx="33.5" cy="55.5" rx="8" ry="9" />
<ellipse cx="57.5" cy="55.5" rx="8" ry="9" />
</g>
</svg>
)

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