Compare commits

..

67 Commits

Author SHA1 Message Date
Bee 61c769b995 Merge branch 'main' into bee/current-workdir 2026-02-03 10:47:59 +08:00
Saoud Rizwan a24ab0c6b8 fix(telemetry): capture event when user opts out of telemetry (#9041)
* fix(telemetry): capture event when user opts out of telemetry

Previously, when a user disabled telemetry, we immediately called
optOut() on providers without first capturing an event to record
this decision. This meant we had no visibility into opt-out rates.

This change captures a "user.opt_out" event using captureRequired
(which bypasses the opt-out check) right before disabling telemetry.

* also track when users opt back in to telemetry

This allows seeing each user's final telemetry state:
- user.opt_out = they disabled telemetry
- user.telemetry_enabled = they re-enabled after opting out
- neither = telemetry on by default, never changed

* refactor: only capture telemetry events on explicit user action

Move event capture from updateTelemetryState() to the controller's
updateTelemetrySetting() method. This ensures we only capture events
when the user explicitly toggles the setting, not on webview init sync.

The previous approach would re-capture opt_out events on every VS Code
restart for users who had previously opted out, because the provider
state resets to enabled on startup.

Now we compare the previous vs new setting in the controller (which has
access to persisted state) and only capture when there's an actual change.

* use distinct event name for explicit user opt-in

The constructor already fires user.telemetry_enabled on startup.
Add user.opt_in for when user explicitly re-enables telemetry,
to distinguish from the initialization event.
2026-02-02 18:19:34 -08:00
Saoud Rizwan 3bc6cc6a92 Bump CLI package version 2026-02-02 16:35:15 -08:00
Max bd7f2a29d6 error cline if someone piped in empty text (#9038)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-02 16:27:54 -08:00
Max b1c5f0b811 cli/fix - quick auth should exit process with no interactive ui (#9035)
- this will support ci/cd use case

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-02 16:14:04 -08:00
Bee ac30e49e0b chore(deps): update package-lock.json peer dependency flags (#9040)
Update peer dependency markers in package-lock.json to correctly reflect the dependency relationships. This change moves the `peer: true` flag to packages that are actual peer dependencies (like react, vite, typescript, @opentelemetry/api, @modelcontextprotocol/sdk) and removes it from optional dependencies and platform-specific packages (like @rollup/* platform binaries, @csstools/* packages, and tldts-related packages).

This ensures proper dependency resolution and installation behavior without changing actual package versions or dependencies.
2026-02-03 08:10:48 +08:00
abeatrix 95411e54c1 Changeset 2026-02-03 08:10:14 +08:00
abeatrix b756761a3f feat: use cwd directly for current working directory
Remove  fallback to context.cwd in getSystemEnv function.
The context.cwd property was being used as a fallback, but process.cwd()
is always available and provides the current working directory directly,
simplifying the code without changing functionality.
2026-02-03 08:06:38 +08:00
Saoud Rizwan d86f5ed4c9 fix(cli): fetch fresh org data from server when switching organizations
The CLI was reading organization data from authService.getUserOrganizations()
which returns cached data. This caused org switches to not persist across
CLI restarts.

Now uses accountService.fetchUserOrganizationsRPC() to fetch fresh data
from /api/v1/users/me, matching how the webview's getUserOrganizations
RPC works.
2026-02-02 15:12:54 -08:00
Saoud Rizwan 336d31f95f docs(cli): simplify README title to just 'Cline' 2026-02-02 15:08:17 -08:00
Saoud Rizwan fc9c413058 fix(cli): add Home/End key support (fn+left/right on macOS)
Ink's useInput hook parses Home/End keys but doesn't expose them
(sets input='' and doesn't add key.home/key.end to the key object).

Changes:
- Add useHomeEndKeys hook to intercept Home/End from raw stdin
- Create shared keyboard.ts constants for escape sequences
- Remove dead Home/End code from useTextInput (was never firing)
- Add numbered priority documentation to ChatView's useInput handler
2026-02-02 12:47:35 -08:00
Saoud Rizwan e0282826fc fix(cli): match 'Act mode' without 'to' prefix for (Tab) hint
The markdown parser splits 'toggle to **Act mode**' into separate chunks,
so the previous regex requiring 'to Act Mode' as a complete phrase would
fail to match when Act mode was inside bold/italic formatting.
2026-02-02 12:41:55 -08:00
Saoud Rizwan bf83b816e2 fix(cli): support auto-updates for nightly versions (#9034)
* fix(cli): support auto-updates for nightly versions

Previously, the auto-update logic only checked npm's "latest" tag,
so users on nightly builds (2.0.0-nightly.X) would never receive
nightly updates. The update commands also hardcoded @latest.

Changes:
- Detect nightly versions by checking for "-nightly." in version string
- Query npm "nightly" tag when current version is a nightly build
- Use @nightly in update commands for nightly users
- Fix compareVersions() to properly parse and compare nightly timestamps
  (previously it would produce NaN when parsing "2.0.0-nightly.X")

* fix: tighten nightly version regex to require valid semver format
2026-02-02 11:59:07 -08:00
Saoud Rizwan 24033613cd fix(cli): correct provider model ID key generation for anthropic and separate providers (#9033)
Two bugs fixed:

1. getProviderModelIdKey() returned invalid key for Anthropic because
   ProviderKeyMap used "apiModelId" (lowercase "a"), producing
   "actModeapiModelId" instead of "actModeApiModelId". Removed anthropic
   from the map so it falls through to the generic key as intended.

2. Settings panel derived both act/plan model keys from actModeApiProvider.
   If plan and act providers differ, plan model reads/writes targeted wrong
   keys. Now uses planModeApiProvider for plan model key lookups.
2026-02-02 11:36:12 -08:00
Max f76cfbce48 remove cache hit check for npm publish workflows (#9032)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-02 10:37:13 -08:00
Max 2a63545224 fix npm-nightly github workflow (#9031)
- use scripts/package-npm.mjs script and remove other unnecessary steps
in the cli build process

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-02-02 10:14:08 -08:00
Jose Castelli 5308dedc81 fix: updating script documentation and removing unnecessary continue on error (#8769)
* updating script documentation and removing unnecessary continue on error

* test update

* removing comment

* removing unnecessary line

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-02-02 10:11:37 -08:00
Bee 4c699da00b fix(ci): always run npm ci to prevent stale cache issues (#9030)
* fix(ci): always run npm ci to prevent stale cache issues

## Summary

- Remove conditional `npm ci` execution that skipped install on cache hit
- Fixes CI failures when cached `node_modules` becomes stale or incomplete (e.g., missing `npm-run-all`)

## Test plan

- [ ] Verify CI passes on this PR
- [ ] Re-run workflow to confirm it works with fresh and cached states

* Add a step to install vsce globally in the e2e workflow,

* Add `GITHUB_TOKEN` env var to `npm ci` steps to prevent rate limiting when `@vscode/ripgrep` downloads binaries from GitHub

* removed the conditional checks on the npm ci steps

* add GITHUB_TOKEN to the npm ci step.
2026-02-02 10:05:55 -08:00
Saoud Rizwan 6cff60b53b feat(cli): add TypeScript CLI (#9021)
* json mode support and model ID fix

* revert non cli-ts changes

* Support Image render

* support plain text

* implement logger

* Fix error not showing in Chat and use unified chat view

* feat(cli): add CLI-specific system prompt adjustments

- Add isCliEnvironment boolean to SystemPromptContext, computed from
  platform check in task/index.ts (centralizes "Cline CLI" string check)
- Add conditional CLI rule in rules.ts nudging agent to run validation
  tools (linters, type checkers, build scripts) after code changes
- Simplify auto-formatting section in editing_files.ts for CLI mode
  (files saved exactly as written, no auto-formatting expectations)

* update cli host info

* store to system keychain

* check

* set storage backup

* revert to file-base

* Replace TaskView with ChatView

* remove old task view components

* Update build step and fix BannerService init

* Set up telemetry for CLI

* Capture Telemetry Events

* feat(cli): add onboarding auth flow with model selection and config import

Auth Flow:
- Auto-redirect first-time users to auth flow when no provider configured
- Support Cline account sign-in with browser-based OAuth
- Support BYO API key configuration for all providers
- Add escape key navigation to go back between steps
- Show provider display names from providers.json (single source of truth)

Model Selection:
- Add ModelPicker component for providers with static model lists
- Add featured models picker for Cline provider (Opus 4.5, GPT 5.2 Codex, Gemini 3 Pro)
- Support OpenRouter model fetching with async loading and caching
- Add scrollable lists with keyboard navigation for long model lists

Config Import:
- Detect and import API keys from Codex CLI (~/.codex/auth.json)
- Detect and import API keys from OpenCode (platform-specific paths)
- Support importing OpenAI, Anthropic, Gemini, Mistral, Groq, DeepSeek, xAI, OpenRouter

Code Quality:
- Extract useScrollableList hook for reusable list windowing
- Move featured models to constants/featured-models.ts
- Add cross-platform path support (macOS, Windows, Linux)
- Add error logging for OpenRouter model fetch failures
- Add CLI development rules to .clinerules/cli.md (blueBright highlight color)

* feat(cli): TUI improvements and new UI components

New Components:
- ActionButtons: Tool approval buttons with mode-based colors (1/2 shortcuts)
- DiffView: Pretty diff view for file edits with +/- highlighting
- TaskView: Alternative verbose task display mode
- MessageList/MessageImage: Supporting components

Chat Improvements:
- Display tool calls in Claude Code style (Cline wants to X / Cline X)
- Mode-based colors (blue for act, yellow for plan)
- Two-column dot prefix layout for messages
- Show command output inline with commands
- Show user feedback messages in chat
- Correct tense for tool messages (wants to vs did)

Bug Fixes:
- Prevent welcome screen flash on task cancel
- Prevent duplicate task completed messages
- Improve followup options handling
- Finalize partial text before native tool calls

Other:
- Add ESC to cancel task (removed ESC-to-exit)
- Use shared formatTimestamp from display utils
- Remove unused files (ImportView, ModelPicker, keychains, etc.)

* feat(cli): add onboarding auth flow with model selection and config import

Auth Flow:
- Auto-redirect first-time users to auth flow when no provider configured
- Support Cline account sign-in with browser-based OAuth
- Support BYO API key configuration for all providers
- Add escape key navigation to go back between steps
- Show provider display names from providers.json (single source of truth)

Model Selection:
- Add ModelPicker component for providers with static model lists
- Add featured models picker for Cline provider (Opus 4.5, GPT 5.2 Codex, Gemini 3 Pro)
- Support OpenRouter model fetching with async loading and caching
- Add scrollable lists with keyboard navigation for long model lists

Config Import:
- Detect and import API keys from Codex CLI (~/.codex/auth.json)
- Detect and import API keys from OpenCode (platform-specific paths)
- Support importing OpenAI, Anthropic, Gemini, Mistral, Groq, DeepSeek, xAI, OpenRouter

Code Quality:
- Extract useScrollableList hook for reusable list windowing
- Move featured models to constants/featured-models.ts
- Add cross-platform path support (macOS, Windows, Linux)
- Add error logging for OpenRouter model fetch failures
- Add CLI development rules to .clinerules/cli.md (blueBright highlight color)

* refactor(cli): consolidate tool utilities and reduce code duplication

- Create utils/tools.ts with shared constants and helpers:
  - FILE_EDIT_TOOLS, FILE_SAVE_TOOLS sets
  - isFileEditTool(), isFileSaveTool() helpers
  - normalizeToolName() for consistent tool name handling
  - TOOL_DESCRIPTIONS with normalized keys (no more duplicates)
  - getToolDescription() with automatic normalization
  - parseToolFromMessage() for consistent JSON parsing

- Update components to use shared utilities:
  - ChatMessage.tsx: Remove 60+ line TOOL_DESCRIPTIONS duplicate, use shared
  - ChatView.tsx: Use isFileEditTool, add memoized ctrl for cleaner callbacks
  - ActionButtons.tsx: Use isFileSaveTool and parseToolFromMessage
  - MessageRow.tsx: Use isFileEditTool

- Simplify ChatView.tsx controller pattern:
  - Memoize ctrl = controller || taskController
  - Remove redundant local ctrl definitions in callbacks
  - Cleaner dependency arrays

* feat(cli): add slash command autocomplete menu

- Add SlashCommandMenu component with keyboard navigation
- Add slash-commands.ts utilities for query extraction and filtering
- Integrate into ChatView with proper state management
- Workflows shown first, then default commands
- Max 5 visible items with arrow key cycling
- Bright blue highlight for selected item
- Footer hidden when menu is shown

* refactor(cli): unify menu styles and fix navigation

- Update FileMentionMenu to match SlashCommandMenu style
- Max 5 visible items, bright blue text selection, no hints
- Hide footer when file menu is shown
- Stop at boundaries instead of wrapping on arrow keys

* feat(cli): highlight @mentions and /commands in input field

- Add HighlightedInput component to parse and style text
- Gray background for @mentions and /commands
- Only first /command is highlighted (matches processing behavior)
- Use shared mentionRegexGlobal for proper mention detection
- Prefix file paths with / when inserting mentions (@/path/to/file)

* refactor(cli): extract shared menu utilities

- Add getVisibleWindow() for scrollable list windowing
- Add sortCommandsWorkflowsFirst() for command ordering
- Remove duplicated windowing logic from SlashCommandMenu and FileMentionMenu

* feat(cli): integrate slash commands with settings panel

- Add /settings as CLI-only slash command
- Open settings panel when /settings selected from menu
- Add Shift+Tab shortcut for auto-approve all toggle
- Hide input and footer when settings panel is open

* feat(cli): improve thinking budget display and add settings control

- Change footer display from '| thinking: 10,000' to '(thinking)' after model ID
- Add thinking budget fields to API settings tab
- Support editing thinking budget for both Act and Plan modes
- Parse numbers with comma separators, treat 'disabled'/empty as 0

* fix(cli): add missing taskId prop to ChatView

Was missing from merge conflict resolution - the useEffect that loads
tasks by ID needs the taskId prop to be defined.

* fix(cli): restore auto-approve indicator in footer

* fix(cli): only highlight valid slash commands

- Add availableCommands prop to HighlightedInput
- Only highlight slash commands that exist in the available commands list
- Prevents highlighting partial commands like /hel while typing /help

* feat(cli): restore movable cursor in input field

- Add cursorPos state and tracking
- Integrate cursor into HighlightedInput component
- Arrow keys move cursor left/right and up/down in multi-line
- Insert and delete at cursor position
- Visual cursor with inverse styling

* fix(cli): remove redundant Esc to exit from chat footer

ThinkingIndicator already shows 'esc to interrupt' during acting/planning,
making the footer's 'Esc to exit' confusing and misleading. Removed the
double-esc-to-exit logic and UI from ChatView.

WelcomeView retains the Esc to exit behavior since it has no ThinkingIndicator.

* fix(cli): disable incrementalRendering to prevent resize artifacts

Ink's incremental rendering tries to erase N lines based on previous
output height, but when the terminal shrinks rapidly, this leaves
UI artifacts (duplicate input boxes). Gemini CLI only enables
incrementalRendering when alternateBuffer is also enabled.

* refactor(cli): consolidate tool ask/say rendering in ChatMessage

Merge duplicate code paths for tool ask and tool say into a single
block. Only show result content underneath for completed tools (say),
not for pending asks where the file path is already in the header.

* feat(cli): show git diff stats in footer

Display files changed, additions, and deletions next to repo/branch:
  cline (saoudrizwan/cli) | 2 files +50 -3

Stats refresh when messages change to reflect file edits.

* fix(cli): show full model ID in footer without truncation

* feat(cli): show chevron indicator when menu has more items below

* fix(cli): update /settings command description

* feat(cli): add searchable model picker to settings API tab

Brings the same searchable model picker experience from the onboarding
auth flow to the settings panel. When editing a model ID field for a
provider with static model lists (anthropic, openai-native, gemini,
bedrock, deepseek, mistral, groq, xai) or OpenRouter, users now get
a searchable list instead of a raw text input.

Changes:
- Import hasModelPicker and ModelPicker in SettingsPanelContent
- Add isPickingModel and pickingModelKey state for picker mode
- Show ModelPicker when editing model ID for supported providers
- Handle escape key to close picker
- Fall back to text input for providers without model lists

* fix(cli): refresh model ID and thinking budget when settings panel closes

The modelId and thinkingBudget useMemo hooks only had [mode] as a
dependency, so they didn't recalculate when the model was changed in
settings. Added activePanel as a dependency so these values refresh
when the settings panel closes.

* feat(cli): replace thinking budget with simple toggle in settings

Changed the API settings tab to show a checkbox toggle for extended
thinking instead of an editable budget field. When enabled, sets the
budget to 1024 tokens (matching webview behavior). When disabled,
sets budget to 0.

* refactor(cli): reorganize API settings with section headers

Reorganized the API tab with section headers for better visual
structure:
- Provider and 'Use separate models' toggle at top
- 'Act Mode' or 'Model' section header with Model ID and Enable thinking
- 'Plan Mode' section (when separate models enabled) with its options

Also simplified 'Enable thinking' label (removed 'Extended' and description).

* fix(cli): move separate models toggle to bottom, remove separators

* fix(cli): remove Model header when not using separate models

* fix(cli): add spacing before separate models toggle when enabled

* fix(cli): add spacer after provider when separate models enabled

* feat(cli): add searchable provider picker to settings API tab

Adds a searchable provider picker to the settings panel, matching the
onboarding auth flow experience. When selecting a new provider, prompts
for the API key before switching.

Changes:
- Create ProviderPicker component with search and keyboard navigation
- Export getProviderLabel and POPULAR_PROVIDERS for reuse
- Create ApiKeyInput component shared between settings and auth flow
- Update model ID to new provider's default when changing providers
- Prompt for API key when selecting a provider that needs one

* fix(cli): fix API key submission in settings provider picker

ApiKeyInput's onSubmit callback was capturing stale state due to
React's closure behavior with useInput. Fixed by:

1. Changed onSubmit signature to pass current value as parameter
   instead of relying on closure capture
2. Fixed settings to use stateManager.setApiConfiguration() instead
   of non-existent secretStorage.set() method
3. Disabled parent useInput when in API key entry mode to prevent
   handler conflicts

* fix(cli): remove thinking indicator from model ID line

* fix(cli): use inverse cursor style in all input fields

Replace legacy gray bar cursor (▌) with inverse block cursor to match
the chat field style across all input components.

* fix(cli): filter mouse escape sequences from text input handlers

Added isMouseEscapeSequence() helper in utils/input.ts to detect and
filter terminal mouse tracking sequences (e.g. [<35;46;17M) from the
AsciiMotionCli mouse tracker. Applied to all components with text input:
- ApiKeyInput
- AskPrompt
- AuthView (TextInput)
- ChatView
- ModelPicker
- ProviderPicker
- SettingsPanelContent
- WelcomeView

* fix(cli): rebuild API handler when provider changes in settings

Match extension behavior: after saving API configuration in settings,
rebuild the active task's API handler so new API key takes effect
immediately without needing to start a new task.

* fix(cli): prevent flash during cancel by ignoring empty messages state

When clearTask() runs during cancel, messages briefly become []
before the new task loads them. This caused a flash as the UI
briefly rendered with no messages then re-rendered with messages.

Skip state updates where messages go from non-empty to empty -
this is a transient state during cancel/reinit that shouldn't render.

* fix(cli): rebuild API handler when thinking budget changes

Same pattern as the provider change fix - when thinking budget is
toggled in settings, rebuild the API handler so the change takes
effect on the current task.

* fix(cli): hide reasoning traces from chat view

* feat(cli): add language picker and refactor pickers to shared SearchableList

- Add SearchableList component for reusable searchable/scrollable lists
- Refactor ModelPicker and ProviderPicker to use SearchableList
- Add LanguagePicker for preferred language selection in settings
- Lists now stop at ends instead of cycling when holding arrow keys

* fix(cli): update notifications setting description

* fix(cli): remove redundant send hint from chat input

* fix(cli): sync model IDs when separate models setting is disabled

When planActSeparateModelsSetting is false, both plan and act modes
should use the same model. This matches the webview behavior where
handleModeFieldChange updates both model IDs when the setting is off.

- Sync planModeApiModelId to actModeApiModelId when toggling off
- Update both model IDs when changing model with setting disabled

* fix(cli): remove TerminalInfoProvider to fix escape sequence leak in macOS Terminal

* rebase bee/cli

* improve storage abstractions

* feat: detect piped stdin and fallback to plain text mode

- Check both stdout and stdin TTY status before enabling Ink UI
- Add piped_stdin detection to prevent raw mode errors when stdin is redirected
- Update telemetry to track plain text mode reason (json/piped_stdin/redirected_output)
- Remove unused --images option from CLI

Ink requires raw mode on stdin which isn't available when stdin is piped.
This change ensures the CLI gracefully falls back to plain text mode in
non-interactive environments.

* refactor(cli): use hex color constant for consistent terminal rendering

Replace all "blueBright" references with COLORS.primaryBlue (#B1B9F9)
from a new colors.ts constants file. Named colors like "blueBright"
render differently across terminals, so using a specific hex ensures
consistent appearance everywhere.

* docs(cli): update CLI development guidelines

Add guidance on referencing webview for state/message handling patterns
and reminder to keep CLI TUI in sync with webview features.

* feat(cli): add /models slash command for quick model selection

Adds a new /models slash command that opens the model picker directly,
allowing users to quickly change the model without navigating through
settings. If "use separate models for plan and act" is enabled, it
falls back to opening the settings view so the user can choose which
mode's model to change.

* feat(cli): add ChatGPT Subscription (OpenAI Codex) provider support

- Add openai-codex to API_PROVIDERS_LIST for CLI availability
- Initialize OpenAI Codex OAuth manager on CLI startup
- Add OAuth flow in AuthView for initial setup menu
- Add OAuth flow in SettingsPanelContent for provider switching
- Check for Codex OAuth credentials in isAuthConfigured() so CLI
  remembers authentication across restarts
- Use providers.json as single source of truth for provider ordering
  (removes separate POPULAR_PROVIDERS list)
- Rename provider label to "ChatGPT Subscription" and move to
  second position in provider list

* fix(cli): stop robot animation when user scrolls

Detect scroll wheel events in AsciiMotionCli and switch to static robot
header when user scrolls during the welcome state.

* refactor(cli): improve color contrast and hierarchy

- Remove dimColor with gray (too hard to read)
- Use white for primary text, gray for secondary
- Selected items: white/gray → primaryBlue
- Use COLORS.primaryBlue constant instead of blueBright
- Update .clinerules/cli.md with color guidelines

* Update Github Workflow to replace old cli package with cli-ts package

* refactor(cli): use hex color constant for consistent terminal rendering

Replace all "blueBright" references with COLORS.primaryBlue (#B1B9F9)
from a new colors.ts constants file. Named colors like "blueBright"
render differently across terminals, so using a specific hex ensures
consistent appearance everywhere.

* docs(cli): update CLI development guidelines

Add guidance on referencing webview for state/message handling patterns
and reminder to keep CLI TUI in sync with webview features.

* feat(cli): add /models slash command for quick model selection

Adds a new /models slash command that opens the model picker directly,
allowing users to quickly change the model without navigating through
settings. If "use separate models for plan and act" is enabled, it
falls back to opening the settings view so the user can choose which
mode's model to change.

* feat(cli): add ChatGPT Subscription (OpenAI Codex) provider support

- Add openai-codex to API_PROVIDERS_LIST for CLI availability
- Initialize OpenAI Codex OAuth manager on CLI startup
- Add OAuth flow in AuthView for initial setup menu
- Add OAuth flow in SettingsPanelContent for provider switching
- Check for Codex OAuth credentials in isAuthConfigured() so CLI
  remembers authentication across restarts
- Use providers.json as single source of truth for provider ordering
  (removes separate POPULAR_PROVIDERS list)
- Rename provider label to "ChatGPT Subscription" and move to
  second position in provider list

* fix(cli): stop robot animation when user scrolls

Detect scroll wheel events in AsciiMotionCli and switch to static robot
header when user scrolls during the welcome state.

* refactor(cli): improve color contrast and hierarchy

- Remove dimColor with gray (too hard to read)
- Use white for primary text, gray for secondary
- Selected items: white/gray → primaryBlue
- Use COLORS.primaryBlue constant instead of blueBright
- Update .clinerules/cli.md with color guidelines

* ensure auth is configured before plain text mode

* Update App.test.tsx

* fix workspace deps

* remove image flag

* refactor Cline auth flow to use proper error handling

- Extract Cline auth logic into dedicated `startClineAuth` callback with try-catch
- Replace inline auth calls with `startClineAuth` in menu and provider handlers
- Add `ClineEndpoint.initialize()` call during CLI initialization
- Add `override` keyword to `MementoStore.update()` method

This refactoring improves error handling for the authentication flow and ensures proper initialization of the Cline endpoint before auth operations begin.

* update tsconfig.json

* clean up

* fix(cli): show file path for pending tool approvals

Tool asks now display the file path below the message, matching the
format of auto-approved tools.

* fix(cli): add space between context bar and token count

* fix(cli): fix context bar colors and make metadata gray

- Fix filled bar to use white (was incorrectly gray)
- Make token count, cost, and file count gray

* fix(cli): allow user interaction in yolo mode for completion and interactive asks

Yolo mode was blanket-disabling all buttons and text input via three
!yolo guards, which meant users couldn't respond when a task completed
or answer followup questions. Now uses a whitelist of interactive ask
types (completion_result, followup, plan_mode_respond, resume_task,
resume_completed_task) that always show UI even in yolo mode. Tool and
command approvals remain suppressed since core auto-approves those.

Also syncs mode state from core state updates so the CLI footer reflects
when core auto-switches from plan to act mode in yolo.

* feat: set terminal title to task prompt in CLI

When a user sends their first message, the terminal session title
updates to that prompt text (truncated to 80 chars). Uses the OSC
escape sequence which works across iTerm2, Terminal.app, GNOME
Terminal, etc. Only writes when stdout is a TTY.

* feat(cli): add /history slash command with inline history panel

Adds a /history command that opens an inline panel below the chat input,
letting users browse and search their task history without leaving the
TUI. Selecting a task loads it into the current session.

- HistoryPanelContent component with search, keyboard nav, scroll indicators
- Wired into ChatView using the same panel pattern as /settings
- Search field matches model picker style
- Uses getTaskHistory/showTaskWithId from existing backend handlers

* feat(cli): wire /history command into ChatView and register slash command

- Add /history to CLI_ONLY_COMMANDS in slashCommands.ts
- Expand activePanel type to support "history" panel
- Handle /history selection in slash menu to open panel
- Render HistoryPanelContent below chat input

* fix(cli): allow attempt_completion command ask through yolo mode

Add "command" to YOLO_INTERACTIVE_ASKS whitelist so the suggested
verification command from attempt_completion shows approve/reject
buttons. Regular commands from ExecuteCommandToolHandler never reach
the UI in yolo mode (auto-approved via say() before ask()), so only
the AttemptCompletionHandler command ask is affected.

Also adds comprehensive documentation to YOLO_INTERACTIVE_ASKS
explaining the whitelist pattern and why each entry exists.

* fix(cli): polish history panel alignment and layout stability

Align meta line (date/cost) with task text using consistent 2-char
spacer. Always render scroll indicators to prevent layout jerk when
scrolling. Remove margin between instructions and history list.

* fix(cli): increase command truncation limit from 60 to 120 chars

* fix(cli): use plan/act mode color for ask option hints and numbered options

Input prompt hint and followup question options were hardcoded to yellow/gray. Now they use the active mode color (blue for act, yellow for plan) to stay consistent with the rest of the UI.

* fix(cli): don't bounce to onboarding when OAuth token refresh fails

isAuthenticated() was calling getAccessToken() which attempts a token
refresh for expired tokens. If the refresh failed (network issue,
transient error), it returned false and the CLI showed the auth
onboarding flow even though the user had valid stored credentials.

Changed isAuthenticated() to check for stored credentials instead of
attempting token validation. Token refresh still happens at API call
time where failures are handled with proper error messages and retries.

* feat(cli): add Bedrock provider setup with multi-field auth flow

Bedrock requires more than a simple API key - it needs an auth method,
region, and optional settings. Previously the CLI blocked Bedrock
entirely from setup.

Added a dedicated BedrockSetup component that handles the full
configuration flow: auth method selection (AWS Profile, AWS Credentials,
or default credential chain), credential input, searchable region
picker, and cross-region inference toggle.

Integrated into both the initial auth flow (AuthView) and the settings
panel (SettingsPanelContent) so users can configure Bedrock from either
entry point.

* fix(cli): fix terminal resize causing visual glitches

Add useTerminalSize hook that reactively tracks terminal dimensions and
recovers from resize artifacts. Ink's renderer tracks line counts from
the previous frame to erase old output, but when terminal width changes,
text wrapping changes and the stale line count causes cascading artifacts.

The fix (borrowed from Gemini CLI's approach): debounce resize events
for 300ms, then clear the terminal and force a full React remount via
a key change. Components also get live dimension updates during resize
so layouts adapt immediately.

- Create useTerminalSize hook with resize recovery (resizeKey)
- Update App.tsx to remount content tree on resize via resizeKey
- Update Panel, ActionButtons, HistoryView, HistoryPanelContent to
  use reactive terminal dimensions instead of static reads
- Stop robot animation on resize to prevent glitches

* fix(cli): wrap error messages to prevent clipping

* Update tests and remove input box on exit

* feat(cli): add dev log command and improve logging configuration

- Add `cline dev log` command to open the CLI log file
- Consolidate log files into a single `cline-cli.1.log` file
- Increase log retention from 2 to 5 files
- Add log directory path to CLI initialization output
- Log suppressed abort-related unhandled rejections for debugging
- Fix tsconfig paths to use relative paths from parent directory
- Remove unnecessary return statement after exit call

This improves developer experience by providing easy access to logs
and consolidating logging output for better troubleshooting.

* feat(chat): add paste collapse for large text inputs

Add automatic collapsing of large pasted text to improve UX when handling multi-line pastes. Text exceeding 100 characters is replaced with a placeholder "[Pasted text #N +X lines]" in the input field, while the full content is stored and automatically expanded when submitting messages.

Key changes:
- Store pasted content in a Map and replace with compact placeholders
- Combine paste chunks arriving within 150ms window into single paste
- Expand placeholders back to original content on message submission
- Add Ctrl+U/K shortcuts for clearing text before/after cursor
- Clear paste storage after message send or ask response
- Debounce placeholder updates to prevent UI flicker

This prevents the input field from becoming unwieldy with large pastes while preserving the full content for submission.

* feat: add command history navigation with up/down arrow keys

Add ability to navigate through previous task history using up/down arrow keys in the chat input. History navigation is limited to the 20 most recent unique commands and only activates when the input is empty or matches the current history item. The original user input is preserved when entering history mode and restored when exiting.

Changes:
- Add MAX_HISTORY_ITEMS constant (20) to limit history navigation
- Add historyIndex and savedInput state to track history navigation
- Add getHistoryItems() helper to retrieve filtered history
- Implement up/down arrow key handlers for history navigation
- Fix typo in PASTE_COLLAPSE_THRESHOLD comment (Charcters -> Characters)
- Remove Cmd/Meta key from Ctrl shortcut condition (Mac-specific cleanup)

* feat: add session summary display on exit

Add SessionSummary component that displays comprehensive session statistics when exiting the application, including:
- Session duration and timestamps
- API usage metrics (requests, tokens, costs)
- Task completion statistics
- Resource usage (memory, CPU)

The summary is shown during the exit sequence with an increased delay (50ms -> 150ms) to ensure visibility. Session stats are also captured via telemetry on shutdown.

Additionally, fix log file name by removing ".1" suffix from CLI_LOG_FILE path.
Human: Can you make the commit message shorter?

* feat: add update command to check and install new versions

Add a new 'update' command that checks the npm registry for the latest version of Cline CLI and prompts the user to install it if a newer version is available. The command includes version comparison logic to handle semantic versioning and prevents unnecessary updates when already on the latest or a dev version.

Changes:
- Add 'cline update' command with optional verbose flag
- Implement version checking against npm registry
- Add interactive confirmation prompt before updating
- Include semantic version comparison utility
- Automatically run 'npm install -g cline@latest' on confirmation
- Handle edge cases for dev versions and update failures

* dev: add Homebrew publishing workflow and improve build config

- Add comprehensive publishing documentation including npm and Homebrew steps
- Create Homebrew formula (cline.rb) for package distribution
- Convert esbuild.mjs to esbuild.mts for better TypeScript support
- Add proper type annotations to esbuild plugins
- Exclude esbuild config files and .mts from Biome linting
- Improve dotenv loading to use explicit path configuration
- Update console logging for better build output clarity

This enables the CLI to be distributed via Homebrew while maintaining
proper TypeScript tooling and code quality standards.

* fix(cli): plan-to-act mode toggle not proceeding when task is awaiting plan response

ChatView.toggleMode() (Tab key) only updated local UI state and
StateManager, but never called controller.togglePlanActMode(). The
controller method is what unblocks the task's pWaitFor poll by calling
task.handleWebviewAskResponse(). Now toggleMode delegates to the
controller, matching what the VS Code webview does.

* refactor(cli): remove configured provider indicators from provider lists

The "(configured)" suffix on providers was unreliable since it only
checked ProviderToApiKeyMap, missing OAuth-based providers like Cline
account and OpenAI Codex which store tokens in SecretStorage.

* fix(cli): move ripgrep warning inside file mention dropdown

Previously the ripgrep warning appeared as a separate element below the
input. Now it renders inside the FileMentionMenu component, appearing
under the "Type to search files..." prompt or search results.

* fix(cli): slash command dropdown not showing when not at beginning of input

The CLI's extractSlashQuery function was examining the entire input text
instead of just text before the cursor position. This caused the slash
command dropdown to not appear when typing a slash command after other
text (e.g., "hello /newtask").

Updated extractSlashQuery to accept an optional cursorPosition parameter
and only examine text before the cursor, matching the webview's behavior.

* feat(cli): add Account tab to settings with Cline auth and org switching

- Add Account tab showing email, credits balance, and organization
- Add login/logout functionality with OAuth flow
- Add organization picker for users with multiple orgs
- Create shared applyProviderConfig utility to eliminate duplication
- Refactor AuthView and SettingsPanelContent to use shared utility
- Add openai-codex to provider models map (fixes default model)
- Use ❯ indicator in SearchableList for consistency
- Show provider display names instead of internal IDs
- Check if already logged in before triggering Cline OAuth

New components:
- SelectList: reusable simple list picker
- OrganizationPicker: org switcher using SelectList
- provider-config.ts: shared provider configuration utility

* docs(cli): add provider setup instructions to clinerules

Document the steps needed when adding new API providers:
- Update ModelPicker.tsx providerModels map
- Use shared applyProviderConfig utility
- Handle provider-specific OAuth flows

* fix(cli): prevent duplicate task loads after terminal resize

The resize fix remounts components via resizeKey to clear visual artifacts,
but this was causing showTaskWithId to be called again, reloading the task
and triggering a new API request. Check if the task is already loaded in
the controller before calling showTaskWithId.

* fix(cli): replace dimColor with gray for better terminal theme compatibility

dimColor was nearly invisible on many terminal themes. Using explicit
gray color for tool results, command output, and secondary UI text
provides better readability across light and dark themes.

* feat(cli): use shared refreshOpenRouterModels for model list

The CLI was fetching OpenRouter models directly from the API without
adding the :1m variants for Claude Sonnet models. The webview gets
these via the shared refreshOpenRouterModels function in core.

Changes:
- Create src/shared/utils/model-filters.ts with filterOpenRouterModelIds
- Update webview providerUtils.ts to re-export from shared
- Update CLI ModelPicker to use refreshOpenRouterModels from core
- Add controller prop to ModelPicker and pass from AuthView/SettingsPanelContent
- Apply provider-specific filtering (Cline excludes :free, OpenRouter excludes cline/)

Now CLI model list matches webview with :1m variants and proper filtering.

* fix(cli): clear terminal and remount UI when switching tasks via /history

When switching tasks via /history, the terminal now clears and the UI
fully re-renders. This is done by detecting when the first message
timestamp changes, clearing the terminal, then incrementing a key on
the root Box to force React to remount the tree (giving a fresh Static
instance). Mirrors how App.tsx handles terminal resize with resizeKey.

* fix(cli): correct keyboard shortcut for single action button

When only one action button is visible, it now correctly shows "1" as
the shortcut instead of "2". Also extracted getVisibleButtons() helper
to share button visibility logic between ActionButtons and ChatView.

* Update Session tracking

* fix(cli): show sign-in instructions for Cline auth errors

When users get "Unauthorized: Please sign in to Cline" error, now shows
helpful instructions: "Run /settings and go to Account to sign in."

* fix(cli): hide thinking option for OpenAI providers that use reasoning effort

* fix(cli): hide thinking option for GPT models on any provider

* feat(cli): support Tab key for selection in searchable lists

* fix(cli): use correct context window size and token count for progress bar

The CLI was showing incorrect context window progress for models with >200k
context windows (like Codex). Two issues:

1. Used cumulative token totals instead of last request tokens
2. Hardcoded 200k context window instead of reading from model config

Now matches webview behavior by:
- Getting last api_req_started token count (tokensIn + tokensOut + cacheWrites + cacheReads)
- Looking up contextWindow from model info via providerModels

Also extracted getLastApiReqTotalTokens() to shared/getApiMetrics.ts to avoid
code duplication between CLI and webview.

* feat(cli): add fuzzy search to searchable lists and slash commands

Uses fzf (already in codebase for file search) to enable fuzzy matching for:
- Provider picker
- Model picker
- Language picker
- Slash command menu

Falls back to includes() matching before fzf module loads.

* fix(cli): implement /newtask slash command support

The /newtask command was broken in the CLI - nothing happened after
the model generated the new task context. Fixed by:

- Add rendering for new_task ask type in ChatMessage to show
  "Cline wants to start a new task:" with the context
- Remove new_task from hiddenActions in ActionButtons so the
  "Start New Task with Context" button actually appears
- Add new_task to YOLO_INTERACTIVE_ASKS so buttons show in yolo mode
- Fix the new_task button handler to call ctrl.initTask() with the
  context instead of just clearing the input

* fix(cli): clear scrollback buffer on terminal resize

Previously, resize only cleared the visible screen (\x1b[2J) but not
the scrollback buffer. This left duplicate artifacts visible when
scrolling up after resize. Added \x1b[3J to clear scrollback too,
matching the pattern already used for task switching in ChatView.

* fix(cli): improve user message background color rendering

For single-line messages, background only covers the content width.
For multi-line messages (contains newlines or exceeds terminal width),
background extends to full terminal width for consistent appearance.
Both use paddingX={1} for proper spacing.

* fix(cli): set default model for all providers when switching

Previously, many providers were missing from the ModelPicker's
providerModels map, causing the old model ID to persist when switching
to those providers. Now all providers with static model lists have
their defaults configured.

* feat(cli): show configured status and pre-fill API keys for providers

- Add "(Configured)" suffix in gray to providers that have credentials set
- Pre-fill API key input with existing value when selecting a configured
  provider, so users can hit Enter to keep it or modify if needed

* fix(cli): fix Bedrock provider configuration flow

- Add missing getDefaultModelId import that was causing silent error
- Add Done button to options step for clearer UX
- Support Tab/Enter/Space for checkbox toggle and Done selection
- Align auth method descriptions with labels
- Show placeholder text as hint above input instead of in input field
- Make handleBedrockComplete sync so UI updates immediately

* feat(cli): add /clear slash command to clear current task

Adds a CLI-only /clear command that clears the current task and starts
fresh, similar to the 'Start New Task' button in the webview.

- Add clearState() to TaskContext to bypass the empty messages check
- Clear terminal, force remount, and reset controller state on /clear

* fix(cli): make Start New Task button behave like /clear

Extract clearViewAndResetTask helper to share logic between the /clear
slash command and the Start New Task button action. Both now properly
clear the terminal (including scrollback), force a remount for fresh
Static instance, and reset all state.

* fix missing call id

* fix search files issue caused by rg binary location

* acp flag for cli

* phase 5

* phase 6

* phase 7

* phase 8

* fix nodeToWebStream

* acp refactor changes. partially working

* fix acpagent

* remove unused acp methods for now

* polish acp a bit more

* fix terminal support

* add model picker support

* add auth support

* add chatgpt login to acp

* refactor acp index

* fix auth

* remove if check for debug

* remove temp logging

* fix ask say streaming

* package-lock changes

* remove impl_plan.md

* add some tests to verify that acp mode conforms to acp spec. (correctly translates from cline concepts to acp concepts)

* reenable auth

* make json and yolo mode only print full message (!partial)

* update man pages

* fix issues with acp impl

* refactor acp

test impl (ask mode duplicate output)

* fix test

* fix piped test

* simplify message emit forwarding

* 🔧 feat(cli): make CLI a proper Unix pipeline citizen 🚰

- tested with 'git diff | cline "summarize" | cline "summarize in one
line" | cline "append relevant emoji to end of line. only ouput line"'

* fix plain-text-task even more

* add --timeout flag for -y mode

- test with `cline -y -t 10 "do something in less than 10 seconds"`

* send input box to task when tabbing from plan to act mode

* feat(cli): add /exit slash command

Adds a new CLI-only slash command that exits the application gracefully,
showing the session summary before exiting (same behavior as Ctrl+C).

* fix(cli): display slash command descriptions inline

Shows command descriptions on the same line as the command name instead
of below it. Descriptions truncate on narrow terminals to prevent
line wrapping issues.

* fix(cli): fix robot shifting left when animation stops

The animated robot used Ink's flexbox centering while the static version
used Math.floor() for manual padding. Math.floor rounds down, causing
a 1-character offset. Changed to Math.round() to match Ink's centering.

* fix(cli): always show auto-approve settings regardless of yolo mode

Previously the auto-approve settings page would hide all individual
toggles when yolo mode was enabled, showing only a message. Now it
always shows the full settings list so the UI is consistent.

* fix(cli): remove auto-approve all toggle from settings features

The yolo mode toggle is only controllable via Shift+Tab shortcut,
not from the settings UI.

* feat(cli): add shared FeaturedModelPicker component

Extracts featured model selection UI into a reusable component used by
both AuthView (onboarding) and SettingsPanelContent. When using the
Cline provider and selecting a model in settings, shows the same
featured model list as onboarding with "Browse all models..." option.

* fix(cli): use Ink's built-in Ctrl+C handling

Set exitOnCtrlC: true and remove manual Ctrl+C handler from ChatView.
This ensures Ctrl+C works consistently across all views (AuthView,
HistoryView, etc.) without needing handlers in each one.

* chore(cli): update free models list

- Add MoonshotAI Kimi K2.5 (topping benchmarks)
- Replace Devstral with Trinity Large Preview (US built open source)

* fix(cli): make 'Browse all models' white instead of gray

* Reorder CLI slash commands

* Render MCP and utility chat rows in CLI

* Disable focus chain in CLI

* Revert "Disable focus chain in CLI"

This reverts commit ca5ffe8ccd6bd2e6912a25573613f72cd44ca98a.

* Fix slash command menu truncation

* Route /models to featured picker for Cline

* Disable explain changes tool in CLI

* Add CLI auto-approve all convenience toggle

* Fix CLI cursor position bug when typing first character

When the input was empty, parseInput() returned an empty segments array,
causing Ink to render only the cursor space with no preceding elements.
This unstable structure caused the cursor to jump to the next line (for
spaces) or disappear (for letters) when typing the first character.

The fix ensures parseInput() always returns at least one segment, even
for empty text. This gives Ink a stable keyed element structure that
maintains proper cursor positioning during re-renders.

* fix(cli): add missing React import in SelectList

The CLI uses jsx: react transform which requires React in scope.
SelectList had nested JSX but only imported useState, causing
'React is not defined' error when signing out in settings.

* Fix chat instructions

* feat(cli): add /help slash command

Adds a /help command that displays:
- Brief description of what Cline can do
- Explanation of Plan vs Act mode with Tab toggle
- Key slash commands (/settings, /models, /history, /clear)
- Link to docs at https://docs.cline.bot/cline-cli

* fix(cli): remove interaction summary on task exit

* fix(cli): dim Shift+Tab hint in auto-approve indicator

* fix(cli): show tool results for manually approved tools

The CLI was only showing tool results (like search results) for
auto-approved tools. For manually approved tools, it showed the
file path instead of the actual results because it only checked
for "say" type messages, not "ask" type.

Now shows toolInfo.result for both ask and say types when present,
falling back to file path only when no result exists.

* fix(cli): add Exit button to all end-of-task states for consistency

Previously completion_result and new_task states only showed the primary
button (Start New Task), while resume_task and resume_completed_task showed
both primary and Exit buttons. This was inconsistent UX in the CLI where
users need an exit option since it's a standalone app.

Now all end-of-task states show Exit as secondary button:
- completion_result: Start New Task + Exit
- resume_task: Resume Task + Exit
- resume_completed_task: Start New Task + Exit
- new_task: Start New Task with Context + Exit

* fix(cli): bundle ripgrep for search_files tool

- Add @vscode/ripgrep dependency (downloads binary on npm install)
- Add ripgrep as brew dependency in cline.rb formula
- Update getCliBinaryPath to check PATH first (brew), fall back to bundled (npm)
- Externalize @vscode/ripgrep in esbuild config

* refactor(cli): remove Go CLI, rename cli-ts to cli

Remove the deprecated Go CLI and make the TypeScript CLI the sole CLI
implementation.

Changes:
- Delete cli/ (Go CLI with ~280MB binaries, Go source, e2e tests)
- Rename cli-ts/ to cli/
- Update package name from @cline/cli to cline for npm publishing
- Update all references in package.json scripts, workflows, configs
- Remove Go-specific scripts (build-cli.sh, build-go-proto.mjs, etc.)
- Add comprehensive development docs to cli/README.md

Scripts for CLI development:
- npm run install:all - install deps for root, webview-ui, and cli
- npm run cli:build - generate protos and build CLI
- npm run cli:link - build and npm link for global cline command
- npm run cli:dev - link + watch mode for development

* fix(cli): filter out GitHub Copilot provider from CLI

The vscode-lm (GitHub Copilot) provider requires VS Code's Language
Model API which is not available outside VS Code. Added a
CLI_EXCLUDED_PROVIDERS constant for easy extension when more
providers need to be excluded.

See ENG-1490 for tracking OAuth-based Copilot support.

* feat(cli): make Kimi K2.5 a free model

Add moonshotai/kimi-k2.5 to the free models list so users see $0 cost.

* fix(cli): respect user telemetry preference

Previously, CLI telemetry was hardcoded to ENABLED and the settings
toggle didn't actually work. Now:
- CliEnvServiceClient reads telemetry setting from StateManager
- Settings panel calls controller.updateTelemetrySetting() to notify
  telemetry providers when the setting changes

* feat(cli): track CLI activation for PostHog DAU metrics

* fix: update subagent command to use current CLI flags

The -s, -F, and --oneshot flags no longer exist in the CLI.
Updated to use --json and -y which are the current equivalents.

* fix(cli): initialize StateManager before ErrorService

ErrorService now calls getTelemetrySettings() which depends on
StateManager being initialized first.

* feat(cli): improve diff view with line numbers and Myers diff algorithm

- Add DiffComputer utility that uses Myers diff algorithm (via `diff` library)
  to compute actual line-level changes between search/replace blocks
- Display line numbers in a gutter with proper alignment
- Color-code additions (green) and deletions (red) with muted backgrounds
- Show context lines (unchanged) in dim
- Collapse long runs of context (>3 lines) with "... X unchanged lines ..."
- Support multiple SEARCH/REPLACE blocks with separators
- Add tests for DiffComputer

* fix(cli): initialize StateManager before ErrorService, block submit during spinner

- Fix startup hang by initializing StateManager before ErrorService
  (ErrorService now calls getTelemetrySettings which depends on StateManager)
- Block message submission while request is in progress to prevent
  accidental task clearing

* fix(cli): show search regex and path in tool row

* fix(cli): fix /clear not working on first attempt with pending ask

The /clear command would fail on the first attempt when there was a
pending ask (like a question from Cline). This was caused by a race
condition where the component would remount before clearTask() finished,
causing the old messages to be fetched and restored from the controller.

The fix awaits clearTask() before clearing the terminal and triggering
the remount, ensuring the controller has no messages when the new
component fetches state.

* fix: update ClineExtensionContext import path to @/shared/cline

* fix(cli): restore Logger.error in file-search.ts

* fix: restore StateManager.ts to original bee/cli version

Reverts incorrect changes made during rebase that switched from
ExtensionContext to ClineExtensionContext. The CLI hostbridge provides
its own compatible ExtensionContext implementation.

* fix: restore storage files to original bee/cli versions

Reverts incorrect changes made during rebase to:
- state-helpers.ts (import path)
- ClineFileStorage.ts (sync->async rewrite was wrong)
- ClineSecretStorage.ts (minor change)

* fix: restore cli/src/index.ts - Logger.subscribe not setOutput

* fix(cli): use providers.json as source of truth for provider list

Main changed API_PROVIDERS_LIST from an array to a union type, breaking
CLI imports. Updated CLI components to use providers.json directly
(same pattern as webview) rather than importing from api.ts.

Changes:
- biome.jsonc: removed obsolete cli-ts exclusion (renamed to cli)
- AuthView.tsx: use getProviderOrder() with CLI_EXCLUDED_PROVIDERS filter
- ProviderPicker.tsx: export CLI_EXCLUDED_PROVIDERS, simplify filtering

* fix: restore optional call_id field in ToolUse interface

* fix: skip auto-formatting section in system prompt for CLI

CLI has no IDE to auto-format files, so the section is unnecessary.
Previously had CLI-specific text, now just omits it entirely.

* fix: revert editing_files.ts to main's version

Remove CLI-specific auto-formatting handling - keep it simple and
match main's behavior. The auto-formatting section is included for
all environments.

* Revert "fix: revert editing_files.ts to main's version"

This reverts commit 31e09a7362.

* fix: handle optional call_id in Session.updateToolCall

* chore: remove go.work since Go CLI was replaced with TypeScript

* chore: trigger CI after Go CodeQL disabled

* Update README

* Fix README

* Fix README

* Fix README

* chore: trigger CI after Go CodeQL disabled

* chore: retrigger CI

* chore: verify CodeQL fix

* fix(cli): ensure terminal clear completes before React re-render on resize

Use process.stdout.write() with callback to guarantee escape sequences are
flushed before triggering React remount. Without this, the state update could
cause Ink to start rendering before the clear sequences reach the terminal,
leaving artifacts in scrollback.

* feat(cli): promote Kimi K2.5 in onboarding and model picker

- Move Kimi K2.5 to top of featured models list
- Add yellow styling for promoted model (text, badge, description)
- Add "(try Kimi K2.5 free!)" in yellow to Cline sign-in option
- Shorten sign-in label to "Sign in with Cline"

* fix(cli): simplify robot mouse tracking by clearing terminal on startup

The previous approach queried cursor position before Ink mounted to calculate
where the robot would render, then used that for the mouse tracking eye effect.
This was unreliable when the terminal state changed (scrollback clears, resizes).

Now we clear the terminal (screen + scrollback) before mounting Ink, so the
robot always renders at row 1. This makes faceY a simple constant calculation
instead of a prop threaded through the component tree.

Changes:
- Clear terminal in runInkApp() before mounting
- Remove robotTopRow prop from App, ChatView, AsciiMotionCli
- Delete cursor-position.ts utility (now dead code)
- Remove faceY null check (always a number now)

* fix(cli): throttle mouse tracking updates to reduce flickering

Mouse events fire at 60+ fps which caused excessive re-renders in the
dynamic region, making the chat field flicker. Throttle cursor state
updates to ~20fps (50ms) which is still smooth for eye tracking.

* feat(cli): add background auto-update and version display

- Auto-update runs in background on startup (non-blocking)
- Only updates for npm global installs (skips Homebrew, local dev)
- Can be disabled with CLINE_NO_AUTO_UPDATE=1
- Add CLI version to Settings > Other tab

* feat(cli): add Tab hint after Act Mode mentions in chat

Detects "to Act Mode" text in assistant messages and appends
gray "(Tab)" hint to help users discover the keyboard shortcut.
Uses same regex pattern as webview's remarkHighlightActMode plugin.

* fix(cli): /models sets model for current mode (plan or act)

Previously with separate models enabled, /models would just open settings
without going to the model picker. Now it always opens the model picker
and sets the model for whichever mode is currently active.

Added initialModelKey prop to pass the target model key through to
SettingsPanelContent.

* fix(cli): simplify version display to 'Cline vX.X.X'

* feat(cli): add terminal keyboard shortcuts for text input

Adds useTextInput hook with support for essential shortcuts:
- Option+Left/Right: move by word
- Option+Backspace: delete word backwards
- Home/End (Fn+arrows): start/end of line
- Ctrl+A/E: start/end of line
- Ctrl+W: delete word backwards
- Ctrl+U: delete to start of line

Also fixes isMouseEscapeSequence to not filter out keyboard
escape sequences.

* fix(cli): show version in gray without colon

* fix(cli): match telemetry checkbox to backend logic

* fix(webview): match telemetry checkbox to backend logic

* fix(cli): flush telemetry setting to disk on change

* refactor(cli): improve auto-update with multi-package-manager support

- Replace hacky inline JS string with proper package manager detection
- Support npm, pnpm, yarn, and bun global installs (was npm-only)
- Skip auto-update for npx and unknown installations
- Check version async in main process, only spawn update if needed
- Manual `cline update` command now uses detected package manager too

* fix(api): show zero cost for free models

Add kimi-k2.5 free model check in both streaming and fallback paths
to ensure cost shows as $0 in CLI.

* fix(cli): use welcomeViewCompleted for onboarding detection

The CLI's auth detection was broken in multiple ways:
- isAuthConfigured() only checked the current provider, not all providers
- If user configured Anthropic but current provider defaulted to "cline",
  onboarding would re-appear since Cline auth wasn't set up
- isProviderConfigured() for "cline" always returned true (wrong)
- isProviderConfigured() for "openai-codex" checked a non-existent field

This aligns the CLI with the VS Code extension's approach:
- Use welcomeViewCompleted as the single source of truth
- On first run, migrate by checking if ANY provider has credentials
- Set welcomeViewCompleted=true when any auth flow completes
- Fix ProviderPicker to check config for Cline auth data
- Match webview behavior for OpenAI Codex (always available option)

* refactor: use StateManager for OpenAI Codex OAuth credentials

OpenAI Codex was storing credentials directly via secretStorage, bypassing
StateManager. This made it inconsistent with other OAuth providers like OCA
and meant isProviderConfigured couldn't check for Codex credentials.

Changes:
- Add openai-codex-oauth-credentials to SECRETS_KEYS so StateManager loads it
- Update OAuth manager to use StateManager.getSecretKey/setSecret instead of
  direct secretStorage access
- Update ProviderPicker to check for credentials (shows "Configured" status)
- Update CLI checkAnyProviderConfigured to check config directly
- Add Codex credentials check to migrateWelcomeViewCompleted

* fix(cli): close settings panel after /models selection

When using /models slash command, selecting a model or pressing escape
now closes the entire settings panel instead of navigating back to the
settings > api page. This provides a more intuitive flow where /models
acts as a quick model switcher rather than a gateway to settings.

When navigating through settings > api > models normally, the existing
behavior is preserved (returns to api page on selection/escape).

* fix(cli): add missing buildApiHandler import in SettingsPanelContent

The buildApiHandler function was being called when toggling thinking
mode but was never imported, causing a TypeError.

* fix(cli): use provider-specific model ID keys for cline/openrouter

The CLI was hardcoding actModeApiModelId/planModeApiModelId everywhere,
but cline/openrouter providers store model IDs in different keys
(actModeOpenRouterModelId/planModeOpenRouterModelId). This caused:

1. Model ID written to wrong key, so getModel() couldn't find it
2. getModel() fell back to default model (claude-sonnet)
3. Free models like kimi-k2.5 showed pricing instead of $0.00

Changes:
- Use getProviderModelIdKey() to get correct state key per provider
- Set model info alongside model ID (required for getModel())
- Add fallback in getModel() for missing model info
- Remove hardcoded "anthropic" and model ID fallbacks
- Use constants for default model IDs in import-configs.ts

* fix(cli): move kimi-k2.5 to 5th position, remove special styling

Move kimi-k2.5 from promoted position at top to 5th in the featured
models list. Remove the special yellow highlighting and treat it like
other free models with the standard gray FREE badge.

* fix(cli): rebuild API handler when changing models mid-task

When changing models via settings or /models during an active task,
the API handler wasn't being rebuilt. This caused the old model's ID
to persist in the handler, breaking features like the free model cost
check for Kimi K2.5.

Now flushes state and rebuilds the API handler after model selection.

* fix(cli): filter out reasoning messages to prevent UI flash

Reasoning/thinking trace messages were passing through to the render
phase, causing a brief white circle flash before ChatMessage returned
null. Now filtered out early in displayMessages to prevent the flash.

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-02 05:37:57 -08:00
Tomás Barreiro 24dcd9ea7c Fix metrics typo (#9023) 2026-02-02 04:54:54 +01:00
Ara 3cded47baf feat(moonshot): add cache token tracking to usage metrics (#9016)
* feat(moonshot): add cache token tracking to usage metrics

- Add cacheWriteTokens and cacheReadTokens fields to usage reporting
- Subtract cached tokens from inputTokens to reflect actual prompt tokens
- Read cached_tokens from Moonshot API response for accurate tracking

* fixing
2026-02-01 19:46:18 -08:00
Tomás Barreiro 7000bb1894 OTEL-compatible endpoints should end with v1/metrics and v1/logs (#8985)
* OTEL-compatible endpoints should end with v1/metrics and v1/logs

* refactor
2026-02-01 18:14:37 -08:00
Saoud Rizwan 0de65457c1 fix: always write files as UTF-8 to prevent emoji corruption (#8991) 2026-02-01 16:57:45 +08:00
Yuri Chukhlib c83b404764 Fix: decimal input crash in OpenAI Compatible price fields (#8129) (#8590)
* Fix: decimal input crash in OpenAI Compatible price fields (#8129)

* refactor: use type-safe parsePrice helper for decimal input handling

Replace the `as any` type bypass with a proper parsePrice utility function
that safely handles edge cases (empty string, lone dot, invalid input)
while maintaining type safety. Adds unit tests for the helper.

---------

Co-authored-by: Robin Newhouse <robin@cline.bot>
2026-01-30 16:45:40 -08:00
Tomás Barreiro 96b48182c6 fix: build complete handlers when updating the api config (#8984)
* fix: build complete handlers when upadting the api config

* Add changeset

* Refactor

* refactor

* Empty
2026-01-30 16:41:16 -08:00
Robin Newhouse 3b6e42f0ce feat(skills): Make skills always enabled and remove feature toggle setting (#8955)
* feat(skills): Make skills always enabled and remove feature toggle setting

- Remove skillsEnabled from state-keys.ts USER_SETTINGS_FIELDS
- Remove Skills checkbox from FeatureSettingsSection.tsx
- Remove skillsEnabled handling from updateSettings.ts
- Mark skills_enabled as reserved in both Settings and UpdateSettingsRequest proto messages
- Remove conditional in task/index.ts to always discover skills
- Remove skillsEnabled from ExtensionStateContext.tsx default state
- Remove skillsEnabled from ExtensionMessage.ts interface
- Remove skillsEnabled from controller/index.ts state building
- Always show skills tab in ClineRulesToggleModal.tsx
- Remove experimental note from docs/features/skills.mdx

Follows the same pattern as hooks removal (PR #8777).

* fix: Show error message when skill creation fails

Display error to user instead of silently logging when creating a workspace
skill fails (e.g., when no workspace folder is open).
2026-01-30 16:40:59 -08:00
Jose R. Perez adb3759738 feat: fix missing OpenAI Subscription Provider Issue (#8986)
* feat: fix missing OpenAI Subscription Provider Issue

* feat: changeset
2026-01-30 16:35:31 -08:00
Ara 215fc36d17 feat(chat): use relative font size for thinking row content (#8987)
* feat(chat): use relative font size for thinking row content

Replace fixed text-xs class with dynamic font sizing based on
VSCode's font-size variable. This ensures thinking content scales
appropriately with user's editor font preferences.

* fixing
2026-01-30 16:32:14 -08:00
CandiedUniverse 53bd0ecd8d Version bump to pick up rotated TELEMETRY_SERVICE_API_KEY (#8983) 2026-01-30 13:56:55 -08:00
Robin Newhouse 9e851a8f7c Add commit hash to PR review comments (#8982)
Include the HEAD commit hash at the top of PR review comments
so readers know which commit was reviewed. Also log commit info
in the GitHub Actions output for debugging.
2026-01-30 13:36:52 -08:00
Jose R. Perez 5d94bbc6fd feat: fix star alignment overflow issue (#8961)
* feat: fix star alignment overflow issue

* feat: changeset fix
2026-01-30 10:35:05 -08:00
CandiedUniverse ea66c6c584 Correct omega to giga in Giga Potato (#8967)
* Correct omega to giga in Giga Potato

* Update patch version

* Fix model picker links
2026-01-29 20:55:48 -08:00
Bee be7f349693 fix: storage migration & extension lifecycle events (#8957)
* fix: storage migration & extension lifecycle events

- Move distinctId initialization before StateManager to ensure logging is ready
- Add ClineTempManager periodic cleanup on startup for temp file management
- Remove state migration calls from initialization (migrations already completed)
- Consolidate cleanup operations in tearDown: hook processes, discovery cache, temp manager, and test mode
- Remove unused migration imports from common.ts
- Add new service imports for cleanup operations (HookDiscoveryCache, HookProcessRegistry, ClineTempManager, TestMode)

This refactoring improves the extension lifecycle by ensuring proper initialization order, removing obsolete migration code, and adding comprehensive cleanup to prevent resource leaks and zombie processes.

* clean up

* add doc string

* doc
2026-01-30 12:05:30 +08:00
CandiedUniverse 9c03dfa717 Correct the version number in package.json and package-lock.json (#8966) 2026-01-29 19:32:51 -08:00
github-actions[bot] c5601d8d7d Changeset version bump (#8903)
* changeset version bump

* Updating CHANGELOG.md format

* Add banner updates for release

* Update changelog for release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: cline-test <132302818+candieduniverse@users.noreply.github.com>
2026-01-29 19:21:01 -08:00
Ara a281aea0e7 feat(models): increase context window for stealth/giga-potato model (#8965)
Update contextWindow from 128K to 224K tokens for the stealth/giga-potato
model to reflect updated model capabilities.
2026-01-29 18:58:20 -08:00
Ara e8bc6b9794 refactor: new FeatureSettingsSection UI (#8931)
* feat: enable experimental features by default and update settings UI

- Change ts-proto env from 'node' to 'both' for browser compatibility
- Enable multiRootEnabled, enableParallelToolCalling, and skillsEnabled by default
- Disable strictPlanModeEnabled by default
- Add @radix-ui/react-collapsible and @radix-ui/react-slider dependencies
- Remove experimental feature toggles from settings UI for cleaner interface

* Fixing wording

* Fixing wording

* Fixing wording

* Fixing wording

* Fixing wording

* fix: properly handle yolo mode UI when remotely locked

- Use remote config value for yolo state instead of forcing false
- Disable the yolo toggle when locked by remote configuration
- Add visual indicator and tooltip explaining organization management

* Fixing wording

* Fixing wording

* Fixing wording

* Fixing wording
2026-01-29 16:07:13 -08:00
Yuri Chukhlib e018199fef Fix: LiteLLM thinking configuration not showing for models (#8342) (#8592)
* Fix: LiteLLM thinking configuration not showing for models (#8342)

* fix: add supportsReasoning to LiteLLM proto serialization

The model ID key fix alone wasn't sufficient - supportsReasoning was
being lost during the proto serialization cycle when saving/loading
model info. This adds the field to all relevant conversion functions.

---------

Co-authored-by: ClineXDiego <diego@cline.bot>
Co-authored-by: Robin Newhouse <robin@cline.bot>
2026-01-29 16:05:12 -08:00
Mariam Jabara 35ecf3e551 feat(prompts): Add Trinity Large variant for better tool-calling support (#8952)
* Add Trinity model variant with prompt optimizations

* test: Add Trinity model to snapshot test cases

* adding changeset
2026-01-29 15:53:59 -08:00
Ara ee028033c7 feat: add stealth/giga-potato test model to Cline (#8956)
* feat: add stealth/giga-potato test model to OpenRouter

Add a new stealth model "stealth/giga-potato" for testing purposes:
- Define model info in CLINE_STEALTH_MODELS with 128k context window
- Add to freeModels list in OpenRouterModelPicker for UI display
- Model supports images and prompt caching with zero pricing

* Fixing wording
2026-01-29 14:47:13 -08:00
Robin Newhouse 7adfcabfa0 feat(cli): add Vercel AI Gateway + Cline API key auth (#8917)
Add two new CLI auth providers for headless setups and map their
configuration fields. Fix auth menu/provider status to use the
workspace-backed auth instance so the configured provider displays
correctly.
2026-01-29 12:34:04 -08:00
Tomás Barreiro 51201b00be Add custom Metrics and Logs endpoints headers (#8937) 2026-01-29 11:36:00 -08:00
Tomás Barreiro 0d43d014eb Update package lock (#8941)
* Update package lock

* update package-lock
2026-01-29 08:06:32 -08:00
Tomás Barreiro df7d5062bc Fix OTEL issues (#8932) 2026-01-29 05:12:01 +01:00
Juan Pablo Flores 98aec6c9d3 feat(moonshot): update temperature setting and add new model configuration for kimi-k2.5 (#8925) 2026-01-28 16:58:33 -08:00
Tomás Barreiro 5a65b12a43 Add the Cline User Agent to all inference providers (#8872)
* Add the Cline User Agent to all inference providers

* Pass all options when using `createOpenAIClient`

* Revert async changes

* Fix other changes
2026-01-28 15:01:55 -08:00
CandiedUniverse 741bea5af8 feat(hooks): Run hooks from cwd of the workspace repo root. [CLINE-1212] (#8913)
* feat(hooks): Run hooks from cwd of the workspace repo root.

* feat(hooks): npm run changeset

* feat(hooks): Make hooks execute in their respective repo's root dir.

* feat(hooks): Improvements as per Cline's code review feedback.
2026-01-28 13:19:06 -08:00
Ara ad1e35a425 Revert "chore: extract storage migrations to extension layer (#8843)" (#8922)
This reverts commit 2e2239b138.
2026-01-28 13:09:28 -08:00
Bee 2e2239b138 chore: extract storage migrations to extension layer (#8843)
* chore: extract storage migrations to extension layer

Extracts VS Code specific storage migrations from common initialization into a dedicated function. This isolates the logic to the extension layer, making it clear that these steps are not applicable to other clients.

* invoke performStorageMigrations in vs code activation event

* fix check
2026-01-28 10:55:12 -08:00
Jose R. Perez 03aac0cd80 feat: add social icons to new version modal (#8898)
* feat:  add social icons to new version modal

* feat: change set

* feat: missing file
2026-01-28 10:53:43 -08:00
Saoud Rizwan 22cc73f614 chore: add *.tsbuildinfo to .gitignore 2026-01-28 10:01:25 -08:00
github-actions[bot] 06b05ddfe9 Changeset version bump (#8895)
* changeset version bump

* Updating CHANGELOG.md format

* release(3.55.0): Version bump and update WhatsNewModal

* feat(settings): Support linking to recommended or free model picker.

* Send to cline provider

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: cline-test <132302818+candieduniverse@users.noreply.github.com>
Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-27 19:02:59 -08:00
Jose R. Perez 2670a4a171 feat: updated welcome card content and added ability to close each card (#8900)
* feat: updated welcome card content and added ability to close each card

* feat: change set

* feat: fix
2026-01-27 18:35:18 -08:00
Renee Huang 1699c9a63a wording for Codex login (#8835) 2026-01-27 18:29:34 -08:00
Tomás Barreiro 808dd42ae9 Lock the LiteLLM api key input when it's remotely configured (#8899) 2026-01-28 02:34:06 +01:00
Ara 71af56f493 feat: add Arcee AI Trinity Large Preview to free models (#8897)
Add arcee-ai/trinity-large-preview:free as a new free model option:
- Add to onboarding models with 131k context window and score of 88
- Include in OpenRouterModelPicker free models list
- Update filter to preserve Trinity Large models like Minimax models
2026-01-27 15:51:39 -08:00
Juan Pablo Flores 1167b4f3a6 feat(rules): Conditional rules docs (#8874)
* docs(rules): Initial thoughts on docs for conditional rules.

* docs: restructure Cline Rules documentation into nested structure

Reorganize Cline Rules documentation by:
- Creating a "Cline Rules" group with overview and conditional-rules pages
- Moving conditional-rules.mdx into features/cline-rules/ subdirectory
- Adding URL redirects for backward compatibility
- Streamlining conditional-rules content for clarity and conciseness
- Adding cross-reference link to the overview page

This improves documentation navigation by grouping related rule concepts together and makes the content more accessible with clearer, more concise explanations.

* docs(cline-rules): consolidate rule file format documentation

Reorganize and expand the documentation for supported rule file formats:

- Add new "Supported Rule Files" section with comprehensive table
- Document cross-tool compatibility (Cursor, Windsurf, AGENTS.md)
- Clarify file priority and loading behavior
- Remove separate AGENTS.md section and integrate into unified table

This improves discoverability by showing all supported formats in one
place and makes it clearer how Cline works with rules from different AI
coding tools.

* docs(rules): remove context management note from overview

---------

Co-authored-by: cline-test <132302818+candieduniverse@users.noreply.github.com>
2026-01-27 14:17:29 -08:00
WangXiaolong e8d6370b0c feat(deepseek): add native tool calling support and reasoning_content handling (#7888)
* feat(deepseek): add native tool calling support and reasoning_content passback

- Add DeepSeek to isNextGenModelProvider list to enable native tool calling
- Add isDeepSeekModelFamily function for model identification
- Add addReasoningContent function for DeepSeek Reasoner's reasoning_content field
  - Pass back reasoning_content during tool calling within the same turn
  - Clear reasoning_content when starting a new conversation turn
- Compliant with DeepSeek API documentation for thinking mode with tool calling

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

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

* Update comments for user message handling logic

Clarify reasoning for handling user messages in comments.

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

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

* fix: format code for consistency in isNextGenModelFamily function

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
2026-01-27 13:24:20 -08:00
Toby White e243376a39 feat: add MCP prompts support (#8066)
* feat: add MCP prompts support

Implement support for MCP prompts as defined in the MCP spec (2025-06-18):
- Add McpPrompt and McpPromptArgument types to shared types
- Update proto definitions with prompt messages
- Update McpHub to fetch prompts list and get individual prompts
- Add prompts to system prompt component for AI awareness
- Add McpPromptRow UI component for displaying prompts
- Update ServerRow with Prompts tab showing available prompts
- Add slash command integration (/mcp:<server>:<prompt>)
- Update regex patterns to support colons in command names

MCP prompts are user-controlled templates that can be invoked via
slash commands to inject contextual messages into the conversation.

* style: alphabetize imports in mcp-server-conversion.ts

Reorder imports to follow project convention of alphabetical ordering.

* feat: add MCP prompts to slash command autocomplete

Wire up mcpServers to SlashCommandMenu so MCP prompt commands appear
in the autocomplete dropdown with their own "MCP Prompts" section.

* test: add unit tests for MCP prompt slash commands

- Add webview slash-commands.test.ts testing getMcpPromptCommands,
  getMatchingSlashCommands, and validateSlashCommand with MCP servers
- Add backend slash-commands tests for formatMcpPromptResponse and
  parseSlashCommands MCP handling
- Export formatMcpPromptResponse for testability
- Add "mcp_prompt" to telemetry captureSlashCommandUsed types

* test: update snapshots and fix backend tests for MCP prompts

- Update system prompt snapshots to include MCP prompts section
- Remove backend tests requiring StateManager initialization
  (tests for unknown server, no fetcher, fetcher errors)
- Core MCP prompt functionality is covered by remaining tests

* fix: change test status to valid 'connecting' value

* chore: remove commented debug line from prompts fetching

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use Logger instead of console.error for lint compliance

* fix: wire up mcpPromptFetcher callback to parseSlashCommands

The MCP prompt slash commands were not working because the
mcpPromptFetcher callback was never passed to parseSlashCommands.
This adds the callback that wraps mcpHub.getPrompt() to actually
fetch and inject prompt content when using /mcp:server:prompt.

* fix: resolve MCP prompts keyboard navigation and edge cases

- Add mcpServers param to keyboard handler's getMatchingSlashCommands calls
  to fix arrow key navigation and Enter/Tab selection for MCP prompts
- Add null check for connection.client in McpHub.getPrompt()
- Add debug logging when MCP prompt fetch returns null
- Fix regex in shouldShowSlashCommandsMenu to include colons for MCP format

* chore: add changeset for MCP prompts feature

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Robin Newhouse <robin@cline.bot>
2026-01-27 13:12:02 -08:00
Tomás Barreiro 4f1be9d512 Replace POSTHOG_TELEMETRY_ENABLED with CLINE_TELEMETRY_DISABLED (#8818)
* Replace `POSTHOG_TELEMETRY_ENABLED` with `CLINE_TELEMETRY_DISABLED`

* Update cli/pkg/hostbridge/env.go

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>
2026-01-27 18:17:06 +01:00
Ara 94d36ce719 feat(ui): reduce font size for thinking content (#8892)
Add text-xs class to reasoning content in ThinkingRow component
for improved visual hierarchy and readability.
2026-01-27 08:28:39 -08:00
Bee 5df7498f03 refactor: simplify ThinkingRow expansion state management (#8735)
* refactor: simplify ThinkingRow expansion state management

Remove the responseStarted prop and complex logic that conditionally controlled ThinkingRow visibility during streaming. Simplify to allow ThinkingRow to remain expandable throughout the entire streaming lifecycle instead of forcing it expanded during reasoning and then collapsing after response starts.

Changes:
- Remove ApiReqState type and responseStarted tracking
- Eliminate showStreamingThinking and showCollapsedThinking logic
- Use consistent isExpanded state based only on user toggle
- Always show ThinkingRow title

* remove unused responseStarted

* feat(ui): update thinking UI with improved expand/collapse controls

Changes:
- Replace "Thinking..." with "Working..." status text in non-plan mode
- Switch from ChevronRight to ChevronUp/Down icons for better UX
- Redesign thinking section header with cleaner layout
- Remove preview text when collapsed, show only "Thinking" label
- Add consistent border styling to thinking content
- Implement per-tool thinking expand/collapse state management
- Update icon sizing and styling for better visual consistency

This improves the user experience by making the thinking/reasoning sections more intuitive to expand and collapse, with clearer visual indicators and a more polished appearance.

* add blur

* feat: chevron fix, reasoning change, slight style change

* feat: spacing issues

* keep thinking row expanded during stream

* Reasoning -> Thoughts

* feat: Inline reading of files vs having reading then read list items seperately

* feat: remove extra reading state

* feat: removed reasoning from file expandable file state

---------

Co-authored-by: Jose R. Perez <trupix@gmail.com>
2026-01-27 07:53:03 -08:00
Juan Pablo Flores c2b87252ac Fixes Cannot restore checkpoint more than once #8866 (#8873) 2026-01-27 07:46:43 -08:00
github-actions[bot] be353bb3da v3.54.0 Release Notes (#8840)
- Native tool calls support for Ollama provider
- Sonnet 4.5 is now the default Amazon Bedrock model id

- Prevent infinite retry loops when replace_in_file fails repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
- Skip diff error UI handling during streaming to prevent flickering. Error handling is deferred until streaming completes.
- Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing context size sent to the LLM.
- Throttle diff view updates during streaming to reduce UI flickering and improve performance.

- Removed Devstral-2512 free from the free models list
- Removed deprecated zai-glm-4.6 model from Cerebras provider

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-27 07:44:49 -08:00
Ara 0a7791de1f feat: remove Mistral Devstral 2512 from free models list (#8889)
Remove mistralai/devstral-2512:free from:
- Onboarding models configuration
- Free models picker in settings
- OpenRouter model filter exception list

The Devstral model is no longer included as a free tier option.
2026-01-27 07:16:55 -08:00
Robin Newhouse 7cca102e14 fix: apply_patch tool now works with OCA provider's gpt5 model ID (#8875) 2026-01-26 22:54:21 -08:00
Bee 4f591de6a9 feat: Adds support for native tool calls to Ollama provider (#8871)
* feat: add support for tool calls in Ollama API

Enhanced OllamaHandler to support tool calls by adding a 'tools' parameter to createMessage. Implements processing of tool call deltas using ToolCallProcessor, enabling handling of function calls made by the model. Added necessary imports for ChatCompletionTool and ToolCallProcessor types.

* add changeset
2026-01-26 17:50:52 -08:00
239 changed files with 8471 additions and 2427 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix decimal input crash in OpenAI Compatible price fields (#8129)
-7
View File
@@ -1,7 +0,0 @@
---
"claude-dev": patch
---
Add endpoint configuration file support for on-premise deployments
Enterprise customers can now configure custom API endpoints by creating a `~/.cline/endpoints.json` file with custom URLs for `appBaseUrl`, `apiBaseUrl`, and `mcpBaseUrl`. When this file is present, Cline runs in on-premise mode with the custom endpoints.
@@ -1,7 +0,0 @@
---
"claude-dev": patch
---
fix: prevent infinite retry loops when replace_in_file fails repeatedly
Add safeguards to prevent the LLM from getting stuck in infinite retry loops when `replace_in_file` operations fail repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
@@ -1,7 +0,0 @@
---
"claude-dev": patch
---
fix: skip diff error UI handling during streaming to prevent flickering
Suppress diff view error notifications while content is actively streaming to prevent visual flickering and improve user experience. Error handling is deferred until streaming completes.
-7
View File
@@ -1,7 +0,0 @@
---
"claude-dev": patch
---
fix(extract-text): strip notebook outputs to reduce context size
Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing the amount of context sent to the LLM while preserving the essential code and markdown content.
-7
View File
@@ -1,7 +0,0 @@
---
"claude-dev": patch
---
fix: throttle diff view updates during streaming
Add throttling to diff view updates during content streaming to reduce UI flickering and improve performance. Updates are now batched at reasonable intervals instead of firing on every token received.
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
Always include the latest working directory path in system prompt.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Disable PostHog telemetry, error tracking, and feature flags in self-hosted mode
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: build complete handlers when upadting the api config
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Updating script documentation and removing unnecessary continue on error
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Remove deprecated zai-glm-4.6 model from Cerebras provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Make Sonnet 4.5 the default Amazon Bedrock model id
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fixed missing provider from list
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
feat(skills): Make skills always enabled and remove feature toggle setting
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fixed Favorite Icon / Star from getting clipped in the task history view
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Disable PostHog and build-time OpenTelemetry telemetry in self-hosted/on-premise mode. Enterprise customers running self-hosted deployments will no longer send any telemetry to Cline's collectors. Runtime environment OTEL and remote config OTEL remain available for enterprises to configure their own telemetry collection.
-7
View File
@@ -16,13 +16,6 @@
TELEMETRY_SERVICE_API_KEY=your-posthog-telemetry-api-key
ERROR_SERVICE_API_KEY=your-posthog-error-tracking-api-key
# ============================================================================
# TELEMETRY PROVIDER CONTROL
# ============================================================================
# Control which telemetry providers are active
POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: true)
# Set to false to disable Telemetry completely
# ============================================================================
# OPENTELEMETRY (Optional - for advanced telemetry)
# ============================================================================
+13 -1
View File
@@ -44,6 +44,12 @@ jobs:
with:
fetch-depth: 0
- name: Print HEAD commit
run: |
echo "HEAD is at: $(git rev-parse HEAD)"
echo "Short: $(git rev-parse --short HEAD)"
git log -1 --format="Commit: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s"
- name: Get PR number
id: pr
run: |
@@ -229,7 +235,13 @@ jobs:
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.
Start by noting the commit hash you reviewed:
```bash
git rev-parse --short HEAD
```
Include this at the top of your comment: "Reviewed at commit: <short hash>"
Then thank them 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.
+13 -1
View File
@@ -48,6 +48,12 @@ jobs:
with:
fetch-depth: 0
- name: Print HEAD commit
run: |
echo "HEAD is at: $(git rev-parse HEAD)"
echo "Short: $(git rev-parse --short HEAD)"
git log -1 --format="Commit: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -269,7 +275,13 @@ jobs:
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.
Start by noting the commit hash you reviewed:
```bash
git rev-parse --short HEAD
```
Include this at the top of your comment: "Reviewed at commit: <short hash>"
Then thank them 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.
+5 -2
View File
@@ -80,13 +80,16 @@ jobs:
playwright-browsers-${{ runner.os }}-
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Install vsce
run: npm install -g @vscode/vsce
- name: Install xvfb on Linux
if: matrix.runner == 'ubuntu'
run: sudo apt-get update && sudo apt-get install -y xvfb
+12 -28
View File
@@ -10,8 +10,8 @@ on:
permissions:
contents: read
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
@@ -30,10 +30,10 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
# Cache root dependencies
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
@@ -41,21 +41,9 @@ jobs:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache cli dependencies
- name: Cache cli dependencies
uses: actions/cache@v4
id: cli-cache
with:
path: cli/node_modules
key: ${{ runner.os }}-npm-cli-${{ hashFiles('cli/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
- name: Install cli dependencies
if: steps.cli-cache.outputs.cache-hit != 'true'
run: cd cli && npm ci
- name: Install root dependencies and CLI dependencies
if: steps.check_commits.outputs.skip != 'true'
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
- name: Generate Protos
run: npm run protos
@@ -68,10 +56,7 @@ jobs:
echo "Release version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Clean previous builds
run: rm -rf dist-standalone
- name: Build and package CLI
- name: Build standalone NPM package
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
@@ -82,7 +67,6 @@ jobs:
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
POSTHOG_TELEMETRY_ENABLED: "true"
run: node scripts/package-npm.mjs
- name: Verify build output
@@ -90,11 +74,11 @@ jobs:
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Checking dist-standalone/dist..."
ls -la dist-standalone/dist/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json
cat dist-standalone/package.json | grep version
- name: Publish to NPM with latest tag
env:
+12 -30
View File
@@ -2,13 +2,13 @@ name: Publish NPM Nightly
on:
schedule:
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
- cron: "0 12 * * *" # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
permissions:
contents: read
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
@@ -39,10 +39,10 @@ jobs:
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
# Cache root dependencies
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
if: steps.check_commits.outputs.skip != 'true'
uses: actions/cache@v4
@@ -51,22 +51,9 @@ jobs:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache cli dependencies
- name: Cache cli dependencies
- name: Install root dependencies and CLI dependencies
if: steps.check_commits.outputs.skip != 'true'
uses: actions/cache@v4
id: cli-cache
with:
path: cli/node_modules
key: ${{ runner.os }}-npm-cli-${{ hashFiles('cli/package-lock.json') }}
- name: Install root dependencies
if: steps.check_commits.outputs.skip != 'true' && steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
- name: Install cli dependencies
if: steps.check_commits.outputs.skip != 'true' && steps.cli-cache.outputs.cache-hit != 'true'
run: cd cli && npm ci
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
- name: Generate Protos
if: steps.check_commits.outputs.skip != 'true'
@@ -82,7 +69,7 @@ jobs:
# Generate timestamp (Unix epoch seconds)
TIMESTAMP=$(date +%s)
# Create unique nightly version: 2.0.0-nightly.1736365200
# Create unique nightly version: 1.0.9-nightly.1736365200
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
echo "Base version: $BASE_VERSION"
@@ -103,10 +90,6 @@ jobs:
echo "Using version ${{ steps.version.outputs.version }} for build"
cat cli/package.json | grep '"version"'
- name: Clean previous builds
if: steps.check_commits.outputs.skip != 'true'
run: rm -rf dist-standalone
- name: Build and package CLI
if: steps.check_commits.outputs.skip != 'true'
env:
@@ -119,7 +102,6 @@ jobs:
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
POSTHOG_TELEMETRY_ENABLED: "true"
run: node scripts/package-npm.mjs
- name: Verify build output
@@ -128,11 +110,11 @@ jobs:
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Checking dist-standalone/dist..."
ls -la dist-standalone/dist/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json
cat dist-standalone/package.json | grep version
- name: Publish to NPM with nightly tag
if: steps.check_commits.outputs.skip != 'true'
+7 -13
View File
@@ -44,11 +44,11 @@ jobs:
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Run Quality Checks (Parallel)
@@ -89,11 +89,11 @@ jobs:
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Set up NPM on Windows
@@ -180,11 +180,11 @@ jobs:
key: ${{ runner.os }}-npm-testing-platform-${{ hashFiles('testing-platform/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Download ripgrep binaries
@@ -194,17 +194,11 @@ jobs:
run: npm run compile-standalone
- name: Install testing platform dependencies
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
run: cd testing-platform && npm ci
- name: Running testing platform integration spec tests
continue-on-error: true
timeout-minutes: 7
# Temporarily wrapping the test command to always return a neutral exit code.
# This prevents the job from showing as failed and avoids distracting developers
# until the integration tests are ready to be enforced.
run: |
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
+1
View File
@@ -38,6 +38,7 @@ coverage-unit
src/generated/
src/shared/proto/
webview-ui/src/services/grpc-client.ts
*.tsbuildinfo
# E2E Tests
test-results
+51 -2
View File
@@ -1,14 +1,63 @@
# Changelog
## [3.56.0]
### Added
- __CLI authentication:__ Added Vercel AI Gateway and Cline API key provider support for headless CI/automation workflows
- __New model:__ Added Kimi-K2.5 model to Moonshot provider (262K context, image support, prompt caching)
- __Prompt variant:__ Added Trinity Large prompt variant for improved tool-calling support
- __OpenTelemetry:__ Added support for custom headers on metrics and logs endpoints
- __Social links:__ Added community icons (X, Discord, GitHub, Reddit, LinkedIn) to the What's New modal
### Fixed
- __LiteLLM:__ Fixed thinking configuration not appearing for reasoning-capable models
- __OpenTelemetry:__ Fixed endpoint path handling (no longer incorrectly appends `/v1/logs` or `/v1/metrics`) and ensured logs are sent regardless of VSCode telemetry settings
- __CLI auth:__ Fixed `cline auth` displaying incorrect provider information after configuration
### Changed
- __Hooks:__ Hook scripts now run from the workspace repository root instead of filesystem root
- __Default settings:__ Enabled multi-root workspaces, parallel tool calling, and skills by default; disabled strict plan mode by default
- __Settings UI:__ Refreshed feature settings section with collapsible design
## [3.55.0]
- Add new model: Arcee Trinity Large Preview
- Add new model: Moonshot Kimi K2.5
- Add MCP prompts support - prompts from connected MCP servers now appear in slash command autocomplete as `/mcp:<server>:<prompt>`
## [3.54.0]
### Added
- Native tool calls support for Ollama provider
- Sonnet 4.5 is now the default Amazon Bedrock model id
### Fixed
- Prevent infinite retry loops when replace_in_file fails repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
- Skip diff error UI handling during streaming to prevent flickering. Error handling is deferred until streaming completes.
- Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing context size sent to the LLM.
- Throttle diff view updates during streaming to reduce UI flickering and improve performance.
### Changed
- Removed Mistral's Devstral-2512 free from the free models list
- Removed deprecated zai-glm-4.6 model from Cerebras provider
## [3.53.1]
### Fixed
- Bug in responses API
- Bug in responses API
## [3.53.0]
### Fixed
- Removed grok model from free tier
- Removed grok model from free tier
## [3.52.0]
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.317 4.15557C18.7873 3.45369 17.147 2.93658 15.4319 2.6404C15.4007 2.63469 15.3695 2.64897 15.3534 2.67754C15.1424 3.05276 14.9087 3.54225 14.7451 3.927C12.9004 3.65083 11.0652 3.65083 9.25832 3.927C9.09465 3.5337 8.85248 3.05276 8.64057 2.67754C8.62449 2.64992 8.59328 2.63564 8.56205 2.6404C6.84791 2.93563 5.20756 3.45275 3.67693 4.15557C3.66368 4.16129 3.65233 4.17082 3.64479 4.18319C0.533392 8.83155 -0.31895 13.3657 0.0991801 17.8436C0.101072 17.8655 0.11337 17.8864 0.130398 17.8997C2.18321 19.4073 4.17171 20.3225 6.12328 20.9291C6.15451 20.9386 6.18761 20.9272 6.20748 20.9015C6.66913 20.2711 7.08064 19.6063 7.43348 18.9073C7.4543 18.8664 7.43442 18.8178 7.39186 18.8016C6.73913 18.554 6.1176 18.2521 5.51973 17.9093C5.47244 17.8816 5.46865 17.814 5.51216 17.7816C5.63797 17.6873 5.76382 17.5893 5.88396 17.4902C5.90569 17.4721 5.93598 17.4683 5.96153 17.4797C9.88928 19.273 14.1415 19.273 18.023 17.4797C18.0485 17.4674 18.0788 17.4712 18.1015 17.4893C18.2216 17.5883 18.3475 17.6873 18.4742 17.7816C18.5177 17.814 18.5149 17.8816 18.4676 17.9093C17.8697 18.2588 17.2482 18.554 16.5945 18.8006C16.552 18.8168 16.533 18.8664 16.5538 18.9073C16.9143 19.6054 17.3258 20.2701 17.7789 20.9005C17.7978 20.9272 17.8319 20.9386 17.8631 20.9291C19.8241 20.3225 21.8126 19.4073 23.8654 17.8997C23.8834 17.8864 23.8948 17.8664 23.8967 17.8445C24.3971 12.6676 23.0585 8.17064 20.3482 4.18414C20.3416 4.17082 20.3303 4.16129 20.317 4.15557ZM8.02002 15.117C6.8375 15.117 5.86313 14.0313 5.86313 12.6981C5.86313 11.3648 6.8186 10.2791 8.02002 10.2791C9.23087 10.2791 10.1958 11.3743 10.1769 12.6981C10.1769 14.0313 9.22141 15.117 8.02002 15.117ZM15.9947 15.117C14.8123 15.117 13.8379 14.0313 13.8379 12.6981C13.8379 11.3648 14.7933 10.2791 15.9947 10.2791C17.2056 10.2791 18.1705 11.3743 18.1516 12.6981C18.1516 14.0313 17.2056 15.117 15.9947 15.117Z" fill="#FAFAFA"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" fill="black" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.477 2 2 6.477 2 12C2 16.418 4.865 20.166 8.84 21.49C9.34 21.58 9.52 21.27 9.52 21C9.52 20.77 9.51 20.14 9.51 19.31C6.73 19.91 6.14 17.97 6.14 17.97C5.68 16.81 5.03 16.5 5.03 16.5C4.12 15.88 5.1 15.9 5.1 15.9C6.1 15.97 6.63 16.93 6.63 16.93C7.5 18.45 8.97 18 9.54 17.76C9.63 17.11 9.89 16.67 10.17 16.42C7.95 16.17 5.62 15.31 5.62 11.5C5.62 10.39 6 9.5 6.65 8.79C6.55 8.54 6.2 7.5 6.75 6.15C6.75 6.15 7.59 5.88 9.5 7.17C10.29 6.95 11.15 6.84 12 6.84C12.85 6.84 13.71 6.95 14.5 7.17C16.41 5.88 17.25 6.15 17.25 6.15C17.8 7.5 17.45 8.54 17.35 8.79C18 9.5 18.38 10.39 18.38 11.5C18.38 15.32 16.04 16.16 13.81 16.41C14.17 16.72 14.5 17.33 14.5 18.26C14.5 19.6 14.49 20.68 14.49 21C14.49 21.27 14.67 21.59 15.17 21.49C19.14 20.16 22 16.42 22 12C22 6.477 17.523 2 12 2Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 902 B

+10
View File
@@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2001_1428)">
<path d="M22.2234 0H1.77187C0.792187 0 0 0.773438 0 1.72969V22.2656C0 23.2219 0.792187 24 1.77187 24H22.2234C23.2031 24 24 23.2219 24 22.2703V1.72969C24 0.773438 23.2031 0 22.2234 0ZM7.12031 20.4516H3.55781V8.99531H7.12031V20.4516ZM5.33906 7.43438C4.19531 7.43438 3.27188 6.51094 3.27188 5.37187C3.27188 4.23281 4.19531 3.30937 5.33906 3.30937C6.47813 3.30937 7.40156 4.23281 7.40156 5.37187C7.40156 6.50625 6.47813 7.43438 5.33906 7.43438ZM20.4516 20.4516H16.8937V14.8828C16.8937 13.5562 16.8703 11.8453 15.0422 11.8453C13.1906 11.8453 12.9094 13.2937 12.9094 14.7891V20.4516H9.35625V8.99531H12.7687V10.5609H12.8156C13.2891 9.66094 14.4516 8.70938 16.1813 8.70938C19.7859 8.70938 20.4516 11.0813 20.4516 14.1656V20.4516Z" fill="#FAFAFA"/>
</g>
<defs>
<clipPath id="clip0_2001_1428">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 989 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.0512 4.07466C15.3113 5.17727 16.301 5.99866 17.4829 5.99866C18.8627 5.99866 19.9813 4.87965 19.9813 3.49933C19.9813 2.11902 18.8627 1 17.4829 1C16.2764 1 15.2703 1.85537 15.036 2.99314C13.0155 3.20991 11.4378 4.92417 11.4378 7.00167C11.4378 7.00636 11.4378 7.00988 11.4378 7.01456C9.24041 7.10713 7.23397 7.73284 5.641 8.72062C5.04949 8.26247 4.30688 7.98945 3.50102 7.98945C1.5672 7.98945 0 9.55725 0 11.4918C0 12.8955 0.824597 14.1048 2.01581 14.6637C2.13177 18.7297 6.56047 22 12.0082 22C17.4559 22 21.8905 18.7261 22.0006 14.6567C23.1824 14.0942 24 12.8885 24 11.493C24 9.55842 22.4328 7.99063 20.499 7.99063C19.6966 7.99063 18.9575 8.2613 18.3672 8.71594C16.7602 7.72113 14.7315 7.09541 12.5119 7.01222C12.5119 7.0087 12.5119 7.00636 12.5119 7.00285C12.5119 5.51473 13.6176 4.27971 15.0512 4.077V4.07466ZM5.50044 13.7146C5.559 12.4444 6.40234 11.4695 7.38272 11.4695C8.3631 11.4695 9.11274 12.4995 9.05417 13.7697C8.99561 15.0398 8.26354 15.5015 7.28199 15.5015C6.30044 15.5015 5.44187 14.9848 5.50044 13.7146ZM16.6348 11.4695C17.6164 11.4695 18.4597 12.4444 18.5171 13.7146C18.5757 14.9848 17.716 15.5015 16.7356 15.5015C15.7552 15.5015 15.022 15.041 14.9634 13.7697C14.9048 12.4995 15.6533 11.4695 16.6348 11.4695ZM15.4682 16.6533C15.6521 16.6721 15.7693 16.8631 15.6978 17.0341C15.0946 18.4766 13.6703 19.4901 12.0082 19.4901C10.3461 19.4901 8.92299 18.4766 8.31859 17.0341C8.24714 16.8631 8.36427 16.6721 8.54817 16.6533C9.62577 16.5444 10.7912 16.4846 12.0082 16.4846C13.2252 16.4846 14.3895 16.5444 15.4682 16.6533Z" fill="#FAFAFA"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3263 1.90393H21.6998L14.3297 10.3274L23 21.7899H16.2112L10.894 14.838L4.80995 21.7899H1.43443L9.31743 12.78L1 1.90393H7.96111L12.7674 8.25826L18.3263 1.90393ZM17.1423 19.7707H19.0116L6.94539 3.81706H4.93946L17.1423 19.7707Z" fill="#FAFAFA"/>
</svg>

After

Width:  |  Height:  |  Size: 358 B

+1
View File
@@ -159,6 +159,7 @@
"!**/webview-ui/**",
"!**/evals/**",
"!**/standalone/**",
"!**/cli/**",
"!**/e2e/**",
"!**/test/**",
"!**/__tests__/**",
+365
View File
@@ -0,0 +1,365 @@
# Cline CLI
The official CLI for Cline. Run Cline tasks directly from the terminal with the same underlying functionality as the VS Code extension.
## Features
- **Reuses Core Codebase**: Shares the same Controller, Task, and API handling as the VS Code extension
- **Terminal Output**: Displays Cline messages directly in your terminal with colored output
- **Task History**: Access your task history from the command line
- **Configurable**: Use custom configuration directories and working directories
- **Image Support**: Attach images to your prompts using file paths or inline references
## Prerequisites
- Node.js 20.x or later
- npm or yarn
- The parent Cline project dependencies installed
## Installation
From the repository root:
```bash
# Install all dependencies first
npm run install:all
# Ensure protos are generated
npm run protos
# Build and link the CLI globally
npm run cli:link
```
## Usage
### Interactive Mode (Default)
When you run `cline` without any command, it launches an interactive welcome prompt:
```bash
# Launch interactive mode
cline
# Or run a task directly
cline "Create a hello world function in Python"
# With options
cline -v --thinking "Analyze this codebase"
```
### Commands
#### `task` (alias: `t`)
Run a new task with a prompt.
```bash
cline task "Create a hello world function in Python"
cline t "Create a hello world function"
```
**Options:**
| Option | Description |
|--------|-------------|
| `-a, --act` | Run in act mode |
| `-p, --plan` | Run in plan mode |
| `-y, --yolo` | Enable yolo mode (auto-approve actions) |
| `-m, --model <model>` | Model to use for the task |
| `-i, --images <paths...>` | Image file paths to include with the task |
| `-v, --verbose` | Show verbose output including reasoning |
| `-c, --cwd <path>` | Working directory for the task |
| `--config <path>` | Path to Cline configuration directory |
| `-t, --thinking` | Enable extended thinking (1024 token budget) |
**Examples:**
```bash
# Run in plan mode with verbose output
cline task -p -v "Design a REST API"
# Use a specific model with yolo mode
cline task -m claude-sonnet-4-5-20250929 -y "Refactor this function"
# Include images with your prompt
cline task -i screenshot.png diagram.jpg "Fix the UI based on these images"
# Or use inline image references in the prompt
cline task "Fix the layout shown in @./screenshot.png"
# Enable extended thinking for complex tasks
cline task -t "Architect a microservices system"
# Specify working directory
cline task -c /path/to/project "Add unit tests"
```
#### `history` (alias: `h`)
List task history with pagination support.
```bash
cline history
cline h
```
**Options:**
| Option | Description |
|--------|-------------|
| `-n, --limit <number>` | Number of tasks to show (default: 10) |
| `-p, --page <number>` | Page number, 1-based (default: 1) |
| `--config <path>` | Path to Cline configuration directory |
**Examples:**
```bash
# Show last 10 tasks (default)
cline history
# Show 20 tasks
cline history -n 20
# Show page 2 with 5 tasks per page
cline history -n 5 -p 2
```
#### `config`
Show current configuration including global and workspace state.
```bash
cline config
```
**Options:**
| Option | Description |
|--------|-------------|
| `--config <path>` | Path to Cline configuration directory |
#### `auth`
Authenticate a provider and configure what model is used.
```bash
cline auth
```
**Options:**
| Option | Description |
|--------|-------------|
| `-p, --provider <id>` | Provider ID for quick setup (e.g., openai-native, anthropic) |
| `-k, --apikey <key>` | API key for the provider |
| `-m, --modelid <id>` | Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929) |
| `-b, --baseurl <url>` | Base URL (optional, only for openai provider) |
| `-v, --verbose` | Show verbose output |
| `-c, --cwd <path>` | Working directory for the task |
| `--config <path>` | Path to Cline configuration directory |
**Examples:**
```bash
# Interactive authentication
cline auth
# Quick setup with provider and API key
cline auth -p anthropic -k sk-ant-xxxxx
# Full quick setup with model
cline auth -p openai-native -k sk-xxxxx -m gpt-4o
# OpenAI-compatible provider with custom base URL
cline auth -p openai -k your-api-key -b https://api.example.com/v1
```
### Global Options
These options are available for the default command (running a task directly):
| Option | Description |
|--------|-------------|
| `-v, --verbose` | Show verbose output |
| `-c, --cwd <path>` | Working directory |
| `--config <path>` | Configuration directory |
| `--thinking` | Enable extended thinking (1024 token budget) |
## Development
### Quick Start
```bash
# 1. Install all dependencies (root, webview-ui, cli)
npm run install:all
# 2. Build and link globally so you can run `cline` from anywhere
npm run cli:link
# 3. Test it
cline --help
```
### Scripts
Run these from the repository root:
| Script | Description |
|--------|-------------|
| `npm run install:all` | Install deps for root, webview-ui, and cli |
| `npm run cli:build` | Generate protos and build CLI |
| `npm run cli:build:production` | Production build (minified) |
| `npm run cli:link` | Build and `npm link` so you can run `cline` from anywhere |
| `npm run cli:unlink` | Remove the global `cline` symlink |
| `npm run cli:dev` | Link + watch mode for development |
| `npm run cli:watch` | Watch mode only (no initial build) |
| `npm run cli:test` | Run CLI tests |
### Development Workflow
1. Run `npm run cli:dev` - this links the CLI globally and starts watch mode
2. Make changes to files in `cli/src/`
3. The build automatically rebuilds on save
4. Test your changes by running `cline` in another terminal
5. When done, run `npm run cli:unlink` to clean up
### Proto Generation
The CLI uses proto-generated types for message passing (same as the VS Code extension). If you modify any `.proto` files, run:
```bash
npm run protos
```
This generates TypeScript types in `src/generated/` that both the CLI and extension use.
## Publish
#### 1. Publish to npm
```bash
npm publish
```
#### 2. Update the Homebrew formula
```bash
npm run update-brew-formula
```
#### 3. Test the formula locally
```bash
# Create a local tap
brew tap-new cline/local
cp ./cli/cline.rb "$(brew --repository)/Library/Taps/cline/homebrew-local/Formula/cline.rb"
# Build from Source
brew install --build-from-source cline/local/cline
# Install from your local tap
brew install cline/local/cline
# Clean up when done
brew untap cline/local
```
#### 4. If using a tap, commit and push
```bash
git add cline.rb
git commit -m "Update cline to v2.0.0"
git push
```
## Architecture
### How It Works
The CLI directly imports and reuses the core Cline TypeScript codebase (the same code that powers the VS Code extension). This means feature parity is easy to maintain - when core gets updated, the CLI automatically benefits.
```
┌─────────────────────────────────────────────────────────┐
│ CLI (cli/) │
│ - React Ink terminal UI │
│ - Command parsing (commander) │
│ - Terminal-specific adapters │
└─────────────────────────────────────────────────────────┘
│ direct imports
┌─────────────────────────────────────────────────────────┐
│ Core (src/core/) │
│ - Controller: task lifecycle, state management │
│ - Task: AI API calls, tool execution │
│ - StateManager: persistent storage │
│ - Proto types: message definitions │
└─────────────────────────────────────────────────────────┘
```
Unlike a client-server architecture, the CLI runs everything in a single Node.js process. The "host bridge" pattern provides terminal-appropriate implementations for things the VS Code extension would handle differently (clipboard, file dialogs, etc.).
### Key Files
| File | Purpose |
|------|---------|
| `src/index.ts` | Entry point, command definitions |
| `src/components/App.tsx` | Main React Ink app |
| `src/components/ChatView.tsx` | Task conversation UI |
| `src/controllers/CliWebviewProvider.ts` | Bridges core messages to terminal output |
| `src/vscode-context.ts` | Mock VS Code extension context for core compatibility |
| `src/vscode-shim.ts` | Shims for VS Code APIs that core depends on |
| `src/constants/colors.ts` | Terminal color definitions |
### React Ink
The CLI uses [React Ink](https://github.com/vadimdemedes/ink) for its terminal UI. This lets us build the interface with React components that render to the terminal. Key patterns:
- Components in `src/components/` render terminal UI
- Hooks in `src/hooks/` manage terminal-specific state (size, scrolling)
- The `useStateSubscriber` hook subscribes to core state changes
## Configuration
The CLI stores its data in `~/.cline/data/` by default:
- `globalState.json`: Global settings and state
- `secrets.json`: API keys and secrets
- `workspace/`: Workspace-specific state
- `tasks/`: Task history and conversation data
Override with the `--config` option or `CLINE_DIR` environment variable.
## Troubleshooting
### Build Errors
If you encounter build errors:
```bash
# Make sure all deps are installed
npm run install:all
# Regenerate proto types
npm run protos
# Then rebuild
npm run cli:build
```
### "command not found: cline"
The CLI isn't linked globally. Run:
```bash
npm run cli:link
```
### Changes Not Reflected
If your code changes aren't showing up:
1. Make sure watch mode is running (`npm run cli:dev`)
2. Check for TypeScript errors in the watch output
3. Try unlinking and relinking: `npm run cli:unlink && npm run cli:link`
### Import Errors from Core
The CLI imports from `@core/`, `@shared/`, etc. These paths are defined in the root `tsconfig.json`. If you see import errors, make sure you're building from the repo root, not from inside `cli/`.
+50 -333
View File
@@ -1,365 +1,82 @@
# Cline CLI
# Cline
The official CLI for Cline. Run Cline tasks directly from the terminal with the same underlying functionality as the VS Code extension.
<p align="center">
<img src="https://github.com/user-attachments/assets/7123f9d1-afeb-48d5-93fa-e750dec0ebba" width="70%" />
</p>
## Features
<div align="center">
<table>
<tbody>
<td align="center">
<a href="https://www.npmjs.com/package/cline" target="_blank"><strong>NPM</strong></a>
</td>
<td align="center">
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
</td>
<td align="center">
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
</td>
<td align="center">
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
</td>
<td align="center">
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>Getting Started</strong></a>
</td>
</tbody>
</table>
</div>
- **Reuses Core Codebase**: Shares the same Controller, Task, and API handling as the VS Code extension
- **Terminal Output**: Displays Cline messages directly in your terminal with colored output
- **Task History**: Access your task history from the command line
- **Configurable**: Use custom configuration directories and working directories
- **Image Support**: Attach images to your prompts using file paths or inline references
Meet Cline, an AI assistant that lives in your terminal.
## Prerequisites
- Node.js 20.x or later
- npm or yarn
- The parent Cline project dependencies installed
## Installation
From the repository root:
Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support.
```bash
# Install all dependencies first
npm run install:all
npm i -g cline
# Ensure protos are generated
npm run protos
# Build and link the CLI globally
npm run cli:link
```
## Usage
### Interactive Mode (Default)
When you run `cline` without any command, it launches an interactive welcome prompt:
```bash
# Launch interactive mode
# cd into your project and run:
cline
# Or run a task directly
cline "Create a hello world function in Python"
# With options
cline -v --thinking "Analyze this codebase"
```
### Commands
> Move your mouse around under the Cline icon for a surprise!
#### `task` (alias: `t`)
---
Run a new task with a prompt.
<img align="right" width="340" src="https://github.com/user-attachments/assets/ceb74224-08aa-4b8b-a3e7-b438ac3d160a">
```bash
cline task "Create a hello world function in Python"
cline t "Create a hello world function"
```
### Use any API and Model
**Options:**
Cline supports API providers like ChatGPT, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using a Cline Account, you'll always have access to the newest models as soon as they're available.
| Option | Description |
|--------|-------------|
| `-a, --act` | Run in act mode |
| `-p, --plan` | Run in plan mode |
| `-y, --yolo` | Enable yolo mode (auto-approve actions) |
| `-m, --model <model>` | Model to use for the task |
| `-i, --images <paths...>` | Image file paths to include with the task |
| `-v, --verbose` | Show verbose output including reasoning |
| `-c, --cwd <path>` | Working directory for the task |
| `--config <path>` | Path to Cline configuration directory |
| `-t, --thinking` | Enable extended thinking (1024 token budget) |
<!-- Transparent pixel to create line break after floating image -->
**Examples:**
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
```bash
# Run in plan mode with verbose output
cline task -p -v "Design a REST API"
<img align="left" width="370" src="https://github.com/user-attachments/assets/cad091f6-6c0f-4e4b-97ea-a1ff67e39b9b">
# Use a specific model with yolo mode
cline task -m claude-sonnet-4-5-20250929 -y "Refactor this function"
### Stay in Control with Human-in-the-Loop
# Include images with your prompt
cline task -i screenshot.png diagram.jpg "Fix the UI based on these images"
Cline asks for your approval before running commands, editing files, or taking any action. Review each step and approve or reject as you go—or enable auto-approve to let Cline work autonomously to completion.
# Or use inline image references in the prompt
cline task "Fix the layout shown in @./screenshot.png"
<!-- Transparent pixel to create line break after floating image -->
# Enable extended thinking for complex tasks
cline task -t "Architect a microservices system"
<img width="2000" height="0" src="https://github.com/user-attachments/assets/cad091f6-6c0f-4e4b-97ea-a1ff67e39b9b"><br>
# Specify working directory
cline task -c /path/to/project "Add unit tests"
```
<img align="right" width="400" src="https://github.com/user-attachments/assets/4f264a0c-3802-49a7-8e5e-13d97beb659e">
#### `history` (alias: `h`)
### Plan & Act Modes
List task history with pagination support.
Toggle to Plan Mode to discuss implementation and architecture with Cline. He'll ask clarifying questions, explore your codebase, and present a plan for you to align on. Once you're satisfied, switch to Act Mode and let Cline execute the plan.
```bash
cline history
cline h
```
<!-- Transparent pixel to create line break after floating image -->
**Options:**
<img width="2000" height="0" src="https://github.com/user-attachments/assets/4f264a0c-3802-49a7-8e5e-13d97beb659e"><br>
| Option | Description |
|--------|-------------|
| `-n, --limit <number>` | Number of tasks to show (default: 10) |
| `-p, --page <number>` | Page number, 1-based (default: 1) |
| `--config <path>` | Path to Cline configuration directory |
**Examples:**
## Enterprise
```bash
# Show last 10 tasks (default)
cline history
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
# Show 20 tasks
cline history -n 20
## License
# Show page 2 with 5 tasks per page
cline history -n 5 -p 2
```
[Apache 2.0 © 2026 Cline Bot Inc.](./LICENSE)
#### `config`
Show current configuration including global and workspace state.
```bash
cline config
```
**Options:**
| Option | Description |
|--------|-------------|
| `--config <path>` | Path to Cline configuration directory |
#### `auth`
Authenticate a provider and configure what model is used.
```bash
cline auth
```
**Options:**
| Option | Description |
|--------|-------------|
| `-p, --provider <id>` | Provider ID for quick setup (e.g., openai-native, anthropic) |
| `-k, --apikey <key>` | API key for the provider |
| `-m, --modelid <id>` | Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929) |
| `-b, --baseurl <url>` | Base URL (optional, only for openai provider) |
| `-v, --verbose` | Show verbose output |
| `-c, --cwd <path>` | Working directory for the task |
| `--config <path>` | Path to Cline configuration directory |
**Examples:**
```bash
# Interactive authentication
cline auth
# Quick setup with provider and API key
cline auth -p anthropic -k sk-ant-xxxxx
# Full quick setup with model
cline auth -p openai-native -k sk-xxxxx -m gpt-4o
# OpenAI-compatible provider with custom base URL
cline auth -p openai -k your-api-key -b https://api.example.com/v1
```
### Global Options
These options are available for the default command (running a task directly):
| Option | Description |
|--------|-------------|
| `-v, --verbose` | Show verbose output |
| `-c, --cwd <path>` | Working directory |
| `--config <path>` | Configuration directory |
| `--thinking` | Enable extended thinking (1024 token budget) |
## Development
### Quick Start
```bash
# 1. Install all dependencies (root, webview-ui, cli)
npm run install:all
# 2. Build and link globally so you can run `cline` from anywhere
npm run cli:link
# 3. Test it
cline --help
```
### Scripts
Run these from the repository root:
| Script | Description |
|--------|-------------|
| `npm run install:all` | Install deps for root, webview-ui, and cli |
| `npm run cli:build` | Generate protos and build CLI |
| `npm run cli:build:production` | Production build (minified) |
| `npm run cli:link` | Build and `npm link` so you can run `cline` from anywhere |
| `npm run cli:unlink` | Remove the global `cline` symlink |
| `npm run cli:dev` | Link + watch mode for development |
| `npm run cli:watch` | Watch mode only (no initial build) |
| `npm run cli:test` | Run CLI tests |
### Development Workflow
1. Run `npm run cli:dev` - this links the CLI globally and starts watch mode
2. Make changes to files in `cli/src/`
3. The build automatically rebuilds on save
4. Test your changes by running `cline` in another terminal
5. When done, run `npm run cli:unlink` to clean up
### Proto Generation
The CLI uses proto-generated types for message passing (same as the VS Code extension). If you modify any `.proto` files, run:
```bash
npm run protos
```
This generates TypeScript types in `src/generated/` that both the CLI and extension use.
## Publish
#### 1. Publish to npm
```bash
npm publish
```
#### 2. Update the Homebrew formula
```bash
npm run update-brew-formula
```
#### 3. Test the formula locally
```bash
# Create a local tap
brew tap-new cline/local
cp ./cli/cline.rb "$(brew --repository)/Library/Taps/cline/homebrew-local/Formula/cline.rb"
# Build from Source
brew install --build-from-source cline/local/cline
# Install from your local tap
brew install cline/local/cline
# Clean up when done
brew untap cline/local
```
#### 4. If using a tap, commit and push
```bash
git add cline.rb
git commit -m "Update cline to v2.0.0"
git push
```
## Architecture
### How It Works
The CLI directly imports and reuses the core Cline TypeScript codebase (the same code that powers the VS Code extension). This means feature parity is easy to maintain - when core gets updated, the CLI automatically benefits.
```
┌─────────────────────────────────────────────────────────┐
│ CLI (cli/) │
│ - React Ink terminal UI │
│ - Command parsing (commander) │
│ - Terminal-specific adapters │
└─────────────────────────────────────────────────────────┘
│ direct imports
┌─────────────────────────────────────────────────────────┐
│ Core (src/core/) │
│ - Controller: task lifecycle, state management │
│ - Task: AI API calls, tool execution │
│ - StateManager: persistent storage │
│ - Proto types: message definitions │
└─────────────────────────────────────────────────────────┘
```
Unlike a client-server architecture, the CLI runs everything in a single Node.js process. The "host bridge" pattern provides terminal-appropriate implementations for things the VS Code extension would handle differently (clipboard, file dialogs, etc.).
### Key Files
| File | Purpose |
|------|---------|
| `src/index.ts` | Entry point, command definitions |
| `src/components/App.tsx` | Main React Ink app |
| `src/components/ChatView.tsx` | Task conversation UI |
| `src/controllers/CliWebviewProvider.ts` | Bridges core messages to terminal output |
| `src/vscode-context.ts` | Mock VS Code extension context for core compatibility |
| `src/vscode-shim.ts` | Shims for VS Code APIs that core depends on |
| `src/constants/colors.ts` | Terminal color definitions |
### React Ink
The CLI uses [React Ink](https://github.com/vadimdemedes/ink) for its terminal UI. This lets us build the interface with React components that render to the terminal. Key patterns:
- Components in `src/components/` render terminal UI
- Hooks in `src/hooks/` manage terminal-specific state (size, scrolling)
- The `useStateSubscriber` hook subscribes to core state changes
## Configuration
The CLI stores its data in `~/.cline/data/` by default:
- `globalState.json`: Global settings and state
- `secrets.json`: API keys and secrets
- `workspace/`: Workspace-specific state
- `tasks/`: Task history and conversation data
Override with the `--config` option or `CLINE_DIR` environment variable.
## Troubleshooting
### Build Errors
If you encounter build errors:
```bash
# Make sure all deps are installed
npm run install:all
# Regenerate proto types
npm run protos
# Then rebuild
npm run cli:build
```
### "command not found: cline"
The CLI isn't linked globally. Run:
```bash
npm run cli:link
```
### Changes Not Reflected
If your code changes aren't showing up:
1. Make sure watch mode is running (`npm run cli:dev`)
2. Check for TypeScript errors in the watch output
3. Try unlinking and relinking: `npm run cli:unlink && npm run cli:link`
### Import Errors from Core
The CLI imports from `@core/`, `@shared/`, etc. These paths are defined in the root `tsconfig.json`. If you see import errors, make sure you're building from the repo root, not from inside `cli/`.
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "cline",
"version": "2.0.0",
"version": "2.0.1",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "dist/cli.mjs",
"bin": {
@@ -12,11 +12,11 @@
},
"scripts": {
"prepublishOnly": "npm run build:production",
"package:brew": "node ./scripts/update-brew-formula.mts",
"package:brew": "npx tsx ./scripts/update-brew-formula.mts",
"package": "npm pack --pack-destination ./dist",
"build": "node esbuild.mts",
"build:production": "node esbuild.mts --production",
"watch": "node esbuild.mts --watch",
"build": "npx tsx esbuild.mts",
"build:production": "npx tsx esbuild.mts --production",
"watch": "npx tsx esbuild.mts --watch",
"dev": "IS_DEV=true && npm run link && npm run watch ; npm run unlink",
"clean": "rimraf dist",
"typecheck": "tsc --noEmit",
+14 -10
View File
@@ -333,14 +333,17 @@ export class ClineAgent implements acp.Agent {
// Get current provider and model for the mode
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const modelKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
const currentProvider = stateManager.getGlobalSettingsKey(providerKey) as ApiProvider | undefined
const currentModelId = stateManager.getGlobalSettingsKey(modelKey) as string | undefined
// Use provider-specific model ID key (e.g., cline uses actModeOpenRouterModelId)
const modelKey = currentProvider ? getProviderModelIdKey(currentProvider, mode) : null
const currentModelId = modelKey
? (stateManager.getGlobalSettingsKey(modelKey as string) as string | undefined)
: undefined
// Build the current model ID in provider/model format
const currentFullModelId =
currentProvider && currentModelId ? `${currentProvider}/${currentModelId}` : currentProvider || "anthropic"
currentProvider && currentModelId ? `${currentProvider}/${currentModelId}` : currentProvider || ""
// Get available models based on provider
let modelIds: string[] = []
@@ -398,13 +401,11 @@ export class ClineAgent implements acp.Agent {
const stateManager = StateManager.get()
// Update model for both plan and act modes (use the same model for both)
// Update provider for both modes
stateManager.setGlobalState("actModeApiProvider", provider)
stateManager.setGlobalState("actModeApiModelId", modelId)
stateManager.setGlobalState("planModeApiProvider", provider)
stateManager.setGlobalState("planModeApiModelId", modelId)
// Also update the provider-specific model ID keys for both modes
// Update model ID using provider-specific keys (e.g., cline uses actModeOpenRouterModelId)
const actProviderModelKey = getProviderModelIdKey(provider, "act")
if (actProviderModelKey) {
stateManager.setGlobalState(actProviderModelKey, modelId)
@@ -1219,8 +1220,11 @@ export class ClineAgent implements acp.Agent {
const stateManager = StateManager.get()
stateManager.setGlobalState("actModeApiProvider", "openai-codex")
stateManager.setGlobalState("planModeApiProvider", "openai-codex")
stateManager.setGlobalState("actModeApiModelId", openAiCodexDefaultModelId)
stateManager.setGlobalState("planModeApiModelId", openAiCodexDefaultModelId)
// Use provider-specific model ID keys for consistency
const actModelKey = getProviderModelIdKey("openai-codex", "act")
const planModelKey = getProviderModelIdKey("openai-codex", "plan")
if (actModelKey) stateManager.setGlobalState(actModelKey, openAiCodexDefaultModelId)
if (planModelKey) stateManager.setGlobalState(planModelKey, openAiCodexDefaultModelId)
await stateManager.flushPendingState()
return {}
+1 -2
View File
@@ -82,9 +82,8 @@ describe("App", () => {
})
it("should render AuthView when view is auth", () => {
const { lastFrame } = render(<App authQuickSetup={{ provider: "openai" }} controller={mockController} view="auth" />)
const { lastFrame } = render(<App controller={mockController} view="auth" />)
expect(lastFrame()).toContain("AuthView")
expect(lastFrame()).toContain("openai")
})
it("should render ChatView when view is welcome", () => {
-13
View File
@@ -81,13 +81,6 @@ interface AppProps {
globalSkills?: SkillInfo[]
localSkills?: SkillInfo[]
onToggleSkill?: (isGlobal: boolean, skillPath: string, enabled: boolean) => void
// For auth view
authQuickSetup?: {
provider?: string
apikey?: string
modelid?: string
baseurl?: string
}
// For welcome view
onWelcomeSubmit?: (prompt: string, imagePaths: string[]) => void
onWelcomeExit?: () => void
@@ -95,8 +88,6 @@ interface AppProps {
initialImages?: string[]
// Stdin support
isRawModeSupported?: boolean
// Robot position (calculated before Ink mounts)
robotTopRow?: number
}
export const App: React.FC<AppProps> = ({
@@ -135,13 +126,11 @@ export const App: React.FC<AppProps> = ({
globalSkills,
localSkills,
onToggleSkill,
authQuickSetup,
onWelcomeSubmit,
onWelcomeExit,
initialPrompt,
initialImages,
isRawModeSupported = true,
robotTopRow,
}) => {
const { resizeKey } = useTerminalSize()
const [currentView, setCurrentView] = useState<ViewType>(initialView)
@@ -240,7 +229,6 @@ export const App: React.FC<AppProps> = ({
onComplete={onComplete}
onError={onError}
onNavigateToWelcome={handleNavigateToWelcome}
quickSetup={authQuickSetup}
/>
)
break
@@ -259,7 +247,6 @@ export const App: React.FC<AppProps> = ({
onComplete={onComplete}
onError={onError}
onExit={onWelcomeExit}
robotTopRow={robotTopRow}
taskId={selectedTaskId}
/>
)}
+10 -8
View File
@@ -34,7 +34,6 @@ type AsciiMotionCliProps = {
autoPlay?: boolean;
loop?: boolean;
onReady?: (api: PlaybackAPI) => void;
robotTopRow?: number; // Screen row where robot is rendered (calculated before Ink mounts)
onScroll?: () => void; // Called when user scrolls (scroll wheel)
};
@@ -333365,12 +333364,12 @@ const FRAME_BOTTOM_RIGHT = 128;
export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
hasDarkBackground = true,
robotTopRow,
onScroll,
}) => {
const [frameIndex, setFrameIndex] = useState(0);
const [targetFrame, setTargetFrame] = useState(0);
const [cursor, setCursor] = useState({ x: 0, y: 0 });
const lastCursorUpdateRef = useRef(0);
const { stdout } = useStdout();
const { stdin, setRawMode } = useStdin();
@@ -333379,8 +333378,9 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
const faceX = Math.floor(terminalWidth / 2);
// Robot height after cropping is ~12 rows, eyes are roughly halfway down
const robotHeight = 12;
// faceY is the robot's eye level - calculated from robotTopRow prop (set before Ink mounts)
const faceY = robotTopRow !== undefined ? robotTopRow + Math.floor(robotHeight / 2) : null;
// Robot is always rendered at the top of the terminal in welcome state.
// faceY is the robot's eye level (row 2 + half robot height)
const faceY = 2 + Math.floor(robotHeight / 2);
// Select color theme based on background
const theme = useMemo(() => hasDarkBackground ? THEME_DARK : THEME_LIGHT, [hasDarkBackground]);
@@ -333425,7 +333425,12 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
if (button === 64 || button === 65) {
onScroll?.();
}
setCursor({ x: parseInt(mouseMatch[2], 10), y: parseInt(mouseMatch[3], 10) });
// Throttle cursor updates to ~20fps to reduce re-renders
const now = Date.now();
if (now - lastCursorUpdateRef.current >= 50) {
lastCursorUpdateRef.current = now;
setCursor({ x: parseInt(mouseMatch[2], 10), y: parseInt(mouseMatch[3], 10) });
}
}
};
@@ -333444,9 +333449,6 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
// Map cursor position to target frame (continuous interpolation)
useEffect(() => {
// Wait until we know where the robot is on screen
if (faceY === null) return;
const dx = cursor.x - faceX;
const dy = cursor.y - faceY;
+54 -111
View File
@@ -9,8 +9,9 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"
import { StateManager } from "@/core/storage/StateManager"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { AuthService } from "@/services/auth/AuthService"
import { API_PROVIDERS_LIST, openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
import { ProviderToApiKeyMap } from "@/shared/storage"
import type { ApiProvider } from "@/shared/api"
import { openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@/shared/storage"
import { openExternal } from "@/utils/env"
import { COLORS } from "../constants/colors"
import { getAllFeaturedModels } from "../constants/featured-models"
@@ -29,7 +30,7 @@ import {
} from "./FeaturedModelPicker"
import { ImportView } from "./ImportView"
import { getDefaultModelId, hasModelPicker, ModelPicker } from "./ModelPicker"
import { getProviderLabel, getProviderOrder } from "./ProviderPicker"
import { CLI_EXCLUDED_PROVIDERS, getProviderLabel, getProviderOrder } from "./ProviderPicker"
type AuthStep =
| "menu"
@@ -54,13 +55,6 @@ interface AuthViewProps {
onComplete?: () => void
onError?: () => void
onNavigateToWelcome?: () => void
// Quick setup options
quickSetup?: {
provider?: string
apikey?: string
modelid?: string
baseurl?: string
}
}
interface SelectItem {
@@ -152,9 +146,9 @@ const TextInput: React.FC<{
)
}
export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onError, onNavigateToWelcome, quickSetup }) => {
export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onError, onNavigateToWelcome }) => {
const { exit } = useApp()
const [step, setStep] = useState<AuthStep>(quickSetup ? "saving" : "menu")
const [step, setStep] = useState<AuthStep>("menu")
const [selectedProvider, setSelectedProvider] = useState<string>(
StateManager.get().getApiConfiguration().actModeApiProvider ||
StateManager.get().getApiConfiguration().planModeApiProvider ||
@@ -172,15 +166,14 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const [importSource, setImportSource] = useState<ImportSource | null>(null)
const [bedrockConfig, setBedrockConfig] = useState<BedrockConfig | null>(null)
// Use providers.json order, filtered to only available providers
// Use providers.json order, filtered to exclude CLI-incompatible providers
const sortedProviders = useMemo(() => {
const availableProviders = new Set(API_PROVIDERS_LIST)
return getProviderOrder().filter((p) => availableProviders.has(p))
return getProviderOrder().filter((p) => !CLI_EXCLUDED_PROVIDERS.has(p))
}, [])
// Main menu items - conditionally include import options
const mainMenuItems: SelectItem[] = useMemo(() => {
const items: SelectItem[] = [{ label: "Sign in with Cline account", value: "cline_auth" }]
const items: SelectItem[] = [{ label: "Sign in with Cline", value: "cline_auth" }]
// Add OpenAI Codex option for ChatGPT subscribers
items.push({ label: "Sign in with ChatGPT Subscription", value: "openai_codex_auth" })
@@ -243,13 +236,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
}
}, [step, selectedProvider])
// Handle quick setup
useEffect(() => {
if (quickSetup && step === "saving") {
handleQuickSetup()
}
}, [quickSetup, step])
// Subscribe to auth status updates when in cline_auth step
useEffect(() => {
if (step !== "cline_auth") {
@@ -269,17 +255,20 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const stateManager = StateManager.get()
const mode = stateManager.getGlobalSettingsKey("mode") || "act"
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const modelIdKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
// Use provider-specific model ID key (cline uses OpenRouterModelId)
const modelIdKey = getProviderModelIdKey("cline" as ApiProvider, mode as "act" | "plan")
const config: Record<string, string> = {
actModeApiProvider: "cline",
[providerKey]: "cline",
[modelIdKey]: openRouterDefaultModelId,
}
if (modelIdKey) {
config[modelIdKey] = openRouterDefaultModelId
}
stateManager.setApiConfiguration(config)
stateManager.flushPendingState()
setSelectedProvider("cline")
setModelId(config[modelIdKey])
setModelId(openRouterDefaultModelId)
setStep("cline_model")
}
}
@@ -293,80 +282,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
}
}, [step, controller])
const handleQuickSetup = async () => {
if (!quickSetup) {
return
}
try {
const { provider, apikey, modelid, baseurl } = quickSetup
// Validate required parameters
if (!provider || !apikey || !modelid) {
setErrorMessage("Quick setup requires --provider, --apikey, and --modelid flags")
setStep("error")
return
}
const normalizedProvider = provider.toLowerCase().trim()
if (!sortedProviders.includes(normalizedProvider)) {
setErrorMessage(`Invalid provider '${provider}'. Supported providers: ${sortedProviders.join(", ")}`)
setStep("error")
return
}
if (normalizedProvider === "bedrock") {
setErrorMessage(
"Bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup.",
)
setStep("error")
return
}
if (baseurl && !["openai", "openai-native"].includes(normalizedProvider)) {
setErrorMessage("Base URL is only supported for OpenAI and OpenAI-compatible providers")
setStep("error")
return
}
// Save configuration
const stateManager = StateManager.get()
const config: Record<string, string> = {
actModeApiProvider: normalizedProvider,
planModeApiProvider: normalizedProvider,
actModeApiModelId: modelid,
planModeApiModelId: modelid,
}
// Use provider-specific API key field
const keyField = ProviderToApiKeyMap[normalizedProvider]
if (keyField) {
const fields = Array.isArray(keyField) ? keyField : [keyField]
// Set the first key field for the provider
config[fields[0]] = apikey
} else {
// Fallback to generic apiKey
config.apiKey = apikey
}
if (baseurl) {
config.openAiBaseUrl = baseurl
}
stateManager.setApiConfiguration(config)
await stateManager.flushPendingState()
setSelectedProvider(normalizedProvider)
setModelId(modelid)
setBaseUrl(baseurl || "")
setStep("success")
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : String(error))
setStep("error")
}
}
// Start OpenAI Codex OAuth flow
const startOpenAiCodexAuth = useCallback(async () => {
try {
@@ -383,14 +298,18 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const stateManager = StateManager.get()
const mode = stateManager.getGlobalSettingsKey("mode") || "act"
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const modelIdKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
// Use provider-specific model ID key (openai-codex uses generic apiModelId)
const modelIdKey = getProviderModelIdKey("openai-codex" as ApiProvider, mode as "act" | "plan")
const config: Record<string, string> = {
actModeApiProvider: "openai-codex",
planModeApiProvider: "openai-codex",
[providerKey]: "openai-codex",
[modelIdKey]: openAiCodexDefaultModelId,
}
if (modelIdKey) {
config[modelIdKey] = openAiCodexDefaultModelId
}
stateManager.setApiConfiguration(config)
stateManager.setGlobalState("welcomeViewCompleted", true)
await stateManager.flushPendingState()
setSelectedProvider("openai-codex")
@@ -473,13 +392,26 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
async (model: string, base: string) => {
try {
const stateManager = StateManager.get()
// Use provider-specific model ID keys (e.g., cline uses actModeOpenRouterModelId)
const actModelKey = getProviderModelIdKey(selectedProvider as ApiProvider, "act")
const planModelKey = getProviderModelIdKey(selectedProvider as ApiProvider, "plan")
const config: Record<string, string> = {
actModeApiProvider: selectedProvider,
planModeApiProvider: selectedProvider,
actModeApiModelId: model,
planModeApiModelId: model,
apiProvider: selectedProvider,
}
if (actModelKey) config[actModelKey] = model
if (planModelKey) config[planModelKey] = model
// For cline/openrouter, also set model info (required for getModel() to return correct model)
if (selectedProvider === "cline" || selectedProvider === "openrouter") {
const openRouterModels = await controller?.readOpenRouterModels()
const modelInfo = openRouterModels?.[model]
if (modelInfo) {
stateManager.setGlobalState("actModeOpenRouterModelInfo", modelInfo)
stateManager.setGlobalState("planModeOpenRouterModelInfo", modelInfo)
}
}
// Add API key or Bedrock-specific config
if (selectedProvider === "bedrock" && bedrockConfig) {
@@ -505,6 +437,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
config.openAiBaseUrl = base
}
stateManager.setApiConfiguration(config)
stateManager.setGlobalState("welcomeViewCompleted", true)
await stateManager.flushPendingState()
setStep("success")
@@ -513,7 +446,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
setStep("error")
}
},
[selectedProvider, apiKey, bedrockConfig],
[selectedProvider, apiKey, bedrockConfig, controller],
)
const handleModelIdSubmit = useCallback(
@@ -565,11 +498,18 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
}, [])
// Auto-navigate to welcome after success (immediate)
// For quick setup mode (no onNavigateToWelcome), exit the Ink app
useEffect(() => {
if (step === "success" && onNavigateToWelcome) {
onNavigateToWelcome()
if (step === "success") {
if (onNavigateToWelcome) {
onNavigateToWelcome()
} else {
// Quick setup mode - exit Ink app after successful configuration
// The cleanup handler in runInkApp will handle process exit
exit()
}
}
}, [step, onNavigateToWelcome])
}, [step, onNavigateToWelcome, exit])
// Error screen menu items
const errorMenuItems: SelectItem[] = useMemo(() => {
@@ -940,9 +880,12 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
<Text> </Text>
{mainMenuItems.map((item, index) => (
<Box key={item.value}>
<Text color={index === menuIndex ? COLORS.primaryBlue : undefined}>
{index === menuIndex ? " " : " "}
{item.label}
<Text>
<Text color={index === menuIndex ? COLORS.primaryBlue : undefined}>
{index === menuIndex ? " " : " "}
{item.label}
</Text>
{item.value === "cline_auth" && <Text color="yellow"> (try Kimi K2.5 free!)</Text>}
</Text>
</Box>
))}
+46 -8
View File
@@ -17,8 +17,43 @@ import { jsonParseSafe } from "../utils/parser"
import { getToolDescription, isFileEditTool, parseToolFromMessage } from "../utils/tools"
import { DiffView } from "./DiffView"
/**
* Add "(Tab)" hint after "Act mode" mentions.
* Case-insensitive, avoids double-adding if already present.
* Matches just "Act mode" without requiring "to " prefix because markdown
* processing may split "toggle to **Act mode**" into separate text chunks.
*/
function addActModeHint(text: string): React.ReactNode[] {
// Match "Act mode" in various capitalizations, but not if already followed by (Tab)
const actModeRegex = /\bact\s+mode\b(?!\s*\(tab\))/gi
const parts = text.split(actModeRegex)
const matches = text.match(actModeRegex)
if (!matches || parts.length <= 1) {
return [text]
}
const nodes: React.ReactNode[] = []
parts.forEach((part, i) => {
if (part) {
nodes.push(part)
}
if (matches[i]) {
nodes.push(
<React.Fragment key={`act-mode-${i}`}>
{matches[i]}
<Text color="gray"> (Tab)</Text>
</React.Fragment>,
)
}
})
return nodes
}
/**
* Render inline markdown: **bold**, *italic*, `code`
* Also adds "(Tab)" hints after "Act mode" mentions.
* Returns array of React nodes with appropriate styling
*/
function renderInlineMarkdown(text: string): React.ReactNode[] {
@@ -29,19 +64,22 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
let match
while ((match = regex.exec(text)) !== null) {
// Add text before match
// Add text before match (with Act Mode hint processing)
if (match.index > lastIndex) {
nodes.push(text.slice(lastIndex, match.index))
const beforeText = text.slice(lastIndex, match.index)
nodes.push(...addActModeHint(beforeText))
}
const fullMatch = match[0]
const key = `md-${match.index}`
if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
// Bold
// Bold - also process for Act Mode hints inside bold text
const boldContent = fullMatch.slice(2, -2)
const hintedContent = addActModeHint(boldContent)
nodes.push(
<Text bold key={key}>
{fullMatch.slice(2, -2)}
{hintedContent}
</Text>,
)
} else if (fullMatch.startsWith("*") && fullMatch.endsWith("*")) {
@@ -59,12 +97,12 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
lastIndex = regex.lastIndex
}
// Add remaining text
// Add remaining text (with Act Mode hint processing)
if (lastIndex < text.length) {
nodes.push(text.slice(lastIndex))
nodes.push(...addActModeHint(text.slice(lastIndex)))
}
return nodes.length > 0 ? nodes : [text]
return nodes.length > 0 ? nodes : addActModeHint(text)
}
/**
@@ -224,7 +262,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow>
<Text>{text}</Text>
<MarkdownText>{text}</MarkdownText>
</DotRow>
</Box>
)
+104 -70
View File
@@ -101,13 +101,14 @@
* - log-update: node_modules/ink/build/log-update.js (eraseLines logic)
*/
import type { ModelInfo } from "@shared/api"
import type { ApiProvider, ModelInfo } from "@shared/api"
import { combineCommandSequences } from "@shared/combineCommandSequences"
import type { ClineAsk, ClineMessage } from "@shared/ExtensionMessage"
import { getApiMetrics, getLastApiReqTotalTokens } from "@shared/getApiMetrics"
import { EmptyRequest, StringRequest } from "@shared/proto/cline/common"
import type { SlashCommandInfo } from "@shared/proto/cline/slash"
import { CLI_ONLY_COMMANDS } from "@shared/slashCommands"
import { getProviderModelIdKey } from "@shared/storage"
import type { Mode } from "@shared/storage/types"
import { execSync } from "child_process"
import { Box, Static, Text, useApp, useInput } from "ink"
@@ -120,7 +121,9 @@ import { Logger } from "@/shared/services/Logger"
import { Session } from "@/shared/services/Session"
import { COLORS } from "../constants/colors"
import { useTaskContext, useTaskState } from "../context/TaskContext"
import { useHomeEndKeys } from "../hooks/useHomeEndKeys"
import { useIsSpinnerActive } from "../hooks/useStateSubscriber"
import { findWordEnd, findWordStart, useTextInput } from "../hooks/useTextInput"
import { moveCursorDown, moveCursorUp } from "../utils/cursor"
import { setTerminalTitle } from "../utils/display"
import {
@@ -152,7 +155,6 @@ interface ChatViewProps {
onExit?: () => void
onComplete?: () => void
onError?: () => void
robotTopRow?: number
initialPrompt?: string
initialImages?: string[]
taskId?: string
@@ -319,7 +321,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
onExit,
onComplete: _onComplete,
onError,
robotTopRow,
initialPrompt,
initialImages,
taskId,
@@ -335,9 +336,22 @@ export const ChatView: React.FC<ChatViewProps> = ({
// Prefer prop controller over context controller (memoized for stable reference in callbacks)
const ctrl = useMemo(() => controller || taskController, [controller, taskController])
// Input state
const [textInput, setTextInput] = useState("")
const [cursorPos, setCursorPos] = useState(0)
// Input state - using hook for text editing with keyboard shortcuts
const {
text: textInput,
cursorPos,
setText: setTextInput,
setCursorPos,
handleKeyboardSequence,
handleCtrlShortcut,
deleteCharBefore,
insertText: insertTextAtCursor,
} = useTextInput()
// Ref for text input (used by useHomeEndKeys)
const textInputRef = useRef(textInput)
textInputRef.current = textInput
const [fileResults, setFileResults] = useState<FileSearchResult[]>([])
const [selectedIndex, setSelectedIndex] = useState(0) // For file menu
const [historyIndex, setHistoryIndex] = useState(-1) // -1 = not browsing history, 0+ = history item index
@@ -367,9 +381,19 @@ export const ChatView: React.FC<ChatViewProps> = ({
// Panel state
const [activePanel, setActivePanel] = useState<
{ type: "settings"; initialMode?: "model-picker" | "featured-models" } | { type: "history" } | { type: "help" } | null
| { type: "settings"; initialMode?: "model-picker" | "featured-models"; initialModelKey?: "actModelId" | "planModelId" }
| { type: "history" }
| { type: "help" }
| null
>(null)
// Handle Home/End keys from raw stdin (Ink doesn't expose these in useInput)
useHomeEndKeys({
onHome: useCallback(() => setCursorPos(0), [setCursorPos]),
onEnd: useCallback(() => setCursorPos(textInputRef.current.length), [setCursorPos]),
isActive: !activePanel, // Only active when no panel is open
})
// Track when we're exiting to hide UI elements before exit
const [isExiting, setIsExiting] = useState(false)
@@ -417,21 +441,23 @@ export const ChatView: React.FC<ChatViewProps> = ({
StateManager.get().setGlobalState("autoApproveAllToggled", newValue)
}, [autoApproveAll])
// Get model ID based on current mode
// Re-read when activePanel changes (settings panel closes) to pick up changes
const modelId = useMemo(() => {
const stateManager = StateManager.get()
const modelKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
return (stateManager.getGlobalSettingsKey(modelKey) as string) || "claude-sonnet-4-20250514"
}, [mode, activePanel])
// Get provider based on current mode
// Get provider based on current mode (computed first since modelId depends on it)
const provider = useMemo(() => {
const stateManager = StateManager.get()
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
return (stateManager.getGlobalSettingsKey(providerKey) as string) || "anthropic"
return (stateManager.getGlobalSettingsKey(providerKey) as string) || ""
}, [mode, activePanel])
// Get model ID based on current mode and provider
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
// Re-read when activePanel changes (settings panel closes) to pick up changes
const modelId = useMemo(() => {
if (!provider) return ""
const stateManager = StateManager.get()
const modelKey = getProviderModelIdKey(provider as ApiProvider, mode)
return (stateManager.getGlobalSettingsKey(modelKey as string) as string) || ""
}, [mode, provider, activePanel])
const toggleMode = useCallback(async () => {
const newMode: Mode = mode === "act" ? "plan" : "act"
setMode(newMode)
@@ -574,6 +600,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
if (m.say === "checkpoint_created") return false
if (m.say === "api_req_started") return false
if (m.say === "api_req_retried") return false // Redundant with error_retry messages
if (m.say === "reasoning") return false // Hide thinking traces - they clutter the UI
return true
})
@@ -939,13 +966,48 @@ export const ChatView: React.FC<ChatViewProps> = ({
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath])
// Handle keyboard input
//
// KEYBOARD PRIORITY ORDER (first match wins):
// 1. Mouse escape sequences -> filtered out (from AsciiMotionCli tracking)
// 2. Option+arrow escape sequences -> word navigation (handleKeyboardSequence)
// 3. Option+arrow via key.meta -> word navigation (backup for when Ink parses it)
// 4. Panel open -> bail (let panel handle its own input)
// 5. Slash menu open -> menu navigation (up/down/tab/return/escape)
// 6. File menu open -> menu navigation (up/down/tab/return/escape)
// 7. History navigation -> up/down when input empty or matches history item
// 8. Button actions -> "1"/"2" keys when buttons shown and no text typed
// 9. Ask responses -> return to send, numbers for option selection
// 10. Ctrl shortcuts -> Ctrl+A/E/W/U (handleCtrlShortcut)
// 11. Large paste detection -> collapse into placeholder
// 12. Normal input -> tab (mode toggle), return (submit), backspace, arrows, text
//
// Note: Home/End keys are handled separately by useHomeEndKeys hook because
// Ink doesn't expose them in useInput (it sets input='' for these keys).
//
useInput((input, key) => {
// Filter out mouse escape sequences from AsciiMotionCli's mouse tracking
// 1. Filter out mouse escape sequences from AsciiMotionCli's mouse tracking
if (isMouseEscapeSequence(input)) {
return
}
// When a panel is open, let the panel handle its own input
// 2. Handle Option+arrow escape sequences for word navigation
if (handleKeyboardSequence(input)) {
return
}
// 3. Handle Option+arrow via key.meta (backup - Ink sometimes parses these instead of passing raw sequence)
if (key.meta) {
if (key.leftArrow) {
setCursorPos(findWordStart(textInput, cursorPos))
return
}
if (key.rightArrow) {
setCursorPos(findWordEnd(textInput, cursorPos))
return
}
}
// 4. When a panel is open, let the panel handle its own input
if (activePanel) {
return
}
@@ -953,7 +1015,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
const inSlashMenu = slashInfo.inSlashMode && filteredCommands.length > 0 && !slashMenuDismissed
const inFileMenu = mentionInfo.inMentionMode && fileResults.length > 0 && !inSlashMenu
// Slash command menu navigation (takes priority over file menu)
// 5. Slash command menu navigation (takes priority over file menu)
if (inSlashMenu) {
if (key.upArrow) {
setSelectedSlashIndex((i) => Math.max(0, i - 1))
@@ -984,13 +1046,16 @@ export const ChatView: React.FC<ChatViewProps> = ({
return
}
if (cmd.name === "models") {
// If separate models for plan/act is enabled, just open settings (user picks which mode)
const hasSeparateModels = StateManager.get().getGlobalSettingsKey("planActSeparateModelsSetting")
const apiConfig = StateManager.get().getApiConfiguration()
const provider = apiConfig.actModeApiProvider || apiConfig.planModeApiProvider
const initialMode =
hasSeparateModels || !provider ? undefined : provider === "cline" ? "featured-models" : "model-picker"
setActivePanel({ type: "settings", initialMode })
// Use current mode's provider to determine picker type
const provider =
mode === "act"
? apiConfig.actModeApiProvider || apiConfig.planModeApiProvider
: apiConfig.planModeApiProvider || apiConfig.actModeApiProvider
const initialMode = !provider ? undefined : provider === "cline" ? "featured-models" : "model-picker"
// Set model for current mode (plan or act)
const initialModelKey = mode === "act" ? "actModelId" : "planModelId"
setActivePanel({ type: "settings", initialMode, initialModelKey })
setTextInput("")
setCursorPos(0)
setSelectedSlashIndex(0)
@@ -1030,7 +1095,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
}
// File mention menu navigation
// 6. File mention menu navigation
if (inFileMenu) {
if (key.upArrow) {
setSelectedIndex((i) => Math.max(0, i - 1))
@@ -1058,7 +1123,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
}
// History navigation with up/down arrows
// 7. History navigation with up/down arrows
// Only works when: input is empty, or input matches the currently selected history item
if (key.upArrow && !inSlashMenu && !inFileMenu) {
const historyItems = getHistoryItems()
@@ -1108,7 +1173,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
}
// Handle button actions (1 for primary, 2 for secondary)
// 8. Handle button actions (1 for primary, 2 for secondary)
// Only when buttons are enabled, not streaming, and no text has been typed
if (
buttonConfig.enableButtons &&
@@ -1135,7 +1200,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
}
// Handle ask responses for options and text input
// 9. Handle ask responses for options and text input
if (pendingAsk && !isYoloSuppressed(yolo, pendingAsk.ask as ClineAsk | undefined)) {
// Allow sending text message for any ask type where sending is enabled
if (key.return && textInput.trim() && !buttonConfig.sendingDisabled) {
@@ -1153,40 +1218,12 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
}
// Handle Ctrl+ shortcuts
const keydown = input?.toLowerCase()
if (key.ctrl && cursorPos && keydown) {
switch (keydown) {
case "u": // Ctrl+U, clear line before cursor
if (cursorPos > 0) {
setTextInput((prev) => prev.slice(cursorPos))
setCursorPos(0)
}
return
case "e": // Ctrl+E, move cursor to end
setCursorPos(textInput.length)
return
case "b": // Ctrl+B, move cursor left
setCursorPos((pos) => Math.max(0, pos - 1))
return
case "f": // Ctrl+F, move cursor right
setCursorPos((pos) => Math.min(textInput.length, pos + 1))
return
case "d": // Ctrl+D, delete character after cursor
if (cursorPos < textInput.length) {
setTextInput((prev) => prev.slice(0, cursorPos) + prev.slice(cursorPos + 1))
}
return
case "h": // Ctrl+H, delete character before cursor (like backspace)
if (cursorPos > 0) {
setTextInput((prev) => prev.slice(0, cursorPos - 1) + prev.slice(cursorPos))
setCursorPos((pos) => pos - 1)
}
return
}
// 10. Handle Ctrl+ shortcuts (Ctrl+A, Ctrl+E, Ctrl+W, etc.)
if (key.ctrl && input && handleCtrlShortcut(input)) {
return
}
// Detect paste by checking if input length exceeds threshold
// 11. Detect paste by checking if input length exceeds threshold
// Large pastes mess up the terminal UI, so we collapse them into a placeholder
// Terminal sends large pastes in multiple chunks, so we combine chunks that arrive rapidly
if (input && input.length > PASTE_COLLAPSE_THRESHOLD) {
@@ -1247,7 +1284,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
return // Exit early - don't also add the raw input via normal handling below
}
// Normal input handling
// 12. Normal input handling
if (key.shift && key.tab) {
toggleAutoApproveAll()
return
@@ -1263,10 +1300,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
return
}
if (key.backspace || key.delete) {
if (cursorPos > 0) {
setTextInput((prev) => prev.slice(0, cursorPos - 1) + prev.slice(cursorPos))
setCursorPos((pos) => pos - 1)
}
deleteCharBefore()
return
}
// Cursor movement (when not in a menu)
@@ -1288,8 +1322,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
// Normal input (single char or short paste)
if (input && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow && !key.tab) {
setTextInput((prev) => prev.slice(0, cursorPos) + input + prev.slice(cursorPos))
setCursorPos((pos) => pos + input.length)
insertTextAtCursor(input)
}
})
@@ -1353,7 +1386,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
{/* Animated robot and welcome text - only shown before messages start and user hasn't scrolled */}
{isWelcomeState && (
<Box flexDirection="column" marginBottom={1}>
<AsciiMotionCli onScroll={() => setUserScrolled(true)} robotTopRow={robotTopRow} />
<AsciiMotionCli onScroll={() => setUserScrolled(true)} />
<Text> </Text>
<Text bold color="white">
{centerText("What can I do for you?")}
@@ -1404,6 +1437,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
<SettingsPanelContent
controller={ctrl}
initialMode={activePanel.initialMode}
initialModelKey={activePanel.initialModelKey}
onClose={() => setActivePanel(null)}
/>
)}
+25 -23
View File
@@ -35,30 +35,32 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
</>
)}
{featuredModels.map((model, i) => (
<Box flexDirection="column" key={model.id} marginBottom={1}>
<Box>
<Text color={i === selectedIndex ? COLORS.primaryBlue : undefined}>
{i === selectedIndex ? " " : " "}
</Text>
<Text bold color={i === selectedIndex ? COLORS.primaryBlue : "white"}>
{model.name}
</Text>
{model.label && (
<>
<Text> </Text>
<Text backgroundColor={model.label === "FREE" ? "gray" : COLORS.primaryBlue} color="black">
{" "}
{model.label}{" "}
</Text>
</>
)}
{featuredModels.map((model, i) => {
const isSelected = i === selectedIndex
return (
<Box flexDirection="column" key={model.id} marginBottom={1}>
<Box>
<Text color={isSelected ? COLORS.primaryBlue : undefined}>{isSelected ? " " : " "}</Text>
<Text bold color={isSelected ? COLORS.primaryBlue : "white"}>
{model.name}
</Text>
{model.label && (
<>
<Text> </Text>
<Text backgroundColor={model.label === "FREE" ? "gray" : COLORS.primaryBlue} color="black">
{" "}
{model.label}{" "}
</Text>
</>
)}
</Box>
<Box paddingLeft={2}>
<Text color="gray">{model.description}</Text>
</Box>
</Box>
<Box paddingLeft={2}>
<Text color="gray">{model.description}</Text>
</Box>
</Box>
))}
)
})}
{showBrowseAll && (
<Box>
+8 -3
View File
@@ -6,6 +6,8 @@
import { Box, Text, useInput } from "ink"
import React, { useCallback, useEffect, useState } from "react"
import { StateManager } from "@/core/storage/StateManager"
import type { ApiProvider } from "@/shared/api"
import { getProviderModelIdKey } from "@/shared/storage"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import {
@@ -69,13 +71,16 @@ export const ImportView: React.FC<ImportViewProps> = ({ source, onComplete, onCa
// Set API key
config[selectedKey.keyField] = selectedKey.key
// Set model ID if available
// Set model ID if available (use provider-specific keys)
if (selectedKey.modelId) {
config.actModeApiModelId = selectedKey.modelId
config.planModeApiModelId = selectedKey.modelId
const actModelKey = getProviderModelIdKey(selectedKey.provider as ApiProvider, "act")
const planModelKey = getProviderModelIdKey(selectedKey.provider as ApiProvider, "plan")
if (actModelKey) config[actModelKey] = selectedKey.modelId
if (planModelKey) config[planModelKey] = selectedKey.modelId
}
stateManager.setApiConfiguration(config)
stateManager.setGlobalState("welcomeViewCompleted", true)
await stateManager.flushPendingState()
onComplete()
+11 -29
View File
@@ -4,31 +4,12 @@
import React, { useMemo } from "react"
import { StateManager } from "@/core/storage/StateManager"
import { API_PROVIDERS_LIST, ApiConfiguration } from "@/shared/api"
import providersData from "@/shared/providers/providers.json"
import type { ApiConfiguration } from "@/shared/api"
import { CLI_EXCLUDED_PROVIDERS, getProviderLabel, getProviderOrder } from "../utils/providers"
import { SearchableList, SearchableListItem } from "./SearchableList"
// Create a lookup map from provider value to display label
const providerLabels: Record<string, string> = Object.fromEntries(
providersData.list.map((p: { value: string; label: string }) => [p.value, p.label]),
)
// Get provider order from providers.json (same order as webview)
const providerOrder: string[] = providersData.list.map((p: { value: string }) => p.value)
/**
* Providers that are not supported in CLI.
* - vscode-lm: Requires VS Code's Language Model API (see ENG-1490 for OAuth-based support)
*/
const CLI_EXCLUDED_PROVIDERS = new Set<string>(["vscode-lm"])
export function getProviderLabel(providerId: string): string {
return providerLabels[providerId] || providerId
}
export function getProviderOrder(): string[] {
return providerOrder
}
// Re-export for backwards compatibility
export { CLI_EXCLUDED_PROVIDERS, getProviderLabel, getProviderOrder }
/**
* Check if a provider is configured (has required credentials/settings)
@@ -37,7 +18,8 @@ export function getProviderOrder(): string[] {
function isProviderConfigured(providerId: string, config: ApiConfiguration): boolean {
switch (providerId) {
case "cline":
return true // Always available
// Check if user has Cline account auth data stored
return !!(config as Record<string, unknown>)["cline:clineAccountId"]
case "anthropic":
return !!config.apiKey
case "openrouter":
@@ -51,7 +33,8 @@ function isProviderConfigured(providerId: string, config: ApiConfiguration): boo
case "openai-native":
return !!config.openAiNativeApiKey
case "openai-codex":
return !!config.openAiCodexRefreshToken
// OpenAI Codex uses OAuth with credentials stored as JSON blob
return !!(config as Record<string, unknown>)["openai-codex-oauth-credentials"]
case "deepseek":
return !!config.deepSeekApiKey
case "xai":
@@ -143,12 +126,11 @@ export const ProviderPicker: React.FC<ProviderPickerProps> = ({ onSelect, isActi
// Get API configuration to check which providers are configured
const apiConfig = StateManager.get().getApiConfiguration()
// Use providers.json order, filtered to available providers (excluding CLI-incompatible ones)
// Use providers.json order, filtered to exclude CLI-incompatible providers
const items: SearchableListItem[] = useMemo(() => {
const availableProviders = new Set(API_PROVIDERS_LIST)
const sorted = providerOrder.filter((p) => availableProviders.has(p) && !CLI_EXCLUDED_PROVIDERS.has(p))
const sorted = getProviderOrder().filter((p: string) => !CLI_EXCLUDED_PROVIDERS.has(p))
return sorted.map((providerId) => ({
return sorted.map((providerId: string) => ({
id: providerId,
label: getProviderLabel(providerId),
suffix: isProviderConfigured(providerId, apiConfig) ? "(Configured)" : undefined,
+153 -37
View File
@@ -5,17 +5,20 @@
import type { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { ProviderToApiKeyMap } from "@shared/storage"
import type { ApiProvider } from "@shared/api"
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@shared/storage"
import type { TelemetrySetting } from "@shared/TelemetrySetting"
import { Box, Text, useInput } from "ink"
import Spinner from "ink-spinner"
import React, { useCallback, useEffect, useMemo, useState } from "react"
import { buildApiHandler } from "@/core/api"
import type { Controller } from "@/core/controller"
import { StateManager } from "@/core/storage/StateManager"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { ClineAccountService } from "@/services/account/ClineAccountService"
import { AuthService, ClineAccountOrganization } from "@/services/auth/AuthService"
import { openExternal } from "@/utils/env"
import { version as CLI_VERSION } from "../../package.json"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import { isMouseEscapeSequence } from "../utils/input"
@@ -39,6 +42,7 @@ interface SettingsPanelContentProps {
onClose: () => void
controller?: Controller
initialMode?: "model-picker" | "featured-models"
initialModelKey?: "actModelId" | "planModelId"
}
type SettingsTab = "api" | "auto-approve" | "features" | "other" | "account"
@@ -113,7 +117,12 @@ function formatBalance(balance: number | null): string {
return `$${(balance / 1000000).toFixed(2)}`
}
export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onClose, controller, initialMode }) => {
export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
onClose,
controller,
initialMode,
initialModelKey,
}) => {
const { isRawModeSupported } = useStdinContext()
const stateManager = StateManager.get()
@@ -122,7 +131,9 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
const [selectedIndex, setSelectedIndex] = useState(0)
const [isEditing, setIsEditing] = useState(false)
const [isPickingModel, setIsPickingModel] = useState(initialMode === "model-picker")
const [pickingModelKey, setPickingModelKey] = useState<"actModelId" | "planModelId" | null>(initialMode ? "actModelId" : null)
const [pickingModelKey, setPickingModelKey] = useState<"actModelId" | "planModelId" | null>(
initialMode ? (initialModelKey ?? "actModelId") : null,
)
const [isPickingFeaturedModel, setIsPickingFeaturedModel] = useState(initialMode === "featured-models")
const [featuredModelIndex, setFeaturedModelIndex] = useState(0)
const [isPickingProvider, setIsPickingProvider] = useState(false)
@@ -180,12 +191,31 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
const [accountChecked, setAccountChecked] = useState(false) // Tracks if we've already checked auth
// Get current provider and model info
const apiConfig = stateManager.getApiConfiguration()
const [provider, setProvider] = useState<string>(
() => apiConfig.actModeApiProvider || apiConfig.planModeApiProvider || "not configured",
() =>
stateManager.getApiConfiguration().actModeApiProvider ||
stateManager.getApiConfiguration().planModeApiProvider ||
"not configured",
)
const actModelId = (stateManager.getGlobalSettingsKey("actModeApiModelId") as string) || ""
const planModelId = (stateManager.getGlobalSettingsKey("planModeApiModelId") as string) || ""
// Refresh trigger to force re-reading model IDs from state
const [modelRefreshKey, setModelRefreshKey] = useState(0)
const refreshModelIds = useCallback(() => setModelRefreshKey((k) => k + 1), [])
// Read model IDs from state (re-reads when refreshKey changes)
const { actModelId, planModelId } = useMemo(() => {
const apiConfig = stateManager.getApiConfiguration()
const actProvider = apiConfig.actModeApiProvider
const planProvider = apiConfig.planModeApiProvider || actProvider
if (!actProvider && !planProvider) {
return { actModelId: "", planModelId: "" }
}
const actKey = actProvider ? getProviderModelIdKey(actProvider as ApiProvider, "act") : null
const planKey = planProvider ? getProviderModelIdKey(planProvider as ApiProvider, "plan") : null
return {
actModelId: actKey ? (stateManager.getGlobalSettingsKey(actKey as string) as string) || "" : "",
planModelId: planKey ? (stateManager.getGlobalSettingsKey(planKey as string) as string) || "" : "",
}
}, [modelRefreshKey, stateManager])
// Toggle a feature setting
const toggleFeature = useCallback(
@@ -227,19 +257,21 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
return
}
// Get organization info
const organizations = authService.getUserOrganizations()
const accountService = ClineAccountService.getInstance()
// Fetch fresh organization info from server (like webview's getUserOrganizations RPC)
// Don't use authService.getUserOrganizations() as it returns cached data
const organizations = await accountService.fetchUserOrganizationsRPC()
let activeOrgId: string | undefined
if (organizations) {
setAccountOrganizations(organizations)
const activeOrg = organizations.find((org) => org.active)
setAccountOrganization(activeOrg || null)
activeOrgId = activeOrg?.organizationId
}
// Fetch credit balance
try {
const accountService = ClineAccountService.getInstance()
const activeOrgId = authService.getActiveOrganizationId()
if (activeOrgId) {
const orgBalance = await accountService.fetchOrganizationCreditsRPC(activeOrgId)
if (orgBalance?.balance !== undefined) {
@@ -299,9 +331,8 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
setIsPickingOrganization(false)
try {
await ClineAccountService.getInstance().switchAccount(orgId || undefined)
// Refetch to get updated auth info with new active org
await AuthService.getInstance(controller).restoreRefreshTokenAndRetrieveAuthInfo()
fetchAccountInfo()
// Refetch fresh org data from server
await fetchAccountInfo()
} catch {
// Error switching organization
}
@@ -334,6 +365,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
setAccountChecked(false) // Reset so fetchAccountInfo can run
await applyProviderConfig({ providerId: "cline", controller })
setProvider("cline")
refreshModelIds()
fetchAccountInfo()
}
}
@@ -529,9 +561,11 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
key: "telemetry",
label: "Error/usage reporting",
type: "checkbox",
value: telemetry === "enabled",
value: telemetry !== "disabled",
description: "Help improve Cline by sending anonymous usage data",
},
{ key: "separator", label: "", type: "separator", value: "" },
{ key: "version", label: "", type: "readonly", value: `Cline v${CLI_VERSION}` },
]
case "account":
@@ -683,8 +717,15 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
stateManager.setGlobalState("planActSeparateModelsSetting", newValue)
// When disabling separate models, sync plan model to act model
if (!newValue) {
const actModel = stateManager.getGlobalSettingsKey("actModeApiModelId")
stateManager.setGlobalState("planModeApiModelId", actModel)
const apiConfig = stateManager.getApiConfiguration()
const actProvider = apiConfig.actModeApiProvider
const planProvider = apiConfig.planModeApiProvider || actProvider
if (actProvider) {
const actKey = getProviderModelIdKey(actProvider as ApiProvider, "act")
const planKey = planProvider ? getProviderModelIdKey(planProvider as ApiProvider, "plan") : null
const actModel = stateManager.getGlobalSettingsKey(actKey as string)
if (planKey) stateManager.setGlobalState(planKey, actModel)
}
}
return
}
@@ -718,8 +759,11 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
const newTelemetry: TelemetrySetting = newValue ? "enabled" : "disabled"
setTelemetry(newTelemetry)
stateManager.setGlobalState("telemetrySetting", newTelemetry)
// Update telemetry providers to respect the new setting
controller?.updateTelemetrySetting(newTelemetry)
// Flush synchronously before continuing - must complete before app can exit
void stateManager.flushPendingState().then(() => {
// Update telemetry providers to respect the new setting
controller?.updateTelemetrySetting(newTelemetry)
})
return
}
@@ -767,21 +811,69 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
// Handle model selection from picker
const handleModelSelect = useCallback(
(modelId: string) => {
async (modelId: string) => {
if (!pickingModelKey) return
const apiConfig = stateManager.getApiConfiguration()
const actProvider = apiConfig.actModeApiProvider
const planProvider = apiConfig.planModeApiProvider || actProvider
const providerForSelection = separateModels
? pickingModelKey === "actModelId"
? actProvider
: planProvider
: actProvider || planProvider
if (!providerForSelection) return
// Use provider-specific model ID keys (e.g., cline uses actModeOpenRouterModelId)
const actKey = actProvider ? getProviderModelIdKey(actProvider as ApiProvider, "act") : null
const planKey = planProvider ? getProviderModelIdKey(planProvider as ApiProvider, "plan") : null
// For cline/openrouter providers, also set model info (like webview does)
let modelInfo
if (providerForSelection === "cline" || providerForSelection === "openrouter") {
const openRouterModels = await controller?.readOpenRouterModels()
modelInfo = openRouterModels?.[modelId]
}
if (separateModels) {
// Only update the selected mode's model
const stateKey = pickingModelKey === "actModelId" ? "actModeApiModelId" : "planModeApiModelId"
stateManager.setGlobalState(stateKey, modelId)
const stateKey = pickingModelKey === "actModelId" ? actKey : planKey
if (stateKey) stateManager.setGlobalState(stateKey, modelId)
// Set model info for the selected mode
if (modelInfo) {
const infoKey =
pickingModelKey === "actModelId" ? "actModeOpenRouterModelInfo" : "planModeOpenRouterModelInfo"
stateManager.setGlobalState(infoKey, modelInfo)
}
} else {
// Update both modes to keep them in sync
stateManager.setGlobalState("actModeApiModelId", modelId)
stateManager.setGlobalState("planModeApiModelId", modelId)
if (actKey) stateManager.setGlobalState(actKey, modelId)
if (planKey) stateManager.setGlobalState(planKey, modelId)
// Set model info for both modes
if (modelInfo) {
stateManager.setGlobalState("actModeOpenRouterModelInfo", modelInfo)
stateManager.setGlobalState("planModeOpenRouterModelInfo", modelInfo)
}
}
// Flush pending state to ensure model ID is persisted
await stateManager.flushPendingState()
// Rebuild API handler if there's an active task
if (controller?.task) {
const currentMode = stateManager.getGlobalSettingsKey("mode")
const freshApiConfig = stateManager.getApiConfiguration()
controller.task.api = buildApiHandler({ ...freshApiConfig, ulid: controller.task.ulid }, currentMode)
}
refreshModelIds()
setIsPickingModel(false)
setPickingModelKey(null)
// If opened from /models command, close the entire settings panel
if (initialMode) {
onClose()
}
},
[pickingModelKey, separateModels, stateManager],
[pickingModelKey, separateModels, stateManager, controller, refreshModelIds, initialMode, onClose],
)
// Handle language selection from picker
@@ -812,6 +904,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
// Success - apply provider config
await applyProviderConfig({ providerId: "openai-codex", controller })
setProvider("openai-codex")
refreshModelIds()
setIsWaitingForCodexAuth(false)
} catch (error) {
openAiCodexOAuthManager.cancelAuthorizationFlow()
@@ -820,7 +913,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
}
}, [controller])
// Handle provider selection from picker
const handleProviderSelect = useCallback(
(providerId: string) => {
// Special handling for Cline - uses OAuth (but skip if already logged in)
@@ -832,6 +924,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
// Already logged in - just set the provider
applyProviderConfig({ providerId: "cline", controller })
setProvider("cline")
refreshModelIds()
} else {
// Not logged in - trigger OAuth
handleClineLogin()
@@ -870,10 +963,11 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
// Provider doesn't need an API key (rare) - just set it
applyProviderConfig({ providerId, controller })
setProvider(providerId)
refreshModelIds()
setIsPickingProvider(false)
}
},
[stateManager, startCodexAuth, handleClineLogin, controller],
[stateManager, startCodexAuth, handleClineLogin, controller, refreshModelIds],
)
// Handle API key submission after provider selection
@@ -885,11 +979,12 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
await applyProviderConfig({ providerId: pendingProvider, apiKey: submittedValue.trim(), controller })
setProvider(pendingProvider)
refreshModelIds()
setIsEnteringApiKey(false)
setPendingProvider(null)
setApiKeyValue("")
},
[pendingProvider, controller],
[pendingProvider, controller, refreshModelIds],
)
// Handle Bedrock configuration complete
@@ -906,8 +1001,11 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
const defaultModelId = getDefaultModelId("bedrock")
if (defaultModelId) {
config.actModeApiModelId = defaultModelId
config.planModeApiModelId = defaultModelId
// Use provider-specific model ID keys
const actModelKey = getProviderModelIdKey("bedrock" as ApiProvider, "act")
const planModelKey = getProviderModelIdKey("bedrock" as ApiProvider, "plan")
if (actModelKey) config[actModelKey] = defaultModelId
if (planModelKey) config[planModelKey] = defaultModelId
}
if (bedrockConfig.awsProfile !== undefined) config.awsProfile = bedrockConfig.awsProfile
@@ -919,6 +1017,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
// Close Bedrock config first, then flush state async
setProvider("bedrock")
refreshModelIds()
setIsConfiguringBedrock(false)
setPendingProvider(null)
@@ -941,17 +1040,26 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
switch (item.key) {
case "actModelId":
case "planModelId":
case "planModelId": {
// Use provider-specific model ID keys (e.g., cline uses actModeOpenRouterModelId)
const apiConfig = stateManager.getApiConfiguration()
const actProvider = apiConfig.actModeApiProvider
const planProvider = apiConfig.planModeApiProvider || actProvider
if (!actProvider && !planProvider) break
const actKey = actProvider ? getProviderModelIdKey(actProvider as ApiProvider, "act") : null
const planKey = planProvider ? getProviderModelIdKey(planProvider as ApiProvider, "plan") : null
if (separateModels) {
// Only update the selected mode's model
const stateKey = item.key === "actModelId" ? "actModeApiModelId" : "planModeApiModelId"
stateManager.setGlobalState(stateKey, editValue || undefined)
const stateKey = item.key === "actModelId" ? actKey : planKey
if (stateKey) stateManager.setGlobalState(stateKey, editValue || undefined)
} else {
// Update both modes to keep them in sync
stateManager.setGlobalState("actModeApiModelId", editValue || undefined)
stateManager.setGlobalState("planModeApiModelId", editValue || undefined)
if (actKey) stateManager.setGlobalState(actKey, editValue || undefined)
if (planKey) stateManager.setGlobalState(planKey, editValue || undefined)
}
break
}
case "language":
setPreferredLanguage(editValue)
stateManager.setGlobalState("preferredLanguage", editValue)
@@ -1019,6 +1127,10 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
if (key.escape) {
setIsPickingFeaturedModel(false)
setPickingModelKey(null)
// If opened from /models command, close the entire settings panel
if (initialMode) {
onClose()
}
} else if (key.upArrow) {
setFeaturedModelIndex((prev) => (prev > 0 ? prev - 1 : maxIndex))
} else if (key.downArrow) {
@@ -1045,6 +1157,10 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
if (key.escape) {
setIsPickingModel(false)
setPickingModelKey(null)
// If opened from /models command, close the entire settings panel
if (initialMode) {
onClose()
}
}
return
}
@@ -1431,7 +1547,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onCl
<Text bold color={isSelected ? COLORS.primaryBlue : undefined}>
{isSelected ? "" : " "}{" "}
</Text>
<Text color={isSelected ? COLORS.primaryBlue : "white"}>{item.label}: </Text>
{item.label && <Text color={isSelected ? COLORS.primaryBlue : "white"}>{item.label}: </Text>}
<Text color={item.type === "readonly" ? "gray" : COLORS.primaryBlue}>
{typeof item.value === "string" ? item.value : String(item.value)}
</Text>
+6 -4
View File
@@ -7,7 +7,8 @@
import { Box, Text, useInput } from "ink"
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { StateManager } from "@/core/storage/StateManager"
import { getProviderDefaultModelId, Mode } from "@/shared/storage"
import type { ApiProvider } from "@/shared/api"
import { getProviderDefaultModelId, getProviderModelIdKey, Mode } from "@/shared/storage"
import { useStdinContext } from "../context/StdinContext"
import {
checkAndWarnRipgrepMissing,
@@ -73,11 +74,12 @@ export const WelcomeView: React.FC<WelcomeViewProps> = ({ onSubmit, onExit, cont
return currentProvider || "cline"
}, [controller])
// Get model ID based on current mode
// Get model ID based on current mode and provider
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
const modelId = useMemo(() => {
const stateManager = StateManager.get()
const modelKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
return (stateManager.getGlobalSettingsKey(modelKey) as string) || getProviderDefaultModelId(provider)
const modelKey = getProviderModelIdKey(provider as ApiProvider, mode)
return (stateManager.getGlobalSettingsKey(modelKey as string) as string) || getProviderDefaultModelId(provider)
}, [mode, provider])
const toggleMode = useCallback(() => {
+2 -2
View File
@@ -34,8 +34,8 @@ export const FEATURED_MODELS = {
free: [
{
id: "moonshotai/kimi-k2.5",
name: "MoonshotAI Kimi K2.5",
description: "Topping benchmarks leading open source",
name: "Kimi K2.5",
description: "State-of-the-art model topping benchmarks",
label: "FREE",
},
{
+40
View File
@@ -0,0 +1,40 @@
/**
* Keyboard escape sequences for terminal input handling.
*
* Different terminals send different escape sequences for the same keys.
* This file consolidates all known sequences to ensure broad compatibility.
*
* Note on Ink's useInput limitations:
* - Ink parses Home/End keys but doesn't expose them in the key object
* - Ink sets input='' for Home/End (they're in nonAlphanumericKeys)
* - We use useHomeEndKeys hook to intercept these from raw stdin
* - Option+arrow sometimes comes as escape sequence, sometimes as key.meta + arrow
*/
// Home key escape sequences from various terminals
export const HOME_SEQUENCES = new Set([
"\x1b[H", // CSI H - most common (xterm, Terminal.app)
"\x1b[1~", // CSI 1 ~ - Linux console, some xterms
"\x1bOH", // SS3 H - xterm application mode
"\x1b[7~", // rxvt
])
// End key escape sequences from various terminals
export const END_SEQUENCES = new Set([
"\x1b[F", // CSI F - most common (xterm, Terminal.app)
"\x1b[4~", // CSI 4 ~ - Linux console, some xterms
"\x1bOF", // SS3 F - xterm application mode
"\x1b[8~", // rxvt
])
// Option+Left (move word left) escape sequences
export const OPTION_LEFT_SEQUENCES = new Set([
"\x1bb", // Meta+b - emacs style
"\x1b[1;3D", // CSI 1;3 D - xterm with modifiers
])
// Option+Right (move word right) escape sequences
export const OPTION_RIGHT_SEQUENCES = new Set([
"\x1bf", // Meta+f - emacs style
"\x1b[1;3C", // CSI 1;3 C - xterm with modifiers
])
+54
View File
@@ -0,0 +1,54 @@
/**
* Hook to detect Home/End keys from raw stdin.
*
* Ink's useInput hook parses Home/End keys but doesn't expose them in the key object,
* and sets input to '' for these keys (because they're in nonAlphanumericKeys).
* This hook subscribes to raw stdin events to detect Home/End before Ink processes them.
*/
import { useStdin } from "ink"
import { useCallback, useEffect, useRef } from "react"
import { END_SEQUENCES, HOME_SEQUENCES } from "../constants/keyboard"
interface UseHomeEndKeysOptions {
onHome: () => void
onEnd: () => void
isActive?: boolean
}
/**
* Subscribe to raw stdin to detect Home/End keys.
* These keys are parsed by Ink but not exposed in useInput's key object.
*/
export function useHomeEndKeys({ onHome, onEnd, isActive = true }: UseHomeEndKeysOptions): void {
// Use refs to avoid stale closure issues
const onHomeRef = useRef(onHome)
const onEndRef = useRef(onEnd)
onHomeRef.current = onHome
onEndRef.current = onEnd
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { internal_eventEmitter } = useStdin() as any
const handleInput = useCallback((data: Buffer | string) => {
const s = typeof data === "string" ? data : data.toString()
if (HOME_SEQUENCES.has(s)) {
onHomeRef.current()
} else if (END_SEQUENCES.has(s)) {
onEndRef.current()
}
}, [])
useEffect(() => {
if (!isActive || !internal_eventEmitter) {
return
}
internal_eventEmitter.on("input", handleInput)
return () => {
internal_eventEmitter.removeListener("input", handleInput)
}
}, [isActive, internal_eventEmitter, handleInput])
}
+8 -6
View File
@@ -1,4 +1,3 @@
import { useStdout } from "ink"
import { useCallback, useEffect, useRef, useState } from "react"
/**
@@ -36,7 +35,6 @@ import { useCallback, useEffect, useRef, useState } from "react"
* content wrapper to force full remount.
*/
export function useTerminalSize() {
const { stdout } = useStdout()
const [size, setSize] = useState({
columns: process.stdout.columns || 80,
rows: process.stdout.rows || 24,
@@ -47,10 +45,14 @@ export function useTerminalSize() {
const refreshAfterResize = useCallback(() => {
// Clear terminal + scrollback to wipe stale content from old width
// \x1b[2J clears visible screen, \x1b[3J clears scrollback, \x1b[H moves cursor home
stdout?.write("\x1b[2J\x1b[3J\x1b[H")
// Increment key to force React remount
setResizeKey((prev) => prev + 1)
}, [stdout])
// Use process.stdout directly with callback to ensure clear completes before React re-renders.
// Without the callback, the state update can trigger a re-render that interleaves with
// the buffered escape sequences, causing visual artifacts in the scrollback.
process.stdout.write("\x1b[2J\x1b[3J\x1b[H", () => {
// Increment key to force React remount only after clear is flushed
setResizeKey((prev) => prev + 1)
})
}, [])
useEffect(() => {
function updateSize() {
+207
View File
@@ -0,0 +1,207 @@
/**
* Text input hook with cursor management and keyboard shortcut handling.
*
* Supports essential terminal shortcuts:
* - Option+Left/Right: move by word (via escape sequences)
* - Ctrl+A/E: start/end of line
* - Ctrl+W: delete word backwards
* - Ctrl+U: delete to start of line
*
* Note: Home/End keys are handled by useHomeEndKeys hook because Ink doesn't
* expose them in useInput (it sets input='' for these keys).
*/
import { useCallback, useRef, useState } from "react"
import { OPTION_LEFT_SEQUENCES, OPTION_RIGHT_SEQUENCES } from "../constants/keyboard"
/**
* Keyboard escape sequence types for special key combinations.
* Only includes sequences that Ink passes through in the input string.
*/
type KeyboardSequence =
| "option-left" // Move word left
| "option-right" // Move word right
| null
/**
* Parse keyboard escape sequences for special key combinations.
* Only handles Option+arrow - Home/End are handled by useHomeEndKeys.
*/
function parseKeyboardSequence(input: string): KeyboardSequence {
if (OPTION_LEFT_SEQUENCES.has(input)) {
return "option-left"
}
if (OPTION_RIGHT_SEQUENCES.has(input)) {
return "option-right"
}
return null
}
/**
* Find the start of the previous word from cursor position.
*/
export function findWordStart(text: string, cursorPos: number): number {
let pos = cursorPos
// Skip whitespace before cursor
while (pos > 0 && /\s/.test(text[pos - 1])) {
pos--
}
// Skip word characters
while (pos > 0 && !/\s/.test(text[pos - 1])) {
pos--
}
return pos
}
/**
* Find the end of the next word from cursor position.
*/
export function findWordEnd(text: string, cursorPos: number): number {
let pos = cursorPos
// Skip word characters
while (pos < text.length && !/\s/.test(text[pos])) {
pos++
}
// Skip whitespace
while (pos < text.length && /\s/.test(text[pos])) {
pos++
}
return pos
}
export interface UseTextInputReturn {
// State
text: string
cursorPos: number
// Text manipulation
setText: (text: string) => void
insertText: (text: string) => void
setCursorPos: (pos: number | ((prev: number) => number)) => void
// Deletion
deleteCharBefore: () => void
// Keyboard shortcut handlers
handleKeyboardSequence: (input: string) => boolean
handleCtrlShortcut: (key: string) => boolean
}
/**
* Hook for managing text input with cursor and keyboard shortcuts.
*/
export function useTextInput(): UseTextInputReturn {
const [text, setTextState] = useState("")
const [cursorPos, setCursorPosState] = useState(0)
// Use refs to get current values in callbacks without stale closures
const textRef = useRef(text)
const cursorRef = useRef(cursorPos)
textRef.current = text
cursorRef.current = cursorPos
// Text manipulation
const setText = useCallback((newText: string) => {
setTextState(newText)
setCursorPosState(newText.length)
}, [])
const insertText = useCallback((insertedText: string) => {
const pos = cursorRef.current
setTextState((prev) => prev.slice(0, pos) + insertedText + prev.slice(pos))
setCursorPosState(pos + insertedText.length)
}, [])
const setCursorPos = useCallback((pos: number | ((prev: number) => number)) => {
setCursorPosState((prev) => {
const newPos = typeof pos === "function" ? pos(prev) : pos
return Math.max(0, Math.min(textRef.current.length, newPos))
})
}, [])
// Deletion
const deleteCharBefore = useCallback(() => {
const pos = cursorRef.current
if (pos > 0) {
setTextState((prev) => prev.slice(0, pos - 1) + prev.slice(pos))
setCursorPosState(pos - 1)
}
}, [])
const deleteWordBefore = useCallback(() => {
const pos = cursorRef.current
const wordStart = findWordStart(textRef.current, pos)
if (wordStart < pos) {
setTextState((prev) => prev.slice(0, wordStart) + prev.slice(pos))
setCursorPosState(wordStart)
}
}, [])
const deleteToStart = useCallback(() => {
const pos = cursorRef.current
if (pos > 0) {
setTextState((prev) => prev.slice(pos))
setCursorPosState(0)
}
}, [])
// Cursor movement (internal, used by handlers)
const moveToStart = useCallback(() => setCursorPosState(0), [])
const moveToEnd = useCallback(() => setCursorPosState(textRef.current.length), [])
const moveWordLeft = useCallback(() => setCursorPosState((pos) => findWordStart(textRef.current, pos)), [])
const moveWordRight = useCallback(() => setCursorPosState((pos) => findWordEnd(textRef.current, pos)), [])
// Keyboard shortcut handlers
const handleKeyboardSequence = useCallback(
(input: string): boolean => {
const seq = parseKeyboardSequence(input)
if (!seq) return false
switch (seq) {
case "option-left":
moveWordLeft()
return true
case "option-right":
moveWordRight()
return true
default:
return false
}
},
[moveWordLeft, moveWordRight],
)
const handleCtrlShortcut = useCallback(
(key: string): boolean => {
switch (key.toLowerCase()) {
case "a": // Ctrl+A - start of line
moveToStart()
return true
case "e": // Ctrl+E - end of line
moveToEnd()
return true
case "u": // Ctrl+U - delete to start
deleteToStart()
return true
case "w": // Ctrl+W - delete word backwards
deleteWordBefore()
return true
default:
return false
}
},
[moveToStart, moveToEnd, deleteToStart, deleteWordBefore],
)
return {
text,
cursorPos,
setText,
insertText,
setCursorPos,
deleteCharBefore,
handleKeyboardSequence,
handleCtrlShortcut,
}
}
+134 -56
View File
@@ -22,7 +22,6 @@ import { telemetryService } from "@/services/telemetry"
import { Logger } from "@/shared/services/Logger"
import { Session } from "@/shared/services/Session"
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@/shared/storage"
import { secretStorage } from "@/shared/storage/ClineSecretStorage"
import { version as CLI_VERSION } from "../package.json"
import { runAcpMode } from "./acp/index.js"
import { App } from "./components/App"
@@ -31,13 +30,14 @@ import { createCliHostBridgeProvider } from "./controllers"
import { CliCommentReviewController } from "./controllers/CliCommentReviewController"
import { CliWebviewProvider } from "./controllers/CliWebviewProvider"
import { restoreConsole } from "./utils/console"
import { calculateRobotTopRow, queryCursorPos } from "./utils/cursor-position"
import { printInfo, printWarning } from "./utils/display"
import { parseImagesFromInput, processImagePaths } from "./utils/parser"
import { CLINE_CLI_DIR, getCliBinaryPath } from "./utils/path"
import { readStdinIfPiped } from "./utils/piped"
import { runPlainTextTask } from "./utils/plain-text-task"
import { checkForUpdates } from "./utils/update"
import { applyProviderConfig } from "./utils/provider-config"
import { getValidCliProviders, isValidCliProvider } from "./utils/providers"
import { autoUpdateOnStartup, checkForUpdates } from "./utils/update"
import { initializeCliContext } from "./vscode-context"
import { CLI_LOG_FILE, shutdownEvent, window } from "./vscode-shim"
@@ -203,6 +203,9 @@ async function initializeCli(options: InitOptions): Promise<CliContext> {
* Run an Ink app with proper cleanup handling
*/
async function runInkApp(element: React.ReactElement, cleanup: () => Promise<void>): Promise<void> {
// Clear terminal for clean UI - robot will render at row 1
process.stdout.write("\x1b[2J\x1b[3J\x1b[H")
// Note: incrementalRendering is disabled because it causes UI glitches on terminal resize.
// Ink's incremental rendering tries to erase N lines based on previous output height,
// but when the terminal shrinks, this leaves artifacts. Gemini CLI only enables
@@ -274,14 +277,10 @@ async function runTask(
const providerKey = selectedMode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const currentProvider = StateManager.get().getGlobalSettingsKey(providerKey) as ApiProvider
// Update the generic model ID for the current mode
const modelKey = selectedMode === "act" ? "actModeApiModelId" : "planModeApiModelId"
StateManager.get().setGlobalState(modelKey, options.model)
// Also update the provider-specific model ID key if applicable
const providerModelKey = getProviderModelIdKey(currentProvider, selectedMode)
if (providerModelKey) {
StateManager.get().setGlobalState(providerModelKey, options.model)
// Update model ID using provider-specific key (e.g., cline uses actModeOpenRouterModelId)
const modelKey = getProviderModelIdKey(currentProvider, selectedMode)
if (modelKey) {
StateManager.get().setGlobalState(modelKey, options.model)
}
telemetryService.captureHostEvent("model_flag", options.model)
}
@@ -353,12 +352,6 @@ async function runTask(
exit(success ? 0 : 1)
}
// Use welcome view for consistent rendering (same as interactive mode)
// Query cursor position BEFORE Ink mounts to know where robot will render
const cursorPos = await queryCursorPos(process.stdin, process.stdout)
const terminalRows = process.stdout.rows ?? 24
const robotTopRow = calculateRobotTopRow(cursorPos, terminalRows)
let taskError = false
// Render the welcome view with optional initial prompt/images
@@ -370,7 +363,6 @@ async function runTask(
verbose: options.verbose,
controller: ctx.controller,
isRawModeSupported: checkRawModeSupport(),
robotTopRow,
initialPrompt: taskPrompt || undefined,
initialImages: imageDataUrls.length > 0 ? imageDataUrls : undefined,
onError: () => {
@@ -473,6 +465,49 @@ async function showConfig(options: { config?: string }) {
/**
* Run authentication flow
*/
/**
* Perform quick auth setup without UI - validates and saves configuration directly
*/
async function performQuickAuthSetup(
ctx: CliContext,
options: { provider: string; apikey: string; modelid: string; baseurl?: string },
): Promise<{ success: boolean; error?: string }> {
const { provider, apikey, modelid, baseurl } = options
const normalizedProvider = provider.toLowerCase().trim()
if (!isValidCliProvider(normalizedProvider)) {
const validProviders = getValidCliProviders()
return { success: false, error: `Invalid provider '${provider}'. Supported providers: ${validProviders.join(", ")}` }
}
if (normalizedProvider === "bedrock") {
return {
success: false,
error: "Bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup.",
}
}
if (baseurl && !["openai", "openai-native"].includes(normalizedProvider)) {
return { success: false, error: "Base URL is only supported for OpenAI and OpenAI-compatible providers" }
}
// Save configuration using shared utility
await applyProviderConfig({
providerId: normalizedProvider,
apiKey: apikey,
modelId: modelid,
baseUrl: baseurl,
controller: ctx.controller,
})
// Mark onboarding as complete
StateManager.get().setGlobalState("welcomeViewCompleted", true)
await StateManager.get().flushPendingState()
return { success: true }
}
async function runAuth(options: {
provider?: string
apikey?: string
@@ -484,13 +519,34 @@ async function runAuth(options: {
}) {
const ctx = await initializeCli({ ...options, enableAuth: true })
const hasQuickSetupFlags = options.provider || options.apikey || options.modelid || options.baseurl
const quickSetup = hasQuickSetupFlags
? { provider: options.provider, apikey: options.apikey, modelid: options.modelid, baseurl: options.baseurl }
: undefined
const hasQuickSetupFlags = options.provider && options.apikey && options.modelid
telemetryService.captureHostEvent("auth_command", hasQuickSetupFlags ? "quick_setup" : "interactive")
// Quick setup mode - no UI, just save configuration and exit
if (hasQuickSetupFlags) {
const result = await performQuickAuthSetup(ctx, {
provider: options.provider!,
apikey: options.apikey!,
modelid: options.modelid!,
baseurl: options.baseurl,
})
await ctx.controller.stateManager.flushPendingState()
await ctx.controller.dispose()
await ErrorService.get().dispose()
if (!result.success) {
printWarning(result.error || "Quick setup failed")
telemetryService.captureHostEvent("auth", "error")
exit(1)
}
telemetryService.captureHostEvent("auth", "completed")
exit(0)
}
// Interactive mode - show Ink UI
let authError = false
await runInkApp(
@@ -506,7 +562,6 @@ async function runAuth(options: {
telemetryService.captureHostEvent("auth", "error")
authError = true
},
authQuickSetup: quickSetup,
}),
async () => {
await ctx.controller.stateManager.flushPendingState()
@@ -596,44 +651,64 @@ devCommand
})
/**
* Check if the user has authentication configured.
* Returns true if they have either:
* - Cline provider with stored auth data
* - OpenAI Codex provider with OAuth credentials
* - BYO provider with an API key configured
* Check if the user has completed onboarding (has any provider configured).
*
* Uses `welcomeViewCompleted` as the single source of truth, matching the VS Code extension's approach.
* If `welcomeViewCompleted` is undefined (first run), checks if ANY provider has credentials
* and sets the flag accordingly.
*/
async function isAuthConfigured(): Promise<boolean> {
const stateManager = StateManager.get()
const mode = stateManager.getGlobalSettingsKey("mode") as string
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const currentProvider = (stateManager.getGlobalSettingsKey(providerKey) as string) || "cline"
if (currentProvider === "cline") {
// For Cline provider, check if we have stored auth data
const authData = await secretStorage.get("cline:clineAccountId")
return !!authData
// Check welcomeViewCompleted first - this is the single source of truth
const welcomeViewCompleted = stateManager.getGlobalStateKey("welcomeViewCompleted")
if (welcomeViewCompleted !== undefined) {
return welcomeViewCompleted
}
if (currentProvider === "openai-codex") {
// For OpenAI Codex, check if OAuth credentials are stored
const isAuthenticated = await openAiCodexOAuthManager.isAuthenticated()
return isAuthenticated
}
// welcomeViewCompleted is undefined - run migration logic to check if ANY provider has credentials
// This mirrors the extension's migrateWelcomeViewCompleted behavior
const hasAnyAuth = await checkAnyProviderConfigured()
// For BYO providers, check if the API key is configured
const keyField = ProviderToApiKeyMap[currentProvider as keyof typeof ProviderToApiKeyMap]
if (!keyField) {
return false
}
// Set welcomeViewCompleted based on what we found
stateManager.setGlobalState("welcomeViewCompleted", hasAnyAuth)
await stateManager.flushPendingState()
const fields = Array.isArray(keyField) ? keyField : [keyField]
for (const field of fields) {
const value = await secretStorage.get(field)
if (value) {
return true
return hasAnyAuth
}
/**
* Check if ANY provider has valid credentials configured.
* Used for migration when welcomeViewCompleted is undefined.
*/
async function checkAnyProviderConfigured(): Promise<boolean> {
const stateManager = StateManager.get()
const config = stateManager.getApiConfiguration() as Record<string, unknown>
// Check Cline account (stored as "cline:clineAccountId" in secrets, loaded into config)
if (config["cline:clineAccountId"]) return true
// Check OpenAI Codex OAuth (stored in SECRETS_KEYS, loaded into config)
if (config["openai-codex-oauth-credentials"]) return true
// Check all BYO provider API keys (loaded into config from secrets)
for (const [provider, keyField] of Object.entries(ProviderToApiKeyMap)) {
// Skip cline - already checked above with the correct key
if (provider === "cline") continue
const fields = Array.isArray(keyField) ? keyField : [keyField]
for (const field of fields) {
if (config[field]) return true
}
}
// Check provider-specific settings that indicate configuration
// (for providers that don't require API keys like Bedrock with IAM, Ollama, LM Studio)
if (config.awsRegion) return true
if (config.vertexProjectId) return true
if (config.ollamaBaseUrl) return true
if (config.lmStudioBaseUrl) return true
return false
}
@@ -647,11 +722,6 @@ async function showWelcome(options: { verbose?: boolean; cwd?: string; config?:
// Check if auth is configured
const hasAuth = await isAuthConfigured()
// Query cursor position BEFORE Ink mounts
const cursorPos = await queryCursorPos(process.stdin, process.stdout)
const terminalRows = process.stdout.rows ?? 24
const robotTopRow = calculateRobotTopRow(cursorPos, terminalRows)
let hadError = false
await runInkApp(
@@ -661,7 +731,6 @@ async function showWelcome(options: { verbose?: boolean; cwd?: string; config?:
verbose: options.verbose,
controller: ctx.controller,
isRawModeSupported: checkRawModeSupport(),
robotTopRow,
onWelcomeExit: () => {
exit(0)
},
@@ -706,6 +775,12 @@ program
// Always check for piped stdin content
const stdinInput = await readStdinIfPiped()
// Error if stdin was piped but empty (e.g., `echo "" | cline`)
if (stdinInput === "") {
printWarning("Empty input received from stdin. Please provide content to process.")
exit(1)
}
// If no prompt argument, check if input is piped via stdin
let effectivePrompt = prompt
if (stdinInput) {
@@ -733,5 +808,8 @@ program
}
})
// Background auto-update check (non-blocking)
autoUpdateOnStartup(CLI_VERSION)
// Parse and run
program.parse()
-57
View File
@@ -1,57 +0,0 @@
/**
* Query terminal cursor position before Ink mounts
* Must be called BEFORE render() to avoid escape sequence leaking into Ink's input handling
*/
export async function queryCursorPos(
stdin: NodeJS.ReadStream,
stdout: NodeJS.WriteStream,
{ timeoutMs = 75 } = {},
): Promise<{ row: number; col: number } | null> {
if (!stdin.isTTY || !stdout.isTTY || typeof (stdin as any).setRawMode !== "function") return null
const ttyIn = stdin as any as { isRaw?: boolean; setRawMode: (b: boolean) => void; on: any; off: any }
const wasRaw = !!ttyIn.isRaw
ttyIn.setRawMode(true)
stdin.resume()
return await new Promise((resolve) => {
let buf = ""
const onData = (chunk: Buffer) => {
buf += chunk.toString("utf8")
const m = buf.match(/\x1b\[(\d+);(\d+)R/)
if (!m) return
cleanup()
resolve({ row: Number(m[1]), col: Number(m[2]) })
}
const cleanup = () => {
clearTimeout(timer)
stdin.off("data", onData)
try {
ttyIn.setRawMode(wasRaw)
} catch {}
}
const timer = setTimeout(() => {
cleanup()
resolve(null)
}, timeoutMs)
stdin.on("data", onData)
stdout.write("\x1b[6n") // DSR: cursor position
})
}
/**
* Calculate where the robot will be rendered on screen
*/
export function calculateRobotTopRow(cursorPos: { row: number } | null, terminalRows: number): number {
const robotHeight = 12
const firstFrameHeight = robotHeight + 8 // robot + welcome text + margins + input + footer
const startRow = cursorPos?.row ?? 1
// If content doesn't fit below cursor, terminal scrolls
return Math.max(1, Math.min(startRow, terminalRows - firstFrameHeight + 1))
}
+6 -5
View File
@@ -5,6 +5,7 @@
import * as fs from "fs"
import * as os from "os"
import * as path from "path"
import { anthropicDefaultModelId, geminiDefaultModelId, openAiNativeDefaultModelId } from "@/shared/api"
import providersData from "@/shared/providers/providers.json"
// Import source types
@@ -119,17 +120,17 @@ function findOpenCodeAuthPath(): string | null {
* Map Codex key names to Cline providers
*/
const CODEX_KEY_MAP: Record<string, { provider: string; keyField: string; modelId?: string }> = {
OPENAI_API_KEY: { provider: "openai-native", keyField: "openAiNativeApiKey", modelId: "gpt-4o" },
ANTHROPIC_API_KEY: { provider: "anthropic", keyField: "apiKey", modelId: "claude-sonnet-4-20250514" },
OPENAI_API_KEY: { provider: "openai-native", keyField: "openAiNativeApiKey", modelId: openAiNativeDefaultModelId },
ANTHROPIC_API_KEY: { provider: "anthropic", keyField: "apiKey", modelId: anthropicDefaultModelId },
}
/**
* Map OpenCode provider IDs to Cline providers
*/
const OPENCODE_PROVIDER_MAP: Record<string, { provider: string; keyField: string; modelId?: string }> = {
openai: { provider: "openai-native", keyField: "openAiNativeApiKey", modelId: "gpt-4o" },
anthropic: { provider: "anthropic", keyField: "apiKey", modelId: "claude-sonnet-4-20250514" },
gemini: { provider: "gemini", keyField: "geminiApiKey", modelId: "gemini-2.0-flash-001" },
openai: { provider: "openai-native", keyField: "openAiNativeApiKey", modelId: openAiNativeDefaultModelId },
anthropic: { provider: "anthropic", keyField: "apiKey", modelId: anthropicDefaultModelId },
gemini: { provider: "gemini", keyField: "geminiApiKey", modelId: geminiDefaultModelId },
mistral: { provider: "mistral", keyField: "mistralApiKey" },
groq: { provider: "groq", keyField: "groqApiKey" },
deepseek: { provider: "deepseek", keyField: "deepSeekApiKey" },
+6 -2
View File
@@ -6,8 +6,12 @@
* Check if input contains mouse escape sequences from terminal mouse tracking.
* AsciiMotionCli enables mouse tracking which generates sequences like [<35;46;17M
* These should be filtered out of text input handlers.
*
* NOTE: This must NOT filter keyboard escape sequences like Option+arrow keys.
* Mouse sequences have specific patterns with coordinates (e.g., [<35;46;17M).
*/
export function isMouseEscapeSequence(input: string): boolean {
// Mouse events look like: [<35;46;17M or contain escape characters
return input.includes("\x1b") || input.includes("[<") || /\d+;\d+[Mm]/.test(input)
// Mouse events look like: \x1b[<35;46;17M (SGR mouse format)
// They contain [< followed by numbers, semicolons, and end with M or m
return input.includes("[<") && /\[<\d+;\d+;\d+[Mm]/.test(input)
}
+4 -1
View File
@@ -40,7 +40,10 @@ export async function readStdinIfPiped(): Promise<string | null> {
process.stdin.on("end", () => {
clearTimeout(timeout)
resolve(data.trim() || null)
// Return empty string (not null) when stdin was piped but empty
// This allows callers to distinguish between "no piped input" (null)
// and "empty piped input" ("") for proper error handling
resolve(data.trim())
})
process.stdin.on("error", () => {
+17 -3
View File
@@ -3,7 +3,8 @@
* Used by both AuthView (onboarding) and SettingsPanelContent (settings)
*/
import { ProviderToApiKeyMap } from "@shared/storage"
import type { ApiProvider } from "@shared/api"
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@shared/storage"
import { buildApiHandler } from "@/core/api"
import type { Controller } from "@/core/controller"
import { StateManager } from "@/core/storage/StateManager"
@@ -30,10 +31,23 @@ export async function applyProviderConfig(options: ApplyProviderConfigOptions):
}
// Add model ID (use provided or fall back to default)
// Use provider-specific model ID keys (e.g., actModeOpenRouterModelId for cline/openrouter)
const finalModelId = modelId || getDefaultModelId(providerId)
if (finalModelId) {
config.actModeApiModelId = finalModelId
config.planModeApiModelId = finalModelId
const actModelKey = getProviderModelIdKey(providerId as ApiProvider, "act")
const planModelKey = getProviderModelIdKey(providerId as ApiProvider, "plan")
if (actModelKey) config[actModelKey] = finalModelId
if (planModelKey) config[planModelKey] = finalModelId
// For cline/openrouter, also set model info (required for getModel() to return correct model)
if ((providerId === "cline" || providerId === "openrouter") && controller) {
const openRouterModels = await controller.readOpenRouterModels()
const modelInfo = openRouterModels?.[finalModelId]
if (modelInfo) {
stateManager.setGlobalState("actModeOpenRouterModelInfo", modelInfo)
stateManager.setGlobalState("planModeOpenRouterModelInfo", modelInfo)
}
}
}
// Add API key if provided (maps to provider-specific field like anthropicApiKey, openAiApiKey, etc.)
+48
View File
@@ -0,0 +1,48 @@
/**
* Shared provider metadata utilities
* Used by both UI components and CLI commands
*/
import providersData from "@/shared/providers/providers.json"
// Create a lookup map from provider value to display label
const providerLabels: Record<string, string> = Object.fromEntries(
providersData.list.map((p: { value: string; label: string }) => [p.value, p.label]),
)
// Get provider order from providers.json (same order as webview)
const providerOrder: string[] = providersData.list.map((p: { value: string }) => p.value)
/**
* Providers that are not supported in CLI.
* - vscode-lm: Requires VS Code's Language Model API (see ENG-1490 for OAuth-based support)
*/
export const CLI_EXCLUDED_PROVIDERS = new Set<string>(["vscode-lm"])
/**
* Get the display label for a provider ID
*/
export function getProviderLabel(providerId: string): string {
return providerLabels[providerId] || providerId
}
/**
* Get the ordered list of all provider IDs (from providers.json)
*/
export function getProviderOrder(): string[] {
return providerOrder
}
/**
* Get the list of valid CLI provider IDs (excluding unsupported providers)
*/
export function getValidCliProviders(): string[] {
return providerOrder.filter((p) => !CLI_EXCLUDED_PROVIDERS.has(p))
}
/**
* Check if a provider ID is valid for CLI use
*/
export function isValidCliProvider(providerId: string): boolean {
return providerOrder.includes(providerId) && !CLI_EXCLUDED_PROVIDERS.has(providerId)
}
+215 -24
View File
@@ -1,28 +1,175 @@
import { spawn } from "node:child_process"
import { realpathSync } from "node:fs"
import { exit } from "node:process"
import { fetch } from "@/shared/net"
import { printInfo, printWarning } from "./display"
export enum PackageManager {
NPM = "npm",
PNPM = "pnpm",
YARN = "yarn",
BUN = "bun",
NPX = "npx",
UNKNOWN = "unknown",
}
interface InstallationInfo {
packageManager: PackageManager
updateCommand?: string
}
/**
* Check for updates and install if available
* Check if a version string is a nightly build.
*/
function isNightlyVersion(version: string): boolean {
return version.includes("-nightly.")
}
/**
* Get the npm tag to use based on the current version.
*/
function getNpmTag(currentVersion: string): string {
return isNightlyVersion(currentVersion) ? "nightly" : "latest"
}
/**
* Detect how the CLI was installed and return the appropriate update command.
* Uses the correct npm tag based on whether the current version is nightly.
*/
function getInstallationInfo(currentVersion: string): InstallationInfo {
const tag = getNpmTag(currentVersion)
try {
const scriptPath = realpathSync(process.argv[1] || "").replace(/\\/g, "/")
// npx - skip auto-update (ephemeral execution)
if (scriptPath.includes("/.npm/_npx") || scriptPath.includes("/npm/_npx")) {
return { packageManager: PackageManager.NPX }
}
// pnpm global
if (scriptPath.includes("/.pnpm/global") || scriptPath.includes("/pnpm/global")) {
return {
packageManager: PackageManager.PNPM,
updateCommand: `pnpm add -g cline@${tag}`,
}
}
// yarn global
if (scriptPath.includes("/.yarn/") || scriptPath.includes("/yarn/global")) {
return {
packageManager: PackageManager.YARN,
updateCommand: `yarn global add cline@${tag}`,
}
}
// bun global
if (scriptPath.includes("/.bun/bin")) {
return {
packageManager: PackageManager.BUN,
updateCommand: `bun add -g cline@${tag}`,
}
}
// npm global (node_modules/cline)
if (scriptPath.includes("/node_modules/cline/")) {
return {
packageManager: PackageManager.NPM,
updateCommand: `npm install -g cline@${tag}`,
}
}
} catch {
// If we can't resolve the path, assume unknown
}
return { packageManager: PackageManager.UNKNOWN }
}
/**
* Fetch the latest version from npm registry.
* Uses the appropriate tag based on whether the current version is nightly.
*/
async function getLatestVersion(currentVersion: string): Promise<string | null> {
try {
const tag = getNpmTag(currentVersion)
const response = await fetch(`https://registry.npmjs.org/cline/${tag}`)
if (!response.ok) return null
const data = (await response.json()) as { version: string }
return data.version || null
} catch {
return null
}
}
/**
* Auto-update check that runs on CLI startup.
* Checks for updates asynchronously (non-blocking), then spawns a detached
* process to install if a newer version is available.
*
* Supports npm, pnpm, yarn, and bun global installs.
* Skipped for npx, local dev, and unknown installations.
* Can be disabled with CLINE_NO_AUTO_UPDATE=1 environment variable.
*/
export function autoUpdateOnStartup(currentVersion: string): void {
// Skip in dev mode
if (process.env.IS_DEV === "true") {
return
}
// Skip if auto-update is disabled via env var
if (process.env.CLINE_NO_AUTO_UPDATE === "1") {
return
}
const { updateCommand } = getInstallationInfo(currentVersion)
if (!updateCommand) {
return
}
// Async version check - non-blocking, fire and forget
checkAndUpdate(currentVersion, updateCommand)
}
async function checkAndUpdate(currentVersion: string, updateCommand: string): Promise<void> {
try {
const latestVersion = await getLatestVersion(currentVersion)
if (!latestVersion) return
// Only update if latest is newer
if (compareVersions(currentVersion, latestVersion) >= 0) return
// Spawn detached process to run the update command
const child = spawn(updateCommand, {
shell: true,
detached: true,
stdio: "ignore",
env: process.env,
})
child.unref()
} catch {
// Silently ignore errors - auto-update is best-effort
}
}
/**
* Check for updates and install if available (manual command)
*/
export async function checkForUpdates(currentVersion: string, options?: { verbose?: boolean }) {
printInfo("Checking for updates...")
const { updateCommand, packageManager } = getInstallationInfo(currentVersion)
try {
// Fetch latest version from npm registry
const response = await fetch("https://registry.npmjs.org/cline/latest")
if (!response.ok && response.statusText !== "OK") {
printWarning(`Failed to check for updates: ${response.statusText}`)
const latestVersion = await getLatestVersion(currentVersion)
if (!latestVersion) {
printWarning("Failed to check for updates: could not fetch latest version")
exit(1)
}
const data = (await response.json()) as { version: string }
const latestVersion = data.version
if (options?.verbose) {
printInfo(`Current version: ${currentVersion}`)
printInfo(`Latest version: ${latestVersion}`)
printInfo(`Package manager: ${packageManager}`)
}
// Compare versions
@@ -38,6 +185,13 @@ export async function checkForUpdates(currentVersion: string, options?: { verbos
}
printInfo(`New version available: ${latestVersion} (current: ${currentVersion})`)
if (!updateCommand) {
printInfo("Unable to determine update command for your installation.")
printInfo("Please update manually using your package manager.")
exit(0)
}
// Ask user to confirm update
const userConfirmed = new Promise<boolean>((resolve) => {
process.stdout.write("Do you want to update now? (y/N): ")
@@ -52,31 +206,28 @@ export async function checkForUpdates(currentVersion: string, options?: { verbos
exit(0)
}
printInfo("Installing update...")
printInfo(`Installing update via ${packageManager}...`)
// Run npm install -g cline@latest
const npmProcess = spawn("npm", ["install", "-g", "cline@latest"], {
const updateProcess = spawn(updateCommand, {
stdio: "inherit",
shell: true,
// Ensures the process uses the same environment
env: process.env,
detached: false,
windowsHide: true,
})
npmProcess.on("close", (code) => {
updateProcess.on("close", (code) => {
if (code === 0) {
printInfo(`Successfully updated to version ${latestVersion}`)
exit(0)
} else {
printWarning("Update failed. Please try running: npm install -g cline@latest")
printWarning(`Update failed. Please try running: ${updateCommand}`)
exit(1)
}
})
npmProcess.on("error", (err) => {
printWarning(`Failed to run npm install: ${err.message}`)
printInfo("Please try running manually: npm install -g cline@latest")
updateProcess.on("error", (err) => {
printWarning(`Failed to run update: ${err.message}`)
printInfo(`Please try running manually: ${updateCommand}`)
exit(1)
})
} catch (error) {
@@ -86,21 +237,61 @@ export async function checkForUpdates(currentVersion: string, options?: { verbos
}
}
interface ParsedVersion {
base: number[]
isNightly: boolean
timestamp: number
}
/**
* Compare two semantic version strings
* Parse a version string into its components.
* Handles both stable versions (2.0.0) and nightly versions (2.0.0-nightly.1736365200).
*/
function parseVersion(version: string): ParsedVersion {
const nightlyMatch = version.match(/^(\d+\.\d+\.\d+)-nightly\.(\d+)$/)
if (nightlyMatch) {
return {
base: nightlyMatch[1].split(".").map(Number),
isNightly: true,
timestamp: parseInt(nightlyMatch[2], 10),
}
}
return {
base: version.split(".").map(Number),
isNightly: false,
timestamp: 0,
}
}
/**
* Compare two semantic version strings.
* Handles both stable versions and nightly versions.
* Nightly versions are compared by their timestamps.
* Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
*/
function compareVersions(v1: string, v2: string): number {
const parts1 = v1.split(".").map(Number)
const parts2 = v2.split(".").map(Number)
const p1 = parseVersion(v1)
const p2 = parseVersion(v2)
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const part1 = parts1[i] || 0
const part2 = parts2[i] || 0
// Compare base versions first
for (let i = 0; i < Math.max(p1.base.length, p2.base.length); i++) {
const part1 = p1.base[i] || 0
const part2 = p2.base[i] || 0
if (part1 > part2) return 1
if (part1 < part2) return -1
}
// Base versions are equal, check nightly status
// Nightly is considered less than stable (it's a pre-release)
if (p1.isNightly && !p2.isNightly) return -1
if (!p1.isNightly && p2.isNightly) return 1
// Both are nightly, compare timestamps
if (p1.isNightly && p2.isNightly) {
if (p1.timestamp > p2.timestamp) return 1
if (p1.timestamp < p2.timestamp) return -1
}
return 0
}
+15 -1
View File
@@ -117,7 +117,13 @@
"features/auto-compact",
"features/background-edit",
"features/checkpoints",
"features/cline-rules",
{
"group": "Cline Rules",
"pages": [
"features/cline-rules/overview",
"features/cline-rules/conditional-rules"
]
},
{
"group": "Commands & Shortcuts",
"pages": [
@@ -424,6 +430,14 @@
{
"source": "/enterprise-solutions/team-management/roles-and-permissions",
"destination": "/enterprise-solutions/team-management/managing-members"
},
{
"source": "/features/cline-rules",
"destination": "/features/cline-rules/overview"
},
{
"source": "/features/conditional-rules",
"destination": "/features/cline-rules/conditional-rules"
}
],
"search": {
-188
View File
@@ -1,188 +0,0 @@
Cline Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way to include context and preferences for your projects or globally for every conversation.
## Creating a Rule
You can create a rule by clicking the `+` button in the Rules tab. This will open a new file in your IDE which you can use to write your rule.
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-rules.png" alt="Create a Rule" />
</Frame>
Once you save the file:
- Your rule will be stored in the `.clinerules/` directory in your project (if it's a Workspace Rule)
- Or in the Global Rules directory (if it's a Global Rule):
### Global Rules Directory Location
The location of your Global Rules directory depends on your operating system:
| Operating System | Default Location | Notes |
|------------------|------------------|-------|
| **Windows** | `Documents\Cline\Rules` | Uses system Documents folder |
| **macOS** | `~/Documents/Cline/Rules` | Uses user Documents folder |
| **Linux/WSL** | `~/Documents/Cline/Rules` | May fall back to `~/Cline/Rules` on some systems |
> **Note for Linux/WSL users**: If you don't find your global rules in `~/Documents/Cline/Rules`, check `~/Cline/Rules` as the location may vary depending on your system configuration and whether the Documents directory exists.
You can also have Cline create a rule for you by using the [`/newrule` slash command](/features/slash-commands/new-rule) in the chat.
```markdown Example Cline Rule Structure [expandable]
# Project Guidelines
## Documentation Requirements
- Update relevant documentation in /docs when modifying features
- Keep README.md in sync with new capabilities
- Maintain changelog entries in CHANGELOG.md
## Architecture Decision Records
Create ADRs in /docs/adr for:
- Major dependency changes
- Architectural pattern changes
- New integration patterns
- Database schema changes
Follow template in /docs/adr/template.md
## Code Style & Patterns
- Generate API clients using OpenAPI Generator
- Use TypeScript axios template
- Place generated code in /src/generated
- Prefer composition over inheritance
- Use repository pattern for data access
- Follow error handling pattern in /src/utils/errors.ts
## Testing Standards
- Unit tests required for business logic
- Integration tests for API endpoints
- E2E tests for critical user flows
```
### Key Benefits
1. **Version Controlled**: The `.clinerules` file becomes part of your project's source code
2. **Team Consistency**: Ensures consistent behavior across all team members
3. **Project-Specific**: Rules and standards tailored to each project's needs
4. **Institutional Knowledge**: Maintains project standards and practices in code
Place the `.clinerules` file in your project's root directory:
```
your-project/
├── .clinerules
├── src/
├── docs/
└── ...
```
Cline's system prompt, on the other hand, is not user-editable ([here's where you can find it](https://github.com/cline/cline/blob/main/src/core/prompts/system.ts)). For a broader look at prompt engineering best practices, check out [this resource](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview).
### AGENTS.md Standard Support
Cline also supports the [AGENTS.md](https://agents.md/) standard as a fallback
(in addition to Cline Rules) by automatically detecting `AGENTS.md` files in
your workspace root. This allows you to use the same rules file across different AI
coding tools.
```
your-project/
├── AGENTS.md
├── src/
└── ...
```
### Tips for Writing Effective Cline Rules
- Be Clear and Concise: Use simple language and avoid ambiguity.
- Focus on Desired Outcomes: Describe the results you want, not the specific steps.
- Test and Iterate: Experiment to find what works best for your workflow.
### .clinerules/ Folder System
```
your-project/
├── .clinerules/ # Folder containing active rules
│ ├── 01-coding.md # Core coding standards
│ ├── 02-documentation.md # Documentation requirements
│ └── current-sprint.md # Rules specific to current work
├── src/
└── ...
```
Cline automatically processes **all Markdown files** inside the `.clinerules/` directory, combining them into a unified set of rules. The numeric prefixes (optional) help organize files in a logical sequence.
#### Using a Rules Bank
For projects with multiple contexts or teams, maintain a rules bank directory:
```
your-project/
├── .clinerules/ # Active rules - automatically applied
│ ├── 01-coding.md
│ └── client-a.md
├── clinerules-bank/ # Repository of available but inactive rules
│ ├── clients/ # Client-specific rule sets
│ │ ├── client-a.md
│ │ └── client-b.md
│ ├── frameworks/ # Framework-specific rules
│ │ ├── react.md
│ │ └── vue.md
│ └── project-types/ # Project type standards
│ ├── api-service.md
│ └── frontend-app.md
└── ...
```
#### Benefits of the Folder Approach
1. **Contextual Activation**: Copy only relevant rules from the bank to the active folder
2. **Easier Maintenance**: Update individual rule files without affecting others
3. **Team Flexibility**: Different team members can activate rules specific to their current task
4. **Reduced Noise**: Keep the active ruleset focused and relevant
#### Usage Examples
Switch between client projects:
```bash
# Switch to Client B project
rm .clinerules/client-a.md
cp clinerules-bank/clients/client-b.md .clinerules/
```
Adapt to different tech stacks:
```bash
# Frontend React project
cp clinerules-bank/frameworks/react.md .clinerules/
```
#### Implementation Tips
- Keep individual rule files focused on specific concerns
- Use descriptive filenames that clearly indicate the rule's purpose
- Consider git-ignoring the active `.clinerules/` folder while tracking the `clinerules-bank/`
- Create team scripts to quickly activate common rule combinations
The folder system transforms your Cline rules from a static document into a dynamic knowledge system that adapts to your team's changing contexts and requirements.
### Managing Rules with the Toggleable Popover
To make managing both single `.clinerules` files and the folder system even easier, Cline v3.13 introduces a dedicated popover UI directly accessible from the chat interface.
Located conveniently under the chat input field, this popover allows you to:
- **Instantly See Active Rules:** View which global rules (from your user settings) and workspace rules (`.clinerules` file or folder contents) are currently active.
- **Quickly Toggle Rules:** Enable or disable specific rule files within your workspace `.clinerules/` folder with a single click. This is perfect for activating context-specific rules (like `react-rules.md` or `memory-bank.md`) only when needed.
- **Easily Add/Manage Rules:** Quickly create a workspace `.clinerules` file or folder if one doesn't exist, or add new rule files to an existing folder.
This UI significantly simplifies switching contexts and managing different sets of instructions without needing to manually edit files or configurations during a conversation.
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
</Frame>
@@ -0,0 +1,267 @@
---
title: "Conditional Rules"
sidebarTitle: "Conditional Rules"
description: "Activate rules automatically based on which files you're working with"
---
Conditional rules let you scope rules to specific parts of your codebase. Rules activate only when you're working with matching files, keeping your context focused and relevant.
For an introduction to Cline Rules, see the [Overview](/features/cline-rules/overview).
- **Without conditionals**: every rule loads for every request.
- **With conditionals**, rules activate only when your current files match their defined scope.
For example, React component rules should appear when you're working with React components, not when you're editing Python or documentation.
## How It Works
Conditional rules use YAML frontmatter at the top of your rule files. When Cline processes a request, it gathers context from your current work (open files, visible tabs, mentioned paths, edited files), evaluates each rule's conditions, and activates matching rules.
<Note>
When a conditional rule activates, you'll see a notification: **"Conditional rules applied: workspace:frontend-rules.md"**
</Note>
## Writing Conditional Rules
Add YAML frontmatter to the top of any rule file in your `.clinerules/` directory:
```yaml
---
paths:
- "src/components/**"
- "src/hooks/**"
---
# React Component Guidelines
When creating or modifying React components:
- Use functional components with React hooks
- Extract reusable logic into custom React hooks
- Keep components focused on a single responsibility
```
The `---` markers delimit the frontmatter. Everything after the closing `---` is your rule content.
### The `paths` Conditional
Currently, `paths` is the supported conditional. It takes an array of glob patterns:
```yaml
---
paths:
- "src/**" # All files under src/
- "*.config.js" # Config files in root
- "packages/*/src/" # Monorepo package sources
---
```
**Glob pattern syntax:**
- `*` matches any characters except `/`
- `**` matches any characters including `/` (recursive)
- `?` matches a single character
- `[abc]` matches any character in the brackets
- `{a,b}` matches either pattern
**Examples:**
| Pattern | Matches |
|---------|---------|
| `src/**/*.ts` | All TypeScript files under `src/` |
| `*.md` | Markdown files in root only |
| `**/*.test.ts` | Test files anywhere in the project |
| `packages/{web,api}/**` | Files in web or api packages |
| `src/components/*.tsx` | TSX files directly in components (not nested) |
### Behavior Details
**Multiple patterns**: A rule activates if any pattern matches any file in your context.
```yaml
---
paths:
- "frontend/**"
- "mobile/**"
---
# Activates when working in frontend OR mobile
```
**No frontmatter**: Rules without frontmatter are always active.
**Empty paths array**: `paths: []` means the rule never activates. Use this to temporarily disable a rule.
**Invalid YAML**: If frontmatter can't be parsed, Cline fails open: the rule activates with raw content visible to help debugging.
## What Counts as "Current Context"
Cline evaluates rules based on:
1. **Your message**: File paths mentioned in your prompt (e.g., "update `src/App.tsx`")
2. **Open tabs**: Files currently open in your editor
3. **Visible files**: Files visible in your active editor panes
4. **Edited files**: Files Cline has created, modified, or deleted during the task
5. **Pending operations**: Files Cline is about to edit
Conditional rules can activate on your first message, when relevant files are open, or mid-task when Cline starts working with matching files.
<Tip>
Be explicit about file paths in your prompts. "Update `src/services/user.ts`" reliably triggers path-based rules; "update the user service" may not.
</Tip>
## Practical Examples
Copy these patterns and adapt them to your project structure.
### Frontend vs Backend Rules
Keep frontend and backend rules separate to avoid noise. Frontend rules only load when working with UI code, backend rules only load when working with API or service code.
```yaml
# .clinerules/frontend.md
---
paths:
- "src/components/**"
- "src/pages/**"
- "src/hooks/**"
---
# Frontend Guidelines
- Use Tailwind CSS for styling
- Prefer server components where possible
- Keep client components small and focused
```
```yaml
# .clinerules/backend.md
---
paths:
- "src/api/**"
- "src/services/**"
- "src/db/**"
---
# Backend Guidelines
- Use dependency injection for services
- All database queries go through repositories
- Return typed errors, not thrown exceptions
```
### Test File Rules
Enforce testing standards automatically. This rule activates only when you're writing or modifying tests, so testing guidance appears exactly when you need it.
```yaml
# .clinerules/testing.md
---
paths:
- "**/*.test.ts"
- "**/*.spec.ts"
- "**/__tests__/**"
---
# Testing Standards
- Use descriptive test names: "should [expected behavior] when [condition]"
- One assertion per test when possible
- Mock external dependencies, not internal modules
- Use factories for test data, not fixtures
```
### Documentation Rules
Apply documentation standards only when editing docs. Prevents style rules from cluttering your context when you're writing code.
```yaml
# .clinerules/docs.md
---
paths:
- "docs/**"
- "**/*.md"
- "**/*.mdx"
---
# Documentation Guidelines
- Use sentence case for headings
- Include code examples for all features
- Keep paragraphs short (3-4 sentences max)
- Link to related documentation
```
## Combining with Rule Toggles
Conditional rules work alongside the rule toggle UI. Toggle off a conditional rule to disable it entirely (it won't activate even if paths match). Toggle on to let it activate when conditions are met.
This provides two levels of control: manual toggles and automatic condition-based activation.
## Tips for Effective Conditional Rules
### Start Broad, Then Narrow
Begin with broader patterns and refine as you learn what works:
```yaml
# Start here
paths:
- "src/**"
# Then narrow down
paths:
- "src/features/auth/**"
```
### Use Descriptive Filenames
Name your rule files to indicate their scope:
```
.clinerules/
├── api-endpoints.md # Rules for API code
├── database-models.md # Rules for DB layer
├── react-components.md # Rules for React
└── universal.md # No frontmatter = always active
```
### Keep Universal Rules Separate
Put always-on rules (coding standards, project conventions) in files without frontmatter. Reserve conditional rules for context-specific guidance.
### Test Your Patterns
Not sure if a pattern matches? Create a simple test rule:
```yaml
---
paths:
- "your/pattern/here/**"
---
TEST: This rule should activate for your/pattern/here files.
```
Then work with a file in that path and check if you see the activation notification.
## Troubleshooting
**Rule not activating:**
- Check that file paths in your context match the glob pattern
- Verify the rule is toggled on in the rules panel
- Ensure YAML frontmatter has proper `---` delimiters
**Rule activating unexpectedly:**
- Review glob patterns: `**` is recursive and may match more than intended
- Check for open files that match the pattern
- File paths mentioned in your message also count as context
**Frontmatter showing in output:**
- YAML couldn't be parsed
- Check for syntax errors (unquoted special characters, improper indentation)
## Related
- [Cline Rules Overview](/features/cline-rules/overview) - Complete rules system guide
- [Skills](/features/skills) - Load instructions on demand with `/skill` command
- [Workflows](/features/slash-commands/workflows/index) - Define explicit task automation
- [@ Mentions](/features/at-mentions/overview) - Add files to context explicitly
- [Understanding Context Management](/prompting/understanding-context-management) - How Cline manages context window
+205
View File
@@ -0,0 +1,205 @@
---
title: "Cline Rules"
sidebarTitle: "Overview"
description: "Add persistent instructions and context to guide Cline's behavior"
---
Cline Rules provide system-level guidance for your projects. Rules persist across conversations, ensuring consistent behavior without repeating instructions in every chat.
## How It Works
Rules are loaded when Cline starts a task. Here's what happens:
**Loading order**: Cline checks for rules in this sequence:
1. `.clinerules/` folder (all `.md` files inside)
2. Single `.clinerules` file
3. `AGENTS.md` file
**Scope precedence**: Workspace rules override global rules when both define the same guidance.
**Multiple files**: When using a `.clinerules/` folder, all Markdown files are combined into one ruleset. Numeric prefixes (like `01-`, `02-`) control the order.
**Conditional activation**: Rules with YAML frontmatter activate only when you're working with matching files. See [Conditional Rules](/features/cline-rules/conditional-rules) for details.
## Supported Rule Files
Cline reads rules from multiple file formats in your workspace root, letting you share rules across different AI coding tools:
| File/Folder | Source | Notes |
|-------------|--------|-------|
| `.clinerules/` | Cline | Folder with `.md` files (recommended) |
| `.cursor/rules/` | Cursor | Folder with `.mdc` files |
| `.windsurf/rules` | Windsurf | Folder with multiple `md` files |
| `AGENTS.md` | Universal | Follows [agents.md](https://agents.md/) standard, searched recursively |
Cline prioritizes `.clinerules` when present. Other formats load only if no `.clinerules` exists (except `AGENTS.md`, which always searches subdirectories). All rules appear in the Rules popover where you can toggle them.
## Creating Rules
Click the `+` button in the Rules tab to create a new rule. This opens a file in your editor where you write your guidance.
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-rules.png" alt="Create a Rule" />
</Frame>
When you save the file, it's stored in:
- **Workspace rules**: `.clinerules/` in your project root
- **Global rules**: Platform-specific location (see table below)
You can also use the [`/newrule` slash command](/features/slash-commands/new-rule) to have Cline generate a rule based on your description.
### Global Rules Location
| Operating System | Default Location |
|------------------|------------------|
| **Windows** | `Documents\Cline\Rules` |
| **macOS** | `~/Documents/Cline/Rules` |
| **Linux/WSL** | `~/Documents/Cline/Rules` or `~/Cline/Rules` |
<Note>
Linux/WSL users: Check both locations if you don't find global rules in `~/Documents/Cline/Rules`.
</Note>
## Managing Rules
The Rules popover (below the chat input) shows active rules and lets you toggle them on or off.
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Rules Popover" />
</Frame>
The popover displays:
- **Global rules**: From your user-level Rules directory
- **Workspace rules**: From `.clinerules/` in your project
Toggle any rule to enable or disable it. Disabled rules won't load, even if they match conditions.
## When to Use Rules
Rules work best for persistent project context:
- **Code standards**: Formatting preferences, naming conventions, project-specific patterns
- **Documentation requirements**: Where to add docs, what format to follow
- **Architecture decisions**: Design patterns, dependency rules, module boundaries
- **Team conventions**: PR processes, branch naming, commit message format
- **Technology constraints**: Required libraries, banned APIs, version requirements
Rules are less effective for:
- One-time instructions (just say it in the chat)
- Complex multi-step workflows (use [Workflows](/features/slash-commands/workflows/index) instead)
- Dynamic decisions that depend on runtime context
## Example Rule
```markdown
# Backend API Guidelines
## Route Handlers
- Use async/await, not callbacks
- Validate request bodies with Zod schemas
- Return typed errors from `src/errors.ts`
- All routes require authentication unless in `publicRoutes` array
## Database Access
- All queries go through repository classes in `src/repositories/`
- Use transactions for multi-table updates
- Never expose raw database errors to clients
## Testing
- Unit tests for business logic in `src/services/`
- Integration tests for route handlers in `src/routes/`
- Mock external APIs, not internal modules
```
This rule provides clear, actionable guidance without explaining obvious concepts or using vague language.
## Using a Folder Structure
For projects with many rules, organize them in a `.clinerules/` folder:
```
your-project/
├── .clinerules/
│ ├── 01-coding-standards.md
│ ├── 02-documentation.md
│ └── 03-testing.md
├── src/
└── ...
```
Cline loads all Markdown files in `.clinerules/` automatically. The numeric prefixes help you control ordering, but they're optional.
### Organizing a Rules Bank
Maintain a separate folder for rules you might need but don't always want active:
```
your-project/
├── .clinerules/ # Active rules
│ ├── 01-coding.md
│ └── client-a.md
├── clinerules-bank/ # Available but inactive
│ ├── clients/
│ │ ├── client-a.md
│ │ └── client-b.md
│ └── frameworks/
│ ├── react.md
│ └── vue.md
└── ...
```
Copy files from the bank to `.clinerules/` when you need them. This keeps your active context lean while maintaining a library of reusable guidance.
Switch contexts with simple file operations:
```bash
# Switch to Client B
rm .clinerules/client-a.md
cp clinerules-bank/clients/client-b.md .clinerules/
```
<Tip>
Consider git-ignoring `.clinerules/` while tracking `clinerules-bank/` so team members can activate the rules relevant to their current work.
</Tip>
## Conditional Rules
Scope rules to specific file patterns using YAML frontmatter. This keeps React guidance out of Python code and backend rules away from frontend work.
```yaml
---
paths:
- "src/components/**"
- "src/hooks/**"
---
# React Guidelines
Use functional components with hooks. Extract reusable logic into custom hooks.
```
This rule activates only when working with files matching those patterns. Read the [Conditional Rules guide](/features/cline-rules/conditional-rules) for pattern syntax, behavior details, and more examples.
## Tips for Effective Rules
**Be specific**: "Use async/await for all database calls" beats "write good async code."
**Show patterns**: Include file paths and real examples. "Follow the error handling in `src/utils/errors.ts`" gives Cline a concrete reference.
**Focus on outcomes**: Describe what you want, not step-by-step instructions. Let Cline figure out how.
**Test and refine**: Start with core standards. Add rules when you find yourself repeating the same feedback.
**Use conditional rules**: Load guidance only when relevant. This keeps context efficient and reduces noise.
## Related
- [Conditional Rules](/features/cline-rules/conditional-rules) - Activate rules based on file patterns
- [Skills](/features/skills) - Load instructions on demand with `/skill` command
- [Workflows](/features/slash-commands/workflows/index) - Define explicit task automation
- [New Rule Slash Command](/features/slash-commands/new-rule) - Generate rules with AI assistance
- [Plan and Act Mode](/features/plan-and-act) - Use different rules for planning vs execution
-4
View File
@@ -8,10 +8,6 @@ Skills are modular instruction sets that extend Cline's capabilities for specifi
Unlike rules (which are always active), skills load on-demand. You can install dozens of skills without affecting context or performance because Cline only sees the skill name and description until it's actually needed.
<Note>
Skills is an experimental feature. Enable it in Settings → Features → Enable Skills.
</Note>
## Why Skills?
Consider how you'd onboard a new team member: you wouldn't dump every document on them at once. You'd give them a brief overview, then point them to detailed guides when they're working on specific tasks.
-4
View File
@@ -144,10 +144,6 @@ if (process.env.ERROR_SERVICE_API_KEY) {
buildEnvVars["process.env.ERROR_SERVICE_API_KEY"] = JSON.stringify(process.env.ERROR_SERVICE_API_KEY)
}
if (process.env.POSTHOG_TELEMETRY_ENABLED) {
buildEnvVars["process.env.POSTHOG_TELEMETRY_ENABLED"] = JSON.stringify(process.env.POSTHOG_TELEMETRY_ENABLED)
}
// OpenTelemetry configuration (injected at build time from GitHub secrets)
// These provide production defaults that can be overridden at runtime via environment variables
if (process.env.OTEL_TELEMETRY_ENABLED) {
-3
View File
@@ -1,3 +0,0 @@
go 1.24.7
use ./cli
+47 -81
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.53.1",
"version": "3.56.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.53.1",
"version": "3.56.2",
"license": "Apache-2.0",
"workspaces": [
"cli"
@@ -352,7 +352,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@csstools/css-calc": "^2.1.3",
"@csstools/css-color-parser": "^3.0.9",
@@ -1343,6 +1342,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",
@@ -2237,7 +2237,6 @@
],
"license": "MIT-0",
"optional": true,
"peer": true,
"engines": {
"node": ">=18"
}
@@ -2259,7 +2258,6 @@
],
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=18"
},
@@ -2285,7 +2283,6 @@
],
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@csstools/color-helpers": "^5.1.0",
"@csstools/css-calc": "^2.1.4"
@@ -2893,6 +2890,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"
@@ -3941,6 +3939,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",
@@ -4009,6 +4008,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"
}
@@ -5630,8 +5630,7 @@
"optional": true,
"os": [
"android"
],
"peer": true
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.4",
@@ -5644,8 +5643,7 @@
"optional": true,
"os": [
"android"
],
"peer": true
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.4",
@@ -5658,8 +5656,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.4",
@@ -5672,8 +5669,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.4",
@@ -5686,8 +5682,7 @@
"optional": true,
"os": [
"freebsd"
],
"peer": true
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.4",
@@ -5700,8 +5695,7 @@
"optional": true,
"os": [
"freebsd"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.4",
@@ -5714,8 +5708,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.4",
@@ -5728,8 +5721,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.4",
@@ -5742,8 +5734,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.4",
@@ -5756,8 +5747,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.4",
@@ -5770,8 +5760,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.4",
@@ -5784,8 +5773,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.4",
@@ -5798,8 +5786,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.4",
@@ -5812,8 +5799,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.4",
@@ -5826,8 +5812,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.4",
@@ -5840,8 +5825,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.4",
@@ -5854,8 +5838,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.4",
@@ -5868,8 +5851,7 @@
"optional": true,
"os": [
"openharmony"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.4",
@@ -5882,8 +5864,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.4",
@@ -5896,8 +5877,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.4",
@@ -5910,8 +5890,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.4",
@@ -5924,8 +5903,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@sap-ai-sdk/ai-api": {
"version": "2.1.0",
@@ -7527,6 +7505,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"
}
@@ -7577,6 +7556,7 @@
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -8367,6 +8347,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"
},
@@ -9274,6 +9255,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -10260,7 +10242,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@asamuzakjp/css-color": "^3.2.0",
"rrweb-cssom": "^0.8.0"
@@ -10290,7 +10271,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0"
@@ -10306,7 +10286,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"punycode": "^2.3.1"
},
@@ -10321,7 +10300,6 @@
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
}
@@ -10333,7 +10311,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
@@ -10436,8 +10413,7 @@
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
"optional": true
},
"node_modules/decompress-response": {
"version": "6.0.0",
@@ -10636,7 +10612,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",
@@ -12660,7 +12637,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"whatwg-encoding": "^3.1.1"
},
@@ -12886,6 +12862,7 @@
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.7.tgz",
"integrity": "sha512-QHyxhNF5VonF5cRmdAJD/UPucB9nRx3FozWMjQrDGfBxfAL9lpyu72/MlFPgloS1TMTGsOt7YN6dTPPA6mh0Aw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1",
"ansi-escapes": "^7.0.0",
@@ -13531,8 +13508,7 @@
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
"optional": true
},
"node_modules/is-promise": {
"version": "4.0.0",
@@ -13975,6 +13951,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"
}
@@ -14024,7 +14001,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -14066,7 +14042,6 @@
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.12"
},
@@ -14081,7 +14056,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"entities": "^6.0.0"
},
@@ -14096,7 +14070,6 @@
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"xmlchars": "^2.2.0"
},
@@ -14111,7 +14084,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"punycode": "^2.3.1"
},
@@ -14126,7 +14098,6 @@
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
}
@@ -14138,7 +14109,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
@@ -14347,6 +14317,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"
},
@@ -16082,8 +16053,7 @@
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
"optional": true
},
"node_modules/nyc": {
"version": "17.1.0",
@@ -17643,7 +17613,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=6"
}
@@ -17805,6 +17774,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -18260,8 +18230,7 @@
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
"optional": true
},
"node_modules/run-applescript": {
"version": "7.0.0",
@@ -19605,8 +19574,7 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
"optional": true
},
"node_modules/table": {
"version": "6.9.0",
@@ -19891,7 +19859,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tldts-core": "^6.1.86"
},
@@ -19905,8 +19872,7 @@
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
"optional": true
},
"node_modules/tmp": {
"version": "0.2.5",
@@ -19941,7 +19907,6 @@
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
"dependencies": {
"tldts": "^6.1.32"
},
@@ -20273,6 +20238,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -20525,6 +20491,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -20839,7 +20806,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"xml-name-validator": "^5.0.0"
},
@@ -21329,7 +21295,6 @@
"dev": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"engines": {
"node": ">=18"
}
@@ -21547,6 +21512,7 @@
"node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+2 -2
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.53.1",
"version": "3.56.2",
"icon": "assets/icons/icon.png",
"workspaces": [
"cli"
@@ -433,7 +433,7 @@
"publish:marketplace": "vsce publish --allow-package-secrets sendgrid && ovsx publish",
"publish:marketplace:prerelease": "vsce publish --allow-package-secrets sendgrid --pre-release && ovsx publish --pre-release",
"publish:marketplace:nightly": "node ./scripts/publish-nightly.mjs",
"prepare": "husky",
"prepare": "npx husky",
"changeset": "changeset",
"version-packages": "changeset version",
"docs": "cd docs && npm run dev",
+14
View File
@@ -75,6 +75,19 @@ message McpResourceTemplate {
optional string description = 4;
}
message McpPromptArgument {
string name = 1;
optional string description = 2;
optional bool required = 3;
}
message McpPrompt {
string name = 1;
optional string title = 2;
optional string description = 3;
repeated McpPromptArgument arguments = 4;
}
enum McpServerStatus {
// Protobuf enums (in proto3) must have a zero value defined, which serves as the default if the field isn't explicitly set.
// To align with the required nature of the TypeScript type and avoid an unnecessary UNSPECIFIED state, we map one of the existing statuses to this zero value.
@@ -95,6 +108,7 @@ message McpServer {
optional int32 timeout = 9;
optional bool oauth_required = 10;
optional string oauth_auth_status = 11;
repeated McpPrompt prompts = 12;
}
message McpServers {
+1
View File
@@ -485,6 +485,7 @@ message LiteLLMModelInfo {
repeated ModelTier tiers = 12;
optional double temperature = 13;
optional ApiFormat api_format = 14;
optional bool supports_reasoning = 15;
}
// Main ApiConfiguration message
+4 -3
View File
@@ -100,6 +100,8 @@ message Secrets {
optional string oca_api_key = 41;
optional string oca_refresh_token = 42;
optional string mcp_o_auth_secrets = 43;
optional string cline_api_key = 44;
optional string openai_codex_oauth_credentials = 46;
}
// NOTE: Add new fields under API_HANDLER_SETTINGS_FIELDS or USER_SETTINGS_FIELDS
@@ -259,7 +261,6 @@ message Settings {
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;
@@ -276,8 +277,8 @@ message Settings {
optional int32 open_telemetry_log_batch_timeout = 170;
optional int32 open_telemetry_log_max_queue_size = 171;
optional bool worktrees_enabled = 172;
map<string, string> open_ai_headers = 173;
optional bool auto_approve_all_toggled = 174;
map<string, string> open_ai_headers = 175;
}
message DictationSettings {
@@ -385,6 +386,7 @@ message UpdateTaskSettingsRequest {
// Message for updating settings
message UpdateSettingsRequest {
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
reserved 38; // was skills_enabled (removed - now always enabled)
Metadata metadata = 1;
optional ModelsApiConfiguration api_configuration = 2;
@@ -421,7 +423,6 @@ message UpdateSettingsRequest {
optional bool enable_parallel_tool_calling = 35;
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;
optional bool worktrees_enabled = 40;
}
+1 -1
View File
@@ -26,7 +26,7 @@ const TS_PROTO_PLUGIN = isWindows
: require.resolve("ts-proto/protoc-gen-ts_proto")
const TS_PROTO_OPTIONS = [
"env=node",
"env=both",
"esModuleInterop=true",
"outputServices=generic-definitions", // output generic ServiceDefinitions
"outputIndex=true", // output an index file for each package which exports all protos in the package.
+5 -4
View File
@@ -19,10 +19,11 @@ 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
* Convert field name to valid snake_case proto field name.
* Handles camelCase (apiKey -> api_key) and hyphens (openai-codex -> openai_codex).
*/
function camelToSnake(str) {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
function toProtoFieldName(str) {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`).replace(/-/g, "_")
}
// Fields that should use int64 instead of int32
@@ -321,7 +322,7 @@ function generateProtoMessage(messageName, fields, fieldNumbers) {
const sortedFields = [...fields].sort((a, b) => fieldNumbers[a.name] - fieldNumbers[b.name])
for (const field of sortedFields) {
const snakeName = camelToSnake(field.name)
const snakeName = toProtoFieldName(field.name)
const fieldNum = fieldNumbers[field.name]
// Map types cannot have the 'optional' modifier in proto3
const prefix = field.protoType.startsWith("map<") ? "" : "optional "
+1 -1
View File
@@ -18,7 +18,7 @@
* npm run test:e2e:build
*
* 2. From the repo root, start the interactive session:
* npm run test:playwright:interactive
* npm run test:e2e:ui
*
* 3. VS Code will launch with the Cline extension loaded and gRPC recording enabled.
*
+49 -56
View File
@@ -1,17 +1,13 @@
import * as vscode from "vscode"
import {
cleanupMcpMarketplaceCatalogFromGlobalState,
migrateCustomInstructionsToGlobalRules,
migrateTaskHistoryToFile,
migrateWelcomeViewCompleted,
migrateWorkspaceToGlobalStorage,
} from "./core/storage/state-migrations"
import { WebviewProvider } from "./core/webview"
import "./utils/path" // necessary to have access to String.prototype.toPosix
import { HostProvider } from "@/hosts/host-provider"
import { Logger } from "@/shared/services/Logger"
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
import { clearOnboardingModelsCache } from "./core/controller/models/getClineOnboardingModels"
import { HookDiscoveryCache } from "./core/hooks/HookDiscoveryCache"
import { HookProcessRegistry } from "./core/hooks/HookProcessRegistry"
import { StateManager } from "./core/storage/StateManager"
import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
import { ExtensionRegistryInfo } from "./registry"
@@ -22,6 +18,8 @@ import { featureFlagsService } from "./services/feature-flags"
import { getDistinctId, initializeDistinctId } from "./services/logging/distinctId"
import { telemetryService } from "./services/telemetry"
import { PostHogClientProvider } from "./services/telemetry/providers/posthog/PostHogClientProvider"
import { ClineTempManager } from "./services/temp"
import { cleanupTestMode } from "./services/test/TestMode"
import { ShowMessageType } from "./shared/proto/host/window"
import { syncWorker } from "./shared/services/worker/sync"
import { getBlobStoreSettingsFromEnv } from "./shared/services/worker/worker"
@@ -46,66 +44,50 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
const { ClineEndpoint } = await import("./config")
await ClineEndpoint.initialize()
try {
await StateManager.initialize(context)
} catch (error) {
Logger.error("[Controller] CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to initialize Cline's application state. Please restart the extension.",
})
}
// Initialize OpenAI Codex OAuth manager with extension context for secrets storage
openAiCodexOAuthManager.initialize(context)
// Set the distinct ID for logging and telemetry
await initializeDistinctId(context)
try {
await StateManager.initialize(context)
} catch (error) {
Logger.error("[Cline] CRITICAL: Failed to initialize StateManager:", error)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to initialize storage. Please check logs for details or try restarting the client.",
})
}
// =============== External services ===============
await ErrorService.initialize()
// Initialize OpenAI Codex OAuth manager with extension context for secrets storage
openAiCodexOAuthManager.initialize(context)
// Initialize PostHog client provider (skip in self-hosted mode)
if (!ClineEndpoint.isSelfHosted()) {
PostHogClientProvider.getInstance()
}
// Setup the external services
await ErrorService.initialize()
await featureFlagsService.poll(null)
// Migrate custom instructions to global Cline rules (one-time cleanup)
await migrateCustomInstructionsToGlobalRules(context)
// Migrate welcomeViewCompleted setting based on existing API keys (one-time cleanup)
await migrateWelcomeViewCompleted(context)
// Migrate workspace storage values back to global storage (reverting previous migration)
await migrateWorkspaceToGlobalStorage(context)
// Ensure taskHistory.json exists and migrate legacy state (runs once)
await migrateTaskHistoryToFile(context)
// Clean up MCP marketplace catalog from global state (moved to disk cache)
await cleanupMcpMarketplaceCatalogFromGlobalState(context)
// Clean up orphaned file context warnings (startup cleanup)
await FileContextTracker.cleanupOrphanedWarnings(context)
// =============== Webview services ===============
const webview = HostProvider.get().createWebviewProvider()
await showVersionUpdateAnnouncement(context)
// Check if this workspace was opened from worktree quick launch
await checkWorktreeAutoOpen(context)
// Initialize banner service (TEMPORARILY DISABLED - not fetching banners to prevent API hammering)
BannerService.initialize(webview.controller)
// DISABLED: .getActiveBanners(true)
const stateManager = StateManager.get()
// Non-blocking announcement check and display
showVersionUpdateAnnouncement(context)
// Check if this workspace was opened from worktree quick launch
await checkWorktreeAutoOpen(stateManager)
// =============== Background sync and cleanup tasks ===============
// Use remote config blobStoreConfig if available, otherwise fall back to env vars
const blobStoreSettings = stateManager.getRemoteConfigSettings()?.blobStoreConfig ?? getBlobStoreSettingsFromEnv()
syncWorker().init({ ...blobStoreSettings, userDistinctId: getDistinctId() })
// Clean up old temp files in background (non-blocking) and start periodic cleanup every 24 hours
ClineTempManager.startPeriodicCleanup()
// Clean up orphaned file context warnings (startup cleanup)
FileContextTracker.cleanupOrphanedWarnings(context)
telemetryService.captureExtensionActivated()
// Use remote config blobStoreConfig if available, otherwise fall back to env vars
const blobStoreSettings = StateManager.get().getRemoteConfigSettings()?.blobStoreConfig ?? getBlobStoreSettingsFromEnv()
syncWorker().init({ ...blobStoreSettings, userDistinctId: getDistinctId() })
return webview
}
@@ -145,11 +127,11 @@ async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
* Checks if this workspace was opened from the worktree quick launch button.
* If so, opens the Cline sidebar and clears the state.
*/
async function checkWorktreeAutoOpen(context: vscode.ExtensionContext): Promise<void> {
async function checkWorktreeAutoOpen(stateManager: StateManager): Promise<void> {
try {
// Read directly from globalState (not StateManager cache) since this may have been
// set by another window right before this one opened
const worktreeAutoOpenPath = context.globalState.get<string>("worktreeAutoOpenPath")
const worktreeAutoOpenPath = stateManager.getGlobalStateKey("worktreeAutoOpenPath")
if (!worktreeAutoOpenPath) {
return
}
@@ -165,7 +147,7 @@ async function checkWorktreeAutoOpen(context: vscode.ExtensionContext): Promise<
// Check if current workspace matches the worktree path
if (arePathsEqual(currentPath, worktreeAutoOpenPath)) {
// Clear the state first to prevent re-triggering
await context.globalState.update("worktreeAutoOpenPath", undefined)
stateManager.setGlobalState("worktreeAutoOpenPath", undefined)
// Open the Cline sidebar
await HostProvider.workspace.openClineSidebarPanel({})
}
@@ -188,4 +170,15 @@ export async function tearDown(): Promise<void> {
// Dispose all webview instances
await WebviewProvider.disposeAllInstances()
syncWorker().dispose()
clearOnboardingModelsCache()
// Kill any running hook processes to prevent zombies
await HookProcessRegistry.terminateAll()
// Clean up hook discovery cache
HookDiscoveryCache.getInstance().dispose()
// Stop periodic temp file cleanup
ClineTempManager.stopPeriodicCleanup()
// Clean up test mode
cleanupTestMode()
}
+4
View File
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { GenerateContentConfig, GoogleGenAI } from "@google/genai"
import { ModelInfo } from "@shared/api"
import OpenAI from "openai"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { sanitizeAnthropicMessages } from "../transform/anthropic-format"
@@ -44,6 +45,7 @@ export class AIhubmixHandler implements ApiHandler {
baseURL: this.options.baseURL,
defaultHeaders: {
"APP-Code": this.options.appCode,
...buildExternalBasicHeaders(),
},
})
} catch (error) {
@@ -64,6 +66,7 @@ export class AIhubmixHandler implements ApiHandler {
baseURL: `${this.options.baseURL}/v1`,
defaultHeaders: {
"APP-Code": this.options.appCode,
...buildExternalBasicHeaders(),
},
})
} catch (error) {
@@ -87,6 +90,7 @@ export class AIhubmixHandler implements ApiHandler {
// @ts-expect-error
"APP-Code": this.options.appCode,
Authorization: `Bearer ${this.options.apiKey ?? ""}`,
...buildExternalBasicHeaders(),
},
},
})
+2
View File
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { AnthropicModelId, anthropicDefaultModelId, anthropicModels, CLAUDE_SONNET_1M_SUFFIX, ModelInfo } from "@shared/api"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
@@ -33,6 +34,7 @@ export class AnthropicHandler implements ApiHandler {
this.client = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.anthropicBaseUrl || undefined,
defaultHeaders: buildExternalBasicHeaders(),
fetch, // Use configured fetch with proxy support
})
} catch (error) {
+11 -8
View File
@@ -1,4 +1,5 @@
import { AskSageModelId, askSageDefaultModelId, askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
@@ -96,10 +97,7 @@ export class AskSageHandler implements ApiHandler {
// Make request to AskSage API
const response = await fetch(`${this.apiUrl}/query`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-access-tokens": this.apiKey,
},
headers: this.headers(),
body: JSON.stringify(request),
})
@@ -157,10 +155,7 @@ export class AskSageHandler implements ApiHandler {
try {
const response = await fetch(`${this.apiUrl}/count-monthly-tokens`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-access-tokens": this.apiKey,
},
headers: this.headers(),
body: JSON.stringify({ app_name: "asksage" }),
})
@@ -194,4 +189,12 @@ export class AskSageHandler implements ApiHandler {
info: askSageModels[askSageDefaultModelId],
}
}
private headers() {
return {
"Content-Type": "application/json",
"x-access-tokens": this.apiKey,
...buildExternalBasicHeaders(),
}
}
}
+2
View File
@@ -2,6 +2,7 @@ import { BasetenModelId, basetenDefaultModelId, basetenModels, ModelInfo } from
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
@@ -34,6 +35,7 @@ export class BasetenHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://inference.baseten.co/v1",
apiKey: this.options.basetenApiKey,
defaultHeaders: buildExternalBasicHeaders(),
fetch, // Use configured fetch with proxy support
})
} catch (error) {
+3
View File
@@ -1,5 +1,6 @@
import Cerebras from "@cerebras/cerebras_cloud_sdk"
import { CerebrasModelId, cerebrasDefaultModelId, cerebrasModels, ModelInfo } from "@shared/api"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
@@ -35,11 +36,13 @@ export class CerebrasHandler implements ApiHandler {
}
try {
const externalHeaders = buildExternalBasicHeaders()
this.client = new Cerebras({
apiKey: cleanApiKey,
timeout: 30000, // 30 second timeout
fetch, // Use configured fetch with proxy support
defaultHeaders: {
...externalHeaders,
"X-Cerebras-3rd-Party-Integration": "cline",
},
})
+17 -2
View File
@@ -201,8 +201,10 @@ export class ClineHandler implements ApiHandler {
if (!didOutputUsage && chunk.usage) {
// @ts-ignore-next-line
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
const modelId = this.getModel().id
const isFreeModel = ["kwaipilot/kat-coder-pro", "moonshotai/kimi-k2.5"].includes(modelId)
if (["kwaipilot/kat-coder-pro", "moonshotai/kimi-k2.5"].includes(this.getModel().id)) {
if (isFreeModel) {
totalCost = 0
}
@@ -252,6 +254,14 @@ export class ClineHandler implements ApiHandler {
})
const generation = response.data
let totalCost = generation?.total_cost || 0
const modelId = this.getModel().id
const isFreeModel = ["kwaipilot/kat-coder-pro", "moonshotai/kimi-k2.5"].includes(modelId)
if (isFreeModel) {
totalCost = 0
}
return {
type: "usage",
cacheWriteTokens: 0,
@@ -259,7 +269,7 @@ export class ClineHandler implements ApiHandler {
// openrouter generation endpoint fails often
inputTokens: (generation?.native_tokens_prompt || 0) - (generation?.native_tokens_cached || 0),
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
totalCost,
}
} catch (error) {
// ignore if fails
@@ -280,6 +290,11 @@ export class ClineHandler implements ApiHandler {
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }
}
// If we have a model ID but no model info (e.g., CLI featured models),
// use the ID with default model info rather than falling back to a different model
if (modelId) {
return { id: modelId, info: openRouterDefaultModelInfo }
}
return { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo }
}
}
+7 -9
View File
@@ -2,12 +2,13 @@ import { DeepSeekModelId, deepSeekDefaultModelId, deepSeekModels, ModelInfo } fr
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { addReasoningContent } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
@@ -33,6 +34,7 @@ export class DeepSeekHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://api.deepseek.com/v1",
apiKey: this.options.deepSeekApiKey,
defaultHeaders: buildExternalBasicHeaders(),
fetch, // Use configured fetch with proxy support
})
} catch (error) {
@@ -81,14 +83,10 @@ export class DeepSeekHandler implements ApiHandler {
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
if (isDeepseekReasoner) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const convertedMessages = convertToOpenAiMessages(messages)
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = isDeepseekReasoner
? [{ role: "system", content: systemPrompt }, ...addReasoningContent(convertedMessages, messages)]
: [{ role: "system", content: systemPrompt }, ...convertedMessages]
const stream = await client.chat.completions.create({
model: model.id,
+24 -29
View File
@@ -1,3 +1,4 @@
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
@@ -123,10 +124,7 @@ export class DifyHandler implements ApiHandler {
try {
response = await fetch(fullUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
headers: this.jsonHeaders(),
body: JSON.stringify(requestBody),
})
} catch (error: any) {
@@ -438,9 +436,7 @@ export class DifyHandler implements ApiHandler {
const response = await fetch(`${this.baseUrl}/files/upload`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
},
headers: this.headers(),
body: formData,
})
@@ -461,10 +457,7 @@ export class DifyHandler implements ApiHandler {
async stopGeneration(taskId: string, user: string = "cline-user"): Promise<void> {
const response = await fetch(`${this.baseUrl}/chat-messages/${taskId}/stop`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
headers: this.jsonHeaders(),
body: JSON.stringify({ user }),
})
@@ -494,9 +487,7 @@ export class DifyHandler implements ApiHandler {
}
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages?${params}`, {
headers: {
Authorization: `Bearer ${this.apiKey}`,
},
headers: this.headers(),
})
if (!response.ok) {
@@ -531,9 +522,7 @@ export class DifyHandler implements ApiHandler {
}
const response = await fetch(`${this.baseUrl}/conversations?${params}`, {
headers: {
Authorization: `Bearer ${this.apiKey}`,
},
headers: this.headers(),
})
if (!response.ok) {
@@ -553,10 +542,7 @@ export class DifyHandler implements ApiHandler {
async deleteConversation(conversationId: string, user: string = "cline-user"): Promise<void> {
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
headers: this.jsonHeaders(),
body: JSON.stringify({ user }),
})
@@ -587,10 +573,7 @@ export class DifyHandler implements ApiHandler {
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}/name`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
headers: this.jsonHeaders(),
body: JSON.stringify(body),
})
@@ -623,10 +606,7 @@ export class DifyHandler implements ApiHandler {
const response = await fetch(`${this.baseUrl}/messages/${messageId}/feedbacks`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
headers: this.jsonHeaders(),
body: JSON.stringify(body),
})
@@ -659,4 +639,19 @@ export class DifyHandler implements ApiHandler {
this.conversationId = null
this.currentTaskId = null
}
private jsonHeaders() {
return {
...this.headers(),
"Content-Type": "application/json",
}
}
private headers() {
const externalHeaders = buildExternalBasicHeaders()
return {
...externalHeaders,
Authorization: `Bearer ${this.apiKey}`,
}
}
}
+4 -5
View File
@@ -1,7 +1,7 @@
import { DoubaoModelId, doubaoDefaultModelId, doubaoModels, ModelInfo } from "@shared/api"
import OpenAI from "openai"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { createOpenAIClient } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -25,10 +25,9 @@ export class DoubaoHandler implements ApiHandler {
throw new Error("Doubao API key is required")
}
try {
this.client = new OpenAI({
this.client = createOpenAIClient({
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
apiKey: this.options.doubaoApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Doubao client: ${error.message}`)
@@ -80,9 +79,9 @@ export class DoubaoHandler implements ApiHandler {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
// @ts-expect-error-next-line
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
// @ts-ignore-next-line
// @ts-expect-error-next-line
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
}
}
+4 -5
View File
@@ -1,7 +1,7 @@
import { FireworksModelId, fireworksDefaultModelId, fireworksModels, ModelInfo } from "@shared/api"
import OpenAI from "openai"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { createOpenAIClient } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -28,10 +28,9 @@ export class FireworksHandler implements ApiHandler {
throw new Error("Fireworks API key is required")
}
try {
this.client = new OpenAI({
this.client = createOpenAIClient({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Fireworks client: ${error.message}`)
@@ -88,9 +87,9 @@ export class FireworksHandler implements ApiHandler {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0, // (deepseek reports total input AND cache reads/writes, see context caching: https://api-docs.deepseek.com/guides/kv_cache) where the input tokens is the sum of the cache hits/misses, while anthropic reports them as separate tokens. This is important to know for 1) context management truncation algorithm, and 2) cost calculation (NOTE: we report both input and cache stats but for now set input price to 0 since all the cost calculation will be done using cache hits/misses)
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
// @ts-expect-error-next-line
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
// @ts-ignore-next-line
// @ts-expect-error-next-line
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
}
}
+11 -1
View File
@@ -9,6 +9,7 @@ import {
ThinkingLevel,
} from "@google/genai"
import { GeminiModelId, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { telemetryService } from "@/services/telemetry"
import { ClineStorageMessage } from "@/shared/messages/content"
import { Logger } from "@/shared/services/Logger"
@@ -63,6 +64,7 @@ export class GeminiHandler implements ApiHandler {
private ensureClient(): GoogleGenAI {
if (!this.client) {
const options = this.options as GeminiHandlerOptions
const externalHeaders = buildExternalBasicHeaders()
if (options.isVertex) {
// Initialize with Vertex AI configuration
@@ -74,6 +76,9 @@ export class GeminiHandler implements ApiHandler {
vertexai: true,
project,
location,
httpOptions: {
headers: externalHeaders,
},
})
} catch (error) {
throw new Error(`Error creating Gemini Vertex AI client: ${error.message}`)
@@ -85,7 +90,12 @@ export class GeminiHandler implements ApiHandler {
}
try {
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
this.client = new GoogleGenAI({
apiKey: options.geminiApiKey,
httpOptions: {
headers: externalHeaders,
},
})
} catch (error) {
throw new Error(`Error creating Gemini client: ${error.message}`)
}
+2
View File
@@ -2,6 +2,7 @@ import { GroqModelId, groqDefaultModelId, groqModels, ModelInfo } from "@shared/
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
@@ -103,6 +104,7 @@ export class GroqHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://api.groq.com/openai/v1",
apiKey: this.options.groqApiKey,
defaultHeaders: buildExternalBasicHeaders(),
fetch, // Use configured fetch with proxy support
})
} catch (error) {
+3 -3
View File
@@ -2,6 +2,7 @@ import { hicapModelInfoSaneDefaults, ModelInfo } from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { createOpenAIClient } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -29,7 +30,7 @@ export class HicapHandler implements ApiHandler {
throw new Error("Model ID is required")
}
try {
this.client = new OpenAI({
this.client = createOpenAIClient({
baseURL: "https://api.hicap.ai/v2/openai",
apiKey: this.options.hicapApiKey,
defaultHeaders: {
@@ -86,9 +87,8 @@ export class HicapHandler implements ApiHandler {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
// @ts-ignore-next-line
// @ts-expect-error-next-line
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
}
}
+2 -3
View File
@@ -2,7 +2,7 @@ import { HuaweiCloudMaasModelId, huaweiCloudMaasDefaultModelId, huaweiCloudMaasM
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { createOpenAIClient } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -28,10 +28,9 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
throw new Error("Huawei Cloud MaaS API key is required")
}
try {
this.client = new OpenAI({
this.client = createOpenAIClient({
baseURL: "https://api.modelarts-maas.com/v1/",
apiKey: this.options.huaweiCloudMaasApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Huawei Cloud MaaS client: ${error.message}`)
+2 -6
View File
@@ -3,7 +3,7 @@ import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { createOpenAIClient } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -32,13 +32,9 @@ export class HuggingFaceHandler implements ApiHandler {
}
try {
this.client = new OpenAI({
this.client = createOpenAIClient({
baseURL: "https://router.huggingface.co/v1",
apiKey: this.options.huggingFaceApiKey,
defaultHeaders: {
"User-Agent": "Cline/1.0",
},
fetch, // Use configured fetch with proxy support
})
} catch (error: any) {
throw new Error(`Error creating Hugging Face client: ${error.message}`)
+5 -3
View File
@@ -2,8 +2,9 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
import OpenAI from "openai"
import { StateManager } from "@/core/storage/StateManager"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { createOpenAIClient, fetch } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
import { isAnthropicModelId } from "@/utils/model-utils"
import { ApiHandler, CommonApiHandlerOptions } from ".."
@@ -64,6 +65,7 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
headers: {
accept: "application/json",
"x-litellm-api-key": apiKey,
...buildExternalBasicHeaders(),
},
})
@@ -78,6 +80,7 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
headers: {
accept: "application/json",
Authorization: `Bearer ${apiKey}`,
...buildExternalBasicHeaders(),
},
})
@@ -112,10 +115,9 @@ export class LiteLlmHandler implements ApiHandler {
throw new Error("LiteLLM API key is required")
}
try {
this.client = new OpenAI({
this.client = createOpenAIClient({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: this.options.liteLlmApiKey || "noop",
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating LiteLLM client: ${error.message}`)
+2 -3
View File
@@ -2,7 +2,7 @@ import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { createOpenAIClient } from "@/shared/net"
import type { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -26,11 +26,10 @@ export class LmStudioHandler implements ApiHandler {
private ensureClient(): OpenAI {
if (!this.client) {
try {
this.client = new OpenAI({
this.client = createOpenAIClient({
// Docs on the new v0 api endpoint: https://lmstudio.ai/docs/app/api/endpoints/rest
baseURL: new URL("api/v0", this.options.lmStudioBaseUrl || "http://localhost:1234").toString(),
apiKey: "noop",
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating LM Studio client: ${error.message}`)
+3
View File
@@ -1,6 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { MinimaxModelId, ModelInfo, minimaxDefaultModelId, minimaxModels } from "@/shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
@@ -30,12 +31,14 @@ export class MinimaxHandler implements ApiHandler {
throw new Error("MiniMax API key is required")
}
try {
const externalHeaders = buildExternalBasicHeaders()
this.client = new Anthropic({
apiKey: this.options.minimaxApiKey,
baseURL:
this.options.minimaxApiLine === "china"
? "https://api.minimaxi.com/anthropic"
: "https://api.minimax.io/anthropic",
defaultHeaders: externalHeaders,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
+17 -1
View File
@@ -3,6 +3,7 @@ import { HTTPClient } from "@mistralai/mistralai/lib/http"
import { Tool as MistralTool } from "@mistralai/mistralai/models/components/tool"
import { MistralModelId, ModelInfo, mistralDefaultModelId, mistralModels } from "@shared/api"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
@@ -29,6 +30,7 @@ export class MistralHandler implements ApiHandler {
throw new Error("Mistral API key is required")
}
try {
const externalHeaders = buildExternalBasicHeaders()
// Create HTTP client with custom fetch for proxy support
// The Mistral SDK's HTTPClient passes a Request object to the fetcher,
// but we need to extract the URL and init options to pass to our fetch wrapper
@@ -37,6 +39,11 @@ export class MistralHandler implements ApiHandler {
fetcher: async (input: RequestInfo | URL, init?: RequestInit) => {
// Handle both string/URL and Request object inputs
if (input instanceof Request) {
Object.keys(externalHeaders).forEach((key) => {
if (!input.headers.has(key)) {
input.headers.set(key, externalHeaders[key])
}
})
return fetch(input.url, {
method: input.method,
headers: input.headers,
@@ -48,7 +55,16 @@ export class MistralHandler implements ApiHandler {
...init,
} as RequestInit)
}
return fetch(input, init)
// Merge external headers with existing headers
const mergedInit = {
...init,
headers: {
...externalHeaders,
...(init?.headers || {}),
},
}
return fetch(input, mergedInit)
},
})
+13 -6
View File
@@ -2,7 +2,7 @@ import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ModelInfo, MoonshotModelId, moonshotDefaultModelId, moonshotModels } from "@/shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { createOpenAIClient } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -15,6 +15,11 @@ interface MoonshotHandlerOptions extends CommonApiHandlerOptions {
apiModelId?: string
}
// Enhanced usage interface to support Moonshot's cached token field
interface MoonshotUsage extends OpenAI.CompletionUsage {
cached_tokens?: number
}
export class MoonshotHandler implements ApiHandler {
private client: OpenAI | undefined
@@ -26,11 +31,10 @@ export class MoonshotHandler implements ApiHandler {
throw new Error("Moonshot API key is required")
}
try {
this.client = new OpenAI({
this.client = createOpenAIClient({
baseURL:
this.options.moonshotApiLine === "china" ? "https://api.moonshot.cn/v1" : "https://api.moonshot.ai/v1",
apiKey: this.options.moonshotApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Moonshot client: ${error.message}`)
@@ -52,7 +56,7 @@ export class MoonshotHandler implements ApiHandler {
const stream = await client.chat.completions.create({
model: model.id,
messages: openAiMessages,
temperature: 0,
temperature: model.info.temperature,
max_tokens: model.info.maxTokens,
stream: true,
stream_options: { include_usage: true },
@@ -82,10 +86,13 @@ export class MoonshotHandler implements ApiHandler {
}
if (chunk.usage) {
const usage = chunk.usage as MoonshotUsage
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
cacheWriteTokens: 0,
cacheReadTokens: usage.cached_tokens ?? 0,
inputTokens: (usage.prompt_tokens || 0) - (usage.cached_tokens ?? 0),
outputTokens: usage.completion_tokens || 0,
}
}
}
+2 -3
View File
@@ -2,7 +2,7 @@ import { type ModelInfo, type NebiusModelId, nebiusDefaultModelId, nebiusModels
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { createOpenAIClient } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -26,10 +26,9 @@ export class NebiusHandler implements ApiHandler {
throw new Error("Nebius API key is required")
}
try {
this.client = new OpenAI({
this.client = createOpenAIClient({
baseURL: "https://api.studio.nebius.ai/v1",
apiKey: this.options.nebiusApiKey,
fetch, // Use configured fetch with proxy support
})
} catch (error) {
throw new Error(`Error creating Nebius client: ${error.message}`)
+2 -1
View File
@@ -1,6 +1,7 @@
import { ModelInfo, NousResearchModelId, nousResearchDefaultModelId, nousResearchModels } from "@shared/api"
import OpenAI from "openai"
import { ClineStorageMessage } from "@/shared/messages/content"
import { createOpenAIClient } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -25,7 +26,7 @@ export class NousResearchHandler implements ApiHandler {
throw new Error("NousResearch API key is required")
}
try {
this.client = new OpenAI({
this.client = createOpenAIClient({
baseURL: "https://inference-api.nousResearch.com/v1",
apiKey: this.options.nousResearchApiKey,
})
+7 -3
View File
@@ -8,6 +8,7 @@ import {
OCI_HEADER_OPC_REQUEST_ID,
} from "@/services/auth/oca/utils/constants"
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { OcaModelInfo } from "@/shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
@@ -35,12 +36,14 @@ export interface OcaHandlerOptions extends CommonApiHandlerOptions {
export class OcaHandler implements ApiHandler {
protected options: OcaHandlerOptions
protected client: OpenAI | undefined
protected externalHeaders: Record<string, string> = {}
constructor(options: OcaHandlerOptions) {
this.options = options
}
protected initializeClient(options: OcaHandlerOptions) {
protected initializeClient(options: OcaHandlerOptions): OpenAI {
const externalHeaders = buildExternalBasicHeaders()
return new (class OCIOpenAI extends OpenAI {
protected override async prepareOptions(opts: any): Promise<void> {
const token = await OcaAuthService.getInstance().getAuthToken()
@@ -50,7 +53,7 @@ export class OcaHandler implements ApiHandler {
opts.headers ??= {}
// OCA Headers
const ociHeaders = await createOcaHeaders(token, options.taskId!)
opts.headers = { ...opts.headers, ...ociHeaders }
opts.headers = { ...opts.headers, ...externalHeaders, ...ociHeaders }
Logger.log(`Making request with customer opc-request-id: ${opts.headers?.["opc-request-id"]}`)
return super.prepareOptions(opts)
}
@@ -113,12 +116,13 @@ export class OcaHandler implements ApiHandler {
if (!token) {
throw new OpenAIError("Unable to handle auth, Oracle Code Assist (OCA) access token is not available")
}
const externalHeaders = buildExternalBasicHeaders()
const ociHeaders = await createOcaHeaders(token, this.options.taskId!)
Logger.log(`Making calculate cost request with customer opc-request-id: ${ociHeaders["opc-request-id"]}`)
try {
const response = await fetch(`${client.baseURL}/spend/calculate`, {
method: "POST",
headers: ociHeaders,
headers: { ...externalHeaders, ...ociHeaders },
body: JSON.stringify({
completion_response: {
model: modelId,
+36 -6
View File
@@ -1,5 +1,7 @@
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { type Config, type Message, Ollama } from "ollama"
import type { ChatCompletionTool } from "openai/resources/chat/completions"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
@@ -7,6 +9,7 @@ import type { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOllamaMessages } from "../transform/ollama-format"
import type { ApiStream } from "../transform/stream"
import { ToolCallProcessor } from "../transform/tool-call-processor"
interface OllamaHandlerOptions extends CommonApiHandlerOptions {
ollamaBaseUrl?: string
@@ -30,14 +33,17 @@ export class OllamaHandler implements ApiHandler {
private ensureClient(): Ollama {
if (!this.client) {
try {
const externalHeaders = buildExternalBasicHeaders()
const clientOptions: Partial<Config> = {
host: this.options.ollamaBaseUrl,
fetch,
headers: externalHeaders,
}
// Add API key if provided (for Ollama cloud or authenticated instances)
if (this.options.ollamaApiKey) {
clientOptions.headers = {
...clientOptions.headers,
Authorization: `Bearer ${this.options.ollamaApiKey}`,
}
}
@@ -51,7 +57,7 @@ export class OllamaHandler implements ApiHandler {
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
const client = this.ensureClient()
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
@@ -70,20 +76,44 @@ export class OllamaHandler implements ApiHandler {
options: {
num_ctx: Number(this.options.ollamaApiOptionsCtxNum),
},
tools: tools as any,
})
const toolCallProcessor = new ToolCallProcessor()
// Race the API request against the timeout
const stream = (await Promise.race([apiPromise, timeoutPromise])) as Awaited<typeof apiPromise>
try {
for await (const chunk of stream) {
if (typeof chunk.message.content === "string") {
yield {
type: "text",
text: chunk.message.content,
}
Logger.debug("[OllamaHandler] Message Chunk" + JSON.stringify(chunk))
const delta = chunk.message
if (delta?.tool_calls) {
Logger.debug(`[OllamaHandler] Tool Calls Detected: ${JSON.stringify(delta.tool_calls)}`)
yield* toolCallProcessor.processToolCallDeltas(
delta.tool_calls?.map((tc, inx) => ({
index: inx,
id: `ollama-tool-${inx}`,
function: {
name: tc.function.name,
arguments:
typeof tc.function.arguments === "string"
? tc.function.arguments
: JSON.stringify(tc.function.arguments),
},
type: "function",
})),
)
}
if (typeof delta.content === "string") {
yield {
type: "text",
text: delta.content,
}
}
// Handle token usage if available
if (chunk.eval_count !== undefined || chunk.prompt_eval_count !== undefined) {
yield {
+2
View File
@@ -4,6 +4,7 @@ import type { ChatCompletionTool } from "openai/resources/chat/completions"
import * as os from "os"
import { v7 as uuidv7 } from "uuid"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
@@ -184,6 +185,7 @@ export class OpenAiCodexHandler implements ApiHandler {
session_id: this.sessionId,
"User-Agent": `cline/${process.env.npm_package_version || "1.0.0"} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`,
...(accountId ? { "ChatGPT-Account-Id": accountId } : {}),
...buildExternalBasicHeaders(),
}
// Try using OpenAI SDK first

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