Compare commits

..
151 Commits
Author SHA1 Message Date
CandiedUniverse 9c03dfa717 Correct the version number in package.json and package-lock.json (#8966) 2026-01-29 19:32:51 -08:00
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
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
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 Floresandcline-test 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
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
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 Barreiroandgithub-actions[bot] 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
BeeandJose R. Perez 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]andArafatkatze 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
Tomás Barreiro 60436b3ddf Do not call feature_flag_called event if the value hasn't changed (#8867)
* Do not call feature_flag_called event if the value hasn't changed

* Send the feature flag called on startup
2026-01-26 10:51:33 -08:00
Saoud Rizwan df5954052d feat: disable extended thinking by default (#8863) 2026-01-26 10:21:21 -08:00
Igor Tceglevskii a66a2784c3 feat: disable PostHog services in self-hosted mode (#8842)
- Skip PostHog client initialization when running in self-hosted mode
- Return no-op config from ErrorProviderFactory and FeatureFlagsProviderFactory
- Add comprehensive tests for self-hosted mode PostHog disabling behavior

This ensures no telemetry or analytics data is sent when users run
the extension in a self-hosted environment.
2026-01-26 07:51:02 -08:00
Bee 47031cea25 feat: add debugLog RPC for host bridge logging (#8841)
* feat: add appendOutputLog RPC for host bridge logging

Add new appendOutputLog RPC endpoint to EnvService proto definition
and refactor VSCode output channel creation to use a dedicated factory
function. This enables structured logging through the host bridge
service instead of direct Logger calls.

* rename appendOutputLog to debugLog and add subscriber pattern

- Rename `appendOutputLog` RPC to `debugLog` with documentation
- Refactor Logger to use subscriber pattern instead of single output
- Update HostProvider to use env.debugLog directly for logging
- Remove redundant logger callback from setupHostProvider

* feat: add multi-subscriber support for Logger output

- Rename Logger.setOutput to Logger.subscribe to better reflect behavior
- Subscribe both output channel and debug logger to receive log messages
- Enable logging to multiple destinations simultaneously

* update mock
2026-01-23 18:18:24 -08:00
Robin Newhouse e29740479e fix: skip diff error UI handling during streaming to prevent flickering (#8788)
* fix: skip diff error UI handling during streaming to prevent flickering

During streaming, handlePartialBlock is called repeatedly, and if the diff
application fails (e.g., search string not found), all the error handling code
was running on every chunk. This caused:
- consecutiveMistakeCount to rapidly increment
- diff_error messages to be added/removed repeatedly
- revertChanges/reset to be called repeatedly
- rapid flickering of the diff viewer

Now we return early from the catch block when block.partial is true, skipping
all error UI handling. The error is only processed once on the final block.

* chore: add changeset for diff error suppression

* test: add unit tests for partial block streaming behavior

Adds tests verifying that error handling is skipped during streaming
(block.partial=true) to prevent counter rapid increment and UI flickering.

* chore: remove unused errorPushedForCallIds tracking

This mechanism was replaced by the simpler block.partial check for
skipping error handling during streaming. Remove the dead code.
2026-01-23 16:55:32 -08:00
Ara 74f607ff8e chore(release): bump version to 3.53.1 (#8839)
- Fix bug in responses API
- Update changeset package name from "cline" to "claude-dev"
- Update version in package.json and package-lock.json
2026-01-23 15:46:59 -08:00
Robin Newhouse 0edf6d777b fix: prevent infinite retry loops when replace_in_file fails repeatedly (#8787)
* fix: prevent infinite retry loops when replace_in_file fails repeatedly

The consecutiveMistakeCount was being reset to 0 at the START of each
WriteToFileToolHandler execution, before the tooManyMistakes check could
see accumulated failures. This allowed the model to retry failing
replace_in_file operations indefinitely, causing context explosion.

Changes:
- Move counter reset from before operation to after successful saveChanges()
- Add consecutiveMistakeCount++ in the diff error catch block
- Fix typo: "his thought process" → "Cline's thought process"

* chore: add changeset for retry loop prevention

* test: add unit tests for consecutiveMistakeCount behavior

Verify the fix for infinite retry loops by testing that:
- Counter is NOT reset at the start of operations
- Counter IS reset only after successful saveChanges()
- Counter IS incremented on diff errors
- Repeated failures accumulate so tooManyMistakes can trigger
2026-01-23 15:37:01 -08:00
Robin Newhouse de630c64d4 fix: throttle diff view updates during streaming (#8785)
* fix: throttle diff view updates during streaming

Skip redundant rapid updates to reduce performance issues in large
streams (e.g., notebooks) and reset throttle state on cleanup.

* chore: add changeset for diff throttling fix

* test: add unit tests for diff view update throttling

Add comprehensive tests for the throttling behavior introduced in the
streaming diff updates fix. Tests cover empty content, unchanged content,
time-based throttling, final update bypass, and state reset.
2026-01-23 15:36:47 -08:00
BeeandCopilot 3ff63562c8 chore: migrate host logging to shared Logger service (#8820)
* chore: migrate host logging to shared Logger service

- Replace HostProvider.logToChannel usage with Logger.log/error
  in controller, webview, and checkpoint migration code
- Remove redundant, low-value log statements from Cline API
  methods to reduce noise
- Centralize logging through shared Logger service for more
  consistent, structured logging and easier maintenance
- Remove redundant , low-value log statements from StateManager where
  we logged error that would be throw and get logged again

* Update src/integrations/checkpoints/CheckpointMigration.ts

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

* fix

* update tests

* update tests

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-23 15:27:11 -08:00
Saoud Rizwan 4d6f908fbd fix: add null check when filtering tools by type in Responses API providers (#8837)
Users reported seeing this error with the OpenAI Codex provider:
{"message":"Cannot read properties of undefined (reading 'type')","modelId":"gpt-5.2-codex"}

The issue occurs when filtering tools before sending to the Responses API.
The filter accessed .type without checking if the tool element was defined:

  tools.filter((tool) => tool.type === "function")

If the tools array contains any undefined elements, this throws. Fixed by
adding optional chaining:

  tools.filter((tool) => tool?.type === "function")

Applied the same fix to all three providers using the Responses API:
- openai-codex.ts (ChatGPT Plus/Pro subscriptions)
- openai-native.ts (OpenAI API with Responses format)
- oca.ts (OpenAI-compatible API with Responses format)
2026-01-23 14:48:51 -08:00
Igor Tceglevskii 6521fdcc94 disable telemetry for self-hosted environments (#8790) 2026-01-23 14:16:50 -08:00
Igor Tceglevskii 1393eace27 Endpoint configuration file (#8645) 2026-01-23 13:40:31 -08:00
eebb99c1e3 Changeset version bump (#8800)
* changeset version bump

* Updating CHANGELOG.md format

* update changelog and banner 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: Max Paulus 🥪 <max@cline.bot>
2026-01-23 13:12:29 -08:00
Ara 9ed44f7a83 feat(cerebras): use model-specific temperature configuration (#8833)
- Extract model retrieval to avoid multiple function calls
- Use temperature from model.info with fallback to 0 instead of hardcoded value
- Allows temperature to be configured per model rather than using a fixed value

This change enables more flexible temperature configuration for different Cerebras models while maintaining backward compatibility with a default value of 0.
2026-01-23 12:50:57 -08:00
Ara 6a95cc5f19 feat: add default temperature to Cerebras model configuration (#8832)
Set default temperature value of 0.9 for Cerebras model in the model
configuration. This establishes a consistent default sampling temperature
for the model's response generation behavior.
2026-01-23 12:42:26 -08:00
er-ri 1d0637f39c fix: add support for haiku 4.5 in JP_SUPPORTED_CRIS_MODELS and enable global endpoint support (#8298) 2026-01-23 11:46:02 -08:00
AJ Juaire f2c16bae7a Make the default bedrock model Sonnet 4.5 (#8830) 2026-01-23 11:45:49 -08:00
Ara 204f15ce1c Remove free period on grok (#8831) 2026-01-23 11:27:42 -08:00
Robin Newhouse 8118e11596 fix(extract-text): strip notebook outputs to reduce context size (#8784)
* fix(extract-text): strip notebook outputs to reduce context size

* chore: add changeset for notebook outputs fix
2026-01-22 17:50:31 -08:00
Bee f7b593df35 chore: remove noisy log when checking file outside workspace (#8814)
* chore: remove noisy log when checking file outside workspace

Removes a `Logger.error` call in `ifFileExistsRelativePath` that triggered whenever a file path was checked without an active workspace. This log was creating excessive noise during long conversations where many files were mentioned but no workspace was open.

* update test
2026-01-22 17:01:22 -08:00
Saoud Rizwan 2e0358a7a1 fix: disable browser tool by default (#8815)
The browser tool conflicts with the new websearch tool. Disabling it by
default provides a better out-of-box experience.
2026-01-22 16:58:29 -08:00
Bee 0fbc10f807 chore: remove unhelpful and noisy log statements - part 1 (#8813)
* chore: remove unhelpful and noisy log statements - part 1

Removes excessive debug and info logs across several services to reduce console noise, specifically:
- Deletes `[DEBUG]` logs for request registration, subscription setup/cleanup, and event dispatching in the gRPC controller and UI handlers.
- Removes verbose file cleanup logs in `ClineTempManager` and process termination logs in `AudioRecordingService`.
- Simplifies the success log in `refreshOpenRouterModels` by removing the large JSON payload dump.
- Upgrades the log level from `debug` to `error` for request cleanup failures in `GrpcRequestRegistry` to ensure exceptions are properly highlighted.

* removes subscription logs
2026-01-22 16:44:05 -08:00
Bee c093ca1760 refactor: replace console with Logger service (#8741)
* chore: add grit rule to enforce Logger service over console calls

Add a new Grit linting rule that detects direct console method usage
(log, debug, error, warn, info) and prompts developers to use the
Logger service instead for consistent logging practices.

The rule is configured in biome.jsonc to apply to most source files
while excluding test files, webview-ui, evals, standalone, e2e tests,
and scripts where direct console usage may be acceptable.

* support variadic args

* wip: migrate console to Logger

* migrate rest of console logger

* Switch to Logger

* Migrations

* shared

* use shared

* revert format change

* Update tests to stub Logger instead of console

* verbose in dev mode
2026-01-22 13:16:37 -08:00
Seb Duerr dc85299a73 Remove deprecated zai-glm-4.6 model from Cerebras provider (#8717) 2026-01-22 12:58:07 -08:00
ClineXDiego 4533ed3ea8 fix: support drag & drop files from SSH remote workspaces (#8804)
Add vscode-remote: scheme to the valid URI filter for drag & drop operations.
This allows files from SSH Remote workspaces to be dropped into the chat.

Fixes #7606
2026-01-22 12:37:11 -08:00
MaxandMax Paulus 🥪 66d7664d1b improve cline command permission parsing logic (#8544)
- cline command permission flag can now parse subshells correctly and
validate that subshells don't contain disallowed commands.

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-01-22 11:11:53 -08:00
dependabot[bot]andClineXDiego 50aeb7098f chore(deps): bump undici (#8783)
Bumps [undici](https://github.com/nodejs/undici) to 6.23.0 and updates ancestor dependency . These dependencies need to be updated together.


Updates `undici` from 6.22.0 to 6.23.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.22.0...v6.23.0)

Updates `undici` from 7.16.0 to 7.19.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.22.0...v6.23.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.23.0
  dependency-type: indirect
- dependency-name: undici
  dependency-version: 7.19.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ClineXDiego <diego@cline.bot>
2026-01-22 10:50:49 -08:00
github-actions[bot]andArafatkatze c2e6eafb45 v3.52.0 Release Notes (#8633)
- Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
- Grok models are now moving out of free tier and into paid plans.
- Introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness.

- Bugs in DiffViewProvider for file editing
- Ollama's recommended models to use correct identifiers

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-22 10:48:32 -08:00
0220628c39 Adds Open AI Codex docs (#8791)
* feat(docs): add OpenAI Codex provider setup instructions and update model selection guidance

* fix(docs): improve clarity and formatting in OpenAI Codex documentation

* Update docs/provider-config/openai-codex.mdx

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

* Update docs/provider-config/openai-codex.mdx

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Tony Loehr <turingxo@gmail.com>
2026-01-22 09:30:12 -08:00
CandiedUniverse 5052220195 feat(hooks): Make hooks always enabled and remove its feature setting. [CLINE-1179] (#8777)
* feat(hooks): Standardize on calling getHooksEnabledSafe().

* feat(hooks): Hard-code getHookEnabledSafe() to return true unless on Windows.

* feat(hooks): Remove hooks setting from the CLI.

* feat(hooks): Remove hooks toggle from the Feature Settings UI.

* feat(hooks): Remove hooksEnabled toggles from settings/task APIs.

* feat(hooks): Stop using hooksEnabled setting.

* feat(hooks): npm run changeset

* feat(hooks): Simplify getHooksEnabledSafe() function signature.

* feat(hooks): Remove hooks setting migration.

feat(hooks): Remove hooksEnabled from updateSettingsCli() conversion.

feat(hooks): Use 'reserved' for removed fields in UpdateSettingsRequest protobuf.
2026-01-22 09:26:16 -08:00
CandiedUniverse abf3081e56 Rules: Wire up conditional rules functionality [ENG-1470] (#8669)
* feat(rules): Write technical design / implementation plan doc.

* update frontmatter plan

* feat(rules): Initial implementation based on plan doc.

* feat(rules): Add tool-call path harvesting for path-scoped Cline Rules.

* chore(rules): exclude internal paths-frontmatter plan doc from PR

* fix(rules): use latest user message for paths frontmatter context

* feat(rules): Implement conditional_rules_applied say type.

* feat(rules): changes as per Cline's code review feedback

* feat(rules): npm run changeset

* feat(rules): Changes as per ellipsis-dev feedback.

* feat(rules): Changes as per code review feedback (i.e. don't bloat the task context).

* feat(rules): Fix failing unit tests.
2026-01-22 09:25:59 -08:00
Michael Gutin 125cb78a31 Switch background element now has a 3:1 contrast ration with thumb and rule row background in vs code light and dark themes (#8747) 2026-01-22 12:16:51 -05:00
CandiedUniverse 17539228ea feat(cli): Add note about ctrl+c to exit the CLI. [CLINE-1162] (#8796)
* feat(cli): Add note about ctrl+c to exit the CLI.

* feat(cli): npm run changeset
2026-01-22 09:01:57 -08:00
Ara bdb7cc36fa feat(DiffEditRow): add button to open file in editor (#8564)
* refactor(diff): return result object with line tracking metadata

Change constructNewFileContent to return an object containing newContent
and line number information instead of just the string content. Add
charIndexToLineNumber helper function to support tracking where changes
occur in the file.

Update all callers and tests to access the newContent property from
the result object.

* minor fix

* fix: hide line numbers when not available from backend

* chore: add changeset

* feat: add startLineNumbers support to ApplyPatchHandler

* fix: split V4A @@ chunks into separate Patch objects for proper line numbers

* fix(DiffEditRow): preserve +/- prefix in diff line display for backwards compatibility

* fix line numbers
2026-01-21 21:38:45 -08:00
Tony Loehr d3e1065707 added jupyter docs (#8742)
* added jupyter docs

* fix jupyter docs and add gifs

* fix jupyter docs 2

* fix jupyter gif placement
2026-01-21 16:15:26 -08:00
Tomás Barreiro 6303951f20 Use getAllFlagsAndPayloads when fetching PostHog feature flags (#8774)
* Use

* Fix typo

* Remove old code
2026-01-21 13:12:34 -08:00
AraandCopilot fc184e07a2 feat: update free onboarding models with Kat Coder Pro and Devstral (#8773)
* feat: update free onboarding models with Kat Coder Pro and Devstral

- Replace xAI Grok Code Fast 1 with KwaiKAT Kat Coder Pro as primary free model
- Add Mistral Devstral 2512 as additional free model option
- Update model specifications (context window, image/cache support)

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-21 12:22:10 -08:00
Ara e88f4ea412 fix: update package name from cline to claude-dev in changesets (#8772)
Rename package identifier in changeset frontmatter to reflect
the correct package name for version tracking.
2026-01-21 11:35:54 -08:00
Saoud Rizwan 133ade3f1a feat(prompt): add stderr redirect guidance for command execution (#8765)
* feat(prompt): add stderr redirect guidance for command execution

* test: update system prompt snapshots
2026-01-21 11:09:18 -08:00
857411c035 fix initial prompt bugs (#8428)
* fix initial prompt bugs

- going straight to act wasn't working in interactive mode.
- slash command autocomplete wasn't working in the initial prompt
- refactored some naming to be more clear

* Apply suggestion from @ellipsis-dev[bot]

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

* Apply suggestion from @ellipsis-dev[bot]

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

* Apply suggestion from @ellipsis-dev[bot]

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

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: CandiedUniverse <132302818+candieduniverse@users.noreply.github.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2026-01-21 09:40:08 -08:00
Robin NewhouseandMax c59f068584 fix: improve Jupyter notebook diff view and LLM context handling (#8759)
* fix: restore switchToSpecializedEditor for Jupyter notebook diff views

This restores the notebook diff view functionality that was accidentally
removed during rebase. The method was incorrectly identified as dead code,
but it was being called in update() when isFinal is true.

Restored functionality:
- Abstract method definition in DiffViewProvider
- Call to switchToSpecializedEditor() in update() after final content
- Full implementation in VscodeDiffViewProvider for notebook diff views
  - Temporary file management for modified content
  - File system watcher for synchronization
  - Proper cleanup in resetDiffView()
- No-op implementations in ExternalDiffViewProvider and FileEditProvider
- Test stub in DiffViewProvider.test.ts

* fix: open notebooks in Jupyter editor after save, strip outputs for LLM

- Override showFile in VscodeDiffViewProvider to open .ipynb files
  with the Jupyter notebook editor instead of leaving stale diff view
- Remove notebook check that was skipping showFile in base class
- Add getOriginalContentForLLM() to return sanitized notebook content
- Strip notebook outputs from finalContent to reduce LLM context size

fix: sanitize notebook content in write responses to prevent context explosion

Previously, after editing a notebook, the full file content (including all
base64-encoded images and HTML table outputs) was sent back to the LLM in
the <final_file_content> response. This caused context to explode to ~200K
tokens for simple edits on notebooks with rendered outputs.

Changes:
- Add stripAllOutputs option to sanitizeNotebookForLLM()
- Apply sanitization in DiffViewProvider.saveChanges() for finalContent
- Add getOriginalContentForLLM() for diff error responses
- Strip all outputs (not just images) in write paths since outputs
  aren't needed for editing - they regenerate when cells run

Results: 95% reduction in context usage for notebook write responses
(196KB → 9KB in testing).

* chore: add changeset for Jupyter notebook diff view fix

* fix: show error message when Jupyter extension is missing for notebook diffs

* refactor: move os require to top-level import

---------

Co-authored-by: Max <maxpaulus43@gmail.com>
2026-01-21 08:07:03 -08:00
Bee f7561c31fa fix: prevent race condition in telemetry service initialization (#8764) 2026-01-21 07:23:21 -08:00
Jose Castelli f0a97dafc8 fix: fixing testing framework and removing old integration tests [PF-413] (#8727)
fix: fixing testing framework and removing old integration tests [PF-413] #8727
2026-01-21 14:38:03 +01:00
Saoud Rizwan d422f689a6 docs(storage): document StateManager multi-instance behavior
Adds a detailed comment explaining that each VS Code window has its own
StateManager cache, which is why settings like plan/act mode don't sync
between running instances. The cache is populated from disk only during
initialize() and never re-read, providing natural isolation.
2026-01-20 20:50:55 -08:00
Robin Newhouse 00efcc1c2b fix: prevent duplicate diff errors when parallel tool calling is enabled (#8763)
* fix: prevent duplicate diff errors when parallel tool calling is enabled

When a `replace_in_file` diff fails during streaming with parallel tool
calling enabled (GPT-5/Codex models), the same error message was being
added to userMessageContent on each streaming chunk, causing hundreds of
duplicates and context window overflow.

Root cause: The duplicate prevention added in 09276ebf4 used the
`didAlreadyUseTool` flag, but this flag is intentionally not set when
parallel tool calling is enabled (per 00e9d6f52). This was because
`didAlreadyUseTool` is designed to block subsequent tools, which is the
opposite of what parallel tool calling needs.

The fix adds per-call_id tracking via `diffErrorPushedForCallIds` Set:
- Track which specific tool calls have already had their error pushed
- Works for both parallel and non-parallel tool calling
- Different parallel tool calls can each report their own errors
- Same call_id only pushes error once, regardless of streaming chunks

This is compatible with the parallel tool calling design because GPT-5
models (which auto-enable parallel tool calling) use native tool calling
and always have a call_id. The mechanism is separate from didAlreadyUseTool
which controls flow (blocking tools) vs this which prevents duplicates.

Related commits:
- 09276ebf4: fix: prevent duplicate error messages during streamed edit
  tool failures (only worked when parallel tool calling disabled)
- 00e9d6f52: feat: add experimental parallel tool calling support
  (deliberately excluded didAlreadyUseTool from parallel mode)

* test: add unit tests for diffErrorPushedForCallIds duplicate prevention

Tests cover:
- Basic Set behavior (initialization, tracking, clearing)
- Duplicate prevention logic for parallel tool calling
- Edge cases (empty/undefined call_id, rapid streaming chunks)
- Reset behavior between API requests

* Add changeset

* refactor: rename diffErrorPushedForCallIds to errorPushedForCallIds

Generalizes the tracking mechanism per reviewer feedback from @abeatrix.

The more generic name allows the same pattern to be reused for other
tool handlers that may need duplicate error prevention in the future,
not just diff-related errors.

No functional changes - just renaming.
2026-01-20 20:48:29 -08:00
Robin Newhouse 59251e9de6 fix: ensure document finalization in approval flow (#8757)
When file operations use the approval flow (isFinal=false), the document
content was not being properly finalized before user approval. This caused
content duplication when shortening files - old content at the end was
preserved instead of being replaced.

Root cause: FileProviderOperations passed isFinal=false to
DiffViewProvider.update(), which:
1. Popped the last line (treated as "incomplete" for streaming)
2. Limited the replacement range to currentLine + 1
3. Skipped truncation of trailing content

Fix: Always pass isFinal=true to update() since the content IS complete.
The isFinal parameter in FileProviderOperations now only controls whether
to save after the update, not the update behavior itself.

This follows the philosophy of dd35448a9 by fixing at the source rather
than adding cleanup logic.
2026-01-20 15:52:12 -08:00
Saoud Rizwan 2df952f571 fix: OpenAI Codex provider improvements based on OpenAI feedback (#8754)
* style: improve OAuth success page design

* fix: hide thinking budget slider for OpenAI Codex provider

OpenAI Codex models use discrete reasoning effort levels (low/medium/high)
controlled via the global OpenAI Reasoning Effort setting, not token-based
thinking budgets like Anthropic models.

* fix: hide thinking toggle with display:none instead of disabled state

* fix: hide cost display for OpenAI Codex provider

Subscription-based provider has no per-token costs, so showing $0.00 is misleading.
2026-01-20 15:31:39 -08:00
Bee 6e7dc55781 fix: disable delete button for favorited history item (#8753)
* fix: disable delete button for favorited history item

- Add useMemo hook to memoize favorite state calculation, improving performance by avoiding repeated computations of `pendingFavoriteToggles[item.id] ?? item.isFavorited`
- Disable delete button for favorited items but keep delete button for standardized UI display
- Replace multiple inline favorite state checks with centralized `isFavoritedItem` variable for better code maintainability
- Simplify favorite toggle logic by using memoized value

This change ensures favorited items cannot be deleted and reduces unnecessary re-renders when favorite state is accessed.

* isFavoritedItem
2026-01-20 13:28:42 -08:00
Tomás Barreiro ec879fc549 Do not update the providers if the currently configured one is valid (#8751) 2026-01-20 21:49:39 +01:00
Tomás Barreiro 75b050de5c Init Sync Worker when applying remote config (#8749) 2026-01-20 19:52:16 +01:00
Thanh Nguyen 0c4acd9457 docs: fix outdated Ollama model names in documentation (#8551)
* docs: fix outdated Ollama model names in documentation

Fixes #7918

- Updated qwen3-coder-30b to qwen2.5-coder:32b (correct identifier)
- Replaced devstral-small with codellama:34b-code (existing model)
- Changed ollama run to ollama pull for initial download

* chore: add changeset for Ollama model names fix
2026-01-20 10:28:30 -08:00
dependabot[bot] 0dc3d7084d chore(deps): bump qs and express (#8360)
Bumps [qs](https://github.com/ljharb/qs) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `qs` from 6.13.0 to 6.14.1
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.13.0...v6.14.1)

Updates `express` from 5.0.1 to 5.2.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/v5.0.1...v5.2.1)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.1
  dependency-type: indirect
- dependency-name: express
  dependency-version: 5.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 09:49:02 -08:00
Tomás Barreiro 3cfafe6583 Clean old Feature Flags (#8748) 2026-01-20 09:47:07 -08:00
d8aefbaabd feat: Jupyter Notebook Enhancements (#8053)
* Jupyter Notebook Enhancements

* fix: implement dynamic notebook instructions for replace_in_file

Leverages the new dynamic prompt infrastructure to conditionally inject Jupyter Notebook-specific instructions into the `replace_in_file` tool. This ensures that the model receives guidance on handling JSON structure in `.ipynb` files only when the `enhancedNotebookInteractionEnabled` setting is active, keeping the default prompt clean for other users.

- Added `enhancedNotebookInteractionEnabled` to global settings and system prompt context
- Updated `replace_in_file` tool to use a dynamic instruction function that appends notebook rules based on context
- Wired up state management to pass the setting value to the prompt builder

* refactor: unify notebook output sanitization across two code paths

Previously, context menu commands (Add to Cline, Explain, etc.) wiped ALL
notebook outputs, while file mentions preserved text and only truncated images.
Additionally, when outputs weren't cleared, massive base64-encoded image data
was sent directly to the LLM, flooding context with garbage.

Changes:
- Create shared notebook-utils.ts with sanitization logic
- Update extract-text.ts to use shared utility
- Update commandUtils.ts to sanitize instead of clearing outputs

Both paths now truncate base64 image data with "[IMAGE DATA TRUNCATED]"
while preserving useful text outputs like print statements and errors.

* feat(improve): unify prompt sending behavior for improveWithCline

Refactor improveWithCline to build a single prompt and handle sending uniformly: for notebooks, populate existing task if available and send immediately; otherwise, create new task. This unifies behavior across selected text and notebook contexts, removing dependency on sendAddToInputEvent and simplifying logic. Minor formatting tweaks in extension.ts for notebook context string.

* refactor: remove notebook_cell_json from proto, move notebook context to dedicated commands

The contributor's original implementation added notebook_cell_json to the
CommandContext proto, which was then populated in getContextForCommand() for
any notebook file when enhancedNotebookInteractionEnabled was set.

This couples notebook-specific functionality to the general command proto,
which feels heavy for a niche feature. Protos should stay clean and general.

Changes:
- Remove notebook_cell_json field from CommandContext proto
- Export findMatchingNotebookCell() from commandUtils.ts
- Update Jupyter commands (JupyterGenerateCell, JupyterExplainCell,
  JupyterImproveCell) in extension.ts to fetch cell JSON directly and
  bundle it into the notebookContext parameter
- Update command files to use only notebookContext parameter
- Remove notebook-specific handling from getContextForCommand()

Result: Notebook context only flows through dedicated Jupyter commands.
Regular commands (Add to Cline, Fix, etc.) work the same for all file types.
The proto stays clean and general-purpose.

Note: This removes the behavior where regular commands would get notebook
context when used on .ipynb files with enhancedNotebookInteractionEnabled.
That feature is now exclusive to the dedicated Jupyter menu commands.

* feat: improve notebook handling for empty notebooks

- Add semicolon to import statement for consistency
- Prevent errors by checking cell count before accessing notebook cells
- Add fallback in getContextForCommand for active notebook editor when no text editor is available
- Ensures robustness when dealing with empty or cell-less notebooks in the VSCode extension

* refactor: extract common notebook context logic for Jupyter commands

Extracted duplicated code into a helper function `getNotebookCommandContext` to handle active notebook checks, context retrieval, and cell JSON fetching. This reduces duplication in `JupyterGenerateCell` and `JupyterExplainCell` commands, improving code maintainability and readability. Minor import semicolon fix for consistency.

* fix: block notebook edits when enhanced interaction disabled

Prevent crashes when enhancedNotebookInteractionEnabled is false by blocking .ipynb file edits in WriteToFileToolHandler. Added validation to return an error message instructing the user to enable the setting, and set didRejectTool to stop the operation. Reading notebooks remains unaffected.

* fix(mentions): reorder parameters in parseMentions signature

Reordered the parameters in the parseMentions function to move the default parameter to the end of the argument list. This change ensures consistency in the function signature and correctly aligns arguments at the call site in the Task class.

This update was done to fix failing tests.

* Created proper diff views for vscode nd removed unnecessary logs

* feat: make replace_in_file prompt dynamic based on open files

Add editorTabs to SystemPromptContext to expose open/visible files.

Populate editorTabs in Task using HostProvider.

Conditionally include notebook-specific instructions in replace_in_file tool only when .ipynb files are open or visible.

Refactor replace_in_file prompt construction for better readability.

* feat: enable enhanced notebook interaction by default

Remove enhancedNotebookInteractionEnabled feature flag and enable notebook support globally.

Update tool handlers to process notebook cells automatically.

Update file extraction logic to support .ipynb files natively.

Clean up settings UI and state management.

* fix: restore accidentally removed promptContext fields

Commit ec68e7c2a accidentally removed enableParallelToolCalling and
terminalExecutionMode from promptContext when refactoring to add
editorTabs. These fields are still in SystemPromptContext interface
and actively used by system prompt templates.

* fix: complete feature flag removal from package.json

Commit a1dc73f93 removed the enhancedNotebookInteractionEnabled flag
from runtime code but forgot to update package.json. The Jupyter menu
items were hidden because the when conditions checked a setting that
defaulted to false.

- Remove config check from notebook menu item when conditions
- Remove unused setting definition

* fix: change changeset from minor to patch

* fix: code quality improvements in VscodeDiffViewProvider

- Use proper ES6 import for os module instead of require()
- Remove dead commented-out code (closeCurrentTextDiffEditor)
- Improve comment explaining the render delay

* fix: watch specific temp file instead of entire directory

* test: update snapshots for replace_in_file whitespace change

The PR's refactoring of replace_in_file.ts changed indentation in
the tool description from tabs to spaces. Updating snapshots to
match.

* fix: remove merge artifact marginTop from checkpoints div

* test: update DiffViewProvider test stub for new abstract method

* fix: remove dead notebook diff view code (switchToSpecializedEditor)

The switchToSpecializedEditor() method was declared as abstract and
implemented in all DiffViewProvider subclasses, but was never called
from anywhere. This meant ~180 lines of notebook diff view code
(temp file management, file watchers, cleanup) would never execute.

Removing this dead code. The notebook diff view feature will need a
follow-up PR to properly integrate it by calling the method from the
update() flow when isFinal is true.

---------

Co-authored-by: Robin Newhouse <robin@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-01-19 20:18:39 -08:00
Bee 1ff60522f1 fix: display history task text without highlight (#8740) 2026-01-19 18:09:00 -08:00
Saoud Rizwan 42321e5b95 fix: reduce JetBrains workflow comment spam on PRs (#8738)
Change trigger from every push to:
- PR open/reopen only (removed synchronize)
- Manual /test-jetbrains comment command

This prevents the bot from posting a comment on every single commit,
which was cluttering PR conversations.
2026-01-19 16:57:20 -08:00
Saoud Rizwan b2634d2276 feat: add OpenAI Codex provider for ChatGPT Plus/Pro subscriptions (#8664)
* feat: add OpenAI Codex provider for ChatGPT Plus/Pro subscriptions

Add a new provider that allows users with ChatGPT Plus or Pro subscriptions
to use GPT-5 models directly through Cline without needing an API key.

Key features:
- OAuth authentication via OpenAI (PKCE flow)
- Routes requests to chatgpt.com/backend-api/codex/responses
- Subscription-based pricing (no per-token costs)
- Models: gpt-5.2-codex, gpt-5.1-codex-max, gpt-5.1-codex-mini, gpt-5.2

New files:
- src/integrations/openai-codex/oauth.ts: OAuth manager with PKCE, token storage/refresh
- src/core/api/providers/openai-codex.ts: API handler for Codex backend
- src/core/controller/account/openAiCodexSignIn.ts: Sign-in RPC handler
- src/core/controller/account/openAiCodexSignOut.ts: Sign-out RPC handler
- webview-ui/src/components/settings/providers/OpenAiCodexProvider.tsx: Settings UI

* fix: force native tool calling for Responses API providers

Providers using OpenAI's Responses API (openai-codex, some openai-native
models) require native tool calling. XML tools don't work with these APIs,
causing duplicate tool calls and malformed arguments.

Changes:
- Add openai-codex to isNextGenModelProvider() list so native variant
  matchers recognize it
- Force enableNativeToolCalls=true when model uses ApiFormat.OPENAI_RESPONSES,
  regardless of user setting
- Document Responses API provider requirements in CLAUDE.md

* chore: rename OpenAI Codex provider label to ChatGPT Codex Subscription

* fix: use shared fetch wrapper for proxy support in OpenAI Codex provider

* revert: remove CLAUDE.md changes from this PR

* fix: restore .clinerules/general.md to match main

* chore: rename provider label to OpenAI Codex (ChatGPT Plus/Pro)

* chore: add network.md reference to clinerules

* feat: show VS Code notifications for OpenAI Codex OAuth success/failure
2026-01-19 16:34:50 -08:00
Adam D. 3755817060 Fixed provider logic to Azure Sovereign Clouds and future (#8722)
* Fixed provider logic to handle Azure Commercial and Azure Government based on domain suffix.

* Adjusted region logic for simple long term modifications for other soverign cloud regions.
2026-01-20 01:32:41 +01:00
Tomás Barreiroandabeatrix 8ae18e6a16 Schema changes for prompt uploading (#8621)
* feat: add cloud storage and sync system infrastructure

Add cloud storage capabilities with support for R2 and S3 adapters:

- Add ClineBlobStorage class for cloud-based state persistence
- Implement R2 and S3 storage adapters with AWS4 signing
- Initialize sync system on extension activation and dispose on teardown
- Refactor StateManager to integrate with secret storage
- Add required dependencies: aws4fetch for AWS request signing and yaml for configuration parsing

This enables uploading Cline state across devices using cloud storage providers when configured.

* refactor: remove yaml dependency and refactor storage/backfill logic

Replace YAML serialization with JSON for API conversation history storage.
Refactor backfill worker to read task IDs from history file instead of
filesystem directory scanning, improving performance and consistency.

Changes:
- Remove yaml package dependency (^2.8.2)
- Switch from YAML.stringify to JSON.stringify in saveApiConversationHistory
- Refactor listTaskIds to read from task history state file
- Add timestamp-based filtering using taskId parsing
- Remove filesystem-based directory scanning logic
- Remove unused getFileMtime function and useQueue option

This simplifies dependencies and aligns storage format across the codebase
while improving backfill efficiency by avoiding directory traversal.

* clean up

* clean up

* feat(worker): add queue cleanup and size enforcement mechanisms

- Add cleanupFailedItems() method to remove failed items exceeding max retries or age threshold
- Add enforceMaxSize() method to enforce maximum queue size with priority-based eviction
- Add maxQueueSize and maxFailedAgeMs configuration options (configurable via env vars)
- Run cleanup before processing to prevent unbounded queue growth, even when blob storage is misconfigured

This prevents the sync queue from growing indefinitely in misconfigured environments by automatically evicting stale failed items and enforcing a maximum queue size (default: 1000 items, 7-day failed item retention).

* feat(sync): add remote config support for blob store settings

- Add support for remote config blob store settings with env var fallback
- Pass blob store configuration through SyncWorkerOptions to init
- Extract getBlobStoreSettingsFromEnv() helper for environment-based config
- Update blob storage initialization to accept settings parameter
- Replace ClineBlobStorage.isConfigured() with blobStorage.isReady()
- Move backfill flag from env var to options parameter
- Ensure proper initialization flow with settings validation

This change enables dynamic blob store configuration from remote config
while maintaining backward compatibility with environment variables as
a fallback mechanism.

* apply feedback

* apply feedback

* remove global fetch import

* remove secretStorage init. use const

* use a single pass with forEach instead of filter-map-delete loops

* Schema changes for prompt uploading

* Fix types and add tests

* Add tests

* Remove test

* Addapt to the BlobStoreSettings

* Add tests

* Add the missing fields

* Extend tests

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
2026-01-20 01:02:04 +01:00
BeeandTomás Barreiro 113d7fb292 feat: add sync system infrastructure with blob store support (ENG-1468) (#8628)
* feat: add cloud storage and sync system infrastructure

Add cloud storage capabilities with support for R2 and S3 adapters:

- Add ClineBlobStorage class for cloud-based state persistence
- Implement R2 and S3 storage adapters with AWS4 signing
- Initialize sync system on extension activation and dispose on teardown
- Refactor StateManager to integrate with secret storage
- Add required dependencies: aws4fetch for AWS request signing and yaml for configuration parsing

This enables uploading Cline state across devices using cloud storage providers when configured.

* refactor: remove yaml dependency and refactor storage/backfill logic

Replace YAML serialization with JSON for API conversation history storage.
Refactor backfill worker to read task IDs from history file instead of
filesystem directory scanning, improving performance and consistency.

Changes:
- Remove yaml package dependency (^2.8.2)
- Switch from YAML.stringify to JSON.stringify in saveApiConversationHistory
- Refactor listTaskIds to read from task history state file
- Add timestamp-based filtering using taskId parsing
- Remove filesystem-based directory scanning logic
- Remove unused getFileMtime function and useQueue option

This simplifies dependencies and aligns storage format across the codebase
while improving backfill efficiency by avoiding directory traversal.

* clean up

* clean up

* feat(worker): add queue cleanup and size enforcement mechanisms

- Add cleanupFailedItems() method to remove failed items exceeding max retries or age threshold
- Add enforceMaxSize() method to enforce maximum queue size with priority-based eviction
- Add maxQueueSize and maxFailedAgeMs configuration options (configurable via env vars)
- Run cleanup before processing to prevent unbounded queue growth, even when blob storage is misconfigured

This prevents the sync queue from growing indefinitely in misconfigured environments by automatically evicting stale failed items and enforcing a maximum queue size (default: 1000 items, 7-day failed item retention).

* feat(sync): add remote config support for blob store settings

- Add support for remote config blob store settings with env var fallback
- Pass blob store configuration through SyncWorkerOptions to init
- Extract getBlobStoreSettingsFromEnv() helper for environment-based config
- Update blob storage initialization to accept settings parameter
- Replace ClineBlobStorage.isConfigured() with blobStorage.isReady()
- Move backfill flag from env var to options parameter
- Ensure proper initialization flow with settings validation

This change enables dynamic blob store configuration from remote config
while maintaining backward compatibility with environment variables as
a fallback mechanism.

* apply feedback

* apply feedback

* remove global fetch import

* remove secretStorage init. use const

* use a single pass with forEach instead of filter-map-delete loops

---------

Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
2026-01-19 15:40:14 -08:00
Bee 592d045565 chore: remove duplicated dify file (#8734)
I belive the file is duplicated of src/core/api/providers/dify.ts file.
new DifyHandler is imported from src/core/api/providers/dify.ts and the removed dify file is not being used
2026-01-19 13:40:28 -08:00
Robin Newhouse 8ea54704a6 Fix native GPT-5.x variant routing for codex models (#8671)
Some GPT-5.1/5.2 (codex) models can trigger OpenAI Responses API errors like: 'function_call was provided without its required reasoning item'.

Root cause: PromptRegistry selects the first matching variant; our native-gpt-5 matcher previously let any 'codex' model bypass the gpt-5.1/gpt-5.2 exclusion, so gpt-5.2-codex could incorrectly match NATIVE_GPT_5 instead of NATIVE_GPT_5_1.

Change: route all GPT-5.1 and GPT-5.2 models (including codex variants) to NATIVE_GPT_5_1; keep GPT-5 (and gpt-5-codex) on the less strict NATIVE_GPT_5.

This was observed as a hard-to-reproduce, sporadic error, but we want the safer routing available for anyone hitting it.
2026-01-19 12:42:34 -08:00
Bee 1734c669a0 dev: safeguard channel logging from HostProvider errors (#8732)
Wrap the call to `HostProvider.logToChannel` in a try/catch block so that logging does not throw when the host provider is not ready or unavailable.
Remove the now‑unused `ErrorService` imports and logging calls, keeping the logger focused on its core responsibility while preventing unnecessary failures during startup or testing. P.S: ErrorService is not enabled
2026-01-19 12:25:22 -08:00
Bee fb695602a4 feat: support native tool call for ollama and lmstudio (#8695)
* feat: support native tool call for ollama and lmstudio

Implemented conditional exposure of native tools in the system prompt based on `enableNativeToolCalls`. Updated the XS variant used by ollama and lmstudio configuration to match local models only and removed obsolete tool references. Added comprehensive tool overrides for both native and non‑native scenarios.

* add changeset

* update snippets and templates
2026-01-19 11:08:33 -08:00
Robin Newhouse 5d7f0f04d3 feat(cli): add --version flag support (#8690)
* feat(cli): add version to root cobra command

Expose --version by setting the root command version.

* feat(cli): include core version in CLI version output

Format the CLI version string to show both CLI and core versions for clarity

* feat(cli): centralize version string output for CLI

Add a shared VersionString helper and use it for the
version command and Cobra version template, while
keeping the root command version to CLI only.
2026-01-19 09:58:05 -08:00
Tomás Barreiro 13cf28d8f4 [PF-404] Lock Vertex and LiteLLM options when they're remotely configured (#8554) 2026-01-18 21:50:03 -03:00
Saoud Rizwan 6238fab366 fix(ui): remove scrollable container from plan/task completed components (#8716) 2026-01-17 19:09:29 -08:00
Saoud Rizwan 7c26a7d16b fix(test): wait for tabs to actually close in getOpenTabs test (#8715) 2026-01-17 19:05:38 -08:00
7885c75a4f feat: add git worktree view (#8308)
* feat: add git worktree management UI

Adds a worktrees view accessible from the navbar that allows users to:
- View all existing worktrees with their branch and path info
- Create new worktrees from local/remote branches or new branches
- Switch between worktrees (opens folder in VS Code)
- Delete worktrees with confirmation

Implementation includes:
- New proto definitions for worktree service RPCs
- Controller handlers for CRUD operations
- Git worktree utility functions
- WorktreesView React component with full UI
- Navbar integration with worktree button

* feat: enhance worktree creation error handling in WorktreesView

Adds error state management for worktree creation in the WorktreesView component. Introduces a new state variable to capture and display error messages when worktree creation fails, improving user feedback during the process.

* feat: add worktree defaults retrieval to WorktreeService and UI

Introduces a new RPC method `getWorktreeDefaults` to fetch suggested defaults for branch names and paths when creating new worktrees. Updates the WorktreesView component to utilize this method, enhancing the user experience by auto-generating branch names and paths. Additionally, integrates tooltips for improved UI interactions and adds a close button to the worktree creation modal.

* feat: implement .worktreeinclude file management in WorktreeService

Adds new RPC methods to the WorktreeService for managing .worktreeinclude files, including retrieving the status of the file and creating it with specified content. Updates the WorktreesView component to handle the creation and status checking of .worktreeinclude, enhancing user experience by automating file management for worktrees. Additionally, modifies the UI to reflect these changes, including updated tooltips and improved error handling.

* feat: add checkout branch functionality to WorktreeService and UI

Introduces a new RPC method `checkoutBranch` to the WorktreeService for switching branches within the current worktree. Updates the WorktreesView component to support this functionality, enhancing user experience by allowing seamless branch switching. Additionally, refines the UI layout for better responsiveness and improves loading/error state handling.

* feat: reposition New Worktree button for improved UI layout

Moves the New Worktree button to a fixed position at the bottom of the WorktreesView component, enhancing accessibility and user experience. The button is now styled to occupy the full width, ensuring better visibility and interaction within the UI.

* feat: update documentation links in WorktreesView component

Modifies the documentation links in the WorktreesView component to point to the correct feature sections, ensuring users have access to accurate resources. Additionally, adds the "features/worktrees" entry in the documentation JSON for better organization.

* feat: add worktree merging functionality and UI enhancements

Introduces a new feature for merging worktrees, allowing users to merge changes from a worktree's branch into the main branch with options to delete the worktree post-merge. Updates the WorktreesView component to include a merge modal, handling merge conflicts, and integrating with the WorktreeService for seamless operations. Additionally, enhances documentation to reflect these changes.

* refactor: replace exec with simple-git for worktree operations

Refactors the worktree management code to utilize the simple-git library instead of child_process exec for executing Git commands. This change enhances code readability and maintainability by providing a more streamlined interface for Git operations in the checkoutBranch, mergeWorktree, and git-worktree modules. Additionally, it improves error handling and reduces the complexity of command execution.

* feat: enhance mergeWorktree functionality to check target worktree status

Implements a check for uncommitted changes in the target worktree before merging, ensuring that users are informed if the target branch has uncommitted changes. This update improves error handling and user feedback during the merge process by verifying the state of both the source and target worktrees. Additionally, it integrates the listWorktrees utility to identify the correct worktree for the target branch.

* refactor: optimize worktree loading to prevent UI flickering

Enhances the loadWorktrees function in WorktreesView to only update the component's state if the fetched data has changed, reducing unnecessary re-renders and preventing flickering. This change improves the user experience by providing a smoother interface when loading worktrees. Additionally, simplifies the polling mechanism for updates.

* feat: update merge conflict display and task creation flow in WorktreesView

Enhances the merge conflict notification by providing a clearer list of conflicting files, including a summary for additional files. Additionally, modifies the task creation flow to close the worktrees view upon task creation, improving user experience during the merge process.

* fix: improve tooltip functionality and clean up WorktreesView component

Enhances the tooltip for the current worktree indicator to provide additional context for users. Additionally, removes the display of commit hashes in the worktree list to streamline the UI, improving overall clarity and user experience.

* feat: add symlink functionality for .worktreeinclude to sync with .gitignore

Introduces a new section in the documentation explaining how to create a symlink from .gitignore to .worktreeinclude. This allows users to automatically sync patterns between the two files, simplifying worktree setup. Additionally, includes a note for users needing different patterns to create a regular .worktreeinclude file instead.

* fix: simplify merge request button in WorktreesView component

Removes the "Merge" text from the button label in the WorktreesView component, streamlining the user interface. This change focuses on clarity by allowing the button to simply prompt users to "Ask Cline to Resolve," enhancing the overall user experience during merge conflict resolution.

* Update docs/features/worktrees.mdx

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

* Update webview-ui/src/components/worktrees/WorktreesView.tsx

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

* Fixes docs not rendering

* perf(worktree): optimize file copying for .worktreeinclude

Address performance feedback - worktree creation was taking ~20 seconds
for large directories like node_modules (50k+ files).

Optimizations:
- Use native `cp -r` for entire directories (10-20x faster)
- Parallelize file copying with batches of 100 (5-10x faster)
- Parallelize directory traversal with Promise.all

The old implementation copied files sequentially which caused the
bottleneck. Now directories like node_modules are copied using the
system's native cp command, and individual files are copied in
parallel batches.

Also adds unit tests for the worktree-include module.

* feat(worktree): add multi-root and subfolder workspace warnings

- Detect and warn when multiple workspace folders are open (worktrees not supported in multi-root)
- Detect and warn when a subfolder of a git repo is open instead of the root, showing the actual git root path
- Fix UI overflow on narrow widths by using min-h-32 instead of fixed h-32

* refactor(worktree): auto-fill defaults when create modal opens

* fix(worktree): add cursor pointer to create modal close button

* feat(worktree): add clear buttons to create modal input fields

* feat(worktree): add quick launch button on home page

Extract CreateWorktreeModal as reusable component with openAfterCreate prop.
Add New Worktree Window button to WelcomeSection that creates a worktree
and opens it in a new window. Shows current worktree branch and path info.

* refactor(ui): polish home screen and worktree modal

- Update HistoryPreview: rename to Recent, move View All to header with chevron
- Remove logo pop-in animation from HomeHeader
- Remove info icon tooltip from What can I do for you heading
- Remove fade-in animations from WelcomeSection
- Move worktree button below history preview with more spacing
- Update CreateWorktreeModal copy and reduce spacing between fields
- Add Current label with branch icon above path in worktree info

* feat(worktree): auto-open Cline sidebar on worktree launch

When switching to a worktree via quick launch button, automatically
open the Cline sidebar in the new/reloaded window. Uses globalState
to pass the target path between windows, reading directly from
context.globalState at startup to bypass StateManager cache timing.

* fix(worktree): improve quick launch UX

- Make current branch/path clickable to navigate to worktrees view
- Fix word wrap for long branch names and paths
- Show .worktreeinclude warning in create modal with learn more link

* chore: ignore .worktrees directory and CLAUDE.local.md

* feat(worktree): add delete confirmation modal

* refactor(ui): remove worktrees button from title bar

* fix(worktree): improve .worktreeinclude warning styling

* docs(worktrees): update for new UI features

- Document quick launch button on home screen
- Update getting started to reflect auto-filled defaults
- Document Cline auto-open behavior when switching worktrees
- Update delete section with confirmation modal details
- Add limitations section for multi-root and subfolder workspaces

* fix(worktree): rename Main badge to Primary

* feat(worktree): add worktrees button to sidebar header

Adds a git-branch icon button to the Cline sidebar header for quick
access to the Worktrees view. Also updates docs to mention this new
entry point and adds a typical workflow section.

* fix(worktree): UI polish

- Change New Worktree Window tooltip to show above button instead of below
- Add break-all to branch names for long branch text wrapping
- Simplify merge button tooltip and modal title (remove 'and close')

* fix(e2e): update tests to match renamed Recent header

* fix(worktree): improve non-git repo message

* fix(worktree): wrap path instead of truncating

* fix(e2e): update auth test to use aria-label instead of removed class

* fix(worktree): add option to delete branch when deleting worktree

- Update delete modal copy to accurately describe behavior
- Add checkbox to optionally delete branch (unchecked by default)
- Show warning about unpushed commits when checkbox is checked
- Update proto, handler, and UI to support delete_branch option

* fix: remove worktrees menu button from sidebar

Remove the worktrees button from the VS Code extension menu bar.

* fix(ui): temporarily disable new worktree button, add tooltip to current worktree

Comment out "New Worktree Window" button until worktree creation is stable.
Add tooltip to current worktree info with "View and manage git worktrees.
Great for running parallel Cline tasks."

* feat: add worktree-exp feature flag for worktrees feature

Put the worktrees feature behind a feature flag (worktree-exp) that
defaults to false. When enabled, users can toggle the feature in
settings. The home page worktree section only shows when both the
feature flag is enabled and the user setting is on.

* feat: add telemetry for worktree feature usage

Track worktree feature engagement:
- worktree.view_opened: when users open worktrees view (with source)
- worktree.created: when worktrees are created (with total count)
- worktree.merge_attempted: when merge is attempted (success/conflicts)

* fix: replace DangerButton with Button variant="danger"

DangerButton component was removed from main. Use the standard
Button component with variant="danger" instead.

* Fix merge conflict artifacts

* Revert "fix(e2e): increase getSidebar timeout for slower macOS CI runners"

This reverts commit 19479a019c.

* fix: clean up shadow git checkpoint data when deleting worktrees

* fix: add worktreesEnabled to proto and fix duplicate import

* fix: revert e2e test changes to match main

* fix: revert Navbar.tsx to match main (JetBrains compat)

* fix: revert package.json navigation order to match main

* fix: properly add worktrees_enabled to proto without moving fields

* fix(e2e): update tests to match UI changes

- Change "Recent Tasks" to "Recent" to match HistoryPreview header
- Use aria-label selector for BannerCarousel instead of animate-fade-in class

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Juan Pablo <juan@cline.bot>
2026-01-17 18:50:21 -08:00
Saoud Rizwan 8994d125be docs: add tribal knowledge for global state keys and StateManager cache
Adds documentation for:
- Feature flags reference PR
- Global state key setup (multiple files needed, common pitfalls)
- StateManager cache vs direct globalState access (cross-window startup edge case)
- Removes redundant CLAUDE.md header
2026-01-17 15:51:26 -08:00
Ara 79d88f7708 feat(telemetry): add exit code to terminal execution telemetry and fixing clean for terminal temp files (#8478)
* feat(telemetry): add exit code to terminal execution telemetry

Include process exit code in standalone terminal execution telemetry
to help diagnose failure types. Common codes like 127 (command not found)
and 126 (permission denied) provide valuable debugging information.

- Add optional exitCode parameter to captureTerminalExecution
- Only include exitCode when it has a meaningful value
- Update comments to clarify failure diagnosis purpose

* feat(temp): add centralized temp file manager with auto-cleanup

Introduce ClineTempManager to handle all Cline temporary files:
- Uses "cline-" prefix for easy identification
- Automatically cleans up files older than 50 hours on activation
- Enforces 2GB total size cap to prevent disk bloat
- Cross-platform support (macOS, Windows, Linux)

Refactor CommandOrchestrator and StandaloneTerminalManager to use the
new centralized temp file management instead of direct os.tmpdir() calls.

* feat: add periodic temp file cleanup every 24 hours

- Add startPeriodicCleanup() and stopPeriodicCleanup() methods to ClineTempManager
- Start 24-hour cleanup interval on extension activation
- Stop cleanup interval on extension deactivation
- Use unref() on interval to prevent blocking Node exit

* minor fix

* minor fix

* fix: centralize temp cleanup and scan full temp dir

Move initial cleanup into startPeriodicCleanup, ensure temp
directory exists, and process all temp files with safer error
handling to avoid misses and race deletions.

* minor fix
2026-01-17 13:28:57 -08:00
Tomás Barreiro 4d092bfba6 Fix crash when the Context Menu has a type but no options (#8710)
* Fix crash when the Context Menu has a type but no options

* Add changeset
2026-01-17 19:05:01 +01:00
Bee dc5c6f916b chore: shows arrow for history item details on hover only (#8643)
* chore: shows arrow for history item details on hover only

Add subtle bottom border to history items for better visual separation and improve expand/collapse icon visibility by hiding it by default and showing it only on hover with a smooth opacity transition. This creates a cleaner interface while maintaining discoverability of the expand functionality.

Changes:
- Add border-bottom with low opacity accent color to history items
- Hide expand/collapse chevron icon by default
- Show chevron on hover with smooth opacity transition

* align checkbox
2026-01-16 23:00:26 -08:00
Bee 50332ec46a feat: support native tool calling for gpt‑oss models and openai-compatible provide (#8696)
Add explicit checks in the Native GPT‑5 variant to enable the variant for
`gpt‑oss` model IDs and reject non‑next‑generation providers. The provider
list in `model-utils.ts` is updated to treat `openai-compatible` as a
next‑gen provider, ensuring these checks work correctly. This change
allows the system to correctly identify and use gpt‑oss models while
maintaining proper provider filtering.
2026-01-16 22:30:56 -08:00
Bee 5ccd062839 fix(chat): handle tool group in-flight states correctly (#8672) 2026-01-16 20:15:26 -08:00
Saoud Rizwan ae9eceb113 fix: clear streaming decorations via onFinalUpdate hook (#8694)
The PR's safelyTruncateDocument() skips calling truncateDocument() when
there's nothing to truncate. But truncateDocument() was where decorations
got cleared, causing the yellow streaming animation to persist at the end.

Fix: Add onFinalUpdate() hook that's always called after the final update.
VscodeDiffViewProvider overrides it to clear decorations.
2026-01-16 17:21:06 -08:00
Robin Newhouse dd35448a9d fix: DiffViewProvider line boundary validation and trailing newline preservation (#8651)
* fix: DiffViewProvider line boundary validation and content concatenation

Two bugs in DiffViewProvider caused file editing failures:

1. **Line boundary validation errors (#8423, #8429)**
   JetBrains hosts using gRPC strictly validate line numbers. When
   truncateDocument() was called with a line number >= document line count,
   it caused "truncateDocument INTERNAL: Wrong line" errors. This occurred
   when new content had >= lines than the original, making truncation
   unnecessary but still attempted.

2. **Content concatenation on final update**
   When replacing content without a trailing newline, the old content at
   line N+1 was concatenated to the new content. For example, writing
   "Hello World" to a file containing "line1\nline2\n" resulted in
   "Hello Worldline2" instead of just "Hello World".

1. Added `getDocumentLineCount()` abstract method to all DiffViewProvider
   implementations to query the current document line count.

2. Added `safelyTruncateDocument()` private helper that validates line
   numbers before calling truncateDocument():
   ```typescript
   private async safelyTruncateDocument(lineNumber: number): Promise<void> {
     const lineCount = await this.getDocumentLineCount()
     if (lineNumber < lineCount) {
       await this.truncateDocument(lineNumber)
     }
   }
   ```

3. Extended the replacement range on final update to cover the entire
   document, preventing content concatenation:
   ```typescript
   const endLine = isFinal
     ? await this.getDocumentLineCount()
     : currentLine + 1
   ```

- src/integrations/editor/DiffViewProvider.ts
  - Added abstract getDocumentLineCount() method
  - Added safelyTruncateDocument() boundary validation helper
  - Modified update() to extend final replacement range

- src/hosts/vscode/VscodeDiffViewProvider.ts
  - Implemented getDocumentLineCount() using editor.document.lineCount

- src/hosts/external/ExternalDiffviewProvider.ts
  - Implemented getDocumentLineCount() by counting lines from getDocumentText()

- src/integrations/editor/FileEditProvider.ts
  - Implemented getDocumentLineCount() from documentContent

- src/integrations/editor/__tests__/DiffViewProvider.test.ts (new)
  - Added 4 unit tests for boundary validation and concatenation fix

Fixes #8423, #8429

* fix: preserve trailing newlines in file edits

Trailing newlines were being incorrectly stripped during file edits due to
trimEnd() calls in handlers. This caused files to lose their final newline
even when the original file had one.

Changes:
- Remove trimEnd() from WriteToFileToolHandler and ApplyPatchHandler that
  was stripping trailing newlines before content reached the editor
- Remove dead code in DiffViewProvider.update() that tried to restore
  newlines after the document was already written
- Add trailing newline fix-up in VscodeDiffViewProvider to handle VS Code's
  applyEdit sometimes normalizing newlines on full-document replacements
- Fix FileEditProvider.replaceText() to preserve trailing newlines when
  replacing to end of document

* fix: preserve trailing newlines in diff text ops

Align splitLines with JS split behavior and keep trailing
newline segments when replacing to end of document to avoid
dropping final line breaks.
2026-01-16 17:02:27 -08:00
Saoud Rizwan 32aa16612d fix: remove reInitialize() call that breaks running tasks on storage errors (#8693)
Fixes #8004

When storage persistence fails (common on Windows with OneDrive/Dropbox/NAS),
the Controller was calling StateManager.reInitialize() to "recover". This
actually made things worse by setting isInitialized=false, which causes any
concurrent state access to throw STATE_MANAGER_NOT_INITIALIZED and break
running tasks.

The fix: just log the error. Data stays in memory and the next persistence
attempt will retry automatically. No need to alarm users with warnings since
nothing is actually lost.
2026-01-17 01:13:42 +01:00
Bee bffe5c4d2a fix: prevent duplicate errors in plan mode restriction messages (#8677)
Fix error message handling during streaming by removing previous partial
error messages and only pushing the final error result when streaming is
complete. This prevents multiple error messages from being displayed for
the same plan mode tool restriction and ensures errors are only finalized
after streaming ends.
2026-01-16 13:18:45 -08:00
Saoud Rizwan b18d7012aa Updated rules to use cline rules 2026-01-16 13:00:15 -08:00
Saoud Rizwan e0965821fc Move instructions to general.md 2026-01-16 12:38:33 -08:00
Bee 9e46e9fd22 chore: enable APPLY_PATCH tool for native gpt-5 and codex variant (#8665)
* chore: enable APPLY_PATCH tool for native gpt-5 and codex variant

Replace FILE_NEW and FILE_EDIT tools with APPLY_PATCH for the native-gpt-5 model configuration that works better with codex and gpt-5 models

* Update changeset

* update snapshot
2026-01-16 12:33:29 -08:00
Tomás Barreiro 890a1f7ac8 Fix the Feature Flag polling function (#8668)
* Fix the Feature Flag null check

* Pass null instead of undefined

* Update the cacheInfo so we don't fetch twice simultaneously

* Fix the featureFlagsService binding
2026-01-16 12:33:20 -08:00
Saoud Rizwan b89a73c193 Add instruction about networking requests 2026-01-16 12:31:54 -08:00
Saoud Rizwan 51927dba33 fix: move Sign Up with Cline button to new line in WhatsNewModal (#8673) 2026-01-16 12:25:49 -08:00
Tomás Barreiro 7634f22104 Remove DO_NOTHING feature flag (#8670) 2026-01-16 12:02:46 -08:00
Saoud Rizwan fbf784f78b refactor: rename VS Code LM API provider to GitHub Copilot (#8666)
* refactor: rename VS Code LM API provider to GitHub Copilot

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

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

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

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

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

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

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

---------

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

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

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

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

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

* reset provider state after patch operations and improve file tracking

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

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

* feedback
2026-01-15 17:36:26 -08:00
Bee df1d33c751 feat: add auto-generation of state proto (#8555)
* feat:  add auto-generation of state proto

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

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

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

* PlanActMode

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

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

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

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

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

* add documentation for proto field generation

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

* fix comment format

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

* update

* udpate styles

* Create wild-ears-poke.md

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

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

* clean up

* remove unused styles

---------

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

* Introduce a throttle RemoteConfigService

* Add changeset

* Change the interval to an hour

* Refactor

* Reintroduce comment and remove await

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

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

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

* add changeset

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

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

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

* Create big-cows-ring.md

* Update maxTokens

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

---------

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

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

* add changeset

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

---------

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

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

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

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

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

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

* add changeset

* refactor: move isOpenAIResponseToolId and fix tool ID truncation

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

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

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

* Fix tool call length

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

---------

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

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

* Revert not fetching if no token is provided

* Remove redundant null

* Make a single call

* Make another request if forceRefresh is true

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

* Add changeset

* Return the provider set by the remote config

* Address comments

* Address comment

* Validate when updating settings

* Refactor

* Revert

* Use a more descriptive name

* Fix types

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

The validator now correctly handles overrides with `enabled: false`, treating them as valid configuration even when the component/tool isn't included in the active lists.
2026-01-13 17:17:52 -08:00
665 changed files with 23958 additions and 11714 deletions
+196
View File
@@ -0,0 +1,196 @@
---
name: create-pull-request
description: Create a GitHub pull request following project conventions. Use when the user asks to create a PR, submit changes for review, or open a pull request. Handles commit analysis, branch management, and PR creation using the gh CLI tool.
---
# Create Pull Request
This skill guides you through creating a well-structured GitHub pull request that follows project conventions and best practices.
## Prerequisites Check
Before proceeding, verify the following:
### 1. Check if `gh` CLI is installed
```bash
gh --version
```
If not installed, inform the user:
> The GitHub CLI (`gh`) is required but not installed. Please install it:
> - macOS: `brew install gh`
> - Other: https://cli.github.com/
### 2. Check if authenticated with GitHub
```bash
gh auth status
```
If not authenticated, guide the user to run `gh auth login`.
### 3. Verify clean working directory
```bash
git status
```
If there are uncommitted changes, ask the user whether to:
- Commit them as part of this PR
- Stash them temporarily
- Discard them (with caution)
## Gather Context
### 1. Identify the current branch
```bash
git branch --show-current
```
Ensure you're not on `main` or `master`. If so, ask the user to create or switch to a feature branch.
### 2. Find the base branch
```bash
git remote show origin | grep "HEAD branch"
```
This is typically `main` or `master`.
### 3. Analyze recent commits relevant to this PR
```bash
git log origin/main..HEAD --oneline --no-decorate
```
Review these commits to understand:
- What changes are being introduced
- The scope of the PR (single feature/fix or multiple changes)
- Whether commits should be squashed or reorganized
### 4. Review the diff
```bash
git diff origin/main..HEAD --stat
```
This shows which files changed and helps identify the type of change.
## Information Gathering
Before creating the PR, you need the following information. Check if it can be inferred from:
- Commit messages
- Branch name (e.g., `fix/issue-123`, `feature/new-login`)
- Changed files and their content
If any critical information is missing, use `ask_followup_question` to ask the user:
### Required Information
1. **Related Issue Number**: Look for patterns like `#123`, `fixes #123`, or `closes #123` in commit messages
2. **Description**: What problem does this solve? Why were these changes made?
3. **Type of Change**: Bug fix, new feature, breaking change, refactor, cosmetic, documentation, or workflow
4. **Test Procedure**: How was this tested? What could break?
### Example clarifying question
If the issue number is not found:
> I couldn't find a related issue number in the commit messages or branch name. What GitHub issue does this PR address? (Enter the issue number, e.g., "123" or "N/A" for small fixes)
## Git Best Practices
Before creating the PR, consider these best practices:
### Commit Hygiene
1. **Atomic commits**: Each commit should represent a single logical change
2. **Clear commit messages**: Follow conventional commit format when possible
3. **No merge commits**: Prefer rebasing over merging to keep history clean
### Branch Management
1. **Rebase on latest main** (if needed):
```bash
git fetch origin
git rebase origin/main
```
2. **Squash if appropriate**: If there are many small "WIP" commits, consider interactive rebase:
```bash
git rebase -i origin/main
```
Only suggest this if commits appear messy and the user is comfortable with rebasing.
### Push Changes
Ensure all commits are pushed:
```bash
git push origin HEAD
```
If the branch was rebased, you may need:
```bash
git push origin HEAD --force-with-lease
```
## Create the Pull Request
**IMPORTANT**: Read and use the PR template at `.github/pull_request_template.md`. The PR body format must **strictly match** the template structure. Do not deviate from the template format.
When filling out the template:
- Replace `#XXXX` with the actual issue number, or keep as `#XXXX` if no issue exists (for small fixes)
- Fill in all sections with relevant information gathered from commits and context
- Mark the appropriate "Type of Change" checkbox(es)
- Complete the "Pre-flight Checklist" items that apply
### Create PR with gh CLI
```bash
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main
```
Alternatively, create as draft if the user wants review before marking ready:
```bash
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main --draft
```
## Post-Creation
After creating the PR:
1. **Display the PR URL** so the user can review it
2. **Remind about CI checks**: Tests and linting will run automatically
3. **Suggest next steps**:
- Add reviewers if needed: `gh pr edit --add-reviewer USERNAME`
- Add labels if needed: `gh pr edit --add-label "bug"`
## Error Handling
### Common Issues
1. **No commits ahead of main**: The branch has no changes to submit
- Ask if the user meant to work on a different branch
2. **Branch not pushed**: Remote doesn't have the branch
- Push the branch first: `git push -u origin HEAD`
3. **PR already exists**: A PR for this branch already exists
- Show the existing PR: `gh pr view`
- Ask if they want to update it instead
4. **Merge conflicts**: Branch conflicts with base
- Guide user through resolving conflicts or rebasing
## Summary Checklist
Before finalizing, ensure:
- [ ] `gh` CLI is installed and authenticated
- [ ] Working directory is clean
- [ ] All commits are pushed
- [ ] Branch is up-to-date with base branch
- [ ] Related issue number is identified, or placeholder is used
- [ ] PR description follows the template exactly
- [ ] Appropriate type of change is selected
- [ ] Pre-flight checklist items are addressed
+194
View File
@@ -0,0 +1,194 @@
This file is the secret sauce for working effectively in this codebase. It captures tribal knowledge—the nuanced, non-obvious patterns that make the difference between a quick fix and hours of back-and-forth & human intervention.
**When to add to this file:**
- User had to intervene, correct, or hand-hold
- Multiple back-and-forth attempts were needed to get something working
- You discovered something that required reading many files to understand
- A change touched files you wouldn't have guessed
- Something worked differently than you expected
- User explicitly asks to "add this to CLAUDE.md"
**Proactively suggest additions** when any of the above happen—don't wait to be asked.
**What NOT to add:** Stuff you can figure out from reading a few files, obvious patterns, or standard practices. This file should be high-signal, not comprehensive.
## Miscellaneous
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
- When creating PRs, if the change is user-facing and significant enough to warrant a changelog entry, run `npm run changeset` and create a patch changeset. Never create minor or major version bumps. Skip changesets for trivial fixes, internal refactors, or minor UI tweaks that users wouldn't notice.
- When adding new feature flags, see this PR as a reference https://github.com/cline/cline/pull/7566
- Additional instructions about making requests: @.clinerules/network.md
## gRPC/Protobuf Communication
The extension and webview communicate via gRPC-like protocol over VS Code message passing.
**Proto files live in `proto/`** (e.g., `proto/cline/task.proto`, `proto/cline/ui.proto`)
- Each feature domain has its own `.proto` file
- For simple data, use shared types in `proto/cline/common.proto` (`StringRequest`, `Empty`, `Int64Request`)
- For complex data, define custom messages in the feature's `.proto` file
- Naming: Services `PascalCaseService`, RPCs `camelCase`, Messages `PascalCase`
- For streaming responses, use `stream` keyword (see `subscribeToAuthCallback` in `account.proto`)
**Run `npm run protos`** after any proto changes—generates types in:
- `src/shared/proto/` - Shared type definitions
- `src/generated/grpc-js/` - Service implementations
- `src/generated/nice-grpc/` - Promise-based clients
- `src/generated/hosts/` - Generated handlers
**Adding new enum values** (like a new `ClineSay` type) requires updating conversion mappings in `src/shared/proto-conversions/cline-message.ts`
**Adding new RPC methods** requires:
- Handler in `src/core/controller/<domain>/`
- Call from webview via generated client: `UiServiceClient.scrollToSettings(StringRequest.create({ value: "browser" }))`
**Example—the `explain-changes` feature touched:**
- `proto/cline/task.proto` - Added `ExplainChangesRequest` message and `explainChanges` RPC
- `proto/cline/ui.proto` - Added `GENERATE_EXPLANATION = 29` to `ClineSay` enum
- `src/shared/ExtensionMessage.ts` - Added `ClineSayGenerateExplanation` type
- `src/shared/proto-conversions/cline-message.ts` - Added mapping for new say type
- `src/core/controller/task/explainChanges.ts` - Handler implementation
- `webview-ui/src/components/chat/ChatRow.tsx` - UI rendering
## Adding a New API Provider
When adding a new provider (e.g., "openai-codex"), you must update the proto conversion layer in THREE places or the provider will silently reset to Anthropic:
1. `proto/cline/models.proto` - Add to the `ApiProvider` enum (e.g., `OPENAI_CODEX = 40;`)
2. `convertApiProviderToProto()` in `src/shared/proto-conversions/models/api-configuration-conversion.ts` - Add case mapping string to proto enum
3. `convertProtoToApiProvider()` in the same file - Add case mapping proto enum back to string
**Why this matters:** Without these, the provider string hits the `default` case and returns `ANTHROPIC`. The webview, provider list, and handler all work fine, but the state silently resets when it round-trips through proto serialization. No error is thrown.
**Other files to update when adding a provider:**
- `src/shared/api.ts` - Add to `ApiProvider` union type, define models
- `src/shared/providers/providers.json` - Add to provider list for dropdown
- `src/core/api/index.ts` - Register handler in `createHandlerForProvider()`
- `webview-ui/src/components/settings/utils/providerUtils.ts` - Add cases in `getModelsForProvider()` and `normalizeApiConfiguration()`
- `webview-ui/src/utils/validate.ts` - Add validation case
- `webview-ui/src/components/settings/ApiOptions.tsx` - Render provider component
## Responses API Providers (OpenAI Codex, OpenAI Native)
Providers using OpenAI's Responses API require native tool calling. XML tools don't work with the Responses API.
**Symptoms of broken native tool calling:**
- Tools get called multiple times (e.g., `ask_followup_question` asks the same question twice)
- Tool arguments get duplicated or malformed
- The model responds but tools aren't recognized
**Root causes to check:**
1. **Provider missing from `isNextGenModelProvider()`** in `src/utils/model-utils.ts`. The native variant matchers (e.g., `native-gpt-5/config.ts`) call this function. If your provider isn't in the list, the matcher returns false and falls back to XML tools.
2. **Model missing `apiFormat: ApiFormat.OPENAI_RESPONSES`** in its model info (`src/shared/api.ts`). This property signals that the model requires native tool calling. The task runner in `src/core/task/index.ts` checks this and forces `enableNativeToolCalls: true` regardless of user settings.
**When adding a new Responses API provider:**
1. Add provider to `isNextGenModelProvider()` list in `src/utils/model-utils.ts`
2. Set `apiFormat: ApiFormat.OPENAI_RESPONSES` on all models that use the Responses API
3. The variant matcher and task runner will handle the rest automatically
## Adding Tools to System Prompt
This is tricky—multiple prompt variants and configs. **Always search for existing similar tools first and follow their pattern.** Look at the full chain from prompt definition → variant configs → handler → UI before implementing.
1. **Add to `ClineDefaultTool` enum** in `src/shared/tools.ts`
2. **Tool definition** in `src/core/prompts/system-prompt/tools/` (create file like `generate_explanation.ts`)
- Define variants for each `ModelFamily` (generic, next-gen, xs, etc.)
- Export variants array (e.g., `export const my_tool_variants = [GENERIC, NATIVE_NEXT_GEN, XS]`)
- **Fallback behavior**: If a variant isn't defined for a model family, `ClineToolSet.getToolByNameWithFallback()` automatically falls back to GENERIC. So you only need to export `[GENERIC]` unless the tool needs model-specific behavior.
3. **Register in `src/core/prompts/system-prompt/tools/init.ts`** - Import and spread into `allToolVariants`
4. **Add to variant configs** - Each model family has its own config in `src/core/prompts/system-prompt/variants/*/config.ts`. Add your tool's enum to the `.tools()` list:
- `generic/config.ts`, `next-gen/config.ts`, `gpt-5/config.ts`, `native-gpt-5/config.ts`, `native-gpt-5-1/config.ts`, `native-next-gen/config.ts`, `gemini-3/config.ts`, `glm/config.ts`, `hermes/config.ts`, `xs/config.ts`
- **Important**: If you add to a variant's config, make sure the tool spec exports a variant for that ModelFamily (or relies on GENERIC fallback)
5. **Create handler** in `src/core/task/tools/handlers/`
6. **Wire up in `ToolExecutor.ts`** if needed for execution flow
7. **Add to tool parsing** in `src/core/assistant-message/index.ts` if needed
8. **If tool has UI feedback**: add `ClineSay` enum in proto, update `src/shared/ExtensionMessage.ts`, update `src/shared/proto-conversions/cline-message.ts`, update `webview-ui/src/components/chat/ChatRow.tsx`
## Modifying System Prompt
**Read these first:** `src/core/prompts/system-prompt/README.md`, `tools/README.md`, `__tests__/README.md`
System prompt is modular: **components** (reusable sections) + **variants** (model-specific configs) + **templates** (with `{{PLACEHOLDER}}` resolution).
**Key directories:**
- `components/` - Shared sections: `rules.ts`, `capabilities.ts`, `editing_files.ts`, etc.
- `variants/` - Model-specific: `generic/`, `next-gen/`, `xs/`, `gpt-5/`, `gemini-3/`, `hermes/`, `glm/`, etc.
- `templates/` - Template engine and placeholder definitions
**Variant tiers (ask user which to modify):**
- **Next-gen** (Claude 4, GPT-5, Gemini 2.5): `next-gen/`, `native-next-gen/`, `native-gpt-5/`, `native-gpt-5-1/`, `gemini-3/`, `gpt-5/`
- **Standard** (default fallback): `generic/`
- **Local/small models**: `xs/`, `hermes/`, `glm/`
**How overrides work:** Variants can override components via `componentOverrides` in their `config.ts`, or provide a custom template in `template.ts` (e.g., `next-gen/template.ts` exports `rules_template`). If no override, the shared component from `components/` is used.
**Example: Adding a rule to RULES section**
1. Check if variant overrides rules: look for `rules_template` in `variants/*/template.ts` or `componentOverrides.RULES` in `config.ts`
2. If shared: modify `components/rules.ts`
3. If overridden: modify that variant's template
4. XS variant is special—has heavily condensed inline content in `template.ts`
**After any changes, regenerate snapshots:**
```bash
UPDATE_SNAPSHOTS=true npm run test:unit
```
Snapshots live in `__tests__/__snapshots__/`. Tests validate across model families and context variations (browser, MCP, focus chain).
## Modifying Default Slash Commands
Three places need updates:
- `src/core/slash-commands/index.ts` - Command definitions
- `src/core/prompts/commands.ts` - System prompt integration
- `webview-ui/src/utils/slash-commands.ts` - Webview autocomplete
## Adding New Global State Keys
Adding a new key to global state requires updates in multiple places. Missing any step causes silent failures.
Required steps:
1. Type definition in `src/shared/storage/state-keys.ts` - Add to `GlobalState` or `Settings` interface
2. Read from globalState in `src/core/storage/utils/state-helpers.ts`:
- Add `const myKey = context.globalState.get<GlobalStateAndSettings["myKey"]>("myKey")` in `readGlobalStateFromDisk()`
- Add to the return object: `myKey: myKey ?? defaultValue,`
3. StateManager handles read/write via `setGlobalState()`/`getGlobalStateKey()` after initialization
Common mistake: Adding only the return value without the `context.globalState.get()` call. This compiles but the value is always `undefined` on load.
## StateManager Cache vs Direct globalState Access
StateManager uses an in-memory cache populated during `StateManager.initialize(context)` in `common.ts`. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
Exception: State needed immediately at extension startup (before cache is ready)
When Window A sets state and immediately opens Window B, the new window's StateManager cache is populated from `context.globalState` during initialization. If you need to read state in Window B right at startup (e.g., in `common.ts` during `initialize()`), read directly from `context.globalState.get()` instead of StateManager's cache.
Example pattern (see `lastShownAnnouncementId` and `worktreeAutoOpenPath`):
```typescript
// Writing (normal pattern)
controller.stateManager.setGlobalState("myKey", value)
// Reading at startup in common.ts (bypass cache)
const value = context.globalState.get<string>("myKey")
```
This is only needed for cross-window state read during the brief startup window before StateManager cache is fully usable. Normal state access after initialization should use StateManager.
## ChatRow Cancelled/Interrupted States
When a ChatRow displays a loading/in-progress state (spinner), you must handle what happens when the task is cancelled. This is non-obvious because cancellation doesn't update the message content—you have to infer it from context.
**The pattern:**
1. A message has a `status` field (e.g., `"generating"`, `"complete"`, `"error"`) stored in `message.text` as JSON
2. When cancelled mid-operation, the status stays `"generating"` forever—no one updates it
3. To detect cancellation, check TWO conditions:
- `!isLast` — if this message is no longer the last message, something else happened after it (interrupted)
- `lastModifiedMessage?.ask === "resume_task" || "resume_completed_task"` — task was just cancelled and is waiting to resume
**Example from `generate_explanation`:**
```tsx
const wasCancelled =
explanationInfo.status === "generating" &&
(!isLast ||
lastModifiedMessage?.ask === "resume_task" ||
lastModifiedMessage?.ask === "resume_completed_task")
const isGenerating = explanationInfo.status === "generating" && !wasCancelled
```
**Why both checks?**
- `!isLast` catches: cancelled → resumed → did other stuff → this old message is stale
- `lastModifiedMessage?.ask === "resume_task"` catches: just cancelled, hasn't resumed yet, this message is still technically "last"
**See also:** `BrowserSessionRow.tsx` uses similar pattern with `isLastApiReqInterrupted` and `isLastMessageResume`.
**Backend side:** When streaming is cancelled, clean up properly (close tabs, clear comments, etc.) by checking `taskState.abort` after the streaming function returns.
+25 -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)
# ============================================================================
@@ -91,6 +84,31 @@ POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: tru
# OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token
# ============================================================================
# OBJECT STORE CONFIGURATION
# ============================================================================
# TO ENABLE S3 OR R2 STORAGE, UNCOMMENT AND FILL IN THE FOLLOWING:
# CLINE_STORAGE_ADAPTER="s3" # Options: "s3" or "r2"
# CLINE_STORAGE_BUCKET="cline"
# CLINE_STORAGE_ACCESS_KEY_ID="key"
# CLINE_STORAGE_SECRET_ACCESS_KEY="secrets"
#
# [OPTIONAL FIELDS FOR R2]
# CLINE_STORAGE_ACCOUNT_ID = "account-id"
# Default R2 endpoint (if not set): "https://<CLINE_STORAGE_ACCOUNT_ID>.r2.cloudflarestorage.com"
# CLINE_STORAGE_ENDPOINT = "http://localhost:8333"
#
# [OPTIONAL FIELDS FOR S3]
# CLINE_STORAGE_REGION = "us-west-1" # AWS Bucket Region (default: "us-east-1")
# Default S3 endpoint (if not set): "https://s3.<CLINE_STORAGE_REGION>.amazonaws.com"
# CLINE_STORAGE_ENDPOINT = "http://localhost:8333"
#
# [OPTIONAL FIELDS FOR ALL STORAGE TYPES]
# CLINE_STORAGE_SYNC_INTERVAL_MS = 30000 # Interval for sync worker in milliseconds
# CLINE_STORAGE_SYNC_MAX_RETRIES = 5 # Max retries for failed sync operations
# CLINE_STORAGE_SYNC_BATCH_SIZE = 10 # Number of files to sync in each batch
# CLINE_STORAGE_SYNC_BACKFILL_ENABLED = false # Enable backfill of existing data on startup
# ============================================================================
# OPTIONAL DEVELOPMENT SETTINGS
# ============================================================================
-1
View File
@@ -97,7 +97,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: npm run compile-standalone-npm
- name: Generate Protos (Second Pass - Bug Workaround)
-1
View File
@@ -138,7 +138,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: npm run compile-standalone-npm
- name: Generate Protos (Second Pass - Bug Workaround)
+3 -12
View File
@@ -187,20 +187,11 @@ jobs:
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache-dependency-path: cli/go.sum
- name: Build CLI binaries
run: npm run compile-cli-all-platforms
- name: Download ripgrep binaries
run: npm run download-ripgrep
- name: Compile NPM package
run: npm run compile-standalone-npm
- name: Compile Standalone
run: npm run compile-standalone
- name: Install testing platform dependencies
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
@@ -213,7 +204,7 @@ jobs:
# This prevents the job from showing as failed and avoids distracting developers
# until the integration tests are ready to be enforced.
run: |
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage || true
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
+38 -10
View File
@@ -1,17 +1,26 @@
name: Trigger Jetbrains Plugin <-> Cline Tests
on:
pull_request_target:
types: [opened, synchronize, reopened]
types: [opened, reopened]
issue_comment:
types: [created]
permissions:
contents: read
pull-requests: read
concurrency:
group: jetbrains-trigger-${{ github.event.number }}
group: jetbrains-trigger-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: true
jobs:
trigger-integration-test:
name: Run Tests
runs-on: ubuntu-latest
# Run on PR open/reopen, or when someone comments /test-jetbrains on a PR
if: |
github.event_name == 'pull_request_target' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '/test-jetbrains'))
steps:
- name: Generate GitHub App Token
id: app-token
@@ -22,16 +31,28 @@ jobs:
owner: cline
repositories: intellij-plugin
- name: Get PR details (for issue_comment trigger)
id: pr-details
if: github.event_name == 'issue_comment'
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }})
echo "head_ref=$(echo "$PR_DATA" | jq -r '.head.ref')" >> $GITHUB_OUTPUT
echo "head_sha=$(echo "$PR_DATA" | jq -r '.head.sha')" >> $GITHUB_OUTPUT
echo "title=$(echo "$PR_DATA" | jq -r '.title')" >> $GITHUB_OUTPUT
echo "html_url=$(echo "$PR_DATA" | jq -r '.html_url')" >> $GITHUB_OUTPUT
- name: Sanitize untrusted inputs
id: sanitize
env:
RAW_BRANCH_NAME: ${{ github.head_ref }}
RAW_PR_TITLE: ${{ github.event.pull_request.title }}
RAW_BRANCH_NAME: ${{ github.event_name == 'pull_request_target' && github.head_ref || steps.pr-details.outputs.head_ref }}
RAW_PR_TITLE: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.title || steps.pr-details.outputs.title }}
run: |
# Sanitize branch name for JSON
BRANCH_NAME_JSON=$(jq -n --arg b "$RAW_BRANCH_NAME" '$b')
echo "branch_name=$BRANCH_NAME_JSON" >> $GITHUB_OUTPUT
# Sanitize PR title for JSON
PR_TITLE_JSON=$(jq -n --arg t "$RAW_PR_TITLE" '$t')
echo "pr_title=$PR_TITLE_JSON" >> $GITHUB_OUTPUT
@@ -40,6 +61,9 @@ jobs:
env:
BRANCH_NAME: ${{ steps.sanitize.outputs.branch_name }}
PR_TITLE: ${{ steps.sanitize.outputs.pr_title }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
PR_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || steps.pr-details.outputs.head_sha }}
PR_URL: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.html_url || steps.pr-details.outputs.html_url }}
run: |
curl -X POST \
-H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \
@@ -51,19 +75,23 @@ jobs:
{
"event_type": "cline-pr-check",
"client_payload": {
"pr_number": "${{ github.event.number }}",
"pr_number": "$PR_NUMBER",
"branch_name": $BRANCH_NAME,
"action": "${{ github.event.action }}",
"sha": "${{ github.event.pull_request.head.sha }}",
"sha": "$PR_SHA",
"pr_title": $PR_TITLE,
"pr_url": "${{ github.event.pull_request.html_url }}"
"pr_url": "$PR_URL"
}
}
EOF
- name: Log trigger details
env:
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
PR_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || steps.pr-details.outputs.head_sha }}
run: |
echo "Triggered IntelliJ Plugin integration test for:"
echo " PR #${{ github.event.number }}"
echo " PR #$PR_NUMBER"
echo " Trigger: ${{ github.event_name }}"
echo " Action: ${{ github.event.action }}"
echo " SHA: ${{ github.event.pull_request.head.sha }}"
echo " SHA: $PR_SHA"
+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
+2
View File
@@ -1,6 +1,8 @@
# Default
.vscode/**
.vscode-test/**
.worktrees/**
CLAUDE.local.md
out/
dist-standalone/
node_modules/
+92 -1
View File
@@ -1,5 +1,96 @@
# 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
## [3.53.0]
### Fixed
- Removed grok model from free tier
## [3.52.0]
### Added
- Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
- Grok models are now moving out of free tier and into paid plans.
- Introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness.
### Fixed
- Bugs in DiffViewProvider for file editing
- Ollama's recommended models to use correct identifiers
## [3.51.0]
### Added
- Adding OpenAI gpt-5.2-codex model to the model picker
## [3.50.0]
### Added
- Add gpt-5.2-codex OpenAI model support
- Add create-pull-request skill
### Fixed
- Fix the selection of remotely configured providers
- Fix act_mode_respond to prevent consecutive calls
- Fix invalid tool call IDs when switching between model formats
## [3.49.1]
### Added
@@ -1718,4 +1809,4 @@ Add Opus 4.1 through Claude Code
## [0.0.6]
- Initial release
- Initial release
+2 -129
View File
@@ -1,129 +1,2 @@
# CLAUDE.md
This file is the secret sauce for working effectively in this codebase. It captures tribal knowledge—the nuanced, non-obvious patterns that make the difference between a quick fix and hours of back-and-forth & human intervention.
**When to add to this file:**
- User had to intervene, correct, or hand-hold
- Multiple back-and-forth attempts were needed to get something working
- You discovered something that required reading many files to understand
- A change touched files you wouldn't have guessed
- Something worked differently than you expected
- User explicitly asks to "add this to CLAUDE.md"
**Proactively suggest additions** when any of the above happen—don't wait to be asked.
**What NOT to add:** Stuff you can figure out from reading a few files, obvious patterns, or standard practices. This file should be high-signal, not comprehensive.
## Miscellaneous
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
- When creating PRs, if the change is user-facing and significant enough to warrant a changelog entry, run `npm run changeset` and create a patch changeset. Never create minor or major version bumps. Skip changesets for trivial fixes, internal refactors, or minor UI tweaks that users wouldn't notice.
## gRPC/Protobuf Communication
The extension and webview communicate via gRPC-like protocol over VS Code message passing.
**Proto files live in `proto/`** (e.g., `proto/cline/task.proto`, `proto/cline/ui.proto`)
- Each feature domain has its own `.proto` file
- For simple data, use shared types in `proto/cline/common.proto` (`StringRequest`, `Empty`, `Int64Request`)
- For complex data, define custom messages in the feature's `.proto` file
- Naming: Services `PascalCaseService`, RPCs `camelCase`, Messages `PascalCase`
- For streaming responses, use `stream` keyword (see `subscribeToAuthCallback` in `account.proto`)
**Run `npm run protos`** after any proto changes—generates types in:
- `src/shared/proto/` - Shared type definitions
- `src/generated/grpc-js/` - Service implementations
- `src/generated/nice-grpc/` - Promise-based clients
- `src/generated/hosts/` - Generated handlers
**Adding new enum values** (like a new `ClineSay` type) requires updating conversion mappings in `src/shared/proto-conversions/cline-message.ts`
**Adding new RPC methods** requires:
- Handler in `src/core/controller/<domain>/`
- Call from webview via generated client: `UiServiceClient.scrollToSettings(StringRequest.create({ value: "browser" }))`
**Example—the `explain-changes` feature touched:**
- `proto/cline/task.proto` - Added `ExplainChangesRequest` message and `explainChanges` RPC
- `proto/cline/ui.proto` - Added `GENERATE_EXPLANATION = 29` to `ClineSay` enum
- `src/shared/ExtensionMessage.ts` - Added `ClineSayGenerateExplanation` type
- `src/shared/proto-conversions/cline-message.ts` - Added mapping for new say type
- `src/core/controller/task/explainChanges.ts` - Handler implementation
- `webview-ui/src/components/chat/ChatRow.tsx` - UI rendering
## Adding Tools to System Prompt
This is tricky—multiple prompt variants and configs. **Always search for existing similar tools first and follow their pattern.** Look at the full chain from prompt definition → variant configs → handler → UI before implementing.
1. **Add to `ClineDefaultTool` enum** in `src/shared/tools.ts`
2. **Tool definition** in `src/core/prompts/system-prompt/tools/` (create file like `generate_explanation.ts`)
- Define variants for each `ModelFamily` (generic, next-gen, xs, etc.)
- Export variants array (e.g., `export const my_tool_variants = [GENERIC, NATIVE_NEXT_GEN, XS]`)
- **Fallback behavior**: If a variant isn't defined for a model family, `ClineToolSet.getToolByNameWithFallback()` automatically falls back to GENERIC. So you only need to export `[GENERIC]` unless the tool needs model-specific behavior.
3. **Register in `src/core/prompts/system-prompt/tools/init.ts`** - Import and spread into `allToolVariants`
4. **Add to variant configs** - Each model family has its own config in `src/core/prompts/system-prompt/variants/*/config.ts`. Add your tool's enum to the `.tools()` list:
- `generic/config.ts`, `next-gen/config.ts`, `gpt-5/config.ts`, `native-gpt-5/config.ts`, `native-gpt-5-1/config.ts`, `native-next-gen/config.ts`, `gemini-3/config.ts`, `glm/config.ts`, `hermes/config.ts`, `xs/config.ts`
- **Important**: If you add to a variant's config, make sure the tool spec exports a variant for that ModelFamily (or relies on GENERIC fallback)
5. **Create handler** in `src/core/task/tools/handlers/`
6. **Wire up in `ToolExecutor.ts`** if needed for execution flow
7. **Add to tool parsing** in `src/core/assistant-message/index.ts` if needed
8. **If tool has UI feedback**: add `ClineSay` enum in proto, update `src/shared/ExtensionMessage.ts`, update `src/shared/proto-conversions/cline-message.ts`, update `webview-ui/src/components/chat/ChatRow.tsx`
## Modifying System Prompt
**Read these first:** `src/core/prompts/system-prompt/README.md`, `tools/README.md`, `__tests__/README.md`
System prompt is modular: **components** (reusable sections) + **variants** (model-specific configs) + **templates** (with `{{PLACEHOLDER}}` resolution).
**Key directories:**
- `components/` - Shared sections: `rules.ts`, `capabilities.ts`, `editing_files.ts`, etc.
- `variants/` - Model-specific: `generic/`, `next-gen/`, `xs/`, `gpt-5/`, `gemini-3/`, `hermes/`, `glm/`, etc.
- `templates/` - Template engine and placeholder definitions
**Variant tiers (ask user which to modify):**
- **Next-gen** (Claude 4, GPT-5, Gemini 2.5): `next-gen/`, `native-next-gen/`, `native-gpt-5/`, `native-gpt-5-1/`, `gemini-3/`, `gpt-5/`
- **Standard** (default fallback): `generic/`
- **Local/small models**: `xs/`, `hermes/`, `glm/`
**How overrides work:** Variants can override components via `componentOverrides` in their `config.ts`, or provide a custom template in `template.ts` (e.g., `next-gen/template.ts` exports `rules_template`). If no override, the shared component from `components/` is used.
**Example: Adding a rule to RULES section**
1. Check if variant overrides rules: look for `rules_template` in `variants/*/template.ts` or `componentOverrides.RULES` in `config.ts`
2. If shared: modify `components/rules.ts`
3. If overridden: modify that variant's template
4. XS variant is special—has heavily condensed inline content in `template.ts`
**After any changes, regenerate snapshots:**
```bash
UPDATE_SNAPSHOTS=true npm run test:unit
```
Snapshots live in `__tests__/__snapshots__/`. Tests validate across model families and context variations (browser, MCP, focus chain).
## Modifying Default Slash Commands
Three places need updates:
- `src/core/slash-commands/index.ts` - Command definitions
- `src/core/prompts/commands.ts` - System prompt integration
- `webview-ui/src/utils/slash-commands.ts` - Webview autocomplete
## ChatRow Cancelled/Interrupted States
When a ChatRow displays a loading/in-progress state (spinner), you must handle what happens when the task is cancelled. This is non-obvious because cancellation doesn't update the message content—you have to infer it from context.
**The pattern:**
1. A message has a `status` field (e.g., `"generating"`, `"complete"`, `"error"`) stored in `message.text` as JSON
2. When cancelled mid-operation, the status stays `"generating"` forever—no one updates it
3. To detect cancellation, check TWO conditions:
- `!isLast` — if this message is no longer the last message, something else happened after it (interrupted)
- `lastModifiedMessage?.ask === "resume_task" || "resume_completed_task"` — task was just cancelled and is waiting to resume
**Example from `generate_explanation`:**
```tsx
const wasCancelled =
explanationInfo.status === "generating" &&
(!isLast ||
lastModifiedMessage?.ask === "resume_task" ||
lastModifiedMessage?.ask === "resume_completed_task")
const isGenerating = explanationInfo.status === "generating" && !wasCancelled
```
**Why both checks?**
- `!isLast` catches: cancelled → resumed → did other stuff → this old message is stale
- `lastModifiedMessage?.ask === "resume_task"` catches: just cancelled, hasn't resumed yet, this message is still technically "last"
**See also:** `BrowserSessionRow.tsx` uses similar pattern with `isLastApiReqInterrupted` and `isLastMessageResume`.
**Backend side:** When streaming is cancelled, clean up properly (close tabs, clear comments, etc.) by checking `taskState.abort` after the streaming function returns.
@.clinerules/general.md
@.clinerules/network.md
+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

+23
View File
@@ -147,6 +147,29 @@
"src/dev/grit/vscode-api.grit"
]
},
{
// Do not use console logging directly, use the Logger service instead.
"plugins": [
"src/dev/grit/console-log.grit"
],
"includes": [
"**",
"!**/webview-ui/**",
"!**/evals/**",
"!**/standalone/**",
"!**/e2e/**",
"!**/test/**",
"!**/__tests__/**",
"!**/*.test.ts",
"!**/*.stories.ts",
"!src/dev/**",
"!**/*.mjs",
"!**/*.js",
"!**/scripts/**",
"!**/*.tsx",
"!**/testing-platform/**"
]
},
{
"includes": [
"**",
-183
View File
@@ -1,183 +0,0 @@
# Cline CLI (TypeScript)
A TypeScript CLI implementation of Cline that reuses the core TypeScript codebase. This allows you to run Cline tasks directly from the terminal while sharing 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
## Prerequisites
- Node.js 20.x or later
- npm or yarn
- The parent Cline project dependencies installed
## Installation
From the repository root:
```bash
# Install root dependencies first
npm install
# Build the CLI
npm run compile-cli-ts
```
Or install the CLI globally:
```bash
cd cli-ts
npm install
npm run build
npm link
```
## Usage
### Run a Task
```bash
# Run a task with a prompt
cline-ts task "Create a hello world function in Python"
# Or use the shorthand
cline-ts t "Create a hello world function"
# Run directly without the 'task' command
cline-ts "Create a hello world function"
```
### Options
```bash
# Show verbose output (including reasoning)
cline-ts task -v "Your prompt"
# Specify working directory
cline-ts task -c /path/to/project "Your prompt"
# Use custom config directory
cline-ts task --config ~/.my-cline "Your prompt"
```
### View Task History
```bash
# List recent tasks
cline-ts history
# Show more tasks
cline-ts history -n 20
```
### Show Configuration
```bash
cline-ts config
```
## Development
```bash
# Build and link the package to your terminal
npm run link
# Set your provider (No Cline provider support yet)
clinedev auth
# Run a task
clinedev task "Tell me about this codebase"
```
### Build
```bash
# Development build with source maps
npm run build
# Production build (minified)
npm run build:production
```
### Watch Mode
```bash
npm run watch
```
### Type Checking
```bash
npm run typecheck
```
## Architecture
The CLI reuses the core Cline TypeScript codebase:
- **Controller** (`@core/controller`): Manages task lifecycle and state
- **Task** (`@core/task`): Executes Cline tasks using the AI API
- **StateManager** (`@core/storage`): Handles persistent state storage
CLI-specific implementations:
- `cli-host-bridge.ts`: CLI implementations of host bridge services
- `cli-webview-provider.ts`: WebviewProvider that outputs to terminal
- `cli-diff-provider.ts`: DiffViewProvider for terminal diff display
- `vscode-context.ts`: Mock VSCode extension context
- `display.ts`: Terminal output formatting utilities
## 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.
## Comparison with Go CLI
This TypeScript CLI differs from the Go CLI (`cli/` directory):
| Feature | Go CLI | TypeScript CLI |
|---------|--------|----------------|
| Language | Go | TypeScript |
| Core sharing | Uses gRPC to communicate | Direct imports |
| Startup time | Fast | Moderate |
| Dependencies | Standalone binary | Requires Node.js |
| Best for | Production deployment | Development, debugging |
Choose the TypeScript CLI when you need to debug or modify the core Cline logic. Choose the Go CLI for production deployment with faster startup.
## Troubleshooting
### Build Errors
If you encounter build errors, ensure you've:
1. Run `npm install` in the repository root
2. Run `npm run protos` to generate proto files
3. Have all peer dependencies installed
### Missing Dependencies
The CLI imports from the parent project. If you see import errors:
```bash
cd .. # Go to repository root
npm install
npm run protos
```
### Permission Denied
Make the CLI executable:
```bash
chmod +x dist/cli.js
```
-214
View File
@@ -1,214 +0,0 @@
import fs from "node:fs"
import path from "node:path"
import { fileURLToPath } from "node:url"
import * as esbuild from "esbuild"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const rootDir = path.resolve(__dirname, "..")
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
/**
* Plugin to resolve path aliases from the parent project
* @type {import('esbuild').Plugin}
*/
const aliasResolverPlugin = {
name: "alias-resolver",
setup(build) {
const aliases = {
"@": path.resolve(rootDir, "src"),
"@core": path.resolve(rootDir, "src/core"),
"@integrations": path.resolve(rootDir, "src/integrations"),
"@services": path.resolve(rootDir, "src/services"),
"@shared": path.resolve(rootDir, "src/shared"),
"@utils": path.resolve(rootDir, "src/utils"),
"@packages": path.resolve(rootDir, "src/packages"),
"@hosts": path.resolve(rootDir, "src/hosts"),
"@generated": path.resolve(rootDir, "src/generated"),
"@api": path.resolve(rootDir, "src/core/api"),
}
// For each alias entry, create a resolver
Object.entries(aliases).forEach(([alias, aliasPath]) => {
const aliasRegex = new RegExp(`^${alias}($|/.*)`)
build.onResolve({ filter: aliasRegex }, (args) => {
const importPath = args.path.replace(alias, aliasPath)
// First, check if the path exists as is
if (fs.existsSync(importPath)) {
const stats = fs.statSync(importPath)
if (stats.isDirectory()) {
// If it's a directory, try to find index files
const extensions = [".ts", ".tsx", ".js", ".jsx"]
for (const ext of extensions) {
const indexFile = path.join(importPath, `index${ext}`)
if (fs.existsSync(indexFile)) {
return { path: indexFile }
}
}
} else {
// It's a file that exists, so return it
return { path: importPath }
}
}
// If the path doesn't exist, try appending extensions
const extensions = [".ts", ".tsx", ".js", ".jsx"]
for (const ext of extensions) {
const pathWithExtension = `${importPath}${ext}`
if (fs.existsSync(pathWithExtension)) {
return { path: pathWithExtension }
}
}
// If nothing worked, return the original path and let esbuild handle the error
return { path: importPath }
})
})
},
}
/**
* Plugin to redirect vscode imports to our shim
* @type {import('esbuild').Plugin}
*/
const vscodeStubPlugin = {
name: "vscode-stub",
setup(build) {
// Redirect 'vscode' imports to our shim
build.onResolve({ filter: /^vscode$/ }, (args) => {
return { path: path.join(__dirname, "src", "vscode-shim.ts") }
})
},
}
const esbuildProblemMatcherPlugin = {
name: "esbuild-problem-matcher",
setup(build) {
build.onStart(() => {
console.log("[cli-ts] Build started...")
})
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`)
if (location) {
console.error(` ${location.file}:${location.line}:${location.column}:`)
}
})
console.log("[cli-ts] Build finished")
})
},
}
const copyWasmFiles = {
name: "copy-wasm-files",
setup(build) {
build.onEnd(() => {
const destDir = path.join(__dirname, "dist")
// Ensure dist directory exists
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true })
}
// tree sitter
const sourceDir = path.join(rootDir, "node_modules", "web-tree-sitter")
// Copy tree-sitter.wasm
const treeSitterWasm = path.join(sourceDir, "tree-sitter.wasm")
if (fs.existsSync(treeSitterWasm)) {
fs.copyFileSync(treeSitterWasm, path.join(destDir, "tree-sitter.wasm"))
}
// Copy language-specific WASM files
const languageWasmDir = path.join(rootDir, "node_modules", "tree-sitter-wasms", "out")
const languages = [
"typescript",
"tsx",
"python",
"rust",
"javascript",
"go",
"cpp",
"c",
"c_sharp",
"ruby",
"java",
"php",
"swift",
"kotlin",
]
if (fs.existsSync(languageWasmDir)) {
languages.forEach((lang) => {
const filename = `tree-sitter-${lang}.wasm`
const sourcePath = path.join(languageWasmDir, filename)
if (fs.existsSync(sourcePath)) {
fs.copyFileSync(sourcePath, path.join(destDir, filename))
}
})
}
})
},
}
const buildEnvVars = {
"import.meta.url": "_importMetaUrl",
"process.env.IS_STANDALONE": JSON.stringify("true"),
"process.env.IS_CLI": JSON.stringify("true"),
}
if (production) {
buildEnvVars["process.env.IS_DEV"] = "false"
}
// Set the environment
if (process.env.CLINE_ENVIRONMENT) {
buildEnvVars["process.env.CLINE_ENVIRONMENT"] = JSON.stringify(process.env.CLINE_ENVIRONMENT)
}
const config = {
entryPoints: [path.join(__dirname, "src", "index.ts")],
bundle: true,
minify: production,
sourcemap: !production,
logLevel: "silent",
define: buildEnvVars,
tsconfig: path.join(__dirname, "tsconfig.json"),
plugins: [copyWasmFiles, aliasResolverPlugin, vscodeStubPlugin, esbuildProblemMatcherPlugin],
format: "cjs",
sourcesContent: false,
platform: "node",
target: "node20",
outfile: path.join(__dirname, "dist", "cli.cjs"),
// These modules need to load files from the module directory at runtime
external: ["@grpc/reflection", "grpc-health-check", "better-sqlite3"],
banner: {
js: `#!/usr/bin/env node
const _importMetaUrl=require('url').pathToFileURL(__filename)`,
},
}
async function main() {
const ctx = await esbuild.context(config)
if (watch) {
await ctx.watch()
console.log("[cli-ts] Watching for changes...")
} else {
await ctx.rebuild()
await ctx.dispose()
// Make the output executable
const outfile = path.join(__dirname, "dist", "cli.cjs")
if (fs.existsSync(outfile)) {
fs.chmodSync(outfile, "755")
}
}
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
-1449
View File
File diff suppressed because it is too large Load Diff
-42
View File
@@ -1,42 +0,0 @@
{
"name": "@cline/cli",
"version": "1.0.0",
"description": "Cline CLI - TypeScript implementation that reuses core Cline functionality",
"main": "dist/cli.cjs",
"bin": {
"clinedev": "./dist/cli.cjs"
},
"type": "module",
"scripts": {
"build": "node esbuild.mjs",
"build:production": "node esbuild.mjs --production",
"watch": "node esbuild.mjs --watch",
"dev": "npm run watch",
"clean": "rimraf dist",
"typecheck": "tsc --noEmit",
"link": "npm run build && npm link"
},
"keywords": [
"cline",
"cli",
"ai",
"coding-assistant"
],
"author": "Cline Bot Inc.",
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "20.x",
"@types/prompts": "^2.4.9",
"esbuild": "^0.25.0",
"rimraf": "^6.0.1",
"typescript": "^5.4.5"
},
"dependencies": {
"chalk": "^5.3.0",
"commander": "^12.1.0",
"ora": "^8.0.1",
"ink": "^5.0.1",
"ink-spinner": "^5.0.0",
"prompts": "^2.4.2"
}
}
-337
View File
@@ -1,337 +0,0 @@
/**
* CLI Authentication Handler
*
* Provides interactive and quick-setup authentication modes for the Cline CLI.
* Supports both interactive menus and command-line flag-based configuration.
*/
import prompts from "prompts"
import { StateManager } from "@/core/storage/StateManager"
import { AuthHandler } from "@/hosts/external/AuthHandler"
import { HostProvider } from "@/hosts/host-provider"
import { FileEditProvider } from "@/integrations/editor/FileEditProvider"
import { StandaloneTerminalManager } from "@/integrations/terminal/standalone/StandaloneTerminalManager"
import { AuthService } from "@/services/auth/AuthService"
import { ErrorService } from "@/services/error/ErrorService"
import { initializeDistinctId } from "@/services/logging/distinctId"
import { API_PROVIDERS_LIST } from "@/shared/api"
import { createCliHostBridgeProvider } from "./cli-host-bridge"
import { CliWebviewProvider } from "./cli-webview-provider"
import { print, printError, printInfo, printSuccess, separator } from "./display"
import { initializeCliContext } from "./vscode-context"
/**
* Options for the auth command
*/
export interface AuthOptions {
provider?: string
apikey?: string
modelid?: string
baseurl?: string
verbose?: boolean
cwd?: string
config?: string
}
/**
* Run authentication flow
* Routes to either interactive mode or quick setup based on provided flags
*/
export async function runAuth(options: AuthOptions): Promise<void> {
try {
// Initialize services
const { extensionContext, EXTENSION_DIR, DATA_DIR } = initializeCliContext({
clineDir: options.config,
workspaceDir: options.cwd || process.cwd(),
})
await ErrorService.initialize()
// Setup minimal host provider
setupHostProvider(extensionContext, EXTENSION_DIR, DATA_DIR, options.cwd || process.cwd())
// Initialize state manager
await StateManager.initialize(extensionContext)
// Create a webview provider to get a controller instance for auth operations
const webview = HostProvider.get().createWebviewProvider() as CliWebviewProvider
const controller = webview.controller
// Initialize telemetry distinct ID (needed by various services)
await initializeDistinctId(extensionContext)
// Initialize AuthService with the controller
AuthService.getInstance(controller)
// Check if flags are provided for quick setup
if (options.provider || options.apikey || options.modelid || options.baseurl) {
await handleQuickSetup(options)
} else {
// No flags - show interactive menu
await handleInteractiveAuth()
}
} catch (error) {
printError(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
}
}
/**
* Setup the host provider for CLI mode
*/
function setupHostProvider(extensionContext: any, extensionDir: string, dataDir: string, workspacePath: string) {
AuthHandler.getInstance().setEnabled(true)
const createWebview = () => new CliWebviewProvider(extensionContext)
const createDiffView = () => new FileEditProvider()
const createCommentReview = () => {
throw new Error("CommentReview not available in auth mode")
}
const createTerminalManager = () => new StandaloneTerminalManager()
const getCallbackUrl = async (): Promise<string> => {
const url = await AuthHandler.getInstance().getCallbackUrl()
return url
}
const getBinaryLocation = async (name: string): Promise<string> => {
const path = await import("path")
return path.join(process.cwd(), name)
}
const logToChannel = (_message: string) => {
// Silent in auth mode
}
HostProvider.initialize(
createWebview,
createDiffView,
createCommentReview,
createTerminalManager,
createCliHostBridgeProvider(workspacePath),
logToChannel,
getCallbackUrl,
getBinaryLocation,
extensionDir,
dataDir,
)
}
/**
* Handle quick setup mode using command-line flags
*/
async function handleQuickSetup(options: AuthOptions): Promise<void> {
// Validate required parameters
if (!options.provider || !options.apikey || !options.modelid) {
printError("Quick setup requires --provider, --apikey, and --modelid flags")
printInfo("Usage: cline auth --provider <provider> --apikey <key> --modelid <model> [--baseurl <url>]")
printInfo("\nExamples:")
printInfo(" cline auth --provider openai-native --apikey sk-xxx --modelid gpt-5")
printInfo(" cline auth -p anthropic -k sk-ant-xxx -m claude-sonnet-4-5-20250929")
printInfo(" cline auth -p openai-compatible -k xxx -m gpt-4 -b https://api.example.com/v1")
process.exit(1)
}
printInfo("🔐 Configuring provider...")
print(separator())
try {
const normalizedProvider = options.provider.toLowerCase().trim()
// Sort by alphabetical order for display
const sortedProviders = API_PROVIDERS_LIST.slice().sort()
if (!sortedProviders.includes(normalizedProvider)) {
throw new Error(`Invalid provider '${options.provider}'. Supported providers: ${sortedProviders.join(", ")}`)
}
// Check for bedrock
if (normalizedProvider === "bedrock") {
throw new Error(
"Bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup: cline auth",
)
}
// Validate baseurl is only for OpenAI
if (options.baseurl && !["openai", "openai-native"].includes(normalizedProvider)) {
throw new Error("Base URL is only supported for OpenAI and OpenAI-compatible providers")
}
// Save configuration to StateManager
const stateManager = StateManager.get()
const config: Record<string, string> = {
actModeApiProvider: normalizedProvider,
planModeApiProvider: normalizedProvider,
actModeApiModelId: options.modelid,
planModeApiModelId: options.modelid,
apiKey: options.apikey,
}
if (options.baseurl) {
config.openAiBaseUrl = options.baseurl
}
await stateManager.setApiConfiguration(config)
printSuccess(`Provider: ${normalizedProvider}`)
printSuccess(`Model: ${options.modelid}`)
if (options.baseurl) {
printSuccess(`Base URL: ${options.baseurl}`)
}
printSuccess("API Key: Configured")
print(separator())
printSuccess("✓ Successfully configured authentication")
printInfo("You can now use Cline with this provider.")
printInfo("Run 'cline task \"<your prompt>\"' to begin a new task.")
} catch (error) {
printError(`Configuration failed: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
}
}
/**
* Handle interactive authentication menu
*/
async function handleInteractiveAuth(): Promise<void> {
try {
printInfo("🔐 Cline Authentication Menu")
print(separator())
printInfo("")
// Show main menu
const mainMenuResponse = await prompts({
type: "select",
name: "action",
message: "What would you like to do?",
choices: [
{ title: "Sign in to Cline", value: "cline_auth" },
{ title: "Configure BYO API provider", value: "configure_byo" },
{ title: "Exit", value: "exit" },
],
})
if (mainMenuResponse.action === "exit" || mainMenuResponse.action === undefined) {
printInfo("Exiting authentication wizard.")
return
}
if (mainMenuResponse.action === "cline_auth") {
await AuthService.getInstance().createAuthRequest()
}
if (mainMenuResponse.action === "configure_byo") {
await handleProviderSetupInteractive()
}
} catch (error) {
printError(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
}
}
/**
* Handle interactive BYO provider setup
*/
async function handleProviderSetupInteractive(): Promise<void> {
// Get current configuration to show which providers are already configured
const stateManager = StateManager.get()
const currentConfig = stateManager.getApiConfiguration()
const currentProvider = currentConfig.actModeApiProvider || currentConfig.planModeApiProvider
// Sort by alphabetical order for display
const sortedProviders = API_PROVIDERS_LIST.slice().sort()
const providerResponse = await prompts({
type: "select",
name: "provider",
message: "Select a provider:",
choices: sortedProviders.map((p) => ({
title: `${capitalize(p)}${currentProvider === p ? " (configured)" : ""}`,
value: p,
})),
})
if (providerResponse.provider === undefined) {
printInfo("Provider setup cancelled.")
return
}
let apiKey: string | undefined
if (providerResponse.provider === "cline") {
AuthService.getInstance().createAuthRequest()
} else {
const apiKeyResponse = await prompts({
type: "password",
name: "apikey",
message: "Enter your API key:",
})
if (apiKeyResponse.apikey === undefined) {
printInfo("Provider setup cancelled.")
return
}
apiKey = apiKeyResponse.apikey
}
const modelResponse = await prompts({
type: "text",
name: "modelid",
message: "Enter the model ID (e.g., gpt-4, claude-sonnet-4.5):",
})
if (modelResponse.modelid === undefined) {
printInfo("Provider setup cancelled.")
return
}
let baseUrl = ""
if (["openai", "openai-native"].includes(providerResponse.provider)) {
const baseUrlResponse = await prompts({
type: "text",
name: "baseurl",
message: "Enter base URL (optional, press Enter to skip):",
initial: "",
})
baseUrl = baseUrlResponse.baseurl || ""
}
// Save configuration to StateManager
try {
const stateManager = StateManager.get()
const config: Record<string, string> = {
actModeApiProvider: providerResponse.provider,
planModeApiProvider: providerResponse.provider,
actModeApiModelId: modelResponse.modelid,
planModeApiModelId: modelResponse.modelid,
}
if (apiKey) {
config.apiKey = apiKey
}
if (baseUrl) {
config.openAiBaseUrl = baseUrl
}
stateManager.setApiConfiguration(config)
print(separator())
printSuccess(`✓ Provider configured successfully`)
printInfo(`Provider: ${capitalize(providerResponse.provider)}`)
printInfo(`Model: ${modelResponse.modelid}`)
if (baseUrl) {
printInfo(`Base URL: ${baseUrl}`)
}
print(separator())
printInfo("You can now use Cline with this provider.")
printInfo("Run 'cline task \"<your prompt>\"' to begin a new task.")
} catch (error) {
printError(`Failed to save configuration: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
}
}
function capitalize(str: string): string {
return str
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
}
-38
View File
@@ -1,38 +0,0 @@
/**
* CLI-specific CommentReviewController implementation
* Handles code review comments in CLI mode
*/
import { CommentReviewController } from "@/integrations/editor/CommentReviewController"
import { print, style } from "./display"
export class CliCommentReviewController extends CommentReviewController {
private comments: Map<string, string[]> = new Map()
override async addComment(filePath: string, line: number, comment: string): Promise<void> {
const key = `${filePath}:${line}`
const existing = this.comments.get(key) || []
existing.push(comment)
this.comments.set(key, existing)
print(style.info(`💬 Comment on ${filePath}:${line}`))
print(style.dim(` ${comment}`))
}
override async clearComments(filePath?: string): Promise<void> {
if (filePath) {
// Clear comments for specific file
for (const key of this.comments.keys()) {
if (key.startsWith(filePath)) {
this.comments.delete(key)
}
}
} else {
this.comments.clear()
}
}
override dispose(): void {
this.comments.clear()
}
}
-259
View File
@@ -1,259 +0,0 @@
/**
* CLI-specific Host Bridge implementations
* These provide stub implementations for the host bridge interfaces that work in CLI mode
*/
import type {
DiffServiceClientInterface,
EnvServiceClientInterface,
WindowServiceClientInterface,
WorkspaceServiceClientInterface,
} from "@generated/hosts/host-bridge-client-types"
import type { HostBridgeClientProvider, StreamingCallbacks } from "@hosts/host-provider-types"
import * as proto from "@shared/proto/index"
import { printError, printInfo, printWarning } from "./display"
/**
* CLI implementation of DiffService - handles diff operations for terminal
*/
export class CliDiffServiceClient implements DiffServiceClientInterface {
async openDiff(request: proto.host.OpenDiffRequest): Promise<proto.host.OpenDiffResponse> {
printInfo(`📝 Opening diff for: ${request.leftUri || request.rightUri}`)
return proto.host.OpenDiffResponse.create({})
}
async getDocumentText(request: proto.host.GetDocumentTextRequest): Promise<proto.host.GetDocumentTextResponse> {
// In CLI mode, we'd read from the file system directly
return proto.host.GetDocumentTextResponse.create({ text: "" })
}
async replaceText(request: proto.host.ReplaceTextRequest): Promise<proto.host.ReplaceTextResponse> {
printInfo(`✏️ Replacing text in document`)
return proto.host.ReplaceTextResponse.create({})
}
async scrollDiff(request: proto.host.ScrollDiffRequest): Promise<proto.host.ScrollDiffResponse> {
// No-op in CLI
return proto.host.ScrollDiffResponse.create({})
}
async truncateDocument(request: proto.host.TruncateDocumentRequest): Promise<proto.host.TruncateDocumentResponse> {
return proto.host.TruncateDocumentResponse.create({})
}
async saveDocument(request: proto.host.SaveDocumentRequest): Promise<proto.host.SaveDocumentResponse> {
printInfo(`💾 Saving document`)
return proto.host.SaveDocumentResponse.create({})
}
async closeAllDiffs(request: proto.host.CloseAllDiffsRequest): Promise<proto.host.CloseAllDiffsResponse> {
return proto.host.CloseAllDiffsResponse.create({})
}
async openMultiFileDiff(request: proto.host.OpenMultiFileDiffRequest): Promise<proto.host.OpenMultiFileDiffResponse> {
printInfo(`📝 Opening multi-file diff`)
return proto.host.OpenMultiFileDiffResponse.create({})
}
}
/**
* CLI implementation of EnvService - handles environment operations
*/
export class CliEnvServiceClient implements EnvServiceClientInterface {
private clipboardContent: string = ""
async clipboardWriteText(request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
this.clipboardContent = request.value || ""
printInfo(`📋 Copied to clipboard`)
return proto.cline.Empty.create()
}
async clipboardReadText(request: proto.cline.EmptyRequest): Promise<proto.cline.String> {
return proto.cline.String.create({ value: this.clipboardContent })
}
async getHostVersion(request: proto.cline.EmptyRequest): Promise<proto.host.GetHostVersionResponse> {
return proto.host.GetHostVersionResponse.create({
version: "1.0.0",
platform: "Cline CLI",
})
}
async getIdeRedirectUri(request: proto.cline.EmptyRequest): Promise<proto.cline.String> {
// CLI doesn't have IDE redirect
return proto.cline.String.create({ value: "" })
}
async getTelemetrySettings(request: proto.cline.EmptyRequest): Promise<proto.host.GetTelemetrySettingsResponse> {
return proto.host.GetTelemetrySettingsResponse.create({
isTelemetryEnabled: false,
isCrashReporterEnabled: false,
})
}
subscribeToTelemetrySettings(
request: proto.cline.EmptyRequest,
callbacks: StreamingCallbacks<proto.host.TelemetrySettingsEvent>,
): () => void {
// Send initial settings
callbacks.onResponse(
proto.host.TelemetrySettingsEvent.create({
isTelemetryEnabled: false,
isCrashReporterEnabled: false,
}),
)
// Return unsubscribe function
return () => {}
}
async shutdown(request: proto.cline.EmptyRequest): Promise<proto.cline.Empty> {
printInfo("Shutting down...")
return proto.cline.Empty.create()
}
}
/**
* CLI implementation of WindowService - handles window/UI operations
*/
export class CliWindowServiceClient implements WindowServiceClientInterface {
async showTextDocument(request: proto.host.ShowTextDocumentRequest): Promise<proto.host.TextEditorInfo> {
printInfo(`📄 Opening file: ${request.path}`)
return proto.host.TextEditorInfo.create({
path: request.path,
})
}
async showOpenDialogue(request: proto.host.ShowOpenDialogueRequest): Promise<proto.host.SelectedResources> {
printWarning("Open dialog not available in CLI mode")
return proto.host.SelectedResources.create({ uris: [] })
}
async showMessage(request: proto.host.ShowMessageRequest): Promise<proto.host.SelectedResponse> {
const message = request.message || ""
const type = request.type
switch (type) {
case proto.host.ShowMessageType.ERROR:
printError(message)
break
case proto.host.ShowMessageType.WARNING:
printWarning(message)
break
case proto.host.ShowMessageType.INFORMATION:
default:
printInfo(message)
break
}
return proto.host.SelectedResponse.create({})
}
async showInputBox(request: proto.host.ShowInputBoxRequest): Promise<proto.host.ShowInputBoxResponse> {
// In CLI mode, we could use readline, but for now return empty
printWarning("Input box not available in CLI mode")
return proto.host.ShowInputBoxResponse.create({ value: "" })
}
async showSaveDialog(request: proto.host.ShowSaveDialogRequest): Promise<proto.host.ShowSaveDialogResponse> {
printWarning("Save dialog not available in CLI mode")
return proto.host.ShowSaveDialogResponse.create({ uri: "" })
}
async openFile(request: proto.host.OpenFileRequest): Promise<proto.host.OpenFileResponse> {
printInfo(`📂 Opening: ${request.path}`)
return proto.host.OpenFileResponse.create({})
}
async openSettings(request: proto.host.OpenSettingsRequest): Promise<proto.host.OpenSettingsResponse> {
printInfo("Settings can be configured in ~/.cline/data/globalState.json")
return proto.host.OpenSettingsResponse.create({})
}
async getOpenTabs(request: proto.host.GetOpenTabsRequest): Promise<proto.host.GetOpenTabsResponse> {
// CLI doesn't have tabs
return proto.host.GetOpenTabsResponse.create({ tabs: [] })
}
async getVisibleTabs(request: proto.host.GetVisibleTabsRequest): Promise<proto.host.GetVisibleTabsResponse> {
return proto.host.GetVisibleTabsResponse.create({ tabs: [] })
}
async getActiveEditor(request: proto.host.GetActiveEditorRequest): Promise<proto.host.GetActiveEditorResponse> {
return proto.host.GetActiveEditorResponse.create({})
}
}
/**
* CLI implementation of WorkspaceService - handles workspace operations
*/
export class CliWorkspaceServiceClient implements WorkspaceServiceClientInterface {
private workspacePath: string
constructor(workspacePath: string = process.cwd()) {
this.workspacePath = workspacePath
}
setWorkspacePath(path: string) {
this.workspacePath = path
}
async getWorkspacePaths(request: proto.host.GetWorkspacePathsRequest): Promise<proto.host.GetWorkspacePathsResponse> {
return proto.host.GetWorkspacePathsResponse.create({
paths: [this.workspacePath],
})
}
async saveOpenDocumentIfDirty(
request: proto.host.SaveOpenDocumentIfDirtyRequest,
): Promise<proto.host.SaveOpenDocumentIfDirtyResponse> {
return proto.host.SaveOpenDocumentIfDirtyResponse.create({})
}
async getDiagnostics(request: proto.host.GetDiagnosticsRequest): Promise<proto.host.GetDiagnosticsResponse> {
// In CLI mode, we could run linters here
return proto.host.GetDiagnosticsResponse.create({ diagnostics: [] })
}
async openProblemsPanel(request: proto.host.OpenProblemsPanelRequest): Promise<proto.host.OpenProblemsPanelResponse> {
printInfo("Run linters to see problems")
return proto.host.OpenProblemsPanelResponse.create({})
}
async openInFileExplorerPanel(
request: proto.host.OpenInFileExplorerPanelRequest,
): Promise<proto.host.OpenInFileExplorerPanelResponse> {
printInfo(`📁 ${request.path}`)
return proto.host.OpenInFileExplorerPanelResponse.create({})
}
async openClineSidebarPanel(
request: proto.host.OpenClineSidebarPanelRequest,
): Promise<proto.host.OpenClineSidebarPanelResponse> {
// No sidebar in CLI
return proto.host.OpenClineSidebarPanelResponse.create({})
}
async openTerminalPanel(request: proto.host.OpenTerminalRequest): Promise<proto.host.OpenTerminalResponse> {
printInfo("Terminal is already available in CLI mode")
return proto.host.OpenTerminalResponse.create({})
}
async executeCommandInTerminal(
request: proto.host.ExecuteCommandInTerminalRequest,
): Promise<proto.host.ExecuteCommandInTerminalResponse> {
printInfo(`⚙️ Executing: ${request.command}`)
return proto.host.ExecuteCommandInTerminalResponse.create({})
}
}
/**
* Create a CLI host bridge provider
*/
export function createCliHostBridgeProvider(workspacePath?: string): HostBridgeClientProvider {
return {
workspaceClient: new CliWorkspaceServiceClient(workspacePath),
envClient: new CliEnvServiceClient(),
windowClient: new CliWindowServiceClient(),
diffClient: new CliDiffServiceClient(),
}
}
-27
View File
@@ -1,27 +0,0 @@
/**
* CLI-specific WebviewProvider implementation
* Instead of rendering to a webview, this outputs to the terminal
*/
import type * as vscode from "vscode"
import { WebviewProvider } from "@/core/webview"
export class CliWebviewProvider extends WebviewProvider {
constructor(context: vscode.ExtensionContext) {
super(context)
}
override getWebviewUrl(path: string): string {
// CLI doesn't have webview URLs
return `file://${path}`
}
override getCspSource(): string {
return "'self'"
}
override isVisible(): boolean {
// CLI is always "visible"
return true
}
}
-29
View File
@@ -1,29 +0,0 @@
/**
* Console management for CLI
*
* Captures original console methods BEFORE any core modules are imported,
* so CLI output works even when console.log is suppressed.
*/
// Capture original console methods immediately
export const originalConsoleLog = console.log.bind(console)
export const originalConsoleError = console.error.bind(console)
export const originalConsoleWarn = console.warn.bind(console)
// Check for verbose flag early (before commander parses)
const isVerbose = process.argv.includes("-v") || process.argv.includes("--verbose")
// Suppress console output unless verbose mode
if (!isVerbose) {
console.log = () => {}
console.warn = () => {}
}
/**
* Restore original console methods (for cleanup)
*/
export function restoreConsole() {
console.log = originalConsoleLog
console.error = originalConsoleError
console.warn = originalConsoleWarn
}
-460
View File
@@ -1,460 +0,0 @@
/**
* Terminal display utilities for rendering Cline messages in the CLI
*/
import type { ClineAsk, ClineMessage, ClineSay, ExtensionState } from "@shared/ExtensionMessage"
import { originalConsoleError, originalConsoleLog } from "./console"
// ANSI color codes for terminal output
const colors = {
reset: "\x1b[0m",
bold: "\x1b[1m",
dim: "\x1b[2m",
italic: "\x1b[3m",
underline: "\x1b[4m",
// Foreground colors
black: "\x1b[30m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
white: "\x1b[37m",
// Bright foreground colors
brightBlack: "\x1b[90m",
brightRed: "\x1b[91m",
brightGreen: "\x1b[92m",
brightYellow: "\x1b[93m",
brightBlue: "\x1b[94m",
brightMagenta: "\x1b[95m",
brightCyan: "\x1b[96m",
brightWhite: "\x1b[97m",
// Background colors
bgBlack: "\x1b[40m",
bgRed: "\x1b[41m",
bgGreen: "\x1b[42m",
bgYellow: "\x1b[43m",
bgBlue: "\x1b[44m",
bgMagenta: "\x1b[45m",
bgCyan: "\x1b[46m",
bgWhite: "\x1b[47m",
}
export function colorize(text: string, ...colorCodes: string[]): string {
return colorCodes.join("") + text + colors.reset
}
// Helper functions for common color combinations
export const style = {
bold: (text: string) => colorize(text, colors.bold),
dim: (text: string) => colorize(text, colors.dim),
italic: (text: string) => colorize(text, colors.italic),
error: (text: string) => colorize(text, colors.red, colors.bold),
warning: (text: string) => colorize(text, colors.yellow),
success: (text: string) => colorize(text, colors.green),
info: (text: string) => colorize(text, colors.cyan),
// Message type colors
task: (text: string) => colorize(text, colors.brightWhite, colors.bold),
tool: (text: string) => colorize(text, colors.blue),
command: (text: string) => colorize(text, colors.magenta),
api: (text: string) => colorize(text, colors.brightBlack),
user: (text: string) => colorize(text, colors.green),
assistant: (text: string) => colorize(text, colors.cyan),
// Special formatting
path: (text: string) => colorize(text, colors.underline, colors.blue),
code: (text: string) => colorize(text, colors.bgBlack, colors.brightWhite),
}
/**
* Format a timestamp for display
*/
export function formatTimestamp(ts: number): string {
const date = new Date(ts)
return date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})
}
/**
* Get a prefix icon for different message types
*/
function getMessageIcon(message: ClineMessage): string {
if (message.type === "ask") {
switch (message.ask) {
case "followup":
return "❓"
case "command":
case "command_output":
return "⚙️ "
case "tool":
return "🔧"
case "completion_result":
return "✅"
case "api_req_failed":
return "❌"
case "resume_task":
case "resume_completed_task":
return "▶️ "
case "browser_action_launch":
return "🌐"
case "use_mcp_server":
return "🔌"
default:
return "❔"
}
} else {
switch (message.say) {
case "task":
return "📋"
case "error":
return "❌"
case "text":
return "💬"
case "reasoning":
return "🧠"
case "completion_result":
return "✅"
case "user_feedback":
return "👤"
case "command":
case "command_output":
return "⚙️ "
case "tool":
return "🔧"
case "browser_action":
case "browser_action_launch":
case "browser_action_result":
return "🌐"
case "mcp_server_request_started":
case "mcp_server_response":
return "🔌"
case "api_req_started":
case "api_req_finished":
return "🔄"
case "checkpoint_created":
return "💾"
case "info":
return "️ "
default:
return " "
}
}
}
/**
* Format a ClineMessage for terminal display
*/
export function formatMessage(message: ClineMessage, verbose: boolean = false): string {
const icon = getMessageIcon(message)
const timestamp = formatTimestamp(message.ts)
const lines: string[] = []
const prefix = `${style.dim(timestamp)} ${icon}`
if (message.type === "ask") {
lines.push(formatAskMessage(message, prefix, verbose))
} else {
lines.push(formatSayMessage(message, prefix, verbose))
}
return lines.filter(Boolean).join("\n")
}
function formatAskMessage(message: ClineMessage, prefix: string, verbose: boolean): string {
const ask = message.ask as ClineAsk
switch (ask) {
case "followup": {
// Parse JSON question format
let question = message.text || ""
try {
const parsed = JSON.parse(message.text || "{}")
question = parsed.question || question
} catch {
// Fallback to raw text if not JSON
question = message.text || ""
}
return `${prefix} ${style.info("Question:")} ${question}`
}
case "command":
return `${prefix} ${style.command("Execute command?")} ${style.code(message.text || "")}`
case "tool":
return `${prefix} ${style.tool("Use tool?")} ${message.text || ""}`
case "completion_result":
return `${prefix} ${style.success("Task completed")} ${message.text ? `- ${message.text}` : ""}`
case "api_req_failed":
return `${prefix} ${style.error("API request failed")} ${message.text || ""}`
case "resume_task":
case "resume_completed_task":
return `${prefix} ${style.info("Resume task?")} ${message.text || ""}`
case "browser_action_launch":
return `${prefix} ${style.info("Launch browser?")} ${message.text || ""}`
case "use_mcp_server":
return `${prefix} ${style.info("Use MCP server?")} ${message.text || ""}`
case "plan_mode_respond":
return `${prefix} ${style.info("Plan mode response:")} ${message.text || ""}`
default:
return verbose ? `${prefix} [ASK:${ask}] ${message.text || ""}` : ""
}
}
function formatSayMessage(message: ClineMessage, prefix: string, verbose: boolean): string {
const say = message.say as ClineSay
switch (say) {
case "task":
return `${prefix} ${style.task("Task:")} ${message.text || ""}`
case "text":
return `${prefix} ${style.assistant(message.text || "")}`
case "reasoning":
return `${prefix} ${style.dim("Thinking:")} ${style.italic(message.text || "")}`
case "error":
return `${prefix} ${style.error("Error:")} ${message.text || ""}`
case "completion_result":
return `${prefix} ${style.success("✓ Completed:")} ${message.text || ""}`
case "user_feedback":
return `${prefix} ${style.user("User:")} ${message.text || ""}`
case "command":
return `${prefix} ${style.command("Command:")} ${style.code(message.text || "")}`
case "command_output":
const output = message.text || ""
const truncated = output.length > 500 ? output.substring(0, 500) + "..." : output
return `${prefix} ${style.dim("Output:")} ${truncated}`
case "tool":
return `${prefix} ${style.tool("Tool:")} ${message.text || ""}`
case "browser_action":
case "browser_action_launch":
return `${prefix} ${style.info("Browser:")} ${message.text || ""}`
case "browser_action_result":
return `${prefix} ${style.dim("Browser result")} ${message.text ? `- ${message.text.substring(0, 100)}...` : ""}`
case "mcp_server_request_started":
return `${prefix} ${style.info("MCP request started")} ${message.text || ""}`
case "mcp_server_response":
return `${prefix} ${style.info("MCP response")} ${message.text ? message.text.substring(0, 200) : ""}`
case "api_req_started":
return verbose ? `${prefix} ${style.api("API request started")}` : `${message.text || ""}`
case "api_req_finished":
return verbose ? `${prefix} ${style.api("API request finished")}` : ""
case "checkpoint_created":
return `${prefix} ${style.success("Checkpoint created")} ${message.text || ""}`
case "info":
return `${prefix} ${style.info(message.text || "")}`
case "hook_status":
return `${prefix} ${style.dim("Hook:")} ${message.text || ""}`
case "task_progress":
return `${prefix} ${style.info("Progress:")} ${message.text || ""}`
default:
return verbose ? `${prefix} [SAY:${say}] ${message.text || ""}` : ""
}
}
/**
* Display a horizontal separator
*/
export function separator(char: string = "─", width: number = 60): string {
return style.dim(char.repeat(width))
}
/**
* Display the task header
*/
export function taskHeader(taskId: string, task?: string): string {
const lines = [
separator("═"),
style.bold(` Task: ${taskId}`),
task ? ` ${style.dim(task.substring(0, 80))}${task.length > 80 ? "..." : ""}` : "",
separator("═"),
]
return lines.filter(Boolean).join("\n")
}
/**
* Format the current state for display
*/
export function formatState(state: ExtensionState, verbose: boolean = false): string {
const lines: string[] = []
if (state.currentTaskItem) {
lines.push(taskHeader(state.currentTaskItem.id, state.currentTaskItem.task))
}
// Show messages
if (state.clineMessages && state.clineMessages.length > 0) {
const messagesToShow = verbose
? state.clineMessages
: state.clineMessages.filter((m) => {
// Filter out noisy messages in non-verbose mode
// if (m.say === "api_req_started" || m.say === "api_req_finished") return false
return true
})
for (const message of messagesToShow) {
const formatted = formatMessage(message, verbose)
if (formatted) {
lines.push(formatted)
}
}
}
return lines.join("\n")
}
/**
* Display a spinner with message
*/
export class Spinner {
private frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
private frameIndex = 0
private interval: NodeJS.Timeout | null = null
private message: string = ""
start(message: string) {
this.message = message
this.interval = setInterval(() => {
const frame = this.frames[this.frameIndex]
process.stdout.write(`\r${style.info(frame)} ${this.message}`)
this.frameIndex = (this.frameIndex + 1) % this.frames.length
}, 80)
}
update(message: string) {
this.message = message
}
stop(finalMessage?: string) {
if (this.interval) {
clearInterval(this.interval)
this.interval = null
}
if (finalMessage) {
process.stdout.write(`\r${style.success("✓")} ${finalMessage}\n`)
} else {
process.stdout.write("\r" + " ".repeat(this.message.length + 4) + "\r")
}
}
fail(message?: string) {
if (this.interval) {
clearInterval(this.interval)
this.interval = null
}
if (message) {
process.stdout.write(`\r${style.error("✗")} ${message}\n`)
}
}
}
/**
* Clear the current line
*/
export function clearLine() {
process.stdout.write("\r\x1b[K")
}
/**
* Move cursor up n lines
*/
export function cursorUp(n: number = 1) {
process.stdout.write(`\x1b[${n}A`)
}
/**
* Print a message to stdout with newline
* Uses original console.log to work even when console is suppressed
*/
export function print(message: string) {
originalConsoleLog(message)
}
/**
* Print an error message to stderr
* Uses original console.error to work even when console is suppressed
*/
export function printError(message: string) {
originalConsoleError(style.error(message))
}
/**
* Print a success message
*/
export function printSuccess(message: string) {
originalConsoleLog(style.success(message))
}
/**
* Print an info message
*/
export function printInfo(message: string) {
originalConsoleLog(style.info(message))
}
/**
* Print a warning message
*/
export function printWarning(message: string) {
originalConsoleLog(style.warning(message))
}
/**
* Prompt user for input from stdin
*/
export async function promptUser(question: string): Promise<string> {
const readline = await import("readline")
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
return new Promise((resolve) => {
rl.question(style.info(question) + " ", (answer: string) => {
rl.close()
resolve(answer.trim())
})
})
}
/**
* Prompt user for yes/no confirmation
*/
export async function promptConfirmation(question: string): Promise<boolean> {
const answer = await promptUser(`${question} ${style.dim("(y/n)")}`)
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes"
}
-690
View File
@@ -1,690 +0,0 @@
/**
* Cline CLI - TypeScript implementation
*
* A command-line interface for Cline that reuses the core TypeScript codebase,
* allowing you to run Cline tasks directly from the terminal.
*/
import type { ClineAsk, ClineMessage, ExtensionState } from "@shared/ExtensionMessage"
import { Command } from "commander"
import { StateManager } from "@/core/storage/StateManager"
import { HostProvider } from "@/hosts/host-provider"
import { FileEditProvider } from "@/integrations/editor/FileEditProvider"
import { StandaloneTerminalManager } from "@/integrations/terminal/standalone/StandaloneTerminalManager"
import { BannerService } from "@/services/banner/BannerService"
import { ErrorService } from "@/services/error/ErrorService"
import { initializeDistinctId } from "@/services/logging/distinctId"
import { runAuth } from "./cli-auth"
import { CliCommentReviewController } from "./cli-comment-review"
import { createCliHostBridgeProvider } from "./cli-host-bridge"
import { CliWebviewProvider } from "./cli-webview-provider"
// IMPORTANT: Import console module FIRST - it suppresses console.log before core imports
import { restoreConsole } from "./console"
import { print, printError, printInfo, printSuccess, promptConfirmation, promptUser, Spinner, separator, style } from "./display"
import { jsonParseSafe } from "./utils"
import { initializeCliContext } from "./vscode-context"
// Version from package.json
const VERSION = "0.0.0"
/**
* Setup the host provider for CLI mode
*/
function setupHostProvider(
extensionContext: any,
extensionDir: string,
dataDir: string,
workspacePath: string,
verbose: boolean = false,
) {
const createWebview = () => new CliWebviewProvider(extensionContext)
const createDiffView = () => new FileEditProvider()
const createCommentReview = () => new CliCommentReviewController()
const createTerminalManager = () => new StandaloneTerminalManager()
const getCallbackUrl = async (): Promise<string> => {
// CLI doesn't support OAuth callbacks
return ""
}
const getBinaryLocation = async (name: string): Promise<string> => {
const path = await import("path")
return path.join(process.cwd(), name)
}
// Only log in verbose mode to avoid cluttering the CLI output
const logToChannel = verbose
? (message: string) => printInfo(message)
: (_message: string) => {
// Silent in non-verbose mode
}
HostProvider.initialize(
createWebview,
createDiffView,
createCommentReview,
createTerminalManager,
createCliHostBridgeProvider(workspacePath),
logToChannel,
getCallbackUrl,
getBinaryLocation,
extensionDir,
dataDir,
)
}
/**
* State subscriber that streams text updates to the terminal
*/
class CliStateSubscriber {
private lastMessageTexts = new Map<number, string>() // Track message text by index for streaming
private processedAskMessages = new Set<number>() // Track which ask messages we've already prompted for
private processedSayMessages = new Set<number>() // Track which say messages we've already displayed
private verbose: boolean
private spinner: Spinner | null
private controller: any // Reference to the task controller
// private lastStreamedMsgTs = 0
constructor(verbose: boolean = false, controller?: any) {
this.verbose = verbose
this.spinner = null
this.controller = controller
}
onStateUpdate(state: ExtensionState) {
const messages = state.clineMessages || []
// Stream partial text updates only
for (let i = 0; i < messages.length; i++) {
const message = messages[i]
const currentText = message.text || ""
const lastText = this.lastMessageTexts.get(i) || ""
// Handle different message types
if (message.say === "text" && currentText !== lastText) {
// Stream text messages incrementally
const newContent = currentText.slice(lastText.length)
if (newContent) {
process.stdout.write(newContent)
}
this.lastMessageTexts.set(i, currentText)
} else if (message.say !== "text" && currentText !== lastText && lastText === "") {
// New non-text message (hasn't been seen before)
this.displayNewMessage(message)
this.lastMessageTexts.set(i, currentText)
}
if (
message.type === "say" &&
message.say === "tool" &&
message.partial === false &&
!this.processedSayMessages.has(message.ts)
) {
this.processedSayMessages.add(message.ts)
this.displayNewMessage(message)
}
// Check if this is a completed ask message and prompt for input
if (message.type === "ask" && message.partial === false && !this.processedAskMessages.has(i)) {
this.processedAskMessages.add(i)
// Handle ask message asynchronously without blocking state updates
this.handleAskMessage(message).catch((error) => {
if (this.verbose) {
printError(`Error handling ask message: ${error instanceof Error ? error.message : String(error)}`)
}
})
}
}
}
private async handleAskMessage(message: ClineMessage) {
if (!this.controller || !this.controller.task) {
return
}
const ask = message.ask as ClineAsk
try {
switch (ask) {
case "followup":
case "plan_mode_respond":
{
if (message.text) {
const parts = jsonParseSafe(message.text, {
response: undefined as string | undefined,
options: undefined as string[] | undefined,
selected: undefined as string | undefined,
question: undefined as string | undefined,
})
if (parts.response) {
print(style.assistant(`[${message.ask}] ${parts.response}`))
}
// Text input questions
if (parts.question) {
print(style.assistant(`[${message.ask}] ${parts.question}`))
const userText = await promptUser("Reply:")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse("messageResponse", userText)
}
} else if (parts.options && parts.options.length > 0) {
// Multiple choice options
printInfo("Options:")
parts.options.forEach((opt, idx) => {
printInfo(` ${idx + 1}. ${opt}`)
})
const choiceStr = await promptUser("Select an option (number):")
const choiceIdx = parseInt(choiceStr, 10) - 1
if (choiceIdx >= 0 && choiceIdx < parts.options.length) {
const selectedOption = parts.options[choiceIdx]
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse("optionSelected", selectedOption)
}
} else {
printError("Invalid option selected.")
}
}
}
}
break
case "act_mode_respond":
print(style.success(`[${message.ask}] ${message.text || ""}`))
break
case "command":
const approveCmd = await promptConfirmation("Execute this command?")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse(approveCmd ? "yesButtonClicked" : "noButtonClicked")
}
break
case "tool":
const approveTool = await promptConfirmation("Use this tool?")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse(approveTool ? "yesButtonClicked" : "noButtonClicked")
}
break
case "completion_result":
const confirmComplete = await promptConfirmation("Task complete?")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse(
confirmComplete ? "yesButtonClicked" : "noButtonClicked",
)
}
break
case "resume_task":
case "resume_completed_task":
const confirmResume = await promptConfirmation("Resume task?")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse(
confirmResume ? "yesButtonClicked" : "noButtonClicked",
)
}
break
case "browser_action_launch":
const confirmBrowser = await promptConfirmation("Launch browser?")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse(
confirmBrowser ? "yesButtonClicked" : "noButtonClicked",
)
}
break
case "use_mcp_server":
const confirmMcp = await promptConfirmation("Use MCP server?")
if (this.controller?.task) {
await this.controller.task.handleWebviewAskResponse(confirmMcp ? "yesButtonClicked" : "noButtonClicked")
}
break
// Silent asks that don't require user input
case "command_output":
case "api_req_failed":
case "mistake_limit_reached":
// These are informational - no response needed
break
default:
if (this.verbose) {
printInfo(`Ask type "${ask}" requires manual response`)
}
}
} catch (error) {
// Silently ignore errors when controller is being disposed
if (this.controller && this.verbose) {
printError(`Failed to handle user response: ${error instanceof Error ? error.message : String(error)}`)
}
}
}
private displayNewMessage(message: ClineMessage) {
// Stop spinner if running
if (this.spinner) {
const spinner = this.spinner
spinner.stop()
this.spinner = null
}
// Format and display based on message type
if (message.type === "say") {
switch (message.say) {
case "text":
// Text messages are streamed incrementally, so we don't need to display again
// Just ensure the spinner is stopped above
break
case "task":
print(separator("═"))
print(style.task(`📋 Task: ${message.text || ""}`))
print(separator("═"))
break
case "reasoning":
if (this.verbose) {
print(style.dim(`🧠 ${message.text || ""}`))
}
break
case "error":
printError(message.text || "An error occurred")
break
case "completion_result":
print(separator())
printSuccess(`${message.text || "Task completed"}`)
print(separator())
break
case "command":
print(style.command(`⚙️ Command: ${message.text || ""}`))
break
case "command_output":
if (message.text) {
const lines = message.text.split("\n")
const displayLines = lines.slice(0, 10)
for (const line of displayLines) {
print(style.dim(` ${line}`))
}
if (lines.length > 10) {
print(style.dim(` ... and ${lines.length - 10} more lines`))
}
}
break
case "tool":
print(style.tool(`🔧 ${message.text || ""}`))
break
case "api_req_started":
this.spinner = new Spinner()
if (message.text) {
// Parse the JSON API request info and format it nicely
// try {
// const apiInfo = JSON.parse(message.text)
// const details: string[] = []
// if (apiInfo.tokensIn) details.push(`in: ${apiInfo.tokensIn}`)
// if (apiInfo.tokensOut) details.push(`out: ${apiInfo.tokensOut}`)
// if (apiInfo.cost) details.push(`$${apiInfo.cost.toFixed(4)}`)
// const detailStr = details.length > 0 ? ` [${details.join(", ")}]` : ""
// printInfo(`API request${message.text}`)
// } catch {
// // Fallback if JSON parsing fails
// this.spinner.start("Thinking...")
// }
} else {
this.spinner.start("Thinking...")
}
break
case "api_req_finished":
this.spinner = null
break
case "info":
printInfo(message.text || "")
break
case "checkpoint_created":
if (this.verbose) {
print(style.dim(`💾 Checkpoint created`))
}
break
}
} else if (message.type === "ask") {
switch (message.ask) {
case "followup": {
// Parse JSON question format
let question = "Question"
try {
const parsed = JSON.parse(message.text || "{}")
question = parsed.question || question
} catch {
// Fallback to raw text if not JSON
question = message.text || question
}
print(style.info(`${question}`))
break
}
case "command":
print(style.warning(`⚙️ Execute command: ${message.text || ""}`))
break
case "tool":
print(style.info(`🔧 Use tool: ${message.text || ""}`))
break
case "completion_result":
printSuccess(`${message.text || "Task completed"}`)
break
case "api_req_failed":
printError(`${message.text || "API request failed"}`)
break
}
}
}
reset() {
this.lastMessageTexts.clear()
if (this.spinner) {
this.spinner.stop()
this.spinner = null
}
}
}
/**
* Run a task with the given prompt
*/
async function runTask(
prompt: string,
options: { mode?: string; model?: string; verbose?: boolean; cwd?: string; config?: string },
) {
const workspacePath = options.cwd || process.cwd()
if (options.mode) {
StateManager.get().setGlobalState("mode", options.mode === "plan" ? "plan" : "act")
}
if (options.model) {
const selectedMode = StateManager.get().getGlobalSettingsKey("mode") || "act"
StateManager.get().setGlobalState(selectedMode === "act" ? "actModeApiModelId" : "planModeApiModelId", options.model)
}
printInfo(`🚀 Starting Cline task...`)
printInfo(`📁 Working directory: ${workspacePath}`)
print(separator())
// Initialize context
const { extensionContext, DATA_DIR, EXTENSION_DIR } = initializeCliContext({
clineDir: options.config,
workspaceDir: workspacePath,
})
// Initialize ErrorService (required by BannerService)
await ErrorService.initialize()
// Setup host provider
setupHostProvider(extensionContext, EXTENSION_DIR, DATA_DIR, workspacePath, options.verbose)
// Initialize state manager
await StateManager.initialize(extensionContext)
// Create webview provider (which creates the controller)
const webview = HostProvider.get().createWebviewProvider() as CliWebviewProvider
const controller = webview.controller
// Initialize telemetry distinct ID (needed by various services)
await initializeDistinctId(extensionContext)
// Initialize BannerService (required by Controller.getStateToPostToWebview)
if (!BannerService.isInitialized()) {
BannerService.initialize(controller)
}
// Setup state subscriber
const subscriber = new CliStateSubscriber(options.verbose, controller)
// Override postStateToWebview to also update CLI
const originalPostState = controller.postStateToWebview.bind(controller)
controller.postStateToWebview = async () => {
await originalPostState()
const state = await controller.getStateToPostToWebview()
subscriber.onStateUpdate(state)
}
// Start the task
try {
const taskId = await controller.initTask(prompt)
printInfo(`Task ID: ${taskId}`)
// Wait for task completion by monitoring state updates
const result = await new Promise<{ success: boolean }>((resolve) => {
let completionTimeout: NodeJS.Timeout | null = null
const checkInterval: NodeJS.Timeout | null = null
// Wrap the original onStateUpdate to detect completion
const originalOnStateUpdate = subscriber.onStateUpdate.bind(subscriber)
subscriber.onStateUpdate = async (state: ExtensionState) => {
// Call original update
originalOnStateUpdate(state)
// Check for completion
const lastMessage = state.clineMessages[state.clineMessages.length - 1]
if (lastMessage) {
if (lastMessage.say === "completion_result" || lastMessage.ask === "completion_result") {
if (completionTimeout) clearTimeout(completionTimeout)
if (checkInterval) clearInterval(checkInterval)
// Add newline after streaming output
console.log()
resolve({ success: true })
} else if (lastMessage.say === "error" || lastMessage.ask === "api_req_failed") {
if (completionTimeout) clearTimeout(completionTimeout)
if (checkInterval) clearInterval(checkInterval)
resolve({ success: false })
}
}
}
// Also set up a fallback check every 500ms in case state updates are slow
// checkInterval = setInterval(async () => {
// const state = await controller.getStateToPostToWebview()
// const lastMessage = state.clineMessages[state.clineMessages.length - 1]
// if (lastMessage) {
// if (
// lastMessage.say === "completion_result" ||
// lastMessage.ask === "plan_mode_respond" ||
// lastMessage.ask === "completion_result"
// ) {
// if (checkInterval) clearInterval(checkInterval)
// if (completionTimeout) clearTimeout(completionTimeout)
// console.log()
// resolve({ success: true })
// } else if (lastMessage.say === "error" || lastMessage.ask === "api_req_failed") {
// if (checkInterval) clearInterval(checkInterval)
// if (completionTimeout) clearTimeout(completionTimeout)
// resolve({ success: false })
// }
// }
// }, 500)
// Safety timeout - resolve after 10 minutes
completionTimeout = setTimeout(
() => {
if (checkInterval) clearInterval(checkInterval)
resolve({ success: false })
},
10 * 60 * 1000,
)
})
if (result.success) {
printSuccess("Task completed!")
} else {
process.exit(1)
}
} catch (error) {
printError(`Task failed: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
} finally {
// Cleanup
restoreConsole()
await controller.dispose()
await ErrorService.get().dispose()
}
}
/**
* List task history
*/
async function listHistory(options: { config?: string; limit?: number }) {
const { extensionContext, DATA_DIR, EXTENSION_DIR } = initializeCliContext({
clineDir: options.config,
})
await ErrorService.initialize()
setupHostProvider(extensionContext, EXTENSION_DIR, DATA_DIR, process.cwd())
await StateManager.initialize(extensionContext)
const stateManager = StateManager.get()
const taskHistory = stateManager.getGlobalStateKey("taskHistory") || []
const limit = options.limit || 10
const recentTasks = [...taskHistory.slice(0, limit)]?.reverse()
if (recentTasks.length === 0) {
printInfo("No task history found.")
return
}
print(style.bold(`\n📜 Task History (${recentTasks.length} most recent):\n`))
print(separator())
for (const task of recentTasks) {
const date = new Date(task.ts).toLocaleString()
const taskText = task.task?.substring(0, 60) || "Unknown task"
const truncated = (task.task?.length || 0) > 60 ? "..." : ""
print(`${style.dim(date)}`)
print(` ${style.info(task.id)}`)
print(` ${taskText}${truncated}`)
if (task.totalCost) {
print(` ${style.dim(`Cost: $${task.totalCost.toFixed(4)}`)}`)
}
print("")
}
print(separator())
}
/**
* Show current configuration
*/
async function showConfig(options: { config?: string }) {
const { extensionContext, DATA_DIR } = initializeCliContext({
clineDir: options.config,
})
await ErrorService.initialize()
setupHostProvider(extensionContext, DATA_DIR, DATA_DIR, process.cwd())
await StateManager.initialize(extensionContext)
const stateManager = StateManager.get()
print(style.bold(`\n⚙️ Cline Configuration:\n`))
print(separator())
print(`Data directory: ${style.path(DATA_DIR)}`)
print(separator())
// Get all global state and workspace state entries
const globalStateEntries = stateManager.getAllGlobalStateEntries()
const workspaceStateEntries = stateManager.getAllWorkspaceStateEntries()
const apiConfig = stateManager.getApiConfiguration()
const EXCLUDED_KEYS = ["taskHistory"]
const shouldExcluded = (key: string, value: any): boolean => {
if (EXCLUDED_KEYS.includes(key)) return true
if (key.endsWith("Toggles")) return true
if (key.startsWith("apiConfig_")) return true
if (apiConfig[key as keyof typeof apiConfig] !== undefined) return true
if (!value) return true
if (typeof value === "object" && Object.keys(value).length === 0) return true
if (Array.isArray(value) && value.length === 0) return true
if (typeof value === "string" && value.trim() === "") return true
return false
}
if (Object.keys(globalStateEntries).length > 0) {
print(style.bold(`\nGlobal State:\n`))
for (const [key, value] of Object.entries(globalStateEntries)) {
if (shouldExcluded(key, value)) continue
const displayValue = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value)
print(`${style.info(key)}: ${displayValue}`)
}
print("")
}
if (Object.keys(workspaceStateEntries).length > 0) {
print(style.bold(`\nWorkspace State:\n`))
for (const [key, value] of Object.entries(workspaceStateEntries)) {
if (shouldExcluded(key, value)) continue
const displayValue = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value)
print(`${style.info(key)}: ${displayValue}`)
}
print("")
}
print(separator())
}
// Setup CLI commands
const program = new Command()
program.name("cline").description("Cline CLI - AI coding assistant in your terminal").version(VERSION)
program
.command("task")
.alias("t")
.description("Run a new task")
.argument("<prompt>", "The task prompt")
.option("-s, --switch <mode>", "Switch mode: act, plan")
.option("-m, --model <model>", "Model to use for the task")
.option("-v, --verbose", "Show verbose output including reasoning")
.option("-c, --cwd <path>", "Working directory for the task")
.option("--config <path>", "Path to Cline configuration directory")
.action(runTask)
program
.command("history")
.alias("h")
.description("List task history")
.option("-n, --limit <number>", "Number of tasks to show", "10")
.option("--config <path>", "Path to Cline configuration directory")
.action(listHistory)
program
.command("config")
.description("Show current configuration")
.option("--config <path>", "Path to Cline configuration directory")
.action(showConfig)
program
.command("auth")
.description("Authenticate a provider and configure what model is used")
.option("-p, --provider <id>", "Provider ID for quick setup (e.g., openai-native, anthropic)")
.option("-k, --apikey <key>", "API key for the provider")
.option("-m, --modelid <id>", "Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929)")
.option("-b, --baseurl <url>", "Base URL (optional, only for openai provider)")
.option("-v, --verbose", "Show verbose output")
.option("-c, --cwd <path>", "Working directory for the task")
.option("--config <path>", "Path to Cline configuration directory")
.action(runAuth)
// Interactive mode (default when no command given)
program
.argument("[prompt]", "Task prompt (starts task immediately)")
.option("-v, --verbose", "Show verbose output")
.option("-c, --cwd <path>", "Working directory")
.option("--config <path>", "Configuration directory")
.action(async (prompt, options) => {
if (prompt) {
await runTask(prompt, options)
} else {
// Show help if no prompt given
program.help()
}
})
// Parse and run
program.parse()
-74
View File
@@ -1,74 +0,0 @@
/**
* Suppress noisy console output during module initialization.
* This file must be imported first (before any other imports) to take effect.
*
* This prevents variant configuration warnings and other debug logs from cluttering the CLI.
*/
// Save original console methods
const originalConsoleWarn = console.warn
const originalConsoleLog = console.log
const originalConsoleError = console.error
// Patterns of messages we want to suppress in non-verbose mode
const suppressedPatterns = [
/variant configuration warnings/i,
/BannerService/i,
/TelemetryProviderFactory/i,
/TelemetryService/i,
/NoOpTelemetryProvider/i,
/Telemetry ID/i,
/Telemetry distinct ID/i,
/WorkspaceManager/i,
/CheckpointTracker/i,
/checkpoint/i,
/shadow git/i,
/Lock manager/i,
/Task lock/i,
/Registry health check/i,
/Component.*not found/i,
/\[CLI\]/i,
/punycode.*deprecated/i,
/No user found/i,
/authentication data found/i,
/legacy checkpoints/i,
/ClineProvider instantiated/i,
/CommandExecutor/i,
/StandaloneTerminalManager/i,
/Using HostProvider/i,
/Cline API Error/i,
]
function shouldSuppress(args: unknown[]): boolean {
const message = args.map((a) => String(a)).join(" ")
return suppressedPatterns.some((pattern) => pattern.test(message))
}
// Check if we're in verbose mode by looking at command line args
const isVerboseMode = process.argv.includes("-v") || process.argv.includes("--verbose")
if (!isVerboseMode) {
console.warn = (...args: unknown[]) => {
if (!shouldSuppress(args)) {
originalConsoleWarn.apply(console, args as [unknown?, ...unknown[]])
}
}
console.log = (...args: unknown[]) => {
if (!shouldSuppress(args)) {
originalConsoleLog.apply(console, args as [unknown?, ...unknown[]])
}
}
// Keep errors visible but filter some noisy ones
console.error = (...args: unknown[]) => {
if (!shouldSuppress(args)) {
originalConsoleError.apply(console, args as [unknown?, ...unknown[]])
}
}
}
// Export a function to restore original console methods if needed
export function restoreConsole() {
console.warn = originalConsoleWarn
console.log = originalConsoleLog
console.error = originalConsoleError
}
-7
View File
@@ -1,7 +0,0 @@
export function jsonParseSafe<T>(data: string, defaultValue: T): T {
try {
return JSON.parse(data) as T
} catch {
return defaultValue
}
}
-255
View File
@@ -1,255 +0,0 @@
/**
* VSCode context stub for CLI mode
* Provides mock implementations of VSCode extension context
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
import os from "os"
import path from "path"
import type { Extension, ExtensionContext, Memento, SecretStorage } from "vscode"
import { ExtensionRegistryInfo } from "@/registry"
import { ExtensionKind, ExtensionMode, URI } from "./vscode-shim"
const SETTINGS_SUBFOLDER = "data"
/**
* Simple file-based Memento store for persisting state
*/
class MementoStore implements Memento {
private data: Record<string, any> = {}
private filePath: string
constructor(filePath: string) {
this.filePath = filePath
this.load()
}
private load() {
try {
if (existsSync(this.filePath)) {
const content = readFileSync(this.filePath, "utf8")
this.data = JSON.parse(content)
}
} catch (error) {
console.error(`Failed to load state from ${this.filePath}:`, error)
this.data = {}
}
}
private save() {
try {
mkdirSync(path.dirname(this.filePath), { recursive: true })
writeFileSync(this.filePath, JSON.stringify(this.data, null, 2))
} catch (error) {
console.error(`Failed to save state to ${this.filePath}:`, error)
}
}
keys(): readonly string[] {
return Object.keys(this.data)
}
get<T>(key: string): T | undefined
get<T>(key: string, defaultValue: T): T
get<T>(key: string, defaultValue?: T): T | undefined {
const value = this.data[key]
return value !== undefined ? value : defaultValue
}
async update(key: string, value: any): Promise<void> {
if (value === undefined) {
delete this.data[key]
} else {
this.data[key] = value
}
this.save()
}
setKeysForSync(_keys: readonly string[]): void {
// No-op for CLI
}
}
/**
* Simple file-based secret storage
*/
class SecretStore implements SecretStorage {
private data: Record<string, string> = {}
private filePath: string
private onDidChangeEmitter = {
event: () => ({ dispose: () => {} }),
fire: (_e: any) => {},
dispose: () => {},
}
onDidChange = this.onDidChangeEmitter.event
constructor(filePath: string) {
this.filePath = filePath
this.load()
}
private load() {
try {
if (existsSync(this.filePath)) {
const content = readFileSync(this.filePath, "utf8")
this.data = JSON.parse(content)
}
} catch (error) {
this.data = {}
}
}
private save() {
try {
mkdirSync(path.dirname(this.filePath), { recursive: true })
writeFileSync(this.filePath, JSON.stringify(this.data, null, 2))
} catch (error) {
console.error(`Failed to save secrets:`, error)
}
}
async get(key: string): Promise<string | undefined> {
return this.data[key]
}
async store(key: string, value: string): Promise<void> {
this.data[key] = value
this.save()
}
async delete(key: string): Promise<void> {
delete this.data[key]
this.save()
}
}
/**
* Mock environment variable collection
*/
class EnvironmentVariableCollection {
private variables: Map<string, any> = new Map()
persistent = true
description = "CLI Environment Variables"
entries(): IterableIterator<[string, any]> {
return this.variables.entries()
}
replace(variable: string, value: string) {
this.variables.set(variable, { value, type: "replace" })
}
append(variable: string, value: string) {
this.variables.set(variable, { value, type: "append" })
}
prepend(variable: string, value: string) {
this.variables.set(variable, { value, type: "prepend" })
}
get(variable: string) {
return this.variables.get(variable)
}
forEach(callback: (variable: string, mutator: any, collection: any) => void) {
this.variables.forEach((mutator, variable) => {
callback(variable, mutator, this)
})
}
delete(variable: string) {
return this.variables.delete(variable)
}
clear() {
this.variables.clear()
}
getScoped(_scope: any) {
return this
}
}
function readJson(filePath: string): any {
try {
if (existsSync(filePath)) {
return JSON.parse(readFileSync(filePath, "utf8"))
}
} catch (error) {
// Return empty object if file doesn't exist
}
return {}
}
export interface CliContextConfig {
clineDir?: string
workspaceDir?: string
}
/**
* Initialize the VSCode-like context for CLI mode
*/
export function initializeCliContext(config: CliContextConfig = {}) {
const CLINE_DIR = config.clineDir || process.env.CLINE_DIR || path.join(os.homedir(), ".cline")
const DATA_DIR = path.join(CLINE_DIR, SETTINGS_SUBFOLDER)
const WORKSPACE_STORAGE_DIR = config.workspaceDir || process.env.WORKSPACE_STORAGE_DIR || path.join(DATA_DIR, "workspace")
// Ensure directories exist
mkdirSync(DATA_DIR, { recursive: true })
mkdirSync(WORKSPACE_STORAGE_DIR, { recursive: true })
console.log(`[CLI] Using data directory: ${DATA_DIR}`)
// For CLI, extension dir is the root of the project (parent of cli-ts)
const EXTENSION_DIR = path.resolve(__dirname, "..", "..")
const EXTENSION_MODE = process.env.IS_DEV === "true" ? ExtensionMode.Development : ExtensionMode.Production
const extension: Extension<void> = {
id: ExtensionRegistryInfo.id,
isActive: true,
extensionPath: EXTENSION_DIR,
extensionUri: URI.file(EXTENSION_DIR),
packageJSON: readJson(path.join(EXTENSION_DIR, "package.json")),
exports: undefined,
activate: async () => {},
extensionKind: ExtensionKind.UI,
}
const extensionContext: ExtensionContext = {
extension: extension,
extensionMode: EXTENSION_MODE,
// Set up KV stores
globalState: new MementoStore(path.join(DATA_DIR, "globalState.json")),
secrets: new SecretStore(path.join(DATA_DIR, "secrets.json")),
// Set up URIs
storageUri: URI.file(WORKSPACE_STORAGE_DIR),
storagePath: WORKSPACE_STORAGE_DIR,
globalStorageUri: URI.file(DATA_DIR),
globalStoragePath: DATA_DIR,
// Logs
logUri: URI.file(DATA_DIR),
logPath: DATA_DIR,
extensionUri: URI.file(EXTENSION_DIR),
extensionPath: EXTENSION_DIR,
asAbsolutePath: (relPath: string) => path.join(EXTENSION_DIR, relPath),
subscriptions: [],
environmentVariableCollection: new EnvironmentVariableCollection() as any,
// Workspace state
workspaceState: new MementoStore(path.join(WORKSPACE_STORAGE_DIR, "workspaceState.json")),
}
return {
extensionContext,
DATA_DIR,
EXTENSION_DIR,
WORKSPACE_STORAGE_DIR,
}
}
-277
View File
@@ -1,277 +0,0 @@
/**
* VSCode namespace shim for CLI mode
* Provides minimal stubs for VSCode types and enums used by the codebase
*/
// Re-export common types from vscode-uri for URI handling
export { URI } from "vscode-uri"
// Extension mode enum
export enum ExtensionMode {
Production = 1,
Development = 2,
Test = 3,
}
// Extension kind enum
export enum ExtensionKind {
UI = 1,
Workspace = 2,
}
// Diagnostic severity enum
export enum DiagnosticSeverity {
Error = 0,
Warning = 1,
Information = 2,
Hint = 3,
}
// End of line enum
export enum EndOfLine {
LF = 1,
CRLF = 2,
}
// Position class
export class Position {
constructor(
public readonly line: number,
public readonly character: number,
) {}
isAfter(other: Position): boolean {
return this.line > other.line || (this.line === other.line && this.character > other.character)
}
isAfterOrEqual(other: Position): boolean {
return this.line > other.line || (this.line === other.line && this.character >= other.character)
}
isBefore(other: Position): boolean {
return this.line < other.line || (this.line === other.line && this.character < other.character)
}
isBeforeOrEqual(other: Position): boolean {
return this.line < other.line || (this.line === other.line && this.character <= other.character)
}
isEqual(other: Position): boolean {
return this.line === other.line && this.character === other.character
}
translate(lineDelta?: number, characterDelta?: number): Position {
return new Position(this.line + (lineDelta || 0), this.character + (characterDelta || 0))
}
with(line?: number, character?: number): Position {
return new Position(line ?? this.line, character ?? this.character)
}
compareTo(other: Position): number {
if (this.line < other.line) return -1
if (this.line > other.line) return 1
if (this.character < other.character) return -1
if (this.character > other.character) return 1
return 0
}
}
// Range class
export class Range {
constructor(
public readonly start: Position,
public readonly end: Position,
)
constructor(startLine: number, startCharacter: number, endLine: number, endCharacter: number)
constructor(
startOrStartLine: Position | number,
endOrStartCharacter: Position | number,
endLine?: number,
endCharacter?: number,
) {
if (typeof startOrStartLine === "number") {
this.start = new Position(startOrStartLine, endOrStartCharacter as number)
this.end = new Position(endLine!, endCharacter!)
} else {
this.start = startOrStartLine
this.end = endOrStartCharacter as Position
}
}
get isEmpty(): boolean {
return this.start.isEqual(this.end)
}
get isSingleLine(): boolean {
return this.start.line === this.end.line
}
contains(positionOrRange: Position | Range): boolean {
if (positionOrRange instanceof Range) {
return this.contains(positionOrRange.start) && this.contains(positionOrRange.end)
}
return positionOrRange.isAfterOrEqual(this.start) && positionOrRange.isBeforeOrEqual(this.end)
}
isEqual(other: Range): boolean {
return this.start.isEqual(other.start) && this.end.isEqual(other.end)
}
intersection(range: Range): Range | undefined {
const start = Position.prototype.isAfter.call(this.start, range.start) ? this.start : range.start
const end = Position.prototype.isBefore.call(this.end, range.end) ? this.end : range.end
if (start.isAfter(end)) {
return undefined
}
return new Range(start, end)
}
union(other: Range): Range {
const start = this.start.isBefore(other.start) ? this.start : other.start
const end = this.end.isAfter(other.end) ? this.end : other.end
return new Range(start, end)
}
with(start?: Position, end?: Position): Range {
return new Range(start ?? this.start, end ?? this.end)
}
}
// Selection class (extends Range)
export class Selection extends Range {
constructor(
public readonly anchor: Position,
public readonly active: Position,
)
constructor(anchorLine: number, anchorCharacter: number, activeLine: number, activeCharacter: number)
constructor(
anchorOrAnchorLine: Position | number,
activeOrAnchorCharacter: Position | number,
activeLine?: number,
activeCharacter?: number,
) {
if (typeof anchorOrAnchorLine === "number") {
const anchor = new Position(anchorOrAnchorLine, activeOrAnchorCharacter as number)
const active = new Position(activeLine!, activeCharacter!)
super(anchor.isBefore(active) ? anchor : active, anchor.isBefore(active) ? active : anchor)
this.anchor = anchor
this.active = active
} else {
const anchor = anchorOrAnchorLine
const active = activeOrAnchorCharacter as Position
super(anchor.isBefore(active) ? anchor : active, anchor.isBefore(active) ? active : anchor)
this.anchor = anchor
this.active = active
}
}
get isReversed(): boolean {
return this.anchor.isAfter(this.active)
}
}
// Cancellation token
export interface CancellationToken {
isCancellationRequested: boolean
onCancellationRequested: any
}
// Event emitter (simplified)
export class EventEmitter<T> {
private listeners: Array<(e: T) => void> = []
event = (listener: (e: T) => void) => {
this.listeners.push(listener)
return {
dispose: () => {
const index = this.listeners.indexOf(listener)
if (index >= 0) {
this.listeners.splice(index, 1)
}
},
}
}
fire(data: T): void {
for (const listener of this.listeners) {
listener(data)
}
}
dispose(): void {
this.listeners = []
}
}
// Disposable
export class Disposable {
constructor(private callOnDispose: () => void) {}
static from(...disposables: { dispose(): any }[]): Disposable {
return new Disposable(() => {
for (const d of disposables) {
d.dispose()
}
})
}
dispose(): void {
this.callOnDispose()
}
}
// Minimal workspace namespace
export const workspace = {
workspaceFolders: undefined as any[] | undefined,
getWorkspaceFolder: (_uri: any) => undefined,
onDidChangeWorkspaceFolders: () => ({ dispose: () => {} }),
fs: {
readFile: async (_uri: any): Promise<Uint8Array> => new Uint8Array(),
writeFile: async (_uri: any, _content: Uint8Array): Promise<void> => {},
delete: async (_uri: any): Promise<void> => {},
stat: async (_uri: any): Promise<any> => ({ type: 1, size: 0 }),
readDirectory: async (_uri: any): Promise<any[]> => [],
createDirectory: async (_uri: any): Promise<void> => {},
},
}
// Minimal window namespace
export const window = {
showInformationMessage: async (message: string) => {
console.log(`[INFO] ${message}`)
return undefined
},
showWarningMessage: async (message: string) => {
console.warn(`[WARN] ${message}`)
return undefined
},
showErrorMessage: async (message: string) => {
console.error(`[ERROR] ${message}`)
return undefined
},
createOutputChannel: (_name: string) => ({
appendLine: (line: string) => console.log(line),
append: (text: string) => process.stdout.write(text),
clear: () => {},
show: () => {},
hide: () => {},
dispose: () => {},
}),
terminals: [] as any[],
activeTerminal: undefined as any,
createTerminal: (_options?: any) => ({
name: "CLI Terminal",
processId: Promise.resolve(process.pid),
sendText: (text: string) => console.log(`[Terminal] ${text}`),
show: () => {},
hide: () => {},
dispose: () => {},
}),
}
// Export types that are commonly used
export type ExtensionContext = any
export type Memento = any
export type SecretStorage = any
export type Extension<T> = any
-71
View File
@@ -1,71 +0,0 @@
{
"compilerOptions": {
"esModuleInterop": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"lib": [
"es2022"
],
"module": "esnext",
"moduleResolution": "Bundler",
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noUnusedLocals": false,
"resolveJsonModule": true,
"rootDir": ".",
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "es2022",
"useDefineForClassFields": true,
"useUnknownInCatchVariables": false,
"baseUrl": "..",
"paths": {
"@/*": [
"src/*"
],
"@api/*": [
"src/core/api/*"
],
"@core/*": [
"src/core/*"
],
"@generated/*": [
"src/generated/*"
],
"@hosts/*": [
"src/hosts/*"
],
"@integrations/*": [
"src/integrations/*"
],
"@packages/*": [
"src/packages/*"
],
"@services/*": [
"src/services/*"
],
"@shared/*": [
"src/shared/*"
],
"@utils/*": [
"src/utils/*"
]
},
"outDir": "dist"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
],
"references": [
{
"path": ".."
}
]
}
-7
View File
@@ -1,7 +0,0 @@
{
"workflowToggles": {},
"localClineRulesToggles": {},
"localWindsurfRulesToggles": {},
"localCursorRulesToggles": {},
"localAgentsRulesToggles": {}
}
+81 -89
View File
@@ -10,11 +10,12 @@ import (
"strings"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/cline/cli/pkg/cli"
"github.com/cline/cli/pkg/cli/auth"
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/output"
"github.com/cline/cli/pkg/cli/slash"
"github.com/cline/cli/pkg/common"
"github.com/cline/grpc-go/cline"
"github.com/spf13/cobra"
@@ -37,8 +38,9 @@ var (
func main() {
rootCmd := &cobra.Command{
Use: "cline [prompt]",
Short: "Cline CLI - AI-powered coding assistant",
Use: "cline [prompt]",
Short: "Cline CLI - AI-powered coding assistant",
Version: global.CliVersion,
Long: `A command-line interface for interacting with Cline AI coding assistant.
Start a new task by providing a prompt:
@@ -70,8 +72,6 @@ see the manual page: man cline`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
var instanceAddress string
// Validate workspace paths exist
if err := common.ValidateDirsExist(workspaces); err != nil {
return err
@@ -88,13 +88,13 @@ see the manual page: man cline`,
if global.Config.Verbose {
fmt.Println("Starting new Cline instance...")
}
instance, err := global.Clients.StartNewInstance(ctx, allWorkspaces...)
instance, err := global.Instances.StartNewInstance(ctx, allWorkspaces...)
if err != nil {
return fmt.Errorf("failed to start new instance: %w", err)
}
instanceAddress = instance.Address
global.Config.CoreAddress = instance.CoreAddress
if global.Config.Verbose {
fmt.Printf("Started instance at %s\n\n", instanceAddress)
fmt.Printf("Started instance at %s\n\n", global.Config.CoreAddress)
}
// Set up cleanup on exit
@@ -102,38 +102,35 @@ see the manual page: man cline`,
if global.Config.Verbose {
fmt.Println("\nCleaning up instance...")
}
registry := global.Clients.GetRegistry()
if err := global.KillInstanceByAddress(context.Background(), registry, instanceAddress); err != nil {
registry := global.Instances.GetRegistry()
if err := global.KillInstanceByAddress(context.Background(), registry, global.Config.CoreAddress); err != nil {
if global.Config.Verbose {
fmt.Printf("Warning: Failed to clean up instance: %v\n", err)
}
}
}()
}
// Check if user has credentials configured
if !isUserReadyToUse(ctx, instanceAddress) {
// Create renderer for welcome messages
renderer := display.NewRenderer(global.Config.OutputFormat)
fmt.Printf("\n%s\n\n", renderer.Dim("Hey there! Looks like you're new here. Let's get you set up"))
// Check if user has credentials configured
if !isUserReadyToUse(ctx) {
// Create renderer for welcome messages
renderer := display.NewRenderer(global.Config.OutputFormat)
fmt.Printf("\n%s\n\n", renderer.Dim("Hey there! Looks like you're new here. Let's get you set up"))
if err := auth.HandleAuthMenuNoArgs(ctx); err != nil {
// Check if user cancelled - exit cleanly
if err == huh.ErrUserAborted {
return nil
}
return fmt.Errorf("auth setup failed: %w", err)
if err := auth.HandleAuthMenuNoArgs(ctx); err != nil {
// Check if user cancelled - exit cleanly
if err == huh.ErrUserAborted {
return nil
}
// Re-check after auth wizard
if !isUserReadyToUse(ctx, instanceAddress) {
return fmt.Errorf("credentials still not configured - please run 'cline auth' to complete setup")
}
fmt.Printf("\n%s\n\n", renderer.Dim("✓ Setup complete, you can now use the Cline CLI"))
return fmt.Errorf("auth setup failed: %w", err)
}
} else {
// User specified --address flag, use that
instanceAddress = coreAddress
// Re-check after auth wizard
if !isUserReadyToUse(ctx) {
return fmt.Errorf("credentials still not configured - please run 'cline auth' to complete setup")
}
fmt.Printf("\n%s\n\n", renderer.Dim("✓ Setup complete, you can now use the Cline CLI"))
}
// Get content from both args and stdin
@@ -142,10 +139,13 @@ see the manual page: man cline`,
return fmt.Errorf("failed to read prompt: %w", err)
}
// If no prompt from args or stdin, show interactive input
if prompt == "" {
// If no prompt (or just a mode switch with no message), show interactive input
// Loop to allow mode switches without a message
bannerShown := false
for prompt == "" {
// Pass the mode flag and workspaces to banner so it shows correct info
prompt, err = promptForInitialTask(ctx, instanceAddress, mode, allWorkspaces)
prompt, err = promptForInitialTask(ctx, mode, allWorkspaces, !bannerShown)
bannerShown = true
if err != nil {
// Check if user cancelled - exit cleanly without error
if err == huh.ErrUserAborted {
@@ -153,6 +153,23 @@ see the manual page: man cline`,
}
return err
}
// Check if user entered a mode switch command
if newMode, remaining, isModeSwitch := slash.ParseModeSwitch(prompt); isModeSwitch {
mode = newMode
prompt = remaining
// If just a mode switch with no message, continue loop to re-prompt
if prompt == "" {
renderer := display.NewRenderer(global.Config.OutputFormat)
if mode == "act" {
fmt.Printf("\n%s\n\n", renderer.Success("Switched to act mode"))
} else {
fmt.Printf("\n%s\n\n", renderer.Success("Switched to plan mode"))
}
continue
}
}
if prompt == "" {
return fmt.Errorf("prompt required")
}
@@ -170,13 +187,15 @@ see the manual page: man cline`,
Mode: mode,
Settings: settings,
Yolo: yolo,
Address: instanceAddress,
Address: global.Config.CoreAddress,
Verbose: verbose,
Workspaces: allWorkspaces,
})
},
}
rootCmd.SetVersionTemplate(cli.VersionString())
rootCmd.PersistentFlags().StringVar(&coreAddress, "address", fmt.Sprintf("localhost:%d", common.DEFAULT_CLINE_CORE_PORT), "Cline Core gRPC address")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "F", "rich", "output format (rich|json|plain)")
@@ -204,51 +223,30 @@ see the manual page: man cline`,
}
}
func promptForInitialTask(ctx context.Context, instanceAddress, modeFlag string, workspaces []string) (string, error) {
// Show session banner before the initial input
showSessionBanner(ctx, instanceAddress, modeFlag, workspaces)
var prompt string
// Create custom theme with mode-colored cursor and title
theme := huh.ThemeCharm()
// Set cursor and title color based on mode
modeColor := lipgloss.Color("3") // Yellow for plan
if modeFlag == "act" {
modeColor = lipgloss.Color("39") // Blue for act
func promptForInitialTask(ctx context.Context, modeFlag string, workspaces []string, showBanner bool) (string, error) {
// Show session banner before the initial input (only on first prompt)
if showBanner {
showSessionBanner(ctx, modeFlag, workspaces)
}
theme.Focused.TextInput.Cursor = theme.Focused.TextInput.Cursor.Foreground(modeColor)
theme.Focused.Title = theme.Focused.Title.Foreground(modeColor)
form := huh.NewForm(
huh.NewGroup(
huh.NewText().
Title("Start a new Cline task").
Description("What would you like Cline to help you with?").
Placeholder("e.g., Create a REST API with authentication...").
Lines(5).
Value(&prompt),
),
).WithWidth(48).WithTheme(theme)
err := form.Run()
prompt, err := output.PromptForInitialTask(
"Start a new Cline task",
"/plan or /act to switch modes\ntab to autocomplete commands\nctrl+e to open editor\nctrl+c to exit",
modeFlag,
slash.NewRegistry(ctx),
)
if err != nil {
// Check if user cancelled with Control-C
if err == huh.ErrUserAborted {
// Return a special error that indicates clean cancellation
// This allows deferred cleanup to run
if err == output.ErrUserAborted {
return "", huh.ErrUserAborted
}
return "", err
}
return strings.TrimSpace(prompt), nil
return prompt, nil
}
// showSessionBanner displays session info before initial prompt
func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string, workspaces []string) {
func showSessionBanner(ctx context.Context, modeFlag string, workspaces []string) {
bannerInfo := display.BannerInfo{
Version: global.CliVersion,
Mode: modeFlag, // Use the mode from command flag, not state
@@ -262,21 +260,18 @@ func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string, wo
bannerInfo.Workdirs = workspaces
// Get provider/model using auth functions (same logic as auth menu)
manager, err := cli.NewTaskManagerForAddress(ctx, instanceAddress)
if err == nil {
if providerList, err := auth.GetProviderConfigurations(ctx, manager); err == nil {
// Show provider/model for the mode we'll be using
var providerDisplay *auth.ProviderDisplay
if bannerInfo.Mode == "plan" && providerList.PlanProvider != nil {
providerDisplay = providerList.PlanProvider
} else if bannerInfo.Mode == "act" && providerList.ActProvider != nil {
providerDisplay = providerList.ActProvider
}
if providerList, err := auth.GetProviderConfigurations(ctx); err == nil {
// Show provider/model for the mode we'll be using
var providerDisplay *auth.ProviderDisplay
if bannerInfo.Mode == "plan" && providerList.PlanProvider != nil {
providerDisplay = providerList.PlanProvider
} else if bannerInfo.Mode == "act" && providerList.ActProvider != nil {
providerDisplay = providerList.ActProvider
}
if providerDisplay != nil {
bannerInfo.Provider = auth.GetProviderIDForEnum(providerDisplay.Provider)
bannerInfo.ModelID = providerDisplay.ModelID
}
if providerDisplay != nil {
bannerInfo.Provider = auth.GetProviderIDForEnum(providerDisplay.Provider)
bannerInfo.ModelID = providerDisplay.ModelID
}
}
@@ -289,25 +284,22 @@ func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string, wo
// isUserReadyToUse checks if the user has completed initial setup
// Returns true if welcomeViewCompleted flag is set OR user is authenticated
// Matches extension logic: welcomeViewCompleted = Boolean(globalState.welcomeViewCompleted || user?.uid)
func isUserReadyToUse(ctx context.Context, instanceAddress string) bool {
manager, err := cli.NewTaskManagerForAddress(ctx, instanceAddress)
func isUserReadyToUse(ctx context.Context) bool {
grpcClient, err := global.GetClientForAddress(ctx, global.Config.CoreAddress)
if err != nil {
return false
}
// Get state
state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
state, err := grpcClient.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return false
}
// Parse state JSON
stateMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(state.StateJson), &stateMap); err != nil {
return false
}
// Check 1: welcomeViewCompleted flag
if welcomeCompleted, ok := stateMap["welcomeViewCompleted"].(bool); ok && welcomeCompleted {
return true
}
+10 -10
View File
@@ -23,7 +23,7 @@ func TestMultiInstanceDefaultUnchanged(t *testing.T) {
if len(out1.CoreInstances) != 1 {
t.Fatalf("expected 1 instance, got %d", len(out1.CoreInstances))
}
firstAddr := out1.CoreInstances[0].Address
firstAddr := out1.CoreInstances[0].CoreAddress
waitForAddressHealthy(t, firstAddr, defaultTimeout)
// Start second instance
@@ -56,29 +56,29 @@ func TestDefaultJsonUpdateAfterRemoval(t *testing.T) {
// Choose second as new default
target := out.CoreInstances[1]
waitForAddressHealthy(t, target.Address, defaultTimeout)
waitForAddressHealthy(t, target.CoreAddress, defaultTimeout)
// Set as default
_ = mustRunCLI(ctx, t, "instance", "use", target.Address)
_ = mustRunCLI(ctx, t, "instance", "use", target.CoreAddress)
// Verify default switched
out = listInstancesJSON(ctx, t)
if out.DefaultInstance != target.Address {
t.Fatalf("default_instance not updated to %s (got %s)", target.Address, out.DefaultInstance)
if out.DefaultInstance != target.CoreAddress {
t.Fatalf("default_instance not updated to %s (got %s)", target.CoreAddress, out.DefaultInstance)
}
// Kill the default instance using runtime PID discovery
corePID := getCorePID(t, target.Address)
corePID := getCorePID(t, target.CoreAddress)
if corePID <= 0 {
t.Fatalf("could not find PID for core process at %s", target.Address)
t.Fatalf("could not find PID for core process at %s", target.CoreAddress)
}
t.Logf("Killing cline-core process PID %d for instance %s", corePID, target.Address)
t.Logf("Killing cline-core process PID %d for instance %s", corePID, target.CoreAddress)
if err := syscall.Kill(corePID, syscall.SIGKILL); err != nil {
t.Fatalf("kill pid %d: %v", corePID, err)
}
// Wait for removal
waitForAddressRemoved(t, target.Address, longTimeout)
waitForAddressRemoved(t, target.CoreAddress, longTimeout)
// Clean up dangling host process (SIGKILL leaves these behind by design)
t.Logf("Cleaning up dangling host process on port %d", target.HostPort())
@@ -91,7 +91,7 @@ func TestDefaultJsonUpdateAfterRemoval(t *testing.T) {
if len(out.CoreInstances) > 0 {
found := false
for _, it := range out.CoreInstances {
if out.DefaultInstance == it.Address {
if out.DefaultInstance == it.CoreAddress {
found = true
break
}
+3 -3
View File
@@ -117,7 +117,7 @@ func listInstancesJSON(ctx context.Context, t *testing.T) common.InstancesOutput
func hasAddress(in common.InstancesOutput, addr string) bool {
for _, it := range in.CoreInstances {
if it.Address == addr {
if it.CoreAddress == addr {
return true
}
}
@@ -126,7 +126,7 @@ func hasAddress(in common.InstancesOutput, addr string) bool {
func getByAddress(in common.InstancesOutput, addr string) (common.CoreInstanceInfo, bool) {
for _, it := range in.CoreInstances {
if it.Address == addr {
if it.CoreAddress == addr {
return it, true
}
}
@@ -313,7 +313,7 @@ func getCorePIDViaRPC(t *testing.T, address string) int {
defer cancel()
// Get client for the address
client, err := global.Clients.GetRegistry().GetClient(ctx, address)
client, err := global.Instances.GetRegistry().GetClient(ctx, address)
if err != nil {
t.Logf("Warning: failed to get client for %s, falling back to lsof: %v", address, err)
return getCorePIDViaLsof(t, address)
+10 -10
View File
@@ -26,7 +26,7 @@ func TestMixedLocalhostVs127Coexist(t *testing.T) {
t.Fatalf("expected at least 1 instance")
}
inst := out.CoreInstances[0]
waitForAddressHealthy(t, inst.Address, defaultTimeout)
waitForAddressHealthy(t, inst.CoreAddress, defaultTimeout)
// Manually add a SQLite entry for the same port but 127.0.0.1 host
addr127 := fmt.Sprintf("127.0.0.1:%d", inst.CorePort())
@@ -37,12 +37,12 @@ func TestMixedLocalhostVs127Coexist(t *testing.T) {
}
// Verify both addresses appear and are healthy
waitForAddressHealthy(t, inst.Address, defaultTimeout)
waitForAddressHealthy(t, inst.CoreAddress, defaultTimeout)
waitForAddressHealthy(t, addr127, defaultTimeout)
out = listInstancesJSON(ctx, t)
if !hasAddress(out, inst.Address) || !hasAddress(out, addr127) {
t.Fatalf("expected both %s and %s present", inst.Address, addr127)
if !hasAddress(out, inst.CoreAddress) || !hasAddress(out, addr127) {
t.Fatalf("expected both %s and %s present", inst.CoreAddress, addr127)
}
}
@@ -58,7 +58,7 @@ func TestStartStopStress(t *testing.T) {
before := listInstancesJSON(ctx, t)
beforeSet := map[string]struct{}{}
for _, it := range before.CoreInstances {
beforeSet[it.Address] = struct{}{}
beforeSet[it.CoreAddress] = struct{}{}
}
// Start a new instance
@@ -69,8 +69,8 @@ func TestStartStopStress(t *testing.T) {
waitFor(t, defaultTimeout, func() (bool, string) {
after := listInstancesJSON(ctx, t)
for _, it := range after.CoreInstances {
if _, ok := beforeSet[it.Address]; !ok {
newAddr = it.Address
if _, ok := beforeSet[it.CoreAddress]; !ok {
newAddr = it.CoreAddress
return true, ""
}
}
@@ -88,12 +88,12 @@ func TestStartStopStress(t *testing.T) {
}
// Get PID using runtime discovery
corePID := getCorePID(t, info.Address)
corePID := getCorePID(t, info.CoreAddress)
if corePID <= 0 {
t.Fatalf("could not find PID for new instance at %s", info.Address)
t.Fatalf("could not find PID for new instance at %s", info.CoreAddress)
}
t.Logf("Killing new instance %s (PID %d) for iteration %d", info.Address, corePID, i)
t.Logf("Killing new instance %s (PID %d) for iteration %d", info.CoreAddress, corePID, i)
if err := syscall.Kill(corePID, syscall.SIGKILL); err != nil {
t.Fatalf("kill pid %d: %v", corePID, err)
}
+1 -1
View File
@@ -55,7 +55,7 @@ func readInstancesFromSQLite(t *testing.T, clineDir string) []common.CoreInstanc
// Create InstanceInfo
info := common.CoreInstanceInfo{
Address: heldBy,
CoreAddress: heldBy,
HostServiceAddress: lockTarget,
Status: grpc_health_v1.HealthCheckResponse_UNKNOWN, // Will be updated by health check
LastSeen: time.Unix(lockedAt/1000, 0), // Convert from milliseconds
+14 -14
View File
@@ -29,7 +29,7 @@ func TestStartAndList(t *testing.T) {
t.Fatalf("expected 1 instance, got %d", len(out.CoreInstances))
}
addr := out.CoreInstances[0].Address
addr := out.CoreInstances[0].CoreAddress
t.Logf("Instance address: %s, status: %s", addr, out.CoreInstances[0].Status)
t.Logf("Waiting for address %s to become healthy...", addr)
@@ -44,8 +44,8 @@ func TestStartAndList(t *testing.T) {
if out.DefaultInstance == "" {
t.Fatalf("default_instance not set")
}
if out.DefaultInstance != out.CoreInstances[0].Address {
t.Fatalf("expected default_instance=%s, got %s", out.CoreInstances[0].Address, out.DefaultInstance)
if out.DefaultInstance != out.CoreInstances[0].CoreAddress {
t.Fatalf("expected default_instance=%s, got %s", out.CoreInstances[0].CoreAddress, out.DefaultInstance)
}
t.Logf("TestStartAndList completed successfully")
@@ -64,7 +64,7 @@ func TestTaskNewDefault(t *testing.T) {
if len(out.CoreInstances) != 1 {
t.Fatalf("expected 1 instance, got %d", len(out.CoreInstances))
}
addr := out.CoreInstances[0].Address
addr := out.CoreInstances[0].CoreAddress
waitForAddressHealthy(t, addr, defaultTimeout)
// Create a new task at default (success is sufficient)
@@ -108,21 +108,21 @@ func TestCrashCleanup(t *testing.T) {
// Test 1: Graceful shutdown (SIGTERM) - should clean up both processes
gracefulTarget := out.CoreInstances[0]
waitForAddressHealthy(t, gracefulTarget.Address, defaultTimeout)
waitForAddressHealthy(t, gracefulTarget.CoreAddress, defaultTimeout)
// Get PID using runtime discovery
gracefulPID := getCorePID(t, gracefulTarget.Address)
gracefulPID := getCorePID(t, gracefulTarget.CoreAddress)
if gracefulPID <= 0 {
t.Fatalf("could not find PID for graceful target at %s", gracefulTarget.Address)
t.Fatalf("could not find PID for graceful target at %s", gracefulTarget.CoreAddress)
}
t.Logf("Testing graceful shutdown (SIGTERM) for instance %s (PID %d)", gracefulTarget.Address, gracefulPID)
t.Logf("Testing graceful shutdown (SIGTERM) for instance %s (PID %d)", gracefulTarget.CoreAddress, gracefulPID)
if err := syscall.Kill(gracefulPID, syscall.SIGTERM); err != nil {
t.Fatalf("kill SIGTERM pid %d: %v", gracefulPID, err)
}
// Wait for registry cleanup
waitForAddressRemoved(t, gracefulTarget.Address, longTimeout)
waitForAddressRemoved(t, gracefulTarget.CoreAddress, longTimeout)
// Verify both core and host ports are freed (no dangling processes)
waitForPortsClosed(t, gracefulTarget.CorePort(), gracefulTarget.HostPort(), defaultTimeout)
@@ -132,21 +132,21 @@ func TestCrashCleanup(t *testing.T) {
// Test 2: Crash cleanup (SIGKILL) - creates dangling host process that we must clean up
crashTarget := out.CoreInstances[1]
waitForAddressHealthy(t, crashTarget.Address, defaultTimeout)
waitForAddressHealthy(t, crashTarget.CoreAddress, defaultTimeout)
// Get PID using runtime discovery
crashPID := getCorePID(t, crashTarget.Address)
crashPID := getCorePID(t, crashTarget.CoreAddress)
if crashPID <= 0 {
t.Fatalf("could not find PID for crash target at %s", crashTarget.Address)
t.Fatalf("could not find PID for crash target at %s", crashTarget.CoreAddress)
}
t.Logf("Testing crash cleanup (SIGKILL) for instance %s (PID %d)", crashTarget.Address, crashPID)
t.Logf("Testing crash cleanup (SIGKILL) for instance %s (PID %d)", crashTarget.CoreAddress, crashPID)
if err := syscall.Kill(crashPID, syscall.SIGKILL); err != nil {
t.Fatalf("kill SIGKILL pid %d: %v", crashPID, err)
}
// Wait for registry cleanup
waitForAddressRemoved(t, crashTarget.Address, longTimeout)
waitForAddressRemoved(t, crashTarget.CoreAddress, longTimeout)
// Verify the instance is removed from SQLite (no file to check anymore)
// The waitForAddressRemoved already confirms the instance is gone from the registry
+58
View File
@@ -298,6 +298,64 @@ cline task view
\f[I]# Start interactive chat with this task\f[R]
cline task chat
.EE
.SH ENVIRONMENT
.TP
\f[B]CLINE_COMMAND_PERMISSIONS\f[R]
JSON configuration for restricting which shell commands Cline can
execute.
When set, commands are validated against allow/deny patterns before
execution.
When not set, all commands are allowed (backward compatibility).
.RS
.PP
Format:
\f[CR]{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirects": true}\f[R]
.PP
\f[B]Fields:\f[R]
.IP \(bu 2
\f[B]allow\f[R] (array of strings): Glob patterns for allowed commands.
If specified, only matching commands are permitted.
Uses \f[CR]*\f[R] to match any characters and \f[CR]?\f[R] to match a
single character.
.IP \(bu 2
\f[B]deny\f[R] (array of strings): Glob patterns for denied commands.
Deny rules take precedence over allow rules.
.IP \(bu 2
\f[B]allowRedirects\f[R] (boolean): Whether to allow shell redirects
(\f[CR]>\f[R], \f[CR]>>\f[R], \f[CR]<\f[R], etc.).
Defaults to false.
.PP
\f[B]Rule evaluation:\f[R]
.IP "1." 3
Check for dangerous characters (backticks outside single quotes,
unquoted newlines)
.IP "2." 3
Parse command into segments split by operators (\f[CR]&&\f[R],
\f[CR]||\f[R], \f[CR]|\f[R], \f[CR];\f[R])
.IP "3." 3
If redirects detected and \f[CR]allowRedirects\f[R] is not true, command
is denied
.IP "4." 3
Each segment is validated against deny rules first, then allow rules
.IP "5." 3
Subshell contents (\f[CR]$(...)\f[R] and \f[CR](...)\f[R]) are
recursively validated
.IP "6." 3
All segments must pass for the command to be allowed
.PP
\f[B]Examples:\f[R]
.IP
.EX
\f[I]# Allow only npm and git commands\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["npm *", "git *"]}\(aq
\f[I]# Allow development commands but deny dangerous ones\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["npm *", "git *", "node *"], "deny": ["rm \-rf *", "sudo *"]}\(aq
\f[I]# Allow file operations with redirects\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["cat *", "echo *"], "allowRedirects": true}\(aq
.EE
.RE
.SH ARCHITECTURE
Cline operates on a three\-layer architecture:
.TP
+36
View File
@@ -323,6 +323,42 @@ cline task view
cline task chat
```
# ENVIRONMENT
**CLINE_COMMAND_PERMISSIONS**
: JSON configuration for restricting which shell commands Cline can execute. When set, commands are validated against allow/deny patterns before execution. When not set, all commands are allowed.
Format: `{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirects": true}`
**Fields:**
- **allow** (array of strings): Glob patterns for allowed commands. If specified, only matching commands are permitted. Uses `*` to match any characters and `?` to match a single character. Setting allow on anything will deny all others.
- **deny** (array of strings): Glob patterns for denied commands. Deny rules take precedence over allow rules.
- **allowRedirects** (boolean): Whether to allow shell redirects (`>`, `>>`, `<`, etc.). Defaults to false.
**Rule evaluation:**
1. Check for dangerous characters (backticks outside single quotes, unquoted newlines)
2. Parse command into segments split by operators (`&&`, `||`, `|`, `;`)
3. If redirects detected and `allowRedirects` is not true, command is denied
4. Each segment is validated against deny rules first, then allow rules
5. Subshell contents (`$(...)` and `(...)`) are recursively validated
6. All segments must pass for the command to be allowed
**Examples:**
```bash
# Allow only npm and git commands.
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"]}'
# Allow development commands but deny dangerous ones. Deny not strictly required here since allow is set.
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *", "node *"], "deny": ["rm -rf *", "sudo *"]}'
# Allow file operations with redirects
export CLINE_COMMAND_PERMISSIONS='{"allow": ["cat *", "echo *"], "allowRedirects": true}'
```
# ARCHITECTURE
Cline operates on a three-layer architecture:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "cline",
"version": "1.0.9",
"version": "1.0.10",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "cline-core.js",
"bin": {
+21 -12
View File
@@ -3,6 +3,7 @@ package auth
import (
"context"
"fmt"
"os"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/display"
@@ -45,22 +46,29 @@ const (
// RunAuthFlow is the entry point for the entire auth flow with instance management
// It spawns a fresh instance for auth operations and cleans it up when done
func RunAuthFlow(ctx context.Context, args []string) error {
// Spawn a fresh instance for auth operations
instanceInfo, err := global.Clients.StartNewInstance(ctx)
// Get current working directory to use as workspace
// This ensures auth operations use the same state database as the main cline command
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}
// Spawn a fresh instance for auth operations with the current workspace
instanceInfo, err := global.Instances.StartNewInstance(ctx, cwd)
if err != nil {
return fmt.Errorf("failed to start auth instance: %w", err)
}
// Cleanup when done (success, error, or panic)
defer func() {
verboseLog("Shutting down auth instance at %s", instanceInfo.Address)
if err := global.KillInstanceByAddress(context.Background(), global.Clients.GetRegistry(), instanceInfo.Address); err != nil {
verboseLog("Shutting down auth instance at %s", instanceInfo.CoreAddress)
if err := global.KillInstanceByAddress(context.Background(), global.Instances.GetRegistry(), instanceInfo.CoreAddress); err != nil {
verboseLog("Warning: Failed to kill auth instance: %v", err)
}
}()
// Store instance address in context for all auth handlers to use
authCtx := context.WithValue(ctx, authInstanceAddressKey, instanceInfo.Address)
authCtx := context.WithValue(ctx, authInstanceAddressKey, instanceInfo.CoreAddress)
// Route to existing auth flow
return HandleAuthCommand(authCtx, args)
@@ -108,12 +116,10 @@ func HandleAuthMenuNoArgs(ctx context.Context) error {
// Get current provider config for display
var currentProvider string
var currentModel string
if manager, err := createTaskManager(ctx); err == nil {
if providerList, err := GetProviderConfigurations(ctx, manager); err == nil {
if providerList.ActProvider != nil {
currentProvider = GetProviderDisplayName(providerList.ActProvider.Provider)
currentModel = providerList.ActProvider.ModelID
}
if providerList, err := GetProviderConfigurations(ctx); err == nil {
if providerList.ActProvider != nil {
currentProvider = GetProviderDisplayName(providerList.ActProvider.Provider)
currentModel = providerList.ActProvider.ModelID
}
}
@@ -189,9 +195,12 @@ func ShowAuthMenuWithStatus(isClineAuthenticated bool, hasOrganizations bool, cu
var title string
renderer := display.NewRenderer(global.Config.OutputFormat)
// Always show Cline authentication status
// Show authentication status - distinguish between OAuth and API key auth
if isClineAuthenticated {
title = fmt.Sprintf("Cline Account: %s Authenticated\n", renderer.Green("✓"))
} else if currentProvider == "Cline (Official)" {
// Using Cline provider with API key (not OAuth)
title = fmt.Sprintf("Cline API Key: %s Configured\n", renderer.Green("✓"))
} else {
title = fmt.Sprintf("Cline Account: %s Not authenticated\n", renderer.Red("✗"))
}
+12 -10
View File
@@ -157,7 +157,7 @@ func validateQuickSetupProvider(providerID string) (cline.ApiProvider, error) {
// Provider not found - provide helpful error message
supportedProviders := []string{
"openai-native", "openai", "anthropic", "gemini",
"openrouter", "xai", "cerebras", "ollama",
"openrouter", "xai", "cerebras", "ollama", "vercel-ai-gateway", "cline",
}
return cline.ApiProvider_ANTHROPIC, fmt.Errorf(
"invalid provider '%s'. Supported providers: %s",
@@ -168,15 +168,17 @@ func validateQuickSetupProvider(providerID string) (cline.ApiProvider, error) {
// Validate against supported quick setup providers
supportedProviders := map[cline.ApiProvider]bool{
cline.ApiProvider_OPENAI_NATIVE: true,
cline.ApiProvider_OPENAI: true,
cline.ApiProvider_ANTHROPIC: true,
cline.ApiProvider_GEMINI: true,
cline.ApiProvider_OPENROUTER: true,
cline.ApiProvider_XAI: true,
cline.ApiProvider_CEREBRAS: true,
cline.ApiProvider_OLLAMA: true,
cline.ApiProvider_NOUSRESEARCH: true,
cline.ApiProvider_OPENAI_NATIVE: true,
cline.ApiProvider_OPENAI: true,
cline.ApiProvider_ANTHROPIC: true,
cline.ApiProvider_GEMINI: true,
cline.ApiProvider_OPENROUTER: true,
cline.ApiProvider_XAI: true,
cline.ApiProvider_CEREBRAS: true,
cline.ApiProvider_OLLAMA: true,
cline.ApiProvider_NOUSRESEARCH: true,
cline.ApiProvider_VERCEL_AI_GATEWAY: true,
cline.ApiProvider_CLINE: true,
}
if !supportedProviders[provider] {
+6 -1
View File
@@ -14,9 +14,9 @@ type BYOProviderOption struct {
}
// GetBYOProviderList returns the list of supported BYO providers for CLI configuration.
// This list excludes Cline provider which is handled separately.
func GetBYOProviderList() []BYOProviderOption {
return []BYOProviderOption{
{Name: "Cline (API Key)", Provider: cline.ApiProvider_CLINE},
{Name: "Anthropic", Provider: cline.ApiProvider_ANTHROPIC},
{Name: "OpenAI Compatible", Provider: cline.ApiProvider_OPENAI},
{Name: "OpenAI (Official)", Provider: cline.ApiProvider_OPENAI_NATIVE},
@@ -28,6 +28,7 @@ func GetBYOProviderList() []BYOProviderOption {
{Name: "Cerebras", Provider: cline.ApiProvider_CEREBRAS},
{Name: "NousResearch", Provider: cline.ApiProvider_NOUSRESEARCH},
{Name: "Oracle Code Assist", Provider: cline.ApiProvider_OCA},
{Name: "Vercel AI Gateway", Provider: cline.ApiProvider_VERCEL_AI_GATEWAY},
}
}
@@ -105,6 +106,10 @@ func GetBYOProviderPlaceholder(provider cline.ApiProvider) string {
return "e.g., Hermes-4-405B"
case cline.ApiProvider_OCA:
return "e.g., oca/llama4"
case cline.ApiProvider_VERCEL_AI_GATEWAY:
return "e.g., anthropic/claude-sonnet-4.5"
case cline.ApiProvider_CLINE:
return "e.g., anthropic/claude-sonnet-4.5"
default:
return "Enter model ID"
}
+33 -3
View File
@@ -10,6 +10,7 @@ import (
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
"github.com/cline/grpc-go/client"
)
// ProviderDisplay represents a configured provider for display purposes
@@ -29,13 +30,24 @@ type ProviderListResult struct {
}
// GetProviderConfigurations retrieves and parses provider configurations from Cline Core state
func GetProviderConfigurations(ctx context.Context, manager *task.Manager) (*ProviderListResult, error) {
func GetProviderConfigurations(ctx context.Context) (*ProviderListResult, error) {
if global.Config.Verbose {
fmt.Println("[DEBUG] Retrieving provider configurations from Cline Core")
}
// Get latest state from Cline Core
state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
// Use auth instance if available, otherwise fall back to default
var grpcClient *client.ClineClient
var err error
if authAddr, ok := ctx.Value(authInstanceAddressKey).(string); ok && authAddr != "" {
grpcClient, err = global.GetClientForAddress(ctx, authAddr)
} else {
grpcClient, err = global.GetDefaultClient(ctx)
}
if err != nil {
return nil, fmt.Errorf("failed to get provider configs due to unable to get gRPC client: %w", err)
}
state, err := grpcClient.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return nil, fmt.Errorf("failed to get state: %w", err)
}
@@ -114,6 +126,7 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
cline.ApiProvider_NOUSRESEARCH,
cline.ApiProvider_OCA,
cline.ApiProvider_HICAP,
cline.ApiProvider_VERCEL_AI_GATEWAY,
}
// Check each provider to see if it's ready to use
@@ -244,6 +257,8 @@ func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
return cline.ApiProvider_HICAP, true
case "nousResearch":
return cline.ApiProvider_NOUSRESEARCH, true
case "vercel-ai-gateway":
return cline.ApiProvider_VERCEL_AI_GATEWAY, true
default:
return cline.ApiProvider_ANTHROPIC, false // Return 0 value with false
}
@@ -279,6 +294,8 @@ func GetProviderIDForEnum(provider cline.ApiProvider) string {
return "hicap"
case cline.ApiProvider_NOUSRESEARCH:
return "nousResearch"
case cline.ApiProvider_VERCEL_AI_GATEWAY:
return "vercel-ai-gateway"
default:
return ""
}
@@ -364,6 +381,8 @@ func GetProviderDisplayName(provider cline.ApiProvider) string {
return "Hicap"
case cline.ApiProvider_NOUSRESEARCH:
return "NousResearch"
case cline.ApiProvider_VERCEL_AI_GATEWAY:
return "Vercel AI Gateway"
default:
return "Unknown"
}
@@ -460,11 +479,21 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
var configuredProviders []cline.ApiProvider
// Check for Cline provider (uses authentication instead of API key)
if IsAuthenticated(ctx) {
isOAuthAuth := IsAuthenticated(ctx)
if isOAuthAuth {
configuredProviders = append(configuredProviders, cline.ApiProvider_CLINE)
verboseLog("[DEBUG] Cline provider is authenticated")
}
// Also check for Cline API key (non-OAuth)
if clineKey, ok := apiConfig["clineApiKey"].(string); ok && clineKey != "" {
// Only add if not already added via OAuth
if !isOAuthAuth {
configuredProviders = append(configuredProviders, cline.ApiProvider_CLINE)
verboseLog("[DEBUG] Cline provider configured with API key")
}
}
// Check OCA provider via global auth subscription (state presence)
if state, _ := GetLatestOCAState(context.Background(), 2*time.Second); state != nil && state.User != nil {
configuredProviders = append(configuredProviders, cline.ApiProvider_OCA)
@@ -487,6 +516,7 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
{cline.ApiProvider_CEREBRAS, []string{"cerebrasApiKey"}},
{cline.ApiProvider_HICAP, []string{"hicapApiKey"}},
{cline.ApiProvider_NOUSRESEARCH, []string{"nousResearchApiKey"}},
{cline.ApiProvider_VERCEL_AI_GATEWAY, []string{"vercelAiGatewayApiKey"}},
}
for _, providerCheck := range providersToCheck {
@@ -174,6 +174,17 @@ func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
ActModeProviderSpecificModelIDField: "actModeNousResearchModelId",
}, nil
case cline.ApiProvider_VERCEL_AI_GATEWAY:
return ProviderFields{
APIKeyField: "vercelAiGatewayApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeModelInfoField: "planModeVercelAiGatewayModelInfo",
ActModeModelInfoField: "actModeVercelAiGatewayModelInfo",
PlanModeProviderSpecificModelIDField: "planModeVercelAiGatewayModelId",
ActModeProviderSpecificModelIDField: "actModeVercelAiGatewayModelId",
}, nil
default:
return ProviderFields{}, fmt.Errorf("unsupported provider: %v", provider)
}
@@ -291,6 +302,8 @@ func setAPIKeyField(apiConfig *cline.ModelsApiConfiguration, fieldName string, v
apiConfig.HicapApiKey = value
case "nousResearchApiKey":
apiConfig.NousResearchApiKey = value
case "vercelAiGatewayApiKey":
apiConfig.VercelAiGatewayApiKey = value
}
}
@@ -318,6 +331,9 @@ func setProviderSpecificModelID(apiConfig *cline.ModelsApiConfiguration, fieldNa
case "planModeNousResearchModelId":
apiConfig.PlanModeNousResearchModelId = value
apiConfig.ActModeNousResearchModelId = value
case "planModeVercelAiGatewayModelId":
apiConfig.PlanModeVercelAiGatewayModelId = value
apiConfig.ActModeVercelAiGatewayModelId = value
}
}
+5 -6
View File
@@ -215,7 +215,7 @@ func (pw *ProviderWizard) handleAddOcaProvider() error {
// handleListProviders retrieves and displays configured providers
func (pw *ProviderWizard) handleListProviders() error {
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
result, err := GetProviderConfigurations(pw.ctx)
if err != nil {
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
}
@@ -413,7 +413,7 @@ func (pw *ProviderWizard) manualModelEntry(provider cline.ApiProvider) (string,
// handleChangeModel allows changing the model for any configured provider
func (pw *ProviderWizard) handleChangeModel() error {
// Step 1: Get current provider configurations
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
result, err := GetProviderConfigurations(pw.ctx)
if err != nil {
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
}
@@ -585,11 +585,11 @@ func getProviderModelIDFromState(stateData map[string]interface{}, provider clin
return ""
}
// getProviderAPIKeyFromState retrieves the API key for a specific provider from state
// getProviderAPIKeyFromState retrieves the API key for a specific provider from state
func getProviderAPIKeyFromState(stateData map[string]interface{}, provider cline.ApiProvider) string {
// OCA uses account authentication, not API keys. Consider it "present" if authenticated.
if provider == cline.ApiProvider_OCA {
if state, _ := GetLatestOCAState(context.TODO(), 2 * time.Second); state != nil && state.User != nil {
if state, _ := GetLatestOCAState(context.TODO(), 2*time.Second); state != nil && state.User != nil {
// Return a sentinel non-empty string so upstream checks pass.
return "OCA_AUTH_VERIFIED"
}
@@ -648,7 +648,7 @@ func convertMapToOpenRouterModelInfo(data map[string]interface{}) *cline.OpenRou
// handleRemoveProvider allows removing a configured provider by clearing its API key
func (pw *ProviderWizard) handleRemoveProvider() error {
// Step 1: Get current provider configurations
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
result, err := GetProviderConfigurations(pw.ctx)
if err != nil {
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
}
@@ -748,7 +748,6 @@ func (pw *ProviderWizard) clearProviderAPIKey(provider cline.ApiProvider) error
return RemoveProviderPartial(pw.ctx, pw.manager, provider)
}
func signOutOca(ctx context.Context) error {
client, err := global.GetDefaultClient(ctx)
if err != nil {
+3 -3
View File
@@ -40,7 +40,7 @@ func ensureConfigManager(ctx context.Context, address string) error {
}
// Always set the instance we're using as the default
registry := global.Clients.GetRegistry()
registry := global.Instances.GetRegistry()
if err := registry.SetDefaultInstance(instanceAddress); err != nil {
// Log warning but don't fail - this is not critical
fmt.Printf("Warning: failed to set default instance: %v\n", err)
@@ -123,11 +123,11 @@ func setCommand() *cobra.Command {
Use: "set <key=value> [key=value...]",
Aliases: []string{"s"},
Short: "Set configuration variables",
Long: `Set one or more global configuration variables using key=value format.
Long: `Set one or more global configuration variables using key=value format.
This command merges the provided settings with existing values, preserving
unspecified fields. Only the fields you explicitly set will be updated.`,
Args: cobra.MinimumNArgs(1),
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
+2 -3
View File
@@ -32,8 +32,8 @@ func NewManager(ctx context.Context, address string) (*Manager, error) {
// Get the actual address being used
clientAddress := address
if address == "" && global.Clients != nil {
clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
if address == "" && global.Instances != nil {
clientAddress = global.Instances.GetRegistry().GetDefaultInstance()
}
return &Manager{
@@ -110,7 +110,6 @@ func (m *Manager) ListSettings(ctx context.Context) error {
"dictationSettings",
"autoCondenseThreshold",
"autoApprovalSettings",
"hooksEnabled",
}
// Render each field using the renderer
+1 -1
View File
@@ -79,7 +79,7 @@ func RenderField(key string, value interface{}, censor bool) error {
"planActSeparateModelsSetting", "enableCheckpointsSetting",
"terminalReuseEnabled", "mcpResponsesCollapsed", "strictPlanModeEnabled",
"useAutoCondense", "yoloModeToggled", "shellIntegrationTimeout",
"terminalOutputLineLimit", "autoCondenseThreshold", "hooksEnabled":
"terminalOutputLineLimit", "autoCondenseThreshold":
fmt.Printf("%s: %s\n", camelToKebab(key), formatValue(value, key, censor))
return nil
@@ -14,21 +14,20 @@ import (
"github.com/cline/grpc-go/cline"
)
// ClineClients manages Cline instances using the new registry system
type ClineClients struct {
registry *ClientRegistry
type ClineInstances struct {
registry *InstanceRegistry
}
// NewClineClients creates a new ClineClients instance
func NewClineClients(configPath string) *ClineClients {
registry := NewClientRegistry(configPath)
return &ClineClients{
// NewClineInstances creates a new ClineInstances instance
func NewClineInstances(configPath string) *ClineInstances {
registry := NewInstanceRegistry(configPath)
return &ClineInstances{
registry: registry,
}
}
// Initialize performs cleanup of stale instances
func (c *ClineClients) Initialize(ctx context.Context) error {
func (c *ClineInstances) Initialize(ctx context.Context) error {
// Clean up stale entries (direct SQLite operations)
_ = c.registry.CleanupStaleInstances(ctx)
@@ -36,7 +35,8 @@ func (c *ClineClients) Initialize(ctx context.Context) error {
}
// StartNewInstance starts a new Cline instance and waits for cline-core to self-register
func (c *ClineClients) StartNewInstance(ctx context.Context, workspaces ...string) (*common.CoreInstanceInfo, error) {
// An "instance" is a pair of cline-core and cline-host processes
func (c *ClineInstances) StartNewInstance(ctx context.Context, workspaces ...string) (*common.CoreInstanceInfo, error) {
// Find available ports
corePort, hostPort, err := common.FindAvailablePortPair()
if err != nil {
@@ -102,7 +102,7 @@ func (c *ClineClients) StartNewInstance(ctx context.Context, workspaces ...strin
if Config.Verbose {
fmt.Println("Services started and registered successfully!")
fmt.Printf(" Address: %s\n", instance.Address)
fmt.Printf(" Address: %s\n", instance.CoreAddress)
fmt.Printf(" Core Port: %d\n", instance.CorePort())
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
fmt.Printf(" Process PID: %d\n", coreCmd.Process.Pid)
@@ -120,7 +120,7 @@ func (c *ClineClients) StartNewInstance(ctx context.Context, workspaces ...strin
}
// StartNewInstanceAtPort starts a new Cline instance at the specified port and waits for self-registration
func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int, workspaces ...string) (*common.CoreInstanceInfo, error) {
func (c *ClineInstances) StartNewInstanceAtPort(ctx context.Context, corePort int, workspaces ...string) (*common.CoreInstanceInfo, error) {
// Find available host port (core port + 1000)
hostPort := corePort + 1000
coreAddress := fmt.Sprintf("localhost:%d", corePort)
@@ -189,7 +189,7 @@ func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int,
if Config.Verbose {
fmt.Println("Services started and registered successfully!")
fmt.Printf(" Address: %s\n", instance.Address)
fmt.Printf(" Address: %s\n", instance.CoreAddress)
fmt.Printf(" Core Port: %d\n", instance.CorePort())
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
fmt.Printf(" Process PID: %d\n", coreCmd.Process.Pid)
@@ -207,12 +207,12 @@ func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int,
}
// GetRegistry returns the client registry
func (c *ClineClients) GetRegistry() *ClientRegistry {
func (c *ClineInstances) GetRegistry() *InstanceRegistry {
return c.registry
}
// EnsureInstanceAtAddress ensures an instance exists at the given address, starting one if needed
func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address string) error {
func (c *ClineInstances) EnsureInstanceAtAddress(ctx context.Context, address string) error {
// Expect host:port everywhere
normalized := address
if normalized == "" {
@@ -230,7 +230,7 @@ func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address stri
return fmt.Errorf("invalid address format %s", address)
}
// Use IPv6-compatible localhost detection
// Use IPv6-compatible localhost detection
if common.IsLocalAddress(host) {
_, err := c.StartNewInstanceAtPort(ctx, port)
if err != nil {
@@ -305,7 +305,7 @@ func startClineHost(hostPort int, workspaces []string) (*exec.Cmd, error) {
}
// KillInstanceByAddress kills a Cline instance by its address
func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, address string) error {
func KillInstanceByAddress(ctx context.Context, registry *InstanceRegistry, address string) error {
// Check if the instance exists in the registry
_, err := registry.GetInstance(address)
if err != nil {
@@ -355,9 +355,9 @@ func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, addres
defaultInstance := registry.GetDefaultInstance()
if defaultInstance == address || defaultInstance == "" {
if len(instances) > 0 {
if err := registry.SetDefaultInstance(instances[0].Address); err == nil {
if err := registry.SetDefaultInstance(instances[0].CoreAddress); err == nil {
if Config.Verbose {
fmt.Printf("Updated default instance to: %s\n", instances[0].Address)
fmt.Printf("Updated default instance to: %s\n", instances[0].CoreAddress)
}
}
}
+21 -15
View File
@@ -22,8 +22,8 @@ type GlobalConfig struct {
}
var (
Config *GlobalConfig
Clients *ClineClients
Config *GlobalConfig
Instances *ClineInstances
// Version info - set at build time via ldflags
// Version is the Cline Core version (from root package.json)
@@ -37,11 +37,16 @@ var (
func InitializeGlobalConfig(cfg *GlobalConfig) error {
if cfg.ConfigPath == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
// Check CLINE_DIR environment variable first
if clineDir := os.Getenv("CLINE_DIR"); clineDir != "" {
cfg.ConfigPath = clineDir
} else {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
cfg.ConfigPath = filepath.Join(homeDir, ".cline")
}
cfg.ConfigPath = filepath.Join(homeDir, ".cline")
}
// Ensure .cline directory exists
@@ -56,11 +61,11 @@ func InitializeGlobalConfig(cfg *GlobalConfig) error {
// Otherwise lipgloss auto-detects terminal capabilities (default behavior)
Config = cfg
Clients = NewClineClients(cfg.ConfigPath)
Instances = NewClineInstances(cfg.ConfigPath)
// Initialize the clients registry
ctx := context.Background()
if err := Clients.Initialize(ctx); err != nil {
if err := Instances.Initialize(ctx); err != nil {
return fmt.Errorf("failed to initialize clients: %w", err)
}
@@ -71,25 +76,25 @@ func InitializeGlobalConfig(cfg *GlobalConfig) error {
func GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
if Config.CoreAddress != "" && Config.CoreAddress != fmt.Sprintf("localhost:%d", common.DEFAULT_CLINE_CORE_PORT) {
// User specified a specific address, use that
return Clients.GetRegistry().GetClient(ctx, Config.CoreAddress)
return Instances.GetRegistry().GetClient(ctx, Config.CoreAddress)
}
// Use the default instance from registry
return Clients.GetRegistry().GetDefaultClient(ctx)
return Instances.GetRegistry().GetDefaultClient(ctx)
}
// GetClientForAddress returns a client for a specific address
func GetClientForAddress(ctx context.Context, address string) (*client.ClineClient, error) {
return Clients.GetRegistry().GetClient(ctx, address)
return Instances.GetRegistry().GetClient(ctx, address)
}
// EnsureDefaultInstance ensures a default instance exists
func EnsureDefaultInstance(ctx context.Context) error {
if Clients == nil {
if Instances == nil {
return fmt.Errorf("global clients not initialized")
}
registry := Clients.GetRegistry()
registry := Instances.GetRegistry()
// First, check if there are any instances already registered in SQLite
instances := registry.ListInstances()
@@ -103,11 +108,12 @@ func EnsureDefaultInstance(ctx context.Context) error {
if registry.GetDefaultInstance() == "" {
// No instances exist, start a new one
// Note: StartNewInstance will automatically set it as default since it's the first instance
_, err := Clients.StartNewInstance(ctx)
_, err := Instances.StartNewInstance(ctx)
if err != nil {
return fmt.Errorf("failed to start new default instance: %w", err)
}
}
return nil
}
}
+27 -28
View File
@@ -18,28 +18,28 @@ import (
"google.golang.org/grpc/health/grpc_health_v1"
)
// ClientRegistry manages Cline client connections using direct SQLite operations
type ClientRegistry struct {
// InstanceRegistry manages Cline client connections using direct SQLite operations
type InstanceRegistry struct {
lockManager *sqlite.LockManager
configPath string
}
// NewClientRegistry creates a new client registry
func NewClientRegistry(configPath string) *ClientRegistry {
// NewInstanceRegistry creates a new instance registry
func NewInstanceRegistry(configPath string) *InstanceRegistry {
lockManager, err := sqlite.NewLockManager(configPath)
if err != nil {
// Log error but continue - we can still function without SQLite
log.Fatalf("Warning: Failed to initialize SQLite lock manager: %v\n", err)
}
return &ClientRegistry{
return &InstanceRegistry{
lockManager: lockManager,
configPath: configPath,
}
}
// GetDefaultInstance returns the default instance address from settings file
func (r *ClientRegistry) GetDefaultInstance() string {
func (r *InstanceRegistry) GetDefaultInstance() string {
defaultAddr, err := sqlite.GetDefaultInstance(r.configPath)
if err != nil {
return ""
@@ -48,7 +48,7 @@ func (r *ClientRegistry) GetDefaultInstance() string {
}
// SetDefaultInstance sets the default instance (writes default.json)
func (r *ClientRegistry) SetDefaultInstance(address string) error {
func (r *InstanceRegistry) SetDefaultInstance(address string) error {
// Verify the instance exists in SQLite
if r.lockManager != nil {
exists, err := r.lockManager.HasInstanceAtAddress(address)
@@ -64,7 +64,7 @@ func (r *ClientRegistry) SetDefaultInstance(address string) error {
}
// GetInstance returns instance information directly from SQLite
func (r *ClientRegistry) GetInstance(address string) (*common.CoreInstanceInfo, error) {
func (r *InstanceRegistry) GetInstance(address string) (*common.CoreInstanceInfo, error) {
if r.lockManager == nil {
return nil, fmt.Errorf("lock manager not available")
}
@@ -73,7 +73,7 @@ func (r *ClientRegistry) GetInstance(address string) (*common.CoreInstanceInfo,
}
// GetClient returns a connected client for the given address (created on-demand)
func (r *ClientRegistry) GetClient(ctx context.Context, address string) (*client.ClineClient, error) {
func (r *InstanceRegistry) GetClient(ctx context.Context, address string) (*client.ClineClient, error) {
// Verify instance exists in SQLite
if r.lockManager != nil {
exists, err := r.lockManager.HasInstanceAtAddress(address)
@@ -104,7 +104,7 @@ func (r *ClientRegistry) GetClient(ctx context.Context, address string) (*client
}
// GetDefaultClient returns a client for the default instance
func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
func (r *InstanceRegistry) GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
defaultAddr := r.GetDefaultInstance()
if defaultAddr == "" {
return nil, fmt.Errorf("no default instance configured")
@@ -117,7 +117,7 @@ func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineCli
// Database is unavailable - Return error instead of attempting cleanup
return nil, fmt.Errorf("cannot verify default instance: database unavailable: %w", err)
}
if !exists {
// Instance doesn't exist in database but config file references it
// This is a stale config - remove it and try to find another instance
@@ -127,14 +127,14 @@ func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineCli
} else {
fmt.Printf("Removed stale default instance config (instance %s not found in database)\n", defaultAddr)
}
// Try to find and set a new default instance
instances := r.ListInstances()
if len(instances) > 0 {
if err := r.EnsureDefaultInstance(instances); err != nil {
return nil, fmt.Errorf("failed to set new default instance: %w", err)
}
// Retry with the new default
newDefaultAddr := r.GetDefaultInstance()
if newDefaultAddr != "" {
@@ -142,7 +142,7 @@ func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineCli
return r.GetClient(ctx, newDefaultAddr)
}
}
return nil, fmt.Errorf("no default instance configured")
}
}
@@ -151,7 +151,7 @@ func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineCli
}
// ListInstances returns all registered instances directly from SQLite
func (r *ClientRegistry) ListInstances() []*common.CoreInstanceInfo {
func (r *InstanceRegistry) ListInstances() []*common.CoreInstanceInfo {
if r.lockManager == nil {
return []*common.CoreInstanceInfo{}
}
@@ -170,7 +170,7 @@ func (r *ClientRegistry) ListInstances() []*common.CoreInstanceInfo {
}
// HasInstanceAtAddress checks if an instance exists at the given address (delegates to SQLite)
func (r *ClientRegistry) HasInstanceAtAddress(address string) bool {
func (r *InstanceRegistry) HasInstanceAtAddress(address string) bool {
if r.lockManager == nil {
return false
}
@@ -185,7 +185,7 @@ func (r *ClientRegistry) HasInstanceAtAddress(address string) bool {
}
// CleanupStaleInstances removes stale instances using direct SQLite operations
func (r *ClientRegistry) CleanupStaleInstances(ctx context.Context) error {
func (r *InstanceRegistry) CleanupStaleInstances(ctx context.Context) error {
if r.lockManager == nil {
return nil
}
@@ -202,15 +202,15 @@ func (r *ClientRegistry) CleanupStaleInstances(ctx context.Context) error {
// Try to gracefully shutdown the paired host process before cleanup
fmt.Printf("Attempting to shutdown dangling host service %s for stale cline core instance %s\n",
instance.HostServiceAddress, instance.Address)
instance.HostServiceAddress, instance.CoreAddress)
r.tryShutdownHostProcess(instance.HostServiceAddress)
// Remove from SQLite database
if err := r.lockManager.RemoveInstanceLock(instance.Address); err != nil {
return fmt.Errorf("failed to remove stale instance %s: %w", instance.Address, err)
if err := r.lockManager.RemoveInstanceLock(instance.CoreAddress); err != nil {
return fmt.Errorf("failed to remove stale instance %s: %w", instance.CoreAddress, err)
}
fmt.Printf("Removed stale instance: %s\n", instance.Address)
fmt.Printf("Removed stale instance: %s\n", instance.CoreAddress)
}
}
@@ -218,8 +218,7 @@ func (r *ClientRegistry) CleanupStaleInstances(ctx context.Context) error {
}
// tryShutdownHostProcess attempts to gracefully shutdown a host process via RPC
// Best effort, don't throw errors i guess
func (r *ClientRegistry) tryShutdownHostProcess(hostServiceAddress string) {
func (r *InstanceRegistry) tryShutdownHostProcess(hostServiceAddress string) {
err := common.RetryOperation(3, 2*time.Second, func() error {
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
@@ -252,7 +251,7 @@ func (r *ClientRegistry) tryShutdownHostProcess(hostServiceAddress string) {
}
// ListInstancesCleaned performs cleanup and returns instances with health checks
func (r *ClientRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.CoreInstanceInfo, error) {
func (r *InstanceRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.CoreInstanceInfo, error) {
// 1. Clean up stale entries (best-effort)
_ = r.CleanupStaleInstances(ctx)
@@ -268,7 +267,7 @@ func (r *ClientRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.Co
}
// EnsureDefaultInstance ensures a default instance is set if instances exist but no default is configured
func (r *ClientRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceInfo) error {
func (r *InstanceRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceInfo) error {
currentDefault := r.GetDefaultInstance()
// If we have no instances, clear any stale default and remove settings file
@@ -283,13 +282,13 @@ func (r *ClientRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceI
// If we have instances but no default, pick the first one
if currentDefault == "" {
return sqlite.SetDefaultInstance(r.configPath, instances[0].Address)
return sqlite.SetDefaultInstance(r.configPath, instances[0].CoreAddress)
}
// Validate current default still exists in the instances
defaultExists := false
for _, instance := range instances {
if instance.Address == currentDefault {
if instance.CoreAddress == currentDefault {
defaultExists = true
break
}
@@ -297,7 +296,7 @@ func (r *ClientRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceI
if !defaultExists {
// Current default doesn't exist, pick a new one from available instances
return sqlite.SetDefaultInstance(r.configPath, instances[0].Address)
return sqlite.SetDefaultInstance(r.configPath, instances[0].CoreAddress)
}
return nil
+25 -25
View File
@@ -92,12 +92,12 @@ func newInstanceKillCommand() *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if global.Clients == nil {
if global.Instances == nil {
return fmt.Errorf("clients not initialized")
}
ctx := cmd.Context()
registry := global.Clients.GetRegistry()
registry := global.Instances.GetRegistry()
if killAllCLI {
return killAllCLIInstances(ctx, registry)
@@ -112,7 +112,7 @@ func newInstanceKillCommand() *cobra.Command {
return cmd
}
func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) error {
func killAllCLIInstances(ctx context.Context, registry *global.InstanceRegistry) error {
// Get all instances from registry
instances, err := registry.ListInstancesCleaned(ctx)
if err != nil {
@@ -135,7 +135,7 @@ func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) e
cliInstances = append(cliInstances, instance)
} else {
skippedNonCLI++
fmt.Printf("⊘ Skipping %s instance: %s\n", platform, instance.Address)
fmt.Printf("⊘ Skipping %s instance: %s\n", platform, instance.CoreAddress)
}
}
}
@@ -160,16 +160,16 @@ func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) e
// Kill all CLI instances
for _, instance := range cliInstances {
result := killInstanceProcess(ctx, registry, instance.Address)
result := killInstanceProcess(ctx, registry, instance.CoreAddress)
killResults = append(killResults, result)
if result.err != nil {
fmt.Printf("✗ Failed to kill %s: %v\n", instance.Address, result.err)
fmt.Printf("✗ Failed to kill %s: %v\n", instance.CoreAddress, result.err)
} else if result.alreadyDead {
fmt.Printf("⚠ Instance %s appears to be already dead\n", instance.Address)
fmt.Printf("⚠ Instance %s appears to be already dead\n", instance.CoreAddress)
} else {
fmt.Printf("✓ Killed %s (PID %d)\n", instance.Address, result.pid)
killedAddresses[instance.Address] = true
fmt.Printf("✓ Killed %s (PID %d)\n", instance.CoreAddress, result.pid)
killedAddresses[instance.CoreAddress] = true
}
}
@@ -190,8 +190,8 @@ func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) e
// Check if any of the killed instances are still in the registry
stillPresent := []string{}
for _, remaining := range remainingInstances {
if killedAddresses[remaining.Address] {
stillPresent = append(stillPresent, remaining.Address)
if killedAddresses[remaining.CoreAddress] {
stillPresent = append(stillPresent, remaining.CoreAddress)
}
}
@@ -247,7 +247,7 @@ type killResult struct {
err error
}
func killInstanceProcess(ctx context.Context, registry *global.ClientRegistry, address string) killResult {
func killInstanceProcess(ctx context.Context, registry *global.InstanceRegistry, address string) killResult {
// Get gRPC client and process info
client, err := registry.GetClient(ctx, address)
if err != nil {
@@ -276,12 +276,12 @@ func newInstanceListCommand() *cobra.Command {
Short: "List all registered Cline instances",
Long: `List all registered Cline instances with their status and connection details.`,
RunE: func(cmd *cobra.Command, args []string) error {
if global.Clients == nil {
if global.Instances == nil {
return fmt.Errorf("clients not initialized")
}
ctx := cmd.Context()
registry := global.Clients.GetRegistry()
registry := global.Instances.GetRegistry()
// Load, cleanup stale local entries, and update health
instances, err := registry.ListInstancesCleaned(ctx)
@@ -310,7 +310,7 @@ func newInstanceListCommand() *cobra.Command {
var rows []instanceRow
for _, instance := range instances {
isDefault := ""
if instance.Address == defaultInstance {
if instance.CoreAddress == defaultInstance {
isDefault = "✓"
}
@@ -324,7 +324,7 @@ func newInstanceListCommand() *cobra.Command {
platform := platformNA
if instance.Status == grpc_health_v1.HealthCheckResponse_SERVING {
// Get PID from core
if client, err := registry.GetClient(ctx, instance.Address); err == nil {
if client, err := registry.GetClient(ctx, instance.CoreAddress); err == nil {
if processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{}); err == nil {
pid = fmt.Sprintf("%d", processInfo.ProcessId)
// Update version from RPC if available
@@ -341,7 +341,7 @@ func newInstanceListCommand() *cobra.Command {
}
rows = append(rows, instanceRow{
address: instance.Address,
address: instance.CoreAddress,
status: instance.Status.String(),
version: instance.Version,
lastSeen: lastSeen,
@@ -428,11 +428,11 @@ func newInstanceDefaultCommand() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
address := args[0]
if global.Clients == nil {
if global.Instances == nil {
return fmt.Errorf("clients not initialized")
}
registry := global.Clients.GetRegistry()
registry := global.Instances.GetRegistry()
// Verify the instance exists
_, err := registry.GetInstance(address)
@@ -464,34 +464,34 @@ func newInstanceNewCommand() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if global.Clients == nil {
if global.Instances == nil {
return fmt.Errorf("clients not initialized")
}
fmt.Println("Starting new Cline instance...")
instance, err := global.Clients.StartNewInstance(ctx)
instance, err := global.Instances.StartNewInstance(ctx)
if err != nil {
return fmt.Errorf("failed to start instance: %w", err)
}
fmt.Printf("Successfully started new instance:\n")
fmt.Printf(" Address: %s\n", instance.Address)
fmt.Printf(" Address: %s\n", instance.CoreAddress)
fmt.Printf(" Core Port: %d\n", instance.CorePort())
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
registry := global.Clients.GetRegistry()
registry := global.Instances.GetRegistry()
// If --default flag provided, set this instance as the default
if setDefault {
if err := registry.SetDefaultInstance(instance.Address); err != nil {
if err := registry.SetDefaultInstance(instance.CoreAddress); err != nil {
fmt.Printf("Warning: Failed to set as default: %v\n", err)
} else {
fmt.Printf(" Status: Set as default instance\n")
}
} else {
// Otherwise, check if EnsureDefaultInstance already set it as default
if registry.GetDefaultInstance() == instance.Address {
if registry.GetDefaultInstance() == instance.CoreAddress {
fmt.Printf(" Status: Default instance\n")
}
}
+72 -1
View File
@@ -348,7 +348,7 @@ func (m *InputModel) handleSubmit() (tea.Model, tea.Cmd) {
return ChangeInputTypeMsg{
InputType: InputTypeFeedback,
Title: "Your feedback",
Placeholder: "/plan or /act to switch modes\nctrl+e to open editor",
Placeholder: "/plan or /act to switch modes\nctrl+e to open editor\nctrl+c to exit",
}
}
}
@@ -547,3 +547,74 @@ func (m *InputModel) openEditor() tea.Cmd {
func (m *InputModel) SetSlashRegistry(registry *slash.Registry) {
m.completion.SetRegistry(registry)
}
// initialPromptWrapper wraps InputModel to capture the submit result for initial task prompts
type initialPromptWrapper struct {
model *InputModel
result string
cancelled bool
}
func (w *initialPromptWrapper) Init() tea.Cmd {
return w.model.Init()
}
func (w *initialPromptWrapper) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case InputSubmitMsg:
w.result = msg.Value
clearCodes := w.model.ClearScreen()
if clearCodes != "" {
fmt.Print(clearCodes)
}
return w, tea.Quit
case InputCancelMsg:
w.cancelled = true
clearCodes := w.model.ClearScreen()
if clearCodes != "" {
fmt.Print(clearCodes)
}
return w, tea.Quit
}
// Forward to wrapped model
_, cmd := w.model.Update(msg)
return w, cmd
}
func (w *initialPromptWrapper) View() string {
return w.model.View()
}
// ErrUserAborted is returned when the user cancels the input prompt
var ErrUserAborted = fmt.Errorf("user aborted")
// PromptForInitialTask displays an interactive prompt for the initial task with slash command autocomplete.
// Returns the entered text, or ErrUserAborted if cancelled.
func PromptForInitialTask(title, placeholder, mode string, registry *slash.Registry) (string, error) {
model := NewInputModelWithRegistry(
InputTypeMessage,
title,
placeholder,
mode,
registry,
)
wrapper := &initialPromptWrapper{
model: &model,
}
p := tea.NewProgram(wrapper)
_, err := p.Run()
if err != nil {
return "", fmt.Errorf("input prompt failed: %w", err)
}
if wrapper.cancelled {
return "", ErrUserAborted
}
return strings.TrimSpace(wrapper.result), nil
}
+39 -22
View File
@@ -2,10 +2,11 @@ package slash
import (
"context"
"fmt"
"strings"
"sync"
"github.com/cline/grpc-go/client"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/grpc-go/cline"
)
@@ -32,25 +33,31 @@ var cliLocalCommands = []Command{
}
// NewRegistry creates a new slash command registry
func NewRegistry() *Registry {
return &Registry{
commands: make([]Command, 0),
func NewRegistry(ctx context.Context) *Registry {
defaultCommands := append([]Command{}, cliLocalCommands...)
r := &Registry{
commands: defaultCommands,
}
r.FetchFromBackend(ctx)
return r
}
// FetchFromBackend fetches available commands from cline-core backend
func (r *Registry) FetchFromBackend(ctx context.Context, c *client.ClineClient) error {
resp, err := c.Slash.GetAvailableSlashCommands(ctx, &cline.EmptyRequest{})
if err != nil {
return err
func (r *Registry) FetchFromBackend(ctx context.Context) error {
grpcClient, err := global.GetDefaultClient(ctx)
if err != nil && global.Config.Verbose {
fmt.Printf("Warning: could not get gRPC client: %v\n", err)
return nil
}
resp, err := grpcClient.Slash.GetAvailableSlashCommands(ctx, &cline.EmptyRequest{})
if err != nil && global.Config.Verbose {
fmt.Printf("Warning: could not get gRPC client: %v\n", err)
return nil
}
r.mu.Lock()
defer r.mu.Unlock()
// Start with CLI-local commands
r.commands = append([]Command{}, cliLocalCommands...)
// Add backend commands (only CLI-compatible ones)
for _, cmd := range resp.Commands {
if cmd.CliCompatible {
@@ -66,17 +73,6 @@ func (r *Registry) FetchFromBackend(ctx context.Context, c *client.ClineClient)
return nil
}
// GetCommands returns all available commands
func (r *Registry) GetCommands() []Command {
r.mu.RLock()
defer r.mu.RUnlock()
// Return a copy to avoid race conditions
result := make([]Command, len(r.commands))
copy(result, r.commands)
return result
}
// GetMatching returns commands that start with the given prefix (case-insensitive)
func (r *Registry) GetMatching(prefix string) []Command {
r.mu.RLock()
@@ -123,3 +119,24 @@ func (r *Registry) HasCommands() bool {
defer r.mu.RUnlock()
return len(r.commands) > 0
}
// ParseModeSwitch checks if message starts with /act or /plan and extracts the mode and remaining message.
// Returns (mode, remainingMessage, isModeSwitch).
// This is a package-level function so it can be used both during initial task creation
// and during interactive input handling.
func ParseModeSwitch(message string) (string, string, bool) {
trimmed := strings.TrimSpace(message)
lower := strings.ToLower(trimmed)
if strings.HasPrefix(lower, "/plan") {
remaining := strings.TrimSpace(trimmed[5:])
return "plan", remaining, true
}
if strings.HasPrefix(lower, "/act") {
remaining := strings.TrimSpace(trimmed[4:])
return "act", remaining, true
}
return "", message, false
}
+7 -7
View File
@@ -19,20 +19,20 @@ import (
// Handles localhost/127.0.0.1 equivalence by returning both forms.
func normalizeAddressVariants(address string) []string {
variants := []string{address}
// Extract host and port
host, port, err := net.SplitHostPort(address)
if err != nil {
return variants
}
// Add the alternate form for localhost/127.0.0.1
if host == "localhost" {
variants = append(variants, net.JoinHostPort("127.0.0.1", port))
} else if host == "127.0.0.1" {
variants = append(variants, net.JoinHostPort("localhost", port))
}
return variants
}
@@ -182,7 +182,7 @@ func (lm *LockManager) GetInstanceInfo(address string) (*common.CoreInstanceInfo
query := common.SelectInstanceLockByHolderSQL
variants := normalizeAddressVariants(address)
var heldBy, lockTarget string
var lockedAt int64
var lastErr error
@@ -193,7 +193,7 @@ func (lm *LockManager) GetInstanceInfo(address string) (*common.CoreInstanceInfo
if err == nil {
// Found it!
return &common.CoreInstanceInfo{
Address: heldBy,
CoreAddress: heldBy,
HostServiceAddress: lockTarget,
Status: grpc_health_v1.HealthCheckResponse_UNKNOWN,
LastSeen: time.Unix(lockedAt/1000, 0),
@@ -204,7 +204,7 @@ func (lm *LockManager) GetInstanceInfo(address string) (*common.CoreInstanceInfo
lastErr = err
}
}
// None of the variants were found
if lastErr != nil {
return nil, fmt.Errorf("failed to query instance: %w", lastErr)
@@ -235,7 +235,7 @@ func (lm *LockManager) ListInstancesWithHealthCheck(ctx context.Context) ([]*com
}
info := &common.CoreInstanceInfo{
Address: lock.HeldBy,
CoreAddress: lock.HeldBy,
HostServiceAddress: lock.LockTarget,
Status: status,
LastSeen: time.Unix(lock.LockedAt/1000, 0),
+5 -10
View File
@@ -80,7 +80,7 @@ func ensureTaskManager(ctx context.Context, address string) error {
}
// Always set the instance we're using as the default
registry := global.Clients.GetRegistry()
registry := global.Instances.GetRegistry()
if err := registry.SetDefaultInstance(instanceAddress); err != nil {
// Log warning but don't fail - this is not critical
fmt.Printf("Warning: failed to set default instance: %v\n", err)
@@ -91,10 +91,10 @@ func ensureTaskManager(ctx context.Context, address string) error {
// ensureInstanceAtAddress ensures an instance exists at the given address
func ensureInstanceAtAddress(ctx context.Context, address string) error {
if global.Clients == nil {
if global.Instances == nil {
return fmt.Errorf("global clients not initialized")
}
return global.Clients.EnsureInstanceAtAddress(ctx, address)
return global.Instances.EnsureInstanceAtAddress(ctx, address)
}
func newTaskNewCommand() *cobra.Command {
@@ -117,7 +117,7 @@ func newTaskNewCommand() *cobra.Command {
ctx := cmd.Context()
// Check if an instance exists when no address specified
if address == "" && global.Clients.GetRegistry().GetDefaultInstance() == "" {
if address == "" && global.Instances.GetRegistry().GetDefaultInstance() == "" {
fmt.Println("No instances available for creating tasks")
return nil
}
@@ -230,7 +230,7 @@ func newTaskSendCommand() *cobra.Command {
ctx := cmd.Context()
// Check if an instance exists when no address specified
if address == "" && global.Clients.GetRegistry().GetDefaultInstance() == "" {
if address == "" && global.Instances.GetRegistry().GetDefaultInstance() == "" {
fmt.Println("No instances available for sending messages")
return nil
}
@@ -620,11 +620,6 @@ func CleanupTaskManager() {
}
}
// NewTaskManagerForAddress is an exported wrapper around task.NewManagerForAddress
func NewTaskManagerForAddress(ctx context.Context, address string) (*task.Manager, error) {
return task.NewManagerForAddress(ctx, address)
}
// CreateAndFollowTask creates a new task and immediately follows it in interactive mode
// This is used by the root command to provide a streamlined UX
func CreateAndFollowTask(ctx context.Context, prompt string, opts TaskOptions) error {
+29 -44
View File
@@ -13,38 +13,41 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/output"
"github.com/cline/cli/pkg/cli/slash"
"github.com/cline/cli/pkg/cli/types"
)
// InputHandler manages interactive user input during follow mode
type InputHandler struct {
manager *Manager
coordinator *StreamCoordinator
cancelFunc context.CancelFunc
mu sync.RWMutex
isRunning bool
pollTicker *time.Ticker
program *tea.Program
programRunning bool
programDoneChan chan struct{} // Signals when program actually exits
resultChan chan output.InputSubmitMsg
cancelChan chan struct{}
feedbackApproval bool // Track if we're in feedback after approval
feedbackApproved bool // Track the approval decision
approvalMessage *types.ClineMessage // Store the approval message for determining action
ctx context.Context // Context for restart callback
manager *Manager
coordinator *StreamCoordinator
cancelFunc context.CancelFunc
mu sync.RWMutex
isRunning bool
pollTicker *time.Ticker
program *tea.Program
programRunning bool
programDoneChan chan struct{} // Signals when program actually exits
resultChan chan output.InputSubmitMsg
cancelChan chan struct{}
feedbackApproval bool // Track if we're in feedback after approval
feedbackApproved bool // Track the approval decision
approvalMessage *types.ClineMessage // Store the approval message for determining action
slashCommandRegistry *slash.Registry // Slash command registry for autocomplete
ctx context.Context // Context for restart callback
}
// NewInputHandler creates a new input handler
func NewInputHandler(manager *Manager, coordinator *StreamCoordinator, cancelFunc context.CancelFunc) *InputHandler {
return &InputHandler{
manager: manager,
coordinator: coordinator,
cancelFunc: cancelFunc,
isRunning: false,
pollTicker: time.NewTicker(500 * time.Millisecond),
resultChan: make(chan output.InputSubmitMsg, 1),
cancelChan: make(chan struct{}, 1),
manager: manager,
coordinator: coordinator,
cancelFunc: cancelFunc,
isRunning: false,
pollTicker: time.NewTicker(500 * time.Millisecond),
resultChan: make(chan output.InputSubmitMsg, 1),
slashCommandRegistry: slash.NewRegistry(context.Background()),
cancelChan: make(chan struct{}, 1),
}
}
@@ -163,7 +166,7 @@ func (ih *InputHandler) Start(ctx context.Context, errChan chan error) {
if shouldSend {
// Check for mode switch commands first
newMode, remainingMessage, isModeSwitch := ih.parseModeSwitch(message)
newMode, remainingMessage, isModeSwitch := slash.ParseModeSwitch(message)
if isModeSwitch {
// Create styles for mode switch messages (respect global color profile)
actStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true)
@@ -284,9 +287,9 @@ func (ih *InputHandler) promptForInput(ctx context.Context) (string, bool, error
model := output.NewInputModelWithRegistry(
output.InputTypeMessage,
"Cline is ready for your message...",
"/plan or /act to switch modes\nctrl+e to open editor\ntab to autocomplete commands",
"/plan or /act to switch modes\ntab to autocomplete commands\nctrl+e to open editor\nctrl+c to exit",
currentMode,
ih.manager.GetSlashRegistry(),
ih.slashCommandRegistry,
)
return ih.runInputProgram(ctx, model)
@@ -302,7 +305,7 @@ func (ih *InputHandler) promptForApproval(ctx context.Context, msg *types.ClineM
"Let Cline use this tool?",
"",
ih.manager.GetCurrentMode(),
ih.manager.GetSlashRegistry(), // Pass registry for feedback input after approval
ih.slashCommandRegistry,
)
message, shouldSend, err := ih.runInputProgram(ctx, model)
@@ -478,24 +481,6 @@ func (w *inputProgramWrapper) View() string {
return w.model.View()
}
// parseModeSwitch checks if message starts with /act or /plan and extracts the mode and remaining message
func (ih *InputHandler) parseModeSwitch(message string) (string, string, bool) {
trimmed := strings.TrimSpace(message)
lower := strings.ToLower(trimmed)
if strings.HasPrefix(lower, "/plan") {
remaining := strings.TrimSpace(trimmed[5:])
return "plan", remaining, true
}
if strings.HasPrefix(lower, "/act") {
remaining := strings.TrimSpace(trimmed[4:])
return "act", remaining, true
}
return "", message, false
}
// handleSpecialCommand processes special commands like /cancel, /exit
func (ih *InputHandler) handleSpecialCommand(ctx context.Context, message string) bool {
switch strings.ToLower(strings.TrimSpace(message)) {
+7 -34
View File
@@ -14,7 +14,6 @@ import (
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/handlers"
"github.com/cline/cli/pkg/cli/slash"
"github.com/cline/cli/pkg/cli/types"
"github.com/cline/grpc-go/client"
"github.com/cline/grpc-go/cline"
@@ -38,7 +37,6 @@ type Manager struct {
systemRenderer *display.SystemMessageRenderer
streamingDisplay *display.StreamingDisplay
handlerRegistry *handlers.HandlerRegistry
slashRegistry *slash.Registry
isStreamingMode bool
isInteractive bool
currentMode string // "plan" or "act"
@@ -68,7 +66,6 @@ func NewManager(client *client.ClineClient) *Manager {
systemRenderer: systemRenderer,
streamingDisplay: streamingDisplay,
handlerRegistry: registry,
slashRegistry: slash.NewRegistry(),
currentMode: "plan", // Default mode
}
}
@@ -83,9 +80,6 @@ func NewManagerForAddress(ctx context.Context, address string) (*Manager, error)
manager := NewManager(client)
manager.clientAddress = address
// Fetch slash commands from backend (non-blocking, errors are logged)
manager.fetchSlashCommands(ctx)
return manager, nil
}
@@ -99,29 +93,13 @@ func NewManagerForDefault(ctx context.Context) (*Manager, error) {
manager := NewManager(client)
// Get the default instance address
if global.Clients != nil {
manager.clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
if global.Instances != nil {
manager.clientAddress = global.Instances.GetRegistry().GetDefaultInstance()
}
// Fetch slash commands from backend (non-blocking, errors are logged)
manager.fetchSlashCommands(ctx)
return manager, nil
}
// fetchSlashCommands fetches available slash commands from the backend
// This is non-blocking and errors are logged but don't prevent manager creation
func (m *Manager) fetchSlashCommands(ctx context.Context) {
if err := m.slashRegistry.FetchFromBackend(ctx, m.client); err != nil {
if global.Config.Verbose {
m.renderer.RenderDebug("Failed to fetch slash commands: %v", err)
}
// Non-fatal: CLI-local commands are still available
} else if global.Config.Verbose {
m.renderer.RenderDebug("Loaded %d slash commands", len(m.slashRegistry.GetCommands()))
}
}
// SwitchToInstance switches the manager to use a different Cline instance
func (m *Manager) SwitchToInstance(ctx context.Context, address string) error {
m.mu.Lock()
@@ -1206,11 +1184,11 @@ func (m *Manager) displayMessage(msg *types.ClineMessage, isLast, isPartial bool
m.mu.RUnlock()
dc := &handlers.DisplayContext{
State: m.state,
Renderer: m.renderer,
ToolRenderer: m.toolRenderer,
HookRenderer: m.hookRenderer,
SystemRenderer: m.systemRenderer,
State: m.state,
Renderer: m.renderer,
ToolRenderer: m.toolRenderer,
HookRenderer: m.hookRenderer,
SystemRenderer: m.systemRenderer,
IsLast: isLast,
IsPartial: isPartial,
Verbose: global.Config.Verbose,
@@ -1320,11 +1298,6 @@ func (m *Manager) GetCurrentMode() string {
return m.currentMode
}
// GetSlashRegistry returns the slash command registry
func (m *Manager) GetSlashRegistry() *slash.Registry {
return m.slashRegistry
}
// extractModeFromState extracts the current mode from state JSON
func (m *Manager) extractModeFromState(stateJson string) string {
var rawState map[string]interface{}
-6
View File
@@ -290,12 +290,6 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
return err
}
settings.ActModeAwsBedrockCustomSelected = boolPtr(val)
case "hooks_enabled":
val, err := parseBool(value)
if err != nil {
return err
}
settings.HooksEnabled = boolPtr(val)
case "azure_identity":
val, err := parseBool(value)
if err != nil {
+14 -11
View File
@@ -8,6 +8,19 @@ import (
"github.com/spf13/cobra"
)
// VersionString returns the full version information string
func VersionString() string {
return fmt.Sprintf(`Cline CLI
Cline CLI Version: %s
Cline Core Version: %s
Commit: %s
Built: %s
Built by: %s
Go version: %s
OS/Arch: %s/%s
`, global.CliVersion, global.Version, global.Commit, global.Date, global.BuiltBy, runtime.Version(), runtime.GOOS, runtime.GOARCH)
}
// NewVersionCommand creates the version command
func NewVersionCommand() *cobra.Command {
var short bool
@@ -18,21 +31,11 @@ func NewVersionCommand() *cobra.Command {
Short: "Show version information",
Long: `Display version information for the Cline CLI.`,
RunE: func(cmd *cobra.Command, args []string) error {
// Versions are injected at build time via ldflags
if short {
fmt.Println(global.CliVersion)
return nil
}
fmt.Printf("Cline CLI\n")
fmt.Printf("Cline CLI Version: %s\n", global.CliVersion)
fmt.Printf("Cline Core Version: %s\n", global.Version)
fmt.Printf("Commit: %s\n", global.Commit)
fmt.Printf("Built: %s\n", global.Date)
fmt.Printf("Built by: %s\n", global.BuiltBy)
fmt.Printf("Go version: %s\n", runtime.Version())
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
fmt.Print(VersionString())
return nil
},
}
+2 -2
View File
@@ -10,7 +10,7 @@ import (
// This is the canonical definition used across all CLI packages
type CoreInstanceInfo struct {
// Full core address including port
Address string `json:"address"`
CoreAddress string `json:"address"`
// Host bridge service address that core holds (host is ALWAYS running on localhost FYI)
HostServiceAddress string `json:"host_port"`
Status grpc_health_v1.HealthCheckResponse_ServingStatus `json:"status"`
@@ -20,7 +20,7 @@ type CoreInstanceInfo struct {
}
func (c *CoreInstanceInfo) CorePort() int {
_, port, _ := ParseHostPort(c.Address)
_, port, _ := ParseHostPort(c.CoreAddress)
return port
}
+17 -5
View File
@@ -47,7 +47,10 @@ func (s *DiffService) generateDiffID() string {
return fmt.Sprintf("diff_%d_%d", os.Getpid(), id)
}
// splitLines splits content into lines, preserving line ending information
// splitLines splits content into lines, preserving trailing newlines.
// This matches the behavior of JavaScript's String.split("\n"):
// - "hello\nworld\n" -> ["hello", "world", ""]
// - "hello\nworld" -> ["hello", "world"]
func splitLines(content string) []string {
if content == "" {
return []string{}
@@ -65,10 +68,9 @@ func splitLines(content string) []string {
}
}
// Add the last line if it doesn't end with newline
if current != "" {
lines = append(lines, current)
}
// Always add the last segment - if content ends with newline, this will be
// an empty string which preserves the trailing newline when joined back
lines = append(lines, current)
return lines
}
@@ -176,9 +178,19 @@ func (s *DiffService) ReplaceText(ctx context.Context, req *proto.ReplaceTextReq
endLine = startLine
}
// Check if we're replacing to the end of the document
replacingToEnd := endLine >= len(session.lines)
// Split new content into lines
newLines := splitLines(newContent)
// Remove trailing empty line for proper splicing, BUT only when NOT replacing
// to the end of the document. When replacing to the end, keep the trailing
// empty string to preserve trailing newlines from the content.
if !replacingToEnd && len(newLines) > 0 && newLines[len(newLines)-1] == "" {
newLines = newLines[:len(newLines)-1]
}
// Ensure we have enough lines in the current content
for len(session.lines) < endLine {
session.lines = append(session.lines, "")
-39
View File
@@ -1,39 +0,0 @@
package hostbridge
import (
"log"
"github.com/cline/grpc-go/host"
)
// WatchService implements the host.WatchServiceServer interface
type WatchService struct {
host.UnimplementedWatchServiceServer
coreAddress string
verbose bool
}
// NewWatchService creates a new WatchService
func NewWatchService(coreAddress string, verbose bool) *WatchService {
return &WatchService{
coreAddress: coreAddress,
verbose: verbose,
}
}
// SubscribeToFile subscribes to file change notifications
func (s *WatchService) SubscribeToFile(req *host.SubscribeToFileRequest, stream host.WatchService_SubscribeToFileServer) error {
if s.verbose {
log.Printf("SubscribeToFile called for path: %s", req.GetPath())
}
// For console implementation, we'll just log that we would watch the file
// In a real implementation, we'd use fsnotify or similar to watch file changes
log.Printf("[Cline] Would watch file: %s", req.GetPath())
// Keep the stream open but don't send any events for now
// In a real implementation, we'd send FileChangeEvent messages when files change
<-stream.Context().Done()
return nil
}
+6 -4
View File
@@ -5,6 +5,7 @@ import (
"log"
"os"
"github.com/cline/grpc-go/cline"
"github.com/cline/grpc-go/host"
)
@@ -43,24 +44,25 @@ func (s *WorkspaceService) GetWorkspacePaths(ctx context.Context, req *host.GetW
// SaveOpenDocumentIfDirty saves an open document if it has unsaved changes
func (s *WorkspaceService) SaveOpenDocumentIfDirty(ctx context.Context, req *host.SaveOpenDocumentIfDirtyRequest) (*host.SaveOpenDocumentIfDirtyResponse, error) {
if s.verbose {
log.Printf("SaveOpenDocumentIfDirty called for path: %s", req.GetPath())
log.Printf("SaveOpenDocumentIfDirty called for path: %v", req.FilePath)
}
// For console implementation, we'll assume the document is already saved
// In a real implementation, we'd check if the file has unsaved changes
f := false
return &host.SaveOpenDocumentIfDirtyResponse{
WasSaved: false, // Assume no changes to save
WasSaved: &f, // Assume no changes to save
}, nil
}
// GetDiagnostics returns diagnostic information for a file
func (s *WorkspaceService) GetDiagnostics(ctx context.Context, req *host.GetDiagnosticsRequest) (*host.GetDiagnosticsResponse, error) {
if s.verbose {
log.Printf("GetDiagnostics called for path: %s", req.GetPath())
log.Printf("GetDiagnostics called")
}
// For console implementation, return empty diagnostics
return &host.GetDiagnosticsResponse{
Diagnostics: []*host.Diagnostic{},
FileDiagnostics: []*cline.FileDiagnostics{},
}, nil
}
+7 -4
View File
@@ -104,14 +104,18 @@ func (s *EnvService) Shutdown(ctx context.Context, req *cline.EmptyRequest) (*cl
return &cline.Empty{}, nil
}
func (s *EnvService) isTelemetryEnabled() bool {
// In CLI mode, check the CLINE_TELEMETRY_DISABLED environment variable
return os.Getenv("CLINE_TELEMETRY_DISABLED") != "true"
}
// GetTelemetrySettings returns the telemetry settings for CLI mode
func (s *EnvService) GetTelemetrySettings(ctx context.Context, req *cline.EmptyRequest) (*host.GetTelemetrySettingsResponse, error) {
if s.verbose {
log.Printf("GetTelemetrySettings called")
}
// In CLI mode, check the POSTHOG_TELEMETRY_ENABLED environment variable
telemetryEnabled := os.Getenv("POSTHOG_TELEMETRY_ENABLED") == "true"
telemetryEnabled := s.isTelemetryEnabled()
var setting host.Setting
if telemetryEnabled {
@@ -133,8 +137,7 @@ func (s *EnvService) SubscribeToTelemetrySettings(req *cline.EmptyRequest, strea
log.Printf("SubscribeToTelemetrySettings called")
}
// Send initial telemetry state
telemetryEnabled := os.Getenv("POSTHOG_TELEMETRY_ENABLED") == "true"
telemetryEnabled := s.isTelemetryEnabled()
var setting host.Setting
if telemetryEnabled {
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

+18 -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": [
@@ -148,6 +154,7 @@
"features/hooks/samples"
]
},
"features/jupyter-notebooks",
"features/multiroot-workspace",
"features/plan-and-act",
"features/skills",
@@ -177,6 +184,7 @@
"features/tasks/task-management"
]
},
"features/worktrees",
"features/yolo-mode"
]
},
@@ -197,6 +205,7 @@
"provider-config/anthropic",
"provider-config/claude-code",
"provider-config/openai",
"provider-config/openai-codex",
"provider-config/openrouter",
"provider-config/cerebras",
"provider-config/deepseek",
@@ -421,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": {
+4 -1
View File
@@ -48,7 +48,7 @@ These labels match what you see in the Auto Approve menu.
| Execute all commands | Run commands marked as requiring approval | Requires “Execute safe commands” |
| Use the browser | Allows use of the browser tool for web fetching and searching | Proxy issues can apply |
| Use MCP servers | Use MCP tools and access MCP resources | Some servers also have per-tool auto-approve |
| Enable notifications | Notifies you about long-running auto-approved commands | Helpful for terminal work |
| Enable notifications | Notifies you about long-running auto-approved commands | Accessible directly in the Auto Approve menu |
<Warning>
“Read all files” and “Edit all files” only matter if their base toggle is enabled. They extend access outside your workspace.
@@ -92,6 +92,9 @@ These are examples, not guarantees.
Auto-approved actions can run for a while, especially long terminal commands. If you enable notifications, Cline can notify you when an auto-approved command has been running for a while and may need attention.
The **Enable notifications** toggle is located at the bottom of the Auto Approve menu, below a separator line. This puts the notification setting right where you manage your auto-approval permissions, making it easy to discover and adjust.
## Recommendations
A good default setup is:
-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
+112
View File
@@ -0,0 +1,112 @@
---
title: "Jupyter Notebooks"
description: "AI-assisted editing of Jupyter notebooks with cell-level context awareness"
---
# Jupyter Notebooks
Cline provides comprehensive support for Jupyter notebooks (`.ipynb` files), enabling AI-assisted editing with full cell-level context awareness. This feature was developed in collaboration with Amazon to bring AI coding assistance to data science workflows.
## Getting started
Jupyter notebook support is a built-in feature of Cline. To use it, you just need to have the Jupyter notebook extension enabled in VS Code. Once you open any `.ipynb` file, you'll see AI-assisted buttons in your notebook interface.
## How to use
### Generate Cell
Click the sparkle icon (✨) in the notebook toolbar to generate new cells with AI assistance.
![Generate Cell demo](../assets/jupyter-generate-cell.gif)
**How it works:** The AI receives context from surrounding cells, so it understands the variables and imports already in scope. This means you can reference existing DataFrames, functions, and other objects without re-explaining them.
**Example prompt:** "Create a visualization showing the correlation matrix of numeric columns with a heatmap"
The cell is inserted with proper notebook JSON structure, preserving metadata and ready to execute.
### Explain Cell
Click the Explain button in any cell's title bar to get a detailed explanation of what the cell does.
This is useful for:
- Revisiting old notebooks
- Onboarding to a teammate's analysis
- Understanding complex transformations
**How it works:** Cline extracts the full cell context, including outputs, so explanations can reference actual results like column names, row counts, and computed values.
### Improve Cell
Click the Improve button in any cell's title bar to enhance existing cells with AI suggestions.
![Explain and Improve Cell demo](../assets/jupyter-explain-improve-cell.gif)
Use this to:
- Optimize slow pandas operations
- Add error handling
- Refactor for clarity
- Convert loops to vectorized operations
**How it works:** Cline suggests improvements while preserving the cell's position and metadata in the notebook structure. The AI explains what was changed and why.
## How cell context works
Unlike traditional file editing, Jupyter notebooks are JSON documents containing arrays of cells. Each cell has its own type, source content, metadata, execution count, and outputs.
When you use a Jupyter command, Cline extracts structured context that includes:
- **Cell type** (code, markdown, or raw)
- **Source content** as an array of lines
- **Cell metadata** and configuration
- **Execution count** for code cells
- **Outputs** including data, text, and error traces
This structured representation allows the AI to understand not just the code, but its context within the notebook and its actual output.
### JSON structure preservation
Cline is designed to work carefully with the cell JSON structure, aiming to:
- Keep cell boundaries intact
- Preserve execution counts
- Maintain cell metadata
- Keep outputs associated with their source cells
The AI is specifically prompted to preserve notebook structure, though you should always review changes to ensure your notebook format remains correct.
## Keyboard shortcuts
You can bind any of these commands to keyboard shortcuts for faster access:
1. Open VS Code keyboard shortcuts (Cmd/Ctrl + K, Cmd/Ctrl + S)
2. Search for `cline.jupyterGenerateCell`, `cline.jupyterExplainCell`, or `cline.jupyterImproveCell`
3. Assign your preferred key combinations
## Tips for best results
**For Generate Cell:**
- Be specific about what you want the cell to do
- Reference existing variables by name (the AI can see them)
- Mention preferred libraries if you have a preference (e.g., "use seaborn" or "use plotly")
**For Explain Cell:**
- Works best on cells that have been executed (outputs provide additional context)
- Good for complex chained operations like pandas groupby/merge sequences
**For Improve Cell:**
- Mention what aspect you want to improve (performance, readability, error handling)
- The AI will explain the changes it suggests
## Limitations
- Notebook support requires the Jupyter notebook extension to be enabled in VS Code
- Cell context extraction depends on VS Code's notebook API
- Very large notebooks may require more context than some models can handle efficiently
## Related
- [Cline Tools Guide](/exploring-clines-tools/cline-tools-guide) -- Overview of all Cline commands
- [Model Selection Guide](/core-features/model-selection-guide) -- Choosing the right model for your workflow
+270
View File
@@ -0,0 +1,270 @@
---
title: "Worktrees"
sidebarTitle: "Worktrees"
---
Worktrees let you work on multiple branches simultaneously, each in its own folder. This enables Cline to work on tasks in parallel across separate VS Code windows, or lets Cline work independently while you continue coding in your main workspace.
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/worktrees-overview.png"
alt="Worktrees view showing multiple linked worktrees"
/>
</Frame>
## What Are Git Worktrees?
A Git worktree is a linked copy of your repository in a separate folder, checked out to a specific branch. All worktrees share the same Git history and `.git` directory, but each has its own working directory with different code checked out.
Key concepts:
- **Main worktree**: Your original repository folder where the `.git` directory lives
- **Linked worktrees**: Additional folders you create, each checked out to a different branch
- **Shared history**: All worktrees share commits, branches, and Git configuration
<Tip>
Unlike regular branch switching, worktrees let you have multiple branches checked out at the same time in different folders. This means you can have VS Code windows open for different features simultaneously.
</Tip>
## Why Use Worktrees with Cline?
Worktrees solve a common problem: **Cline takes over your VS Code window while working on a task**. With worktrees, you can:
1. **Run Cline in parallel** - Have Cline work on multiple tasks simultaneously, each in its own worktree and VS Code window
2. **Keep working while Cline works** - Let Cline handle a task in a separate worktree while you continue coding in your main workspace
3. **Isolate experimental changes** - Test risky changes in a worktree without affecting your main branch
4. **Quick context switching** - Jump between features without stashing or committing incomplete work
## Getting Started
### Quick Launch (Recommended)
The fastest way to start using worktrees is the **New Worktree Window** button on Cline's home screen:
1. Click **New Worktree Window** on the home screen
2. Enter a branch name and folder path (defaults are auto-filled)
3. Click **Create & Open**
A new VS Code window opens with your worktree, and Cline automatically opens ready to work.
<Tip>
The home screen also shows your current branch and worktree path. Click it to open the full Worktrees view.
</Tip>
### Full Worktrees View
For more control, open the full Worktrees view by clicking the **Worktrees** button in the Cline sidebar header, or by clicking your current branch info on the home screen:
<Steps>
<Step title="Create a New Worktree">
Click **New Worktree** at the bottom of the view. Enter a branch name and path (defaults are auto-filled).
</Step>
<Step title="Open in New Window">
Once created, click the **Open in new window** button to open the worktree in a separate VS Code window. Cline will automatically open in the new window.
</Step>
</Steps>
## Typical Workflow
Here's how a typical worktree session looks:
<Steps>
<Step title="Create a new worktree">
Click **New Worktree Window** on the home screen or use the Worktrees view. A new VS Code window opens with Cline ready to go.
</Step>
<Step title="Do your work">
Work on your feature or let Cline handle a task. Make commits as you go.
</Step>
<Step title="Close the worktree window">
When you're done, close the worktree's VS Code window.
</Step>
<Step title="Merge from your primary worktree">
Back in your main VS Code window, open the Worktrees view and click the **merge button** on the worktree you just worked in. This merges the branch and optionally deletes the worktree.
</Step>
</Steps>
## Managing Worktrees
### Viewing Worktrees
The Worktrees view shows all worktrees for your repository:
- **Current**: The worktree you're currently in (highlighted)
- **Main**: The primary worktree where your `.git` directory lives (cannot be deleted)
- **Locked**: Worktrees that are locked to prevent accidental deletion
### Opening Worktrees
Each worktree has two open options:
- **Open in current window**: Replace your current workspace with the worktree
- **Open in new window**: Open the worktree in a separate VS Code window (recommended for parallel Cline sessions)
Either way, Cline automatically opens in the new workspace, ready to start a task.
### Deleting Worktrees
Click the trash icon on any linked worktree to delete it. A confirmation dialog will show you exactly what will be deleted:
- The branch itself
- All project files in the worktree folder
<Warning>
Deleting a worktree permanently removes the branch and all files in that folder. Make sure any important changes are committed and pushed first.
</Warning>
<Note>
You cannot delete the main worktree. It's the primary repository where your `.git` directory lives.
</Note>
### Merging Worktrees
When you're done working in a worktree and ready to merge your changes back to the main branch:
1. Click the **merge icon** (git merge symbol) on any linked worktree
2. Review the merge details in the confirmation modal
3. Choose whether to delete the worktree after merging
4. Click **Merge**
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/worktrees-merge.png"
alt="Merge worktree modal"
/>
</Frame>
#### Handling Merge Conflicts
If your branch has conflicts with the main branch, Cline will detect them and show you the conflicting files. You have two options:
1. **Ask Cline to Resolve & Merge** - Creates a new Cline task with a prompt asking Cline to resolve the conflicts, complete the merge, and clean up the worktree
2. **Resolve Manually** - Close the modal and resolve conflicts yourself using your preferred Git tools
<Tip>
The "Ask Cline to Resolve" option is particularly useful for complex conflicts. Cline will analyze the conflicting files and attempt to merge them intelligently based on the intent of both branches.
</Tip>
## .worktreeinclude: Automatic File Copying
When you create a new worktree, it starts with a fresh checkout—no `node_modules`, no build artifacts, no IDE settings. This means you'd normally need to run `npm install` or similar setup commands.
The `.worktreeinclude` file solves this by automatically copying specified files to new worktrees.
### How It Works
1. Create a `.worktreeinclude` file in your repository root
2. Add glob patterns for files you want copied (using `.gitignore` syntax)
3. When Cline creates a new worktree, files matching **both** `.worktreeinclude` **and** `.gitignore` are copied automatically
<Note>
Only files that are both matched by `.worktreeinclude` AND listed in `.gitignore` are copied. This prevents accidentally duplicating tracked files.
</Note>
### Example `.worktreeinclude`
```gitignore
# Copy node_modules to avoid npm install
node_modules/
# Copy IDE settings
.vscode/
# Copy build cache
.next/
dist/
# Copy environment files (if gitignored)
.env.local
```
### Creating a `.worktreeinclude` File
The Worktrees view will show a tip if you don't have a `.worktreeinclude` file. If you have a `.gitignore`, you can click **Create from .gitignore** to create one pre-filled with your gitignore contents. Then edit it to keep only the patterns you want copied.
<Tip>
For most JavaScript/TypeScript projects, just including `node_modules/` in your `.worktreeinclude` saves significant setup time for each new worktree.
</Tip>
### Pro Tip: Symlink to .gitignore
Since `.gitignore` usually contains most of the files you'd want copied to new worktrees (dependencies, environment files, build caches, etc.), you can create a symlink so they stay in sync automatically:
```bash
# In your repository root
ln -s .gitignore .worktreeinclude
```
Now whenever you update your `.gitignore`, your `.worktreeinclude` will have the same patterns. This is especially useful for projects where gitignored files are exactly what you want copied—no need to maintain two separate files.
<Note>
If you need different patterns than your `.gitignore`, create a regular `.worktreeinclude` file instead of a symlink.
</Note>
## Best Practices
<AccordionGroup>
<Accordion title="For Parallel Cline Sessions">
1. **Create purpose-specific worktrees** - Name branches clearly (e.g., `cline/refactor-auth`, `cline/add-tests`)
2. **Open in new windows** - Always use "Open in new window" for true parallelism
3. **Use .worktreeinclude** - Set up automatic file copying to reduce setup time
</Accordion>
<Accordion title="For Solo Development">
1. **Keep your main branch clean** - Use worktrees for experimental or risky changes
2. **Quick feature switches** - Instead of stashing, create a worktree for interruptions
3. **Review in isolation** - Create worktrees to review PRs without disrupting your work
</Accordion>
<Accordion title="Worktree Hygiene">
1. **Delete unused worktrees** - Remove worktrees when their branches are merged
2. **Use meaningful names** - Branch names should indicate the worktree's purpose
3. **Check for stale worktrees** - Periodically review and clean up old worktrees
</Accordion>
</AccordionGroup>
## Limitations
Worktrees are not available in certain workspace configurations:
- **Multi-root workspaces**: If you have multiple folders open in VS Code, worktrees are disabled. Open a single repository folder instead.
- **Subfolder of a repository**: If you've opened a subfolder within a Git repository (not the root), worktrees are disabled. Open the repository root folder instead.
The Worktrees view will display a message explaining the limitation if either of these applies to your workspace.
## Troubleshooting
<AccordionGroup>
<Accordion title="Branch already exists error">
Git doesn't allow the same branch to be checked out in multiple worktrees. Either:
- Use a different branch name
- Delete the existing worktree using that branch
</Accordion>
<Accordion title="Worktree folder already exists">
The path you specified already contains files. Choose a different path or delete the existing folder first.
</Accordion>
<Accordion title="Can't delete worktree">
If a worktree is locked, you'll need to unlock it first using `git worktree unlock <path>` in the terminal. If the worktree has uncommitted changes, you may need to use force delete.
</Accordion>
<Accordion title=".worktreeinclude files not copying">
Make sure the files you want copied are:
1. Listed in your `.worktreeinclude` file
2. Also listed in your `.gitignore` (only gitignored files are copied)
3. Actually exist in your current worktree
</Accordion>
</AccordionGroup>
## Technical Details
<AccordionGroup>
<Accordion title="How Worktrees Work Internally">
- Worktrees are a native Git feature (`git worktree` command)
- All worktrees share the same `.git` directory and object database
- Each worktree has its own index, working directory, and HEAD
- Worktree list is stored in `.git/worktrees/`
</Accordion>
<Accordion title="Storage Considerations">
- Each worktree contains a full checkout of the repository
- `.worktreeinclude` can significantly increase worktree size (e.g., copying `node_modules`)
- Consider your disk space when creating many worktrees
</Accordion>
<Accordion title="Relationship with Checkpoints">
Worktrees are separate from Cline's [checkpoint system](/features/checkpoints). Each worktree has its own checkpoint history. Checkpoints track changes within a single worktree, while worktrees let you work across multiple branches simultaneously.
</Accordion>
</AccordionGroup>
Worktrees unlock true parallel development with Cline. Create a worktree, open it in a new window, and let Cline work independently while you continue coding!
+19 -2
View File
@@ -25,11 +25,28 @@ The easiest way to get started is with **Cline** as your provider:
## Alternative: Use Another Provider
If you prefer to use your own API keys, you can select from providers like:
If you prefer to use your own accounts or API keys, you have several options:
### Sign in with OpenAI (Recommended)
The easiest way to use OpenAI models is with **OpenAI Codex**—no API keys needed:
1. Select **"OpenAI Codex"** from the API Provider dropdown
2. Click **"Sign in with OpenAI"**
3. Authorize Cline in your browser
4. Choose your model
<Card title="OpenAI Codex Setup Guide" icon="key" href="/provider-config/openai-codex">
See the full setup guide with screenshots and troubleshooting tips.
</Card>
### Other Providers
You can also use API keys with these providers:
- **OpenRouter** - Great value, multiple models
- **Anthropic** - Direct access to Claude models
- **OpenAI** - Access to GPT models
- **OpenAI** - Access to GPT models via API key
- **Google Gemini** - Google's AI models
- **Ollama** - Run models locally on your computer
-1
View File
@@ -18,7 +18,6 @@ Cerebras delivers the world's fastest AI inference through their revolutionary w
Cline supports the following Cerebras models:
- `zai-glm-4.6` - Fast general-purpose model on Cerebras (up to 1,000 tokens/s). To be deprecated soon.
- `zai-glm-4.7` - Highly capable general-purpose model on Cerebras (up to 1,000 tokens/s), competitive with leading proprietary models on coding tasks.
- `qwen-3-235b-a22b-instruct-2507` - Advanced instruction-following model
- `qwen-3-235b-a22b-thinking-2507` - Reasoning model with step-by-step thinking
+105
View File
@@ -0,0 +1,105 @@
---
title: "OpenAI Codex"
sidebarTitle: "OpenAI Codex"
description: "Connect your OpenAI account to Cline via OAuth for seamless access to OpenAI models."
---
OpenAI Codex lets you connect your OpenAI account directly to Cline using OAuth, meaning you don't need to manage API keys. Simply sign in with your OpenAI account through a one-click browser authentication, and you'll automatically have access to all the models available on your OpenAI plan. No additional configuration required.
<Tip>
If you prefer to use API keys instead, see the [OpenAI (API Key)](/provider-config/openai) provider configuration.
</Tip>
## Prerequisites
- An [OpenAI account](https://platform.openai.com/signup)
- An active OpenAI subscription or API access plan
<Note>
The models available to you depend on your OpenAI account's subscription tier. See [OpenAI's pricing page](https://openai.com/pricing) for details on what's included in each plan.
</Note>
## Connecting Your OpenAI Account
<Steps>
<Step title="Open Cline Settings">
Click the settings gear icon in the top-right corner of the Cline extension.
</Step>
<Step title="Select OpenAI Codex Provider">
Choose **"OpenAI Codex"** from the "API Provider" dropdown menu.
</Step>
<Step title="Sign In with OpenAI">
Click the **"Sign in with OpenAI"** button. This will open a browser window for authentication.
</Step>
<Step title="Authorize Cline">
In the browser window that opens:
1. Sign in to your OpenAI account (if not already signed in)
2. Review the permissions Cline is requesting
3. Click **"Authorize"** to grant access
</Step>
<Step title="Select Your Model">
Once authorized, you'll be returned to Cline. Choose your desired model from the **"Model"** dropdown.
</Step>
</Steps>
<Note>
If a model doesn't appear in your dropdown, it may not be available on your OpenAI subscription tier.
</Note>
For the most current list of available models and their capabilities, see the official [OpenAI Models documentation](https://platform.openai.com/docs/models).
## Managing Your Connection
### Disconnecting Your Account
To disconnect your OpenAI account from Cline:
1. Open Cline Settings
2. With OpenAI Codex selected as the provider, click **"Sign out"**
3. Confirm the disconnection
This removes the OAuth connection. You can reconnect at any time by signing in again.
### Token Refresh
OAuth tokens are automatically refreshed by Cline. If you encounter authentication errors:
1. Try disconnecting and reconnecting your account
2. Ensure your OpenAI account is in good standing
3. Check that your subscription is active
## Troubleshooting
### "Authentication failed" error
- Ensure you're signing in with the correct OpenAI account
- Check that your OpenAI subscription is active
- Try clearing your browser cache and signing in again
### Models not appearing
- The available models depend on your OpenAI subscription tier
- Some models may require specific plan upgrades
- Check [OpenAI's pricing page](https://openai.com/pricing) for model availability by plan
### OAuth window doesn't open
- Check if pop-ups are blocked in your browser
- Try using a different browser
- Ensure you have a stable internet connection
### Connection keeps expiring
- This is rare but can happen if your OpenAI account session expired
- Disconnect and reconnect to refresh your authentication
## Good to Know
Cline only requests the permissions necessary to make API calls on your behalf. Your OpenAI credentials are never stored by Cline.
When using OpenAI Codex, Cline accesses models through your OpenAI/ChatGPT subscription. There is no separate per-token API billing or consumption of OpenAI API credits for this provider; usage and limits are governed by your ChatGPT (or OpenAI account) subscription plan, not by Cline.
## Related Documentation
- [OpenAI (API Key)](/provider-config/openai): Alternative configuration using API keys
- [Model Selection Guide](/core-features/model-selection-guide): Help choosing the right model
+8 -11
View File
@@ -16,20 +16,17 @@ Cline supports accessing models directly through the official OpenAI API.
### Supported Models
Cline is compatible with a variety of OpenAI models, including but not limited to:
Cline is compatible with a variety of OpenAI models, including common choices from OpenAI's featured/frontier lists:
- 'o3'
- `o3-mini` (medium reasoning effort)
- 'o4-mini'
- `o3-mini-high` (high reasoning effort)
- `o3-mini-low` (low reasoning effort)
- `o1`
- `o1-preview`
- `o1-mini`
- `gpt-5.2`
- `gpt-5.2-codex`
- `gpt-5-mini`
- `gpt-5-nano`
- `gpt-4.1`
- `gpt-4o`
- `gpt-4o-mini`
- 'gpt-4.1'
- 'gpt-4.1-mini'
- `o3`
- `o4-mini`
For the most current list of available models and their capabilities, please refer to the official [OpenAI Models documentation](https://platform.openai.com/docs/models).
+4 -4
View File
@@ -73,16 +73,16 @@ Open VS Code and configure Cline:
### Recommended Models
For the best experience with Cline, use **Qwen3 Coder 30B**. This model provides strong coding capabilities and reliable tool use for local development.
For the best experience with Cline, use **Qwen 2.5 Coder 32B**. This model provides strong coding capabilities and reliable tool use for local development.
To download it:
```bash
ollama run qwen3-coder-30b
ollama pull qwen2.5-coder:32b
```
Other capable models include:
- `mistral-small` - Good balance of performance and speed
- `devstral-small` - Optimized for coding tasks
- `mistral-small:latest` - Good balance of performance and speed
- `codellama:34b-code` - Optimized for coding tasks
### Important Notes
-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) {
+309 -231
View File
File diff suppressed because it is too large Load Diff
+44 -5
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.49.1",
"version": "3.56.0",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -201,6 +201,24 @@
"title": "Improve with Cline",
"category": "Cline"
},
{
"command": "cline.jupyterGenerateCell",
"title": "Generate Jupyter Cell with Cline",
"category": "Cline",
"icon": "$(sparkle)"
},
{
"command": "cline.jupyterExplainCell",
"title": "Explain Jupyter Cell with Cline",
"category": "Cline",
"icon": "$(question)"
},
{
"command": "cline.jupyterImproveCell",
"title": "Improve Jupyter Cell with Cline",
"category": "Cline",
"icon": "$(lightbulb)"
},
{
"command": "cline.openWalkthrough",
"title": "Open Walkthrough",
@@ -304,6 +322,25 @@
"when": "config.git.enabled && scmProvider == git && cline.isGeneratingCommit"
}
],
"notebook/toolbar": [
{
"command": "cline.jupyterGenerateCell",
"group": "navigation/add@1",
"when": "notebookType == 'jupyter-notebook'"
}
],
"notebook/cell/title": [
{
"command": "cline.jupyterExplainCell",
"group": "inline@1",
"when": "notebookType == 'jupyter-notebook'"
},
{
"command": "cline.jupyterImproveCell",
"group": "inline@2",
"when": "notebookType == 'jupyter-notebook'"
}
],
"commandPalette": [
{
"command": "cline.generateGitCommitMessage",
@@ -344,10 +381,6 @@
"compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
"compile-standalone-npm": "npm run protos && npm run protos-go && npm run check-types && npm run lint && node esbuild.mjs --standalone",
"compile-cli": "scripts/build-cli.sh",
"compile-cli-ts": "cd cli-ts && npm run build",
"compile-cli-ts:production": "cd cli-ts && npm run build:production",
"watch-cli-ts": "cd cli-ts && npm run watch",
"dev:cli-ts": "npm run compile-cli-ts && npm run watch-cli-ts",
"compile-cli-all-platforms": "scripts/build-cli-all-platforms.sh",
"compile-cli-man-page": "pandoc cli/man/cline.1.md -s -t man -o cli/man/cline.1",
"test:install": "bash scripts/test-install.sh",
@@ -407,6 +440,10 @@
"storybook": "cd webview-ui && npm run storybook"
},
"lint-staged": {
"src/shared/storage/state-keys.ts": [
"node scripts/generate-state-proto.mjs",
"git add proto/cline/state.proto"
],
"*": [
"biome check --write --staged --no-errors-on-unmatched --files-ignore-unknown=true"
]
@@ -498,6 +535,7 @@
"@types/uuid": "^10.0.0",
"@vscode/codicons": "^0.0.36",
"archiver": "^7.0.1",
"aws4fetch": "^1.0.20",
"axios": "^1.12.0",
"better-sqlite3": "^12.4.1",
"cheerio": "^1.0.0",
@@ -532,6 +570,7 @@
"p-timeout": "^6.1.4",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
"picomatch": "^4.0.3",
"posthog-node": "^5.8.0",
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
+7
View File
@@ -44,6 +44,13 @@ service AccountService {
// Returns a link the webview can use to redirect back to the user's IDE.
rpc getRedirectUrl(EmptyRequest) returns (String);
// OpenAI Codex OAuth authentication
// Starts the OAuth flow and opens browser for user to sign in with ChatGPT Plus/Pro
rpc openAiCodexSignIn(EmptyRequest) returns (Empty);
// Signs out of OpenAI Codex and clears stored credentials
rpc openAiCodexSignOut(EmptyRequest) returns (Empty);
}
message AuthStateChangedRequest {
+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 {
+2
View File
@@ -439,6 +439,7 @@ enum ApiProvider {
HICAP = 37;
AIHUBMIX = 38;
NOUSRESEARCH = 39;
OPENAI_CODEX = 40;
}
enum ApiFormat {
@@ -484,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
+220 -172
View File
@@ -54,184 +54,230 @@ message AutoApprovalSettings {
optional bool enable_notifications = 3;
}
// NOTE: Add the new secret fields under SECRETS_KEYS in src/shared/storage/state-keys.ts
// and use the scripts/generate-state-proto.mjs script to regenerate this list.
message Secrets {
optional string api_key = 1;
optional string open_router_api_key = 4;
optional string aws_access_key = 5;
optional string aws_secret_key = 6;
optional string aws_session_token = 7;
optional string aws_bedrock_api_key = 8;
optional string open_ai_api_key = 9;
optional string gemini_api_key = 10;
optional string open_ai_native_api_key = 11;
optional string ollama_api_key = 12;
optional string deep_seek_api_key = 13;
optional string requesty_api_key = 14;
optional string together_api_key = 15;
optional string fireworks_api_key = 16;
optional string qwen_api_key = 17;
optional string doubao_api_key = 18;
optional string mistral_api_key = 19;
optional string lite_llm_api_key = 20;
optional string auth_nonce = 21;
optional string asksage_api_key = 22;
optional string xai_api_key = 23;
optional string moonshot_api_key = 24;
optional string zai_api_key = 25;
optional string hugging_face_api_key = 26;
optional string nebius_api_key = 27;
optional string sambanova_api_key = 28;
optional string cerebras_api_key = 29;
optional string sap_ai_core_client_id = 30;
optional string sap_ai_core_client_secret = 31;
optional string groq_api_key = 32;
optional string huawei_cloud_maas_api_key = 33;
optional string baseten_api_key = 34;
optional string vercel_ai_gateway_api_key = 35;
optional string dify_api_key = 36;
optional string oca_api_key = 37;
optional string oca_refresh_token = 38;
optional string hicap_api_key = 39;
optional string mcp_oauth_secrets = 40;
optional string cline_account_id = 2;
optional string open_router_api_key = 3;
optional string aws_access_key = 4;
optional string aws_secret_key = 5;
optional string aws_session_token = 6;
optional string aws_bedrock_api_key = 7;
optional string open_ai_api_key = 8;
optional string gemini_api_key = 9;
optional string open_ai_native_api_key = 10;
optional string ollama_api_key = 11;
optional string deep_seek_api_key = 12;
optional string requesty_api_key = 13;
optional string together_api_key = 14;
optional string fireworks_api_key = 15;
optional string qwen_api_key = 16;
optional string doubao_api_key = 17;
optional string mistral_api_key = 18;
optional string lite_llm_api_key = 19;
optional string auth_nonce = 20;
optional string asksage_api_key = 21;
optional string xai_api_key = 22;
optional string moonshot_api_key = 23;
optional string zai_api_key = 24;
optional string hugging_face_api_key = 25;
optional string nebius_api_key = 26;
optional string sambanova_api_key = 27;
optional string cerebras_api_key = 28;
optional string sap_ai_core_client_id = 29;
optional string sap_ai_core_client_secret = 30;
optional string groq_api_key = 31;
optional string huawei_cloud_maas_api_key = 32;
optional string baseten_api_key = 33;
optional string vercel_ai_gateway_api_key = 34;
optional string dify_api_key = 35;
optional string minimax_api_key = 36;
optional string hicap_api_key = 37;
optional string aihubmix_api_key = 38;
optional string nous_research_api_key = 39;
optional string remote_lite_llm_api_key = 40;
optional string oca_api_key = 41;
optional string oca_refresh_token = 42;
optional string mcp_o_auth_secrets = 43;
optional string cline_api_key = 44;
}
// NOTE: Add new fields under API_HANDLER_SETTINGS_FIELDS or USER_SETTINGS_FIELDS
// in src/shared/storage/state-keys.ts and use the scripts/generate-state-proto.mjs
// script to regenerate this list.
message Settings {
optional string aws_region = 1;
optional bool aws_use_cross_region_inference = 2;
optional bool aws_bedrock_use_prompt_cache = 3;
optional string aws_bedrock_endpoint = 4;
optional string aws_profile = 5;
optional string aws_authentication = 6;
optional bool aws_use_profile = 7;
optional string vertex_project_id = 8;
optional string vertex_region = 9;
optional string requesty_base_url = 10;
optional string open_ai_base_url = 11;
// map<string, string> open_ai_headers = 12;
optional string ollama_base_url = 13;
optional string ollama_api_options_ctx_num = 14;
optional string lm_studio_base_url = 15;
optional string lm_studio_max_tokens = 16;
optional string anthropic_base_url = 17;
optional string gemini_base_url = 18;
optional string azure_api_version = 19;
optional string open_router_provider_sorting = 20;
optional AutoApprovalSettings auto_approval_settings = 21;
optional BrowserSettings browser_settings = 24;
optional string lite_llm_base_url = 25;
optional bool lite_llm_use_prompt_cache = 26;
optional int32 fireworks_model_max_completion_tokens = 27;
optional int32 fireworks_model_max_tokens = 28;
optional string lite_llm_base_url = 1;
optional bool lite_llm_use_prompt_cache = 2;
optional string anthropic_base_url = 4;
optional string open_router_provider_sorting = 5;
optional string aws_region = 6;
optional bool aws_use_cross_region_inference = 7;
optional bool aws_use_global_inference = 8;
optional bool aws_bedrock_use_prompt_cache = 9;
optional string aws_authentication = 10;
optional bool aws_use_profile = 11;
optional string aws_profile = 12;
optional string aws_bedrock_endpoint = 13;
optional string claude_code_path = 14;
optional string vertex_project_id = 15;
optional string vertex_region = 16;
optional string open_ai_base_url = 17;
optional string ollama_base_url = 18;
optional string ollama_api_options_ctx_num = 19;
optional string lm_studio_base_url = 20;
optional string lm_studio_max_tokens = 21;
optional string gemini_base_url = 22;
optional string requesty_base_url = 23;
optional int32 fireworks_model_max_completion_tokens = 24;
optional int32 fireworks_model_max_tokens = 25;
optional string qwen_code_oauth_path = 26;
optional string azure_api_version = 27;
optional bool azure_identity = 28;
optional string qwen_api_line = 29;
optional string moonshot_api_line = 30;
optional string zai_api_line = 31;
optional string telemetry_setting = 32;
optional string asksage_api_url = 33;
optional bool plan_act_separate_models_setting = 34;
optional bool enable_checkpoints_setting = 35;
optional int32 request_timeout_ms = 36;
optional int32 shell_integration_timeout = 37;
optional string default_terminal_profile = 38;
optional int32 terminal_output_line_limit = 39;
optional string sap_ai_core_token_url = 40;
optional string sap_ai_core_base_url = 41;
optional string sap_ai_resource_group = 42;
optional bool sap_ai_core_use_orchestration_mode = 43;
optional string claude_code_path = 44;
optional string qwen_code_oauth_path = 45;
optional bool strict_plan_mode_enabled = 46;
optional bool yolo_mode_toggled = 47;
optional bool use_auto_condense = 48;
optional string preferred_language = 49;
optional OpenaiReasoningEffort openai_reasoning_effort = 50;
optional PlanActMode mode = 51;
optional DictationSettings dictation_settings = 52;
optional FocusChainSettings focus_chain_settings = 53;
optional string custom_prompt = 54;
optional string dify_base_url = 55;
optional double auto_condense_threshold = 56;
optional string oca_base_url = 57;
optional ApiProvider plan_mode_api_provider = 58;
optional string plan_mode_api_model_id = 59;
optional int64 plan_mode_thinking_budget_tokens = 60;
optional string plan_mode_reasoning_effort = 61;
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 62;
optional bool plan_mode_aws_bedrock_custom_selected = 63;
optional string plan_mode_aws_bedrock_custom_model_base_id = 64;
optional string plan_mode_open_router_model_id = 65;
optional OpenRouterModelInfo plan_mode_open_router_model_info = 66;
optional string plan_mode_open_ai_model_id = 67;
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 68;
optional string plan_mode_ollama_model_id = 69;
optional string plan_mode_lm_studio_model_id = 70;
optional string plan_mode_lite_llm_model_id = 71;
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 72;
optional string plan_mode_requesty_model_id = 73;
optional OpenRouterModelInfo plan_mode_requesty_model_info = 74;
optional string plan_mode_together_model_id = 75;
optional string plan_mode_fireworks_model_id = 76;
optional string plan_mode_sap_ai_core_model_id = 77;
optional string plan_mode_sap_ai_core_deployment_id = 78;
optional string plan_mode_groq_model_id = 79;
optional OpenRouterModelInfo plan_mode_groq_model_info = 80;
optional string plan_mode_baseten_model_id = 81;
optional OpenRouterModelInfo plan_mode_baseten_model_info = 82;
optional string plan_mode_hugging_face_model_id = 83;
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 84;
optional string plan_mode_huawei_cloud_maas_model_id = 85;
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 86;
optional string plan_mode_oca_model_id = 87;
optional OcaModelInfo plan_mode_oca_model_info = 88;
optional ApiProvider act_mode_api_provider = 89;
optional string act_mode_api_model_id = 90;
optional int64 act_mode_thinking_budget_tokens = 91;
optional string act_mode_reasoning_effort = 92;
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 93;
optional bool act_mode_aws_bedrock_custom_selected = 94;
optional string act_mode_aws_bedrock_custom_model_base_id = 95;
optional string act_mode_open_router_model_id = 96;
optional OpenRouterModelInfo act_mode_open_router_model_info = 97;
optional string act_mode_open_ai_model_id = 98;
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 99;
optional string act_mode_ollama_model_id = 100;
optional string act_mode_lm_studio_model_id = 101;
optional string act_mode_lite_llm_model_id = 102;
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 103;
optional string act_mode_requesty_model_id = 104;
optional OpenRouterModelInfo act_mode_requesty_model_info = 105;
optional string act_mode_together_model_id = 106;
optional string act_mode_fireworks_model_id = 107;
optional string act_mode_sap_ai_core_model_id = 108;
optional string act_mode_sap_ai_core_deployment_id = 109;
optional string act_mode_groq_model_id = 110;
optional OpenRouterModelInfo act_mode_groq_model_info = 111;
optional string act_mode_baseten_model_id = 112;
optional OpenRouterModelInfo act_mode_baseten_model_info = 113;
optional string act_mode_hugging_face_model_id = 114;
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 115;
optional string act_mode_huawei_cloud_maas_model_id = 116;
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 117;
optional string plan_mode_vercel_ai_gateway_model_id = 118;
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 119;
optional string act_mode_vercel_ai_gateway_model_id = 120;
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 121;
optional string act_mode_oca_model_id = 122;
optional OcaModelInfo act_mode_oca_model_info = 123;
optional int32 max_consecutive_mistakes = 124;
optional bool subagents_enabled = 125;
optional int32 subagent_terminal_output_line_limit = 126;
optional string aihubmix_api_key = 127;
optional string aihubmix_base_url = 128;
optional string aihubmix_app_code = 129;
optional string plan_mode_aihubmix_model_id = 130;
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 131;
optional string act_mode_aihubmix_model_id = 132;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 133;
optional bool cline_web_tools_enabled = 134;
optional bool hooks_enabled = 135;
optional bool azure_identity = 136;
optional bool skills_enabled = 137;
optional bool opt_out_of_remote_config = 138;
optional string asksage_api_url = 31;
optional int32 request_timeout_ms = 32;
optional string sap_ai_resource_group = 33;
optional string sap_ai_core_token_url = 34;
optional string sap_ai_core_base_url = 35;
optional bool sap_ai_core_use_orchestration_mode = 36;
optional string dify_base_url = 37;
optional string zai_api_line = 38;
optional string oca_base_url = 39;
optional string minimax_api_line = 40;
optional string oca_mode = 41;
optional string aihubmix_base_url = 42;
optional string aihubmix_app_code = 43;
optional string plan_mode_api_model_id = 44;
optional int64 plan_mode_thinking_budget_tokens = 45;
optional string gemini_plan_mode_thinking_level = 46;
optional string plan_mode_reasoning_effort = 47;
optional string plan_mode_verbosity = 48;
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 49;
optional bool plan_mode_aws_bedrock_custom_selected = 50;
optional string plan_mode_aws_bedrock_custom_model_base_id = 51;
optional string plan_mode_open_router_model_id = 52;
optional OpenRouterModelInfo plan_mode_open_router_model_info = 53;
optional string plan_mode_open_ai_model_id = 54;
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 55;
optional string plan_mode_ollama_model_id = 56;
optional string plan_mode_lm_studio_model_id = 57;
optional string plan_mode_lite_llm_model_id = 58;
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 59;
optional string plan_mode_requesty_model_id = 60;
optional OpenRouterModelInfo plan_mode_requesty_model_info = 61;
optional string plan_mode_together_model_id = 62;
optional string plan_mode_fireworks_model_id = 63;
optional string plan_mode_sap_ai_core_model_id = 64;
optional string plan_mode_sap_ai_core_deployment_id = 65;
optional string plan_mode_groq_model_id = 66;
optional OpenRouterModelInfo plan_mode_groq_model_info = 67;
optional string plan_mode_baseten_model_id = 68;
optional OpenRouterModelInfo plan_mode_baseten_model_info = 69;
optional string plan_mode_hugging_face_model_id = 70;
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 71;
optional string plan_mode_huawei_cloud_maas_model_id = 72;
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 73;
optional string plan_mode_oca_model_id = 74;
optional OcaModelInfo plan_mode_oca_model_info = 75;
optional string plan_mode_oca_reasoning_effort = 76;
optional string plan_mode_aihubmix_model_id = 77;
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 78;
optional string plan_mode_hicap_model_id = 79;
optional OpenRouterModelInfo plan_mode_hicap_model_info = 80;
optional string plan_mode_nous_research_model_id = 81;
optional string plan_mode_vercel_ai_gateway_model_id = 82;
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 83;
optional string act_mode_api_model_id = 84;
optional int64 act_mode_thinking_budget_tokens = 85;
optional string gemini_act_mode_thinking_level = 86;
optional string act_mode_reasoning_effort = 87;
optional string act_mode_verbosity = 88;
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 89;
optional bool act_mode_aws_bedrock_custom_selected = 90;
optional string act_mode_aws_bedrock_custom_model_base_id = 91;
optional string act_mode_open_router_model_id = 92;
optional OpenRouterModelInfo act_mode_open_router_model_info = 93;
optional string act_mode_open_ai_model_id = 94;
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 95;
optional string act_mode_ollama_model_id = 96;
optional string act_mode_lm_studio_model_id = 97;
optional string act_mode_lite_llm_model_id = 98;
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 99;
optional string act_mode_requesty_model_id = 100;
optional OpenRouterModelInfo act_mode_requesty_model_info = 101;
optional string act_mode_together_model_id = 102;
optional string act_mode_fireworks_model_id = 103;
optional string act_mode_sap_ai_core_model_id = 104;
optional string act_mode_sap_ai_core_deployment_id = 105;
optional string act_mode_groq_model_id = 106;
optional OpenRouterModelInfo act_mode_groq_model_info = 107;
optional string act_mode_baseten_model_id = 108;
optional OpenRouterModelInfo act_mode_baseten_model_info = 109;
optional string act_mode_hugging_face_model_id = 110;
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 111;
optional string act_mode_huawei_cloud_maas_model_id = 112;
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 113;
optional string act_mode_oca_model_id = 114;
optional OcaModelInfo act_mode_oca_model_info = 115;
optional string act_mode_oca_reasoning_effort = 116;
optional string act_mode_aihubmix_model_id = 117;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 118;
optional string act_mode_hicap_model_id = 119;
optional OpenRouterModelInfo act_mode_hicap_model_info = 120;
optional string act_mode_nous_research_model_id = 121;
optional string act_mode_vercel_ai_gateway_model_id = 122;
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 123;
optional ApiProvider plan_mode_api_provider = 124;
optional ApiProvider act_mode_api_provider = 125;
optional string hicap_model_id = 126;
optional string lm_studio_model_id = 127;
optional AutoApprovalSettings auto_approval_settings = 128;
optional string global_cline_rules_toggles = 129;
optional string global_workflow_toggles = 130;
optional string global_skills_toggles = 131;
optional BrowserSettings browser_settings = 132;
optional string telemetry_setting = 133;
optional bool plan_act_separate_models_setting = 134;
optional bool enable_checkpoints_setting = 135;
optional int32 shell_integration_timeout = 136;
optional string default_terminal_profile = 137;
optional int32 terminal_output_line_limit = 138;
optional int32 max_consecutive_mistakes = 139;
optional int32 subagent_terminal_output_line_limit = 140;
optional bool strict_plan_mode_enabled = 141;
optional bool yolo_mode_toggled = 142;
optional bool use_auto_condense = 143;
optional bool cline_web_tools_enabled = 144;
optional string preferred_language = 145;
optional OpenaiReasoningEffort openai_reasoning_effort = 146;
optional PlanActMode mode = 147;
optional DictationSettings dictation_settings = 148;
optional FocusChainSettings focus_chain_settings = 149;
optional string custom_prompt = 150;
optional double auto_condense_threshold = 151;
optional bool subagents_enabled = 153;
optional bool enable_parallel_tool_calling = 154;
optional bool background_edit_enabled = 155;
optional bool skills_enabled = 156;
optional bool opt_out_of_remote_config = 157;
optional bool open_telemetry_enabled = 158;
optional string open_telemetry_metrics_exporter = 159;
optional string open_telemetry_logs_exporter = 160;
optional string open_telemetry_otlp_protocol = 161;
optional string open_telemetry_otlp_endpoint = 162;
optional string open_telemetry_otlp_metrics_protocol = 163;
optional string open_telemetry_otlp_metrics_endpoint = 164;
optional string open_telemetry_otlp_logs_protocol = 165;
optional string open_telemetry_otlp_logs_endpoint = 166;
optional int32 open_telemetry_metric_export_interval = 167;
optional bool open_telemetry_otlp_insecure = 168;
optional int32 open_telemetry_log_batch_size = 169;
optional int32 open_telemetry_log_batch_timeout = 170;
optional int32 open_telemetry_log_max_queue_size = 171;
optional bool worktrees_enabled = 172;
map<string, string> open_ai_headers = 173;
}
message DictationSettings {
@@ -338,6 +384,8 @@ message UpdateTaskSettingsRequest {
// Message for updating settings
message UpdateSettingsRequest {
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
Metadata metadata = 1;
optional ModelsApiConfiguration api_configuration = 2;
optional string telemetry_setting = 3;
@@ -362,7 +410,6 @@ message UpdateSettingsRequest {
optional DictationSettings dictation_settings = 23;
optional double auto_condense_threshold = 24;
optional bool multi_root_enabled = 25;
optional bool hooks_enabled = 26;
optional string vscode_terminal_execution_mode = 27;
optional int32 max_consecutive_mistakes = 28;
optional bool subagents_enabled = 29;
@@ -376,6 +423,7 @@ message UpdateSettingsRequest {
optional string oca_reasoning_effort = 37;
optional bool skills_enabled = 38;
optional bool opt_out_of_remote_config = 39;
optional bool worktrees_enabled = 40;
}
message UpdateTerminalConnectionTimeoutRequest {
+4
View File
@@ -70,6 +70,7 @@ enum ClineSay {
HOOK_STATUS = 30;
HOOK_OUTPUT_STREAM = 31;
COMMAND_PERMISSION_DENIED = 32;
CONDITIONAL_RULES_APPLIED = 33;
}
// Enum for ClineSayTool tool types
@@ -256,6 +257,9 @@ service UiService {
// Subscribe to settings button clicked events
rpc subscribeToSettingsButtonClicked(EmptyRequest) returns (stream Empty);
// Subscribe to worktrees button clicked events
rpc subscribeToWorktreesButtonClicked(EmptyRequest) returns (stream Empty);
// Subscribe to partial message updates (streaming Cline messages as they're built)
rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage);
+153
View File
@@ -0,0 +1,153 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_multiple_files = true;
option java_package = "bot.cline.proto";
// Service for git worktree operations
service WorktreeService {
// Lists all worktrees in the current repository
rpc listWorktrees(EmptyRequest) returns (WorktreeList);
// Creates a new worktree
rpc createWorktree(CreateWorktreeRequest) returns (WorktreeResult);
// Deletes an existing worktree
rpc deleteWorktree(DeleteWorktreeRequest) returns (WorktreeResult);
// Switches to a different worktree (opens in VS Code)
rpc switchWorktree(SwitchWorktreeRequest) returns (WorktreeResult);
// Gets available branches for creating worktrees
rpc getAvailableBranches(EmptyRequest) returns (BranchList);
// Gets suggested defaults for creating a new worktree (auto-generated branch name and path)
rpc getWorktreeDefaults(EmptyRequest) returns (WorktreeDefaults);
// Gets the status of .worktreeinclude file and .gitignore contents for creating one
rpc getWorktreeIncludeStatus(EmptyRequest) returns (WorktreeIncludeStatus);
// Creates a .worktreeinclude file with the provided content
rpc createWorktreeInclude(CreateWorktreeIncludeRequest) returns (WorktreeResult);
// Switches to a different branch in the current worktree (git checkout)
rpc checkoutBranch(CheckoutBranchRequest) returns (WorktreeResult);
// Merges a worktree's branch into the target branch and optionally deletes the worktree
rpc mergeWorktree(MergeWorktreeRequest) returns (MergeWorktreeResult);
// Tracks when the worktrees view is opened (for telemetry)
rpc trackWorktreeViewOpened(TrackWorktreeViewOpenedRequest) returns (Empty);
}
// Represents a single git worktree
message Worktree {
string path = 1; // Absolute path to the worktree
string branch = 2; // Branch name (empty if detached)
string commit_hash = 3; // Current commit hash
bool is_current = 4; // Whether this is the current worktree
bool is_bare = 5; // Whether this is the bare repository
bool is_detached = 6; // Whether HEAD is detached
bool is_locked = 7; // Whether the worktree is locked
optional string lock_reason = 8; // Reason for lock if locked
}
// Response containing list of worktrees
message WorktreeList {
repeated Worktree worktrees = 1;
bool is_git_repo = 2; // Whether the current workspace is a git repo
string error = 3; // Error message if any
bool is_multi_root = 4; // Whether multiple workspace folders are open (worktrees not supported)
bool is_subfolder = 5; // Whether workspace is a subfolder of a git repo (not at repo root)
string git_root_path = 6; // The actual git root path (useful when is_subfolder is true)
}
// Request to create a new worktree
message CreateWorktreeRequest {
Metadata metadata = 1;
string path = 2; // Path for the new worktree
optional string branch = 3; // Branch name (creates new if doesn't exist)
optional string base_branch = 4; // Base branch for new branch creation
bool create_new_branch = 5; // Whether to create a new branch
}
// Request to delete a worktree
message DeleteWorktreeRequest {
Metadata metadata = 1;
string path = 2; // Path of the worktree to delete
bool force = 3; // Force deletion even if dirty
bool delete_branch = 4; // Also delete the branch
string branch_name = 5; // Name of the branch to delete (required if delete_branch is true)
}
// Request to switch to a worktree
message SwitchWorktreeRequest {
Metadata metadata = 1;
string path = 2; // Path of the worktree to switch to
bool new_window = 3; // Whether to open in a new window
}
// Result of worktree operations
message WorktreeResult {
bool success = 1;
string message = 2; // Success or error message
optional Worktree worktree = 3; // The affected worktree (for create)
}
// List of available branches
message BranchList {
repeated string local_branches = 1;
repeated string remote_branches = 2;
string current_branch = 3;
}
// Suggested defaults for creating a new worktree
message WorktreeDefaults {
string suggested_branch = 1; // Auto-generated branch name like "worktree/cline-abc12"
string suggested_path = 2; // Path in Documents/Cline/Worktrees/<project>-<suffix>
}
// Status of .worktreeinclude file
message WorktreeIncludeStatus {
bool exists = 1; // Whether .worktreeinclude exists
string gitignore_content = 2; // Content of .gitignore (for prefilling)
bool has_gitignore = 3; // Whether .gitignore exists
}
// Request to create .worktreeinclude file
message CreateWorktreeIncludeRequest {
string content = 1; // Content for the .worktreeinclude file
}
// Request to checkout a branch in the current worktree
message CheckoutBranchRequest {
Metadata metadata = 1;
string branch = 2; // Branch name to checkout
}
// Request to merge a worktree's branch into target branch
message MergeWorktreeRequest {
Metadata metadata = 1;
string worktree_path = 2; // Path of the worktree to merge
string target_branch = 3; // Branch to merge into (e.g., "main")
bool delete_after_merge = 4; // Whether to delete the worktree after successful merge
}
// Result of merge operation
message MergeWorktreeResult {
bool success = 1;
string message = 2; // Success or error message
bool has_conflicts = 3; // Whether merge resulted in conflicts
repeated string conflicting_files = 4; // List of files with conflicts
string source_branch = 5; // The branch that was merged
string target_branch = 6; // The branch merged into
}
// Request to track worktree view opened (for telemetry)
message TrackWorktreeViewOpenedRequest {
string source = 1; // Where the view was opened from: "home_page" or "menu_bar"
}
+3
View File
@@ -33,6 +33,9 @@ service EnvService {
// Initiates a graceful shutdown of the host bridge service.
rpc shutdown(cline.EmptyRequest) returns (cline.Empty);
// Logs a debug message to the host environment's log/output console.
rpc debugLog(cline.StringRequest) returns (cline.Empty);
}
message GetHostVersionResponse {
+13
View File
@@ -35,6 +35,9 @@ service WorkspaceService {
// Executes a command in a new terminal
rpc executeCommandInTerminal(ExecuteCommandInTerminalRequest) returns (ExecuteCommandInTerminalResponse);
// Opens a folder/workspace in the IDE
rpc openFolder(OpenFolderRequest) returns (OpenFolderResponse);
}
message GetWorkspacePathsRequest {
@@ -107,3 +110,13 @@ message ExecuteCommandInTerminalRequest {
message ExecuteCommandInTerminalResponse {
bool success = 1; // Whether the command was successfully sent to the terminal
}
// Request to open a folder/workspace
message OpenFolderRequest {
string path = 1; // The path to the folder to open
bool new_window = 2; // Whether to open in a new window
}
message OpenFolderResponse {
bool success = 1;
}
+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.
+413
View File
@@ -0,0 +1,413 @@
#!/usr/bin/env node
/**
* Generates proto message definitions from TypeScript source of truth.
*
* This script reads the field definitions from src/shared/storage/state-keys.ts
* and generates the corresponding proto message definitions for Secrets and Settings.
*
* Usage: node scripts/generate-state-proto.mjs
*
* The generated proto content is written to proto/cline/state.proto,
* replacing only the Secrets and Settings messages while preserving
* the rest of the file (services, enums, other messages).
*/
import * as fs from "node:fs/promises"
import { Project, SyntaxKind } from "ts-morph"
const STATE_KEYS_PATH = "src/shared/storage/state-keys.ts"
const STATE_PROTO_PATH = "proto/cline/state.proto"
/**
* Convert camelCase to snake_case for proto field names
*/
function camelToSnake(str) {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
}
// Fields that should use int64 instead of int32
const INT64_FIELDS = new Set(["planModeThinkingBudgetTokens", "actModeThinkingBudgetTokens"])
// Fields that should use double instead of int32
const DOUBLE_FIELDS = new Set(["autoCondenseThreshold"])
/**
* Infer proto type from TypeScript type expression
* @param {string} typeText - The TypeScript type expression
* @param {string} [fieldName] - Optional field name for field-specific overrides
*/
function inferProtoType(typeText, fieldName) {
// Remove 'undefined' from union types
const cleanType = typeText
.replace(/\s*\|\s*undefined/g, "")
.replace(/undefined\s*\|\s*/g, "")
.trim()
// Handle common types
if (cleanType === "string") {
return "string"
}
if (cleanType === "boolean") {
return "bool"
}
if (cleanType === "number") {
// Some number fields need specific numeric types
if (fieldName && INT64_FIELDS.has(fieldName)) {
return "int64"
}
if (fieldName && DOUBLE_FIELDS.has(fieldName)) {
return "double"
}
return "int32"
}
// Handle Record<string, string> as map<string, string>
if (/Record\s*<\s*string\s*,\s*string\s*>/.test(cleanType)) {
return "map<string, string>"
}
// Handle specific known types that map to proto messages/enums
// Order matters! More specific types must come before generic ones
// (e.g., OpenAiCompatibleModelInfo before ModelInfo)
// Check known types BEFORE string literals, since types like `"act" as Mode`
// contain quotes but should map to proto enums
const knownTypes = [
// Specific model info types first
["OpenAiCompatibleModelInfo", "OpenAiCompatibleModelInfo"],
["LiteLLMModelInfo", "LiteLLMModelInfo"],
["OcaModelInfo", "OcaModelInfo"],
// Generic ModelInfo last (catches OpenRouterModelInfo, etc.)
["ModelInfo", "OpenRouterModelInfo"],
// Other types - order matters for substring matching
["AutoApprovalSettings", "AutoApprovalSettings"],
["BrowserSettings", "BrowserSettings"],
["DictationSettings", "DictationSettings"],
["FocusChainSettings", "FocusChainSettings"],
["OpenaiReasoningEffort", "OpenaiReasoningEffort"],
["PlanActMode", "PlanActMode"],
["ApiProvider", "ApiProvider"],
["LanguageModelChatSelector", "LanguageModelChatSelector"], // Must come before "Mode" check
]
for (const [tsType, protoType] of knownTypes) {
if (cleanType.includes(tsType)) {
return protoType
}
}
// Check for Mode type separately with word boundary to avoid matching "VsCodeLmModelSelector"
// This handles TS `Mode` type which maps to proto `PlanActMode`
if (/\bMode\b/.test(cleanType)) {
return "PlanActMode"
}
// Handle specific string literal unions (treat as string)
// This comes after known types check since some types like `"act" as Mode` contain quotes
if (cleanType.includes('"') || cleanType.includes("'")) {
return "string"
}
// Default to string for complex types we can't map
return "string"
}
/**
* Parse the SECRETS_KEYS array from state-keys.ts
*/
function parseSecretsKeys(sourceFile) {
const secretsDecl = sourceFile.getVariableDeclaration("SECRETS_KEYS")
if (!secretsDecl) {
throw new Error("Could not find SECRETS_KEYS declaration")
}
let initializer = secretsDecl.getInitializer()
if (!initializer) {
throw new Error("SECRETS_KEYS has no initializer")
}
// Handle 'as const' expression
if (initializer.getKind() === SyntaxKind.AsExpression) {
initializer = initializer.getExpression()
}
if (initializer.getKind() !== SyntaxKind.ArrayLiteralExpression) {
throw new Error(`SECRETS_KEYS is not an array literal (got ${SyntaxKind[initializer.getKind()]})`)
}
const keys = []
for (const element of initializer.getElements()) {
const text = element.getText()
// Remove quotes and handle special prefixes
const key = text.replace(/^['"]|['"]$/g, "")
// Skip prefixed keys like "cline:clineAccountId"
if (!key.includes(":")) {
keys.push(key)
}
}
return keys
}
/**
* Parse field definitions from an object literal in state-keys.ts
*/
function parseFieldDefinitions(sourceFile, variableName) {
const decl = sourceFile.getVariableDeclaration(variableName)
if (!decl) {
throw new Error(`Could not find ${variableName} declaration`)
}
const initializer = decl.getInitializer()
if (!initializer) {
throw new Error(`${variableName} has no initializer`)
}
// Handle 'satisfies' expression
let objectLiteral = initializer
if (initializer.getKind() === SyntaxKind.SatisfiesExpression) {
objectLiteral = initializer.getExpression()
}
if (objectLiteral.getKind() !== SyntaxKind.ObjectLiteralExpression) {
throw new Error(`${variableName} is not an object literal`)
}
const fields = []
for (const prop of objectLiteral.getProperties()) {
if (prop.getKind() !== SyntaxKind.PropertyAssignment) {
continue
}
const name = prop.getName()
const propInit = prop.getInitializer()
if (!propInit || propInit.getKind() !== SyntaxKind.ObjectLiteralExpression) {
continue
}
// Get the 'default' property to infer the type
const defaultProp = propInit.getProperty("default")
if (!defaultProp) {
continue
}
let typeText = "string"
const defaultInit = defaultProp.getInitializer()
if (defaultInit) {
// Check for 'as' expression to get the type
if (defaultInit.getKind() === SyntaxKind.AsExpression) {
const typeNode = defaultInit.getTypeNode()
if (typeNode) {
typeText = typeNode.getText()
}
} else {
// Infer from literal
const text = defaultInit.getText()
if (text === "true" || text === "false") {
typeText = "boolean"
} else if (/^\d+$/.test(text)) {
typeText = "number"
} else if (/^\d+\.\d+$/.test(text)) {
typeText = "number"
}
}
}
fields.push({
name,
tsType: typeText,
protoType: inferProtoType(typeText, name),
})
}
return fields
}
/**
* Convert snake_case to camelCase for mapping proto fields back to TS keys
*/
function snakeToCamel(str) {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
}
/**
* Parse field numbers from an existing proto message definition
* Returns a map of camelCase field names to their field numbers
*/
function parseProtoMessageFieldNumbers(protoContent, messageName) {
const fieldNumbers = {}
// Match the message block (handles single-level nesting for now)
const messageRegex = new RegExp(`message\\s+${messageName}\\s*\\{([^}]*(?:\\{[^}]*\\}[^}]*)*)\\}`, "s")
const match = protoContent.match(messageRegex)
if (!match) {
return fieldNumbers
}
const messageBody = match[1]
// Match field definitions: optional/required/repeated type name = number;
const fieldRegex = /(?:optional|required|repeated)?\s*\w+\s+(\w+)\s*=\s*(\d+)\s*;/g
const matches = messageBody.matchAll(fieldRegex)
for (const fieldMatch of matches) {
const snakeName = fieldMatch[1]
const fieldNum = parseInt(fieldMatch[2], 10)
const camelName = snakeToCamel(snakeName)
fieldNumbers[camelName] = fieldNum
}
return fieldNumbers
}
/**
* Load field number mappings from existing proto file
*/
async function loadFieldNumbersFromProto() {
try {
const protoContent = await fs.readFile(STATE_PROTO_PATH, "utf-8")
const secrets = parseProtoMessageFieldNumbers(protoContent, "Secrets")
const settings = parseProtoMessageFieldNumbers(protoContent, "Settings")
console.log(` Found ${Object.keys(secrets).length} existing Secrets fields`)
console.log(` Found ${Object.keys(settings).length} existing Settings fields`)
return { Secrets: secrets, Settings: settings }
} catch {
// Proto file doesn't exist, start fresh
return { Secrets: {}, Settings: {} }
}
}
/**
* Assign field numbers, preserving existing assignments and adding new ones
*/
function assignFieldNumbers(fields, existingNumbers, startNumber = 1) {
const result = {}
let nextNumber = startNumber
// Find the highest existing number
for (const num of Object.values(existingNumbers)) {
if (num >= nextNumber) {
nextNumber = num + 1
}
}
// Preserve existing assignments
for (const field of fields) {
if (existingNumbers[field.name] !== undefined) {
result[field.name] = existingNumbers[field.name]
}
}
// Assign new numbers for new fields
for (const field of fields) {
if (result[field.name] === undefined) {
result[field.name] = nextNumber++
}
}
return result
}
/**
* Generate proto message definition
*/
function generateProtoMessage(messageName, fields, fieldNumbers) {
const lines = [`message ${messageName} {`]
// Sort fields by field number for consistent output
const sortedFields = [...fields].sort((a, b) => fieldNumbers[a.name] - fieldNumbers[b.name])
for (const field of sortedFields) {
const snakeName = camelToSnake(field.name)
const fieldNum = fieldNumbers[field.name]
// Map types cannot have the 'optional' modifier in proto3
const prefix = field.protoType.startsWith("map<") ? "" : "optional "
lines.push(` ${prefix}${field.protoType} ${snakeName} = ${fieldNum};`)
}
lines.push("}")
return lines.join("\n")
}
/**
* Generate Secrets message from SECRETS_KEYS
*/
function generateSecretsMessage(secretsKeys, fieldNumbers) {
const fields = secretsKeys.map((key) => ({
name: key,
protoType: "string",
}))
return generateProtoMessage("Secrets", fields, fieldNumbers)
}
/**
* Replace a message in the proto file content
*/
function replaceMessage(protoContent, messageName, newMessageContent) {
// Match the message definition including nested braces
const messageRegex = new RegExp(`message\\s+${messageName}\\s*\\{[^}]*(?:\\{[^}]*\\}[^}]*)*\\}`, "g")
if (messageRegex.test(protoContent)) {
return protoContent.replace(messageRegex, newMessageContent)
} else {
// Message doesn't exist, append before the first message or at end
console.warn(`Warning: ${messageName} message not found in proto file, appending`)
return protoContent + "\n\n" + newMessageContent
}
}
async function main() {
console.log("Generating proto definitions from TypeScript source...")
// Parse TypeScript source
const project = new Project({
tsConfigFilePath: "tsconfig.json",
})
const sourceFile = project.addSourceFileAtPath(STATE_KEYS_PATH)
// Parse definitions
const secretsKeys = parseSecretsKeys(sourceFile)
console.log(`Found ${secretsKeys.length} secret keys`)
const apiHandlerFields = parseFieldDefinitions(sourceFile, "API_HANDLER_SETTINGS_FIELDS")
const userSettingsFields = parseFieldDefinitions(sourceFile, "USER_SETTINGS_FIELDS")
const settingsFields = [...apiHandlerFields, ...userSettingsFields]
console.log(`Found ${settingsFields.length} settings fields`)
// Load existing field numbers from proto file
const existingFieldNumbers = await loadFieldNumbersFromProto()
// Assign field numbers (preserving existing, adding new ones)
const secretsFieldNumbers = assignFieldNumbers(
secretsKeys.map((k) => ({ name: k })),
existingFieldNumbers.Secrets,
1,
)
const settingsFieldNumbers = assignFieldNumbers(settingsFields, existingFieldNumbers.Settings, 1)
// Generate messages
const secretsMessage = generateSecretsMessage(secretsKeys, secretsFieldNumbers)
const settingsMessage = generateProtoMessage("Settings", settingsFields, settingsFieldNumbers)
// Read existing proto file
let protoContent = await fs.readFile(STATE_PROTO_PATH, "utf-8")
// Replace messages
protoContent = replaceMessage(protoContent, "Secrets", secretsMessage)
protoContent = replaceMessage(protoContent, "Settings", settingsMessage)
// Write updated proto file
await fs.writeFile(STATE_PROTO_PATH, protoContent)
console.log(`Updated ${STATE_PROTO_PATH}`)
console.log("\nGeneration complete! Run 'npm run protos' to regenerate TypeScript from protos.")
}
main().catch((error) => {
console.error("Error:", error)
process.exit(1)
})
+20 -4
View File
@@ -22,19 +22,34 @@ echo ""
# Always rebuild CLI to ensure latest changes
echo -e "${CYAN}${NC} ${DIM}Rebuilding CLI binaries...${NC}"
cd "$PROJECT_ROOT"
if npm run compile-cli 2>&1 | grep -E "(built|error|Error)" || true; then
rm -rf "$PROJECT_ROOT/cli/bin"
if command -v go >/dev/null 2>&1; then
GO_BIN_DIR="$(go env GOPATH 2>/dev/null)/bin"
if [ -d "$GO_BIN_DIR" ]; then
export PATH="$GO_BIN_DIR:$PATH"
fi
fi
if npm run compile-cli; then
echo -e "${GREEN}${NC} CLI binaries rebuilt"
else
echo -e "${YELLOW}${NC} CLI build may have issues - check output above"
echo -e "${YELLOW}${NC} CLI build failed - aborting install"
exit 1
fi
# Always rebuild standalone to ensure latest cline-core.js
echo -e "${CYAN}${NC} ${DIM}Rebuilding standalone package (this may take ~30 seconds)...${NC}"
if npm run compile-standalone 2>&1 | tail -5; then
rm -rf "$PROJECT_ROOT/dist-standalone"
if npm run compile-standalone; then
echo -e "${GREEN}${NC} Standalone package rebuilt"
else
echo -e "${YELLOW}${NC} Standalone build may have issues - check output above"
echo -e "${YELLOW}${NC} Standalone build failed - aborting install"
exit 1
fi
# Ensure extension package.json is present for cline-core startup
mkdir -p "$PROJECT_ROOT/dist-standalone/extension"
cp "$PROJECT_ROOT/package.json" "$PROJECT_ROOT/dist-standalone/extension/package.json"
echo ""
echo -e "${CYAN}${NC} ${DIM}Installing to $INSTALL_DIR${NC}"
@@ -57,6 +72,7 @@ rsync -a --exclude='bin' "$PROJECT_ROOT/dist-standalone/" "$INSTALL_DIR/"
echo -e "${CYAN}${NC} ${DIM}Installing runtime dependencies...${NC}"
cd "$PROJECT_ROOT/standalone/runtime-files"
npm install --silent 2>/dev/null || npm install
rm -rf "$INSTALL_DIR/node_modules"
cp -r node_modules "$INSTALL_DIR/"
cp -r vscode "$INSTALL_DIR/node_modules/"
cd "$PROJECT_ROOT"
+1 -1
View File
@@ -105,7 +105,7 @@ async function main(): Promise<void> {
console.log("Extracting standalone.zip to extensions directory...")
try {
if (!fs.existsSync(extensionsDir)) {
execSync(`unzip -q "${standaloneZipPath}" -d "${extensionsDir}"`, { stdio: "inherit" })
execSync(`unzip -o -q "${standaloneZipPath}" -d "${extensionsDir}"`, { stdio: "inherit" })
}
console.log(`Successfully extracted standalone.zip to: ${extensionsDir}`)
} catch (error) {
+542
View File
@@ -0,0 +1,542 @@
import { afterEach, beforeEach, describe, it } from "mocha"
import "should"
import fs from "fs/promises"
import os from "os"
import path from "path"
import sinon from "sinon"
import { ClineConfigurationError, ClineEndpoint, ClineEnv, Environment } from "../config"
describe("ClineEndpoint configuration", () => {
let sandbox: sinon.SinonSandbox
let tempDir: string
let originalHomedir: typeof os.homedir
beforeEach(async () => {
sandbox = sinon.createSandbox()
tempDir = path.join(os.tmpdir(), `config-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
await fs.mkdir(tempDir, { recursive: true })
// Create .cline directory
await fs.mkdir(path.join(tempDir, ".cline"), { recursive: true })
// Stub os.homedir to return our temp directory
originalHomedir = os.homedir
sandbox
.stub(os, "homedir")
.returns(tempDir)
// Reset the singleton state using internal method
;(ClineEndpoint as any)._instance = null
;(ClineEndpoint as any)._initialized = false
})
afterEach(async () => {
sandbox.restore()
// Reset singleton state
;(ClineEndpoint as any)._instance = null
;(ClineEndpoint as any)._initialized = false
try {
await fs.rm(tempDir, { recursive: true, force: true })
} catch {
// Ignore cleanup errors
}
})
describe("valid config parsing", () => {
it("should parse valid endpoints.json with all required fields", async () => {
const validConfig = {
appBaseUrl: "https://app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
await ClineEndpoint.initialize()
const config = ClineEndpoint.config
config.appBaseUrl.should.equal("https://app.enterprise.com")
config.apiBaseUrl.should.equal("https://api.enterprise.com")
config.mcpBaseUrl.should.equal("https://mcp.enterprise.com")
config.environment.should.equal(Environment.selfHosted)
})
it("should work without endpoints.json (standard mode)", async () => {
// No endpoints.json file exists
await ClineEndpoint.initialize()
const config = ClineEndpoint.config
config.environment.should.not.equal(Environment.selfHosted)
// Should use production defaults
config.appBaseUrl.should.equal("https://app.cline.bot")
config.apiBaseUrl.should.equal("https://api.cline.bot")
})
it("should accept URLs with ports", async () => {
const validConfig = {
appBaseUrl: "http://localhost:3000",
apiBaseUrl: "http://localhost:7777",
mcpBaseUrl: "http://localhost:8080/mcp",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
await ClineEndpoint.initialize()
const config = ClineEndpoint.config
config.appBaseUrl.should.equal("http://localhost:3000")
config.apiBaseUrl.should.equal("http://localhost:7777")
config.mcpBaseUrl.should.equal("http://localhost:8080/mcp")
})
it("should accept URLs with paths", async () => {
const validConfig = {
appBaseUrl: "https://proxy.enterprise.com/cline/app",
apiBaseUrl: "https://proxy.enterprise.com/cline/api",
mcpBaseUrl: "https://proxy.enterprise.com/cline/mcp",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
await ClineEndpoint.initialize()
const config = ClineEndpoint.config
config.appBaseUrl.should.equal("https://proxy.enterprise.com/cline/app")
})
})
describe("invalid JSON handling", () => {
it("should throw ClineConfigurationError for invalid JSON syntax", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "{ invalid json }", "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("Invalid JSON")
}
})
it("should throw ClineConfigurationError for truncated JSON", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), '{"appBaseUrl": "https://test.com"', "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("Invalid JSON")
}
})
it("should throw ClineConfigurationError for empty file", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "", "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
}
})
it("should throw ClineConfigurationError for non-object JSON", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), '"just a string"', "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("must contain a JSON object")
}
})
it("should throw ClineConfigurationError for array JSON", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "[]", "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
// Arrays pass the object check but fail on required fields
error.message.should.containEql("Missing required field")
}
})
it("should throw ClineConfigurationError for null JSON", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "null", "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("must contain a JSON object")
}
})
})
describe("missing required fields", () => {
it("should throw ClineConfigurationError when appBaseUrl is missing", async () => {
const config = {
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql('Missing required field "appBaseUrl"')
}
})
it("should throw ClineConfigurationError when apiBaseUrl is missing", async () => {
const config = {
appBaseUrl: "https://app.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql('Missing required field "apiBaseUrl"')
}
})
it("should throw ClineConfigurationError when mcpBaseUrl is missing", async () => {
const config = {
appBaseUrl: "https://app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql('Missing required field "mcpBaseUrl"')
}
})
it("should throw ClineConfigurationError when all fields are missing", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "{}", "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("Missing required field")
}
})
it("should throw ClineConfigurationError when field is null", async () => {
const config = {
appBaseUrl: null,
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql('Missing required field "appBaseUrl"')
}
})
it("should throw ClineConfigurationError when field is empty string", async () => {
const config = {
appBaseUrl: "",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("cannot be empty")
}
})
it("should throw ClineConfigurationError when field is whitespace only", async () => {
const config = {
appBaseUrl: " ",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("cannot be empty")
}
})
it("should throw ClineConfigurationError when field is non-string", async () => {
const config = {
appBaseUrl: 12345,
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("must be a string")
}
})
})
describe("invalid URL detection", () => {
it("should throw ClineConfigurationError for invalid URL format", async () => {
const config = {
appBaseUrl: "not-a-valid-url",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("must be a valid URL")
}
})
it("should throw ClineConfigurationError for URL without protocol", async () => {
const config = {
appBaseUrl: "app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("must be a valid URL")
}
})
it("should throw ClineConfigurationError for malformed URL", async () => {
const config = {
appBaseUrl: "https://",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("must be a valid URL")
}
})
it("should include the invalid URL value in error message", async () => {
const invalidUrl = "definitely-not-a-url"
const config = {
appBaseUrl: invalidUrl,
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql(invalidUrl)
}
})
})
describe("environment switching blocked in self-hosted mode", () => {
it("should throw error when trying to change environment in self-hosted mode", async () => {
const config = {
appBaseUrl: "https://app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
await ClineEndpoint.initialize()
// Verify we're in self-hosted mode
ClineEndpoint.config.environment.should.equal(Environment.selfHosted)
// Try to change environment - should throw
try {
ClineEnv.setEnvironment("staging")
throw new Error("Should have thrown")
} catch (error: any) {
error.message.should.containEql("Cannot change environment in on-premise mode")
}
})
it("should throw error for all environment values in self-hosted mode", async () => {
const config = {
appBaseUrl: "https://app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
await ClineEndpoint.initialize()
const environments = ["staging", "local", "production", "anything"]
for (const env of environments) {
try {
ClineEnv.setEnvironment(env)
throw new Error(`Should have thrown for environment: ${env}`)
} catch (error: any) {
error.message.should.containEql("Cannot change environment in on-premise mode")
}
}
})
it("should allow environment switching in standard mode", async () => {
// No endpoints.json file - standard mode
await ClineEndpoint.initialize()
// Verify we're NOT in self-hosted mode
ClineEndpoint.config.environment.should.not.equal(Environment.selfHosted)
// Should be able to change environment
ClineEnv.setEnvironment("staging")
ClineEnv.getEnvironment().environment.should.equal("staging")
ClineEnv.setEnvironment("local")
ClineEnv.getEnvironment().environment.should.equal("local")
ClineEnv.setEnvironment("production")
ClineEnv.getEnvironment().environment.should.equal("production")
})
})
describe("self-hosted mode behavior", () => {
it("should report selfHosted environment in self-hosted mode", async () => {
const config = {
appBaseUrl: "https://app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
await ClineEndpoint.initialize()
const envConfig = ClineEndpoint.config
envConfig.environment.should.equal(Environment.selfHosted)
})
it("should use custom endpoints from file", async () => {
const customConfig = {
appBaseUrl: "https://custom-app.internal",
apiBaseUrl: "https://custom-api.internal",
mcpBaseUrl: "https://custom-mcp.internal/v1",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(customConfig), "utf8")
await ClineEndpoint.initialize()
const config = ClineEndpoint.config
config.appBaseUrl.should.equal("https://custom-app.internal")
config.apiBaseUrl.should.equal("https://custom-api.internal")
config.mcpBaseUrl.should.equal("https://custom-mcp.internal/v1")
})
})
describe("initialization behavior", () => {
it("should only initialize once", async () => {
await ClineEndpoint.initialize()
ClineEndpoint.isInitialized().should.be.true()
// Second initialize should be a no-op
await ClineEndpoint.initialize()
ClineEndpoint.isInitialized().should.be.true()
})
it("should throw error when accessing config before initialization", async () => {
// Already reset in beforeEach, so accessing should throw
try {
const _ = ClineEndpoint.config
throw new Error("Should have thrown")
} catch (error: any) {
error.message.should.containEql("not initialized")
}
})
})
describe("isSelfHosted() method", () => {
it("should return true when not initialized (safety fallback)", async () => {
// Reset singleton state - already done in beforeEach, not initialized
ClineEndpoint.isInitialized().should.be.false()
ClineEndpoint.isSelfHosted().should.be.true()
})
it("should return true when in self-hosted mode", async () => {
const config = {
appBaseUrl: "https://app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
await ClineEndpoint.initialize()
ClineEndpoint.isSelfHosted().should.be.true()
})
it("should return false when in normal mode (no endpoints.json)", async () => {
// No endpoints.json file exists
await ClineEndpoint.initialize()
ClineEndpoint.isSelfHosted().should.be.false()
})
})
})
-295
View File
@@ -1,295 +0,0 @@
import { ModelInfo } from "@shared/api"
import { Logger } from "@/services/logging/Logger"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiHandler } from "../../core/api/index"
import { ApiStream } from "../../core/api/transform/stream"
interface DifyHandlerOptions {
difyApiKey?: string
difyBaseUrl?: string
}
export class DifyHandler implements ApiHandler {
private options: DifyHandlerOptions
private baseUrl: string
private apiKey: string
private conversationId: string | null = null
constructor(options: DifyHandlerOptions) {
this.options = options
this.apiKey = options.difyApiKey || ""
this.baseUrl = options.difyBaseUrl || ""
Logger.log("[DIFY DEBUG] Constructor called with:", {
hasApiKey: !!this.apiKey,
baseUrl: this.baseUrl,
})
if (!this.apiKey) {
throw new Error("Dify API key is required")
}
if (!this.baseUrl) {
throw new Error("Dify base URL is required")
}
}
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
Logger.log("[DIFY DEBUG] createMessage called with:", {
systemPromptLength: systemPrompt?.length || 0,
messagesCount: messages?.length || 0,
})
// Convert messages to Dify format
const query = this.convertMessagesToQuery(systemPrompt, messages)
const requestBody = {
inputs: {},
query: query,
response_mode: "streaming",
conversation_id: this.conversationId || "",
user: "cline-user", // A unique user identifier
files: [],
}
const fullUrl = `${this.baseUrl}/chat-messages`
Logger.log("[DIFY DEBUG] Making request to:", fullUrl)
Logger.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
Logger.log("[DIFY DEBUG] Current process environment variables (for proxy debugging):", process.env)
let response: Response
try {
response = await fetch(fullUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
})
} catch (error: any) {
Logger.error("[DIFY DEBUG] Network error during fetch:", error)
// Log more detailed error information if available (e.g., from undici)
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
throw new Error(`Dify API network error: ${error.message}${cause}`)
}
Logger.log("[DIFY DEBUG] Response status:", response.status)
const headersObj: Record<string, string> = {}
response.headers.forEach((value, key) => {
headersObj[key] = value
})
Logger.log("[DIFY DEBUG] Response headers:", headersObj)
if (!response.ok) {
const errorText = await response.text()
Logger.debug("[DIFY DEBUG] Error response:", errorText)
throw new Error(`Dify API error: ${response.status} ${response.statusText} - ${errorText}`)
}
if (!response.body) {
throw new Error("No response body from Dify API")
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ""
let fullText = ""
Logger.log("[DIFY DEBUG] Starting to read streaming response...")
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
Logger.log("[DIFY DEBUG] Stream ended naturally")
break
}
const chunk = decoder.decode(value, { stream: true })
Logger.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
buffer += chunk
const lines = buffer.split("\n")
// Keep the last incomplete line in the buffer
buffer = lines.pop() || ""
for (const line of lines) {
Logger.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
if (line.startsWith("data: ")) {
const data = line.slice(6).trim()
Logger.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
if (data === "[DONE]") {
Logger.log("[DIFY DEBUG] Received [DONE] signal")
return // Explicitly return on [DONE]
}
if (data === "") {
Logger.log("[DIFY DEBUG] Empty data line, skipping")
continue
}
try {
const parsed = JSON.parse(data)
Logger.log("[DIFY DEBUG] Parsed JSON:", parsed)
// Capture conversation_id as soon as it's available
if (parsed.conversation_id && !this.conversationId) {
this.conversationId = parsed.conversation_id
Logger.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
}
// Handle different Dify event types based on actual Dify API
if (parsed.event === "message") {
Logger.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
// Dify sends the full text in each "answer" chunk, so we replace.
if (typeof parsed.answer === "string") {
fullText = parsed.answer
Logger.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
}
}
} else if (parsed.event === "message_replace") {
Logger.log("[DIFY DEBUG] Replace message event:", parsed)
if (parsed.answer) {
fullText = parsed.answer // Replace instead of append
Logger.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
}
}
} else if (parsed.event === "message_end") {
Logger.log("[DIFY DEBUG] Message end event", parsed)
// Message completed. Yield final text if we have any.
if (fullText) {
yield {
type: "text",
text: fullText,
}
}
// Yield usage data if available
if (parsed.usage) {
yield {
type: "usage",
inputTokens: parsed.usage.prompt_tokens || 0,
outputTokens: parsed.usage.completion_tokens || parsed.usage.total_tokens || 0,
totalCost: parsed.usage.total_price || 0,
}
}
return // End of stream
} else if (parsed.event === "error") {
Logger.error("[DIFY DEBUG] Error event:", parsed)
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
} else if (parsed.event === "workflow_started" || parsed.event === "workflow_finished") {
Logger.log("[DIFY DEBUG] Workflow event:", parsed.event)
// These are informational events, continue processing
} else if (parsed.event === "node_started" || parsed.event === "node_finished") {
Logger.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
// These are informational events, continue processing
} else if (parsed.event === "ping") {
Logger.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
// Ping event, do nothing
} else {
Logger.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
// Try to extract text from other possible fields
if (parsed.text) {
fullText += parsed.text
yield {
type: "text",
text: fullText,
}
} else if (parsed.content) {
fullText += parsed.content
yield {
type: "text",
text: fullText,
}
}
}
} catch (e) {
Logger.log("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
}
} else if (line.trim() !== "") {
Logger.log(
"[DIFY DEBUG] Non-data line (not starting with 'data:'), trying to parse as direct JSON:",
JSON.stringify(line),
)
// Try to parse as direct JSON (fallback for non-SSE responses, though Dify uses SSE)
try {
const parsed = JSON.parse(line.trim())
Logger.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
// Handle the same event types as above
if (parsed.event === "message" && parsed.answer) {
fullText += parsed.answer
yield {
type: "text",
text: fullText,
}
} else if (parsed.event === "message_end") {
if (fullText) {
yield {
type: "text",
text: fullText,
}
}
return
} else if (parsed.event === "error") {
Logger.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
}
} catch (e) {
// Not JSON, continue
Logger.log("[DIFY DEBUG] Line is not direct JSON, continuing")
}
}
}
}
} finally {
reader.releaseLock()
Logger.log("[DIFY DEBUG] Stream reader released")
}
}
private convertMessagesToQuery(systemPrompt: string, messages: ClineStorageMessage[]): string {
// Dify's context is managed by `conversation_id`. The `query` should be the last user message.
// The system prompt is typically configured in the Dify App itself.
const lastUserMessage = messages.filter((m) => m.role === "user").pop()
if (!lastUserMessage) {
return "" // Should not happen in normal flow
}
const userQuery = Array.isArray(lastUserMessage.content)
? lastUserMessage.content.map((c) => ("text" in c ? c.text : "")).join("\n")
: (lastUserMessage.content as string)
// Only prepend the system prompt if it's the very first message of a new conversation.
if (!this.conversationId && systemPrompt) {
Logger.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
return `${systemPrompt}\n\n---\n\n${userQuery}`
}
return userQuery
}
getModel(): { id: string; info: ModelInfo } {
return {
id: "dify-workflow",
info: {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Dify workflow - model selection is configured in your Dify application",
},
}
}
}

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