Compare commits

...

533 Commits

Author SHA1 Message Date
abeatrix 2ae6db5620 feat(ui): add new shadcn/ui components (dialog, slider, toggle, skeleton)
Add four new UI components built on Radix UI primitives:
- Dialog: modal dialog component for overlays and alerts
- Slider: range input component for numeric value selection
- Toggle: button component for on/off state toggling
- Skeleton: loading placeholder component for content

These components follow the shadcn/ui pattern and include the necessary
Radix UI dependencies (@radix-ui/react-dialog, @radix-ui/react-slider,
@radix-ui/react-toggle) to provide accessible, customizable UI primitives
for the webview interface.
2025-12-04 11:15:30 -08:00
abeatrix 83fe701b9c Merge branch 'main' into bee/ui-stories 2025-12-04 10:34:20 -08:00
abeatrix d47b054757 update 2025-12-04 10:31:49 -08:00
abeatrix 05d816848d unify styles 2025-12-04 10:22:40 -08:00
Bee 4e3fe004f4 feat: enable native tool calling for deepseek 3.2 [AI-27] (#7877)
* feat: enable native tool calling for deepseek 3.2

Add isDeepSeek32ModelFamily() function to identify DeepSeek 3.2 models and integrate it into the isNextGenModelFamily() check. This classifies DeepSeek 3.2 as a next-generation model family, enabling native tool calling support.

* typo
2025-12-04 09:59:27 -08:00
Zhongying Qiao 2b63eed85e feat: remove mcp enable setting for individual users (#7879) 2025-12-04 09:36:44 -08:00
Tomás Barreiro 2ffdc50ea1 Prevent simultaneous refreshes when restoring auth info (#7835)
* Prevent multiple simultaneos refreshes when retrieving auth info

* Add changeset

* refactor
2025-12-04 14:45:10 +01:00
celestial-vault 74808431e5 add litellm provider to remote config in the extension (#7775) 2025-12-04 03:01:05 -08:00
Saoud Rizwan 7a523fbaf6 feat(prompt): add command output limiting guidance to capabilities (#7884)
* feat(prompt): add command output limiting guidance to capabilities

Add guidance in the system prompt instructing the model to proactively
limit command output when anticipating large results. Includes examples
like piping to grep/head/tail or using more specific arguments.

Idea by @AraTheBoss

* chore: add changeset

* refactor: move command output limiting guidance to execute_command tool

Move the guidance from capabilities.ts to execute_command.ts where it
belongs. Extract into a shared COMMAND_BEST_PRACTICES constant to avoid
duplication across model variants (GENERIC, NATIVE_GPT_5, NATIVE_NEXT_GEN,
GEMINI_3).
2025-12-03 21:06:07 -08:00
github-actions[bot] c22ea39dc1 v3.40.0 Release Notes (#7865)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md to reflect recent changes including fixes for highlighted text flashing, terminal command issues, and enhancements for slash command usage and message padding.

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-12-03 19:24:18 -08:00
Saoud Rizwan c9f23076c2 fix: consolidate successive error retry messages in chat UI (#7880)
When API requests fail and auto-retry is enabled, multiple error_retry
messages were shown (e.g., "Attempt 1 of 3", "Attempt 2 of 3", etc.).
This change consolidates them to only show the latest retry message,
reducing visual clutter during retry sequences.
2025-12-03 19:18:17 -08:00
Bee a5f6c1d732 feat: add auto-recovery for corrupted task history state (#7875)
* feat: add auto-recovery for corrupted task history state

Add automatic reconstruction of task history when JSON parsing fails.

Changes:
- Modified `reconstructTaskHistory()` to return reconstruction result or null
- Enhanced `readTaskHistoryFromState()` with automatic corruption recovery
- Added recursive reconstruction attempt with loop prevention flag
- Wrapped JSON parsing in try-catch to handle corruption gracefully

When task history state file is corrupted, the system now automatically
attempts to reconstruct history from existing task folders, providing
better resilience against file corruption issues.

* feat: Add telemetry tracking for extension storage errors

Replace console.error logging with structured telemetry capture for extension storage operations. This change:

- Adds a new EXTENSION_STORAGE_ERROR telemetry event type to track storage-related failures
- Implements captureExtensionStorageError method with error message truncation to prevent excessive data
- Replaces three console.error calls in readTaskHistoryFromState with telemetry events

This improves error monitoring and provides better insights into extension storage failures while maintaining data efficiency through message truncation.

* fix: improve type safety and error handling in task history

Add explicit return type to reconstructTaskHistory() function and refactor error handling in readTaskHistoryFromState() with nested try-catch blocks to better distinguish between file read errors and JSON parse errors. This improves error recovery and makes error tracking more precise through separate telemetry calls.

* add param to reconstructTaskHistory for manually called action

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-12-03 17:49:28 -08:00
Toshii 3c37a160ac support multi-index search over inner messages to find file mentions (#7850) 2025-12-03 15:27:08 -08:00
Saoud Rizwan 5c3294051f Revert "fix: don't return empty array on parse failure (#7773)" (#7874)
This reverts commit 14ccf33d25.
2025-12-03 14:30:58 -08:00
Tony Loehr 4c2f28f2af docs: remove Advanced Patterns and Testing & Debugging from Hooks documentation (#7869)
- Removed advanced-patterns.mdx and testing-and-debugging.mdx files
- Updated docs.json to remove these pages from navigation
- Updated hooks/index.mdx to remove corresponding Card components
- Simplified Hooks documentation to focus on core concepts: Overview, Hook Reference, and Samples
2025-12-03 12:11:43 -08:00
Ara 6e016298cb chore: bump version to 3.39.2 and update dependencies (#7851)
- Update package version from 3.39.1 to 3.39.2
- Upgrade @changesets/* packages to latest versions
- Update @inquirer/external-editor to 1.0.2
- Upgrade js-yaml from v3 to v4 in @changesets/parse
2025-12-03 11:23:29 -08:00
pashpashpash 0cd7bebfba markdown styling fix (#7840)
* markdown styling fix

* nested ul
2025-12-03 01:23:02 -08:00
Bee 363aac61fb fix: OpenAI Response API message format (#7842)
Fixed the message structure to match the OpenAI Responses API format.

Updated Message ID placement: The message id is stored and set at the message level, not inside the content array.

This fixes an error occuring in the current code when reasoning item is followed by a message text block: 400 Item 'rs_...' of type 'reasoning' was provided without its required following item."
2025-12-02 17:41:19 -08:00
Tony Loehr eeb1cc7da8 added subpages and content to hooks (#7797)
* added subpages and content to hooks

* Update docs/features/hooks/advanced-patterns.mdx

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

* Add complete hook type coverage with examples for TaskCancel, TaskComplete, TaskResume, PreCompact, UserPromptSubmit

* Fix hook documentation API mismatches and add TaskComplete

- Add missing TaskComplete hook to reference documentation
- Fix TaskCancel/TaskResume field paths to match protobuf API
- Improve security practices in hook examples
- Add proper error handling and validation

* Update hooks documentation: rename samples, remove PreCompact, improve structure

- Rename 'Real World Examples' to 'Samples' with skill-based organization
- Remove PreCompact references (feature not yet available)
- Update navigation structure in docs.json
- Add multiworkspace mention to Overview
- Create 9 comprehensive examples (beginner/intermediate/advanced)
- Clean up duplicate content and fix cross-references

* Update hooks documentation: Add Windows support

- Remove incorrect warning that hooks don't work on Windows
- Add positive cross-platform support note (Windows, macOS, Linux)
- Clarify that bash examples work with standard shells including Git Bash/WSL on Windows

* fixed hooks overview redirect

* Add UI screenshots to hooks documentation

* hooks in action

* fixed hooks overview and examples

* fixed terminology

* fixed hooks examples

* hooks groupings

* fixed appearance of hook names

* refactor hook docs

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-12-02 17:35:07 -08:00
Sarah Fortune f760f13de5 Don't log otel events to the console because they are really spammy (#7841) 2025-12-02 17:20:52 -08:00
canvrno dd52a4a39c feat: apply_patch auto approve (#7777)
* Added apply_patch to auto approve, strict mode, and minor prompting adjustment

* changeset
2025-12-02 15:36:01 -08:00
Toshii 639edb5db6 correctly handle new and old tool call formats for context rewriting (#7809)
* correctly handle new and old tool call formats

* spelling change
2025-12-02 15:12:05 -08:00
Jack Reinhardt 3eac9b04de fix(bedrock): add sts userAgentAppId (#7719) 2025-12-02 14:40:39 -08:00
Bee 09692d7d3a feat: add mode and token metrics info to storage messages [CLIENTS-26] (#7795)
* feat(storage): add mode and token metrics to storage messages

Add mode (plan/act) tracking to ApiProviderInfo and ClineMessageModelInfo interfaces, ensuring each storage message contains the operational mode used during API requests.

Refactor token metrics tracking by consolidating cache write/read tokens, input/output tokens, and total cost into a centralized taskMetrics object. This enables better tracking and storage of token usage and costs throughout the task lifecycle, including for partial/cancelled streams.

Updated api_req_started and api_req_finished messages to include comprehensive token metrics, allowing for accurate cost reporting even when streams are cancelled or fail mid-execution.

* update unit tests with mode

* store task metrics per assistant turn
2025-12-02 13:52:44 -08:00
Andrei Eternal c81fa0a9d6 set the cli's 'ide version' to just the cli version rather than being blank, to make environment_history work for CLI (#7712)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-12-02 13:21:58 -08:00
Bee 326c9c9f99 feat: set default thinking level for Gemini 3 Pro models (#7831)
- Reorder thinking level checks to prioritize high over low
- Auto-set thinking level to LOW for Gemini 3 Pro models when not specified
- Add clarifying comment for thinking budget usage
- Ensure thinking level is always defined for Gemini 3 models to prevent errors

This change ensures Gemini 3 Pro models always have a thinking level set (required by the API) and removes the thinking budget when a level is specified, as they are mutually exclusive parameters.
2025-12-02 12:54:13 -08:00
celestial-vault a4518b90c2 add atomic file write (#7754)
* add atomic write file using write to temp file + rename to avoid situations where invalid data is written to files due to process interrupt

* adjust concurrency test for windows to expect error

* Add JSON ending to temporary file and don't await unlink
2025-12-02 13:30:53 -06:00
celestial-vault 79f4d938e6 remove unused sentry dependency (#7823) 2025-12-02 13:20:30 -06:00
canvrno 37152329cd v3.39.2 Release Notes (#7829) 2025-12-02 10:44:38 -08:00
Seb Duerr 1332d1d70d feat(cerebras): add X-Cerebras-3rd-Party-Integration header (#7824)
* feat(cerebras): add X-Cerebras-3rd-Party-Integration header

* chore: add changeset
2025-12-02 09:56:41 -08:00
canvrno 6a0d92d683 Skip reasoning_details on microwave model (#7825) 2025-12-02 09:35:30 -08:00
Ara e761a8c252 fix(changesets): remove quotes from claude-dev package name (#7822)
The quotes around the package name "claude-dev" in all changeset files were removed to adhere to the correct YAML format. This ensures proper parsing and consistency across the changeset files.
2025-12-02 07:36:09 -08:00
Ara c26d0a076d v3.39.1 Release Notes (#7818)
* v3.39.1 Release Notes

* v3.39.1 Release Notes
2025-12-02 06:16:43 -08:00
Ara c037619b90 feat: enable ModelInfoView in OpenRouterModelPicker (#7817)
Uncomment the ModelInfoView component to display model information
in the OpenRouter model picker settings panel.
2025-12-02 06:06:54 -08:00
Ara a575a76e8f v3.39.0 Release Notes (#7813)
* v3.39.0 Release Notes

* v3.39.0 Release Notes

* feat: enhance Announcement component with new microwave model and account login functionality

- Updated Announcement component to include a new free microwave model button
- Adjusted active tab logic in OpenRouterModelPicker to default to "free" if a free model is selected

* Add demo link

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-12-02 05:11:23 -08:00
Dominic Cooney b1d15d4fe7 fix: Standalone, ensure cwd is install dir (#7781)
Our resource loading assumes cwd is the install
dir.
2025-12-02 18:17:58 +09:00
Saoud Rizwan a0708e57ee Move notification toggle to auto-approve menu (#7812)
- Remove "Configure notification settings" link and move the toggle directly into the auto-approve menu
- Remove notification setting from General Settings since it now lives in auto-approve menu
- Remove hover:opacity-80 from icon button variant to prevent dimming on hover
- Make docs link font size inherit and separator line thinner
2025-12-01 22:44:29 -08:00
Ara ab5796fa72 Remove auto approve menu popups (#7806)
* Remove popups from auto approve settings

* feat(ui): add documentation link to auto-approve modal

Add a "Docs" link in the auto-approve modal that directs users to
the auto-approve documentation page on docs.cline.bot.

* Remove popups from auto approve settings

* Remove popups from auto approve settings

* Remove popups from auto approve settings

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-12-01 22:22:53 -08:00
Saoud Rizwan b15c364a62 feat: add 'Explain Changes' feature for code review (#7765) 2025-12-01 22:02:33 -08:00
Ara 29dcc4e1e1 feat: move stealth/microwave model from recommended to free models section (#7808)
Move the stealth/microwave model entry from the recommendedModels array
to the freeModels array for better categorization of free model options.
2025-12-01 21:54:34 -08:00
Ara 126d066893 Adding Stealth model (#7764)
* Revert "Remove old models (#7118)"

This reverts commit c7c4e43322.

* Adding stealth

* Adding stealth

* Adding stealth

* Adding minor fix

* Update webview-ui/src/components/settings/OpenRouterModelPicker.tsx

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

* Apply suggestion from @abeatrix

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

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

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
2025-12-01 21:18:43 -08:00
Ara fa3e095a79 Enable NTC by default (#7804)
* Enable NTC by default

* Enable NTC by default

* Enable NTC by default
2025-12-01 20:40:12 -08:00
Sarah Fortune 4033c83b51 Log the name of the telemetry provider(s) enabled in the extension (#7799)
Right now we are logging the provider type by logging the name of the constructor, but in the compiled code this is obfuscated so it is just some random characters.

Add a name property to the telemetry provider interface.
2025-12-01 19:22:43 -08:00
canvrno 0b7ea86e9b Added microwave family system prompt configuration (#7798) 2025-12-01 17:29:27 -08:00
reneehuang1 1d9a0b5986 add enterprise to readme (#7589)
Co-authored-by: Renee Huang <reneehuang@Renees-MacBook-Pro.local>
2025-12-01 16:23:35 -08:00
CandiedUniverse c47ffe2861 fix(hooks): Polish for UserPromptSubmit [ENG-1345] (#7656)
* fix(hooks): Trigger UserPromptSubmit hook when continuing a completed task

* fix(hooks): Make prompt formating consistent for all UserPromptSubmit entrypoints

* fix(hooks): Code reviewing w/ Cline before submitting PR for human review

* fix(hooks): Improve type safety

* fix(hooks): Add unit tests for buildUserFeedbackContent

* fix(hooks): Minor Cline code review changes

* fix(hooks): Fix test assertion technique

* fix(hooks): Simplify PR

* feat(hooks): Consolidate constants to a shared location as per PR feedback
2025-12-01 15:36:07 -08:00
mintlify[bot] e85d918816 Add CLI context window configuration docs (#7796)
* Update docs/cline-cli/overview.mdx

* Update docs/cline-cli/cli-reference.mdx

* Update docs/cline-cli/three-core-flows.mdx

* Update docs/cline-cli/overview.mdx

---------

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2025-12-01 15:27:55 -08:00
Ara 49be10ead8 fix: move OptionsButtons outside WithCopyButton component (#7783)
Relocate OptionsButtons component to be a sibling of WithCopyButton
rather than a child. This fixes the component hierarchy for followup
and completion_result message types, ensuring proper rendering and
interaction behavior. Also adds QuoteButton support to completion_result.
2025-12-01 15:16:16 -08:00
Ara aa9573fb0a feat: add direct navigation to settings sections (#7770)
Replace delayed scroll-to-settings approach with direct section targeting.
Settings sections can now be opened directly via navigateToSettings(section)
parameter, eliminating the need for setTimeout-based scrolling workarounds.
2025-12-01 15:15:56 -08:00
Nick Baumann b913e47332 feat: add tabbed model picker with Recommended and Free tabs (#7769) 2025-12-01 14:25:27 -08:00
Zhongying Qiao 5be7a1b3cf Add support for Banner dismissal, event logging (#7642)
* feat: add banners ui, dismissal state handling and event log

* feat: add cli as ide type, clean up some code

* feat: wire up controller and UI for banners

* audit every rule check to ensure it is doing correct filtering and working locally

* seperate out frontend code

* clean up

* fix proto file

* fix quality check errors

* fix banner service tests

* feat: use json polling approach for active banners

* fix: build error

* fix ci

* fix quality check

* use BannerService.isInitialized() instead

* do not log error when banner array is empty, only when missing or not defined

* do not hash instance id
2025-12-01 15:32:51 -06:00
Toshii c2e91aa9c9 running context rewriting prior to running auto compact (#7774)
* running context rewriting prior to running auto compact

* use sample timestamp as in getNewContextMessagesAndMetadata

* clean up return var
2025-12-01 12:23:58 -08:00
Bee 68b93fcbea fix(ui): memoize highlighted text [ENG-1355] (#7786)
* fix(ui): memoize highlighted text

Optimize UserMessage and TaskHeader components by using useMemo to cache highlighted text results. This prevents unnecessary recalculations of text highlighting on every render, improving performance when text or editedText props haven't changed.

Changes:
- Add useMemo hook to UserMessage component for highlightText result
- Replace inline highlightText calls with memoized values
- Reduces redundant text processing during re-renders

* add changeset
2025-12-01 10:50:56 -08:00
Bee 0b0e8c36cb fix(ui): Add bottom padding for last message item [ENG-1354] (#7787)
* fix(ui): Add bottom padding for last message item

- Add conditional className to message wrapper div
- Apply `pb-2.5` bottom margin only when message is last in group

* add changeset
2025-12-01 10:29:54 -08:00
Juan Pablo Flores b0bd0e3974 Docs/task history recovery (#7776) 2025-12-01 09:24:40 -08:00
Ara 42b7a1e450 feat(cli): add active task check before entering follow mode (#7745)
* feat(cli): add active task check before entering follow mode

Add validation in FollowConversationUntilCompletion to check if a task
is currently running before entering follow mode. If no active task
exists, display a user-friendly message and exit gracefully instead of
waiting indefinitely.

Also includes minor whitespace formatting cleanup in related functions.

* Fix detached process conditions

* Fix detached process conditions

* Adding stealth
2025-12-01 04:57:22 -08:00
celestial-vault 14ccf33d25 fix: don't return empty array on parse failure (#7773) 2025-11-30 17:13:10 -08:00
Toshii d9a340523c add case for skipping autoCondense in truncation (#7763) 2025-11-30 16:12:27 -08:00
Saoud Rizwan 87b3e79b90 Instruct AI to prefer non-interactive commands (#7762)
Update system prompt to guide AI toward using non-interactive command variants
to avoid interrupting workflow. This includes using flags like --no-pager,
auto-confirming prompts with -y when safe, and providing input via
flags/arguments rather than stdin.
2025-11-30 00:16:18 -08:00
Saoud Rizwan 60f2e85fc7 Add find-pr-reviewers and address-pr-comments workflows (#7761)
* Add find-sme workflow for identifying subject matter experts

* Rename find-sme to find-reviewers

* Remove old find-sme.md file

* Address Copilot PR feedback: fix find syntax and add git config command

* Add address-pr-comments workflow

* Rename find-reviewers to find-pr-reviewers

* Simplify address-pr-comments workflow
2025-11-30 00:16:06 -08:00
Saoud Rizwan 1826d98019 Allow slash commands anywhere in message input (#7760)
* Allow slash commands anywhere in message input

Previously, slash commands could only be typed at the beginning of a
message. This change allows users to type slash commands anywhere in
the message, similar to how @ mentions work.

Changes:
- Update shouldShowSlashCommandsMenu() to show suggestions when slash
  is preceded by whitespace (not just at start)
- Update insertSlashCommand() to find the slash nearest to cursor
- Update extension-side parseSlashCommands() to find commands anywhere
  in tag content using a safer regex that avoids matching URLs/paths
- Update highlight layer to highlight slash commands anywhere
- Only the FIRST slash command per message is processed/highlighted
  to maintain consistency with backend behavior
- Fix backspace deletion to work for slash commands anywhere in text

* Add changeset
2025-11-30 00:07:56 -08:00
Saoud Rizwan af69b30a36 Add sticky user message header for better navigation (#7749)
* Add sticky user message header for better navigation

When users scroll down through a long conversation, a sticky header now appears showing their most recent message that has scrolled out of view. Clicking the header scrolls back to that message.

Key changes:
- New StickyUserMessage component that appears when user messages scroll past viewport
- Track scrolled-past user messages via scroll position detection in useScrollBehavior
- Add data-message-ts attributes to enable message element lookup
- Adjust TaskHeader padding for consistent alignment with sticky header
- Minor styling tweaks to UserMessage and FocusChain for visual consistency

* Fix type error: accept null for lastUserMessage prop

* Replace color-mix() with brightness filter for better compatibility

Use hover:brightness-110 instead of color-mix() for the sticky message
hover effect, as color-mix() may not be supported in all VS Code webview
contexts.

* Address Copilot review feedback for sticky user message

- Remove unused slide-down animation CSS
- Extract magic number 32 to STICKY_HEADER_HEIGHT constant
- Use cn() utility for conditional className in MessagesArea
- Add keyboard accessibility (role, tabIndex, onKeyDown) to StickyUserMessage

* Address additional Copilot review feedback

- Fix virtualized element detection: only consider missing elements as scrolled past
  if we've already found visible elements after them (fixes incorrect sticky header
  appearing when scrolling to top of long conversations)
- Rename truncatedText to messageText for accuracy (truncation happens via CSS)
2025-11-29 02:13:50 -08:00
DL Techy e62fbf6b0c Add shell option for cmd.exe to prevent double quote escaping (#7630)
* fix(terminal): Add shell option for cmd.exe to prevent double quote escaping

Added shell: true option specifically for cmd.exe to prevent double quotes
from being over escaped during command execution. This resolves Windows-specific
issues with terminal command handling while maintaining compatibility with
other shells.

* chore: Add changeset for terminal command execution fix
2025-11-28 13:02:47 -08:00
Saoud Rizwan 64254fc97a Fix API request badge causing text to wrap when hidden (#7739)
The cost badge was using opacity:0 to hide itself when there's no cost,
but still rendered "$0.0000" which took up horizontal space. This caused
the "API Request..." label to wrap to a second line unnecessarily.

Now the badge renders empty content when hidden, taking up no width
while still maintaining its height contribution to the row layout.
2025-11-28 08:05:09 -08:00
Luna c312c4aef6 Asksage usage fetch models (#7329)
* Add flagship models

* Add model fetching

* Add usage handling, tool result handling

* Update AskSageProvider.tsx

* Create eight-pants-explode.md

---------

Co-authored-by: alex-mcgraw-askSage <alex.mcgraw@asksage.ai>
2025-11-27 12:35:00 -06:00
celestial-vault fab49e810b Add fixed header to ClineRulesToggleModal (#7729)
- Add flex-shrink-0 to header section containing tabs and description text
- Keep tabs and description visible when content area scrolls
2025-11-27 12:32:17 -06:00
celestial-vault 2a20523e16 View remote rules and workflows in the editor (#7702)
* allow the user to view remote rules and workflows in the editor by creating a temp file

* add await
2025-11-27 11:37:47 -06:00
celestial-vault 06585821d1 conditionally fetch litellm models based on presence of api key and baseUrl (#7713) 2025-11-27 11:37:13 -06:00
Saoud Rizwan 9e802b11da Revert "Add Claude Code GitHub Workflow (#7717)"
This reverts commit afb77c5a8d.
2025-11-27 01:45:05 -08:00
Saoud Rizwan afb77c5a8d Add Claude Code GitHub Workflow (#7717)
* "Claude PR Assistant workflow"

* "Claude Code Review workflow"
2025-11-26 20:01:50 -08:00
Saoud Rizwan 0a4811222f fix: unblock opening a task when using cline account (#7715) 2025-11-26 18:40:51 -08:00
canvrno b4ce378e4b v3.38.3 Release Notes (#7711)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-11-26 15:59:16 -08:00
CandiedUniverse 2f60a898af fix(hooks): Fix issue identified by linter in proto file (#7707) 2025-11-26 14:50:12 -08:00
Walter Korman 8ffd82eda3 feat(context): add context window error detection for vercel ai gateway (#7623)
feat(context): add context window error detection for vercel ai gateway #7623
2025-11-26 22:58:57 +01:00
Andrei Eternal 81276fdf85 Add os/cline ver/host info to task metadata & change Task History -> EXPORT to just open the task directory (#7706)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-11-26 13:40:42 -08:00
celestial-vault 164e11aae1 Refresh models in the LiteLLM provider component when the base URL changes (#7705) 2025-11-26 13:03:16 -08:00
Enrico Carlesso 9792f174b1 Adding Grok 4.1 and Grok Code to Cline (#7632) 2025-11-26 12:56:25 -08:00
canvrno a590200c64 Remove native tool calling feature flag (#7704) 2025-11-26 12:41:20 -08:00
Bee 297a45d73a feat(providers): add thinking level config to Vertex and Anthropic model support (#7701)
* feat(providers): add thinking level config to Vertex and Anthropic model support

- Pass thinking level configuration (plan/act mode) to Vertex provider through VertexHandlerOptions interface
- Add support for @-versioned Anthropic model IDs (e.g., claude-haiku-4-5@20251001) in cache control logic

This enables mode-specific thinking level configuration for the Vertex provider by propagating geminiPlanModeThinkingLevel and geminiActModeThinkingLevel settings based on the current mode. Also extends Anthropic model compatibility with newer versioning format.

* reasoning

* sonnet

* yield signature delta
2025-11-26 12:14:19 -08:00
Seb Duerr e84de0ab3c feat: update Cerebras models and speed (#7631) 2025-11-26 11:08:24 -08:00
Ara 515cb81439 fix(terminal): simplify cmd.exe command arguments (#7695)
Remove /s flag and extra quoting from cmd.exe shell arguments.
The previous approach with /s /c and quoted command was causing
issues with proper command execution in Windows cmd.exe.
2025-11-26 11:05:10 -08:00
celestial-vault 550428eabd LiteLLM provider dynamic model fetching (#7679)
* add dynamic model fetching for litellm provider and get rid of manual model config; also implement dynamic modelinfo lookup

* don't clear the models list when a fetch fails
2025-11-26 10:54:51 -08:00
schardosin 22c22a1cfc Fixed SAP AI Core Deployments Mode (#7675)
* fixed sap ai core deployments not working

* isolated chunk to string in a function

* added changeset
2025-11-26 09:26:06 -06:00
Dominic Cooney 8202479cec fix: Add proxy rules, proxy support for McpHub & others (#7659) 2025-11-26 02:06:56 -08:00
Dominic Cooney bcbaa4518d docs: Document proxy settings. (#7637)
* docs: Document proxy settings.

* Update docs/troubleshooting/networking-and-proxies.mdx

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-11-26 01:30:28 -08:00
canvrno 9d799643ba npm audit fix docs + webview (#7687) 2025-11-26 00:49:40 -08:00
canvrno 6271c5da37 Remind models of new_task tool parameters when deep_planning is invoked (#7685)
* Remind models of new_task tool parameters when deep_planning is invoked

* cleanup
2025-11-25 21:59:53 -08:00
Auroter 89aeb3db3d feat(telemetry): Add OpenTelemetry metrics infrastructure (#7211)
* feat(telemetry): Add OpenTelemetry metrics infrastructure

Implement OpenTelemetry metrics support (counters, histograms, gauges) while maintaining backward compatibility with PostHog dashboards.

Changes:
- Updated ITelemetryProvider interface with recordCounter, recordHistogram, and recordGauge methods
- Implemented full OpenTelemetry metrics in OpenTelemetryTelemetryProvider with lazy instrument creation
- Added stub implementations in PostHogTelemetryProvider for backward compatibility
- Updated NoOpTelemetryProvider with metric method stubs
- Added comprehensive documentation in METRICS_IMPLEMENTATION_SUMMARY.md

Architecture:
- Dual instrumentation: existing PostHog events remain unchanged
- OpenTelemetry gets proper metrics for quantitative analysis
- Each provider handles metrics appropriately for its platform

Next steps:
- Add helper methods to TelemetryService for recording metrics with standard attributes
- Update high-priority capture methods (tokens, API performance) to call metric recording
- Validate with OpenTelemetry collector setup

* refactor(telemetry): add structured metrics and improve error handling

- Add userId and userEmail tracking to TelemetryService
- Implement helper methods (recordCounter, recordHistogram, recordGauge) with standardized attributes
- Add structured metrics for turns, tokens, costs, cache usage, and API performance
- Remove default case from TelemetryProviderFactory switch to enable exhaustive type checking
- Improve error handling by moving unsupported provider type logging outside switch
- Ensure all metric recordings include standard attributes (userId, email, metadata)

This refactoring enables better observability by recording key metrics (counters, histograms, gauges) across all telemetry providers while maintaining consistent attribute propagation and error isolation.

* fix: add logs back in to no-op provider

* fix: use Logger instead of console

* fix: remove unreachable code

* fix: satisfy compiler for config.type

* fix: remove metrics implementation summary

* chore: update telemetry to include mode in conversation turn events

- Added mode parameter to captureConversationTurnEvent in TelemetryService.
- Updated related telemetry metrics to include mode for better tracking.
- Adjusted tests to verify mode is correctly captured in telemetry events.

* fix: call signatures from merge detritus

* feat(telemetry): add optional description parameter to metric recording methods

Add optional `description` parameter to `recordCounter`, `recordHistogram`,
and `recordGauge` methods across the telemetry service layer. This enables
providers to include descriptive metadata when recording metrics.

Changes:
- Updated ITelemetryProvider interface methods to accept description parameter
- Modified TelemetryService private methods to pass description to providers
- Updated NoOpTelemetryProvider stub implementation
- Enhanced FakeProvider test implementation to capture descriptions
- Updated test assertions to verify description parameter handling

This change maintains backward compatibility as the description parameter
is optional.

* feat(telemetry): update recordGauge method to handle null values for metric retirement

- Modified the `recordGauge` method in `ITelemetryProvider` and its implementations to accept `null` as a valid value, allowing for the retirement of gauge series.
- Updated the `TelemetryService` to ensure proper cleanup of gauge entries when the series ends.
- Enhanced the `FakeProvider` test to validate the new behavior of gauge recording and retirement.
- Adjusted related tests to confirm that previous series are retired correctly when new values are recorded.

This change improves the management of gauge metrics, preventing stale entries and ensuring accurate telemetry data.

* refactor(telemetry): remove user email from telemetry service and related tests

- Removed the user email property from the TelemetryService and its associated methods, streamlining user attribute handling.
- Updated tests to reflect the removal of email, ensuring that metrics and events no longer rely on this attribute.
- Adjusted documentation in ITelemetryProvider to clarify the attributes used in metric recording.

This change enhances data privacy and simplifies the telemetry data model.

* feat(telemetry): enhance task metrics tracking with new counters and histograms

- Introduced new maps to track task turn counts, tool call counts, and error counts.
- Added methods to increment task counters and reset aggregates for better metric management.
- Updated existing telemetry capture methods to utilize the new counters and record histograms for task-related metrics.
- Enhanced tests to validate the new histogram entries for task turns, tool calls, and errors.

This change improves the granularity of telemetry data, allowing for more detailed analysis of task performance and error rates.

* refactor(telemetry): improve token usage handling in TelemetryService

- Updated conditions for recording cache write/read tokens and total cost to check for finite values, ensuring proper handling of undefined or null values.
- Introduced default values for token counts and total cost to prevent potential errors in metric recording.
- Enhanced readability by using descriptive variable names for token values.

This change enhances the robustness of telemetry data collection by ensuring that only valid numeric values are recorded.

* feat(telemetry): centralize metric definitions in TelemetryService

- Introduced a static METRICS object in TelemetryService to define all metric names, improving maintainability and readability.
- Updated existing telemetry recording methods to utilize the new METRICS constants, ensuring consistency across metric names.
- Enhanced tests to validate the use of METRICS constants in assertions for counters and histograms.

This change streamlines metric management and reduces the risk of errors due to hardcoded strings.

* refactor(telemetry): improve gauge observation handling in OpenTelemetryTelemetryProvider

- Replaced direct access to gauge values with a snapshot method to enhance data integrity during observable gauge callbacks.
- Introduced a new `snapshotGaugeSeries` method to encapsulate the logic for retrieving gauge data, improving code readability and maintainability.
- Updated the observable gauge callback to utilize the new snapshot method, ensuring that the latest values are accurately observed.

This change streamlines the process of observing gauge metrics, reducing potential errors and improving the overall telemetry data collection.

* refactor(telemetry): add required parameter to metric recording methods

- Updated `recordCounter`, `recordHistogram`, and `recordGauge` methods across the telemetry service and providers to include an optional `required` parameter, allowing for more flexible metric recording.
- Adjusted implementations in `NoOpTelemetryProvider`, `OpenTelemetryTelemetryProvider`, `PostHogTelemetryProvider`, and `FakeProvider` to handle the new parameter.
- Enhanced tests to validate the behavior of the `required` parameter in metric recording.

This change improves the control over metric recording conditions, enhancing the telemetry data collection process.

* fixed testing system

---------

Co-authored-by: NightTrek <Daniels@dual4t.com>
Co-authored-by: Daniel Steigman <35793213+NightTrek@users.noreply.github.com>
2025-11-25 18:56:30 -08:00
Saoud Rizwan 0caeea1b37 Enhance text overflow handling in TaskHeader component (#7674) 2025-11-25 14:23:39 -08:00
Saoud Rizwan 852a7c9198 Remove TaskTimeline from TaskHeader (#7670) 2025-11-25 13:09:15 -08:00
canvrno c4ef472aeb npm audit fix for glob package vulnerability (#7661) 2025-11-25 10:59:26 -08:00
Bee e22c457d19 fix: improve error property extraction from nested response objects (#7669)
- Remove intermediate response extraction to preserve full error structure
- Add fallback to error.response.message and error.response.status
- Stringify error object in logException for better console output
2025-11-25 10:58:55 -08:00
Juan Pablo Flores c15287ace0 Creates and Refactor Enterprise Docs (#7365)
* Refactor enterprise documentation: reorganize member management and roles, add AWS Bedrock configuration guides, and remove outdated security concerns section.

* Update docs/enterprise-solutions/provider-remote-config/aws-bedrock/admin-configuration.mdx

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: reneehuang1 <100229782+reneehuang1@users.noreply.github.com>
2025-11-24 16:49:36 -08:00
Bee d30f54a89c fix: add a refresh guard flag for auth (#7654)
* add a refresh guard flag (

* Implemented atomic refresh handling

Replaced the boolean flag with a Promise

- When a refresh is needed, the code first checks if _refreshPromise exists
- If it exists, concurrent calls wait for the same Promise to complete and then return the refreshed token
- If it doesn't exist, a new Promise is created and stored in _refreshPromise
- The Promise is cleared in the finally block after completion
2025-11-24 15:35:56 -08:00
Bee 56b913d951 fix: feature-flags cache persistence during auth transitions (#7652)
* fix: feature-flags cache persistence during auth transitions

Restructured feature flags polling and cache management to prevent empty cache states during authentication transitions:

- Move cache timestamp update to after successful population in poll() method to ensure cache validity reflects actual data availability
- Remove cache.clear() from reset() method to preserve existing flag values until new data is fetched
- Split polling logic in AuthService to explicitly handle authenticated vs unauthenticated states
- Poll feature flags immediately after reset for authenticated users to ensure cache is populated

This prevents temporary cache misses when users log in/out while maintaining cache freshness guarantees.

* remove reset method and usage in auth flow

Remove the FeatureFlagsService.reset() method and its call during user
authentication. The feature flags polling mechanism is sufficient to
keep flags up-to-date for authenticated users without requiring an
explicit cache reset on auth state changes.

Changes:
- Remove reset() method from FeatureFlagsService
- Remove featureFlagsService.reset() call from AuthService after user identification
- Rely solely on poll() to manage feature flags cache updates
2025-11-24 15:35:45 -08:00
Saoud Rizwan 55a30e0ffa Add support for opus 4.5 global endpoint in bedrock (#7653) 2025-11-24 14:49:23 -08:00
Saoud Rizwan a017f3dfd3 Add Claude Opus 4.5 (#7648) 2025-11-24 13:33:31 -08:00
Saoud Rizwan 41ebe7c9d1 Make npm installation less strict about package-lock needing to be in sync 2025-11-24 12:26:44 -08:00
Bee 4d11f0d2fa feat: implement edit tools conversion adapter [CLIENTS-23] (#7601)
* feat: implement edit tools conversion adapter

Add logic to transform `apply_patch` tool calls into specific `write_to_file` and `replace_in_file` operations. This adapter bridges the gap between patch-based model outputs and atomic file system tools.

- Implement `transformToolCallMessages` to parse patch content:
  - Converts "Add File" patches to `write_to_file`.
  - Converts "Update File" patches to `replace_in_file` with search/replace blocks.
- Add logic to reconstruct tool result messages to match the expected V4A patch format (including `<final_file_content>`).
- Add comprehensive unit tests in `src/core/api/adapters/__tests__/adapters.test.ts` covering add/update operations, multiple tool blocks, and result reconstruction.

* fix typos

* typo
2025-11-23 13:41:18 -05:00
abeatrix e0489bb6f4 dev: add stories for all UI components 2025-11-21 19:15:48 -08:00
Bee 4baa2474eb fix: ensure reasoning signature is accessible at top level (#7615)
* fix: ensure reasoning signature is accessible at top level

Extract signature from nested summary object and promote it to the top-level
reasoning structure when not already present. This ensures consistent access
to the signature field across all providers, regardless of where it's initially
provided in the reasoning details. The fomatter that each provider runs would then reconstruct the messages in the format they need.

* clean up
2025-11-21 16:36:39 -08:00
Juan Pablo Flores c94e2cf913 Upgrade/workflows docs (#7593)
* feat(workflows): restructure and enhance workflows documentation with best practices and quick start guide

* feat(workflows): enhance documentation with modular workflow practices and new PR review workflow example

* feat(workflows): improve clarity in workflow creation instructions

* Update docs/features/slash-commands/workflows/best-practices.mdx

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-21 15:45:37 -08:00
Bee dbedc6cfaa chore: clean up prompts and fix model family identifier (#7614)
- Simplified plan_mode_respond instruction text by removing redundant explanations and usage field
- Fixed incorrect MODEL_FAMILY references in native-gpt-5-1 config (was using NATIVE_GPT_5 instead of NATIVE_GPT_5_1)
- Added call_id to reasoning handler output for tracking and OpenAI Response API (unreleased)

The prompt simplification makes the response parameter instruction more concise while maintaining clarity. The model family correction ensures the GPT-5-1 variant uses the correct identifier throughout.
2025-11-21 14:25:18 -08:00
Sarah Fortune 2ac568e649 Add a setting to disable the Add Remote Servers feature in the extension. (#7612)
* Add a setting to disable the `Add Remote Servers` feature in the extension.

* Add setting to unit test

* Rename setting
2025-11-21 12:36:47 -08:00
tjandy98 b13d0e75ea Add support for Perplexity sonar and sonar-pro models to SAP AI Core Provider (#7605)
* Add perplexity models

* Add perplexity models to sap aicore

* Update api.ts

* Update sapaicore.ts

* add changeset

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

* update maxTokens and contextWindow

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

---------

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>
2025-11-21 09:42:39 -08:00
Bee 4d395deefd fix: improve error handling and ui for auth failures (#7591) 2025-11-21 08:56:28 -08:00
CandiedUniverse 834a5b1df2 fix(compaction): Use consistent icon for compaction (#7598) 2025-11-21 05:41:58 -08:00
CandiedUniverse 3089233298 feat(hooks): Implement Hooks tab in Rules & Workflows modal [ENG-1325] (#7547)
* feat(hooks): Add hooks tab to Rules & Workflows modal

* feat(hooks): Implement hooks tab content in Rules & Workflows modal

* feat(hooks): Enable creating new hooks in modal from dropdown selection list

* feat(hooks): Change hook template scripts to use bash

* feat(hooks): Windows not yet supported for hooks, so grey-out toggle on windows

* feat(hooks): Improvements to PR as per Cline reviewing the changes before code review

* feat(hooks): Implement tests for hook management (what the UI does under the hood)

* feat(hooks): Changes as per code review feedback from humans
2025-11-20 19:47:39 -08:00
Tomás Barreiro f2b7347a5c Add ApiKeys to the remote config (#7595) 2025-11-20 18:59:49 -08:00
Bee 04bfef75cf feat: replace robot icon with custom cline-bot icon font (#7594)
- Add cline-bot icon font assets (SVG, TTF, WOFF) generated from IcoMoon
- Register custom icon font in VS Code extension manifest
- Replace PNG-based command icon with font-based cline-icon
- Update terminal icon references from generic "robot" to "cline-icon"

This provides a consistent branded icon across the extension and improves visual identity by using the official Cline bot logo instead of the generic robot icon from codicon that was updated by VS Code.
2025-11-20 18:12:44 -08:00
Bee 716e8f236b feat(storybook): add OnboardingView story (#7578)
* chore: clear onboarding models on deactivate

* feat(storybook): add OnboardingView story

- Added OnboardingView component to Storybook with new story
- Integrated onboarding models from shared constants
- Updated MockApp to conditionally render OnboardingView based on onboardingModels state
- Renamed WelcomeScreen story to Welcome for clarity
- Added interaction tests for onboarding buttons (Get Started/Use your own API key)
- Configured onboarding models in mock state to support new story

This enables visual testing and documentation of the user onboarding flow within Storybook.

* Update Storybook missing vscode theme color

* Update task name

* typo
2025-11-20 17:21:28 -08:00
Alex Ker f2ddab71f1 updated baseten docs to include kimi instructions and updated location (#7588)
Co-authored-by: AlexKer <AlexKer@users.noreply.github.com>
2025-11-20 16:04:46 -08:00
canvrno 0b56a45a65 Add thinking level setting for Gemini 3.0 Pro (#7539)
* Added thinking level setting for Gemini 3.0 Pro

* changset
2025-11-20 10:48:31 -08:00
Bee d6ebd2438a fix: parse mentions/commands in tool results and before auto-condense (#7575)
* fix: parse mentions/commands in tool results and before auto-condense

**Changes:**
- Move `loadContext` call before auto-condense check to ensure slash commands and mentions are parsed before context condensing occurs
- Extract parsing logic into reusable `parseInputBlock` helper function
- Add recursive handling for `tool_result` blocks containing nested content arrays
- Remove duplicate `loadContext` calls from conditional branches

**Why:**
Previously, mentions (@file.ts) and slash commands in tool results (like attempt_completion feedback) were not expanded because parsing only handled top-level text blocks. Tool handlers return content arrays within tool_result blocks per Anthropic's API format.

Additionally, `loadContext` was called after the auto-condense check, meaning if condensing was triggered, user commands wouldn't be parsed and could be lost during summarization.

**Result:**
- All user feedback with @mentions or /commands is properly expanded regardless of nesting level
- Commands are detected before context management operations
- Cleaner code flow with single parsing point

* preserve array structure in backward-compatible tool results

When using the backward-compatible "cline" tool use ID, spread array
content directly into userMessageContent instead of wrapping it in
createToolResultBlock. This prevents array content from being
JSON.stringify'd and losing its block structure (e.g., tool_result
blocks with array content).

Previously, array content like [{type: "tool_result", content: [...]}]
was being converted to {type: "text", text: "[...]"}, which prevented
loadContext from properly parsing tool_result blocks.

* clean up

* fix(task): preserve block structure when processing string content

Instead of returning only the processed content, now properly updates the
block.content property with the processed text wrapped in an array and
returns the complete block object. This ensures the block structure is
maintained throughout the processing pipeline rather than being discarded.
2025-11-20 10:38:27 -08:00
Alex Ker 4c07c7e5c5 added Kimi K2 Thinking to static models list and set as default (#7511)
* kimik2 thinking added to static model dropdown

* numerical separators

* don't set kimi k2 thinking as default

---------

Co-authored-by: AlexKer <AlexKer@users.noreply.github.com>
2025-11-20 00:58:19 -08:00
Bee ab66a5fd93 fix: disable switch for required rules to prevent toggling (#7580)
Update RuleRow component to use isDisabled for both the switch disabled state and tooltip visibility, ensuring required rules cannot be toggled off by users. Previously used separate logic that may have miscalibrated disabling for required remote rules.
2025-11-19 23:25:30 -08:00
celestial-vault bd1d6159fc automatically derive openrouter modelinfo based on modelId when calling getModel() (#7568) 2025-11-19 21:16:48 -08:00
Bee 7f2d28716f chore: clear onboarding models on deactivate (#7569) 2025-11-19 18:01:58 -08:00
Toshii 653727db2a updating parser for webfetch to handle current format (#7571) 2025-11-19 17:44:01 -08:00
Bee d3c2f1878d fix(api): attach reasoning details to tool blocks (#7567)
* fix: Centralize reasoning details within thinking blocks

This commit refactors how reasoning details are managed across the system, integrating them directly into `ClineAssistantThinkingBlock` to improve consistency and reduce complexity.

Previously, `reasoning_details` were often explicitly deleted or inconsistently handled, leading to their loss or difficulty in tracking. This change ensures that reasoning details are always associated with their corresponding thinking blocks.

Key changes include:
- `StreamResponseHandler`: The `ReasoningHandler`'s `getCurrentReasoning` method now directly returns a `ClineAssistantThinkingBlock` which encapsulates the reasoning content and its `summary` (formerly `details`). The separate `getThinkingBlock` method has been removed.
- `convertToOpenAiMessages`: Explicit deletion of `part.reasoning_details` for `thinking` parts is replaced by setting it to `undefined` with a comment, indicating that these details are now expected to be part of the thinking block in the stream.
- `Task`: Simplified streaming logic by directly consuming the `ClineAssistantThinkingBlock` from `reasonsHandler.getCurrentReasoning()`. Redundant temporary variables for reasoning content and details have been removed.

This refactoring centralizes the management of reasoning details, providing a more robust and streamlined approach to handling assistant thinking processes.

* fix(api): attach reasoning details to tool blocks and improve logging

Updates validity of reasoning details within tool blocks and enhances debugging visibility.

- Modify `StreamResponseHandler` to append reasoning details/summary to finalized tool use blocks.
- Update `convertToOpenAiMessages` to extract and aggregate `reasoning_details` from tool messages instead of discarding them.
- Add `Logger.debug` calls in `ClineHandler` and OpenAI transformation for better observability of message chunks and conversion.
- Remove redundant `continue` statements in `ClineHandler` stream processing loop.

* clean up

* remove Logger
2025-11-19 17:28:20 -08:00
CellenLee ba92be9401 feat: add kimi-k2-thinking and kimi-k2-thinking-turbo (#7386) 2025-11-19 15:49:46 -08:00
Bee 66eb5a62ba feat: Enable native tool calling for Baseten and Kimi K2 models (#7562)
* feat: Enable native tool calling for Baseten and Kimi K2 models

Introduces native tool calling capabilities for Baseten and Kimi K2 models, aligning with the OpenAI Chat Completions API specification for function calling.

This change includes:
- Updating the `ApiHandler` interface and `createMessage` methods to accept an optional `tools` parameter.
- Implementing a `ToolCallProcessor` to incrementally build and emit tool call payloads from streaming deltas.
- Modifying the `BasetenHandler` to pass `tools` to the Baseten API and process `tool_calls` deltas.
- Updating the `ClineHandler` to process `tool_calls` deltas received from the Kimi K2 model.
- Enhancing the `CompletionStreamChunk` to include `ToolCall` and `ToolCallDelta` types.
- Marking Baseten and Kimi K2 models in their respective definitions with `native_tool_calling: true`.
- Adjustments to streaming logic in handlers to allow multiple delta types (content, tool_calls, reasoning) to be processed from a single chunk.

* add changeset

* clean up
2025-11-19 15:37:10 -08:00
Bee 499ee22b3b fix(task): update UI with final usage after stream completion (#7552)
Ensure the UI displays accurate token usage and costs by updating the API request message when the stream completes. This commit adds a call to updateApiReqMsg followed by saving messages and posting state to the webview, which occurs before finalizing tool calls. This ensures users see the final usage statistics (input/output tokens, cache tokens, and total cost) reflected in the interface immediately after stream processing.
2025-11-19 02:54:31 -08:00
CandiedUniverse 7abeae5019 feat(hooks): Implement TaskComplete hook (#7510) 2025-11-18 23:43:57 -08:00
Bee accf47cb52 fix: await presentAssistantMessage calls to prevent race condition (#7548)
* fix: await presentAssistantMessage calls to prevent race condition

Add await to all presentAssistantMessage() calls to ensure proper
sequencing of message presentation. Previously, the method was called
without awaiting, which could cause race conditions when streaming
tool use content blocks. This ensures that message presentation
completes before continuing execution, particularly important when
handling multiple content blocks or tool interactions.

* revert pr change
2025-11-18 17:48:15 -08:00
Bee 556d3e6f79 fix: rules modal positioning (#7546)
Add overflow-y-auto to the modal container to allow scrolling when content exceeds viewport height. This fixes an issue where modal content would be inaccessible on smaller screens or with large rule sets.
2025-11-18 16:23:48 -08:00
Bee 9e35048db2 feat: add support for Responses API for openai-native provider [ENG-1227] [ENG-1311] (#7504)
* feat: add support for Responses API for openai-native provider [ENG-1227]

- Upgrade openai dependency to v6.9.0 to use the Responses API
- Implement internal handling for reasoning/thinking and redacted output
- Align Anthropic handler message types with the latest SDK interfaces
- Clean up obsolete tooling imports related to tool-use handling
- Enable newer OpenAI capabilities while keeping provider APIs consistent

* Add openai_native_response_api feature flag

* clean up

* clean up 2

* add back gpt-5.1 models

* use call_id
2025-11-18 16:20:45 -08:00
Ara 9b0f2b82ef v3.38.1 Release Notes (#7544) 2025-11-18 14:32:41 -08:00
Bee c2c23054b9 fix: Remove 'signature' from sanitizeAnthropicContentBlock (#7543)
* fix: Remove 'signature' from sanitizeAnthropicContentBlock

Remove 'signature' from sanitizeAnthropicContentBlock as the signature field is required by Anthropic when thinking is enabled.

* Add Changeset

* empty commit

---------

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2025-11-18 14:10:34 -08:00
Bee 3baaa5c8b4 refactor: replace custom UI toggle with shadcn Switch component (#7308)
* refactor(webview-ui): replace custom UI toggle with shadcn Switch component

- Add @radix-ui/react-switch dependency (v1.2.6) https://ui.shadcn.com/docs/components/switch
- Refactor ClineRulesToggleModal to use Radix Switch instead of VSCode buttons
- Improve button styling with reduced padding and adjusted icon sizes
- Enhance form layout with conditional rendering based on expansion state
- Update input field styling with better focus states and border handling

This change provides a more consistent UI experience by leveraging Radix UI's
accessible Switch component while maintaining the same functionality.

* clean up

* clean up

* update switch color

* adjust

* revert unrelated changes

* size

* toggle

* Update webview-ui/src/components/cline-rules/RuleRow.tsx

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-11-18 12:39:50 -08:00
github-actions[bot] abafcc7290 Changeset version bump (#7473)
* v3.38.0 Release Notes

- Gemini 3 Pro Preview model
- AquaVoice Avalon model for voice-to-text dictation

- Automatic context truncation when AWS Bedrock token usage rate limits are exceeded
- SAP AI SDK JS packages upgraded to latest major version
- SAP provider OrchestrationClient now matches OrchestrationModuleConfig type and no longer uses invalid promptTemplating property
- Removed new_task tool from system prompts, updated slash command prompts, and added helper function for native tool calling validation

* Update CHANGELOG.md

---------

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2025-11-18 12:15:10 -08:00
Bee d82aa0add9 feat: add Gemini 3.0 Pro to onboarding list (#7542)
- Add `google/gemini-3-pro-preview` to `CLINE_ONBOARDING_MODELS`.
- Configure model details including context window, pricing, and capabilities (images, prompt cache).
- Enable users to select the new Gemini 3.0 Pro model during setup.
2025-11-18 11:49:08 -08:00
Bee abe4721a0b feat: add thought signature support for Gemini SDK [ENG-1320] (#7536)
* feat: add thought signature support for Gemini SDK

Update @google/genai dependency from v1.15.0 to v1.30.0, including nested deps like google-auth-library. Enhance API interfaces with JSDoc comments and new fields such as signature, id, and redacted_data in ApiStreamThinkingChunk to support thought signatures from Gemini SDK as requested. This improves integration with Gemini's reasoning capabilities and ensures compatibility with updated SDK features.

* add changeset

* meaning val check

* typo

* either

* Do not use think budget with gemini-3
2025-11-18 10:57:53 -08:00
Bee 60d55b69a8 fix: Only update reasoning UI when content changes (#7540)
This commit addresses two issues related to how reasoning messages are processed and displayed.

Previously, the `say` function was called on every iteration of the reasoning stream loop, even if the current chunk contained no new reasoning content. This caused unnecessary UI updates and could lead to errors if a task was cancelled mid-stream. The `say` call is now conditional, only executing when new `chunk.reasoning` is available.

Additionally, the final reasoning block was only appended to the assistant's message history if a signature was present. This meant reasoning could be lost from the UI if the task was cancelled before a signature was generated. The logic is now updated to append the reasoning block if either a message or a signature exists.
2025-11-18 10:57:11 -08:00
Ara d18e0271d3 Fix cancellation for background terminal commands (#7521)
* refactor(task): improve background command cancellation with better error handling

Enhance the cancelBackgroundCommand method with:
- Consolidated early return conditions for cleaner code
- Proper async/await for process termination
- Comprehensive error handling with try-catch blocks for each operation
- Improved logging for termination success/failure scenarios
- Updated cancellation notification message
- Use finally block to ensure notification is always sent

Improve StandaloneTerminalProcess.terminate() with:
- Better guard clauses and early returns
- Enhanced error handling for SIGTERM and SIGKILL operations
- More detailed logging for graceful vs forced termination
- Fallback to SIGKILL if SIGTERM fails immediately

Fix critical issue where terminate() method was not accessible on the merged promise object returned by executeCommand, preventing Task.cancelBackgroundCommand() from properly killing background processes.

* Fix: add cancel ui

* Fix: add cancel ui

* Fix: add cancel ui

* Fix: add cancel ui
2025-11-18 10:41:58 -08:00
Ara af71f9da90 fix: resolve double quote escaping in Windows cmd.exe for Background Exec mode (#7523)
Fixes #7470

When Terminal Execution Mode is set to "Background Exec", commands with
double quotes were being incorrectly escaped on Windows cmd.exe, causing
commands like `echo "\""` or `type "test.txt"` to fail.

The issue was that cmd.exe requires the /s flag and outer quotes when
passing commands with special characters via spawn(). Changed from
`["/c", command]` to `["/s", "/c", `"${command}"`]` for cmd.exe only.

This is a minimal Windows-specific fix that:
- Only affects Windows cmd.exe (PowerShell and Unix shells unchanged)
- Uses standard Windows cmd.exe syntax for proper quote handling
- No changes to process execution flow or behavior
2025-11-18 10:17:37 -08:00
canvrno 2a1c8826aa Add Gemini 3.0 to featuredModels (#7537) 2025-11-18 10:13:06 -08:00
Ara 9a54f2d246 fix(auth): enable provider persistence when applying model changes (#7530)
- Change `UpdateProviderPartial` persist flag from false to true in `applyModelChange`
- Add missing newline at end of state.proto file

This ensures that model changes are properly persisted to storage when users
update their provider configuration through the wizard.
2025-11-18 10:05:01 -08:00
canvrno d928d58a40 Feat: Gemini 3.0 prompt/tool changes (#7532)
* Enhanced Gemini 3.0 support in Cline

* Updated Gemini 3.0 snapshots

* Update src/utils/model-utils.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* Updated system prompt

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

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* Pricing change, narrowed native tool spec to just gemini 3 on vertex

* Update src/core/prompts/system-prompt/registry/ClineToolSet.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

---------

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
2025-11-18 10:02:43 -08:00
canvrno 31859b5fda Add Gemini 3.0 to Gemini provider (#7533)
* Added Gemini 3.0 to Gemnini provider

* Add thinking option for Gemini 3.0
2025-11-18 09:20:19 -08:00
Ara 0d5d89e8c7 feat(bedrock): add context window error detection and retry handling (#7515)
* feat(bedrock): add context window error detection and retry handling

Add proper context window error detection for AWS Bedrock provider to enable automatic retry with context truncation. Previously, context window errors were yielded as error text instead of being thrown, preventing the retry mechanism from handling them.

Changes:
- Detect ValidationException errors matching context window patterns in both Converse API and stream processing
- Throw context window errors instead of yielding them as text to trigger retry logic
- Add checkIsBedrockContextWindowError() function to identify Bedrock-specific context limit errors
- Support multiple error message patterns (input too long, context exceed, maximum tokens, etc.)
- Handle nested error structures from Vercel AI SDK and AWS SDK

This enables automatic context management when Bedrock models hit token limits, improving reliability and user experience.

* Fix: raise errors
2025-11-18 09:09:47 -08:00
Bee af34451eec fix: remove h-full from TaskTimeline (#7525) 2025-11-18 02:10:35 -08:00
Bee 027a4f6386 fix: remove automatic native tool calls inference (#7522)
Remove automatic enablement of native tool calls for next-gen models and providers. The feature should be controlled exclusively by explicit user settings (feature flag and global state) rather than being automatically inferred based on the model type during experimental state.

Changes:
- Removed `isNextGenModelProvider` import (no longer needed)
- Eliminated `inferredNativeToolCalls` logic that auto-enabled the feature for next-gen models
- Simplified `enableNativeToolCalls` to only check explicit feature flag and global state settings
- Makes behavior more predictable and user-controlled
2025-11-18 00:49:36 -08:00
Bee 49642882c5 fix: ensure tool arguments are streamed during native tool calling [ENG-1305] (#7508)
* fix: ensure tool arguments are streamed during file operations

- Update userMessageContentReady condition to include streaming tool arguments, not just new content blocks
- Add null check for input object in tool-use-handler to prevent errors
- Improve partial JSON parsing with better fallback handling
- Replace console.log with Logger.debug for tool call chunks
- Add clarifying comments for lock mechanism and streaming behavior

This fixes an issue where new file content was not being properly streamed to tools during write operations, causing the UI to stop updating while tool arguments were being received.

* Add changeset

* typo

* fix(task): reset content index to execute tool blocks during streaming

Reset the currentStreamingContentIndex to the first tool block position
when tool blocks are present in the assistant message. This ensures that
tool blocks are properly executed instead of being skipped when the index
advances past them or goes out of bounds during content streaming.

Previously, the index could advance beyond tool blocks, causing them to
remain unexecuted. Now, when tool blocks are detected, the index is
explicitly set to textBlocks.length (the start of tool blocks) and
userMessageContentReady is set to false to trigger execution.

* fix(task): reset stream index to enable tool block execution

Reset currentStreamingContentIndex to the first tool block position when
tool blocks are present in the assistant message. This ensures that
presentAssistantMessage processes tool blocks instead of text blocks
during streaming, allowing tool blocks to be executed properly while
streaming is in progress.

The index is set to textBlocks.length, which points to where tool blocks
start in the content array, enabling correct sequential processing of
tools during the streaming phase.

* fix(streaming): improve tool execution flow and prevent control flow fall-through

- Add continue statements after yielding content in cline provider to prevent unintended fall-through behavior
- Mark all streamed tool uses as partial to ensure proper state tracking
- Allow complete tool blocks to bypass presentation lock for immediate execution during streaming
- Simplify userMessageContentReady reset logic and remove redundant tool_call check

These changes improve tool execution responsiveness by allowing completed tools to execute without waiting for the presentation lock, while ensuring proper control flow and state management throughout the streaming process.

* revert WriteToFileToolHandler
2025-11-17 23:50:04 -08:00
Bee 21ed6bc432 fix: do not add MCP tool with invalid names as native tools (#7516)
* fix: do not add MCP tool with invalid names as native tools

- Filter out MCP tools with names >= 64 characters to avoid provider API rejection
- Reduce nanoid length from default (21) to 5 characters for server UIDs

Provider APIs reject tool registration when tool names exceed 64 characters.
This change prevents registration errors by skipping tools with long names
and generating shorter UIDs to minimize the constructed name length
(uid + identifier + tool name).

* Add Changeset

* Update src/services/mcp/McpHub.ts

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

* Update src/core/prompts/system-prompt/registry/ClineToolSet.ts

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-17 23:24:17 -08:00
Bee da2689f885 fix: correct TaskTimeline height (#7520)
* fix: correct TaskTimeline height

- Remove TIMELINE_HEIGHT constant in favor of h-full and h-4 utilities
- Replace inline styles with Tailwind classes for better maintainability
- Change timeline blocks from rounded-xs to rounded-full for consistency
- Fix timeline display being cut off due to incorrect height constraints

This refactor resolves the visual layout issue where timeline items were
truncated while improving code consistency by leveraging Tailwind's
utility-first approach throughout the component.

* add changeset
2025-11-17 23:05:09 -08:00
CandiedUniverse 5049326f02 fix(hooks): Fix two minor cancel-resume issues (#7502)
* fix(hooks): Fix cancel: true returned by TaskResume

* fix(hooks): Prevent TaskCancel from being triggered twice by TaskStart cancel and by TaskResume cancel scenarios
2025-11-17 14:44:16 -08:00
Ara b02ce46a57 Fix: Vercel provider token usage (#7481) 2025-11-17 14:15:32 -08:00
Saoud Rizwan de974737c8 fix: improve layout and styling in OnboardingView component for small width viewport (#7391) 2025-11-17 13:35:13 -08:00
Bee d072156e9a fix(account): memoize credits history table component (#7439)
Use React.memo to wrap CreditsHistoryTable, reducing unnecessary re-renders
when props are unchanged and improving performance of the account view that makes it looks like it glinches.
2025-11-17 11:36:19 -08:00
celestial-vault 4939309a09 fix openrouter defaulting modelId when modelInfo is not present (#7482) 2025-11-15 13:50:29 -08:00
CandiedUniverse 1a07ca7906 fix(hooks): Honor '"cancel": true' in hook JSON output (#7479) 2025-11-14 20:37:16 -08:00
Bee c1eefbad3f refactor(api): unify provider message type with ClineStorageMessage (#7478)
* refactor: replace Anthropic MessageParam with ClineStorageMessage type

Replace Anthropic SDK's MessageParam type with the new ClineStorageMessage type across API providers and tests in the effort of storing api messages in a type safe environment that we can expand from and avoid adding undocumented properties to Anthropc Message type that are not visible to the downstream services.

This change:

- Removes dependency on @anthropic-ai/sdk types in multiple providers
- Introduces ClineStorageMessage from shared messages module
- Updates method signatures in Dify, OpenAI, LiteLLM, and ClaudeCode handlers
- Updates corresponding test files to use the new type

This decouples the codebase from Anthropic-specific types and standardizes message handling using an internal storage format across all providers that  improves type-safety, preparing for the properties added by the Response API use.

As ClineStorageMessage is an extension of the Anthropic Message type, everything should work the same with no breaking changes. Green CI is expected.

* clean up
2025-11-14 20:24:56 -08:00
canvrno 1bfdce9b84 Remove new_task from system prompts (#7350)
* Removed new_task from system prompts, updated slash command prompt, added helper function for native tool calling checks

* Update src/core/prompts/system-prompt/registry/PromptBuilder.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* Update src/core/task/index.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* Updates with requested changes for PR #7350

* Updated package-lock.json

---------

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
2025-11-14 17:57:47 -08:00
canvrno b002cdacdb Maint: package updates (#7477)
* maint: package updates

* Updated download-ripgrep script for compatability with new tar dependency
2025-11-14 16:09:03 -08:00
CandiedUniverse cf4005b25e fix(hooks): Reorder the UI elements so that PreToolUse appears above tool (#7449)
* fix(hooks): Reorder the UI elements so that PreToolUse appears above tool

* fix(hooks): Prevent PreToolUse hook from migrating down the screen

* fix(hooks): PreToolUse reordering should apply to 'tool', 'command', 'use_mcp_server', and 'browser_action_launch'  message types
2025-11-14 15:40:23 -08:00
Bee 1494d145d5 feat: support feature flag payload & remote dynamic onboarding model list (#7454)
* feat: support feature flag payload & dynamic onboarding model list

- Updated proto to use OnboardingModelGroup instead of bool flag for flexible onboarding
- Added getClineOnboardingModels function with caching and remote overrides for dynamic model fetching
- Modified controller to fetch and pass onboarding models to webview
- Updated UI to use dynamic models for selection, enabling flexible onboarding
- Enhanced feature flag service to support non-boolean payloads for better configurability

* clearOnboardingModelsCache
2025-11-14 14:49:05 -08:00
canvrno 1ab4b3cc24 fix:SAP provider type error - See PR #6547 (#7475) 2025-11-14 14:15:13 -08:00
canvrno 535b653228 Added stronger prompting around the use of act_mode_respond (#7448) 2025-11-14 12:12:34 -08:00
Igor Tceglevskii 1335fa5452 Retire firebase (#7362) 2025-11-14 09:29:47 -08:00
yuvalman b2a4395f71 feat: upgrade sap ai-sdk-js packages major version (#6547)
* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version

* feat: upgrade sap-ai-sdk major version
2025-11-14 09:11:03 -08:00
Toshii ae34a3a8c5 adding state variable for clineWebToolsEnabled (noop) (#7455)
* adding state variable for clineWebToolsEnabled

* removing console log
2025-11-14 08:20:01 -08:00
github-actions[bot] 0fb4a6c7e9 v3.37.1 Release Notes (#7451)
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for version 3.37.1

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-11-13 17:42:42 -08:00
Saoud Rizwan 855db7d8d8 feat(models): Add free minimax/mimax-m2 model to the model picker (#7453)
* feat(models): Add free minimax/mimax-m2 model to the model picker

* Add free minimax/mimax-m2 model to model picker
2025-11-13 17:41:16 -08:00
CandiedUniverse bb375b78ca fix(hooks): Prevent PreToolUse hook from running before attempt_completion tool (#7450) 2025-11-13 16:14:13 -08:00
CandiedUniverse 31af254f0a fix(hooks): Run PreToolUse only after approval (#7446)
* fix(hooks): Run PreToolUse only after approval; get it working for read_file first

* fix(hooks): Run PreToolUse only after approval; get it working for six more tools now

* fix(hooks): Run PreToolUse only after approval; get it working for the remaining six tools now

* fix(hooks): Implement HookExecution type to avoid using 'any'
2025-11-13 15:41:57 -08:00
github-actions[bot] 2745cdd54b Changeset version bump (#7358)
* v3.37.0 Release Notes

- GPT-5.1 with model-specific prompting: tailored system prompts, tool usage, focus chain, and deep-planning optimizations
- Nous Research provider with Hermes 4 model family and custom system prompts
- Model-family breakouts for deep-planning prompting, laying groundwork for enhanced slash commands
- Expanded HTTP proxy support throughout the codebase
- Improved focus chain prompting for frontier models (Anthropic, OpenAI, Gemini, xAI)

- Duplicate tool results prevention through existence checking
- XML entity escaping in model content processor
- Commit message generation in command palette
- OpenAI Compatible provider temperature parameter type conversion

- Added missing proto generation step in CONTRIBUTING.md
- New `npm run dev` script for streamlined terminal workflow (fixes #7335)

* Adding image optimizations

* Adding image optimizations

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2025-11-13 11:47:42 -08:00
celestial-vault c94c6fd8f1 Fix CLI auth state persistence with explicit flush mechanism (#7445)
Fixes issue where 'cline auth' command would lose all configuration due to process termination race condition.

Root cause: StateManager uses 500ms debounced persistence, but CLI process terminated before setTimeout callback could fire.

Solution: Implemented explicit flushPendingState() mechanism:
- Added StateManager.flushPendingState() method for immediate persistence
- Refactored to extract shared persistence logic (DRY)
- Added flushPendingState gRPC endpoint
- CLI now calls flush instead of using 5s sleep workaround

Results:
- Deterministic persistence (no race condition)
- Faster auth flow (~2s vs 7s)
- Cleaner, more maintainable code
2025-11-13 11:34:17 -08:00
Ara a87664318e Adding Gpt-5.1 to model picker and onboarding list (#7444)
* feat(settings): add GPT-5.1 to Model picker featured models

* adding

* adding

* adding
2025-11-13 11:20:08 -08:00
Ara ba70718c83 Remove Qwen3 coder from Cerebras providers (#7404)
* Remove Qwen3 coder from Cerebras providers

* Update Cerebras Docs to include new model `zai-glm-4.6` and remove `qwen-3-coder-480b-free` and `qwen-3-coder-480b` entries.

---------

Co-authored-by: Juan Pablo <juan@cline.bot>
2025-11-13 11:13:25 -08:00
canvrno cf8dd1c150 Feat:Enhanced support for OpenAI GPT 5.1 (#7443)
* Comprehensive changes to better support GPT 5.1 - System prompt, tools, deep-planning, focus chain, etc.

* Fix typos

* Fix more typos
2025-11-13 11:06:23 -08:00
Bee e714c74ec4 fix(anthropic): sanitize messages to SDK MessageParam shape (#7441)
- Use Anthropic SDK types and return Array<Messages.MessageParam>
- Replace removeReasoningDetails with removeUnknownParams
- Strip unsupported fields (e.g., reasoning_details) and normalize role
- Keep string content unchanged; map array content to known blocks
- Continue adding ephemeral cache_control to targeted user messages

This ensures only Anthropic-supported fields are sent, improving type safety and preventing API errors.
2025-11-13 10:28:40 -08:00
Sarah Fortune a6f8b33895 Change the label in the webview to say Enterprise Rules/Workflows instead of Remote Rules/Workflows (#7440) 2025-11-13 10:15:27 -08:00
Saoud Rizwan 02abbcf045 feat: add AGENTS.md support (#7437)
* feat: add AGENTS.md support

* Update webview-ui/src/components/cline-rules/RuleRow.tsx

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

* Update webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx

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

* Update font size for documentation link in ClineRulesToggleModal component

* docs: add support for AGENTS.md standard in Cline rules documentation

* fix: delete agents.md

* Add AGENTS.md support

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-13 09:42:24 -08:00
Bee 8ca2706cca chore: cleanup leftover code from anthropic provider (#7434)
Follow up from https://github.com/cline/cline/pull/7399

- Only add the redacted reasoning block when a reasoningSignature is present,
  instead of checking message length. This aligns with Claude extended thinking
  requirements and prevents unsignatured reasoning content from being sent.
- Remove leftover thinkingDeltaAccumulator from the Anthropic provider.
2025-11-12 18:33:13 -08:00
Bee e747d211e6 refactor: standardize reasoning yield types across providers [ENG-1226] (#7399)
* refactor(api): standardize reasoning yield types across providers

- Unify reasoning output format in Anthropic, Cline, and Minimax handlers
- Change "ant_thinking" and "reasoning_details" to "reasoning" type
- Add signature and redacted_data properties for consistency
- Wrap message_start cases in braces for scoping
- Consolidate yields to reduce redundancy and improve maintainability

* VercelAIGatewayHandler

* feat: add model information tracking to tasks and messages

Add modelId field to TaskResponse and TaskItem proto messages, and introduce ClineModelInfo message type to track provider and model IDs throughout the system. Update API transform functions to use ClineStorageMessage types and refactor message handling to support model information tracking.

This enables better tracking and auditing of which AI models are used for specific tasks and messages, improving observability and allowing for model-specific analytics.

* clean up protos

* remove console log

* minimax

* use new interface

* clean up
2025-11-12 15:33:48 -08:00
Bee 19aa81a1cc fix(dev): add back CLINE_ENVIRONMENT_OVERRIDE support (#7427)
CLINE_ENVIRONMENT_OVERRIDE was removed in https://github.com/cline/cline/pull/6621 accidentally. This PR adds it back:
Prioritize CLINE_ENVIRONMENT_OVERRIDE over CLINE_ENVIRONMENT when
setting the runtime environment at module load.
This enables temporary/test-specific environment selection without
modifying the base env var. Existing behavior is unchanged when the
override is not set. Updated inline comment for clarity.
2025-11-12 14:58:53 -08:00
Bee f5f3654966 refactor: Extract model info into method in WriteToFileToolHandler (#7420)
* refactor: Extract provider and model info into method in WriteToFileToolHandler

- Introduce private getModelInfo method to centralize extraction of providerId and modelId
- Replace duplicated telemetry extraction logic across multiple function calls
- Update telemetry service invocations to use the new method's return values for consistency and reduced code duplication/script>

* Update src/core/task/tools/handlers/WriteToFileToolHandler.ts

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-11-12 14:40:51 -08:00
Bee 344b99988b feat: add native tool calls tracking to task events (#7429)
Add `isNativeToolCall` parameter to telemetry service to track when
assistant turns and provider API errors occur with native tool calls
enabled. This enables better analytics and debugging of native tool
call behavior across conversation turns.
2025-11-12 13:03:41 -08:00
Bee 7d3c39bcdb feat: make baseUrl dynamic by removing static field (#7428)
Remove the static _baseUrl field and replace all references with direct calls to ClineEnv.config().apiBaseUrl. This ensures the base URL updates correctly when configuration changes, fixing an issue where the URL remained static even after configuration updates.
2025-11-12 12:38:13 -08:00
celestial-vault 33c1692f8a add remotely configured rules and workflows (#7411)
* add remotely configured rules and workflows

* fix toggles state not being passes to webview on updates; minor format updates

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2025-11-12 10:33:19 -08:00
Bee 3be3ffae4c fix: prevent duplicate tool results by adding existence check (#7419)
* fix: prevent duplicate tool results by adding existence check

Add a check in ToolResultUtils to skip adding a tool result if one already exists for the same tool_use_id, logging a warning to avoid redundant entries in userMessageContent and maintain message integrity. Introduces Logger import for warning output.

* add changeset
2025-11-11 21:37:11 -08:00
murkvin 87c6708fbe fix: restore commit msg generation functionality to command palette (#7417)
Co-authored-by: Kevin Murphy <murkvin@amazon.com>
Co-authored-by: CandiedUniverse <132302818+candieduniverse@users.noreply.github.com>
2025-11-11 17:36:30 -08:00
Juan Pablo Flores b296706bc0 docs: add guidance for handling strings with quotes in JSON payloads for hooks (#7396)
* docs: add guidance for handling strings with quotes in JSON payloads in hooks documentation

- Introduced a new section on using jq's --arg flag for proper escaping of unescaped quote characters in JSON output.
- Provided a code example demonstrating how to handle complex strings or nested JSON structures safely.

This update enhances the documentation by clarifying how to manage JSON payloads effectively within hooks.

* docs: clarify context modification behavior in hooks documentation

- Updated the explanation regarding context modifications and their effect on AI decisions.
- Emphasized the need to return `cancel: true` in PreToolUse hooks to block actions effectively.

This change enhances the clarity of the documentation, ensuring users understand how to implement immediate effects in their hooks.

---------

Co-authored-by: CandiedUniverse <132302818+candieduniverse@users.noreply.github.com>
2025-11-11 15:24:52 -08:00
canvrno 7fdc82ae23 Renamed Nous proto config strings (#7416)
* Renamed Nous proto config strings

* Revert "feat: migrate terminal execution mode default to background" (#7412)

This reverts commit 0b7393f50e.

---------

Co-authored-by: Ara <arafat.da.khan@gmail.com>
2025-11-11 14:42:13 -08:00
Bee 246ed87350 fix: ensure temperature parameter is explicitly converted to number (#7397)
* fix(openai): ensure temperature parameter is explicitly converted to number

Changed temperature assignment to explicitly check for undefined and convert
the value to Number type before use. This prevents potential type issues when
the temperature configuration is provided as a string or other non-number type,
ensuring consistent numeric handling for the OpenAI API.

* set 0 tempature to undefined

* add changeset
2025-11-11 14:10:21 -08:00
Ara a28465151f Revert "feat: migrate terminal execution mode default to background" (#7412)
This reverts commit 0b7393f50e.
2025-11-11 12:53:18 -08:00
Saoud Rizwan f1967bc3a1 feat: Add OAuth 2.1 authentication support for remote MCP servers (#7376) 2025-11-11 12:35:11 -08:00
Bee ccc83d6728 feat: add feature flag to toggle between new and old onboarding view [ENG-1224] (#7410)
* feat: add feature flag to toggle between new and old onboarding view

Add a new feature flag `show_onboarding_flow` that controls whether users see the new onboarding experience or the legacy welcome view. This enables A/B testing and gradual rollout of the new onboarding flow, and gives me better control when model list has changed and the onboarding model list is outdated.

Changes:
- Add ONBOARDING feature flag (enabled by default)
- Add showOnboardingFlow to ExtensionState and proto definitions
- Add getOnboardingEnabled() method to FeatureFlagsService
- Update App component to conditionally render OnboardingView or WelcomeView based on flag
- Extend ExtensionStateContext with showOnboardingFlow state management

This allows for controlled rollout and easy rollback if issues arise with the new onboarding experience.

* default true

* default to false unless e2e test

* showOnboardingFlow default to off
2025-11-11 11:33:42 -08:00
Szymon Stasik c151d719c8 fix: Handle tool_use blocks in Claude Code backward compatibility path (#7394)
- Transform tool_use content blocks to tool_calls format
- Map Anthropic tool_use structure to ApiStreamToolCallsChunk
- Fixes 'tool_use is not supported yet' error
- Resolves 'Current ask promise was ignored' downstream errors
2025-11-11 11:13:22 -08:00
canvrno cc25833963 Add Nous Research provider (#7141)
* Added Nous Research provider

* Fix casing on import
2025-11-11 10:43:12 -08:00
canvrno 2ef3b7cf0f Nous Hermes-4 family system prompt (#7142)
* Hermes-4 model family system prompt

* Update src/core/prompts/system-prompt/variants/hermes/overrides.ts

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-11-11 10:14:24 -08:00
Mike Mikula 19d7bd5198 fix: Fix XML entity escaping in model content processor (#7385)
* refactor(task): replace HTML escaping logic with model-specific content fixes

Updated ExecuteCommandToolHandler and WriteToFileToolHandler to utilize a new utility function, applyModelContentFixes, for handling model-specific quirks. This change centralizes the logic for fixing escaped characters and invalid characters, improving maintainability and consistency across command execution and file writing processes. Additionally, introduced ModelContentProcessor to encapsulate the new content processing logic.

* test(ModelContentProcessor): add comprehensive unit tests for applyModelContentFixes

Introduced a new test suite for the ModelContentProcessor, covering various scenarios for the applyModelContentFixes function. Tests include model ID detection, file type handling, orchestration logic, and integration with real-world use cases, ensuring robust validation of content processing for different model types and file formats.

* fix: add changeset for XML escaping bug fix

* refactor(ModelContentProcessor): make modelId parameter optional in applyModelContentFixes

Updated the applyModelContentFixes function to accept an optional modelId parameter, allowing for fixes to be applied even when the model ID is not provided. Adjusted related logic in ExecuteCommandToolHandler and added a test case to verify this new behavior.
2025-11-11 07:26:27 -08:00
Ara 121066f938 feat: migrate terminal execution mode default to background (#7368)
- Add one-time migration to set all users to backgroundExec mode
- Change default terminal execution mode from vscodeTerminal to backgroundExec
- Update UI labels to clarify background mode is recommended
- Add migration sentinel flag to prevent re-running

This migration improves the default user experience by using background
terminal execution, which provides a cleaner UI and better performance
compared to the interactive VSCode terminal mode. Existing users will be
automatically migrated once, while new users will get the improved default.
2025-11-10 14:06:32 -08:00
Juan Pablo Flores f9b88318ea docs(dictation): add notice on Windows support (#7059)
* docs(dictation): remove Windows support from system requirements

- Add note indicating dictation is currently unavailable on Windows
- Remove Windows FFmpeg installation instructions from the list
- Clarify that Windows support is planned for a future release

This update reflects the current state of the feature and sets proper user expectations about platform availability.

* Update docs/features/dictation.mdx

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-10 13:26:10 -08:00
Ara 1d64f64f43 Enable voice mode for linux also (#7369) 2025-11-10 12:19:41 -08:00
canvrno 766e2e6a25 Model-family specific variants for deep-planning slash command (#7337) 2025-11-10 11:23:40 -08:00
Ara 8bd7260350 Fix Standalone Terminal Background Transition Issues (#7390) 2025-11-10 11:11:16 -08:00
canvrno a1f4e8b9d4 Adjusted focus chain prompting - more guidance for models using native tool calling, consolidated prompting for other models (#7340) 2025-11-10 11:05:51 -08:00
Johnny Rice aa3e0860cd docs: add missing proto generation step in CONTRIBUTING.md (#7336)
- Add step 4 in Local Development Instructions to run npm run protos
- Add proto generation step in Extension section
- Create new npm run dev script that runs protos + watch
- Update Terminal Workflow documentation to mention npm run dev

Fixes #7335
2025-11-10 10:20:44 -08:00
Dominic Cooney 67bc9932e5 Add proxy configuration to more network fetches. (#7328) 2025-11-10 09:54:54 -08:00
Ara ba98b44504 feat(task): cancel active background command during task abort (#7363)
Add cleanup logic to cancel any active background command when aborting a task. This ensures background processes are properly terminated before the task abort completes, preventing orphaned background commands.
2025-11-07 19:13:31 -08:00
celestial-vault fdeb639f75 make field mask type a string array so that the kotlin/go protobuf libraries don't convert it to snake case (#7364) 2025-11-07 18:52:38 -08:00
Ara 76410b42b0 Removing deprecated Fireworks Models (#7268) 2025-11-07 14:34:29 -08:00
Tomás Barreiro e4e347a8a4 Add Remote Server MCPs to the remote config (#7357) 2025-11-07 23:33:07 +01:00
canvrno 59e7c2b7b9 Fix CLI quick auth issue in docker envs (#7256) 2025-11-07 14:26:39 -08:00
Ara 5371377b21 Removing Minimax M2 from free models list (#7359) 2025-11-07 14:13:19 -08:00
canvrno 5a444dc30a Hide context window usage from env details until usage reaches elevated state (ng models only) (#7345) 2025-11-07 13:46:36 -08:00
Bee 34c48264c0 fix(models): ensure thinking config has default maxBudget value (#7319)
* fix(models): ensure thinking config has default maxBudget value

- Add ANTHROPIC_MAX_THINKING_BUDGET import and use as fallback
- Check for both "include_reasoning" and "reasoning" parameters
- Set default maxBudget when thinking is supported but value not provided
- Ensures thinking models always have valid budget configuration

This prevents issues when OpenRouter API doesn't return thinking_config.maxBudget, ensuring all thinking-enabled models have a proper budget value set as it's used to determine if a model support thinking in some part of our code.

* update thinkingConfig placeholder
2025-11-07 12:52:14 -08:00
Tomás Barreiro daf14ef181 Add LiteLLM to the remote config (#7307)
* Add Google Vertex and LiteLLM to the remote config

* Add the LiteLLM models and export types
2025-11-07 20:24:27 +01:00
Bee f1bf9b3f90 feat: new onboarding flow [ENG-1128] (#7088)
* feat(auth): new onboarding UI

- Add optional `strict` parameter to `createAuthRequest()` to prevent opening new auth windows when already authenticated
- Update onboarding flow with new UI text and button labels ("Login to Cline", "I have my own key", "Ready")
- Add new onboarding data models and step configuration for improved user experience
- Update all e2e tests to reflect new button labels and authentication flow
- Remove obsolete `closeBanners` utility function
- Refactor authentication logic to support strict mode for better control over auth window behavior

This change improves the authentication UX by preventing duplicate auth windows and provides a more streamlined onboarding experience with clearer call-to-action buttons.

* Debug buttons

* Update model list

* Add search box

* improve model selection with names and improved search

- Add name field to ModelInfo interface for better model identification
- Populate model name from OpenRouter API response
- Improve model search filtering to exclude embedding models
- Add case-insensitive search for better UX
- Enhance UI with badges for model capabilities and pricing
- Update styling for better visual hierarchy and selected state
- Display model names in search results for clarity

This improves the onboarding experience by making model selection more informative and user-friendly with better search capabilities and visual feedback.

* clean up

* async onboarding

* Update langauge

* spacing

* search result info text

* update e2e test

* update badge

* style(onboarding): adjust max-width constraint placement

Move max-width constraint from parent container to content wrapper div to improve responsive layout behavior. This ensures the full width is utilized at the top level while constraining only the scrollable content area.

Changes:
- Added w-full to root container for proper width handling
- Removed max-w-lg from middle container
- Applied max-w-lg to content wrapper instead

* Update search box placeholder text

* remove unused description field

* clean up

* Fix search box reset state

* feat: add loading screen to onboarding flow

- Add loading state (step 2) during authentication process
- Simplify welcomeViewCompleted logic by removing auth service check
- Move welcomeViewCompleted state update to auth success handler
- Display loading spinner during sign-in to improve UX
- Ensure auth status update occurs in finally block for reliability

The loading screen provides visual feedback while authentication completes, preventing users from seeing incomplete UI states during the sign-in process.

* update AuthServiceMock

* capture onboarding events

* smaller badge radius

* radius-xs = 4px & add speed label

* update to MiniMax M2
2025-11-07 10:30:50 -08:00
Bee 6e20047c3e refactor(ui): simplify flexible SettingsView (#7352)
* refactor(ui): simplify SettingsView tab system

- Remove ResizeObserver-based compact mode detection and related state
- Replace dynamic CSS class constants with inline Tailwind classes using cn utility
- Remove debounce dependency and handleTabChange callback
- Simplify tab rendering logic by removing conditional compact/full mode rendering
- Clean up unused imports (debounce, useRef) and state management
- Streamline component architecture for better maintainability
- simplify ModelDescriptionMarkdown

This refactoring reduces complexity while maintaining the same visual functionality, making the code easier to understand and maintain.

* clean up
2025-11-07 10:01:14 -08:00
celestial-vault 1d4fe88bec simplify updateApiConfiguration calls (#7299)
* simplify calls to updateApiConfiguration by moving the logic around which mode to update to the RPC; change proto field name; use new calls in MoonshotProvider, and update calls in LiteLlmProvider

* remove console logs
2025-11-07 08:12:20 -08:00
Sarah Fortune 74dbbb9d6b Use staging endpoint for MCP marketplace (#7348) 2025-11-07 00:03:02 -08:00
github-actions[bot] 3073cf40be v3.36.1 Release Notes (#7317)
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog format for version 3.36.1

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-11-06 18:54:30 -08:00
canvrno 50056768d6 Add MCP tool usage to GLM prompt (#7347)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-11-06 18:03:23 -08:00
Bee e7cd32b830 fix: function declarations tool converter for native tool calling (#7342)
* fix:  function declarations tool converter for native tool calling

- Replace crypto-based ID generation with nanoid in McpHub that is causing invalid function name issue with Gemini models
- Refactor Google tool parameter schema mapping with typed constant
- Improve parameter properties handling in toolSpecFunctionDeclarations
  - Add support for nested properties and enum values
  - Skip $schema property during parameter processing

This change fix type safety for Google Gemini function calling parameter definitions.

* disable native tool calling by default

* Always starts with a letter

* whoops

* reuse id

* fix run time package issue
2025-11-06 17:06:17 -08:00
Bee d983b451e1 fix: Remove Gemini and XAI from next-gen model providers (#7341)
Removes "gemini" and "xai" from the list of providers considered "next-gen" in `isNextGenModelProvider`.
This change prevents Cline from attempting to use native tool callings with these providers until existing issues are resolved.
2025-11-06 14:18:55 -08:00
Bee bb32aac8e7 fix: exclude grok-code from Grok-4 model family check (#7338)
Remove "grok-code" from isGrok4ModelFamily function to exclude it from the next-gen model list, disabling native tool calling for grok-code as it does not work well with the model.
2025-11-06 12:59:58 -08:00
chenxue e6d5f55fb2 fix bug (#7324) 2025-11-06 11:42:14 -08:00
Tomás Barreiro 93f60ebe5b Share provider information (#7331)
* Share provider information

* Move files

* Move files
2025-11-06 20:40:32 +01:00
Bee a3e000d5dd fix: refactor APPLY_PATCH tool [ENG-1174] (#7189)
* feat: refactor APPLY_PATCH tool

Replace separate file creation and editing tools with unified APPLY_PATCH tool for the native-gpt-5 model variant. This consolidation simplifies file operations through a single patch-based interface.

Changes:
- Replace FILE_NEW and FILE_EDIT tools with APPLY_PATCH in config for gpt-5 with native tool calling
- Disable EDITING_FILES system prompt section (no longer needed)
- Remove EDITING_FILES section from base template
- Refactor ApplyPatchHandler with improved architecture:
  - Extract patch parsing logic into PatchParser utility class
  - Extract file operations into FileProviderOperations utility
  - Extract path resolution into PathResolver utility
  - Add comprehensive error handling with DiffError types
  - Improve type safety with shared Patch types
- Add extensive test coverage for PatchParser including:
  - Edge cases (empty files, large files, unicode)
  - Error conditions (malformed patches, invalid operations)
  - Complex scenarios (multiple chunks, context matching)
- Export PATCH_MARKERS and BASH_WRAPPERS for reusability

This refactoring improves maintainability, testability, and provides a more robust patch application system for the GPT-5 model variant when native tool calling is enabled.

* Add diagnostic to result

* Update unit tests

* add feedback

* typo

* captureToolUsage

* fix test

* fix test

* Update ClineMessage

* update diff editor on stream

* revert content partial stream

* Disable Apply patch and add native gpt snap shot

* Remove delete action and make preserveEscaping conditional

* Add delete function to diff provider

* Update UI for file deletion
2025-11-05 20:10:26 -08:00
Bee 0076be531d fix: removes reasoning_details field from Anthropic and Gemini providers (#7282)
* fix: sanitize Anthropic message and removes reasoning_details field

- Extract duplicate Anthropic message formatting code into sanitizeAnthropicMessages function
- Remove code duplication between AnthropicHandler and VertexHandler
- Add support for claude-sonnet-4-5-20250929:1m model
- Remove unused MessageParam import from anthropic provider

* add changeset

* Create thinking content block for gemini
2025-11-05 20:10:09 -08:00
github-actions[bot] 9de4fc2a2f v3.36.0 Release Notes (#7271)
- Add: Hooks allow you to inject custom logic into Cline's workflow
- Add: new provider AIhubmix
- Add: Use http_proxy, https_proxy and no_proxy in JetBrains
- Fix: Oca Token Refresh logic
- Fix: issues where assistant message with empty content is added to conversation history
- Fix: bug where the checkbox shows in the model selector dropdown
- Fix: Switch from defaultUserAgentProvider to customUserAgent for Bedrock
- Fix: support for `<think>` tags for better compatibility with open-source models, and refinements to the GLM-4.6 system prompt

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-05 18:10:09 -08:00
CandiedUniverse 8a4b87a133 feat(hooks): Improve examples in .clinerules/hooks/ directory (#7316) 2025-11-05 16:52:26 -08:00
Juan Pablo Flores 571480e3b6 Hooks docs (#7173)
* docs(features): add comprehensive hooks documentation

Add detailed documentation for the Hooks feature, including:
- Overview of hooks functionality and use cases
- Getting started guide with step-by-step setup instructions
- Hook types and lifecycle explanations
- JSON communication format and examples
- Validation and blocking capabilities
- Best practices and implementation patterns

Update docs navigation to include the new hooks documentation page
in the features section.

* docs(features): reorganize and clarify Hooks documentation

- add frontmatter description and insert demo GIF in Getting Started
- reorganize Hook Types into Task Lifecycle and Communication categories
- convert tables into dedicated sections per hook with explicit "Input Fields" examples
- add JSON input/output examples and clarify execution timing, limits, and context behavior
- remove embedded bash examples and Advanced Topics clutter; streamline troubleshooting and security warning

* Update docs/features/hooks.mdx

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

* docs: reorganize hooks documentation into four categories

Restructure the hooks documentation to improve clarity and organization:

- Change from two categories to four logical categories: Tool Execution,
  User Interaction, Task Lifecycle, and API Response
- Reorder hooks by category based on trigger points and use cases
- Add complete JSON examples with all base fields for each hook type
- Enhance descriptions with clearer use case explanations
- Improve consistency across hook documentation format

This reorganization makes it easier for developers to find the right hook
for their specific needs and understand the complete data structure they
will receive.

* docs: enhance TaskStart hook example with JSON payload handling

Replace basic "Hello World" example with practical demonstration of reading JSON input from stdin and using jq to inspect payload structure and field types. Added explanatory text to help developers understand hook input/output mechanics before building complex logic.

* Update docs/features/hooks.mdx

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

* comments out preCompact and taskComplete

* fix: clarify description of hook types in documentation

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: CandiedUniverse <132302818+candieduniverse@users.noreply.github.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-11-05 16:50:22 -08:00
Chris Sells 5c2c264a1d initial CLI samples (#7040)
* initial CLI samples

* Add GitHub integration and root cause analysis samples to CLI documentation

* docs(cline-cli): restructure samples into nested documentation group

Convert the single samples page into a grouped navigation structure with three separate sample pages:
- Overview (main samples page)
- GitHub Issue RCA
- GitHub Integration

Add redirect from the old `/cline-cli/samples` path to the new `/cline-cli/samples/overview` location to maintain backward compatibility with existing links.

* docs: improve GitHub issue RCA sample docs with better structure and links

Reorganize documentation with source code links, move demo video to prominent position,
simplify explanations, and add tutorial references for better discoverability.

* docs: improve github-issue-rca documentation and add download instructions

- Remove redundant "Demo Video" heading from mdx documentation
- Add "Getting the Script" section to README with two options:
  - Option 1: Clone/view repository with direct link to script
  - Option 2: Direct download using curl with example commands

These changes make it easier for users to quickly access and download
the analyze-issue.sh script without needing to navigate the repository
structure, improving the overall user experience.

* docs: update GitHub Root Cause Analysis sample links and enhance README with installation instructions

* docs(samples): improve GitHub integration setup guide and add prerequisites

Enhanced the GitHub integration sample documentation with:
- Added prerequisites section listing required knowledge and resources
- Improved setup instructions with direct curl commands for downloading files
- Added prominent warning callout for editing GITORG and GITREPO variables
- Included beginner-friendly note recommending simpler sample first
- Updated documentation link to point directly to GitHub repository
- Provided two options for adding analysis script (download or copy)
- Enhanced formatting and clarity throughout the guide

These changes make the sample more accessible to new users and reduce
setup friction by providing copy-paste commands and clearer guidance.

* docs(samples): consolidate CLI samples documentation and enhance GitHub integration README with improved image formatting

* Apply suggestion from @Copilot

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

* Apply suggestion from @Copilot

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

* removes bookmarks and creates padding for images

* Update image source in README for CLI sample

* Add GitHub CLI samples and update docs navigation

- Add GitHub Issue RCA sample (docs/cline-cli/samples/github-issue-rca.mdx)
- Add GitHub Actions integration sample (docs/cline-cli/samples/github-integration.mdx)
- Add samples overview page (docs/cline-cli/samples/overview.mdx)
- Update docs.json to include new CLI samples navigation entries

* docs: simplify "Getting the Script" section in GitHub Issue RCA sample

Reorder options so curl download is Option 1 and the full script view is Option 2.
Remove the clone/view-repo entry, move the chmod +x instruction into a Note
after the script accordion, and clarify the Basic Usage intro text.

* docs: tidy GitHub Integration sample — remove redundant workflow placement note, simplify script copy options, fix GitHub Action capitalization and related-samples link

* docs: remove GitHub Integration and GitHub Issue RCA sample READMEs

---------

Co-authored-by: Juan Pablo <juan@cline.bot>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-05 16:49:57 -08:00
CandiedUniverse 04549cf1e3 feat(hooks): Trim characters around JSON in hook output (#7314) 2025-11-05 15:59:56 -08:00
Bee f2a93b6201 fix(account): correct tooltip visibility logic for remote config lock (#7313)
Invert the boolean condition for TooltipContent's hidden prop to properly
show the tooltip when organization has remote configuration enabled.
Previously, the tooltip was hidden when it should be visible and vice versa.
2025-11-05 15:57:47 -08:00
CandiedUniverse 2f09b994dd feat(hooks): Fix contextModification injection into API Request (#7311) 2025-11-05 15:28:34 -08:00
Bee 8aac67bf75 feat: enable native tool call by default (#7304)
Change the default value of `nativeToolCallEnabled` from `false` to `true` in the global state initialization. This enables native tool calling functionality by default when no previous state exists, improving the out-of-box experience for users.
2025-11-05 15:23:02 -08:00
CandiedUniverse 10422dd3fc feat(hooks): Remove hooks feature flag (step 1), keeping hooks setting toggle (#7309) 2025-11-05 14:51:11 -08:00
CandiedUniverse 6191c681ce feat(hooks): Fix the missing Start New Task button after task completion (#7303) 2025-11-05 14:49:42 -08:00
CandiedUniverse 9ab7b41c24 feat(hooks): Remove debug logging to the console (#7306) 2025-11-05 14:10:19 -08:00
Saoud Rizwan bddfb93b38 fix: order of AIHubMix in API providers (#7302) 2025-11-05 12:51:18 -08:00
Bee a219eb8000 fix(settings): remove highlight function from provider search (#7300)
Remove redundant highlight import and simplify provider search results
mapping in ApiOptions component. Search results now directly map to
items without applying highlighting, reducing unnecessary dependencies
and simplifying the code.
2025-11-05 12:44:34 -08:00
Saoud Rizwan 515b4e5d6a Fix vercel gateway thinking and model list (#7196) 2025-11-05 12:43:44 -08:00
Sarah Fortune aaa3846b82 Add GCP Vertex provider to remote config schema (#7200)
* Add global rules and workflows to the remote config schema

Add a way for the admin to configure global cline rules and workflows for their users.

* Add GCP Vertex provider to remote config

Add config settings for the GCP Vertex provider
2025-11-05 12:28:08 -08:00
canvrno 8229dcfe88 Fix openai compatible auth issue in CLI (#7291) 2025-11-05 11:34:31 -08:00
Dominic Cooney b4ea1a17a5 WIP proxy patch (#7263)
Better documentation in shared/net

Better docs, wire up when running standalone.

Use esbuild to statically decide which proxying we are doing.

Add the changeset.

Add the ability to mock this fetch.
2025-11-05 11:26:12 -08:00
Nick Baumann e263c74325 Add model/provider attribution to telemetry events (#7230)
- Add provider parameter to diff edit failure events
- Add provider parameter to context summarization events
- Add mode parameter to conversation turn events
- Add model/provider parameters to focus chain completion events

Addresses review feedback by removing checkpoint telemetry changes.
Follows pattern from PR #7214 for consistent telemetry attribution.
2025-11-05 11:15:01 -08:00
nihar-oracle 08ca115c66 fix: Fixing token refresh logic for oca on startup (#7209)
* fix: Fixing token refresh logic for oca on startup

* fix: Fixing token refresh logic for oca on startup
2025-11-05 09:14:42 -08:00
Bee 68ffe20b7d feat(telemetry): add native tool call tracking (#7279) 2025-11-05 06:57:38 -08:00
TechBrewBoss 4517408537 Fixes Hicap model selector dropdown display (#7285)
* Fixes Hicap model selector dropdown display

Fixes a bug where the checkbox was showing in the model selector dropdown.

The z-index was being improperly applied to the VSCodeTextField, causing the dropdown to be obscured. This change moves the z-index styling to be applied directly to the style attribute of the VSCodeTextField component, resolving the display issue.

* Update .changeset/proud-humans-itch.md

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-04 23:56:12 -08:00
AJ Juaire 42c947afd4 Fix Bedrock user agent to report full SDK details (#7273)
* switch from defaultUserAgentProvider to customUserAgent for Bedrock

* update to userAgentAppId
2025-11-04 18:47:08 -08:00
Bee d4736afce2 fix(navbar): align navbar tooltip buttons with new shadcn component (#7283)
Switch Navbar actions to the shared Button component and tooptip to use the new shadcn tooltip component so the navbar render correctly and spacing matches the previous design.
2025-11-04 18:22:35 -08:00
Bee b00053ff16 fix(settings): restore Native Tool Call setting option (#7284)
Re-add the Native Tool Call checkbox setting to the FeatureSettingsSection
that was accidentally removed in PR #7126 due to merge conflicts.

The setting allows users to enable/disable native tool calling through the
API when the feature flag is enabled. The UI includes an experimental
warning and descriptive text explaining the functionality.

Changes:
- Add nativeToolCallSetting to useExtensionState destructuring
- Add conditional checkbox UI for Native Tool Call feature
- Include experimental warning and feature description
2025-11-04 18:17:49 -08:00
Bee 2ab8fcb54f fix(task): prevent empty assistant messages in conversation history (#7280)
* fix(task): prevent empty assistant messages in conversation history

Add conditional check to only append assistant content to API conversation
history when the content array is not empty. This prevents unnecessary empty
assistant messages from cluttering the conversation history and potentially
causing issues with the API.

Changes:
- Wrap addToApiConversationHistory call in length check
- Add explanatory comment for the conditional logic

* add changeset
2025-11-04 16:15:37 -08:00
celestial-vault e697c12e94 Remote config - add info banner for mcp marketplace lockdown state (#7278)
* filter mcp marketplace results based on remote config

* add info ui banner for mcp marketplace remote config state

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2025-11-04 13:57:59 -08:00
celestial-vault 64dcc3fc89 filter mcp marketplace results based on remote config (#7277) 2025-11-04 13:50:50 -08:00
Bee 8215d3fdb9 fix: flaky e2e test for slash commands (#7206)
* fix: flaky e2e test for slash commands

* use click

* @problems

* newtask click

* exact: false

* exact: false

* revert unrelated change
2025-11-04 12:43:29 -08:00
canvrno 59260cefab Small timeout increaase in diff.test (#7270) 2025-11-04 11:04:07 -08:00
chenxue a767f844b9 feat: add new provider AIhubmix (#7259)
* feat: add AIhubmix provider integration

- Add AIhubmix as a new provider with full API integration
- Implement AIhubmixHandler for API interactions
- Add model fetching functionality via getAihubmixModels
- Create UI components for AIhubmix configuration
- Update proto definitions and API configuration
- Add changeset for version tracking

* docs: add AIhubmix integration documentation

* fix: add plan/act mode fields for AIhubmix and Hicap providers

- Add planModeAihubmixModelId and planModeAihubmixModelInfo
- Add actModeAihubmixModelId and actModeAihubmixModelInfo
- Add planModeHicapModelId and planModeHicapModelInfo
- Add actModeHicapModelId and actModeHicapModelInfo
- Ensures model selection works correctly in both plan and act modes

* merge

* Delete AIHUBMIX_INTEGRATION.md

* fix: bot

* Update providerUtils.ts
2025-11-04 10:51:45 -08:00
canvrno bb7c3556d6 Use context-aware task manager for quick auth (#7255) 2025-11-03 21:34:52 -08:00
github-actions[bot] 8b80c337ae v3.35.1 Release Notes (#7207)
- Add: Hicap API integration as provider
- Fix: enable Add Header button in OpenAICompatibleProvider UI
- Fix: Remove orphaned tool_results after truncation and empty content field issues in native tool call
- Fix: render model description in markdown

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-11-03 20:08:34 -08:00
celestial-vault d0cac53d5b Add a field mask to the new updateApiConfiguration RPC (#7197)
* add a field mask to updateApiConfiguration and update LiteLLM provider component calls to use it

* remove console log
2025-11-03 19:43:45 -08:00
celestial-vault 8aedc90214 add configured providers to remote config to limit options (#7250) 2025-11-03 19:39:52 -08:00
celestial-vault 0c118086df fix: add explicit wait for slash commands menu in e2e test (#7063)
This fixes a flaky e2e test by adding an explicit wait for the slash
commands menu to be visible before attempting to interact with it.

Changes:
- Add data-testid to SlashCommandMenu component for test targeting
- Add waitFor() call in test to ensure menu is visible before Tab press
2025-11-03 18:10:42 -08:00
Bee 261a7dd505 fix: enable Add Header button in OpenAICompatibleProvider (#7246)
* fix: enable Add Header button in OpenAICompatibleProvider

Implement functionality for the Add Header button that was previously disabled. The button now:
- Enables when headers are not managed by remote configuration
- Creates new header entries with auto-incremented keys (header1, header2, etc.)
- Updates the API configuration state with the new header

This allows users to add custom headers to their OpenAI-compatible provider configuration unless restricted by organization settings.

* add changeset
2025-11-03 15:01:14 -08:00
canvrno 9abf2f567a Fix empty content field issue (#7247) 2025-11-03 14:57:49 -08:00
Marco Alejandro Chavez Santos 42de7c81b4 Hicap integration as new provider (#6988)
* Feat:
* add hicap as provider option
* add new variable to handle hicapApiKey
* add variable to handle hicapModelId in Plan and Act mode
* get hicap available model from Hicap Endpoint
* create hicap provider (ui)
* hicap handler

* rebase main into this branch and fixing errors

* add changeset

* fix typo with hicap api key

* resolve comments from cline team on PR

* revert some delete console logs

* HicapModelPicker changed styled components for tailwind format, remove unnecessary hicapModelId migration, refreshHicapModel use setGlobalState function

* rebase main branch
2025-11-03 14:57:23 -08:00
Saoud Rizwan 06b710f62b fix: context mentions are positioned incorrectly (#7248) 2025-11-03 14:43:52 -08:00
Ara ea9b8fe0b1 Enable Terminal Timeouts by defualt (#7171)
* Remove redundant test code that's very confusing

* feat: add vscodeTerminalExecutionMode to TaskConfig and apply timeout

Add vscodeTerminalExecutionMode parameter to TaskConfig and thread it through ToolExecutor and Task classes. Apply default 30-second timeout to commands executed in backgroundExec mode, similar to existing yolo mode behavior.

This change enables different execution strategies for VSCode terminal commands and ensures background executions have appropriate timeout protection to prevent hanging processes.
2025-11-03 11:33:10 -08:00
canvrno 05213f2a71 fix: Remove orphaned tool_results after truncation (#7225) 2025-11-03 10:25:16 -08:00
Ara d5bad1357d fix: Support interleaved thinking for miniMax provider (#7162)
* fix: interleaved thinking

* Adding native tool calling
2025-11-03 09:31:08 -08:00
Saoud Rizwan a4b1549dac Revise title in README.md
Updated the title of the README file and removed the subtitle.
2025-11-02 23:29:49 -08:00
Andrei Eternal e015ce94c0 cli polish ahead of release - disable doctor, better node errors (#7215)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-11-02 14:52:54 -08:00
Wahaj Ahmed Khan be076cf407 feat: Add support for Claude 4.5 Sonnet in SAP AI Core provider (#7217)
- Add anthropic--claude-4.5-sonnet model definition to sapAiCoreModels
- Update SAP AI Core provider to handle Claude 4.5 Sonnet in anthropicModels array
- Enable caching support for Claude 4.5 Sonnet using converse-stream endpoint
- Add Claude 4.5 Sonnet to streamCompletionSonnet37 method for proper response handling

Fixes #7216

Co-authored-by: Wahaj Ahmed Khan <“wahaj.khan@sap.com”>
2025-11-01 20:01:04 -07:00
Nick Baumann 580db36476 Add provider field to tool usage telemetry events (#7214)
- Add provider parameter to captureToolUsage() and captureDiffEditFailure() methods in TelemetryService
- Update all 11 tool handlers to extract and pass provider information
- Extract provider using plan/act mode differentiation from state manager
- Update UIHelpers.ts captureTelemetry wrapper to include provider
- Update test file to include provider parameter in test calls

This enables tracking which API provider (anthropic, openai, etc.) was used for each tool execution.
2025-10-31 18:31:45 -07:00
Sarah Fortune 8f8b98bb58 Add global rules and workflows to the remote config schema (#7198)
Add a way for the admin to configure global cline rules and workflows for their users.
2025-10-31 10:24:40 -07:00
CandiedUniverse 36022438cb 🪝Hooks: Exclude .clinerules/hooks/ files from Rules feature (#7202)
* Exclude .clinerules/hooks/ files from Rules feature

* Escape whitespace in paths correctly when discovering hooks/ directories
2025-10-31 10:18:51 -07:00
Bee c7afb61e28 fix: react-remark rendering in ModelDescriptionMarkdown (#7205)
* fix: react-remark rendering in ModelDescriptionMarkdown

- Add useRemark hook to properly parse and render markdown content that was removed in last git commit
- Extract props interface to ModelDescriptionMarkdownProps for better type safety
- Add useEffect to reactively update markdown when content changes
- Set fixed height (h-20) for collapsed state to improve layout consistency
- Replace raw markdown text display with processed reactContent

This change ensures markdown formatting (links, bold, italics, etc.) is correctly rendered in model descriptions instead of showing raw markdown syntax.

* changeset
2025-10-31 10:03:27 -07:00
CandiedUniverse dfd113a6e5 🪝Hooks: Gorgeous UI [ENG-994] (#7126)
* feat(hooks): Initial implementation of hooks UI using background terminal UI

* Hooks UI improvements

* Hook discovery improvements

* Separate hooks UI into separate files

* Fix failing tests

* Add missing docs

* Hooks hardening and handling edge cases

* Improvements to hooks error messaging

* Improve TaskCancel hook UI

* Simplify and improve console.log() output from hooks

* Changes from usability feedback

* test: verify linting fixes

* fix: resolve cancel functionality in PostToolUse hook using flag pattern

* fix: add cancel button support for TaskResume and UserPromptSubmit hooks

* Getting things working again

* Fixing cancel behaviors

* Trying another fix

* Demoed

* Add examples to .clinerules/hooks/ directory

* Refactored away loadTaskStateWithoutWorkflow

* Fix unsafe return in finally block

Refactored PostToolUse hook execution to avoid using return statement
in finally block, which the linter flags as unsafe. Instead of early
return, wrapped the hook logic in a conditional check for abort status.

This resolves the biome lint error:
lint/correctness/noUnsafeFinally - Unsafe usage of 'return' in finally block

* Remove unnecessary files from branch

* remove cancelHookExecution() in favor of cancelTask()

* Remove unnecessary polling pattern for hook cancellation

* Fixing cancel behavior

* Fixing cancel behavior

* Fix resume

* Sqaush commits to improve merge tool behavior

Plan out the work needed to resolve race conditions

Fix race conditions (1)

Fix race conditions (2)

Fix race conditions (3)

Fix race conditions (4)

Fix race conditions (5)

Fix unit tests

Updated implementation plan doc

Code quality improvement

Temporary logging to troubleshoot race conditions in hooks UI blocks

Fix streaming output for hooks

Remove in-progress planning file

Revert breakage

Remove unneeded arg output in PreToolUse hook UI

Fix race condition with PreToolUse and PostToolUse hooks

Fix PreToolUse attempt_completion use case

Fix ChatTextArea prompt input

Fixed part of the rersume bug

Clean-up before code review

Change cancel button to abort button

Fixed TaskCancel behavior

Fixing resume behavior (partially fixed)

Make notch arrow visible in hooks block up expand/contract toggle

Prevent cancel/abort from clearing unsent user message

Change 'Cancelled' to 'Aborted' in hook UI block

Use color styles consistent with background terminal

Fix npm compile issue

Remove unneeded hot-cold tracking from hooks

Remove unused constant as per PR feedback

Minor changes

Skip combineHookSequences() if hooks feature setting not enabled

Improve mutex pattern for general use and fix additional race condition

Refactor to reduce duplication (keep it DRY)

Refactor to reduce duplication (keep it DRY) part 2

Fix tests

Skip some tests on Windows (not yet supported)

Hooks unsupported on Windows

Fix broken unit tests

* Fix broken unit tests

* Bee and Eve fixing 'API Request...' test in e2e tests

* Add comment as per ellipsis-dev PR feedback

* Changes as per PR feedback from Evan

* Remove if that always resolves to true

* Don't use vscode API; use cross-compatible solutions instead

* Remove mention of obsolete clineAsk handler
2025-10-31 05:50:18 -07:00
github-actions[bot] 3698d2356c v3.35.0 Release Notes (#7127)
- Add native tool calling support with configurable setting.
- Auto-approve is now always-on with a redesigned expanding menu. Settings simplified and notifications moved to General Settings.
- added zai-glm-4.6 as a Cerebras model
- Created GPT5 family specific system prompt template
- Fix: show reasoning budget slider to models with valid thinking config
- Requesty base URL, and API key fixes
- Delete all Auth Tokens when logging out
- Support for <think> tags for models that prefer that over <thinking>

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-30 18:41:23 -07:00
Bee ff20c4addc feat: add native tool call setting with feature flag support (#7193)
* feat: add native tool call setting with feature flag support

Add a new native tool call setting that allows users to enable/disable native tool calling functionality. The setting is controlled by both a user preference and a feature flag, providing flexibility for gradual rollout. Changes include:
- Added native_tool_call_enabled field to UpdateSettingsRequest proto
- Implemented state management for the setting in controller and storage
- Added UI toggle in feature settings section
- Updated task execution logic to check both user setting and feature flag
- Extended extension state interface to include the new setting

* Add changeset
2025-10-30 17:47:35 -07:00
Bee 8eeeabb966 fix: ensure hasThinkingBudget validates non-empty thinkingConfig object (#7192)
Previously, hasThinkingBudget would return true for empty objects ({}),
causing incorrect behavior. Now checks both for truthiness and that the
object contains at least one key.

Also updates thinkingConfig assignment logic to avoid setting empty
objects and converts imports to type-only imports for better tree-shaking.
2025-10-30 16:22:51 -07:00
Saoud Rizwan 9b1dc5bd92 Improve auto-approve menu design and remove unnecessary options (#7180) 2025-10-30 16:10:14 -07:00
CandiedUniverse 1cfff0a45f Hide hooks toggle from appearing in the the Feature Settings (#7191) 2025-10-30 15:44:31 -07:00
Bee e7d00dec2d refactor(ui): improve tooltip and task header responsiveness (#7186)
Refine TaskHeader controls so actions wrap naturally and the cost
label scales down on narrow viewports.
Update tooltip content to wrap flexibly, tweak padding,
add collision padding, and cap width on small screens.
Introduce xs and xxs breakpoint variables to support responsive styles.
2025-10-30 11:41:23 -07:00
Bee 2c5748ccfd fix: set free pricing for featured models in Cline provider (#7179)
* fix: set free pricing for featured models in Cline provider

Add isFree flag to featured models and ensure free models have $0 pricing when using Cline provider. This updates the OpenRouterModelPicker to automatically set input, output, and cache prices to zero for free featured models, preventing confusing info display to users.

* update

* fix
2025-10-30 11:41:14 -07:00
Bee 5196adce33 fix: adjust mode toggle hover and colors (#7177)
* fix: adjust mode toggle hover and colors

- add hover state to plan/act toggle buttons with toolbar hover color
- set inactive text color to text-foreground
- align toggle styling with previous design

* feat: toggle spacing fix and transparent color

* SwitchContainer to tailwind

* revert

---------

Co-authored-by: Jose R. Perez <trupix@gmail.com>
2025-10-30 10:20:30 -07:00
celestial-vault d95d86f329 updateApiConfiguration v2 (#7160)
* add new version of updateApiConfiguration that splits options and secrets

* remove console log

* remove unnecessary modelInfo conversion
2025-10-29 22:39:51 -06:00
Bee f6eb3aa386 update: GPT-5 native tools apply_patch to write_to_file (#7178)
- extend replace_in_file and write_to_file specs with a native GPT-5 variant to support the model
- swap native GPT-5 config to use write_to_file and replace_in_file tools instead of apply_patch tool
2025-10-29 19:56:29 -07:00
Bee 5c4b9e54c2 Bee/mcp server key (#7175)
* refactor: use hashed keys for MCP server names in native tool identifiers

- Replace server names with unique hashed keys in MCP tool names to avoid length issues and ensure uniqueness.
- Add McpHub methods to generate and retrieve server names from keys.
- Update McpServer type to include uid field.
- Modify tool-use-handler to resolve server names from keys during tool execution.

* Add server name to tool description

* Fix collision case

* clean up on error
2025-10-29 18:59:29 -07:00
Juan Pablo Flores 29a1d08685 docs: updates Cursor AI Panel drag instructions and updates Gif (#7176)
* docs: clarify Cursor AI Panel drag instructions and fix image path

* docs: replace "AI Panel" with "AI Pane" in Cursor right-sidebar instructions
2025-10-29 18:58:48 -07:00
Tomás Barreiro 9664ddd106 Ensure WorkOS Auth token deletion when logging out (#7174)
* Ensure WorkOS Auth token deletion when logging out

* Add changeset

* refactor
2025-10-30 02:19:33 +01:00
Daniel Steigman 3c1327b115 fix(cli): Fix config set overwriting all settings instead of merging (#6980)
* fix(cli): Fix config set overwriting all settings instead of merging

ENG-1115

This fix resolves the issue where 'cline config set' would overwrite all
settings instead of merging with existing values.

Changes:
1. CLI (cli/pkg/cli/config.go): Changed setCommand to use
   UpdateSettingsPartial() instead of UpdateSettings()

2. Server (src/core/controller/state/updateSettingsCli.ts): Added defensive
   checks for defaultTerminalProfile to prevent undefined errors

3. Added merge.go with proper settings merge logic

The fix follows git-style behavior where 'cline config set key=value'
merges with existing settings, preserving all other values.

Tested and verified:
- Setting max-requests to 999 works
- Setting edit-files-externally preserves max-requests
- Multiple successive config sets preserve all previous values

* fix: use optional proto bools to fix config set field overwrite

Root cause: Non-optional bool fields in AutoApprovalSettings proto were
transmitting zero values (false) even when not set by user, causing the
server-side merge to overwrite existing settings.

Solution: Made 'enabled' and 'enableNotifications' fields optional in proto,
matching the pattern used by AutoApprovalActions fields. This allows proper
server-side merge detection using 'field !== undefined' checks.

Changes:
- proto/cline/state.proto: Added optional keyword to two bool fields
- cli/pkg/cli/task/settings_parser.go: Use boolPtr() for optional fields
- cli/pkg/cli/task/manager.go: Fix UpdateTaskAutoApprovalAction to use boolPtr()
- Removed client-side merge logic (merge.go, UpdateSettingsPartial method)
- Simplified config.go to use UpdateSettings() directly

This approach is simpler and more maintainable than the previous client-side
merge solution, relying on the existing server-side merge logic that already
handles undefined values correctly.

* fix: Address celestial-vault feedback on proto optional fields

- Remove unnecessary error suppression in updateSettingsCli.ts
- Make max_requests optional in AutoApprovalSettings proto
- Update Go parser to use int32Ptr() for optional max_requests
- Remove hardcoded MaxRequests default in UpdateTaskAutoApprovalAction

This prevents the CLI from overwriting user-configured maxRequests values
and maintains consistency with other optional fields (enabled, enableNotifications).

* fix: Remove error suppression for missing terminal manager in updateSettingsCli

Addresses celestial-vault's feedback on ENG-1115 PR.

Previously, the check 'if (controller.task && controller.task.terminalManager)'
silently suppressed errors when a task existed but terminalManager was missing.

Now properly throws an error if task exists without terminalManager (error case),
while allowing terminal profile updates when no task is running (normal case).
2025-10-29 17:40:10 -07:00
Sarah Fortune bb993e4a9a Remove duplicated code for getting the MCP catalog from the server (#7163)
Combine almost identitical methods for getting the MCP catalog.

We are always using silent=true, so remove the param and silent=false code path.
2025-10-29 17:01:15 -07:00
Sarah Fortune 473b3d0204 Update remote config schema to use URL for MCP ID (#7168) 2025-10-29 17:01:03 -07:00
Bee 7e68614631 refactor(ui): update BrowserSessionRow URL bar spacing (#7158)
* refactor(ui): update BrowserSessionRow URL bar spacing

Replace inline styles with Tailwind CSS classes for the URL bar component in BrowserSessionRow. This change:
- Removes the `urlTextStyle` CSSProperties object
- Converts inline style objects to Tailwind utility classes using `cn()`
- Maintains the same visual appearance and conditional styling
- Improves code maintainability and consistency with the project's styling approach

* width

---------

Co-authored-by: Jose R. Perez <trupix@gmail.com>
2025-10-29 16:10:34 -07:00
Bee a19e9907d5 fix(ui): hide tooltip arrow for auto approve menu item (#7157)
* fix(ui): hide tooltip arrow for auto approve menu item

Remove the arrow from the tooltip in AutoApproveMenuItem by setting showArrow={false} on TooltipContent. This improves the visual appearance of the tooltip for auto-approve action descriptions.

* clean up

---------

Co-authored-by: Jose R. Perez <trupix@gmail.com>
2025-10-29 16:03:02 -07:00
canvrno 19f74cac01 Add domain to openai compatible telemetry (#7170)
* OpenAI compatible domain telemetry

* Updated tests for openAiCompatibleDomain inclusion in telemtry
2025-10-29 15:51:50 -07:00
Bee b877abc708 fix(ui): align toolbar buttons and improve modal visibility control (#7159)
* fix(ui): align toolbar buttons and improve modal visibility control

- Remove inconsistent top margin (mt-0.5, mt-1) from toolbar buttons for consistent vertical alignment
- Add explicit `open` prop to ServersToggleModal Popover for better visibility state control
- Replace inline style with Tailwind class (mb-2.5) for consistent spacing
- Restructure ClineRulesToggleModal layout with proper flex container hierarchy
- Standardize icon sizing and remove redundant flex classes
- Remove trailing whitespace in ServersToggleModal

These changes ensure toolbar buttons align properly and modal visibility states are managed consistently across the chat interface.

* remove space

---------

Co-authored-by: Jose R. Perez <trupix@gmail.com>
2025-10-29 15:12:48 -07:00
Thibault Jaigu c90e64e763 feat: add Requesty OAuth with custom base URL support (#6953)
* feat: add Requesty OAuth with custom base URL support

* fix: changing base url to undefined when user unselected base url checkbox

* chore: adding change set

* chore: minor style changes to requesty provider

---------

Co-authored-by: John Costa <john@requesty.ai>
2025-10-29 14:47:11 -07:00
Bee e02e1eca7f adjust see more styles (#7150) 2025-10-29 14:44:48 -07:00
Bee f8925fb8fd fix: improve CheckpointError layout and Alert component flex behavior (#7148)
* fix: improve CheckpointError layout and Alert component flex behavior

- Add explicit sizing and spacing classes to AlertTriangleIcon in CheckpointError
- Refactor Alert component to use flex layout instead of absolute positioning
- Reduce padding from p-4 to p-2 and adjust gap spacing for tighter layout
- Remove absolute positioning from dismiss button and icon elements
- Update AlertTitle to use grow class for proper flex behavior

This fixes layout issues with the CheckpointError component on flex containers by replacing absolute positioning with flexbox, resulting in more predictable and responsive behavior.

* alert styles

* title
2025-10-29 14:44:39 -07:00
Zhongying Qiao ac81aeaf4e feat: Add banner api calls and evaluate banner display rule sets locally (#7087)
* scaffold banner api calls

* feat: add basic json rules local evaludation

* use another way to fetch ide name and version

* refine rules as well as rules evaluations in BannerService

* refactor: refactor BannerService so it is unit testable

* feat: add tests to banner service

* refactor: use separate initialize and get for banner service

* refactor: use Logger.error to log errors

* refactor: rename personal to personal only

* fix: move import to the top

* fix: remove unnecessary  wrapper

* move banner initialization to common.ts

* fix test

* fix: move import statement to the top in common.ts

* fix: remove feature targeting from rulesets

* fix: remove this._controller check in BannerService

* fix: add getProviderName in AuthService

* fix: include the owner role, treat it same as admin in applying banner rules
2025-10-29 14:03:33 -07:00
Ara f12b5a1573 fix: Removing language selection from voice mode to support aqua voice (#7086)
* fix: Removing language selection from voice mode to support aqua voice

* fix: Removing language selection from voice mode to support aqua voice

* fix: Removing language selection from voice mode to support aqua voice

* fix: bug fixes

* fix: bug fixes
2025-10-29 13:29:22 -07:00
Tomás Barreiro a66b57ef51 Add MCP settings to the remote config (#7123)
* Add MCP settings to the remote config

* Refactor MCP server schema

* Add model list to the root remote config

* Add tests

* Add type
2025-10-29 12:20:29 -07:00
Tomás Barreiro 944ed41f4a Add headers to the OTEL remote settings (#7149) 2025-10-29 16:36:31 +01:00
canvrno 970e941e57 Remove <think> tags for models that prefer this tag over <thinking> (#7144) 2025-10-28 20:24:46 -07:00
Bee 45abe977e4 update: use diff edit tools for gpt-5 (#7147)
GPT-5 with native tool calling support has updated to use the new apply_patch tool. Before we can confirm the new tool works consistently, the old GPT-5 variant without native tool calling enabled should continue to use the current file edit tools.
2025-10-28 18:10:30 -07:00
Bee 5755b30bce refactor: migrate ModelDescriptionMarkdown to Tailwind CSS (#7145)
Replace styled-components with Tailwind CSS classes in ModelDescriptionMarkdown component. Remove react-remark dependency in favor of simpler rendering approach. Update component to use shadcn/ui Button component instead of VSCodeLink for "See more" action. Add displayName for better debugging. Remove duplicate and unused styled markdown implementation from OpenRouterModelPicker and RequestyModelPicker.

This change improves consistency with the project's UI component library and codebase clean up.

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-10-28 17:51:26 -07:00
Bee 5fde21dffd native tool calling (#6921)
* (WIP)feat(api): add tool calling support to openai API handlers

Add ChatCompletionTool parameter to createMessage interface and implement tool calling functionality across API providers. Includes new StreamingToolCallHandler for processing tool call deltas and utility functions for converting tool specifications to OpenAI format.

* Replace path with absolutePath

* Simplify tool spec

* Parse stream and use variant matcher

* Simplify StreamingToolCallHandler

* Add CLINE_NEXT_GEN

* Enable parallel_tool_calls

* ApplyPatchHandler

* Simplify ApplyPatchHandler

* store tool use chunk

* feat(anthropic): add streaming tool use support with OpenAI-compatible format

Add comprehensive support for streaming tool use blocks from Anthropic API:

- Add tool_use case handler in AnthropicHandler to convert streaming tool
  calls to OpenAI-compatible format during content block start events
- Implement ToolUseHandler class to accumulate and parse streaming tool use
  deltas with incremental JSON parsing support
- Add proper handling for undefined tool message content in openai-format
  converter to prevent errors
- Integrate ToolUseHandler into TaskState for managing streaming tool calls
  across the request lifecycle

This enables real-time processing of tool calls as they stream from Anthropic's
API while maintaining compatibility with OpenAI's tool calling format.

* feat(anthropic): add tool support with OpenAI tool conversion

Add support for passing tools to Anthropic API with automatic conversion from OpenAI tool format. Implements tool_choice configuration to force tool usage when tools are provided.

Changes:
- Add tools parameter to createMessage method in AnthropicHandler
- Implement openAIToolToAnthropic converter to transform OpenAI ChatCompletionTool format to Anthropic Tool format
- Set tool_choice to "any" when tools are provided to ensure Claude uses one of the available tools
- Fix tool result handling to avoid invalid tool_use_id "cline" by converting to text blocks for backward compatibility
- Add documentation for tool_choice options (none, auto, any)

This enables proper tool calling functionality with Anthropic models while maintaining compatibility with the existing OpenAI tool definitions.

* oops

* fix: remove .mjs extensions from import paths

Remove `.mjs` file extensions from OpenAI and Anthropic SDK import paths to ensure compatibility with current module resolution. This updates imports across multiple files:
- openai/resources/chat/completions.mjs → openai/resources/chat/completions
- @anthropic-ai/sdk/resources/index.mjs → @anthropic-ai/sdk/resources/index

* revert styles.css change from merge main

* Update config and fix tests

* gpt-5 variant for cline provider only

* Fix GPT-5 web fetch tool

* Add native next gen for supported openai providers

* add native tool calling support to more openai comp providers

* list supported providers for gpt-5

* clean up

* Update ApplyPatchHandler

* clean up

* Update ask_followup_question

* Add system prompt test for cline claude 4.5 sonnet

* noToolsUsed

* Add support to gemini api provider

* add native tool calling to more providers

* Skip XML parsing

* clean up

* add back TASK_PROGRESS_PARAMETER to native tools

* feature flag NATIVE_TOOL_CALLS_NEXT_GEN_MODELS

* add allowNativeToolCalls to system prompt context

* add grok-code

* feat(context): ensure tool_result blocks follow tool_use blocks

Add validation to ensure every tool_use block in assistant messages has a corresponding tool_result in the following user message. When tool_result blocks are missing, they are automatically added with "result missing" content to maintain proper message pairing required by the Anthropic API.

This prevents API errors caused by orphaned tool_use blocks and ensures conversation history integrity after context management operations.

* feat(task): refactor ApplyPatchHandler to use FileEditProvider with revert support

Refactor the ApplyPatchHandler to use FileEditProvider and DiffViewProvider
for file operations instead of direct filesystem access. This change adds:

- State tracking for applied commits to enable rollback functionality
- Automatic revert of changes when user denies approval or errors occur
- Enhanced response format that includes full file contents after patching
- Consistent file reading through DiffViewProvider for better editor integration
- Proper cleanup of applied state after successful operations

This improves the patch application workflow by ensuring changes can be
safely reverted and provides better visibility into the applied changes.
The refactoring also aligns file operations with the existing editor
integration patterns used elsewhere in the codebase.

* Finalizing...

* MCP tools converter

* remove ClineDefaultTool.MCP_USE

* Update snapshot

* comments

* use diffViewProvider

* Fix MCP tool use with images

* fix mcp tool converter with reserved keys

* Fix apply patch streaming issue

* add vercel-ai-gateway to supported provider & clean up

* simplfy apply patch handler

* Turn off thinking when native tools are enabled for anthropic provider

* clean up ToolUseHandler

* anthropic tool_choice logic

* improve tool result ordering

- Extract anthropic message mapping logic into a separate variable for better readability
- Add MessageParam import from Anthropic SDK
- Update ensureToolResultsFollowToolUse to maintain tool_result order matching tool_use blocks
- Improve documentation to clarify tool result ordering requirements

This refactoring improves code maintainability by separating the message transformation logic and ensures tool results are properly ordered to match their corresponding tool use blocks in the conversation flow.
<budget:token_budget>200000</budget:token_budget>

* refactor tool result validation for improved efficiency

Simplify ensureToolResultsFollowToolUse method by streamlining the logic
for validating and reordering tool_result blocks. Changes include:

- Use early returns to reduce nesting and improve readability
- Process tool results and other blocks in a single pass instead of multiple iterations
- Eliminate unnecessary deep cloning by mutating content array directly
- Add flag-based update detection to avoid unnecessary array rebuilding
- Simplify the reordering logic while maintaining the same validation rules

This refactoring maintains the same functional behavior (ensuring tool_result
blocks follow their corresponding tool_use blocks) while reducing complexity
and improving performance through more efficient array operations.
<budget:token_budget>200000</budget:token_budget>

* attempt_completion

* use helper function

* remove FreeModelIDs from merge conflicts

* clean up

* Add NATIVE_GPT_5 & exclude gpt-5-chat

* shouldAutoApproveToolWithPath for apply patch

* add openai-native to supported provider

* add lightweight incremental chunk extraction

* update apply patch handler

* revert version regex

* Fix browser action
2025-10-28 17:49:19 -07:00
canvrno cf9f2a8630 Package updates (#7001) 2025-10-28 17:45:19 -07:00
nihar-oracle 5a3416ff09 Feat/nturumel/proto-python (#7090)
* feat(proto-python): add script-generated Python gRPC stubs, Go-like client, docs, and PyPI publish workflow

- add scripts/build-python-proto.mjs

  - invokes python -m grpc_tools.protoc over proto/**/*.proto
  - outputs to src/generated/grpc-python
  - mirrors Go layout under client/: connection.py, cline_client.py, services/_client.py
  - supports PYTHON env override (use a venv interpreter easily)
  - generates src/generated/grpc-python/pyproject.toml so output can be pip installed (pip install -e src/generated/grpc-python)

- package.json

  - add protos-python script to run the generator

- docs

  - add docs/exploring-clines-tools/python-protos.mdx with venv setup, generation steps, and import examples
  - emphasize: everything in src/generated is produced by scripts (do not commit manual edits)

- CI: publish to PyPI only

  - add .github/workflows/publish-grpc-python.yml
  - workflow generates code via script, builds wheel/sdist from src/generated/grpc-python, and uploads to PyPI
  - requires repo secret: PYPI_API_TOKEN (TWINE_USERNAME=__token__)
  - optional version override input for workflow_dispatch

Notes:

- generation strictly produces all content under src/generated/grpc-python (including pyproject.toml)
- default package name in generated pyproject is cline-grpc-python (adjustable in the script if needed)
- recommended usage on macOS: PYTHON=/Users/nturumel/projects/oracle-github/cline/.venv-proto/bin/python npm run protos-python

* Delete .github/workflows/publish-grpc-python.yml

* Delete docs/exploring-clines-tools/python-protos.mdx

* Update tired-banks-show.md

---------

Co-authored-by: Andrei Eternal <206184+Garoth@users.noreply.github.com>
2025-10-28 17:43:05 -07:00
Bee 826b2b1276 refactor: replace HeroUI Alert with shadcn Alert component (#7137)
* refactor: replace HeroUI Alert with shadcn Alert component

Replace HeroUI Alert component with a new custom Alert component in CheckpointError. The new component provides better control over styling and behavior, removes the dismiss functionality from CheckpointError, and uses a cleaner structure with AlertTitle and AlertDescription. This change improves consistency with the UI design system and simplifies the error display logic.

* Add storybook for TaskHeader with CheckpointError

* clean up
2025-10-28 17:29:51 -07:00
Kevin Taylor 8da38b2a2e Add Cerebras GLM 4.6 model support and set as default (#7143)
* Add Cerebras GLM 4.6 model support and set as default

- Add zai-glm-4.6 model configuration with 128k context window
- Set GLM 4.6 as default Cerebras model (replacing qwen-3-coder-480b-free)
- Configure 40k max tokens and 2,000 tokens/s performance

* add changeset
2025-10-28 14:22:31 -07:00
Saoud Rizwan cb121170cb Revise tip for opening Cline on the right (#7122)
Updated the tip section to provide a link for opening Cline in the sidebar.
2025-10-28 13:55:23 -07:00
Bee e9d2d344c6 fix: stop event propagation on task header button clicks (#7139)
Prevent default actions and stop event propagation on button clicks to avoid unintended behavior.
2025-10-28 12:19:13 -07:00
Daniel Steigman 69fb954a6c feat: Set default OpenTelemetry exporters to console,otlp (#7129)
* feat(ci): configure OpenTelemetry exporters for production

Set default OpenTelemetry log and metric exporters to console,otlp in both nightly and release workflows to ensure proper telemetry collection in production environment.

* ci: remove OTEL_METRIC_EXPORT_INTERVAL from publish workflows

Removed the OTEL_METRIC_EXPORT_INTERVAL environment variable from both nightly and stable publish workflows as it's no longer needed for the publishing process.
2025-10-28 11:31:11 -07:00
Bee e9e616e317 Replace HeroUI Tooltip with shadcn (#6872)
* Set up Tailwind v4

npx @tailwindcss/upgrade                                                                                                                                   1 ↵
≈ tailwindcss v4.1.13

│ Searching for CSS files in the current directory and its subdirectories…

│ Migrating stylesheets…

│ ↳ Migrated stylesheet: `./src/index.css`

│ Updating dependencies…

│ ↳ Updated package: `tailwindcss`

│ ↳ Updated package: `@tailwindcss/vite`

│ Migrating templates…

│ ↳ Migrated templates for: `./src/index.css`

│ Verify the changes and commit them to your repository.

* Migrate HeroUITooltip to radix-ui shadcn components

* import main.css

* Update e2e test text

* clean up

* Update mode switch test

* Fix auto approve modal z-index number

* Unify styles with theme

* fix spacing and sizes

* update logo id

* Fix e2e test

* Clean up

* npm install tailwindcss @tailwindcss/vite

* npx @tailwindcss/upgrade
≈ tailwindcss v4.1.14

│ ↳ Upgrading from Tailwind CSS `v4.1.14`

│ Searching for CSS files in the current directory and its subdirectories…

│ Migrating stylesheets…

│ ↳ Migrated stylesheet: `./webview-ui/src/index.css`

│ Updating dependencies…

│ ↳ Updated package: `tailwindcss`

│ ↳ Updated package: `@tailwindcss/vite`

│ Migrating templates…

│ ↳ Migrated templates for: `./webview-ui/src/index.css`

│ Verify the changes and commit them to your repository.

* clean up

* clean up

* Remove DRY code and update descriptionForeground class name

* Update TaskHeader classnames

* unify font size

* clean up

* update test with clear test id

* size

* Fix tooltip trigger in settings

* Apply feedback - hide arrow for autoapprove menu

* Align chat toolbox icon stylings

* update data-testid

* set

* CheckpointError

* feat: arrow alignment issues

* text-wrap tooltip

* feat: mcp tooltip arrow fix

---------

Co-authored-by: Jose R. Perez <trupix@gmail.com>
2025-10-28 09:15:29 -07:00
canvrno 545ac29e07 GPT5 system prompt snapshots + small adjustment. (#7120)
* gpt5 system prompt adjustments

* changeset
2025-10-27 15:19:47 -07:00
canvrno d38489aebc glm-4.6 system prompt adjustments (#7121) 2025-10-27 14:35:43 -07:00
github-actions[bot] 8d47026640 v3.34.1 Release Notes (#7061)
- Added support for MiniMax provider with MiniMax-M2 model
- Remove Cline/code-supernova-1-million model
- Changes to allow users to manually enter model names (eg. presets) when using OpenRouter

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-27 13:49:59 -07:00
Ara 268cd5c527 feat(settings): allow Minimax models with :free suffix for Cline provider (#7119)
Update OpenRouterModelPicker to include Minimax M2 models even when they
have the :free suffix. Previously, all :free models were filtered out for
the Cline provider, but Minimax models should be available regardless of
their pricing tier to ensure users have access to these specific models.
2025-10-27 13:01:59 -07:00
Ara c7c4e43322 Remove old models (#7118)
* refactor: remove CodeSupernova model and related code

Remove the deprecated cline/code-supernova-1-million model from the codebase:
- Delete clineCodeSupernovaModelInfo export from shared API
- Remove CodeSupernova model entry from CLINE_STEALTH_MODELS
- Remove CodeSupernova announcement UI and related state management
- Clean up unused imports (EmptyRequest, AccountServiceClient)
- Update import formatting in refreshOpenRouterModels

This model is no longer supported and has been replaced by other offerings.

* hel

* fix: bug fixes
2025-10-27 12:26:09 -07:00
Maosghoul aae9d432fd Feat: Add MiniMax AI provider (#7094)
* feat: add minimax ai

* feat: api

* feat: mmx

* feat: name

* feat: model name

* feat: fix

* feat: fix

* feat: add model info

* feat: format code

* feat: format code

* feat: code

* fix: log

* feat: param

* feat: add m2

* feat: add m2

* feat: format code

* feat: info

---------

Co-authored-by: xiaose <xiaose@minmaxi.com>
2025-10-27 11:18:34 -07:00
Saoud Rizwan 604dbd7bb0 fix: error_retry message breaking browser session row flow (#7106) 2025-10-26 09:43:01 -07:00
Bee 062a32f93d fix(scripts): fix proto-lint script execution on Windows (#7089)
* fix(scripts): fix proto-lint script execution on Windows

On Windows, directly calling 'scripts/proto-lint.sh' fails because it's not recognized as an internal or external command. This change wraps the script in an npm run command to ensure cross-platform compatibility. Added a new 'lint:proto' script for better organization.

* Update lint:proto script path to use relative path

* bash
2025-10-24 19:57:12 -07:00
canvrno 535b29f465 Support OpenRouter presets entry (#7083) 2025-10-24 17:18:59 -07:00
nihar-oracle a8027dc570 feat: Adding oracle code assist to the cli (#7004)
wip:

wip:

wip:

fix: Adding oca auth state instead of using model id check

fix: Adding oca auth state instead of using model id check

chore: Undoing debug changes
2025-10-24 14:50:37 -07:00
Toshii 978a8a0aa6 update e2e evals to use cline cli (#6977)
* remove un-implemented tests and create foundation for running cline in cli for exercism

* running version for python language

* remove unused code and reorder benchmark adapter

* remove optional helper functions from BenchmarkAdapter

* unskipping tests for java and javascript

* updating db schema

* updating output to match schema

* functional tests for all languages

* clean up unused commit and stored result

* nits

* small changes to wording

* adding to the test outputs

* using stdin for cline task send

* adding results dir to gitignore

* updating readme

* small nits for readme
2025-10-24 09:21:21 -07:00
Sarah Fortune 0cd462a414 Add linter check for proto files and add autoformatting (#7066)
Add a linter check for proto files to avoid issues like https://github.com/cline/cline/pull/7054
Format the proto files while linting
2025-10-23 14:37:26 -07:00
canvrno 65dbd85a92 Updating trending model list (#7018)
* Updating trending model list

* exacto
2025-10-23 14:33:00 -07:00
Tomás Barreiro ee1bb2f788 Support Feature Flags default values (#7027)
* Support Feature Flags default values

* Update src/services/feature-flags/FeatureFlagsService.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* Update FeatureFlag support for unknown values

* refactor isFeatureFlagEnabeld

---------

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
2025-10-23 23:30:09 +02:00
celestial-vault 6f69ffb16f Remove apiConfiguration conversion function from updateApiConfiguration (#7045)
* remove massive conversion function and only convert what's needed

* add modelinfo conversion for all providers
2025-10-23 10:47:59 -07:00
canvrno f91769bda7 Fixed proto name issue (#7054) 2025-10-23 09:05:05 -06:00
Juan Pablo Flores 7692adacf5 Claude docs update and fixing missing images (#7041)
* Remove Windows setup accordion and streamline instructions for finding Claude Code path

* fix: update image source for Cline chat prompt to use a public URL
2025-10-22 19:43:32 -07:00
Ara a98faf5af4 fix: Removing Eslint from package lock json (#7047) 2025-10-22 19:32:59 -07:00
github-actions[bot] e3f4ce618f Changeset version bump (#7037)
* v3.34.0 Release Notes

- Cline Teams is now free through 2025 for unlimited users. Includes Jetbrains, RBAC, centralized billing and more.
- Use the “exacto” versions of GLM-4.6, Kimi-K2, and Qwen3-Coder in the Cline provider for the best balance of cost, speed, accuracy and tool-calling.

* fix: Adding Fallbacks

* fix: Adding Fallbacks

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2025-10-22 17:35:54 -07:00
canvrno dcf519d2f7 GLM 4.6 prompt changes (#7046)
* GLM 4.6 prompt changes

* GLM MCP prompt tweaks

* Update src/core/prompts/system-prompt/__tests__/__snapshots__/zai_glm_4_6-no-browser.snap

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

* snapshot update

* snapshot update again

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-10-22 16:42:45 -07:00
pashpashpash 3ef4aea0f7 added opinionated preferences for open source model providers (#7020)
* added opinionated preferences for open source model providers

* moving to apits instead of refreshopenroutermodels

* aras recommendations

* zai fix

* changed name and removed free models

* fix: Adding Fallbacks

---------

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2025-10-22 16:24:54 -07:00
canvrno a820026e0b Multiple CLI auth wizard changes (#7005) 2025-10-22 13:06:23 -07:00
Tomás Barreiro 29d1b0507c Update stored WorkOS Auth Data after refreshing it (#7029)
* Update stored Auth Data after refreshing it

* Update src/services/auth/providers/ClineAuthProvider.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

---------

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
2025-10-22 19:17:26 +02:00
Ara 929d13a4dd Fix(task): use background terminal for subagent command execution (#7017)
* refactor(task): use background terminal for subagent command execution

Replace VSCode terminal with StandaloneTerminalManager for CLI subagent
commands to enable hidden background execution. Falls back to standard
TerminalManager if standalone module is unavailable.

This change allows subagent commands to run in a background terminal
instead of visible VSCode terminals, improving user experience by
reducing terminal clutter during subagent operations.

* fix: added links

* Update src/core/task/index.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* Update src/core/task/index.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

---------

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
2025-10-22 10:09:03 -07:00
Bee c729e8c7c6 feat: expandable long task header (#6966)
* fix: add Read More for long task in header

- Truncate task description to first 3 lines by default and add a
  Read More/Show Less toggle to expand/collapse the full text
- Compute highlighted text based on expansion state; introduce local
  isHighlightedTextExpanded state
- Increase task details container max height (max-h-20 -> max-h-80) for
  better readability when expanded
- Remove unused useAutoCondense from context destructuring

Improves UX by preventing long task text from overwhelming the UI while
giving users control to view more when needed. Also includes minor cleanup.

* update

* Set to 25vh instead

* highlightText

* feat(ui): task text expansion with click-outside collapse

- Replace "Read More/Show Less" button with click-to-expand interaction
- Add click-outside listener to automatically collapse expanded text
- Apply gradient mask to truncated text for better visual indication
- Optimize rendering by removing conditional text highlighting
- Refactor layout to use single container with dynamic height constraints

This improves UX by making text expansion more intuitive and reducing visual clutter from the toggle button.

* update changeset

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-10-22 10:05:24 -07:00
celestial-vault 737452b2b1 wire open command with -s flag to use tasksettings (#7016) 2025-10-22 09:32:41 -06:00
Sarah Fortune ba6a72cf15 Update the remote config when the user logs in (#7025)
* Update the remote config when the user logs in

Subscribe to changes in the auth state, and fetch the remote config when the user logs in.

Move the error handling into `fetchRemoteConfig` to remove duplication.

* Update src/core/storage/remote-config/fetch.ts

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-10-22 09:31:15 -06:00
AJ Juaire 65a0c35163 Add Qwen 3 Coder models to Amazon Bedrock models (#7022)
* Add Qwen 3 Coder models to Amazon Bedrock models

* Update comments to reference qwen

* Update cost.ts to round to avoid flakey tests

* remove math.round
2025-10-21 20:10:34 -07:00
Tomás Barreiro 0c8e02c6e4 Fetch the complete user info when the user logs in using WorkOS (#7026)
* Fetch the complete user info when the user logs in using WorkOS

* Add changeset

* fallback to token data
2025-10-22 03:29:52 +02:00
pashpashpash b636018ef5 fixing no-tty / stdin + standardizing color codes in plain mode (#6992)
* terminal shift enter support

* not needed

* detecting windows

* removing enhancedkeyboard

* removing enhanced keyboard

* ghostty

* proper ghostty support

* docs for posterity

* better logging

* removing md

* doctor command

* adding arguments for sync/async for doctor and keyboard setup - and moved keyboard setup to doctor command

* doctor help

* cleaning up logging and making things more explicit

* language and positioning

* standardizing color codes in plain mode

* wow even more hidden rendering - removed

* fixing stdin for restrictive shell environments
2025-10-21 13:22:20 -07:00
Alex Ker cfc6b0d7f5 added ZAI GLM4.6 to static models list and set as default (#6989)
* added ZAI GLM4.6 to static models list and set as default

* changest

---------

Co-authored-by: AlexKer <AlexKer@users.noreply.github.com>
2025-10-21 13:06:11 -06:00
Bee f116a6323d refactor: simplify BedrockProvider UI code (#7015)
* refactor(settings): migrate BedrockProvider to Tailwind and extract constants

- Extract Claude models and AWS regions into reusable constants
- Add className prop support to DebouncedTextField component
- Replace inline styles with Tailwind CSS classes throughout BedrockProvider
- Improve code maintainability and consistency with modern styling approach

This refactoring improves code organization by moving hardcoded lists to
constants and standardizes the styling approach across the settings UI
components.

* lock icons
2025-10-21 11:32:28 -07:00
Toshii 46d3b2a3ed auto compact updates (#6909)
* update prompting around first task message and summarization prompt and add file read parsing

* replace first user message to handle issue of refousing on old task after condense

* add line about focusing on initial task for history

* adding prompting around our removing of context history and verbosity

* prompting changes

* fix spelling nit in prompting

* update displayPath,absolutePath logic to match read file tool handler

* increment the auto approval usage
2025-10-20 17:04:44 -07:00
pashpashpash 9679917532 cline doctor command: terminal shift enter support + auto updates (#6883)
* terminal shift enter support

* not needed

* detecting windows

* removing enhancedkeyboard

* removing enhanced keyboard

* ghostty

* proper ghostty support

* docs for posterity

* better logging

* removing md

* doctor command

* adding arguments for sync/async for doctor and keyboard setup - and moved keyboard setup to doctor command

* doctor help

* cleaning up logging and making things more explicit

* language and positioning
2025-10-20 16:47:07 -07:00
canvrno 89bf81f7f6 Package updates (#6991)
* tar-fs

* playwright

* mammoth
2025-10-20 13:42:51 -07:00
canvrno afe01df8b4 Added new AWS SE regions (#6990) 2025-10-20 11:26:47 -07:00
celestial-vault eb1325686e separate core and rpc wrappers for refresh models (#6981)
* separate core and rpc wrappers for cleaner calling on the extension

* tweak jsdoc strings

* fix function name error go code
2025-10-20 12:20:46 -06:00
Ara 7c7962ce0f fix: Solve the issue where the notch on the terminal doesn't have full visibility (#6972) 2025-10-20 09:46:34 -07:00
celestial-vault 0707df2205 add cline provider (#6927) 2025-10-19 19:22:14 -06:00
celestial-vault ca87c21b77 use setglobalstatebatch for refresh models instead of setapiconfiguration (#6971) 2025-10-18 11:05:41 -07:00
CandiedUniverse d6f736e8d5 feat(hooks): Implement TaskCancel hook (#6962) 2025-10-17 18:24:23 -07:00
CandiedUniverse 4336471d84 feat(hooks): Implement TaskResume hook (#6928) 2025-10-17 18:23:50 -07:00
Bee 3191e23c1d fix(dev): deauth user on env changed (#6969) 2025-10-17 16:47:04 -07:00
Ara c5f12b8dc6 Fixing banner to not show CLI release for windows users (#6942)
* Fixing banner to not show CLI release for windows users

* Fixing banner

* Fixing banner
2025-10-17 11:36:54 -07:00
celestial-vault b21ff1e44a remove unused ApiConfiguration proto message (#6941) 2025-10-17 11:11:30 -07:00
celestial-vault 2860ffe147 add auto approve option to interactive (#6937)
* add auto approve option to interactive

* removing redundant options

---------

Co-authored-by: pashpashpash <nik@nugbase.com>
2025-10-17 10:25:01 -07:00
Bee 2b25ef63b5 chore: remove unnecessary debug logs for cline env (#6960)
Remove unnecessary console.info debug statements from config methods and fix log message formatting. Add explicit "no-op" case to TelemetryProviderFactory for cleaner telemetry provider selection logic.
2025-10-17 09:59:20 -07:00
pashpashpash e70d60d5c4 fixing duplicate ask headers for tool approvals, and fixing ask statestream not waiting for partial=false (#6945) 2025-10-16 23:40:37 -07:00
Ara bddbea04ef fix: Disable subagents for jetbrains (#6933)
* fix: Disable subagents for jetbrains

* fix: Disable subagents for jetbrains
2025-10-16 21:15:40 -07:00
canvrno 176ccedb2a Updated banner to add that Linux is supported in Cline CLI (#6922)
* Updated banner to note Linux is supported in Cline CLI

* Updated settings for subagents to enable on Linux
2025-10-16 21:06:13 -07:00
pashpashpash 63276aba70 fixing bug where yolo mode didnt output body in plan mode (#6940)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-10-16 20:54:24 -07:00
CandiedUniverse ae0f2557fe Remove unneeded test that's slowing down the test suite (#6932) 2025-10-16 18:47:13 -07:00
celestial-vault 0d9909c80e partially update autoApprovalSettings (#6929)
* make autoApprovalSettings partially updatable

* typos

* make actions fields pointers since they are now optional
2025-10-16 15:17:53 -07:00
Juan Pablo Flores c0b4081a53 Enhance documentation: Add supported model providers and installation… (#6925)
* Enhance documentation: Add supported model providers and installation details for Cline CLI

* Refactor supported model providers list for clarity and consistency
2025-10-16 14:55:17 -07:00
Bee 0c0ba93a41 feat(config): add runtime environment switching support (#6621)
* refactor: convert config module to singleton class

Replaced functional config getters with a singleton ClineEndpoint class to improve encapsulation, enable dynamic environment updates, and eliminate static caching for better flexibility in configuration management. Updated import in cline provider accordingly.

* feat(config): add runtime environment switching support

Add ability to dynamically change Cline environment at runtime through settings:

- Add `cline_env` field to UpdateSettingsRequest proto message
- Refactor ClineEndpoint.setEnvironment() to accept string and parse environment
- Initialize environment with default value to prevent undefined state
- Add early return in constructor when valid environment is set
- Update updateSettings handler to process cline_env changes
- Replace direct clineEnvConfig usage with ClineEnv singleton pattern
- Ensure consistent environment access across auth and remote config modules

This enables users to switch between production, staging, and local environments without restarting the extension, improving developer experience and testing workflows.

* clean up

* Add trusted testers

* UpdateSettingsCli
2025-10-16 13:36:37 -07:00
Sarah Fortune 513c518d19 Add the cline provider to the remote config schema (#6908)
* Add the cline provider to the remote config schema

fixes PF-159

* Remove thinking budget

* Update unit tests for schema
2025-10-16 13:11:09 -07:00
pashpashpash 03d6561383 show cli version in banner instead of core version (#6915)
* v3.33.0 Release Notes (#6732)

- Added Cline CLI (Preview)
- Added Subagent support (Experimental)
- Added Multi-Root Workspaces support (Enable in feature settings)

---------

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

* show cli version in banner instead of core version

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-10-16 11:51:59 -07:00
Juan Pablo Flores 8f8c4561a6 Docs upgrade (#6907)
* style(docs): update background color scheme to neutral tones

Update documentation background colors from purple-tinted theme to neutral gray tones. Changed light mode from lavender (#F0E6FF) to off-white (#fafaf9) and dark mode from pure black (#000000) to dark gray (#0f0f0f) for improved visual consistency.

* refactor(docs): remove gradient decoration from theme config

Remove the "decoration": "gradient" property from the documentation
theme configuration. This simplifies the theme settings by removing
the gradient decoration option from the color configuration object.

* docs: change documentation font family to Geist Mono

Replace Roboto with Geist Mono as the default font family in the documentation configuration. This updates the visual styling of the documentation to use a monospace font, which may improve readability for code-heavy content.

* docs: update branding and restructure navigation

- Replace robot panel logos with new Cline brand logos
- Add icons to navbar links (Docs, GitHub, Discord)
- Restructure navigation from groups to tabs format
- Add icons to navigation items for improved UX
- Include new Docs link in navbar with book icon

This update modernizes the documentation appearance and improves navigation hierarchy for better user experience.

* docs: restructure navigation with hierarchical groups and pages

Restructured documentation navigation from flat menu to organized groups:
- Removed redundant "Docs" link from navbar
- Migrated from "menu" to "groups/pages" structure
- Added comprehensive page organization with nested groups:
  * Introduction, Getting Started, Features
  * Prompting Skills, Cline's Tools, Enterprise Solutions
  * MCP Servers, Provider Configuration
- Organized features into logical subgroups (@ Mentions, Commands,
  Customization, Slash Commands)
- Improved documentation discoverability and hierarchy

This change provides better content organization and easier navigation
for users exploring different aspects of Cline documentation.

* docs: remove contextual options from documentation config

Remove the contextual configuration section containing the "copy" option from docs.json. This simplifies the documentation configuration by removing unused contextual menu options.

* docs(multiroot): improve workspace documentation with limitations and technical details

- Add important note about experimental limitations affecting Cline rules and checkpoints
- Add "How it works" section explaining automatic workspace detection and tracking
- Reorganize technical behavior section with detailed subsections for workspace detection, path resolution, and command execution
- Document workspace hint syntax for explicit file references (@workspaceName:path)
- Standardize heading capitalization to sentence case for consistency
- Improve overall content organization and clarity for better user understanding

This update provides users with clearer information about the multiroot feature's current state, its limitations, and how to effectively use workspace hints when working with multiple project folders.

* docs: restructure overview page with enhanced visual layout

- Convert plain markdown sections to CardGroup and Card components with icons
- Add tabbed interface for Plan & Act Mode explanation
- Update description from "development assistant" to "coding agent"
- Reorganize content for improved readability and visual hierarchy
- Enhance feature presentations with icon-based cards

Improves user experience by transforming the overview documentation into a more visually appealing and scannable format using modern documentation components.

* docs: improve installation guide with enhanced structure and UX

Restructure the Cline installation documentation to improve readability and user experience:

- Add prominent note highlighting 2-minute installation time
- Convert prerequisites into visual card components for better clarity
- Transform installation steps into structured Step components for easier following
- Add manual installation instructions for JetBrains IDEs
- Include feature compatibility accordion for JetBrains users
- Enhance visual hierarchy with improved component usage (CardGroup, Steps, Accordion)
- Simplify language and improve descriptions throughout

This makes the installation process clearer for new users and reduces friction during onboarding.

* style(docs): remove text opacity reduction for better readability

* docs: refactor model selection guide with visual step-by-step instructions

- Replace tab-based layout with linear step-by-step flow
- Add screenshots for each configuration step (config, provider, API, model)
- Reorganize content structure for improved clarity and user experience
- Add quickstart options and streamlined provider recommendations
- Improve navigation with visual aids to help users configure Cline faster

* docs: add installation screenshots and context management guide

* docs: flatten provider config structure in documentation

Remove the "Alternative Providers" grouping and move all provider configuration pages (OpenRouter, Cerebras, DeepSeek, Groq, xAI Grok, Mistral AI, Doubao, Fireworks, and ZAI) to the main provider configuration list. This simplifies the documentation navigation by treating all providers equally rather than categorizing some as alternatives.

* docs: restructure context management docs and improve content clarity

**Changes:**
- Reorganized documentation structure by moving context management from
  `/best-practices` to `/prompting` section for better categorization
- Added URL redirect to maintain backward compatibility for old links
- Updated navigation references in welcome page to point to new location
- Improved readability of context management explanations with more
  narrative, conversational prose
- Enhanced context window documentation by adding cache tokens indicator
  and using emoji-based formatting for better visual clarity
- Streamlined Cline Memory Bank setup instructions from 4 to 3 steps
- Updated context bar screenshot to use newer image asset

**Why:**
Better documentation organization and improved user experience through
clearer explanations of how Cline builds and manages context during tasks.

* docs(context-management): convert Quick Reference to Info component

Replace blockquote formatting with Info component for the Quick Reference
section in the context management documentation. This improves visual
presentation and maintains consistency with documentation standards.

Also removes trailing whitespace at the end of the file for cleaner
formatting.

* docs: add Cline Enterprise overview and restructure enterprise section

- Add comprehensive enterprise overview documentation covering security,
  governance, observability, and developer experience features
- Rename "Enterprise & Security" navigation group to "Enterprise"
- Consolidate enterprise documentation by replacing 4 pages with 2:
  new overview page and security concerns
- Document BYOI (Bring Your Own Inference), SSO authentication, and
  role-based access control capabilities

This restructuring provides a clearer entry point for enterprise users
and consolidates previously scattered enterprise information into a
cohesive overview document.

* docs(enterprise): streamline enterprise overview and update font

- Change documentation font from Geist Mono to Geist Sans
- Add enterprise website link card for detailed information
- Remove Developer Experience, Proven at Scale, and Pricing sections
- Consolidate Flexible Inference section content
- Simplify enterprise overview to focus on core capabilities

These changes reduce redundancy by directing users to the enterprise
website for pricing and detailed features while keeping the docs
focused on technical implementation and core capabilities.

* clean-images

* Update docs/getting-started/installing-cline.mdx

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

* Update docs/styles.css

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

* docs(cline-cli): add platform availability warning to overview

Add a prominent warning callout indicating that Cline CLI is currently in preview and only supports macOS and Linux, with Windows support coming soon. This sets clear expectations for users about platform compatibility.

Also remove redundant introductory text in the "What you can build with this" section to improve content clarity.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 11:51:20 -07:00
pashpashpash 45d751b913 auto update for cli root command (#6914)
* v3.33.0 Release Notes (#6732)

- Added Cline CLI (Preview)
- Added Subagent support (Experimental)
- Added Multi-Root Workspaces support (Enable in feature settings)

---------

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

* auto update for cli root command

* moving to data dir

* auto update

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-10-16 11:44:46 -07:00
CandiedUniverse 915259ca96 Oops. Putting back a small detail that I accidentally removed. (#6920) 2025-10-16 11:04:36 -07:00
CandiedUniverse 46ef8b10b0 🪝Hooks: TaskStart hook [ENG-1001] (#6895)
* feat(hooks): Implement TaskStart hook

* feat(hooks): Change as per code review feedback from ellipsis-dev

* feat(hooks): Fix implementation from manual testing
2025-10-16 10:42:28 -07:00
Saoud Rizwan e4e07fc0d3 v3.33.1 Release Notes 2025-10-16 10:15:00 -07:00
Ara 738e959030 fix: Copy link for CLI installation (#6919) 2025-10-16 10:12:59 -07:00
Chris Sells 16e1c02b98 add cli docs (#6836)
* ready for review

* WIP: late-breaking cli arg change fix-ups (still more to double-check)

* updates for late-breaking CLI changes

* updated docs to match yesterday's usage updates

* docs(cli): restructure documentation and add dedicated installation guide

- Extract installation instructions into separate installation.mdx page
- Simplify overview.mdx to focus on use cases and getting started
- Improve cli-reference.mdx with quick help commands section
- Reorganize content for better information architecture
- Make documentation more user-friendly and action-oriented

This restructuring separates concerns: installation details are now in their
own page, the overview focuses on what Cline CLI can do, and the reference
page is more accessible with inline help examples before the full manual.

---------

Co-authored-by: Juan Pablo <juan@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-10-16 10:07:11 -07:00
github-actions[bot] 9e3c3982ec v3.33.0 Release Notes (#6732)
- Added Cline CLI (Preview)
- Added Subagent support (Experimental)
- Added Multi-Root Workspaces support (Enable in feature settings)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-10-16 01:24:59 -07:00
Saoud Rizwan 2ed5ce9b15 fix: new terminal design showing incorrect states when running in background; use expanded state style by default 2025-10-16 01:24:59 -07:00
Saoud Rizwan 7dceaea056 fix: show auth command suggestion in subagents setting 2025-10-15 23:15:53 -07:00
Saoud Rizwan b351a8b92b feat: add banner to install Cline for CLI and experimental subagents feature (#6782)
* feat: add banner to install Cline for CLI and experimental subagents feature

* Update snapshots

* Fix copy

* Style fixes

* A few small changes to cli detection, prompting, cleanup

* Prompting tweaks and npm command update

* Prompting, bug fix

* Prompt changes around command syntax

* Command parsing and flag substitution for easier subagent invocation

* Added terminal output slider setting for subagents

* Improved CLI subagent settings injection

* Added max_consecutive_mistakes setting and cli flag

* Terminal line limit added to enchanced terminal, prompting and settings tweaks

* feat: Add OpenTelemetry settings schema and state infrastructure (1/5) (#6826)

* feat: Add OpenTelemetry settings schema and state infrastructure (1/5)

- Add 16 OpenTelemetry configuration fields to Settings interface
- Add 17 OpenTelemetry fields to RemoteConfig schema
- Add state persistence helpers for OpenTelemetry settings
- Foundation for dynamic OpenTelemetry configuration

Part 1 of 5 in the telemetry settings refactor series.

* chore: add changeset for OpenTelemetry schema

* fix: Address PR review feedback for OpenTelemetry settings

- Remove | undefined from 8 OpenTelemetry fields with default values
- Add default values in state-helpers.ts for all non-optional fields
- Add OpenTelemetry field mappings to remote-config/utils.ts
- Add comprehensive test coverage for OpenTelemetry fields in schema.test.ts
- Update changeset terminology from 'Otel' to 'OpenTelemetry'

Addresses feedback from:
- sjf: Remote config transformation and test coverage
- celestial-vault: Type cleanup and default values
- Copilot: Terminology improvement

* Prompting changes

* Fixed system prompt empty sections issue, fixed rebase mistake

* Added telemetry for CLI subagent use in IDEs

* Platform aware banner, fixed settings layout issues

* Post rebase fixes & changes to accomodate new terminal UI

* Cline Icon SVG in ChatRow - WIP

* Fixed outputLine limit issue

* Fixed rebase merge conflict remnant

* Rebase fix

* Cleanup and changes to instructions for new CLI users

* Added CLI documentation link

* Small prompt adjustments

* One more bullet point

* Updated remaining npm install command

* Updated subagent command format for CLI release spec

* Added check to prevent subagents from getting  subagent prompt

* Added subagent slash command

* Apply suggestion from @ellipsis-dev[bot]

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

* Remove workdir

* Update src/core/prompts/system-prompt/variants/xs/overrides.ts

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

* removed logging

* Updated expected version output

* Removed checkCliInstallation check in getStateToPostToWebview

* removed more logging

* fix: force subagents to use terminal stuff

---------

Co-authored-by: Kevin Bond <kevin@cline.bot>
Co-authored-by: Daniel Steigman <35793213+NightTrek@users.noreply.github.com>
Co-authored-by: Andrei Eternal <206184+Garoth@users.noreply.github.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: canvrno <46584286+canvrno@users.noreply.github.com>
Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2025-10-15 22:10:08 -07:00
pashpashpash 78c3664bf3 cliversion in hostbridge env implementation (#6912) 2025-10-15 21:43:42 -07:00
Andrei Eternal ec543a230f make cli version built in rather than reading the package.json at runtime (#6910)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-10-15 21:31:53 -07:00
pashpashpash 43fabaab8e approval with feedback fix (#6911) 2025-10-15 21:03:51 -07:00
celestial-vault 86c526b029 fetch all remote configs and lock user to remote config org (#6902)
Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2025-10-15 19:33:27 -07:00
Saoud Rizwan 7162b26430 Fix Claude Sonnet 4 support on Vertex (#6904) 2025-10-15 19:14:29 -07:00
Daniel Steigman e9eb7ae179 fix(cli): Add telemetry settings support to Go host bridge (#6906)
* build: add npm package build script with telemetry injection

Add a new build script that automates the NPM package creation process with proper telemetry key injection. The script:

- Validates required environment variables (TELEMETRY_SERVICE_API_KEY, ERROR_SERVICE_API_KEY)
- Verifies Node.js can access environment variables
- Builds Go CLI binaries for all platforms
- Compiles standalone package with esbuild
- Verifies telemetry keys are properly injected into compiled code
- Provides colored output and detailed error messages

Added npm script `build:npm` to package.json for easy invocation.

This ensures consistent builds with telemetry properly configured for production deployments.

* feat(hostbridge): add telemetry settings support for CLI mode

Add GetTelemetrySettings and SubscribeToTelemetrySettings methods to EnvService to handle telemetry configuration in CLI mode.

- GetTelemetrySettings retrieves telemetry status from POSTHOG_TELEMETRY_ENABLED environment variable
- SubscribeToTelemetrySettings provides a stream for telemetry setting updates, sending initial state and keeping stream open
- In CLI mode, telemetry settings are static and determined by environment variable at startup

This enables proper telemetry control and monitoring in CLI environments.
2025-10-15 19:13:39 -07:00
Andrei Eternal 546e7002e1 improve auth docs to say it configures models too, from influencer feedback (#6905)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-10-15 19:04:56 -07:00
Andrei Eternal fadc961d8f Remove --workdir / -w flag (wasnt completed) (#6903)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-10-15 18:50:43 -07:00
Andrei Eternal 958801e8a8 fix 10s wait when using cline instance kill -a (#6901)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-10-15 18:40:56 -07:00
Andrei Eternal 5152b970c0 fix version output with cli ver + core ver (#6899)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-10-15 18:39:13 -07:00
CandiedUniverse 947996b02e 🪝Hooks: UserPromptSubmit hook [ENG-1000] (#6893)
* feat(hooks): Implement UserPromptSubmit hook

* feat(hooks): Add tests for UserPromptSubmit hook

* feat(hooks): Add fixture-based tests for UserPromptSubmit

* feat(hooks): More UserPromptSubmit tests

* feat(hooks): Change as per ellipsis-dev code review feedback on the PR

* Apply the complete hooks.proto and hook-factory.ts changes
2025-10-15 18:38:05 -07:00
Andrei Eternal 0a25484ea0 ensure t v / t v -f / t c output an error if no task is active (#6894)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-10-15 17:44:17 -07:00
Andrei Eternal 91c5434b5e reference man page in cline --help (#6897)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-10-15 17:40:14 -07:00
Sarah Fortune 6f59480b79 Remove duplicated log (#6869) 2025-10-15 15:51:07 -07:00
celestial-vault e42f0a9aea remote url type from vscode text field (#6890)
Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2025-10-15 14:39:39 -07:00
Ara 385ac33623 fix: add excluded standalone file needed for terminal runs (#6892) 2025-10-15 14:21:07 -07:00
Sarah Fortune 6b963c243a Update remote config schema (#6867)
* Update remote config schema

Update the schema to make models field optional, so we can use undefined to mean unset like the other fields.

* Make the OpenAI headers an otional field

* Update src/shared/remote-config/__tests__/schema.test.ts

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-10-15 14:07:13 -07:00
Saoud Rizwan 66a4eb0f8e hotfix: Add Claude Haiku 4.5 support (#6889)
* Add Claude Haiku 4.5 support

* Fix Claude Haiku 4.5 outputting "<function_calls>"

* v3.32.8 Release Notes
2025-10-15 13:03:04 -07:00
canvrno 9a7c6ed201 CLI Subagents - settings & telemetry framework (#6888)
* Added new settings for future subagent PR

* Updated test

* Update src/integrations/terminal/TerminalManager.ts

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-10-15 12:49:11 -07:00
CandiedUniverse c7143a627a Initial global clinerules dir implementation (#6846) 2025-10-15 12:18:09 -07:00
CandiedUniverse b09751b841 feat(hooks): Implement test fixtures (#6862) 2025-10-15 11:42:32 -07:00
pashpashpash 7c51236aef approval hints in task view (#6887)
* approval hints in task view

* more descriptive
2025-10-15 08:42:51 -07:00
pashpashpash f47d07784a clean exits (#6886)
* clean exits

* cleanup
2025-10-15 08:37:51 -07:00
pashpashpash 65e5f0feb7 fixing control c cancel task reliably (#6885) 2025-10-15 08:37:42 -07:00
Tomás Barreiro b2e3e9b3f9 Allow package secrets when publishing the nightly release (#6884) 2025-10-15 07:38:06 -07:00
pashpashpash 7be84fd5bf cli banner (#6882)
* banner

* banner

* nice

* side by side

* more color alignment

* preview

* nicer
2025-10-15 06:13:15 -07:00
pashpashpash 43683440dc cli polish round 3 (#6880)
* colors and bold

* matching colors for plan act

* dark mode for all
2025-10-15 04:11:27 -07:00
Andrei Eternal 153e24b94a fix ctrl-c in task view. should just disconnect the view (#6876)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-10-15 03:40:12 -07:00
pashpashpash 65d46a3691 setting input width to 46 to minimize resize issues (#6879)
* setting input width to 48 to minimize resize issues

* fixing typo

* text

* 46 instead of 48
2025-10-15 03:38:49 -07:00
pashpashpash aa2cbc39a4 bubbles (#6871)
* bubbles

* making the input look nicer

* okay nice - clearing properly

* not allowing input while streaming command output

* better placeholder text

* way better resize handling
2025-10-15 03:02:25 -07:00
Andrei Eternal 689e7f0e13 fix piping stdin into cline (#6874)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-10-15 01:16:48 -07:00
Andrei Eternal 3c3188073b Fix: cline provider auth should print url in case it doesn't auto-open (#6873)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-10-15 00:38:47 -07:00
Andrei Eternal 8e3ee11966 Man page for cline command, and build system to do it (#6870)
* cline manpage

* completed man cline

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-10-14 23:14:26 -07:00
Ara 4d525e065c Adding Terminal background process (#6598)
* adding terminal Background process

* Adding match making blog

* Adding match making blog

* fixing colors

* fixing cancel

* Fixing: Ripgrep download for integration tests

* Fixing: Ripgrep download for integration tests

* fixing animation

* fixing animation

* fixing animation

* fixing animation

* fixing animation

* make theme aware

* make theme aware

* feat(cli): fixing cancel command

* feat(cli): fixing cancel command

* fix: minor nits

* fix: remove logs

* fix: remove logs

* fix: remove logs

* fix: remove logs

* fix: remove logs

* fix: standalone mode
2025-10-14 23:07:28 -07:00
celestial-vault 5832ca4792 add task settings rpc (#6866) 2025-10-14 21:49:25 -07:00
celestial-vault d85fea15c9 fix telemetry toggle not displaying properly (#6832) 2025-10-14 21:27:02 -07:00
Andrei Eternal 9be3fa5acf NPM install for cline (#6861)
* WIP npm publish setup

* verbose startup + error if cline core not found

* working npm release

* modifications for linux npm package to work

* remove publish npm workflow for now

* readme & package.json tweaks

* fix old reference to compile-standalone-cli in test workflow

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-10-14 20:54:50 -07:00
Toshii e76d9527c8 finalizing the cline task view commands based on spec (#6864)
* updating the cline task view commands

* remove unused function
2025-10-14 20:53:21 -07:00
celestial-vault e8b8ec4f05 bypass auto-approval count for yolo mode (#6859) 2025-10-14 19:06:39 -07:00
canvrno 664ec1e0ac Folder, Task, and Checkpoints locking in cline-core (#6823) 2025-10-14 18:27:51 -07:00
Toshii e34c62ade9 add -o to top level cline command, and remove oneshot command (#6860) 2025-10-14 17:49:42 -07:00
Toshii ffabde6985 rename instance use to instance default and add the --default flag to instance new (#6851)
* updating instance use to instance default

* adding --default flag
2025-10-14 16:51:44 -07:00
Daniel Steigman a8c62dddc6 feat: Add OpenTelemetry settings schema and state infrastructure (1/5) (#6826)
* feat: Add OpenTelemetry settings schema and state infrastructure (1/5)

- Add 16 OpenTelemetry configuration fields to Settings interface
- Add 17 OpenTelemetry fields to RemoteConfig schema
- Add state persistence helpers for OpenTelemetry settings
- Foundation for dynamic OpenTelemetry configuration

Part 1 of 5 in the telemetry settings refactor series.

* chore: add changeset for OpenTelemetry schema

* fix: Address PR review feedback for OpenTelemetry settings

- Remove | undefined from 8 OpenTelemetry fields with default values
- Add default values in state-helpers.ts for all non-optional fields
- Add OpenTelemetry field mappings to remote-config/utils.ts
- Add comprehensive test coverage for OpenTelemetry fields in schema.test.ts
- Update changeset terminology from 'Otel' to 'OpenTelemetry'

Addresses feedback from:
- sjf: Remote config transformation and test coverage
- celestial-vault: Type cleanup and default values
- Copilot: Terminology improvement
2025-10-14 16:27:41 -07:00
Toshii 2b5d6e5d0e add instance check to cline task new and full yolo flags (#6844) 2025-10-14 16:02:57 -07:00
Toshii 0440898584 rename task follow to task chat, remove top level cline send (#6845)
* removing cline send command

* rename task follow to task chat
2025-10-14 16:02:41 -07:00
Toshii 3288defb67 finalize cline task send according to spec - adds yolo flag and instance check (#6843)
* adding the yolo flags

* do not create an instance on send, if one doesn't already exist
2025-10-14 15:24:50 -07:00
CandiedUniverse ec6daccf5f Hooks: Improving tests (step 1) [ENG-989, ENG-990] (#6831)
* feat(hooks): Add comprehensive testing utilities for hooks system

Introduces reusable testing infrastructure to reduce code duplication
and improve test maintainability across the hooks system.

New Utilities:
- setupHookTests(): Standard test environment with automatic cleanup
- createTestHook(): Platform-agnostic hook creation
- buildPreToolUseInput() / buildPostToolUseInput(): Type-safe input builders
- assertHookOutput(): Consistent assertion helper
- MockHookRunner: Fast mock for integration tests without process spawning
- loadFixture(): Fixture loading and platform handling

Benefits:
- Eliminates ~300+ lines of duplicated setup code
- Provides consistent patterns across test files
- Enables platform-neutral testing (Unix/Windows)
- Supports both unit tests (real execution) and integration tests (mocks)

These utilities will be used in subsequent PRs to refactor existing
tests and add comprehensive error scenario coverage.

Related: Part 1 of hooks testing infrastructure improvements

* feat(hooks): Update tests to reflect future plans for Windows implementation
2025-10-14 15:20:34 -07:00
pashpashpash e399a70bd0 terminal-line-wrapping (#6842) 2025-10-14 15:17:36 -07:00
Toshii b52edae01e updated cline task open to include settings, yolo, and mode flags (#6840) 2025-10-14 14:47:39 -07:00
Toshii 467838ac4c adding to cline task send separate --approve and --deny flags (#6838)
* added new approve and deny, separate flags

* adding shorthand for approve and deny flags
2025-10-14 14:46:51 -07:00
Toshii 9be95f2401 updating cline task cancel to pause (#6837) 2025-10-14 14:46:28 -07:00
Ara 814988c929 feat(cli): add local installation script with improved build process (#6834) 2025-10-14 12:45:07 -07:00
Ara 7b7a006123 feat(cli): Fixes CLI authentication redirect error (#6835)
* fix: redirect for CLI after login

* fix: redirect for CLI after login
2025-10-14 12:42:59 -07:00
Juan Pablo Flores 08286a8465 docs(multiroot-workspace): enhance documentation with limitations and… (#6822)
* docs(multiroot-workspace): enhance documentation with limitations and technical details

- Add important note about experimental limitations (Cline rules and checkpoints)
- Expand technical behavior section with workspace detection, path resolution, and command execution details
- Document workspace hints syntax (@workspaceName:path/to/file) for explicit file referencing
- Reorganize content with improved section structure and "How it works" overview
- Normalize heading capitalization for consistency
- Remove outdated experimental date marker

These changes provide users with clearer understanding of multiroot workspace functionality, current limitations, and advanced features like workspace hints for precise file targeting across multiple project folders.

* Update docs/features/multiroot-workspace.mdx

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-14 11:54:47 -07:00
Saoud Rizwan d252c84a04 fix: reasoning_details not consolidated for how openrouter expects in requests (#6772)
* fix: reasoning_details not consolidated for how openrouter expects in requests

* Fix openai models getting different shape for ReasoningDetail with encrypted data

* Show richer error

* Only use last encrypted reasoning chunk for openai format reasoning details preservation

* Fix tests
2025-10-14 10:52:59 -07:00
Remy495 9cd2729d47 Include gpt-5 in reasoning models in openai.ts (#6450)
* Updated openai.ts

Updated openai.ts to passdown reasoning effort parameter for GPT-5 (only reasoning supported models)

* Added GPT-5 for Reasoning Family. Added changeset
2025-10-14 10:12:31 -07:00
celestial-vault bb94a572ac lock down org switching if remote config is detected (#6818)
Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2025-10-14 09:56:57 -07:00
John Costa a7ec39270b Requesty base URL cannot be unchecked (#6804)
* fix: changing base url to undefined when user unselected base url checkbox

* chore: change set
2025-10-14 02:52:16 -07:00
Toshii 600bcab19a updating the task list to read from disk and not use an instance (#6817) 2025-10-13 23:24:07 -07:00
AJ Juaire 85bf52036a Add Bedrock defaultUserAgentProvider for Cline version identification (#6821)
* Add Bedrock defaultUserAgentProvider for Cline version identification

* Use ExtensionRegistryInfo.version instead
2025-10-13 21:10:52 -07:00
Toshii 5e768aceae using a ephemeral instance for cline auth rather than default (#6819)
* updating cline auth to spawn a new instance just for auth, then close it once complete

* using contextKey for ctx
2025-10-13 20:52:24 -07:00
Sarah Fortune c166d36788 Fix deprecation warning (#6820) 2025-10-13 20:24:15 -07:00
Sarah Fortune fc8517b52d Update flakey test for getOpenTabs (#6816)
From the output of this run, it looks like that tabs are not resetting properly in between test runs. Make the file names unique per test so this is easier to debug.
https://github.com/cline/cline/actions/runs/18479896861/job/52652420536#step:12:802
2025-10-13 16:53:35 -07:00
celestial-vault c2f98b6ed7 fetch remote config when switching accounts and when starting a task (#6815) 2025-10-13 16:15:19 -07:00
celestial-vault 98c84cdc8f add interval to fetch and set remote config (#6813)
* add interval to fetch and set remote config

* clear the remote config if the user goes from an org to a private account
2025-10-13 15:51:52 -07:00
Bee cdffc002eb feat(telemetry): add OpenTelemetry integration (#6605)
* feat: Modular telemetry architecture with Jitsu provider support

- Add dual-provider telemetry architecture supporting both Jitsu and PostHog
- Implement JitsuTelemetryProvider with full API compatibility
- Add required telemetry bypass for critical system health events
- Create modular event handler base class for future extensibility
- Add Jitsu configuration with environment variable controls
- Update TelemetryService to support multiple providers with error isolation
- Add .env.example template for development setup
- Maintain backward compatibility with existing PostHog integration
- Enable easy PostHog removal via POSTHOG_TELEMETRY_ENABLED=false
- Install dotenv for local development environment support

Key benefits:
- Dual tracking during transition period
- Error isolation between providers
- Memory efficient static method architecture
- Easy provider enable/disable via environment variables
- Wednesday deployment ready for Jitsu migration

* fix(build): Load environment variables from .env file during development builds

- Add dotenv.config() to esbuild.mjs to load .env variables
- Include all telemetry-related environment variables in build injection:
  - TELEMETRY_SERVICE_API_KEY (PostHog)
  - ERROR_SERVICE_API_KEY (PostHog error tracking)
  - JITSU_WRITE_KEY (Jitsu telemetry)
  - JITSU_HOST (Jitsu host URL)
  - JITSU_ENABLED (Jitsu provider control)
  - POSTHOG_TELEMETRY_ENABLED (PostHog provider control)

This ensures telemetry services work correctly in development builds
by properly injecting API keys and configuration from .env file.

Also updates TelemetryService tests to support multi-provider architecture.

* fix(telemetry): Replace Record<string, unknown> with proper JSON-serializable types

- Add TelemetryPrimitive, TelemetryValue, TelemetryObject, and TelemetryProperties types to ITelemetryProvider
- Update JitsuTelemetryProvider to use TelemetryProperties instead of Record<string, unknown>
- Update PostHogTelemetryProvider to use TelemetryProperties instead of Record<string, unknown>
- Update TelemetryService to use TelemetryProperties for type-safe telemetry data
- Ensures all telemetry properties are JSON-serializable, preventing runtime errors
- Fixes TypeScript compatibility issue between Jitsu's JSONObject type and Record<string, unknown>

* moved and organized the telemetry files and updated the example env file to be more descriptive

* refactor: remove Jitsu telemetry provider

- Remove Jitsu provider implementation and config files
- Remove Jitsu environment variables from .env.example
- Remove Jitsu build configuration from esbuild.mjs
- Update TelemetryProviderFactory to only support PostHog
- Uninstall @jitsu/js dependency
- Add .env to .gitignore to prevent committing local env files

* chore: add changeset for Jitsu removal

* removed jitsu

* fix: update import paths after PostHogClientProvider relocation

* fix: remove race condition in captureToProviders and reorganize PostHog providers

- Changed captureToProviders from async to synchronous method
- Removed unnecessary Promise.allSettled overhead since provider.log() and provider.logRequired() are synchronous
- Changed from .map() to .forEach() for better clarity
- Moved PostHog provider files into posthog/ subdirectory for better organization
- Updated all import paths to reflect new folder structure

* refactor(telemetry): remove unnecessary addProperties method and improve type safety

- Remove addProperties helper method that used 'any' types
- Replace with inline typed spread operations in capture(), captureRequired(), and identifyAccount()
- Fix type errors in captureConversationTurnEvent and captureBrowserError
- All telemetry properties now properly typed as TelemetryProperties
- Ensures OpenTelemetry compatibility through type system enforcement

* refactor: remove dotenv dependency and use launch.json envFile

- Remove dotenv import and config() call from esbuild.mjs
- Add envFile parameter to all launch.json configurations to load .env
- Remove dotenv from package.json devDependencies

Environment variables are now loaded via VSCode's envFile feature for local
development, while CI/production continues to inject via GitHub Actions.
This provides cleaner separation between build-time and runtime environment
handling.

* feat(telemetry): add browser telemetry properties and improve typing

- Add remoteBrowserHost and endpoint fields to browser telemetry events
- Replace generic Record<string, unknown> with TelemetryObject type in EventHandlerBase for better type safety
- Import TelemetryObject type from ITelemetryProvider

These changes enhance browser telemetry tracking capabilities and improve type consistency across the telemetry service.

* feat(telemetry): add OpenTelemetry integration

Add comprehensive OpenTelemetry support alongside existing PostHog telemetry:

- Add OpenTelemetry provider with metrics and logs/events support
- Support multiple exporters: console, OTLP (gRPC/HTTP/Protobuf), and Prometheus
- Implement flexible configuration via environment variables
- Add detailed .env.example documentation with usage examples
- Integrate with existing telemetry infrastructure via TelemetryClient
- Support independent or parallel operation with PostHog
- Add proper attribute flattening for OpenTelemetry primitives
- Include configurable export intervals and protocols

This enables users to export telemetry data to any OpenTelemetry-compatible
backend (Grafana, Jaeger, etc.) while maintaining backward compatibility
with PostHog integration.

* add changeset

* Update packages

* .vscodeignore

* fixed type error

* fix(telemetry): Fix OpenTelemetry gRPC exporter endpoint format

- Strip http:// prefix from gRPC endpoints (gRPC requires 'localhost:4317' not 'http://localhost:4317')
- Clean up debug logging from OpenTelemetry provider classes
- Add helpful comment to .env.example about gRPC endpoint format

This fixes the issue where metrics were being recorded in-memory but silently
failing to export to the OpenTelemetry collector. Metrics now flow end-to-end
from the extension through the collector to Prometheus.

Verified working with test infrastructure at ~/code/@cline/cline-otel-testing

* merged from main and handled conflcits

* fix: ensure exportTimeoutMillis is less than exportIntervalMillis in OpenTelemetry metrics

Changed the timeout calculation to dynamically compute as 80% of the export interval,
capped at 30 seconds. This fixes the error: 'exportIntervalMillis must be greater than
or equal to exportTimeoutMillis' that occurred when the configured interval was less
than 30 seconds.

* feat(otel): add insecure gRPC connection support for development

- Add OTEL_EXPORTER_OTLP_INSECURE config option
- Support insecure (non-TLS) gRPC connections for local testing
- Update OpenTelemetryClientProvider to use grpcCredentials.createInsecure()
- Add comprehensive debug logging for troubleshooting
- Tested and validated with local OTel collector

This enables testing of OTLP gRPC protocol without TLS certificates,
useful for local development and testing environments.

* feat(otel): add comprehensive debug logging for troubleshooting

- Add configuration summary logging at initialization
- Log all exporter creation steps with success/failure status
- Log connection details (protocol, endpoint, insecure mode)
- Log header presence (keys only, not values for security)
- Add try-catch blocks around exporter creation with error logging
- Log reader/processor counts for validation
- Improve visibility for TLS handshake and authentication issues

* test: validate HTTP/Protobuf protocol with path appending fix

- Tested HTTP/Protobuf exporter with binary encoding
- Confirmed path appending fix works for /v1/metrics and /v1/logs
- Validated bearer token authentication over HTTP/Protobuf
- All exports successful with complete data fidelity
- Documented test results in scenario-5-http-protobuf.md

Test Status:  PASSED - HTTP/Protobuf production ready

* pre-cleanup

* refactor(telemetry): clean up OpenTelemetry provider architecture

Major refactoring to improve code quality, maintainability, and align with domain-driven design principles:

**Architecture Improvements:**
- Created OpenTelemetryExporterFactory with pure functions for exporter creation
- Extracted exporter logic from OpenTelemetryClientProvider into factory
- Removed Prometheus support (not a requirement)
- Simplified diagnostic logging with minimal wrapper gated by TEL_DEBUG_DIAGNOSTICS flag

**Interface & Provider Updates:**
- Extended ITelemetryProvider with optional incrementCounter() and recordHistogram() methods
- No OpenTelemetry types leak into provider interface (provider-agnostic)
- Implemented no-op metric stubs in PostHogTelemetryProvider
- Removed eventCounter from OpenTelemetryTelemetryProvider (was incorrectly tracking events as metrics)
- Added lazy counter/histogram creation with Map caches in OpenTelemetry provider
- Logs are now the primary telemetry path, metrics are optional/future-ready

**Code Quality:**
- ~50% reduction in complexity through factory pattern
- Clear separation of concerns between interface, implementation, client management, and exporter creation
- Improved testability with pure functions and lazy instrument creation
- Better maintainability with cleaner code structure

**Configuration:**
- Updated .env.example with comprehensive OpenTelemetry documentation
- Added TEL_DEBUG_DIAGNOSTICS flag for enabling diagnostic logging
- Clarified all configuration options with detailed comments
- Removed Prometheus references

**Verified Working:**
- All protocols tested and working: gRPC, HTTP/JSON, HTTP/Protobuf
- Bearer token authentication validated
- Console exporter functional
- Maintains full compatibility with TelemetryService interface

* OTel: make flattenProperties circular-safe with depth guard and array truncation

Use WeakSet to detect circular references; add MAX_DEPTH=10; limit arrays to 100 items with _truncated and _original_length flags; handle Date via toISOString and Error via message; skip __proto__, constructor, prototype keys; wrap JSON.stringify in try/catch.

* security: restrict sensitive OTel logging to debug mode only

Only log OTLP endpoints and header information when TEL_DEBUG_DIAGNOSTICS=true or IS_DEV=true. In production mode, only show whether these values are configured without exposing actual values. This prevents sensitive infrastructure details and authentication information from appearing in production logs.

* removed debug logging from non debug mode

* feat: add batch configuration for OpenTelemetry log processor

Add configurable batch settings for BatchLogRecordProcessor to allow tuning for different use cases:

- OTEL_LOG_BATCH_SIZE: Maximum logs per batch (default: 512)
- OTEL_LOG_BATCH_TIMEOUT: Maximum wait time in ms (default: 5000)
- OTEL_LOG_MAX_QUEUE_SIZE: Maximum queue size (default: 2048)

Benefits:
- High-volume scenarios can increase queue size to prevent dropped events
- Real-time monitoring can reduce timeout for faster exports
- Low-volume scenarios can reduce batch size to minimize delays

All settings are optional with sensible defaults matching OpenTelemetry SDK standards. Configuration is validated to ensure positive values.

* feat(telemetry): add build-time OpenTelemetry environment variable injection

Add support for injecting OpenTelemetry configuration at build time from
GitHub Actions secrets, following the same pattern as PostHog telemetry.
This enables production builds to have default OpenTelemetry collector
configuration while still allowing runtime overrides.

Changes:

1. esbuild.mjs:
   - Added build-time injection for 7 OpenTelemetry environment variables:
     * OTEL_TELEMETRY_ENABLED - Enable/disable OpenTelemetry
     * OTEL_LOGS_EXPORTER - Logs exporter type (console/otlp)
     * OTEL_METRICS_EXPORTER - Metrics exporter type (console/otlp)
     * OTEL_EXPORTER_OTLP_PROTOCOL - OTLP protocol (grpc/http/json/http/protobuf)
     * OTEL_EXPORTER_OTLP_ENDPOINT - Collector endpoint URL
     * OTEL_EXPORTER_OTLP_HEADERS - Authentication headers (e.g., bearer tokens)
     * OTEL_METRIC_EXPORT_INTERVAL - Metric export interval in milliseconds
   - Variables are read from process.env at build time and injected into
     the bundle via esbuild's define option
   - Follows exact same pattern as existing PostHog API key injection

2. .github/workflows/publish.yml:
   - Added OpenTelemetry environment variables to 'Package and Publish Extension' step
   - Variables are populated from GitHub Actions secrets
   - Applied to both release and pre-release builds

3. .github/workflows/publish-nightly.yml:
   - Added same OpenTelemetry environment variables to nightly builds
   - Ensures consistent configuration across all build types

How it works:

- Build Time (Production):
  * GitHub Actions reads secrets and sets environment variables
  * esbuild.mjs injects these values into the bundled code
  * Production builds ship with default OpenTelemetry configuration

- Runtime (Development):
  * Developers use .env file with their own configuration
  * No changes needed to existing development workflow

- Runtime (Production):
  * Users can override build-time defaults by setting environment variables
  * Runtime values take complete precedence over build-time defaults
  * Enterprise users can point to their own collectors

Next steps:
- Add GitHub secrets to repository (Settings → Secrets and variables → Actions)
- Required secrets: OTEL_TELEMETRY_ENABLED, OTEL_LOGS_EXPORTER,
  OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_ENDPOINT,
  OTEL_EXPORTER_OTLP_HEADERS
- Optional secrets: OTEL_METRICS_EXPORTER, OTEL_METRIC_EXPORT_INTERVAL

Benefits:
- Consistent with existing PostHog telemetry pattern
- Secure: production secrets stay in GitHub, not in code
- Flexible: users can override defaults at runtime
- Development-friendly: .env file continues to work as before
- Production-ready: default collector configuration for all users

* removed ai slop

* updated lock file

* fix: use ExtensionRegistryInfo.version for cross-platform compatibility

Replace process.env.npm_package_version with ExtensionRegistryInfo.version
in OpenTelemetry service version to ensure compatibility across VSCode,
JetBrains, and CLI environments.

Addresses PR #6605 inline comment from Sarah Fortune (sjf)

* fix: restore package-lock.json with proper biome dependencies

Fixes CI test failures caused by corrupted biome package entries.
Restores package-lock.json from main and reinstalls to properly
update OpenTelemetry dependencies while preserving biome integrity.

Addresses PR #6605 comment from Sarah Fortune (sjf) about test failures

---------

Co-authored-by: NightTrek <Daniels@dual4t.com>
Co-authored-by: Daniel Steigman <35793213+NightTrek@users.noreply.github.com>
2025-10-13 15:49:48 -07:00
Toshii 3dc09d698d adding to CheckSendEnabled a check for if there is a curren task (#6812) 2025-10-13 15:41:16 -07:00
Ara c04e2185f9 Fixing: Ripgrep download for integration tests (#6810)
Co-authored-by: CandiedUniverse <132302818+candieduniverse@users.noreply.github.com>
2025-10-13 14:34:47 -07:00
Daniel Steigman 3846e3fca0 Remove MULTI_ROOT_WORKSPACE feature flag (#6808)
* Remove MULTI_ROOT_WORKSPACE feature flag

The multi-root workspace feature is now rolled out to 100% of users,
so the feature flag is no longer needed.

Changes:
- Removed MULTI_ROOT_WORKSPACE from FeatureFlag enum
- Removed getMultiRootEnabled() method from FeatureFlagsService
- Updated isMultiRootEnabled() to only check user setting
- Set multiRootSetting.featureFlag to true (always enabled)
- Updated tests to remove feature flag stubs

* Add changeset for multi-root feature flag removal
2025-10-13 12:20:24 -07:00
Igor Tceglevskii 663e75203b feat: add environment-based visual indicators to UI (#6777) 2025-10-13 11:46:26 -07:00
pashpashpash c5d153551c fixing terminalapp rendering of input forms (#6807) 2025-10-13 11:40:46 -07:00
pashpashpash 1553611dbc added ripgrep download to github action workflow (#6806) 2025-10-13 10:40:28 -07:00
pashpashpash 64bd618779 version v alias (#6801) 2025-10-13 10:26:50 -07:00
pashpashpash fc32061c35 packaging ripgrep cli (#6805) 2025-10-13 10:16:53 -07:00
pashpashpash aeab0d25be onboarding cli + log verbosity cleanup + fixing bug with default instances (#6787)
* starting instance before anything else

* auth wizard from root if no credentials

* removing debug logs

* better design

* moving more things to verbose flag

* ensuring that when a new cline instance is started and there are no others, that it is set as default

* auth wizard should always have an instance at the beginning with ensuredefaultinstance

* setting welcome state to true when user inputs credentials
2025-10-13 09:44:29 -07:00
Igor Tceglevskii d19c043b14 environment override (#6784) 2025-10-13 09:21:41 -07:00
pashpashpash b5fde0adbf added instance list support for jetbrains and kill all (#6800)
* added instance list support for jetbrains and kill all

* copilot reviews
2025-10-13 00:59:59 -07:00
Daniel Steigman 7135fe4c49 feat: Add version information injection to CLI build script (#6780)
* feat: add version information injection to CLI build script

- Extract version from package.json
- Capture git commit hash, build date, and builder info
- Inject version info into CLI binaries via Go ldflags
- Update both cline and cline-host builds with version data

* chore: add changeset for CLI version injection
2025-10-13 00:18:18 -07:00
pashpashpash 1241a2fce2 some chill cleanup (#6799)
* some chill cleanup

* more cleaning
2025-10-12 23:29:35 -07:00
celestial-vault 0957e86046 remote config disable ui (#6794)
* pass remote config state to frontend and disable corresponding UI components

* move state-keys to shared folder

* fix field potentially undefined type error

* move workspace types to shared folder

* fix url validation runtime error

* add lock and tooltip to api provider dropdown and remove filtering
2025-10-12 22:33:51 -07:00
pashpashpash c7712e69d2 org selection in auth wizard in cli (#6798) 2025-10-12 22:14:41 -07:00
pashpashpash e56490106c Pashpashpash/error handling cli (#6797)
* error handling in cli

* error display
2025-10-12 22:14:24 -07:00
Alex Ker 2fce6ef194 Baseten provider Kimi K2 0711, Llama 4 Maverick and Llama 4 Scout Model APIs deprecation (#6681)
* model api deprecation

* changeset

---------

Co-authored-by: AlexKer <AlexKer@users.noreply.github.com>
2025-10-12 20:48:03 -07:00
Toshii b50c2db8b8 add instructions for using summarize task in plan and act modes (#6793) 2025-10-12 18:20:15 -07:00
pashpashpash d0da0b22db logging and cleanup (#6790) 2025-10-13 01:03:05 +00:00
Toshii cc0c9560be add setting deleted range to taskState in checkpoint object (#6788) 2025-10-12 17:33:25 -07:00
pashpashpash 844ecdee38 normie friendly root command (#6778)
* normie friendly root command

* control c logic

* prompt to cline root works

* ask question tool streaming

* simplified & unified tool handling and approval messages, and supporting edge case with attempt completion calling terminal command
2025-10-12 04:40:48 -07:00
canvrno 128b721c0e initial (#6779) 2025-10-12 07:43:35 +00:00
pashpashpash b5ae93dec9 Pashpashpash/UI enhancements cli (#6775)
* adding input to follow mode

* nice cancelling behavior

* approval wip

* fixing ask handling

* unified rendered

* textarea instead of input and unlimited width

* textarea
2025-10-11 23:13:05 -07:00
canvrno 24b5bfd77e auto-cleanup stale default instance config (#6693) 2025-10-11 23:06:17 -07:00
canvrno e7dc04fe56 Add subscribeToCheckpoints proto (#6770)
* Added subscribeToCheckpoints proto

* Following proto conventions for timestamp, better typeing
2025-10-11 21:21:50 -07:00
celestial-vault 42bad19889 remote config state setting (#6776)
* set remote config state and make it override task and global state

* remove console logs

* remove console log
2025-10-11 20:29:40 -07:00
canvrno 5c7bb7c5db Added getCwdHash proto (#6773) 2025-10-12 03:23:56 +00:00
pashpashpash 8c05a9923d help wanted (#6751) 2025-10-11 18:29:29 -07:00
celestial-vault 67ed86ada7 function to fetch remote config (#6756)
* function to fetch remote config

* remove logs

* return undefined if no org and update expected response structure

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2025-10-11 17:10:38 -07:00
CandiedUniverse f212ba20d6 Hooks: Initial implementation of hooks foundation logic [ENG-1011, ENG-985] (#6755)
* Initial implementation of hooks foundation

* Remove duplicate property

* Resolving/implementing TODOs and complexity refactors as per qlty test failures

* Implement tests for hooks changes

* Fix unit tests for CI windows machine (executable files need file extension)

* Adding README and improved examples in .clinerules/hooks/
2025-10-11 12:42:26 -07:00
canvrno 77e3e1e4ed CLI config list changes (#6765)
* Censor cli config list secrets

* addl redaction

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-10-11 03:10:29 -07:00
Ara 0858ff517a Fix CLI installation script (#6760)
* feat(install): improve CLI release detection and error handling

- Redirect error messages to stderr for proper error stream handling
- Filter releases to only match tags ending in '-cli' suffix when fetching latest
- Add explicit .tar.gz extension matching in download URL detection
- Improve error messages to be more specific about missing packages
- Add informative messages about which CLI release is being installed

This ensures the install script correctly identifies CLI-specific releases
and provides better feedback when releases or platform packages are not found.
The stderr redirection prevents error messages from being captured in command
substitutions.

* feat(install): improve shell detection and PATH configuration

- Add support for fish shell and XDG_CONFIG_HOME standard
- Check if bin directory is already in current PATH before modifying config
- Detect shell from $SHELL variable instead of relying on version variables
- Create default config file if none exists for the detected shell
- Use grep -Fq for more reliable PATH entry detection
- Support multiple possible config file locations per shell (zsh, bash, fish)

This improves the installation experience across different shell environments
and prevents duplicate PATH entries when re-running the installer.

* refactor

* better install script

---------

Co-authored-by: pashpashpash <nik@nugbase.com>
2025-10-10 20:55:35 -07:00
Ara 3472e6068c Standalone CLI Installation (#6689)
* Phase 1 download node binary

* Phase 2 include cli binaries

* Phase 3 add scripts

* Phase 4 bug fix

* Phase 5: adding install script

* Phase 6: pushing github actions

* Phase 7: fixing redundancy

* Phase 7: fixing redundancy

* Temporary commit for workspace stuff

* Fix tests

* Fix tests

* Fix tests

* Fix tests

* Fix tests

* Fix tests

* Fix tests

* Adding JB fixes

* Adding JB fixes

* Adding CLI-JB fixes

* Refactor

* Refactor

* refactor

* Update release-standalone.yml

Remove VSCode packaging env vars from CLI workflow.

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2025-10-10 17:37:32 -07:00
canvrno a670c1efa2 CLI Auth Wizard 🧙 (#6742)
* CLI auth wizard

Default Cline model, better menu UX, display active provider and model at main menu

Model switcher for BYO providers

Provider switching, UI changes

List configured providers now shows all providers user has configured

Added remove provider feature

Changes to model picker for BYO providers

Model fetch filtering tweaks

Only update model fields for provider when possible

Consolidated provider field mapping

Change to not set provider as active when updating model

Dont set Cline as active provider when setting Cline model

Added model fetching for OpenAI provider (req API key)

Added model fetching for Ollama provider

Added support for model listing from providers.go for models without remote fetch

Moved auth specific code out of manager

Bedrock fields, OpenAI Native BaseURL

More graceful fetch failure

Fixed auth callback issue

* Added model list feature for AWS
AWS - Profile only support in CLI for now, UX changes (WIP
Remove Save and Exit option
Auto enable filtering/search in model lists
Added cancel option for some menus
2025-10-10 15:33:24 -07:00
Sarah Fortune 078f33a285 Add missing remote config setting for AWS use global inference (#6758) 2025-10-10 14:31:21 -07:00
Toshii a09f70023e remove emojis (#6746) 2025-10-10 14:22:38 -07:00
pashpashpash 4247f7f0d5 removing streaming markdown in favor of stable output (#6747)
* removing streaming markdown in favor of stable output

* proper handling of ask messages

* switching theme to auto

* fixing spacing
2025-10-10 14:22:00 -07:00
pashpashpash 787cd3063d making instance list aesthetic (#6750) 2025-10-10 14:21:44 -07:00
Sarah Fortune 33b3dde9e8 Remove out of date comment (#6757) 2025-10-10 13:43:18 -07:00
Toshii 518733d875 implement cline config get and update listing to use kebab case (#6745)
* implements the newConfigGetCommand function

* change list rendering to kebab case to match inputs
2025-10-10 09:30:57 -07:00
Saoud Rizwan 3b636e1a76 Add auto-retry with exponential backof for failed API requests (#6727)
* add first version of auto-retry failed requests

Remove auto-retry as an option and do it by default for all errors that aren't credit issues for cline provider

Fix error retry showing for insufficient credits error

remove lastAutoRetryDelay

Fixes

Fixes

Fix

Fix

remove retrymessage for consistent UI

* Create lucky-mayflies-arrive.md

* Fix autoRetryAttempt counter not getting reset
2025-10-10 04:27:31 -07:00
pashpashpash 79fd3a99d9 webfetch listdefinitions search and more tools added to cli rendering (#6740)
* webfetch listdefinitions search and more tools added to cli rendering

* code definitions

* removed .rust

* cleanup
2025-10-10 02:10:05 -07:00
CandiedUniverse b7a5e3290a Add hooks feature flag (#6734)
* Add hooks feature flag

* Re-apply the feature flag changes

* Add missing detail about setting state
2025-10-09 22:28:33 -07:00
Ara b1ae417e05 ci: add workflow to auto-label JetBrains plugin issues (#6664)
* ci: add workflow to auto-label JetBrains plugin issues

Add GitHub Actions workflow that automatically applies the 'JetBrains' label to issues when the JetBrains Plugin type is selected in the issue template. The workflow triggers on issue creation and edits, parsing the issue body to detect the plugin type selection and applying the appropriate label for better issue categorization and triage.

* Update .github/workflows/label-jetbrains-issues.yml

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

* adding CLI

* Update .github/workflows/label-jetbrains-issues.yml

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-10-09 21:52:27 -07:00
Toshii 2d496ed62c reusing ensureConfigManager inside of setCommand (#6743) 2025-10-09 20:17:35 -07:00
Toshii 1704df14e1 adding config list command with renderer (#6741)
* adding list command with renderer

* showing empty strings for empty values
2025-10-09 19:23:03 -07:00
celestial-vault e65590c9a0 parse secret fields and set along with config set command (#6739) 2025-10-09 18:11:40 -07:00
pashpashpash a43d375366 fixed plan mode respond reading from memory in cli (#6738)
* fixed plan mode respond partial in cli and cline core

* reverting planmoderespondhandler
2025-10-09 16:26:33 -07:00
Saoud Rizwan 7f05e06d8e Update plan mode response with latest content and partial = false in yolo mode (#6737) 2025-10-09 16:21:32 -07:00
celestial-vault 724c7c778c add config set command (#6736) 2025-10-09 15:44:33 -07:00
celestial-vault 522d00411d remove dynamic state key cline:clineAccountId (#6718) 2025-10-09 15:30:43 -07:00
Toshii a92f9d459d adding config command line arg (#6735)
* adding config command line arg

* import fmt without errors
2025-10-09 14:44:22 -07:00
celestial-vault 2d58c5e9f2 add temp rpc for updating the settings in cli (#6733) 2025-10-09 14:28:16 -07:00
canvrno 835204c7eb Added updateApiConfigurationPartial with FieldMask to allow for partial ApiProvider updates (#6731) 2025-10-09 13:18:36 -07:00
pashpashpash bb4211ff2a added missing cli host stubs (#6716) 2025-10-09 13:05:26 -07:00
Sarah Fortune 261fc7f3d8 Make changes to the remote config schema (#6725) 2025-10-09 14:39:04 +00:00
celestial-vault 554e4d1b94 reorganize protos and add secrets proto message (#6720) 2025-10-08 22:18:52 -07:00
pashpashpash 6476f723d9 markdown streaming support for plan mode respond tool (#6719)
* markdown streaming support for plan mode respond tool

* consistency
2025-10-08 17:11:59 -07:00
celestial-vault beb7ada9b7 add codeowner for storage folder (#6717) 2025-10-08 16:19:46 -07:00
github-actions[bot] 2292cafbb3 v3.32.7 Release Notes (#6616)
* Add JP and Global inference profile options to AWS BedrockAdd a comment on lines R5 to R7Add diff commentMarkdown input:  edit mode selected.WritePreviewAdd a suggestionHeadingBoldItalicQuoteCodeLinkUnordered listNumbered listTask listMentionReferenceSaved repliesAdd FilesPaste, drop, or click to add filesCancelCommentStart a reviewReturn to code
* Adding Improvements to VSCode multi root workspaces
* Added markdown support to focus chain text, allowing the model to display more interesting focus chains

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-08 16:13:34 -07:00
Toshii c9550bf357 now showing historical messages for cline task view (#6711) 2025-10-08 15:09:43 -07:00
pashpashpash 0c63eaac20 cli - markdown streaming ux (#6703)
* wip

* overshooting now

* no more overshooting

* static markdown rendering for non streamed cli stuff

* more aesthetic

* plain text support, and more consistent markdown experience

* moving instance info into renderer
2025-10-08 15:08:28 -07:00
Igor Tceglevskii 8d2ee1dffb do_nothing feature flag (#6712) 2025-10-08 14:35:59 -07:00
Tomás Barreiro 0ea263c1b1 refactor: use different secret keys for each auth provider (#6630)
* refactor: user different secret keys for each auth provider

* Add changeset

* refactor

* refactor: use a constant for the secret key

* refactor: improve fallback handling
2025-10-08 12:06:38 -07:00
celestial-vault 8be46f9cfe Move MCP marketplace catalog from global state to disk cache (#5835)
* Move MCP marketplace catalog from global state to disk cache

* add cleanup function to remove catalog from vs code storage

* type fixes

* move disk related functions to disk file

* add try/catch around file/json operations

* fix weird tab formatting
2025-10-08 11:11:33 -07:00
Sarah Fortune 4469a4fea4 Remove setting Supports Browser Use (#6700) 2025-10-08 18:10:51 +00:00
canvrno feee75b158 Added multiroot docs (#6597)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-10-08 02:03:04 -07:00
Sarah Fortune 09933e7ad5 Add warning message to remote config schema file (#6701) 2025-10-08 00:23:23 -07:00
Sarah Fortune 5b406eca72 Add zod schema for the remote config settings (#6675)
* Add zod schema for the remote config settings MVP

Add a zod schema for the remote config settings JSON.

This is based on the doc here: https://docs.google.com/document/d/1DizuUtV_8nUKlDbkhj6psxgD0Vmn2DV-hUb92Gv369M/edit?usp=sharing

Add unit tests to check that the validation works correctly.

* Change providers from list + seperate settings, to map of provider + settings
2025-10-07 23:05:26 -07:00
celestial-vault e80e0ebc40 add oneshot command (#6695) 2025-10-07 22:45:28 -07:00
pashpashpash d14d05547d fixed ispartial handling for all cases, and cleaned up output to not … (#6691)
* fixed ispartial handling for all cases, and cleaned up output to not have emojis

* consolidating rendermessage function

* timestamps for checkpoints only

* removing useless comments

* showing terminal commands

* terminal command + browser +mcp

* removing unused timestamp argument
2025-10-07 19:22:30 -07:00
celestial-vault c3e24e5d6b add yolo flag (#6690) 2025-10-07 17:01:05 -07:00
celestial-vault 5572096947 add settings flag and settings parsing (#6682)
* add settings flag and settings parsing

* remove note

* remove auto approval settings version

* update note

* fix typo
2025-10-07 16:18:05 -07:00
Ara a85acf8898 feat(logging): replace HostProvider with node-machine-id for machine identification (#6680)
* feat(logging): replace HostProvider with node-machine-id for machine identification

Replace custom HostProvider.env.getMachineId() implementation with the
node-machine-id npm package for retrieving machine identifiers. This
simplifies the codebase by using a well-maintained library instead of
custom host provider logic.

Changes:
- Add node-machine-id dependency (^1.1.12)
- Remove dotenv dev dependency (no longer needed)
- Update distinctId service to use node-machine-id directly
- Refactor tests to stub node-machine-id instead of HostProvider
- Remove HostProvider mock utilities from tests

This change improves maintainability and reduces custom code while
maintaining the same functionality for generating stable machine IDs.

* Removing dead code
2025-10-07 13:51:42 -07:00
AJ Juaire 1eaeb1812d Add Bedrock global and jp inference profile support. (#6666)
* Add Bedrock global and jp inference profile support.

* Update package-lock.json after merging latest main

Regenerated package-lock.json to reflect the latest dependency changes
from the merged main branch updates.

* Revert "Update package-lock.json after merging latest main"

This reverts commit 2fa79acd38.
2025-10-07 11:49:09 -07:00
canvrno 9f25f8ae42 Auth refactor (#6674)
* CLI auth refactor

* changeset

* One small fix
2025-10-07 11:30:55 -07:00
nihar-oracle 816c7ede4a feat: Adding Oca mode for internal and external (#6599)
* feat: Adding Oca mode for internal and external

feat: Adding Oca mode for internal and external

* fix: Addressing cmments
2025-10-07 09:03:58 -07:00
Andrei Eternal 29139a6e13 remove global --config flag for cli (#6673)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-10-06 22:14:53 -07:00
pashpashpash c1195746d1 implemented GetHostVersionResponse rpc in the cli (#6663)
* implemented GetHostVersionResponse rpc in the cli

* undoing machine id

* removed unused uuid import
2025-10-06 18:57:40 -07:00
Toshii a33a3d9186 removing the duplicate error print value (#6670) 2025-10-06 18:03:41 -07:00
Toshii e25fbc1397 adding checkpoint restore to cli (#6669)
* add checkpoint id to messages

* adding command to allow restoring a checkpoint using ts

* add validation of the type for git restore

* validation logic that checkpoint id is valid
2025-10-06 17:59:40 -07:00
canvrno 2917cd234c Provider scripts (#6668)
* Provider auth scripts

* changeset

* Updated default providers for script
2025-10-06 16:10:07 -07:00
canvrno 63a3896669 Multiroot mentions support (#6627)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-10-06 14:24:43 -07:00
Daniel Steigman 529bb3a26c refactor: Support Multiple Telemetry providers (#6582)
* feat: Modular telemetry architecture with Jitsu provider support

- Add dual-provider telemetry architecture supporting both Jitsu and PostHog
- Implement JitsuTelemetryProvider with full API compatibility
- Add required telemetry bypass for critical system health events
- Create modular event handler base class for future extensibility
- Add Jitsu configuration with environment variable controls
- Update TelemetryService to support multiple providers with error isolation
- Add .env.example template for development setup
- Maintain backward compatibility with existing PostHog integration
- Enable easy PostHog removal via POSTHOG_TELEMETRY_ENABLED=false
- Install dotenv for local development environment support

Key benefits:
- Dual tracking during transition period
- Error isolation between providers
- Memory efficient static method architecture
- Easy provider enable/disable via environment variables
- Wednesday deployment ready for Jitsu migration

* fix(build): Load environment variables from .env file during development builds

- Add dotenv.config() to esbuild.mjs to load .env variables
- Include all telemetry-related environment variables in build injection:
  - TELEMETRY_SERVICE_API_KEY (PostHog)
  - ERROR_SERVICE_API_KEY (PostHog error tracking)
  - JITSU_WRITE_KEY (Jitsu telemetry)
  - JITSU_HOST (Jitsu host URL)
  - JITSU_ENABLED (Jitsu provider control)
  - POSTHOG_TELEMETRY_ENABLED (PostHog provider control)

This ensures telemetry services work correctly in development builds
by properly injecting API keys and configuration from .env file.

Also updates TelemetryService tests to support multi-provider architecture.

* fix(telemetry): Replace Record<string, unknown> with proper JSON-serializable types

- Add TelemetryPrimitive, TelemetryValue, TelemetryObject, and TelemetryProperties types to ITelemetryProvider
- Update JitsuTelemetryProvider to use TelemetryProperties instead of Record<string, unknown>
- Update PostHogTelemetryProvider to use TelemetryProperties instead of Record<string, unknown>
- Update TelemetryService to use TelemetryProperties for type-safe telemetry data
- Ensures all telemetry properties are JSON-serializable, preventing runtime errors
- Fixes TypeScript compatibility issue between Jitsu's JSONObject type and Record<string, unknown>

* moved and organized the telemetry files and updated the example env file to be more descriptive

* refactor: remove Jitsu telemetry provider

- Remove Jitsu provider implementation and config files
- Remove Jitsu environment variables from .env.example
- Remove Jitsu build configuration from esbuild.mjs
- Update TelemetryProviderFactory to only support PostHog
- Uninstall @jitsu/js dependency
- Add .env to .gitignore to prevent committing local env files

* chore: add changeset for Jitsu removal

* removed jitsu

* fix: update import paths after PostHogClientProvider relocation

* fix: remove race condition in captureToProviders and reorganize PostHog providers

- Changed captureToProviders from async to synchronous method
- Removed unnecessary Promise.allSettled overhead since provider.log() and provider.logRequired() are synchronous
- Changed from .map() to .forEach() for better clarity
- Moved PostHog provider files into posthog/ subdirectory for better organization
- Updated all import paths to reflect new folder structure

* refactor(telemetry): remove unnecessary addProperties method and improve type safety

- Remove addProperties helper method that used 'any' types
- Replace with inline typed spread operations in capture(), captureRequired(), and identifyAccount()
- Fix type errors in captureConversationTurnEvent and captureBrowserError
- All telemetry properties now properly typed as TelemetryProperties
- Ensures OpenTelemetry compatibility through type system enforcement

* refactor: remove dotenv dependency and use launch.json envFile

- Remove dotenv import and config() call from esbuild.mjs
- Add envFile parameter to all launch.json configurations to load .env
- Remove dotenv from package.json devDependencies

Environment variables are now loaded via VSCode's envFile feature for local
development, while CI/production continues to inject via GitHub Actions.
This provides cleaner separation between build-time and runtime environment
handling.

* feat(telemetry): add browser telemetry properties and improve typing

- Add remoteBrowserHost and endpoint fields to browser telemetry events
- Replace generic Record<string, unknown> with TelemetryObject type in EventHandlerBase for better type safety
- Import TelemetryObject type from ITelemetryProvider

These changes enhance browser telemetry tracking capabilities and improve type consistency across the telemetry service.
2025-10-06 12:05:32 -07:00
celestial-vault 541b51c0fb Add watch build script (#6655)
* add dev command for cline-core changes for cli

* add cli watch build command

* remove other script
2025-10-06 11:53:15 -07:00
Toshii 77395a3088 adding send as a top level command (#6661) 2025-10-06 11:47:16 -07:00
Igor Tceglevskii 124fc6f147 add logout reason tracking and improve cleanup handling (#6657)
- Add LogoutReason enum to track different logout scenarios
- Update handleDeauth() calls to include logout reasons
- Move auth storage cleanup from Controller to AuthService for better encapsulation
- Add telemetry events to capture logout reasons for analytics
2025-10-06 08:57:58 -07:00
956 changed files with 98100 additions and 16561 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add DeepSeek 3.2 to native tool calling allow list
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Added checkpoints warning when users start a multiroot task
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Prevent simultaneuos refreshes when restoring auth info
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Added markdown support to focus chain text, allowing the model to display more interesting focus chains
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Improved system prompt guidance for proactively limiting command output when large results are anticipated
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
echo "PostToolUse running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "PostToolUse response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "PostToolUse hook custom errorMessage"
}
EOF
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
echo "PreToolUse running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "PreToolUse response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "PreToolUse hook custom errorMessage"
}
EOF
+423
View File
@@ -0,0 +1,423 @@
# Cline Hooks Documentation
## Overview
Cline hooks allow you to execute custom scripts at specific points in the agentic workflow. Hooks can be placed in either:
- **Global hooks directory**: `~/Documents/Cline/Hooks/` (applies to all workspaces)
- **Workspace hooks directory**: `.clinerules/hooks/` (applies to the workspace the repo is part of)
Hooks run automatically when enabled.
## Enabling Hooks
1. Open Cline settings in VSCode
2. Navigate to the Feature Settings section
3. Check the "Enable Hooks" checkbox
4. Hooks must be executable files (on Unix/Linux/macOS use `chmod +x hookname`)
## Available Hooks
### TaskStart Hook
- **When**: Runs when a NEW task is started (not when resuming)
- **Purpose**: Initialize task context, validate task requirements, set up environment
- **Global Location**: `~/Documents/Cline/Hooks/TaskStart`
- **Workspace Location**: `.clinerules/hooks/TaskStart`
### TaskResume Hook
- **When**: Runs when an EXISTING task is resumed (after user clicks resume button)
- **Purpose**: Validate resumed task state, restore context, check for changes since last run
- **Global Location**: `~/Documents/Cline/Hooks/TaskResume`
- **Workspace Location**: `.clinerules/hooks/TaskResume`
### TaskCancel Hook
- **When**: Runs when a task is cancelled or a hook is aborted by the user (only if there's actual active work or work was started)
- **Purpose**: Clean up resources, log cancellation, save state
- **Global Location**: `~/Documents/Cline/Hooks/TaskCancel`
- **Workspace Location**: `.clinerules/hooks/TaskCancel`
- **Note**: This hook is NOT cancellable
### TaskComplete Hook (coming soon!)
- **When**: Runs when a task is marked as complete
- **Purpose**: Log completion status, perform final cleanup, generate reports
- **Global Location**: `~/Documents/Cline/Hooks/TaskComplete`
- **Workspace Location**: `.clinerules/hooks/TaskComplete`
### UserPromptSubmit Hook
- **When**: Runs when the user submits a prompt/message (initial task, resume, or feedback)
- **Purpose**: Validate user input, preprocess prompts, add context to user messages
- **Global Location**: `~/Documents/Cline/Hooks/UserPromptSubmit`
- **Workspace Location**: `.clinerules/hooks/UserPromptSubmit`
### PreToolUse Hook
- **When**: Runs BEFORE a tool is executed
- **Purpose**: Validate parameters, block execution, or add context
- **Global Location**: `~/Documents/Cline/Hooks/PreToolUse`
- **Workspace Location**: `.clinerules/hooks/PreToolUse`
### PostToolUse Hook
- **When**: Runs AFTER a tool completes
- **Purpose**: Observe results, track patterns, or add context
- **Global Location**: `~/Documents/Cline/Hooks/PostToolUse`
- **Workspace Location**: `.clinerules/hooks/PostToolUse`
### PreCompact Hook (coming soon!)
- **When**: Runs BEFORE the conversation context is compacted/truncated
- **Purpose**: Observe compaction events, log context management, track token usage
- **Global Location**: `~/Documents/Cline/Hooks/PreCompact`
- **Workspace Location**: `.clinerules/hooks/PreCompact`
## Cross-Platform Hook Format
Cline uses a git-style approach for hooks that works consistently across all platforms:
### Hook Files (All Platforms)
- **No file extensions**: Hooks are named exactly `PreToolUse` or `PostToolUse` (no `.bat`, `.cmd`, `.sh` etc.)
- **Shebang required**: First line must be a shebang (e.g., `#!/usr/bin/env bash` or `#!/usr/bin/env node`)
- **Executable on Unix**: On Unix/Linux/macOS, hooks must be executable: `chmod +x PreToolUse`
- **Windows**: Not currently supported.
### How It Works
Like git hooks, Cline executes hook files through a shell that interprets the shebang line:
- On Unix/Linux/macOS: Native shell execution with shebang support
This means:
- ✅ Same hook script works on all platforms
- ✅ Write once, run anywhere
- ✅ Use any scripting language (bash, node, python, etc.)
### Creating Hooks
**On Unix/Linux/macOS:**
```bash
# Create hook file
nano ~/Documents/Cline/Hooks/PreToolUse
# Make executable
chmod +x ~/Documents/Cline/Hooks/PreToolUse
```
## Context Injection Timing
**IMPORTANT**: Context injected by hooks affects **FUTURE AI decisions**, not the current tool execution.
### Why This Matters
When a hook runs:
1. The AI has already decided what tool to use and with what parameters
2. The hook cannot modify those parameters
3. Context from the hook is added to the conversation
4. The AI sees this context in the **NEXT API request** and can adjust future decisions
### PreToolUse Hook Flow
```
1. AI decides: "I'll use write_to_file with these parameters"
2. PreToolUse hook runs → can block or add context
3. If allowed, tool executes with original parameters
4. Context is added to conversation
5. Next API request includes this context
6. AI adjusts future decisions based on context
```
### PostToolUse Hook Flow
```
1. Tool completes execution
2. PostToolUse hook runs → observes results
3. Hook adds context about the outcome
4. Context is added to conversation
5. Next API request includes this context
6. AI can learn from the results
```
## Hook Input/Output
### Input (via stdin as JSON)
All hooks receive:
```json
{
"clineVersion": "string",
"hookName": "TaskStart" | "TaskResume" | "TaskCancel" | "TaskComplete" | "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PreCompact",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskStart": { // Only for TaskStart
"taskMetadata": {
"taskId": "string",
"ulid": "string",
"initialTask": "string"
}
},
"taskResume": { // Only for TaskResume
"taskMetadata": {
"taskId": "string",
"ulid": "string"
},
"previousState": {
"lastMessageTs": "string",
"messageCount": "string",
"conversationHistoryDeleted": "string"
}
},
"taskCancel": { // Only for TaskCancel
"taskMetadata": {
"taskId": "string",
"ulid": "string",
"completionStatus": "string"
}
},
"taskComplete": { // Only for TaskComplete
"taskMetadata": {
"taskId": "string",
"ulid": "string"
}
},
"userPromptSubmit": { // Only for UserPromptSubmit
"prompt": "string",
"attachments": ["string"]
},
"preToolUse": { // Only for PreToolUse
"toolName": "string",
"parameters": {}
},
"postToolUse": { // Only for PostToolUse
"toolName": "string",
"parameters": {},
"result": "string",
"success": boolean,
"executionTimeMs": number
},
"preCompact": { // Only for PreCompact
"contextSize": number,
"messagesToCompact": number,
"compactionStrategy": "string"
}
}
```
### Output (via stdout as JSON)
All hooks must return:
```json
{
"cancel": boolean, // Required: false to continue, true to block execution
"contextModification": "string", // Optional: Context for future AI decisions
"errorMessage": "string" // Optional: Error details if blocking
}
```
**Note**: The `cancel` field works as follows:
- `false` (or omitted): Allow execution to continue
- `true`: Block execution and show error message to user
## Hook Execution Limits
- **Timeout**: Hooks must complete within 30 seconds (configurable via `HOOK_EXECUTION_TIMEOUT_MS`)
- **Context Size**: Context modifications are limited to 50KB (configurable via `MAX_CONTEXT_MODIFICATION_SIZE`)
- **Error Handling**: Expected errors (file not found, permission denied, not a directory) are handled silently; unexpected file system errors are propagated
## Common Use Cases
### 1. Validation - Block Invalid Operations
```bash
#!/usr/bin/env bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$path" == *.js ]]; then
cat <<EOF
{
"cancel": true,
"errorMessage": "Cannot create .js files in TypeScript project",
"contextModification": "Use .ts/.tsx extensions only"
}
EOF
exit 0
fi
echo '{"cancel": false}'
```
### 2. Context Building - Learn from Operations
```bash
#!/usr/bin/env bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
success=$(echo "$input" | jq -r '.postToolUse.success')
path=$(echo "$input" | jq -r '.postToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$success" == "true" ]]; then
cat <<EOF
{
"cancel": false,
"contextModification": "Created '$path'. Maintain consistency with this file's patterns in future operations."
}
EOF
else
echo '{"cancel": false}'
fi
```
### 3. Performance Monitoring
```bash
#!/usr/bin/env bash
input=$(cat)
execution_time=$(echo "$input" | jq -r '.postToolUse.executionTimeMs')
tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
if [[ "$execution_time" -gt 5000 ]]; then
cat <<EOF
{
"cancel": false,
"contextModification": "Tool '$tool_name' took ${execution_time}ms. Consider optimizing future similar operations."
}
EOF
else
echo '{"cancel": false}'
fi
```
### 4. Logging and Telemetry
```bash
#!/usr/bin/env bash
input=$(cat)
# Log to file
echo "$input" >> ~/.cline/hook-logs/tool-usage.jsonl
# Allow execution
echo '{"cancel": false}'
```
## Global vs Workspace Hooks
Cline supports two levels of hooks:
### Global Hooks
- **Location**: `~/Documents/Cline/Hooks/` (macOS/Linux)
- **Scope**: Apply to ALL workspaces and projects
- **Use Case**: Organization-wide policies, personal preferences, universal validations
- **Priority**: Order not guaranteed when combined with workspace hooks
### Workspace Hooks
- **Location**: `.clinerules/hooks/` in each workspace root
- **Scope**: Apply only to the specific workspace
- **Use Case**: Project-specific rules, team conventions, repository requirements
- **Priority**: Order not guaranteed when combined with global hooks
### Hook Execution
When multiple hooks exist (global and/or workspace):
- All hooks for a given step are executed **concurrently** using `Promise.all`
- **Execution order is not guaranteed** - hooks run in parallel
- If ALL hooks allow execution (`cancel: false`), the tool proceeds
- If ANY hook blocks (`cancel: true`), execution is blocked
**Result Combination:**
- `cancel`: If ANY hook returns `true`, execution is blocked
- `contextModification`: All context strings are concatenated with double newlines (`\n\n`)
- `errorMessage`: All error messages are concatenated with single newlines (`\n`)
### Setting Up Global Hooks
1. The global hooks directory is automatically created at:
- macOS/Linux: `~/Documents/Cline/Hooks/`
2. Add your hook script:
```bash
# Unix/Linux/macOS
nano ~/Documents/Cline/Hooks/PreToolUse
chmod +x ~/Documents/Cline/Hooks/PreToolUse
```
3. Enable hooks in Cline settings
### Example: Global + Workspace Hooks
**Global Hook** (applies to all projects):
```bash
#!/usr/bin/env bash
# ~/Documents/Cline/Hooks/PreToolUse
# Universal rule: Never delete package.json
input=$(cat)
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$path" == *"package.json"* ]]; then
echo '{"cancel": true, "errorMessage": "Global policy: Cannot modify package.json"}'
exit 0
fi
echo '{"cancel": false}'
```
**Workspace Hook** (applies to specific project):
```bash
#!/usr/bin/env bash
# .clinerules/hooks/PreToolUse
# Project rule: Only TypeScript files
input=$(cat)
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$path" == *.js ]]; then
echo '{"cancel": true, "errorMessage": "Project rule: Use .ts files only"}'
exit 0
fi
echo '{"cancel": false}'
```
**All hooks must allow execution for the tool to proceed.** Hooks may execute concurrently.
## Multi-Root Workspaces
If you have multiple workspace roots, you can place hooks in each root's `.clinerules/hooks/` directory. All hooks (global and workspace) may execute concurrently. Their results will be combined:
- **cancel**: If ANY hook returns `true`, execution is blocked
- **contextModification**: All context modifications are concatenated
- **errorMessage**: All error messages are concatenated
**Note:** No execution order is guaranteed between hooks from different directories.
## Troubleshooting
### Hook Not Running
- Ensure the "Enable Hooks" setting is checked
- Verify the hook file is executable (`chmod +x hookname`)
- Check the hook file has no syntax errors
- Look for errors in VSCode's Output panel (Cline channel)
### Hook Timing Out
- Reduce complexity of the hook script
- Avoid expensive operations (network calls, heavy computations)
- Consider moving complex logic to a background process
### Context Not Affecting Behavior
- Remember: context affects FUTURE decisions, not the current tool
- Ensure context modifications are clear and actionable
- Check that context isn't being truncated (50KB limit)
## Security Considerations
- Hooks run with the same permissions as VSCode
- Be cautious with hooks from untrusted sources
- Review hook scripts before enabling them
- Consider using `.gitignore` to avoid committing sensitive hook logic
- Hooks can access all workspace files and environment variables
## Best Practices
1. **Keep hooks fast** - Aim for <100ms execution time
2. **Make context actionable** - Be specific about what the AI should do
3. **Use structured prefixes** - Help the AI categorize context
4. **Handle errors gracefully** - Always return valid JSON
5. **Log for debugging** - Keep logs of hook executions for troubleshooting
6. **Test incrementally** - Start with simple hooks and add complexity
7. **Document your hooks** - Add comments explaining the purpose and logic
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
echo "TaskCancel running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "TaskCancel response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "TaskCancel hook custom errorMessage"
}
EOF
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
echo "TaskResume running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "TaskResume response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "TaskResume hook custom errorMessage"
}
EOF
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
echo "TaskStart running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "TaskStart response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "TaskStart hook custom errorMessage"
}
EOF
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
echo "UserPromptSubmit running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "UserPromptSubmit response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "UserPromptSubmit hook custom errorMessage"
}
EOF
+90
View File
@@ -0,0 +1,90 @@
# Networking & Proxy Support
To ensure Cline works correctly in all environments (VSCode, JetBrains, CLI) and with various network configurations (especially corporate proxies), strictly follow these guidelines for all network activity.
In extension code, do NOT use the global `fetch` or a default `axios` instance. (Note, `shared/net.ts` is exempt from these rules because it sets up the fetch wrappers.) In Webview code, you SHOULD use global `fetch`.
Global `fetch` and default `axios` do not automatically pick up proxy configurations in all environments (specifically JetBrains and CLI). You MUST use the provided utilities in `@/shared/net` which handle proxy agent configuration. In the webview, the browser/embedder handles proxies.
## Guidelines
### 1. Using `fetch`
Instead of `fetch(...)`, import the proxy-aware wrapper:
```typescript
import { fetch } from '@/shared/net'
// Usage is identical to global fetch
const response = await fetch('https://api.example.com/data')
```
### 2. Using `axios`
When using `axios`, you must apply the settings from `getAxiosSettings()`:
```typescript
import axios from 'axios'
import { getAxiosSettings } from '@/shared/net'
const response = await axios.get('https://api.example.com/data', {
headers: { 'Authorization': '...' },
...getAxiosSettings() // <--- CRITICAL: Injects the proxy agent if needed
})
```
### 3. Third-Party Clients (OpenAI, Ollama, etc.)
Most API client libraries allow you to customize the `fetch` implementation. You **MUST** pass the proxy-aware `fetch` to these clients.
**Example (OpenAI):**
```typescript
import OpenAI from "openai"
import { fetch } from "@/shared/net"
this.client = new OpenAI({
apiKey: '...',
fetch, // <--- CRITICAL: Pass our fetch wrapper
})
```
### 4. Tests
Use `mockFetchForTesting` to mock the underlying fetch implementation.
**Example (callback):**
```
import { mockFetchForTesting } from "@/shared/net"
...
let mockFetch = ...
mockFetchForTesting(mockFetch, () => {
// This calls mockFetch
fetch('https://foo.example').then(...)
})
// Original fetch is restored immediately when the call returns.
```
**Example (Promise):**
```
import { mockFetchForTesting } from "@/shared/net"
...
let mockFetch = ...
await mockFetchForTesting(mockFetch, async () => {
await ...
// This calls mockFetch
await fetch('https://foo.example')
...
})
// Original fetch is restored when the Promise from the callback settles
```
## Verification
If you are adding a new network call or integration:
1. Check `@/shared/net.ts` is imported.
2. Ensure `fetch` or `getAxiosSettings` is being used.
3. Verify that third-party clients are configured to use the custom fetch.
@@ -0,0 +1,29 @@
# Address PR Comments
Review and address all comments on the current branch's PR.
## Steps
1. Get the current branch name and find the associated PR:
```bash
gh pr view --json number,title,body
```
2. Understand the PR context:
- Get the full diff: `git diff origin/main...HEAD`
- Read the changed files to understand what the PR is doing
- Read related files if needed to understand the broader context
- Understand the intent and spirit of the changes, not just the code
3. Fetch all PR comments:
- Inline comments: `gh api repos/{owner}/{repo}/pulls/{pr_number}/comments`
- General comments: `gh pr view {pr_number} --json comments,reviews`
4. Present a summary of all comments with your recommendation for each (apply, skip, or respond). Ignore bot noise (changeset-bot, CI status, etc.).
5. **Wait for my approval** before proceeding.
6. After approval:
- Apply code changes and commit
- Reply to comments that were addressed or intentionally skipped
- Push commits
+2 -2
View File
@@ -122,9 +122,9 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
title="Previous Updates:"
classNames={{
trigger: "bg-transparent border-0 pl-0 pb-0 w-fit",
title: "font-bold text-[var(--vscode-foreground)]",
title: "font-bold text-(--vscode-foreground)",
indicator:
"text-[var(--vscode-foreground)] mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
"text-(--vscode-foreground) mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
}}>
<ul style={ulStyle}>
<li>
@@ -0,0 +1,49 @@
# Find Best Reviewers for Current Branch
Analyze my current branch to find the best people to review my PR based on **domain expertise** and git history.
## Steps
1. Get the current branch name and verify it's not `main`
2. Get the diff between the current branch and `origin/main`:
- Use `git diff origin/main...HEAD --name-only` to get changed files
- Use `git diff origin/main...HEAD` to understand the nature/spirit of the changes
3. **Identify the domain/feature area** being changed:
- Read the diff carefully to understand WHAT is being changed conceptually (e.g., "slash commands", "authentication", "API client", "UI components")
- This semantic understanding is crucial for finding the right reviewers
4. Find domain experts by searching for related files and their contributors:
- Identify all files related to the feature/domain (not just the ones changed)
- Example: if changing slash commands, find ALL slash-command related files across the codebase
- Use `git log --format="%an <%ae>" -- <related-files-pattern>` to find who has expertise in that domain
5. For additional context, also gather:
- `git blame -L <start>,<end> origin/main -- <file-path>` for exact lines changed
- Recent commit activity on related files
6. Score and rank contributors by:
- **Highest weight: Domain expertise** - who has the most commits to files in this feature area (even files not touched by this PR)
- **Medium weight: Direct file expertise** - commits to the specific files being changed
- **Lower weight: Line-level ownership** - authored the exact lines being modified
7. Exclude myself (check against my git config user.email)
8. Present the top 5 reviewers as an ordered list
## Output Format
Output an ordered list:
1. **Name** - Domain expert: 15 commits to slash-command related files, authored core parsing logic
2. **Name** - 8 commits to affected files, recently added the feature being modified
3. ...
## Commands Reference
```bash
git config user.email
git diff origin/main...HEAD --name-only
git diff origin/main...HEAD
# Find related files for a domain (adjust pattern based on what you learn from the diff)
find . -type f \( -name "*slash-command*" -o -name "*SlashCommand*" \) | head -20
# Get contributors for related files
find . -type f \( -name "*slash-command*" -o -name "*SlashCommand*" \) -print0 | xargs -0 git log --format="%an <%ae>" -- | sort | uniq -c | sort -rn
git log --format="%an <%ae>" -- <file> | sort | uniq -c | sort -rn
git blame -L 10,20 origin/main -- <file>
```
Do NOT ask questions - analyze the changes, identify the domain, and output the reviewer list.
+116
View File
@@ -0,0 +1,116 @@
# Cline Development Environment Variables
# Copy this file to .env and fill in your actual values
# Values should be obtained from 1Password shared vault for development
# ============================================================================
# DEVELOPMENT FLAGS
# Recomend not changing these unless you know what you're doing they are set by the launch.json normally
# ============================================================================
# IS_DEV=true
# CLINE_ENVIRONMENT=local
# ============================================================================
# POSTHOG TELEMETRY (Existing)
# ============================================================================
# Get these values from 1Password shared vault
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)
# ============================================================================
# OpenTelemetry provides flexible telemetry collection with multiple export options
# Can run alongside PostHog or independently
# Primary focus: Logs (events), with optional metrics support
# Enable OpenTelemetry (set to 1 to enable)
# OTEL_TELEMETRY_ENABLED=1
# Exporters: "console" for local debugging, "otlp" for remote collector
# Logs are the primary signal (recommended)
# OTEL_LOGS_EXPORTER=console
# OTEL_METRICS_EXPORTER=otlp
# OTLP Protocol: "grpc", "http/json", or "http/protobuf"
# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
# OTLP Endpoint (without /v1/logs or /v1/metrics path - auto-appended)
# For gRPC: use "localhost:4317" (no http:// prefix)
# For HTTP: use "http://localhost:4318"
# OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
# OTLP Headers (for authentication, e.g., bearer tokens)
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token-here
# Use insecure gRPC connections (for local testing only, NOT for production)
# OTEL_EXPORTER_OTLP_INSECURE=true
# Metric export interval in milliseconds (default: 60000)
# OTEL_METRIC_EXPORT_INTERVAL=10000
# Batch configuration for logs (optional)
# OTEL_LOG_BATCH_SIZE=512 # Max logs per batch (default: 512)
# OTEL_LOG_BATCH_TIMEOUT=5000 # Max wait time in ms (default: 5000)
# OTEL_LOG_MAX_QUEUE_SIZE=2048 # Max queue size (default: 2048)
# Enable detailed export diagnostics (for debugging)
# TEL_DEBUG_DIAGNOSTICS=true
# Advanced: Separate endpoints for metrics and logs (optional)
# OTEL_EXPORTER_OTLP_METRICS_PROTOCOL=http/protobuf
# OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://metrics.example.com:4318
# OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=grpc
# OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=logs.example.com:4317
# Example configurations:
#
# Console debugging (logs only):
# OTEL_TELEMETRY_ENABLED=1
# OTEL_LOGS_EXPORTER=console
# TEL_DEBUG_DIAGNOSTICS=true
#
# OTLP with gRPC (insecure, for local testing):
# OTEL_TELEMETRY_ENABLED=1
# OTEL_LOGS_EXPORTER=otlp
# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
# OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
# OTEL_EXPORTER_OTLP_INSECURE=true
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token
#
# OTLP with HTTP/JSON (production):
# OTEL_TELEMETRY_ENABLED=1
# OTEL_LOGS_EXPORTER=otlp
# OTEL_EXPORTER_OTLP_PROTOCOL=http/json
# OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token
# ============================================================================
# OPTIONAL DEVELOPMENT SETTINGS
# ============================================================================
# Uncomment and modify as needed for development
# Multi-root workspace debugging
# MULTI_ROOT_TRACE=true
# gRPC recorder for testing
# GRPC_RECORDER_ENABLED=true
# GRPC_RECORDER_FILE_NAME=test-recording
# Test mode
# E2E_TEST=true
# IS_TEST=true
# ============================================================================
# USAGE INSTRUCTIONS
# ============================================================================
# 1. Copy this file: cp .env.example .env
# 2. Get PostHog keys from 1Password shared vault
# 3. Update the values in .env
# 4. The .env file is gitignored for security
+1
View File
@@ -1,3 +1,4 @@
/docs/
/.github/ @saoudrizwan @garoth @sjf
/README.md @saoudrizwan @nickbaumann98
/src/core/storage/ @celestial-vault
+1
View File
@@ -14,6 +14,7 @@ body:
options:
- VSCode Extension
- JetBrains Plugin
- CLI
default: 0
validations:
required: true
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
cache: "npm"
- name: Install Dependencies
run: npm install changeset
run: npm ci
# Check if there are any new changesets to process
- name: Check for changesets
@@ -0,0 +1,53 @@
name: Auto-label Issues
on:
issues:
types: [opened, edited]
jobs:
label:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/github-script@v7
with:
script: |
const body = context.payload.issue.body || '';
const labels = context.payload.issue.labels.map(l => l.name);
// Check if JetBrains Plugin is selected
if (body.match(/###\s*Plugin Type\s*\n+JetBrains Plugin/i)) {
if (!labels.includes('JetBrains')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['JetBrains']
});
}
}
// Check if VSCode Extension is selected
if (body.match(/###\s*Plugin Type\s*\n+VSCode Extension/i)) {
if (!labels.includes('VS Code')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['VS Code']
});
}
}
// Check if CLI is selected
if (body.match(/###\s*Plugin Type\s*\n+CLI/i)) {
if (!labels.includes('CLI')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['CLI']
});
}
}
+8 -1
View File
@@ -72,4 +72,11 @@ jobs:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
run: npm run publish:marketplace:nightly
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
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 }}
run: npm run publish:marketplace:nightly
+9 -2
View File
@@ -60,11 +60,11 @@ jobs:
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
run: npm install --include=optional
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci --include=optional
run: cd webview-ui && npm install --include=optional
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
@@ -97,6 +97,13 @@ jobs:
CLINE_ENVIRONMENT: production
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
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 }}
run: |
# Required to generate the .vsix
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
+17 -2
View File
@@ -187,8 +187,20 @@ jobs:
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Compile standalone
run: npm run compile-standalone
- 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: Install testing platform dependencies
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
@@ -244,11 +256,14 @@ jobs:
- name: Download test platform integration core coverage artifact
uses: actions/download-artifact@v4
continue-on-error: true
id: download-integration-coverage
with:
name: test-platform-integration-core-coverage
path: integration-core-coverage-reports
- name: Upload core integration tests coverage to Qlty
if: steps.download-integration-coverage.outcome == 'success'
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
+1
View File
@@ -26,6 +26,7 @@ coverage-unit
!.github/scripts/coverage/
*evals.env
.env
## Generated files ##
src/generated/
+27
View File
@@ -19,6 +19,7 @@
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
@@ -39,6 +40,7 @@
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
@@ -59,6 +61,7 @@
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
@@ -84,6 +87,7 @@
"preLaunchTask": "clean-tmp-user",
"internalConsoleOptions": "openOnSessionStart",
"postDebugTask": "stop",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"TEMP_PROFILE": "true",
@@ -114,6 +118,7 @@
"tsx"
],
"program": "scripts/test-standalone-core-api-server.ts",
"envFile": "${workspaceFolder}/.env",
"env": {
"PROTOBUS_PORT": "26040",
"HOSTBRIDGE_PORT": "26041",
@@ -151,6 +156,7 @@
"--exit",
"${file}"
],
"envFile": "${workspaceFolder}/.env",
"env": {
"TS_NODE_PROJECT": "./tsconfig.unit-test.json",
"NODE_ENV": "test",
@@ -159,6 +165,27 @@
},
"console": "integratedTerminal",
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": "Open Storybook",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"storybook"
],
"cwd": "${workspaceFolder}/webview-ui",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"serverReadyAction": {
"pattern": "Local:.*http://localhost:([0-9]+)",
"uriFormat": "http://localhost:%s",
"action": "openExternally"
},
"env": {
"IS_DEV": "true"
}
}
]
}
+20
View File
@@ -263,6 +263,26 @@
"watch"
],
"command": "rm -rf ${workspaceFolder}/dist/tmp/user && mkdir -p ${workspaceFolder}/dist/tmp/user"
},
{
"type": "npm",
"script": "storybook",
"group": "build",
"problemMatcher": [],
"isBackground": false,
"label": "npm: storybook",
"dependsOn": [
"npm: protos",
"npm: build:webview"
],
"presentation": {
"reveal": "always"
},
"options": {
"env": {
"IS_DEV": "true"
}
}
}
],
"inputs": [
+8 -1
View File
@@ -18,6 +18,10 @@ tsconfig*.json
eslint-rules/**
.github/**
.husky/**
.env
# cli
cli/**
# Custom
**/demo.gif
@@ -36,6 +40,9 @@ buf.yaml
.changeset/
.clinerules/
# Include specific file needed for Background Exec mode
!standalone/runtime-files/vscode/enhanced-terminal.js
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
webview-ui/src/**
webview-ui/public/**
@@ -69,4 +76,4 @@ test-results/
**/*.stories.tsx
*storybook.log
storybook-static
**/StorybookDecorator.tsx
**/StorybookDecorator.tsx
+164 -1
View File
@@ -1,10 +1,173 @@
# Changelog
## [3.40.0]
- Fix highlighted text flashing when task header is collapsed
- Add X-Cerebras-3rd-Party-Integration header to Cerebras API requests
- Add microwave family system prompt configuration
- Remove tooltips from auto approve menu
- Fix Standalone, ensure cwd is the install dir to find resources reliably
- Fix a bug where terminal commands with double quotes are broken when "Terminal Execution Mode" is set to "Background Exec"
- Add support for slash commands anywhere in a message, not just at the beginning. This matches the behavior of @ mentions for a more flexible input experience.
- Add bottom padding to the last message to fix last response text getting cut off by auto approve settings bar.
- Add default thinking level for Gemini 3 Pro models in Gemini provider
## [3.39.2]
- Fix for microwave model and thinking settings
## [3.39.1]
- Fix Openrouter and Cline Provider model info
## [3.39.0]
- Add Explain Changes feature
- Add microwave Stealth model
- Add Tabbed Model Picker with Recommended and Free tabs
- Add support to View remote rules and workflows in the editor
- Enable NTC (Native Tool Calling) by default
- Bug fixes and improvements for LiteLLM provider
## [3.38.3]
- Task export feature now opens the task directory, allowing easy access to the full task files
- Add Grok 4.1 and Grok Code to XAI provider
- Enabled native tool calling for Baseten and Kimi K2 models
- Add thinking level to Gemini 3.0 Pro preview
- Expanded Hooks functionality
- Removed Task Timeline from Task Header
- Bug fix for slash commands
- Bug fixes for Vertex provider
- Bug fixes for thinking/reasoning issues across multiple providers when using native tool calling
- Bug fixes for terminal usage on Windows devices
## [3.38.2]
- Add Claude Opus 4.5
## [3.38.1]
### Fixed
- Fixed handling of 'signature' field in sanitizeAnthropicContentBlock to properly preserve it when thinking is enabled, as required by Anthropic's API.
## [3.38.0]
### Added
- Gemini 3 Pro Preview model
- AquaVoice Avalon model for voice-to-text dictation
### Fixed
- Automatic context truncation when AWS Bedrock token usage rate limits are exceeded
- Removed new_task tool from system prompts, updated slash command prompts, and added helper function for native tool calling validation
## [3.37.1]
- Comprehensive changes to better support GPT 5.1 - System prompt, tools, deep-planning, focus chain, etc.
- Add AGENTS.md support
- feat(models): Add free minimax/mimax-m2 model to the model picker
## [3.37.0]
### Added
- GPT-5.1 with model-specific prompting: tailored system prompts, tool usage, focus chain, and deep-planning optimizations
- Nous Research provider with Hermes 4 model family and custom system prompts
- Switched to Aqua Voice's Avalon model in speech to text transcription
- Added Linux support for speech to text
- Model-family breakouts for deep-planning prompting, laying groundwork for enhanced slash commands
- Expanded HTTP proxy support throughout the codebase
- Improved focus chain prompting for frontier models (Anthropic, OpenAI, Gemini, xAI)
### Fixed
- Duplicate tool results prevention through existence checking
- XML entity escaping in model content processor
- Commit message generation in command palette
- OpenAI Compatible provider temperature parameter type conversion
## Documentation
- Added missing proto generation step in CONTRIBUTING.md
- New `npm run dev` script for streamlined terminal workflow (fixes #7335)
## [3.36.1]
- fix: remove native tool calling support from Gemini and XAI provider due to invalid tool names issues
- fix: disable native tool callings for grok code models
- Add MCP tool usage to GLM
- Removes reasoning_details content field from Anthropic providers
## [3.36.0]
- Add: Hooks allow you to inject custom logic into Cline's workflow
- Add: new provider AIhubmix
- Add: Use http_proxy, https_proxy and no_proxy in JetBrains
- Fix: Oca Token Refresh logic
- Fix: issues where assistant message with empty content is added to conversation history
- Fix: bug where the checkbox shows in the model selector dropdown
- Fix: Switch from defaultUserAgentProvider to customUserAgent for Bedrock
- Fix: support for `<think>` tags for better compatibility with open-source models
- Fix: refinements to the GLM-4.6 system prompt
## [3.35.1]
- Add: Hicap API integration as provider
- Fix: enable Add Header button in OpenAICompatibleProvider UI
- Fix: Remove orphaned tool_results after truncation and empty content field issues in native tool call
- Fix: render model description in markdown
## [3.35.0]
- Add native tool calling support with configurable setting.
- Auto-approve is now always-on with a redesigned expanding menu. Settings simplified and notifications moved to General Settings.
- added zai-glm-4.6 as a Cerebras model
- Created GPT5 family specific system prompt template
- Fix: show reasoning budget slider to models with valid thinking config
- Requesty base URL, and API key fixes
- Delete all Auth Tokens when logging out
- Support for <think> tags for models that prefer that over <thinking>
## [3.34.1]
- Added support for MiniMax provider with MiniMax-M2 model
- Remove Cline/code-supernova-1-million model
- Changes to allow users to manually enter model names (eg. presets) when using OpenRouter
## [3.34.0]
- Cline Teams is now free through 2025 for unlimited users. Includes Jetbrains, RBAC, centralized billing and more.
- Use the “exacto” versions of GLM-4.6, Kimi-K2, and Qwen3-Coder in the Cline provider for the best balance of cost, speed, accuracy and tool-calling.
## [3.33.1]
- Fix CLI installation copy text
## [3.33.0]
- Added Cline CLI (Preview)
- Added Subagent support (Experimental)
- Added Multi-Root Workspaces support (Enable in feature settings)
- Add auto-retry with exponential backof for failed API requests
## [3.32.8]
- Add Claude Haiku 4.5 support
## [3.32.7]
- Add JP and Global inference profile options to AWS Bedrock
- Adding Improvements to VSCode multi root workspaces
- Added markdown support to focus chain text, allowing the model to display more interesting focus chains
## [3.32.6]
- Add experimental support for VSCode multi root workspaces
- Add Claude Sonnet 4.5 to Claude Code provider
- Add Glm 4.6 to Z AI provider
- Add Glm 4.6 to Z AI provider
## [3.32.5]
+125
View File
@@ -0,0 +1,125 @@
# 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.
## 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.
+7 -1
View File
@@ -46,7 +46,11 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
```bash
npm run install:all
```
4. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
4. Generate Protocol Buffer files (required before first build):
```bash
npm run protos
```
5. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
@@ -85,8 +89,10 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
2. **Local Development**
- Run `npm run install:all` to install dependencies
- Run `npm run protos` to generate Protocol Buffer files (required before first build)
- Run `npm run test` to run tests locally
- Run → Start Debugging or `>Debug: Select and Start Debugging` and wait for a new VS Code instance to open
- **Terminal Workflow**: Use `npm run dev` (generates protos + runs watch mode) or `npm run watch` (if protos already generated)
- Before submitting PR, run `npm run format:fix` to format your code
3. **Linux-specific Setup**
+7 -2
View File
@@ -2,7 +2,7 @@
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
</sub></div>
# Cline \#1 on OpenRouter
# Cline
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
@@ -43,7 +43,7 @@ Thanks to [Claude Sonnet's agentic coding capabilities](https://www.anthropic.c
4. When a task is completed, Cline will present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button.
> [!TIP]
> Use the `CMD/CTRL + Shift + P` shortcut to open the command palette and type "Cline: Open In New Tab" to open the extension as a tab in your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
> Follow [this guide](https://docs.cline.bot/features/customization/opening-cline-in-sidebar) to open Cline on the right side of your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
---
@@ -141,6 +141,11 @@ For example, when working with a local web server, you can use 'Restore Workspac
To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.md) to learn the basics. You can also join our [Discord](https://discord.gg/cline) to chat with other contributors in the `#contributors` channel. If you're looking for full-time work, check out our open positions on our [careers page](https://cline.bot/join-us)!
## Enterprise
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
## License
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>
<json>
<![CDATA[
{
"fontFamily": "cline-bot",
"majorVersion": 1,
"minorVersion": 0,
"fontURL": "https://cline.bot",
"designerURL": "https://cline.bot",
"licenseURL": "https://cline.bot",
"version": "Version 1.0",
"fontId": "cline-bot",
"psName": "cline-bot",
"subFamily": "Regular",
"fullName": "cline-bot",
"description": "Font generated by IcoMoon."
}
]]>
</json>
</metadata>
<defs>
<font id="cline-bot" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" glyph-name="cline" data-tags="cline" horiz-adv-x="977" d="M964.553 383.11l-60.285 121.406v69.495c0 115.545-92.939 209.321-207.647 209.321h-102.986c7.536 15.071 11.722 32.654 11.722 51.074 0 64.471-51.912 116.383-115.545 116.383s-115.545-51.912-115.545-116.383 4.186-35.166 11.722-51.074h-102.986c-114.708 0-207.647-93.776-207.647-209.321v-69.495l-61.959-121.406c-5.861-11.722-5.861-26.793 0-38.515l61.959-119.732v-69.495c0-115.545 92.939-209.321 207.647-209.321h415.294c114.708 0 207.647 93.776 207.647 209.321v69.495l60.285 119.732c5.861 11.722 5.861 25.956 0 38.515v0zM426.178 284.311c0-52.749-42.702-95.451-94.613-95.451s-94.613 42.702-94.613 95.451v169.132c0 52.749 42.702 95.451 94.613 95.451s94.613-42.702 94.613-95.451v-169.132zM731.787 284.311c0-52.749-42.702-95.451-94.613-95.451s-94.613 42.702-94.613 95.451v169.132c0 52.749 42.702 95.451 94.613 95.451s94.613-42.702 94.613-95.451v-169.132z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.
Binary file not shown.
+13 -13
View File
@@ -70,7 +70,7 @@
"noControlCharactersInRegex": "off",
"noShadowRestrictedNames": "off",
"noArrayIndexKey": "info",
"noAssignInExpressions": "warn"
"noAssignInExpressions": "info"
},
"complexity": {
"noUselessConstructor": "off",
@@ -82,7 +82,7 @@
"noStaticOnlyClass": "off"
},
"security": {
"noDangerouslySetInnerHtml": "warn"
"noDangerouslySetInnerHtml": "info"
}
}
},
@@ -114,17 +114,17 @@
"files": {
"includes": [
"**",
"!**/dist/**",
"!**/dist-*/**",
"!**/out/**",
"!**/evals/**",
"!**/playwright/**",
"!**/test-results/**",
"!**/node_modules/**",
"!**/webview-ui/build/**",
"!**/generated/**",
"!**/proto/**",
"!**/tests/specs/**"
"!**/dist",
"!**/dist-*",
"!**/out",
"!**/evals",
"!**/playwright",
"!**/test-results",
"!**/node_modules",
"!**/webview-ui/build",
"!**/generated",
"!**/proto",
"!**/tests/specs"
]
},
"plugins": [
+1 -1
View File
@@ -1,2 +1,2 @@
cline-core-debug.log
bin/*
bin/*
+72
View File
@@ -0,0 +1,72 @@
# Cline CLI
```
/_____/\ /_/\ /_______/\/__/\ /__/\ /_____/\
\:::__\/ \:\ \ \__.::._\/\::\_\\ \ \\::::_\/_
\:\ \ __\:\ \ \::\ \ \:. `-\ \ \\:\/___/\
\:\ \/_/\\:\ \____ _\::\ \__\:. _ \ \\::___\/_
\:\_\ \ \\:\/___/\/__\::\__/\\. \`-\ \ \\:\____/\
\_____\/ \_____\/\________\/ \__\/ \__\/ \_____\/
```
Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more.
## Installation
Install Cline globally using npm:
```bash
npm install -g cline
```
## Usage
```bash
cline
```
This will start the Cline CLI interface where you can interact with the autonomous coding agent.
## Features
- **Autonomous Coding**: AI-powered code generation, editing, and refactoring
- **File Operations**: Create, read, update, and delete files and directories
- **Command Execution**: Run shell commands and scripts
- **Browser Automation**: Interact with web pages and applications
- **Multi-Model Support**: Works with Anthropic Claude, OpenAI GPT, and other AI models
- **MCP Integration**: Extensible through Model Context Protocol servers
- **Project Understanding**: Analyzes codebases to provide context-aware assistance
## Requirements
- Node.js 18.0.0 or higher
- Supported platforms: macOS, Linux. Windows soon
- Supported architectures: x64, arm64
## Configuration
Cline can be configured through:
- Environment variables
- Configuration files
- Command-line arguments
See the [main documentation](https://cline.bot) for detailed configuration options.
## Links
- **Website**: [https://cline.bot](https://cline.bot)
- **Documentation**: [https://docs.cline.bot](https://docs.cline.bot)
- **GitHub**: [https://github.com/cline/cline](https://github.com/cline/cline)
- **VSCode Extension**: Available in the VSCode Marketplace
- **JetBrains Extension**: Available in the JetBrains Marketplace
## License
Apache-2.0 - see [LICENSE](https://github.com/cline/cline/blob/main/LICENSE) for details.
## Support
- Report issues: [GitHub Issues](https://github.com/cline/cline/issues)
- Community: [GitHub Discussions](https://github.com/cline/cline/discussions)
- Documentation: [docs.cline.bot](https://docs.cline.bot)
-6
View File
@@ -1,6 +0,0 @@
/_____/\ /_/\ /_______/\/__/\ /__/\ /_____/\
\:::__\/ \:\ \ \__.::._\/\::\_\\ \ \\::::_\/_
\:\ \ __\:\ \ \::\ \ \:. `-\ \ \\:\/___/\
\:\ \/_/\\:\ \____ _\::\ \__\:. _ \ \\::___\/_
\:\_\ \ \\:\/___/\/__\::\__/\\. \`-\ \ \\:\____/\
\_____\/ \_____\/\________\/ \__\/ \__\/ \_____\/
+299 -8
View File
@@ -2,56 +2,347 @@ package main
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"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/common"
"github.com/cline/grpc-go/cline"
"github.com/spf13/cobra"
)
var (
coreAddress string
cfgFile string
verbose bool
outputFormat string
// Task creation flags (for root command)
images []string
files []string
mode string
settings []string
yolo bool
oneshot bool
)
func main() {
rootCmd := &cobra.Command{
Use: "cline",
Use: "cline [prompt]",
Short: "Cline CLI - AI-powered coding assistant",
Long: `A command-line interface for interacting with Cline AI coding assistant.
This CLI provides access to Cline's task management, configuration, and
monitoring capabilities from the terminal.`,
Start a new task by providing a prompt:
cline "Create a new Python script that prints hello world"
Or pipe a prompt via stdin:
echo "Create a todo app" | cline
cat prompt.txt | cline --yolo
Or run with no arguments to enter interactive mode:
cline
This CLI also provides task management, configuration, and monitoring capabilities.
For detailed documentation including all commands, options, and examples,
see the manual page: man cline`,
Args: cobra.ArbitraryArgs,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if outputFormat != "rich" && outputFormat != "json" && outputFormat != "plain" {
return fmt.Errorf("invalid output format '%s': must be one of 'rich', 'json', or 'plain'", outputFormat)
}
return global.InitializeGlobalConfig(&global.GlobalConfig{
ConfigPath: cfgFile,
Verbose: verbose,
OutputFormat: outputFormat,
CoreAddress: coreAddress,
})
},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
var instanceAddress string
// If --address flag not provided, start instance BEFORE getting prompt
if !cmd.Flags().Changed("address") {
if global.Config.Verbose {
fmt.Println("Starting new Cline instance...")
}
instance, err := global.Clients.StartNewInstance(ctx)
if err != nil {
return fmt.Errorf("failed to start new instance: %w", err)
}
instanceAddress = instance.Address
if global.Config.Verbose {
fmt.Printf("Started instance at %s\n\n", instanceAddress)
}
// Set up cleanup on exit
defer func() {
if global.Config.Verbose {
fmt.Println("\nCleaning up instance...")
}
registry := global.Clients.GetRegistry()
if err := global.KillInstanceByAddress(context.Background(), registry, instanceAddress); 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"))
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)
}
// 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"))
}
} else {
// User specified --address flag, use that
instanceAddress = coreAddress
}
// Get content from both args and stdin
prompt, err := getContentFromStdinAndArgs(args)
if err != nil {
return fmt.Errorf("failed to read prompt: %w", err)
}
// If no prompt from args or stdin, show interactive input
if prompt == "" {
// Pass the mode flag to banner so it shows correct mode
prompt, err = promptForInitialTask(ctx, instanceAddress, mode)
if err != nil {
// Check if user cancelled - exit cleanly without error
if err == huh.ErrUserAborted {
return nil
}
return err
}
if prompt == "" {
return fmt.Errorf("prompt required")
}
}
// If oneshot mode, force plan mode and yolo
if oneshot {
mode = "plan"
yolo = true
}
return cli.CreateAndFollowTask(ctx, prompt, cli.TaskOptions{
Images: images,
Files: files,
Mode: mode,
Settings: settings,
Yolo: yolo,
Address: instanceAddress,
Verbose: verbose,
})
},
}
rootCmd.PersistentFlags().StringVar(&coreAddress, "address", fmt.Sprintf("localhost:%d", common.DEFAULT_CLINE_CORE_PORT), "Cline Core gRPC address")
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cline/config.yaml)")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "o", "rich", "output format (rich|json|plain)")
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "F", "rich", "output format (rich|json|plain)")
// Task creation flags (only apply when using root command with prompt)
rootCmd.Flags().StringSliceVarP(&images, "image", "i", nil, "attach image files")
rootCmd.Flags().StringSliceVarP(&files, "file", "f", nil, "attach files")
rootCmd.Flags().StringVarP(&mode, "mode", "m", "plan", "mode (act|plan) - defaults to plan")
rootCmd.Flags().StringSliceVarP(&settings, "setting", "s", nil, "task settings (key=value format)")
rootCmd.Flags().BoolVarP(&yolo, "yolo", "y", false, "enable yolo mode (non-interactive)")
rootCmd.Flags().BoolVar(&yolo, "no-interactive", false, "enable yolo mode (non-interactive)")
rootCmd.Flags().BoolVarP(&oneshot, "oneshot", "o", false, "full autonomous mode")
rootCmd.AddCommand(cli.NewTaskCommand())
rootCmd.AddCommand(cli.NewInstanceCommand())
rootCmd.AddCommand(cli.NewConfigCommand())
rootCmd.AddCommand(cli.NewVersionCommand())
rootCmd.AddCommand(cli.NewAuthCommand())
rootCmd.AddCommand(cli.NewLogsCommand())
// rootCmd.AddCommand(cli.NewDoctorCommand()) // Disabled for now
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func promptForInitialTask(ctx context.Context, instanceAddress, modeFlag string) (string, error) {
// Show session banner before the initial input
showSessionBanner(ctx, instanceAddress, modeFlag)
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
}
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()
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
return "", huh.ErrUserAborted
}
return "", err
}
return strings.TrimSpace(prompt), nil
}
// showSessionBanner displays session info before initial prompt
func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string) {
bannerInfo := display.BannerInfo{
Version: global.CliVersion,
Mode: modeFlag, // Use the mode from command flag, not state
}
// If mode is empty, default to "plan"
if bannerInfo.Mode == "" {
bannerInfo.Mode = "plan"
}
// Get current working directory (this is what Cline will use)
if cwd, err := os.Getwd(); err == nil {
bannerInfo.Workdir = cwd
}
// 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 providerDisplay != nil {
bannerInfo.Provider = auth.GetProviderIDForEnum(providerDisplay.Provider)
bannerInfo.ModelID = providerDisplay.ModelID
}
}
}
// Render and display banner
banner := display.RenderSessionBanner(bannerInfo)
fmt.Println(banner)
fmt.Println() // Extra spacing before form
}
// 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)
if err != nil {
return false
}
// Get state
state, err := manager.GetClient().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
}
// Check 2: Is user authenticated? (matches extension's || user?.uid check)
if userInfo, ok := stateMap["userInfo"].(map[string]interface{}); ok {
if uid, ok := userInfo["uid"].(string); ok && uid != "" {
return true
}
}
return false
}
// getContentFromStdinAndArgs reads content from both command line args and stdin, and combines them
func getContentFromStdinAndArgs(args []string) (string, error) {
var content strings.Builder
// Add command line args first (if any)
if len(args) > 0 {
content.WriteString(strings.Join(args, " "))
}
// Check if stdin has data
stat, err := os.Stdin.Stat()
if err != nil {
return "", fmt.Errorf("failed to stat stdin: %w", err)
}
// Check if data is being piped to stdin
if (stat.Mode() & os.ModeCharDevice) == 0 {
// Only try to read if there's actually data available
if stat.Size() > 0 {
stdinBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return "", fmt.Errorf("failed to read from stdin: %w", err)
}
stdinContent := strings.TrimSpace(string(stdinBytes))
if stdinContent != "" {
if content.Len() > 0 {
content.WriteString(" ")
}
content.WriteString(stdinContent)
}
}
}
return content.String(), nil
}
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"time"
"github.com/cline/cli/pkg/common"
_ "github.com/mattn/go-sqlite3"
_ "github.com/glebarez/go-sqlite"
"google.golang.org/grpc/health/grpc_health_v1"
)
@@ -25,7 +25,7 @@ func readInstancesFromSQLite(t *testing.T, clineDir string) []common.CoreInstanc
return []common.CoreInstanceInfo{}
}
db, err := sql.Open("sqlite3", dbPath)
db, err := sql.Open("sqlite", dbPath)
if err != nil {
t.Logf("Warning: Failed to open SQLite database: %v", err)
return []common.CoreInstanceInfo{}
@@ -97,7 +97,7 @@ func readDefaultInstanceFromSettings(t *testing.T, clineDir string) string {
func insertRemoteInstanceIntoSQLite(t *testing.T, dbPath, address string, corePort, hostPort int) error {
t.Helper()
db, err := sql.Open("sqlite3", dbPath)
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return err
}
@@ -142,7 +142,7 @@ func insertRemoteInstanceIntoSQLite(t *testing.T, dbPath, address string, corePo
func verifyInstanceExistsInSQLite(t *testing.T, dbPath, address string) bool {
t.Helper()
db, err := sql.Open("sqlite3", dbPath)
db, err := sql.Open("sqlite", dbPath)
if err != nil {
t.Logf("Failed to open database: %v", err)
return false
+44 -2
View File
@@ -3,20 +3,62 @@ module github.com/cline/cli
go 1.23.0
require (
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/huh v0.7.1-0.20251005153135-a01a1e304532
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/cline/grpc-go v0.0.0
github.com/mattn/go-sqlite3 v1.14.24
github.com/glebarez/go-sqlite v1.22.0
github.com/muesli/termenv v0.16.0
github.com/spf13/cobra v1.8.0
golang.org/x/term v0.32.0
google.golang.org/grpc v1.75.0
google.golang.org/protobuf v1.36.6
)
replace github.com/cline/grpc-go => ../src/generated/grpc-go
require (
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/protobuf v1.36.6 // indirect
modernc.org/libc v1.37.6 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.28.0 // indirect
)
+116 -2
View File
@@ -1,4 +1,64 @@
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/huh v0.7.1-0.20251005153135-a01a1e304532 h1:+xmbw70JXxmsOqvm1PEIAqFnqI/Hy2RYqrK7CtPmsNY=
github.com/charmbracelet/huh v0.7.1-0.20251005153135-a01a1e304532/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -7,17 +67,55 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
@@ -30,10 +128,18 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
@@ -46,3 +152,11 @@ google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9x
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
+331
View File
@@ -0,0 +1,331 @@
.\" Automatically generated by Pandoc 3.8.2
.\"
.TH "CLINE" "1" "January 2025" "Cline CLI 1.0" "User Commands"
.SH NAME
cline \- orchestrate and interact with Cline AI coding agents
.SH SYNOPSIS
\f[B]cline\f[R] [\f[I]prompt\f[R]] [\f[I]options\f[R]]
.PP
\f[B]cline\f[R] \f[I]command\f[R] [\f[I]subcommand\f[R]]
[\f[I]options\f[R]] [\f[I]arguments\f[R]]
.SH DESCRIPTION
Try: cat README.md | cline \(lqSummarize this for me:\(rq
.PP
\f[B]cline\f[R] is a command\-line interface for orchestrating multiple
Cline AI coding agents.
Cline is an autonomous AI agent who can read, write, and execute code
across your projects.
He operates through a client\-server architecture where \f[B]Cline
Core\f[R] runs as a standalone service, and the CLI acts as a scriptable
interface for managing tasks, instances, and agent interactions.
.PP
The CLI is designed for both interactive use and automation, making it
ideal for CI/CD pipelines, parallel task execution, and terminal\-based
workflows.
Multiple frontends (CLI, VSCode, JetBrains) can attach to the same Cline
Core instance, enabling seamless task handoff between environments.
.SH MODES OF OPERATION
.TP
\f[B]Instant Task Mode\f[R]
The simplest invocation: \f[B]cline \(lqprompt here\(rq\f[R] immediately
spawns an instance, creates a task, and enters chat mode.
This is equivalent to running \f[B]cline instance new && cline task new
&& cline task chat\f[R] in sequence.
.TP
\f[B]Subcommand Mode\f[R]
Advanced usage with explicit control: \f[B]cline <command> [subcommand]
[options]\f[R] provides fine\-grained control over instances, tasks,
authentication, and configuration.
.SH AGENT BEHAVIOR
Cline operates in two primary modes:
.TP
\f[B]ACT MODE\f[R]
Cline actively uses tools to accomplish tasks.
He can read files, write code, execute commands, use a headless browser,
and more.
This is the default mode for task execution.
.TP
\f[B]PLAN MODE\f[R]
Cline gathers information and creates a detailed plan before
implementation.
He explores the codebase, asks clarifying questions, and presents a
strategy for user approval before switching to ACT MODE.
.SH INSTANT TASK OPTIONS
When using the instant task syntax \f[B]cline \(lqprompt\(rq\f[R] the
following options are available:
.TP
\f[B]\-o\f[R], \f[B]\-\-oneshot\f[R]
Full autonomous mode.
Cline completes the task and stops following after completion.
Example: cline \-o \(lqwhat\(cqs 6 + 8?\(rq
.TP
\f[B]\-s\f[R], \f[B]\-\-setting\f[R] \f[I]setting\f[R] \f[I]value\f[R]
Override a setting for this task
.TP
\f[B]\-y\f[R], \f[B]\-\-no\-interactive\f[R], \f[B]\-\-yolo\f[R]
Enable fully autonomous mode.
Disables all interactivity:
.RS
.IP \(bu 2
ask_followup_question tool is disabled
.IP \(bu 2
attempt_completion happens automatically
.IP \(bu 2
execute_command runs in non\-blocking mode with timeout
.IP \(bu 2
PLAN MODE automatically switches to ACT MODE
.RE
.TP
\f[B]\-m\f[R], \f[B]\-\-mode\f[R] \f[I]mode\f[R]
Starting mode.
Options: \f[B]act\f[R] (default), \f[B]plan\f[R]
.SH GLOBAL OPTIONS
These options apply to all subcommands:
.TP
\f[B]\-F\f[R], \f[B]\-\-output\-format\f[R] \f[I]format\f[R]
Output format.
Options: \f[B]rich\f[R] (default), \f[B]json\f[R], \f[B]plain\f[R]
.TP
\f[B]\-h\f[R], \f[B]\-\-help\f[R]
Display help information for the command.
.TP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R]
Enable verbose output for debugging.
.SH COMMANDS
.SS Authentication
\f[B]cline auth\f[R] [\f[I]provider\f[R]] [\f[I]key\f[R]]
.TP
\f[B]cline a\f[R] [\f[I]provider\f[R]] [\f[I]key\f[R]]
Configure authentication for AI model providers.
Launches an interactive wizard if no arguments provided.
If provider is specified without a key, prompts for the key or launches
the appropriate OAuth flow.
.SS Instance Management
Cline Core instances are independent agent processes that can run in the
background.
Multiple instances can run simultaneously, enabling parallel task
execution.
.PP
\f[B]cline instance\f[R]
.TP
\f[B]cline i\f[R]
Display instance management help.
.PP
\f[B]cline instance new\f[R] [\f[B]\-d\f[R]|\f[B]\-\-default\f[R]]
.TP
\f[B]cline i n\f[R] [\f[B]\-d\f[R]|\f[B]\-\-default\f[R]]
Spawn a new Cline Core instance.
Use \f[B]\-\-default\f[R] to set it as the default instance for
subsequent commands.
.PP
\f[B]cline instance list\f[R]
.TP
\f[B]cline i l\f[R]
List all running Cline Core instances with their addresses and status.
.PP
\f[B]cline instance default\f[R] \f[I]address\f[R]
.TP
\f[B]cline i d\f[R] \f[I]address\f[R]
Set the default instance to avoid specifying \f[B]\-\-address\f[R] in
task commands.
.PP
\f[B]cline instance kill\f[R] \f[I]address\f[R]
[\f[B]\-a\f[R]|\f[B]\-\-all\f[R]]
.TP
\f[B]cline i k\f[R] \f[I]address\f[R] [\f[B]\-a\f[R]|\f[B]\-\-all\f[R]]
Terminate a Cline Core instance.
Use \f[B]\-\-all\f[R] to kill all running instances.
.SS Task Management
Tasks represent individual work items that Cline executes.
Tasks maintain conversation history, checkpoints, and settings.
.PP
\f[B]cline task\f[R] [\f[B]\-a\f[R]|\f[B]\-\-address\f[R]
\f[I]ADDR\f[R]]
.TP
\f[B]cline t\f[R] [\f[B]\-a\f[R]|\f[B]\-\-address\f[R] \f[I]ADDR\f[R]]
Display task management help.
The \f[B]\-\-address\f[R] flag specifies which Cline Core instance to
use (e.g., localhost:50052).
.PP
\f[B]cline task new\f[R] \f[I]prompt\f[R] [\f[I]options\f[R]]
.TP
\f[B]cline t n\f[R] \f[I]prompt\f[R] [\f[I]options\f[R]]
Create a new task in the default or specified instance.
Options:
.RS
.TP
\f[B]\-s\f[R], \f[B]\-\-setting\f[R] \f[I]setting\f[R] \f[I]value\f[R]
Set task\-specific settings
.TP
\f[B]\-y\f[R], \f[B]\-\-no\-interactive\f[R], \f[B]\-\-yolo\f[R]
Enable autonomous mode
.TP
\f[B]\-m\f[R], \f[B]\-\-mode\f[R] \f[I]mode\f[R]
Starting mode (act or plan)
.RE
.PP
\f[B]cline task open\f[R] \f[I]task\-id\f[R] [\f[I]options\f[R]]
.TP
\f[B]cline t o\f[R] \f[I]task\-id\f[R] [\f[I]options\f[R]]
Resume a previous task from history.
Accepts the same options as \f[B]task new\f[R].
.PP
\f[B]cline task list\f[R]
.TP
\f[B]cline t l\f[R]
List all tasks in history with their id and snippet
.PP
\f[B]cline task chat\f[R]
.TP
\f[B]cline t c\f[R]
Enter interactive chat mode for the current task.
Allows back\-and\-forth conversation with Cline.
.PP
\f[B]cline task send\f[R] [\f[I]message\f[R]] [\f[I]options\f[R]]
.TP
\f[B]cline t s\f[R] [\f[I]message\f[R]] [\f[I]options\f[R]]
Send a message to Cline.
If no message is provided, reads from stdin.
Options:
.RS
.TP
\f[B]\-a\f[R], \f[B]\-\-approve\f[R]
Approve Cline\(cqs proposed action
.TP
\f[B]\-d\f[R], \f[B]\-\-deny\f[R]
Deny Cline\(cqs proposed action
.TP
\f[B]\-f\f[R], \f[B]\-\-file\f[R] \f[I]FILE\f[R]
Attach a file to the message
.TP
\f[B]\-y\f[R], \f[B]\-\-no\-interactive\f[R], \f[B]\-\-yolo\f[R]
Enable autonomous mode
.TP
\f[B]\-m\f[R], \f[B]\-\-mode\f[R] \f[I]mode\f[R]
Switch mode (act or plan)
.RE
.PP
\f[B]cline task view\f[R] [\f[B]\-f\f[R]|\f[B]\-\-follow\f[R]]
[\f[B]\-c\f[R]|\f[B]\-\-follow\-complete\f[R]]
.TP
\f[B]cline t v\f[R] [\f[B]\-f\f[R]|\f[B]\-\-follow\f[R]] [\f[B]\-c\f[R]|\f[B]\-\-follow\-complete\f[R]]
Display the current conversation.
Use \f[B]\-\-follow\f[R] to stream updates in real\-time, or
\f[B]\-\-follow\-complete\f[R] to follow until task completion.
.PP
\f[B]cline task restore\f[R] \f[I]checkpoint\f[R]
.TP
\f[B]cline t r\f[R] \f[I]checkpoint\f[R]
Restore the task to a previous checkpoint state.
.PP
\f[B]cline task pause\f[R]
.TP
\f[B]cline t p\f[R]
Pause task execution.
.SS Configuration
Configuration can be set globally.
Override these global settings for a task using the
\f[B]\-\-setting\f[R] flag
.PP
\f[B]cline config\f[R]
.PP
\f[B]cline c\f[R]
.PP
\f[B]cline config set\f[R] \f[I]key\f[R] \f[I]value\f[R]
.TP
\f[B]cline c s\f[R] \f[I]key\f[R] \f[I]value\f[R]
Set a configuration variable.
.PP
\f[B]cline config get\f[R] \f[I]key\f[R]
.TP
\f[B]cline c g\f[R] \f[I]key\f[R]
Read a configuration variable.
.PP
\f[B]cline config list\f[R]
.TP
\f[B]cline c l\f[R]
List all configuration variables and their values.
.SH TASK SETTINGS
Task settings are persisted in the \f[I]\(ti/.cline/x/tasks\f[R]
directory.
When resuming a task with \f[B]cline task open\f[R], task settings are
automatically restored.
.PP
Common settings include:
.TP
\f[B]yolo\f[R]
Enable autonomous mode (true/false)
.TP
\f[B]mode\f[R]
Starting mode (act/plan)
.SH NOTES & EXAMPLES
The \f[B]cline task send\f[R] and \f[B]cline task new\f[R] commands
support reading from stdin, enabling powerful pipeline compositions:
.IP
.EX
cat requirements.txt \f[B]|\f[R] cline task send
echo \(dqRefactor this code\(dq \f[B]|\f[R] cline \-y
.EE
.SS Instance Management
Manage multiple Cline instances:
.IP
.EX
\f[I]# Start a new instance and make it default\f[R]
cline instance new \-\-default
\f[I]# List all running instances\f[R]
cline instance list
\f[I]# Kill a specific instance\f[R]
cline instance kill localhost:50052
\f[I]# Kill all CLI instances\f[R]
cline instance kill \-\-all\-cli
.EE
.SS Task History
Work with task history:
.IP
.EX
\f[I]# List previous tasks\f[R]
cline task list
\f[I]# Resume a previous task\f[R]
cline task open 1760501486669
\f[I]# View conversation history\f[R]
cline task view
\f[I]# Start interactive chat with this task\f[R]
cline task chat
.EE
.SH ARCHITECTURE
Cline operates on a three\-layer architecture:
.TP
\f[B]Presentation Layer\f[R]
User interfaces (CLI, VSCode, JetBrains) that connect to Cline Core via
gRPC
.TP
\f[B]Cline Core\f[R]
The autonomous agent service handling task management, AI model
integration, state management, tool orchestration, and real\-time
streaming updates
.TP
\f[B]Host Provider Layer\f[R]
Environment\-specific integrations (VSCode APIs, JetBrains APIs, shell
APIs) that Cline Core uses to interact with the host system
.SH BUGS
Report bugs at: \c
.UR https://github.com/cline/cline/issues
.UE \c
.PP
For real\-time help, join the Discord community at: \c
.UR https://discord.gg/cline
.UE \c
.SH SEE ALSO
Full documentation: \c
.UR https://docs.cline.bot
.UE \c
.SH AUTHORS
Cline is developed by the Cline Bot Inc.\ and the open source community.
.SH COPYRIGHT
Copyright © 2025 Cline Bot Inc.\ Licensed under the Apache License 2.0.
+332
View File
@@ -0,0 +1,332 @@
---
title: CLINE
section: 1
header: User Commands
footer: Cline CLI 1.0
date: January 2025
---
# NAME
cline - orchestrate and interact with Cline AI coding agents
# SYNOPSIS
**cline** [*prompt*] [*options*]
**cline** *command* [*subcommand*] [*options*] [*arguments*]
# DESCRIPTION
Try: cat README.md | cline "Summarize this for me:"
**cline** is a command-line interface for orchestrating multiple Cline AI coding agents. Cline is an autonomous AI agent who can read, write, and execute code across your projects. He operates through a client-server architecture where **Cline Core** runs as a standalone service, and the CLI acts as a scriptable interface for managing tasks, instances, and agent interactions.
The CLI is designed for both interactive use and automation, making it ideal for CI/CD pipelines, parallel task execution, and terminal-based workflows. Multiple frontends (CLI, VSCode, JetBrains) can attach to the same Cline Core instance, enabling seamless task handoff between environments.
# MODES OF OPERATION
**Instant Task Mode**
: The simplest invocation: **cline "prompt here"** immediately spawns an instance, creates a task, and enters chat mode. This is equivalent to running **cline instance new && cline task new && cline task chat** in sequence.
**Subcommand Mode**
: Advanced usage with explicit control: **cline \<command\> [subcommand] [options]** provides fine-grained control over instances, tasks, authentication, and configuration.
# AGENT BEHAVIOR
Cline operates in two primary modes:
**ACT MODE**
: Cline actively uses tools to accomplish tasks. He can read files, write code, execute commands, use a headless browser, and more. This is the default mode for task execution.
**PLAN MODE**
: Cline gathers information and creates a detailed plan before implementation. He explores the codebase, asks clarifying questions, and presents a strategy for user approval before switching to ACT MODE.
# INSTANT TASK OPTIONS
When using the instant task syntax **cline "prompt"** the following options are available:
**-o**, **\--oneshot**
: Full autonomous mode. Cline completes the task and stops following after completion. Example: cline -o "what's 6 + 8?"
**-s**, **\--setting** *setting* *value*
: Override a setting for this task
**-y**, **\--no-interactive**, **\--yolo**
: Enable fully autonomous mode. Disables all interactivity:
- ask_followup_question tool is disabled
- attempt_completion happens automatically
- execute_command runs in non-blocking mode with timeout
- PLAN MODE automatically switches to ACT MODE
**-m**, **\--mode** *mode*
: Starting mode. Options: **act** (default), **plan**
# GLOBAL OPTIONS
These options apply to all subcommands:
**-F**, **\--output-format** *format*
: Output format. Options: **rich** (default), **json**, **plain**
**-h**, **\--help**
: Display help information for the command.
**-v**, **\--verbose**
: Enable verbose output for debugging.
# COMMANDS
## Authentication
**cline auth** [*provider*] [*key*]
**cline a** [*provider*] [*key*]
: Configure authentication for AI model providers. Launches an interactive wizard if no arguments provided. If provider is specified without a key, prompts for the key or launches the appropriate OAuth flow.
## Instance Management
Cline Core instances are independent agent processes that can run in the background. Multiple instances can run simultaneously, enabling parallel task execution.
**cline instance**
**cline i**
: Display instance management help.
**cline instance new** [**-d**|**\--default**]
**cline i n** [**-d**|**\--default**]
: Spawn a new Cline Core instance. Use **\--default** to set it as the default instance for subsequent commands.
**cline instance list**
**cline i l**
: List all running Cline Core instances with their addresses and status.
**cline instance default** *address*
**cline i d** *address*
: Set the default instance to avoid specifying **\--address** in task commands.
**cline instance kill** *address* [**-a**|**\--all**]
**cline i k** *address* [**-a**|**\--all**]
: Terminate a Cline Core instance. Use **\--all** to kill all running instances.
## Task Management
Tasks represent individual work items that Cline executes. Tasks maintain conversation history, checkpoints, and settings.
**cline task** [**-a**|**\--address** *ADDR*]
**cline t** [**-a**|**\--address** *ADDR*]
: Display task management help. The **\--address** flag specifies which Cline Core instance to use (e.g., localhost:50052).
**cline task new** *prompt* [*options*]
**cline t n** *prompt* [*options*]
: Create a new task in the default or specified instance. Options:
**-s**, **\--setting** *setting* *value*
: Set task-specific settings
**-y**, **\--no-interactive**, **\--yolo**
: Enable autonomous mode
**-m**, **\--mode** *mode*
: Starting mode (act or plan)
**cline task open** *task-id* [*options*]
**cline t o** *task-id* [*options*]
: Resume a previous task from history. Accepts the same options as **task new**.
**cline task list**
**cline t l**
: List all tasks in history with their id and snippet
**cline task chat**
**cline t c**
: Enter interactive chat mode for the current task. Allows back-and-forth conversation with Cline.
**cline task send** [*message*] [*options*]
**cline t s** [*message*] [*options*]
: Send a message to Cline. If no message is provided, reads from stdin. Options:
**-a**, **\--approve**
: Approve Cline's proposed action
**-d**, **\--deny**
: Deny Cline's proposed action
**-f**, **\--file** *FILE*
: Attach a file to the message
**-y**, **\--no-interactive**, **\--yolo**
: Enable autonomous mode
**-m**, **\--mode** *mode*
: Switch mode (act or plan)
**cline task view** [**-f**|**\--follow**] [**-c**|**\--follow-complete**]
**cline t v** [**-f**|**\--follow**] [**-c**|**\--follow-complete**]
: Display the current conversation. Use **\--follow** to stream updates in real-time, or **\--follow-complete** to follow until task completion.
**cline task restore** *checkpoint*
**cline t r** *checkpoint*
: Restore the task to a previous checkpoint state.
**cline task pause**
**cline t p**
: Pause task execution.
## Configuration
Configuration can be set globally. Override these global settings for a task using the **\--setting** flag
**cline config**
**cline c**
**cline config set** *key* *value*
**cline c s** *key* *value*
: Set a configuration variable.
**cline config get** *key*
**cline c g** *key*
: Read a configuration variable.
**cline config list**
**cline c l**
: List all configuration variables and their values.
# TASK SETTINGS
Task settings are persisted in the *~/.cline/x/tasks* directory. When resuming a task with **cline task open**, task settings are automatically restored.
Common settings include:
**yolo**
: Enable autonomous mode (true/false)
**mode**
: Starting mode (act/plan)
# NOTES & EXAMPLES
The **cline task send** and **cline task new** commands support reading from stdin, enabling powerful pipeline compositions:
```bash
cat requirements.txt | cline task send
echo "Refactor this code" | cline -y
```
## Instance Management
Manage multiple Cline instances:
```bash
# Start a new instance and make it default
cline instance new --default
# List all running instances
cline instance list
# Kill a specific instance
cline instance kill localhost:50052
# Kill all CLI instances
cline instance kill --all-cli
```
## Task History
Work with task history:
```bash
# List previous tasks
cline task list
# Resume a previous task
cline task open 1760501486669
# View conversation history
cline task view
# Start interactive chat with this task
cline task chat
```
# ARCHITECTURE
Cline operates on a three-layer architecture:
**Presentation Layer**
: User interfaces (CLI, VSCode, JetBrains) that connect to Cline Core via gRPC
**Cline Core**
: The autonomous agent service handling task management, AI model integration, state management, tool orchestration, and real-time streaming updates
**Host Provider Layer**
: Environment-specific integrations (VSCode APIs, JetBrains APIs, shell APIs) that Cline Core uses to interact with the host system
# BUGS
Report bugs at: <https://github.com/cline/cline/issues>
For real-time help, join the Discord community at: <https://discord.gg/cline>
# SEE ALSO
Full documentation: <https://docs.cline.bot>
# AUTHORS
Cline is developed by the Cline Bot Inc. and the open source community.
# COPYRIGHT
Copyright © 2025 Cline Bot Inc. Licensed under the Apache License 2.0.
+68
View File
@@ -0,0 +1,68 @@
{
"name": "cline",
"version": "1.0.3",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "cline-core.js",
"bin": {
"cline": "./bin/cline",
"cline-host": "./bin/cline-host"
},
"man": "./man/cline.1",
"scripts": {
"postinstall": "node postinstall.js"
},
"bundleDependencies": [
"@grpc/grpc-js",
"@grpc/reflection",
"better-sqlite3",
"grpc-health-check",
"open",
"vscode-uri"
],
"engines": {
"node": ">=20.0.0"
},
"keywords": [
"cline",
"claude",
"dev",
"mcp",
"openrouter",
"coding",
"agent",
"autonomous",
"chatgpt",
"sonnet",
"ai",
"llama",
"cli"
],
"author": {
"name": "Cline Bot Inc."
},
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/cline/cline"
},
"homepage": "https://cline.bot",
"bugs": {
"url": "https://github.com/cline/cline/issues"
},
"dependencies": {
"@grpc/grpc-js": "^1.13.3",
"@grpc/reflection": "^1.0.4",
"better-sqlite3": "^12.2.0",
"grpc-health-check": "^2.0.2",
"open": "^10.1.2",
"vscode-uri": "^3.1.0"
},
"os": [
"darwin",
"linux"
],
"cpu": [
"x64",
"arm64"
]
}
+31 -113
View File
@@ -1,125 +1,43 @@
package cli
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/grpc-go/cline"
"github.com/cline/cli/pkg/cli/auth"
"github.com/spf13/cobra"
)
var isSessionAuthenticated bool
func NewAuthCommand() *cobra.Command {
return &cobra.Command{
cmd := &cobra.Command{
Use: "auth",
Short: "Sign in to Cline",
Long: `Complete the authentication flow in browser to sign in to Cline.`,
Short: "Authenticate a provider and configure what model is used",
Long: `Authenticate a provider and configure what model is used
Interactive Mode:
Run without flags to open an interactive menu where you can:
- Sign in to your Cline account
- Configure other LLM providers (Anthropic, OpenAI, etc.)
- Select and switch between AI models
- Manage provider settings
Quick Setup Mode:
Use flags to quickly configure a BYO provider non-interactively:
Examples:
cline auth --provider openai-native --apikey sk-xxx --modelid gpt-5
cline auth -p anthropic -k sk-ant-xxx -m claude-sonnet-4-5-20250929
cline auth -p openai-compatible -k xxx -m gpt-4 -b https://api.example.com/v1
Supported providers: openai-native, openai, anthropic, gemini, openrouter, xai, cerebras, ollama
Note: Bedrock provider requires interactive setup due to complex auth fields`,
RunE: func(cmd *cobra.Command, args []string) error {
return handleAuthCommand(cmd.Context())
return auth.RunAuthFlow(cmd.Context(), args)
},
}
}
func handleAuthCommand(ctx context.Context) error {
fmt.Print("Authenticating with Cline...\n")
if IsAuthenticated(ctx) {
return signOutDialog(ctx)
}
if err := signIn(ctx); err != nil {
return err
}
fmt.Println("You are signed in!")
return nil
}
func signOut(ctx context.Context) error {
client, err := global.GetDefaultClient(ctx)
if err != nil {
return err
}
if _, err = client.Account.AccountLogoutClicked(ctx, &cline.EmptyRequest{}); err != nil {
return err
}
isSessionAuthenticated = false
fmt.Println("You have been signed out of Cline.")
return nil
}
func signOutDialog(ctx context.Context) error {
fmt.Print("You are already signed in to Cline.\nWould you like to sign out? (y/N): ")
scanner := bufio.NewScanner(os.Stdin)
if !scanner.Scan() {
return nil
}
response := strings.ToLower(strings.TrimSpace(scanner.Text()))
if response == "y" || response == "yes" {
if err := signOut(ctx); err != nil {
fmt.Printf("Failed to sign out: %v\n", err)
return err
}
}
return nil
}
func signIn(ctx context.Context) error {
if IsAuthenticated(ctx) {
return nil
}
verboseLog("Ensuring default instance exists...")
if err := ensureDefaultInstance(ctx); err != nil {
verboseLog("Failed to ensure default instance: %v", err)
return err
}
verboseLog("Default instance ensured successfully.")
time.Sleep(2 * time.Second) // Allow services to start
client, err := global.GetDefaultClient(ctx)
if err != nil {
verboseLog("Failed to obtain client: %v", err)
return err
}
_, err = client.Account.AccountLoginClicked(ctx, &cline.EmptyRequest{})
if err != nil {
verboseLog("Failed to login: %v", err)
return err
}
isSessionAuthenticated = true
verboseLog("Login successful")
return nil
}
func IsAuthenticated(ctx context.Context) bool {
if isSessionAuthenticated {
return true
}
client, err := global.GetDefaultClient(ctx)
if err != nil {
return false
}
_, err = client.Account.GetUserCredits(ctx, &cline.EmptyRequest{})
return err == nil
}
func verboseLog(format string, args ...interface{}) {
if global.Config != nil && global.Config.Verbose {
fmt.Printf("[VERBOSE] "+format+"\n", args...)
}
// Add flags for quick setup mode
cmd.Flags().StringVarP(&auth.QuickProvider, "provider", "p", "", "Provider ID for quick setup (e.g., openai-native, anthropic)")
cmd.Flags().StringVarP(&auth.QuickAPIKey, "apikey", "k", "", "API key for the provider")
cmd.Flags().StringVarP(&auth.QuickModelID, "modelid", "m", "", "Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929)")
cmd.Flags().StringVarP(&auth.QuickBaseURL, "baseurl", "b", "", "Base URL (optional, only for openai provider)")
return cmd
}
+285
View File
@@ -0,0 +1,285 @@
package auth
import (
"context"
"fmt"
"time"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
var isSessionAuthenticated bool
// Cline provider specific code
func HandleClineAuth(ctx context.Context) error {
verboseLog("Authenticating with Cline...")
// Check if already authenticated
if IsAuthenticated(ctx) {
return signOutDialog(ctx)
}
// Perform sign in
if err := signIn(ctx); err != nil {
return err
}
fmt.Println()
verboseLog("✓ You are signed in!")
// Configure default Cline model after successful authentication
if err := configureDefaultClineModel(ctx); err != nil {
fmt.Printf("Warning: Could not configure default Cline model: %v\n", err)
fmt.Println("You can configure a model later with 'cline auth' and selecting 'Change Cline model'")
}
// Return to main auth menu after successful authentication
return HandleAuthMenuNoArgs(ctx)
}
func signOut(ctx context.Context) error {
client, err := global.GetDefaultClient(ctx)
if err != nil {
return err
}
if _, err = client.Account.AccountLogoutClicked(ctx, &cline.EmptyRequest{}); err != nil {
return err
}
isSessionAuthenticated = false
fmt.Println("You have been signed out of Cline.")
return nil
}
func signOutDialog(ctx context.Context) error {
var confirm bool
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("You are already signed in to Cline.").
Description("Would you like to sign out?").
Value(&confirm),
),
)
if err := form.Run(); err != nil {
return nil
}
if confirm {
if err := signOut(ctx); err != nil {
fmt.Printf("Failed to sign out: %v\n", err)
return err
}
}
return HandleAuthMenuNoArgs(ctx)
}
func signIn(ctx context.Context) error {
if IsAuthenticated(ctx) {
return nil
}
// Subscribe to auth updates before initiating login
verboseLog("Subscribing to auth status updates...")
listener, err := NewAuthStatusListener(ctx)
if err != nil {
verboseLog("Failed to subscribe to auth updates: %v", err)
return fmt.Errorf("failed to subscribe to auth updates: %w", err)
}
defer listener.Stop()
if err := listener.Start(); err != nil {
verboseLog("Failed to start auth listener: %v", err)
return fmt.Errorf("failed to start auth listener: %w", err)
}
// Initiate login (opens browser with callback URL from cline-core's AuthHandler)
verboseLog("Initiating login...")
client, err := global.GetDefaultClient(ctx)
if err != nil {
verboseLog("Failed to obtain client: %v", err)
return fmt.Errorf("failed to obtain client: %w", err)
}
response, err := client.Account.AccountLoginClicked(ctx, &cline.EmptyRequest{})
if err != nil {
verboseLog("Failed to initiate login: %v", err)
return fmt.Errorf("failed to initiate login: %w", err)
}
fmt.Println("\n Opening browser for authentication...")
if response != nil && response.Value != "" {
fmt.Printf(" If the browser doesn't open automatically, visit this URL:\n %s\n\n", response.Value)
}
fmt.Println(" Waiting for you to complete authentication in your browser...")
fmt.Println(" (This may take a few moments. Timeout: 5 minutes)")
// Wait for auth status update confirming success
verboseLog("Waiting for authentication to complete...")
if err := listener.WaitForAuthentication(5 * time.Minute); err != nil {
verboseLog("Authentication failed or timed out: %v", err)
fmt.Println("\n Authentication failed or timed out.")
fmt.Println(" Please try again with 'cline auth'")
return err
}
// Only NOW set the session flag after confirmed authentication
isSessionAuthenticated = true
verboseLog("Login successful")
return nil
}
func IsAuthenticated(ctx context.Context) bool {
if isSessionAuthenticated {
verboseLog("Session is already authenticated")
return true
}
verboseLog("Verifying authentication with server...")
client, err := global.GetDefaultClient(ctx)
if err != nil {
verboseLog("Failed to get client for auth check: %v", err)
return false
}
_, err = client.Account.GetUserCredits(ctx, &cline.EmptyRequest{})
if err == nil {
// Update session variable for future fast-path checks
verboseLog("Server verification successful, updating session flag")
isSessionAuthenticated = true
return true
}
verboseLog("Server verification failed: %v", err)
return false
}
// HandleChangeClineModel allows Cline-authenticated users to change their Cline model selection. Hidden when not authenticated.
func HandleChangeClineModel(ctx context.Context) error {
// Ensure user is authenticated
if !IsAuthenticated(ctx) {
return fmt.Errorf("you must be authenticated with Cline to change models. Run 'cline auth' to sign in")
}
// Get task manager
manager, err := createTaskManager(ctx)
if err != nil {
return fmt.Errorf("failed to create task manager: %w", err)
}
// Launch Cline model selection
return SelectClineModel(ctx, manager)
}
// configureDefaultClineModel configures the default Cline model after authentication
func configureDefaultClineModel(ctx context.Context) error {
verboseLog("Configuring default Cline model...")
// Create task manager
manager, err := task.NewManagerForDefault(ctx)
if err != nil {
return fmt.Errorf("failed to create task manager: %w", err)
}
// Set default Cline model
return SetDefaultClineModel(ctx, manager)
}
// HandleSelectOrganization allows Cline-authenticated users to select which organization to use
func HandleSelectOrganization(ctx context.Context) error {
// Ensure user is authenticated
if !IsAuthenticated(ctx) {
return fmt.Errorf("you must be authenticated with Cline to select an organization. Run 'cline auth' to sign in")
}
// Get client
client, err := global.GetDefaultClient(ctx)
if err != nil {
return fmt.Errorf("failed to get client: %w", err)
}
// Fetch user organizations
orgsResponse, err := client.Account.GetUserOrganizations(ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to fetch organizations: %w", err)
}
organizations := orgsResponse.GetOrganizations()
if len(organizations) == 0 {
fmt.Println("You don't have any organizations yet.")
fmt.Println("Visit https://app.cline.bot/dashboard to create an organization.")
return HandleAuthMenuNoArgs(ctx)
}
// Build options list: Personal + Organizations
var options []huh.Option[string]
options = append(options, huh.NewOption("Personal", "personal"))
for _, org := range organizations {
displayName := org.Name
// Show active indicator
if org.Active {
displayName = fmt.Sprintf("%s (active)", displayName)
}
options = append(options, huh.NewOption(displayName, org.OrganizationId))
}
options = append(options, huh.NewOption("(Cancel)", "cancel"))
// Show selection menu
var selected string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Select which account to use").
Options(options...).
Value(&selected),
),
)
if err := form.Run(); err != nil {
return fmt.Errorf("failed to select organization: %w", err)
}
if selected == "cancel" {
return HandleAuthMenuNoArgs(ctx)
}
// Set the organization
var orgId *string
if selected != "personal" {
orgId = &selected
}
req := &cline.UserOrganizationUpdateRequest{
OrganizationId: orgId,
}
if _, err := client.Account.SetUserOrganization(ctx, req); err != nil {
return fmt.Errorf("failed to set organization: %w", err)
}
if selected == "personal" {
fmt.Println("✓ Switched to personal account")
} else {
// Find the org name to display
var orgName string
for _, org := range organizations {
if org.OrganizationId == selected {
orgName = org.Name
break
}
}
fmt.Printf("✓ Switched to organization: %s\n", orgName)
}
return HandleAuthMenuNoArgs(ctx)
}
+323
View File
@@ -0,0 +1,323 @@
package auth
import (
"context"
"fmt"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// contextKey is a distinct type for context keys to avoid collisions
type contextKey string
const authInstanceAddressKey contextKey = "authInstanceAddress"
// AuthAction represents the type of authentication action
type AuthAction string
const (
AuthActionClineLogin AuthAction = "cline_login"
AuthActionBYOSetup AuthAction = "provider_setup"
AuthActionChangeClineModel AuthAction = "change_cline_model"
AuthActionSelectOrganization AuthAction = "select_organization"
AuthActionSelectProvider AuthAction = "select_provider"
AuthActionExit AuthAction = "exit_wizard"
)
// Cline Auth Menu
// Example Layout
//
// ┃ Cline Account: <authenticated/not authenticated>
// ┃ Active Provider: <provider name or none configured>
// ┃ Active Model: <model name or none configured>
// ┃
// ┃ What would you like to do?
// ┃ Change Cline model (only if authenticated) - hidden if not authenticated
// ┃ Authenticate with Cline account / Sign out of Cline - changes based on auth status
// ┃ Select active provider (Cline or BYO) - always shown. Used to switch between Cline and BYO providers
// ┃ Configure BYO API providers - always shown. Launches provider setup wizard
// ┃ Exit authorization wizard - always shown. Exits the auth menu
// 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)
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("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)
// Route to existing auth flow
return HandleAuthCommand(authCtx, args)
}
// Main entry point for handling the `cline auth` command
// HandleAuthCommand routes the auth command based on the number of arguments
func HandleAuthCommand(ctx context.Context, args []string) error {
// Check if flags are provided for quick setup
if QuickProvider != "" || QuickAPIKey != "" || QuickModelID != "" || QuickBaseURL != "" {
if QuickProvider == "" || QuickAPIKey == "" || QuickModelID == "" {
return fmt.Errorf("quick setup requires --provider, --apikey, and --modelid flags. Use 'cline auth --help' for more information")
}
return QuickSetupFromFlags(ctx, QuickProvider, QuickAPIKey, QuickModelID, QuickBaseURL)
}
switch len(args) {
case 0:
// No args: Show uth wizard
return HandleAuthMenuNoArgs(ctx)
case 1, 2, 3, 4:
fmt.Println("Invalid positional arguments. Correct usage:")
fmt.Println(" cline auth --provider <provider> --apikey <key> --modelid <model> --baseurl <optional>")
return nil
default:
return fmt.Errorf("too many arguments. Use flags for quick setup: --provider, --apikey, --modelid --baseurl(optional)")
}
}
// getAuthInstanceAddress retrieves the auth instance address from context
// Returns empty string if not found (falls back to default behavior)
func getAuthInstanceAddress(ctx context.Context) string {
if addr, ok := ctx.Value(authInstanceAddressKey).(string); ok {
return addr
}
return ""
}
// HandleAuthMenuNoArgs prepares the auth menu when no arguments are provided
func HandleAuthMenuNoArgs(ctx context.Context) error {
// Check if Cline is authenticated
isClineAuth := IsAuthenticated(ctx)
// 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
}
}
}
// Fetch organizations if authenticated
var hasOrganizations bool
if isClineAuth {
if client, err := global.GetDefaultClient(ctx); err == nil {
if orgsResponse, err := client.Account.GetUserOrganizations(ctx, &cline.EmptyRequest{}); err == nil {
hasOrganizations = len(orgsResponse.GetOrganizations()) > 0
}
}
}
action, err := ShowAuthMenuWithStatus(isClineAuth, hasOrganizations, currentProvider, currentModel)
if err != nil {
// Check if user cancelled - propagate for clean exit
if err == huh.ErrUserAborted {
return huh.ErrUserAborted
}
return err
}
switch action {
case AuthActionClineLogin:
return HandleClineAuth(ctx)
case AuthActionBYOSetup:
return HandleAPIProviderSetup(ctx)
case AuthActionChangeClineModel:
return HandleChangeClineModel(ctx)
case AuthActionSelectOrganization:
return HandleSelectOrganization(ctx)
case AuthActionSelectProvider:
return HandleSelectProvider(ctx)
case AuthActionExit:
return nil
default:
return fmt.Errorf("invalid action")
}
}
// ShowAuthMenuWithStatus displays the main auth menu with Cline + provider status
func ShowAuthMenuWithStatus(isClineAuthenticated bool, hasOrganizations bool, currentProvider, currentModel string) (AuthAction, error) {
var action AuthAction
var options []huh.Option[AuthAction]
// Build menu options based on authentication status
if isClineAuthenticated {
options = []huh.Option[AuthAction]{
huh.NewOption("Change Cline model", AuthActionChangeClineModel),
}
// Add organization selection if user has organizations
if hasOrganizations {
options = append(options, huh.NewOption("Select organization", AuthActionSelectOrganization))
}
options = append(options,
huh.NewOption("Sign out of Cline", AuthActionClineLogin),
huh.NewOption("Select active provider (Cline or BYO)", AuthActionSelectProvider),
huh.NewOption("Configure BYO API providers", AuthActionBYOSetup),
huh.NewOption("Exit authorization wizard", AuthActionExit),
)
} else {
options = []huh.Option[AuthAction]{
huh.NewOption("Authenticate with Cline account", AuthActionClineLogin),
huh.NewOption("Select active provider (Cline or BYO)", AuthActionSelectProvider),
huh.NewOption("Configure BYO API providers", AuthActionBYOSetup),
huh.NewOption("Exit authorization wizard", AuthActionExit),
}
}
// Determine menu title based on status
var title string
renderer := display.NewRenderer(global.Config.OutputFormat)
// Always show Cline authentication status
if isClineAuthenticated {
title = fmt.Sprintf("Cline Account: %s Authenticated\n", renderer.Green("✓"))
} else {
title = fmt.Sprintf("Cline Account: %s Not authenticated\n", renderer.Red("✗"))
}
// Show active provider and model if configured (regardless of Cline auth status)
if currentProvider != "" && currentModel != "" {
title += fmt.Sprintf("Active Provider: %s\nActive Model: %s\n",
renderer.White(currentProvider),
renderer.White(currentModel))
}
// Always end with a huh?
title += "\nWhat would you like to do?"
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[AuthAction]().
Title(title).
Options(options...).
Value(&action),
),
)
if err := form.Run(); err != nil {
// Check if user cancelled with Control-C
if err == huh.ErrUserAborted {
// Return the error to allow deferred cleanup to run
return "", huh.ErrUserAborted
}
return "", fmt.Errorf("failed to get menu choice: %w", err)
}
return action, nil
}
// HandleAPIProviderSetup launches the API provider configuration wizard
func HandleAPIProviderSetup(ctx context.Context) error {
wizard, err := NewProviderWizard(ctx)
if err != nil {
return fmt.Errorf("failed to create provider wizard: %w", err)
}
return wizard.Run()
}
// HandleSelectProvider allows users to switch between Cline provider and BYO providers
func HandleSelectProvider(ctx context.Context) error {
// Get task manager
manager, err := createTaskManager(ctx)
if err != nil {
return fmt.Errorf("failed to create task manager: %w", err)
}
// Detect all providers with valid configurations (is an API key present)
availableProviders, err := DetectAllConfiguredProviders(ctx, manager)
if err != nil {
return fmt.Errorf("failed to detect configured providers: %w", err)
}
// Build list of available providers
var providerOptions []huh.Option[string]
var providerMapping = make(map[string]cline.ApiProvider)
// Add each configured provider to the selection menu
for _, provider := range availableProviders {
providerName := GetProviderDisplayName(provider)
providerKey := fmt.Sprintf("provider_%d", provider)
providerOptions = append(providerOptions, huh.NewOption(providerName, providerKey))
providerMapping[providerKey] = provider
}
if len(providerOptions) == 0 {
fmt.Println("No providers available. Please configure a provider first.")
return HandleAuthMenuNoArgs(ctx)
}
providerOptions = append(providerOptions, huh.NewOption("(Cancel)", "cancel"))
// Show selection menu
var selected string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Select which provider to use").
Options(providerOptions...).
Value(&selected),
),
)
if err := form.Run(); err != nil {
// Check if user cancelled with Control-C
if err == huh.ErrUserAborted {
return huh.ErrUserAborted
}
return fmt.Errorf("failed to select provider: %w", err)
}
if selected == "cancel" {
return HandleAuthMenuNoArgs(ctx)
}
// Get the selected provider
selectedProvider := providerMapping[selected]
// Apply the selected provider
if selectedProvider == cline.ApiProvider_CLINE {
// Configure Cline as the active provider
return SelectClineModel(ctx, manager)
} else {
// Switch to the selected BYO provider
return SwitchToBYOProvider(ctx, manager, selectedProvider)
}
}
// createTaskManager is a helper to create a task manager (avoids import cycles)
// Uses the auth instance address from context if available, otherwise falls back to default
func createTaskManager(ctx context.Context) (*task.Manager, error) {
authAddr := getAuthInstanceAddress(ctx)
if authAddr != "" {
return task.NewManagerForAddress(ctx, authAddr)
}
return task.NewManagerForDefault(ctx)
}
func verboseLog(format string, args ...interface{}) {
if global.Config != nil && global.Config.Verbose {
fmt.Printf("[VERBOSE] "+format+"\n", args...)
}
}
+130
View File
@@ -0,0 +1,130 @@
package auth
import (
"context"
"fmt"
"io"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/grpc-go/cline"
)
// AuthStatusListener manages subscription to auth status updates
type AuthStatusListener struct {
stream cline.AccountService_SubscribeToAuthStatusUpdateClient
updatesCh chan *cline.AuthState
errCh chan error
ctx context.Context
cancel context.CancelFunc
}
// NewAuthStatusListener creates a new auth status listener
func NewAuthStatusListener(parentCtx context.Context) (*AuthStatusListener, error) {
client, err := global.GetDefaultClient(parentCtx)
if err != nil {
return nil, fmt.Errorf("failed to get client: %w", err)
}
// Create cancellable context
ctx, cancel := context.WithCancel(parentCtx)
// Subscribe to auth status updates
stream, err := client.Account.SubscribeToAuthStatusUpdate(ctx, &cline.EmptyRequest{})
if err != nil {
cancel()
return nil, fmt.Errorf("failed to subscribe to auth updates: %w", err)
}
return &AuthStatusListener{
stream: stream,
updatesCh: make(chan *cline.AuthState, 10),
errCh: make(chan error, 1),
ctx: ctx,
cancel: cancel,
}, nil
}
// Start begins listening to the auth status update stream
func (l *AuthStatusListener) Start() error {
verboseLog("Starting auth status listener...")
go l.readStream()
return nil
}
// readStream reads from the gRPC stream and forwards messages to channels
func (l *AuthStatusListener) readStream() {
defer close(l.updatesCh)
defer close(l.errCh)
for {
select {
case <-l.ctx.Done():
verboseLog("Auth listener context cancelled")
return
default:
state, err := l.stream.Recv()
if err != nil {
if err == io.EOF {
verboseLog("Auth status stream closed")
return
}
verboseLog("Error reading from auth status stream: %v", err)
select {
case l.errCh <- err:
case <-l.ctx.Done():
}
return
}
verboseLog("Received auth state update: user=%v", state.User != nil)
select {
case l.updatesCh <- state:
case <-l.ctx.Done():
return
}
}
}
}
// WaitForAuthentication blocks until authentication succeeds or timeout occurs
func (l *AuthStatusListener) WaitForAuthentication(timeout time.Duration) error {
verboseLog("Waiting for authentication (timeout: %v)...", timeout)
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case <-timer.C:
return fmt.Errorf("authentication timeout after %v - please try again", timeout)
case <-l.ctx.Done():
return fmt.Errorf("authentication cancelled")
case err := <-l.errCh:
return fmt.Errorf("authentication stream error: %w", err)
case state := <-l.updatesCh:
if isAuthenticated(state) {
verboseLog("Authentication successful!")
return nil
}
verboseLog("Received auth update but not authenticated yet...")
}
}
}
// Stop closes the stream and cleans up resources
func (l *AuthStatusListener) Stop() {
verboseLog("Stopping auth status listener...")
l.cancel()
}
// isAuthenticated checks if AuthState indicates successful authentication
func isAuthenticated(state *cline.AuthState) bool {
return state != nil && state.User != nil
}
+247
View File
@@ -0,0 +1,247 @@
package auth
import (
"context"
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// Package-level variables for command-line flags
var (
QuickProvider string // Provider ID (e.g., "openai", "anthropic")
QuickAPIKey string // API key for the provider
QuickModelID string // Model ID to configure
QuickBaseURL string // Base URL (optional, for openai compatible only)
)
// QuickSetupFromFlags performs quick setup using command-line flags
// Returns error if validation fails or configuration cannot be applied
func QuickSetupFromFlags(ctx context.Context, provider, apiKey, modelID, baseURL string) error {
// Validate all input parameters
providerEnum, err := validateQuickSetupInputs(provider, apiKey, modelID, baseURL)
if err != nil {
return err
}
// Create task manager for state operations
manager, err := createTaskManager(ctx)
if err != nil {
return fmt.Errorf("failed to create task manager: %w", err)
}
// Validate and fetch model information if needed
finalModelID, modelInfo, err := validateAndFetchModel(ctx, manager, providerEnum, modelID, apiKey)
if err != nil {
return fmt.Errorf("model validation failed: %w", err)
}
// For Ollama, baseURL is stored in the API key field
finalAPIKey := apiKey
finalBaseURL := baseURL
if providerEnum == cline.ApiProvider_OLLAMA {
if baseURL != "" {
finalAPIKey = baseURL
finalBaseURL = ""
} else if apiKey != "" {
// User provided API key for Ollama - treat it as baseURL
finalAPIKey = apiKey
finalBaseURL = ""
} else {
// Use default Ollama baseURL
finalAPIKey = "http://localhost:11434"
finalBaseURL = ""
}
}
// Configure the provider using existing AddProviderPartial function
if err := AddProviderPartial(ctx, manager, providerEnum, finalModelID, finalAPIKey, finalBaseURL, modelInfo); err != nil {
return fmt.Errorf("failed to configure provider: %w", err)
}
// Set the provider as active for both Plan and Act modes
if err := UpdateProviderPartial(ctx, manager, providerEnum, ProviderUpdatesPartial{}, true); err != nil {
return fmt.Errorf("failed to set provider as active: %w", err)
}
// Mark welcome view as completed
if err := markWelcomeViewCompleted(ctx, manager); err != nil {
// Non-fatal error, just log it
if global.Config.Verbose {
fmt.Printf("[DEBUG] Warning: failed to mark welcome view as completed: %v\n", err)
}
}
// Flush pending state changes to disk immediately
// This ensures all configuration changes are persisted before the instance terminates
if _, err := manager.GetClient().State.FlushPendingState(ctx, &cline.EmptyRequest{}); err != nil {
return fmt.Errorf("failed to flush pending state: %w", err)
}
// Success message
fmt.Printf("\n✓ Successfully configured %s provider\n", GetProviderDisplayName(providerEnum))
fmt.Printf(" Model: %s\n", finalModelID)
if providerEnum == cline.ApiProvider_OLLAMA {
fmt.Printf(" Base URL: %s\n", finalAPIKey)
} else {
fmt.Println(" API Key: Configured")
}
if finalBaseURL != "" {
fmt.Printf(" Custom Base URL: %s\n", finalBaseURL)
}
fmt.Println("\nYou can now use Cline with this provider.")
fmt.Println("Run 'cline start' to begin a new task.")
return nil
}
// validateQuickSetupInputs validates all input parameters for quick setup
// Returns the validated provider enum or an error if validation fails
func validateQuickSetupInputs(provider, apiKey, modelID, baseURL string) (cline.ApiProvider, error) {
// Validate required parameters
if provider == "" {
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("provider is required. Use --provider or -p flag")
}
if strings.TrimSpace(apiKey) == "" && provider != "ollama" {
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("API key is required for %s provider. Use --apikey or -k flag", provider)
}
if strings.TrimSpace(modelID) == "" {
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("model ID is required. Use --modelid or -m flag")
}
// Validate and map provider string to enum
providerEnum, err := validateQuickSetupProvider(provider)
if err != nil {
return cline.ApiProvider_ANTHROPIC, err
}
// Validate that baseURL is only provided for OpenAI-compatible providers
if err := validateBaseURL(baseURL, providerEnum); err != nil {
return cline.ApiProvider_ANTHROPIC, err
}
return providerEnum, nil
}
// validateBaseURL checks if the user's input includes a baseURL for a provider other than OpenAI (compatible)
// Returns error if baseURL is provided for unsupported providers
func validateBaseURL(baseURL string, providerEnum cline.ApiProvider) error {
if providerEnum != cline.ApiProvider_OPENAI {
if baseURL != "" {
return fmt.Errorf("base URL is only supported for OpenAI and OpenAI-compatible providers")
}
}
return nil
}
// validateQuickSetupProvider validates the provider ID and returns the enum value
// Returns error if provider is invalid or not supported for quick setup
func validateQuickSetupProvider(providerID string) (cline.ApiProvider, error) {
// Normalize provider ID (trim whitespace, lowercase)
normalizedID := strings.TrimSpace(strings.ToLower(providerID))
// Explicitly block Bedrock
if normalizedID == "bedrock" {
return cline.ApiProvider_BEDROCK, fmt.Errorf("bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup: cline auth")
}
// Map provider string to enum using existing function
provider, ok := mapProviderStringToEnum(normalizedID)
if !ok {
// Provider not found - provide helpful error message
supportedProviders := []string{
"openai-native", "openai", "anthropic", "gemini",
"openrouter", "xai", "cerebras", "ollama",
}
return cline.ApiProvider_ANTHROPIC, fmt.Errorf(
"invalid provider '%s'. Supported providers: %s",
providerID,
strings.Join(supportedProviders, ", "),
)
}
// 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,
}
if !supportedProviders[provider] {
return provider, fmt.Errorf(
"provider '%s' is not supported for quick setup. Please use interactive setup: cline auth",
providerID,
)
}
return provider, nil
}
// validateAndFetchModel validates the model ID or fetches from provider if needed
// Returns the final model ID and optional model info
// For providers with static models, validates against the list
// For providers with dynamic models, fetches the list if possible
func validateAndFetchModel(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID, apiKey string) (string, interface{}, error) {
// Normalize model ID
modelID = strings.TrimSpace(modelID)
if modelID == "" {
return "", nil, fmt.Errorf("model ID cannot be empty")
}
// For most providers, we trust the user's input since we can't easily validate without making API calls
// The actual validation will happen when the model is used
switch provider {
case cline.ApiProvider_OPENROUTER:
// OpenRouter supports model info fetching, but it requires an API call
// For quick setup, we'll trust the user's input and return nil for model info
// The actual model info will be fetched when needed
if global.Config.Verbose {
fmt.Printf("[DEBUG] OpenRouter model ID: %s (will be validated on first use)\n", modelID)
}
return modelID, nil, nil
case cline.ApiProvider_OLLAMA:
// Ollama models can be validated by fetching the list, but this requires the server to be running
// For quick setup, we'll trust the user's input
if global.Config.Verbose {
fmt.Printf("[DEBUG] Ollama model ID: %s (will be validated when server is accessible)\n", modelID)
}
return modelID, nil, nil
default:
// For other providers (Anthropic, OpenAI, Gemini, XAI, Cerebras), trust user input
// Model validation will occur when the model is actually used
if global.Config.Verbose {
fmt.Printf("[DEBUG] %s model ID: %s (will be validated on first use)\n", GetProviderDisplayName(provider), modelID)
}
return modelID, nil, nil
}
}
// markWelcomeViewCompleted marks the welcome view as completed in the state
// This prevents the welcome view from showing up after quick setup
func markWelcomeViewCompleted(ctx context.Context, manager *task.Manager) error {
// Use the State service to update the welcome view flag
_, err := manager.GetClient().State.SetWelcomeViewCompleted(ctx, &cline.BooleanRequest{Value: true})
if err != nil {
return fmt.Errorf("failed to mark welcome view as completed: %w", err)
}
if global.Config.Verbose {
fmt.Println("[DEBUG] Marked welcome view as completed")
}
return nil
}
+141
View File
@@ -0,0 +1,141 @@
package auth
import (
"context"
"fmt"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// DefaultClineModelID is the default model ID for Cline provider.
// Cline uses OpenRouter-compatible model IDs.
const DefaultClineModelID = "anthropic/claude-sonnet-4.5"
// FetchClineModels fetches available Cline models from Cline Core.
// Note: Cline provider uses OpenRouter-compatible API and model format.
// The models are fetched using the same method as OpenRouter.
func FetchClineModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OpenRouterModelInfo, error) {
if global.Config.Verbose {
fmt.Println("Fetching Cline models (using OpenRouter-compatible API)")
}
// Cline uses OpenRouter model fetching
models, err := FetchOpenRouterModels(ctx, manager)
if err != nil {
return nil, fmt.Errorf("failed to fetch Cline models: %w", err)
}
return models, nil
}
// GetClineModelInfo retrieves information for a specific Cline model.
func GetClineModelInfo(modelID string, models map[string]*cline.OpenRouterModelInfo) (*cline.OpenRouterModelInfo, error) {
modelInfo, exists := models[modelID]
if !exists {
return nil, fmt.Errorf("model %s not found", modelID)
}
return modelInfo, nil
}
// SetDefaultClineModel configures the default Cline model after authentication.
// This is called automatically after successful Cline sign-in.
func SetDefaultClineModel(ctx context.Context, manager *task.Manager) error {
// Fetch available models
models, err := FetchClineModels(ctx, manager)
if err != nil {
// If we can't fetch models, we'll use the default without model info
fmt.Printf("Warning: Could not fetch Cline models: %v\n", err)
fmt.Printf("Using default model: %s\n", DefaultClineModelID)
return applyDefaultClineModel(ctx, manager, nil)
}
// Check if default model is available
modelInfo, err := GetClineModelInfo(DefaultClineModelID, models)
if err != nil {
fmt.Printf("Warning: Default model not found: %v\n", err)
// Try to use any available model
for modelID := range models {
fmt.Printf("Using available model: %s\n", modelID)
return applyClineModelConfiguration(ctx, manager, modelID, models[modelID])
}
return fmt.Errorf("no usable Cline models found")
}
if err := applyClineModelConfiguration(ctx, manager, DefaultClineModelID, modelInfo); err != nil {
return err
}
if err := setWelcomeViewCompletedWithManager(ctx, manager); err != nil {
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
}
return nil
}
// SelectClineModel presents a menu to select a Cline model and applies the configuration.
func SelectClineModel(ctx context.Context, manager *task.Manager) error {
// Fetch models (uses OpenRouter-compatible format)
models, err := FetchClineModels(ctx, manager)
if err != nil {
return fmt.Errorf("failed to fetch Cline models: %w", err)
}
// Convert to interface map for generic utilities
modelMap := ConvertOpenRouterModelsToInterface(models)
// Get model IDs as a sorted list
modelIDs := ConvertModelsMapToSlice(modelMap)
// Display selection menu
selectedModelID, err := DisplayModelSelectionMenu(modelIDs, "Cline")
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Get the selected model info
modelInfo := models[selectedModelID]
// Apply the configuration
if err := applyClineModelConfiguration(ctx, manager, selectedModelID, modelInfo); err != nil {
return err
}
fmt.Println()
// Return to main auth menu after model selection
return HandleAuthMenuNoArgs(ctx)
}
// applyClineModelConfiguration applies a Cline model configuration to both Act and Plan modes using UpdateProviderPartial.
// Cline uses OpenRouter-compatible model format.
func applyClineModelConfiguration(ctx context.Context, manager *task.Manager, modelID string, modelInfo *cline.OpenRouterModelInfo) error {
provider := cline.ApiProvider_CLINE
updates := ProviderUpdatesPartial{
ModelID: &modelID,
ModelInfo: modelInfo,
}
return UpdateProviderPartial(ctx, manager, provider, updates, true)
}
func applyDefaultClineModel(ctx context.Context, manager *task.Manager, modelInfo *cline.OpenRouterModelInfo) error {
if err := applyClineModelConfiguration(ctx, manager, DefaultClineModelID, modelInfo); err != nil {
return err
}
if err := setWelcomeViewCompletedWithManager(ctx, manager); err != nil {
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
}
return nil
}
func setWelcomeViewCompletedWithManager(ctx context.Context, manager *task.Manager) error {
_, err := manager.GetClient().State.SetWelcomeViewCompleted(ctx, &cline.BooleanRequest{Value: true})
return err
}
+156
View File
@@ -0,0 +1,156 @@
package auth
import (
"context"
"fmt"
"os"
"sort"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
"golang.org/x/term"
)
// FetchOpenRouterModels fetches available OpenRouter models from Cline Core
func FetchOpenRouterModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OpenRouterModelInfo, error) {
resp, err := manager.GetClient().Models.RefreshOpenRouterModelsRpc(ctx, &cline.EmptyRequest{})
if err != nil {
return nil, fmt.Errorf("failed to fetch OpenRouter models: %w", err)
}
return resp.Models, nil
}
// FetchOcaModels fetches available Oca models from Cline Core
func FetchOcaModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OcaModelInfo, error) {
resp, err := manager.GetClient().Models.RefreshOcaModels(ctx, &cline.StringRequest{})
if err != nil {
return nil, fmt.Errorf("failed to fetch Oca models: %w", err)
}
return resp.Models, nil
}
// ConvertOpenRouterModelsToInterface converts OpenRouter model map to generic interface map.
// This allows OpenRouter and Cline models to be used with the generic fetching utilities.
func ConvertOpenRouterModelsToInterface(models map[string]*cline.OpenRouterModelInfo) map[string]interface{} {
result := make(map[string]interface{}, len(models))
for k, v := range models {
result[k] = v
}
return result
}
// FetchOpenAiModels fetches available OpenAI models from Cline Core
// Takes the API key and returns a list of model IDs
func FetchOpenAiModels(ctx context.Context, manager *task.Manager, baseURL, apiKey string) ([]string, error) {
req := &cline.OpenAiModelsRequest{
BaseUrl: baseURL,
ApiKey: apiKey,
}
resp, err := manager.GetClient().Models.RefreshOpenAiModels(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to fetch OpenAI models: %w", err)
}
return resp.Values, nil
}
// FetchOllamaModels fetches available Ollama models from Cline Core
// Takes the base URL (empty string for default) and returns a list of model IDs
func FetchOllamaModels(ctx context.Context, manager *task.Manager, baseURL string) ([]string, error) {
req := &cline.StringRequest{
Value: baseURL,
}
resp, err := manager.GetClient().Models.GetOllamaModels(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to fetch Ollama models: %w", err)
}
return resp.Values, nil
}
// DisplayModelSelectionMenu shows an interactive menu for selecting a model from a list.
// Models are displayed alphabetically. Uses model ID as the option value to avoid
// index-based bugs when list order changes.
// Returns the selected model ID.
func DisplayModelSelectionMenu(models []string, providerName string) (string, error) {
if len(models) == 0 {
return "", fmt.Errorf("no models available for selection")
}
// Use model ID as the value (not index) to avoid positional coupling bugs
var selectedModel string
options := make([]huh.Option[string], len(models))
for i, model := range models {
options[i] = huh.NewOption(model, model)
}
title := fmt.Sprintf("Select a %s model", providerName)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title(title).
Options(options...).
Height(calculateSelectHeight()).
Filtering(true).
Value(&selectedModel),
),
)
if err := form.Run(); err != nil {
return "", fmt.Errorf("failed to select model: %w", err)
}
return selectedModel, nil
}
// ConvertModelsMapToSlice converts a map of models to a sorted slice of model IDs.
// This is useful for displaying models in a consistent order in UI components.
func ConvertModelsMapToSlice(models map[string]interface{}) []string {
result := make([]string, 0, len(models))
for modelID := range models {
result = append(result, modelID)
}
// Sort alphabetically for consistent display
sort.Strings(result)
return result
}
// ConvertOcaModelsToInterface converts Oca model map to generic interface map.
// This allows Oca and Cline models to be used with the generic fetching utilities.
func ConvertOcaModelsToInterface(models map[string]*cline.OcaModelInfo) map[string]interface{} {
result := make(map[string]interface{}, len(models))
for k, v := range models {
result[k] = v
}
return result
}
// getTerminalHeight returns the terminal height (rows)
func getTerminalHeight() int {
_, height, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || height <= 0 {
return 25 // safe fallback for non-TTY or errors
}
return height
}
// calculateSelectHeight computes appropriate height for Select component
// Reserves space for title, search UI, and margins
func calculateSelectHeight() int {
height := getTerminalHeight()
// Reserve ~10 rows for UI chrome (title, search, margins)
visibleRows := height - 10
// Clamp between 8 (minimum usable) and 25 (maximum before unwieldy)
if visibleRows < 8 {
return 8
}
if visibleRows > 25 {
return 25
}
return visibleRows
}
+69
View File
@@ -0,0 +1,69 @@
package auth
import (
"fmt"
"sort"
"github.com/cline/cli/pkg/generated"
"github.com/cline/grpc-go/cline"
)
// SupportsStaticModelList returns true if the provider has a predefined static model list
func SupportsStaticModelList(provider cline.ApiProvider) bool {
providerID := GetProviderIDForEnum(provider)
if providerID == "" {
return false
}
// Check if this provider has static models defined
def, err := generated.GetProviderDefinition(providerID)
if err != nil {
return false
}
// Return true if provider has models and isn't dynamic-only
// (Dynamic providers like OpenRouter/OpenAI/Ollama fetch from API)
return len(def.Models) > 0 && !def.HasDynamicModels
}
// FetchStaticModels retrieves the static model list for a provider from generated definitions
// Returns a sorted list of model IDs and a map of model IDs to their info
func FetchStaticModels(provider cline.ApiProvider) ([]string, map[string]generated.ModelInfo, error) {
providerID := GetProviderIDForEnum(provider)
if providerID == "" {
return nil, nil, fmt.Errorf("unknown provider enum: %v", provider)
}
def, err := generated.GetProviderDefinition(providerID)
if err != nil {
return nil, nil, fmt.Errorf("failed to get provider definition: %w", err)
}
if len(def.Models) == 0 {
return nil, nil, fmt.Errorf("no models defined for provider %s", providerID)
}
// Extract model IDs and sort them
modelIDs := make([]string, 0, len(def.Models))
for modelID := range def.Models {
modelIDs = append(modelIDs, modelID)
}
sort.Strings(modelIDs)
return modelIDs, def.Models, nil
}
// GetDefaultModelForProvider returns the default model ID for a provider if one is defined
func GetDefaultModelForProvider(provider cline.ApiProvider) string {
providerID := GetProviderIDForEnum(provider)
if providerID == "" {
return ""
}
def, err := generated.GetProviderDefinition(providerID)
if err != nil {
return ""
}
return def.DefaultModelID
}
+184
View File
@@ -0,0 +1,184 @@
package auth
import (
"fmt"
"github.com/charmbracelet/huh"
"github.com/cline/grpc-go/cline"
)
// BYOProviderOption represents a selectable BYO (bring-your-own) provider option
type BYOProviderOption struct {
Name string
Provider cline.ApiProvider
}
// 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: "Anthropic", Provider: cline.ApiProvider_ANTHROPIC},
{Name: "OpenAI Compatible", Provider: cline.ApiProvider_OPENAI},
{Name: "OpenAI (Official)", Provider: cline.ApiProvider_OPENAI_NATIVE},
{Name: "OpenRouter", Provider: cline.ApiProvider_OPENROUTER},
{Name: "X AI (Grok)", Provider: cline.ApiProvider_XAI},
{Name: "AWS Bedrock", Provider: cline.ApiProvider_BEDROCK},
{Name: "Google Gemini", Provider: cline.ApiProvider_GEMINI},
{Name: "Ollama", Provider: cline.ApiProvider_OLLAMA},
{Name: "Cerebras", Provider: cline.ApiProvider_CEREBRAS},
{Name: "NousResearch", Provider: cline.ApiProvider_NOUSRESEARCH},
{Name: "Oracle Code Assist", Provider: cline.ApiProvider_OCA},
}
}
// SelectBYOProvider displays a menu for selecting a BYO provider.
func SelectBYOProvider() (cline.ApiProvider, error) {
providers := GetBYOProviderList()
var selectedIndex int
options := make([]huh.Option[int], len(providers)+1)
for i, provider := range providers {
options[i] = huh.NewOption(provider.Name, i)
}
options[len(providers)] = huh.NewOption("(Cancel)", -1)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[int]().
Title("Select an API provider").
Options(options...).
Value(&selectedIndex),
),
)
if err := form.Run(); err != nil {
return 0, fmt.Errorf("failed to select provider: %w", err)
}
if selectedIndex == -1 {
return 0, fmt.Errorf("provider selection cancelled")
}
return providers[selectedIndex].Provider, nil
}
// SupportsBYOModelFetching returns true if the provider supports fetching models dynamically
// from a remote API, or if it has a static list of predefined models.
// This is used to determine whether to show a model list before prompting for manual entry.
func SupportsBYOModelFetching(provider cline.ApiProvider) bool {
switch provider {
case cline.ApiProvider_OPENROUTER:
return true
case cline.ApiProvider_OPENAI:
return true
case cline.ApiProvider_OLLAMA:
return true
case cline.ApiProvider_OCA:
return true
}
return SupportsStaticModelList(provider)
}
// GetBYOProviderPlaceholder returns a placeholder model ID for manual entry based on provider.
func GetBYOProviderPlaceholder(provider cline.ApiProvider) string {
switch provider {
case cline.ApiProvider_ANTHROPIC:
return "e.g., claude-sonnet-4-5-20250929"
case cline.ApiProvider_OPENAI:
return "e.g., openai/gpt-oss-120b"
case cline.ApiProvider_OPENAI_NATIVE:
return "e.g., gpt-5-2025-08-07"
case cline.ApiProvider_OPENROUTER:
return "e.g., google/gemini-2.0-flash-exp:free"
case cline.ApiProvider_XAI:
return "e.g., grok-code-fast-1"
case cline.ApiProvider_BEDROCK:
return "e.g., anthropic.claude-sonnet-4-5-20250929-v1:0"
case cline.ApiProvider_GEMINI:
return "e.g., gemini-2.5-pro"
case cline.ApiProvider_OLLAMA:
return "e.g., qwen3-coder:30b"
case cline.ApiProvider_CEREBRAS:
return "e.g., gpt-oss-120b"
case cline.ApiProvider_NOUSRESEARCH:
return "e.g., Hermes-4-405B"
case cline.ApiProvider_OCA:
return "e.g., oca/llama4"
default:
return "Enter model ID"
}
}
// GetBYOAPIKeyFieldConfig returns field configuration for API key input based on provider.
type APIKeyFieldConfig struct {
Title string
EchoMode huh.EchoMode
IsRequired bool
}
// GetBYOAPIKeyFieldConfig returns the configuration for the API key field based on provider.
func GetBYOAPIKeyFieldConfig(provider cline.ApiProvider) APIKeyFieldConfig {
if provider == cline.ApiProvider_OLLAMA {
return APIKeyFieldConfig{
Title: "Base URL (optional, press Enter for default)",
EchoMode: huh.EchoModeNormal,
IsRequired: false,
}
}
return APIKeyFieldConfig{
Title: "API Key",
EchoMode: huh.EchoModePassword,
IsRequired: true,
}
}
// PromptForAPIKey prompts the user to enter an API key (or base URL for Ollama).
// For OpenAI (Compatible) provider, also prompts for an optional base URL.
func PromptForAPIKey(provider cline.ApiProvider) (string, string, error) {
var apiKey string
config := GetBYOAPIKeyFieldConfig(provider)
apiKeyField := huh.NewInput().
Title(config.Title).
EchoMode(config.EchoMode).
Value(&apiKey)
if config.IsRequired {
apiKeyField = apiKeyField.Validate(func(s string) error {
if s == "" {
return fmt.Errorf("API key cannot be empty")
}
return nil
})
}
form := huh.NewForm(huh.NewGroup(apiKeyField))
if err := form.Run(); err != nil {
return "", "", fmt.Errorf("failed to get API key: %w", err)
}
// For OpenAI (Compatible) provider, prompt for base URL
if provider == cline.ApiProvider_OPENAI {
var baseURL string
baseURLForm := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Base URL (optional, for OpenAI-compatible providers)").
Placeholder("e.g., https://api.example.com/v1").
Value(&baseURL).
Description("Press Enter to skip if using standard OpenAI API"),
),
)
if err := baseURLForm.Run(); err != nil {
return "", "", fmt.Errorf("failed to get base URL: %w", err)
}
return apiKey, baseURL, nil
}
return apiKey, "", nil
}
+517
View File
@@ -0,0 +1,517 @@
package auth
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// ProviderDisplay represents a configured provider for display purposes
type ProviderDisplay struct {
Mode string // "Plan" or "Act"
Provider cline.ApiProvider // Provider enum
ModelID string // Model identifier
HasAPIKey bool // Whether an API key is configured (never show actual key)
BaseURL string // Base URL for providers like Ollama (can be shown publicly)
}
// ProviderListResult holds the parsed provider configuration from state
type ProviderListResult struct {
PlanProvider *ProviderDisplay
ActProvider *ProviderDisplay
apiConfig map[string]interface{} // Store the raw apiConfig for scanning all providers
}
// GetProviderConfigurations retrieves and parses provider configurations from Cline Core state
func GetProviderConfigurations(ctx context.Context, manager *task.Manager) (*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{})
if err != nil {
return nil, fmt.Errorf("failed to get state: %w", err)
}
stateJSON := state.StateJson
if global.Config.Verbose {
fmt.Printf("[DEBUG] Retrieved state, parsing JSON (length: %d)\n", len(stateJSON))
}
// Parse state_json as map[string]interface{}
var stateData map[string]interface{}
if err := json.Unmarshal([]byte(stateJSON), &stateData); err != nil {
return nil, fmt.Errorf("failed to parse state JSON: %w", err)
}
if global.Config.Verbose {
fmt.Printf("[DEBUG] Parsed state data with %d keys\n", len(stateData))
}
// Extract apiConfiguration object from state
apiConfig, ok := stateData["apiConfiguration"].(map[string]interface{})
if !ok {
if global.Config.Verbose {
fmt.Println("[DEBUG] No apiConfiguration found in state")
}
return &ProviderListResult{
apiConfig: make(map[string]interface{}),
}, nil
}
if global.Config.Verbose {
fmt.Printf("[DEBUG] Found apiConfiguration with %d keys\n", len(apiConfig))
}
// Extract plan mode configuration
planProvider := extractProviderFromState(apiConfig, "plan")
if global.Config.Verbose && planProvider != nil {
fmt.Printf("[DEBUG] Plan mode: provider=%v, model=%s\n", planProvider.Provider, planProvider.ModelID)
}
// Extract act mode configuration
actProvider := extractProviderFromState(apiConfig, "act")
if global.Config.Verbose && actProvider != nil {
fmt.Printf("[DEBUG] Act mode: provider=%v, model=%s\n", actProvider.Provider, actProvider.ModelID)
}
return &ProviderListResult{
PlanProvider: planProvider,
ActProvider: actProvider,
apiConfig: apiConfig,
}, nil
}
// GetAllReadyProviders returns all providers that have both a model and API key configured
func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
if r.apiConfig == nil {
return []*ProviderDisplay{}
}
var readyProviders []*ProviderDisplay
seenProviders := make(map[cline.ApiProvider]bool)
// Check all possible providers
allProviders := []cline.ApiProvider{
cline.ApiProvider_CLINE,
cline.ApiProvider_ANTHROPIC,
cline.ApiProvider_OPENAI,
cline.ApiProvider_OPENAI_NATIVE,
cline.ApiProvider_OPENROUTER,
cline.ApiProvider_XAI,
cline.ApiProvider_BEDROCK,
cline.ApiProvider_GEMINI,
cline.ApiProvider_OLLAMA,
cline.ApiProvider_CEREBRAS,
cline.ApiProvider_NOUSRESEARCH,
cline.ApiProvider_OCA,
cline.ApiProvider_HICAP,
}
// Check each provider to see if it's ready to use
// We use "plan" mode to check, since both plan and act should have the same providers configured
for _, provider := range allProviders {
// Skip if we've already seen this provider
if seenProviders[provider] {
continue
}
// Check if this provider has a model configured
modelID := getProviderSpecificModelID(r.apiConfig, "plan", provider)
// Determine if credentials exist
hasCreds := checkAPIKeyExists(r.apiConfig, provider)
// Determine readiness: OCA uses auth state presence; others need creds and model
if provider == cline.ApiProvider_OCA {
state, _ := GetLatestOCAState(context.Background(), 2 *time.Second)
if state == nil || state.User == nil {
continue
}
} else {
// Provider is not ready unless it has credentials AND a model configured
if !hasCreds || modelID == "" {
continue
}
}
// Get base URL for Ollama
baseURL := ""
if provider == cline.ApiProvider_OLLAMA {
if url, ok := r.apiConfig["ollamaBaseUrl"].(string); ok {
baseURL = url
}
}
// This provider is ready to use
readyProviders = append(readyProviders, &ProviderDisplay{
Mode: "Ready",
Provider: provider,
ModelID: modelID,
HasAPIKey: checkAPIKeyExists(r.apiConfig, provider),
BaseURL: baseURL,
})
seenProviders[provider] = true
}
return readyProviders
}
// extractProviderFromState extracts provider configuration for specific plan/act mode
func extractProviderFromState(stateData map[string]interface{}, mode string) *ProviderDisplay {
// Build key names based on mode
providerKey := mode + "ModeApiProvider"
// Extract provider string from state
providerStr, ok := stateData[providerKey].(string)
if !ok || providerStr == "" {
if global.Config.Verbose {
fmt.Printf("[DEBUG] No provider configured for %s mode\n", mode)
}
return nil
}
// Map provider string to enum
provider, ok := mapProviderStringToEnum(providerStr)
if !ok {
if global.Config.Verbose {
fmt.Printf("[DEBUG] Unknown provider type: %s\n", providerStr)
}
return nil
}
// Get provider-specific model ID
modelID := getProviderSpecificModelID(stateData, mode, provider)
// Check if API key exists
hasAPIKey := checkAPIKeyExists(stateData, provider)
// Get base URL for Ollama (can be shown publicly)
baseURL := ""
if provider == cline.ApiProvider_OLLAMA {
if url, ok := stateData["ollamaBaseUrl"].(string); ok {
baseURL = url
}
}
return &ProviderDisplay{
Mode: capitalizeMode(mode),
Provider: provider,
ModelID: modelID,
HasAPIKey: hasAPIKey,
BaseURL: baseURL,
}
}
// mapProviderStringToEnum converts provider string from state to ApiProvider enum
// Returns (provider, ok) where ok is false if the provider is unknown
func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
normalizedStr := strings.ToLower(providerStr)
// Map string values to enum values
switch normalizedStr {
case "anthropic":
return cline.ApiProvider_ANTHROPIC, true
case "openai", "openai-compatible": // internal name is 'openai', but this is actually the openai-compatible provider
return cline.ApiProvider_OPENAI, true
case "openai-native": // This is the native, official Open AI provider
return cline.ApiProvider_OPENAI_NATIVE, true
case "openrouter":
return cline.ApiProvider_OPENROUTER, true
case "xai":
return cline.ApiProvider_XAI, true
case "bedrock":
return cline.ApiProvider_BEDROCK, true
case "gemini":
return cline.ApiProvider_GEMINI, true
case "ollama":
return cline.ApiProvider_OLLAMA, true
case "cerebras":
return cline.ApiProvider_CEREBRAS, true
case "cline":
return cline.ApiProvider_CLINE, true
case "oca":
return cline.ApiProvider_OCA, true
case "hicap":
return cline.ApiProvider_HICAP, true
case "nousResearch":
return cline.ApiProvider_NOUSRESEARCH, true
default:
return cline.ApiProvider_ANTHROPIC, false // Return 0 value with false
}
}
// GetProviderIDForEnum converts a provider enum to the provider ID string
// This is the inverse of mapProviderStringToEnum and is used for provider definitions
func GetProviderIDForEnum(provider cline.ApiProvider) string {
switch provider {
case cline.ApiProvider_ANTHROPIC:
return "anthropic"
case cline.ApiProvider_OPENAI:
return "openai-compatible"
case cline.ApiProvider_OPENAI_NATIVE:
return "openai-native"
case cline.ApiProvider_OPENROUTER:
return "openrouter"
case cline.ApiProvider_XAI:
return "xai"
case cline.ApiProvider_BEDROCK:
return "bedrock"
case cline.ApiProvider_GEMINI:
return "gemini"
case cline.ApiProvider_OLLAMA:
return "ollama"
case cline.ApiProvider_CEREBRAS:
return "cerebras"
case cline.ApiProvider_CLINE:
return "cline"
case cline.ApiProvider_OCA:
return "oca"
case cline.ApiProvider_HICAP:
return "hicap"
case cline.ApiProvider_NOUSRESEARCH:
return "nousResearch"
default:
return ""
}
}
// getProviderSpecificModelID gets the provider-specific model ID field from state
func getProviderSpecificModelID(stateData map[string]interface{}, mode string, provider cline.ApiProvider) string {
modelKey, err := GetModelIDFieldName(provider, mode)
if err != nil {
if global.Config.Verbose {
fmt.Printf("[DEBUG] Error getting model ID field name: %v\n", err)
}
return ""
}
if global.Config.Verbose {
fmt.Printf("[DEBUG] Looking for model ID in key: %s\n", modelKey)
}
// Extract model ID from state
modelID, _ := stateData[modelKey].(string)
return modelID
}
// checkAPIKeyExists checks if API key field exists in state (never retrieve actual key)
func checkAPIKeyExists(stateData map[string]interface{}, provider cline.ApiProvider) bool {
// Get field mapping from centralized function
fields, err := GetProviderFields(provider)
if err != nil {
return false
}
keyField := fields.APIKeyField
// Check if the key exists and is not empty
if value, ok := stateData[keyField]; ok {
if str, ok := value.(string); ok && str != "" {
return true
}
}
return false
}
// capitalizeMode capitalizes the mode string for display
func capitalizeMode(mode string) string {
if len(mode) == 0 {
return mode
}
return strings.ToUpper(mode[:1]) + mode[1:]
}
// GetProviderDisplayName returns a user-friendly name for the provider
func GetProviderDisplayName(provider cline.ApiProvider) string {
switch provider {
case cline.ApiProvider_ANTHROPIC:
return "Anthropic"
case cline.ApiProvider_OPENAI:
return "OpenAI Compatible"
case cline.ApiProvider_OPENAI_NATIVE:
return "OpenAI (Official)"
case cline.ApiProvider_OPENROUTER:
return "OpenRouter"
case cline.ApiProvider_XAI:
return "X AI (Grok)"
case cline.ApiProvider_BEDROCK:
return "AWS Bedrock"
case cline.ApiProvider_GEMINI:
return "Google Gemini"
case cline.ApiProvider_OLLAMA:
return "Ollama"
case cline.ApiProvider_CEREBRAS:
return "Cerebras"
case cline.ApiProvider_CLINE:
return "Cline (Official)"
case cline.ApiProvider_OCA:
return "Oracle Code Assist"
case cline.ApiProvider_HICAP:
return "Hicap"
case cline.ApiProvider_NOUSRESEARCH:
return "NousResearch"
default:
return "Unknown"
}
}
// FormatProviderList formats the complete provider list for console display
// This now shows ALL providers that have both a model and API key configured
func FormatProviderList(result *ProviderListResult) string {
var output strings.Builder
output.WriteString("\n=== Configured API Providers ===\n\n")
// Get the currently active provider
var activeProvider cline.ApiProvider
var activeProviderSet bool
if result.ActProvider != nil {
activeProvider = result.ActProvider.Provider
activeProviderSet = true
}
// Get all ready-to-use providers (those with both API key and model configured)
readyProviders := result.GetAllReadyProviders()
if len(readyProviders) == 0 {
output.WriteString(" No providers ready to use.\n")
output.WriteString(" A provider is ready when it has both a model and API key configured.\n")
output.WriteString(" Use 'Configure a new provider' to configure one.\n\n")
} else {
//output.WriteString(fmt.Sprintf(" %d provider(s) ready to use:\n\n", len(readyProviders)))
for _, display := range readyProviders {
// Check if this is the active provider
isActive := activeProviderSet && display.Provider == activeProvider
if isActive {
output.WriteString(fmt.Sprintf(" ✓ %s (ACTIVE)\n", GetProviderDisplayName(display.Provider)))
} else {
output.WriteString(fmt.Sprintf(" • %s\n", GetProviderDisplayName(display.Provider)))
}
output.WriteString(fmt.Sprintf(" Model: %s\n", display.ModelID))
// Show status based on provider type
if display.Provider == cline.ApiProvider_OLLAMA {
if display.BaseURL != "" {
output.WriteString(fmt.Sprintf(" Base URL: %s\n", display.BaseURL))
} else {
output.WriteString(" Base URL: (default)\n")
}
} else if display.Provider == cline.ApiProvider_CLINE || display.Provider == cline.ApiProvider_OCA {
output.WriteString(" Status: Authenticated\n")
} else {
output.WriteString(" API Key: Configured\n")
}
output.WriteString("\n")
}
}
output.WriteString("================================\n")
return output.String()
}
// DetectAllConfiguredProviders scans the state to find all providers that have API keys configured.
// This allows switching between multiple providers even when only one is currently active.
func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([]cline.ApiProvider, error) {
verboseLog("[DEBUG] Detecting all configured providers...")
// Get latest state from Cline Core
state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return nil, fmt.Errorf("failed to get state: %w", err)
}
stateJSON := state.StateJson
// Parse state_json as map[string]interface{}
var stateData map[string]interface{}
if err := json.Unmarshal([]byte(stateJSON), &stateData); err != nil {
return nil, fmt.Errorf("failed to parse state JSON: %w", err)
}
// Extract apiConfiguration object from state
apiConfig, ok := stateData["apiConfiguration"].(map[string]interface{})
if !ok {
verboseLog("[DEBUG] No apiConfiguration found in state")
verboseLog("[DEBUG] Available keys in stateData: %v", getMapKeys(stateData))
return []cline.ApiProvider{}, nil
}
verboseLog("[DEBUG] apiConfiguration keys: %v", getMapKeys(apiConfig))
var configuredProviders []cline.ApiProvider
// Check for Cline provider (uses authentication instead of API key)
if IsAuthenticated(ctx) {
configuredProviders = append(configuredProviders, cline.ApiProvider_CLINE)
verboseLog("[DEBUG] Cline provider is authenticated")
}
// 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)
verboseLog("[DEBUG] OCA provider has active auth state")
}
// Check each BYO provider for API key presence
providersToCheck := []struct {
provider cline.ApiProvider
keyField string
}{
{cline.ApiProvider_ANTHROPIC, "apiKey"},
{cline.ApiProvider_OPENAI, "openAiApiKey"},
{cline.ApiProvider_OPENAI_NATIVE, "openAiNativeApiKey"},
{cline.ApiProvider_OPENROUTER, "openRouterApiKey"},
{cline.ApiProvider_XAI, "xaiApiKey"},
{cline.ApiProvider_BEDROCK, "awsAccessKey"},
{cline.ApiProvider_GEMINI, "geminiApiKey"},
{cline.ApiProvider_OLLAMA, "ollamaBaseUrl"}, // Ollama uses baseUrl instead of API key
{cline.ApiProvider_CEREBRAS, "cerebrasApiKey"},
{cline.ApiProvider_HICAP, "hicapApiKey"},
{cline.ApiProvider_NOUSRESEARCH, "nousResearchApiKey"},
}
for _, providerCheck := range providersToCheck {
verboseLog("[DEBUG] Checking for %s key: %s", GetProviderDisplayName(providerCheck.provider), providerCheck.keyField)
if value, ok := apiConfig[providerCheck.keyField]; ok {
verboseLog("[DEBUG] Found key, value type: %T, is empty: %v", value, value == "")
if str, ok := value.(string); ok && str != "" {
configuredProviders = append(configuredProviders, providerCheck.provider)
verboseLog("[DEBUG] ✓ Provider %s is configured", GetProviderDisplayName(providerCheck.provider))
}
} else {
verboseLog("[DEBUG] Key %s not found", providerCheck.keyField)
}
}
verboseLog("[DEBUG] Total configured providers: %d", len(configuredProviders))
for _, p := range configuredProviders {
verboseLog("[DEBUG] - %s", GetProviderDisplayName(p))
}
return configuredProviders, nil
}
// getMapKeys returns the keys of a map for debugging
func getMapKeys(m map[string]interface{}) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
@@ -0,0 +1,647 @@
package auth
import (
"context"
"fmt"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// updateApiConfigurationPartial is a helper that calls the gRPC method with optional verbose logging.
// This replaces the Manager.updateApiConfigurationPartial method to keep auth-specific code in the auth package.
func updateApiConfigurationPartial(ctx context.Context, manager *task.Manager, request *cline.UpdateApiConfigurationPartialRequest) error {
if global.Config.Verbose {
fmt.Println("[DEBUG] Updating API configuration (partial)")
if request.UpdateMask != nil && len(request.UpdateMask.Paths) > 0 {
fmt.Printf("[DEBUG] Field mask paths: %v\n", request.UpdateMask.Paths)
}
if request.ApiConfiguration != nil {
apiConfig := request.ApiConfiguration
if apiConfig.PlanModeApiProvider != nil {
fmt.Printf("[DEBUG] Plan mode provider: %s\n", *apiConfig.PlanModeApiProvider)
}
if apiConfig.ActModeApiProvider != nil {
fmt.Printf("[DEBUG] Act mode provider: %s\n", *apiConfig.ActModeApiProvider)
}
}
}
// Call the Models service to update API configuration
_, err := manager.GetClient().Models.UpdateApiConfigurationPartial(ctx, request)
if err != nil {
return fmt.Errorf("failed to update API configuration (partial): %w", err)
}
if global.Config.Verbose {
fmt.Println("[DEBUG] API configuration updated successfully (partial)")
}
return nil
}
// ProviderFields defines all the field names associated with a specific provider
type ProviderFields struct {
APIKeyField string // API key field name (e.g., "apiKey", "openAiApiKey")
BaseURLField string // Base URL field name (optional, empty if not applicable)
PlanModeModelIDField string // Plan mode model ID field (e.g., "planModeApiModelId")
ActModeModelIDField string // Act mode model ID field (e.g., "actModeApiModelId")
PlanModeModelInfoField string // Plan mode model info field (optional, empty if not applicable)
ActModeModelInfoField string // Act mode model info field (optional, empty if not applicable)
// Provider-specific additional model ID fields
PlanModeProviderSpecificModelIDField string // e.g., "planModeOpenRouterModelId"
ActModeProviderSpecificModelIDField string // e.g., "actModeOpenRouterModelId"
}
// GetProviderFields returns the field mapping for a given provider
func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
switch provider {
case cline.ApiProvider_ANTHROPIC:
return ProviderFields{
APIKeyField: "apiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_OPENAI:
return ProviderFields{
APIKeyField: "openAiApiKey",
BaseURLField: "openAiBaseUrl",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeProviderSpecificModelIDField: "planModeOpenAiModelId",
ActModeProviderSpecificModelIDField: "actModeOpenAiModelId",
}, nil
case cline.ApiProvider_OPENROUTER:
return ProviderFields{
APIKeyField: "openRouterApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeModelInfoField: "planModeOpenRouterModelInfo",
ActModeModelInfoField: "actModeOpenRouterModelInfo",
PlanModeProviderSpecificModelIDField: "planModeOpenRouterModelId",
ActModeProviderSpecificModelIDField: "actModeOpenRouterModelId",
}, nil
case cline.ApiProvider_XAI:
return ProviderFields{
APIKeyField: "xaiApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_BEDROCK:
return ProviderFields{
APIKeyField: "awsAccessKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeProviderSpecificModelIDField: "planModeAwsBedrockCustomModelBaseId",
ActModeProviderSpecificModelIDField: "actModeAwsBedrockCustomModelBaseId",
}, nil
case cline.ApiProvider_GEMINI:
return ProviderFields{
APIKeyField: "geminiApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_OPENAI_NATIVE:
return ProviderFields{
APIKeyField: "openAiNativeApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_OLLAMA:
return ProviderFields{
APIKeyField: "ollamaBaseUrl",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeProviderSpecificModelIDField: "planModeOllamaModelId",
ActModeProviderSpecificModelIDField: "actModeOllamaModelId",
}, nil
case cline.ApiProvider_CEREBRAS:
return ProviderFields{
APIKeyField: "cerebrasApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_CLINE:
return ProviderFields{
APIKeyField: "clineApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeModelInfoField: "planModeOpenRouterModelInfo",
ActModeModelInfoField: "actModeOpenRouterModelInfo",
PlanModeProviderSpecificModelIDField: "planModeOpenRouterModelId",
ActModeProviderSpecificModelIDField: "actModeOpenRouterModelId",
}, nil
case cline.ApiProvider_OCA:
return ProviderFields{
APIKeyField: "ocaApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeModelInfoField: "planModeOcaModelInfo",
ActModeModelInfoField: "actModeOcaModelInfo",
PlanModeProviderSpecificModelIDField: "planModeOcaModelId",
ActModeProviderSpecificModelIDField: "actModeOcaModelId",
}, nil
case cline.ApiProvider_HICAP:
return ProviderFields{
APIKeyField: "hicapApiKey",
PlanModeModelInfoField: "planModeHicapModelInfo",
ActModeModelInfoField: "actModeHicapModelInfo",
PlanModeProviderSpecificModelIDField: "planModeHicapModelId",
ActModeProviderSpecificModelIDField: "actModeHicapModelId",
}, nil
case cline.ApiProvider_NOUSRESEARCH:
return ProviderFields{
APIKeyField: "nousResearchApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeProviderSpecificModelIDField: "planModeNousResearchModelId",
ActModeProviderSpecificModelIDField: "actModeNousResearchModelId",
}, nil
default:
return ProviderFields{}, fmt.Errorf("unsupported provider: %v", provider)
}
}
// ProviderUpdatesPartial defines optional fields for partial provider updates
// Uses pointers to distinguish between "not provided" and "set to empty"
type ProviderUpdatesPartial struct {
ModelID *string // New model ID (optional)
APIKey *string // New API key (optional)
ModelInfo interface{} // New model info (optional, provider-specific)
BaseURL *string // New base URL (optional, e.g., for OCA, Ollama)
RefreshToken *string // New refresh token (optional, e.g., for OCA)
Mode *string // New mode (optional, e.g., "internal" or "external" for OCA)
}
// GetModelIDFieldName returns the appropriate model ID field name for a provider and mode.
// This helper centralizes the logic for determining whether to use provider-specific
// or generic model ID fields.
func GetModelIDFieldName(provider cline.ApiProvider, mode string) (string, error) {
fields, err := GetProviderFields(provider)
if err != nil {
return "", err
}
if mode == "plan" {
// Use provider-specific field if available, otherwise use generic field
if fields.PlanModeProviderSpecificModelIDField != "" {
return fields.PlanModeProviderSpecificModelIDField, nil
}
return fields.PlanModeModelIDField, nil
}
// Act mode
if fields.ActModeProviderSpecificModelIDField != "" {
return fields.ActModeProviderSpecificModelIDField, nil
}
return fields.ActModeModelIDField, nil
}
// buildProviderFieldMask builds a list of camelCase field paths for the field mask.
// When includeProviderEnums is true, the provider enum fields are included (for setting active provider).
// When false, only the data fields are included (for configuring without activating).
func buildProviderFieldMask(fields ProviderFields, includeAPIKey bool, includeModelID bool, includeModelInfo bool, includeBaseURL bool, includeProviderEnums bool) []string {
var fieldPaths []string
// Include provider enums if requested (used when setting active provider)
if includeProviderEnums {
fieldPaths = append(fieldPaths, "planModeApiProvider", "actModeApiProvider")
}
// Add API key field if requested
if includeAPIKey {
fieldPaths = append(fieldPaths, fields.APIKeyField)
// Special case: Bedrock also needs secret key
if fields.APIKeyField == "awsAccessKey" {
fieldPaths = append(fieldPaths, "awsSecretKey")
}
}
// Add base URL field if requested and applicable
if includeBaseURL && fields.BaseURLField != "" {
fieldPaths = append(fieldPaths, fields.BaseURLField)
}
// Add model ID fields if requested
if includeModelID {
// Only include provider-specific fields if they exist, otherwise use generic fields
if fields.PlanModeProviderSpecificModelIDField != "" {
// Provider has specific fields - use ONLY those
fieldPaths = append(fieldPaths, fields.PlanModeProviderSpecificModelIDField)
fieldPaths = append(fieldPaths, fields.ActModeProviderSpecificModelIDField)
} else {
// Provider uses generic fields - update those
fieldPaths = append(fieldPaths, fields.PlanModeModelIDField)
fieldPaths = append(fieldPaths, fields.ActModeModelIDField)
}
}
// Add model info fields if requested and applicable
if includeModelInfo && fields.PlanModeModelInfoField != "" {
fieldPaths = append(fieldPaths, fields.PlanModeModelInfoField)
fieldPaths = append(fieldPaths, fields.ActModeModelInfoField)
}
return fieldPaths
}
// setAPIKeyField sets the appropriate API key field in the config based on the field name
func setAPIKeyField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
switch fieldName {
case "apiKey":
apiConfig.ApiKey = value
case "openAiApiKey":
apiConfig.OpenAiApiKey = value
case "openAiNativeApiKey":
apiConfig.OpenAiNativeApiKey = value
case "openRouterApiKey":
apiConfig.OpenRouterApiKey = value
case "xaiApiKey":
apiConfig.XaiApiKey = value
case "awsAccessKey":
apiConfig.AwsAccessKey = value
case "geminiApiKey":
apiConfig.GeminiApiKey = value
case "ollamaBaseUrl":
apiConfig.OllamaBaseUrl = value
case "cerebrasApiKey":
apiConfig.CerebrasApiKey = value
case "clineApiKey":
apiConfig.ClineApiKey = value
case "ocaApiKey":
apiConfig.OcaApiKey = value
case "hicapApiKey":
apiConfig.HicapApiKey = value
case "nousResearchApiKey":
apiConfig.NousResearchApiKey = value
}
}
// setProviderSpecificModelID sets the appropriate provider-specific model ID fields when possible
func setProviderSpecificModelID(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
switch fieldName {
case "planModeOpenAiModelId":
apiConfig.PlanModeOpenAiModelId = value
apiConfig.ActModeOpenAiModelId = value
case "planModeOpenRouterModelId":
apiConfig.PlanModeOpenRouterModelId = value
apiConfig.ActModeOpenRouterModelId = value
case "planModeOllamaModelId":
apiConfig.PlanModeOllamaModelId = value
apiConfig.ActModeOllamaModelId = value
case "planModeAwsBedrockCustomModelBaseId":
apiConfig.PlanModeAwsBedrockCustomModelBaseId = value
apiConfig.ActModeAwsBedrockCustomModelBaseId = value
case "planModeOcaModelId":
apiConfig.PlanModeOcaModelId = value
apiConfig.ActModeOcaModelId = value
case "planModeHicapModelId":
apiConfig.PlanModeHicapModelId = value
apiConfig.ActModeHicapModelId = value
case "planModeNousResearchModelId":
apiConfig.PlanModeNousResearchModelId = value
apiConfig.ActModeNousResearchModelId = value
}
}
// AddProviderPartial configures a new provider with all necessary fields using partial updates.
func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID string, apiKey string, baseURL string, modelInfo interface{}) error {
// Get field mapping for this provider
fields, err := GetProviderFields(provider)
if err != nil {
return err
}
// Build a ModelsApiConfiguration with only the relevant provider fields set
apiConfig := &cline.ModelsApiConfiguration{}
// Set API key field
if apiKey != "" || fields.APIKeyField != "ollamaBaseUrl" {
setAPIKeyField(apiConfig, fields.APIKeyField, proto.String(apiKey))
}
// Set base URL field if provided and applicable
includeBaseURL := false
if baseURL != "" && fields.BaseURLField != "" {
setBaseURLField(apiConfig, fields.BaseURLField, proto.String(baseURL))
includeBaseURL = true
}
// Set model ID fields
apiConfig.PlanModeApiModelId = proto.String(modelID)
apiConfig.ActModeApiModelId = proto.String(modelID)
// Set provider-specific model ID fields if applicable
if fields.PlanModeProviderSpecificModelIDField != "" {
setProviderSpecificModelID(apiConfig, fields.PlanModeProviderSpecificModelIDField, proto.String(modelID))
}
// Set model info if applicable and provided
if fields.PlanModeModelInfoField != "" && modelInfo != nil {
if openRouterInfo, ok := modelInfo.(*cline.OpenRouterModelInfo); ok {
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
}
}
// Build field mask including all fields we're setting (without provider enums)
includeModelInfo := fields.PlanModeModelInfoField != "" && modelInfo != nil
fieldPaths := buildProviderFieldMask(fields, true, true, includeModelInfo, includeBaseURL, false)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
// Apply the partial update
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to update API configuration: %w", err)
}
return nil
}
// UpdateProviderPartial updates specific fields for an existing provider using partial updates.
// If setAsActive is true, this will also set the provider as the active provider for both Plan and Act modes.
func UpdateProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, updates ProviderUpdatesPartial, setAsActive bool) error {
// Get field mapping for this provider
fields, err := GetProviderFields(provider)
if err != nil {
return err
}
// Build a ModelsApiConfiguration with only the fields being updated
apiConfig := &cline.ModelsApiConfiguration{}
// Set provider enum for BOTH Plan and Act modes if setAsActive is true
if setAsActive {
apiConfig.PlanModeApiProvider = &provider
apiConfig.ActModeApiProvider = &provider
}
// Track what we're updating for field mask
includeAPIKey := updates.APIKey != nil
includeModelID := updates.ModelID != nil
includeModelInfo := updates.ModelInfo != nil && fields.PlanModeModelInfoField != ""
// Update API key if provided
if updates.APIKey != nil {
setAPIKeyField(apiConfig, fields.APIKeyField, updates.APIKey)
}
// Update model ID if provided
if updates.ModelID != nil {
// Only set provider-specific fields if they exist, otherwise use generic fields
if fields.PlanModeProviderSpecificModelIDField != "" {
setProviderSpecificModelID(apiConfig, fields.PlanModeProviderSpecificModelIDField, updates.ModelID)
} else {
// Provider uses generic fields - set those
apiConfig.PlanModeApiModelId = updates.ModelID
apiConfig.ActModeApiModelId = updates.ModelID
}
}
// Update model info if provided
if updates.ModelInfo != nil && fields.PlanModeModelInfoField != "" {
if openRouterInfo, ok := updates.ModelInfo.(*cline.OpenRouterModelInfo); ok {
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
}
}
// Build field mask for only the fields being updated
fieldPaths := buildProviderFieldMask(fields, includeAPIKey, includeModelID, includeModelInfo, false, setAsActive)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
// Apply the partial update
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to update API configuration: %w", err)
}
return nil
}
// RemoveProviderPartial removes a provider by clearing its API key using partial updates
func RemoveProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider) error {
// Get field mapping for this provider
fields, err := GetProviderFields(provider)
if err != nil {
return err
}
// Build an EMPTY ModelsApiConfiguration (or one with empty API key field)
// Fields in the mask without values will be cleared
apiConfig := &cline.ModelsApiConfiguration{}
// Build field mask with only the API key field(s)
// For Bedrock, include both access key and secret key
fieldPaths := []string{fields.APIKeyField}
if provider == cline.ApiProvider_BEDROCK {
fieldPaths = append(fieldPaths, "awsSecretKey")
}
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
// Apply the partial update (clearing API key by including in mask without value)
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to update API configuration: %w", err)
}
return nil
}
// setBaseURLField sets the appropriate base URL field in the config based on the field name
func setBaseURLField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
switch fieldName {
case "ocaBaseUrl":
apiConfig.OcaBaseUrl = value
case "ollamaBaseUrl":
apiConfig.OllamaBaseUrl = value
case "openAiBaseUrl":
apiConfig.OpenAiBaseUrl = value
case "geminiBaseUrl":
apiConfig.GeminiBaseUrl = value
case "liteLlmBaseUrl":
apiConfig.LiteLlmBaseUrl = value
case "anthropicBaseUrl":
apiConfig.AnthropicBaseUrl = value
case "requestyBaseUrl":
apiConfig.RequestyBaseUrl = value
case "lmStudioBaseUrl":
apiConfig.LmStudioBaseUrl = value
case "oca":
apiConfig.OcaBaseUrl = value
}
}
// setRefreshTokenField sets the appropriate refresh token field in the config
func setRefreshTokenField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
switch fieldName {
case "ocaRefreshToken":
apiConfig.OcaRefreshToken = value
}
}
// setModeField sets the appropriate mode field in the config
func setModeField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
switch fieldName {
case "ocaMode":
apiConfig.OcaMode = value
}
}
// BedrockOptionalFields holds optional configuration fields for AWS Bedrock
type BedrockOptionalFields struct {
SessionToken *string // Optional: AWS session token for temporary credentials
Region *string // Optional: AWS region
UseCrossRegionInference *bool // Optional: Enable cross-region inference
UseGlobalInference *bool // Optional: Use global inference endpoint
UsePromptCache *bool // Optional: Enable prompt caching
Authentication *string // Optional: Authentication method
UseProfile *bool // Optional: Use AWS profile
Profile *string // Optional: AWS profile name
Endpoint *string // Optional: Custom endpoint URL
}
// OcaOptionalFields holds optional configuration fields for Oracle Code Assist
type OcaOptionalFields struct {
BaseURL *string // Optional: Base URL
Mode *string // Optional: Mode ("internal" or "external")
}
// setBedrockOptionalFields sets optional Bedrock-specific fields in the API configuration
func setBedrockOptionalFields(apiConfig *cline.ModelsApiConfiguration, fields *BedrockOptionalFields) {
if fields == nil {
return
}
if fields.SessionToken != nil {
apiConfig.AwsSessionToken = fields.SessionToken
}
if fields.Region != nil {
apiConfig.AwsRegion = fields.Region
}
if fields.UseCrossRegionInference != nil {
apiConfig.AwsUseCrossRegionInference = fields.UseCrossRegionInference
}
if fields.UseGlobalInference != nil {
apiConfig.AwsUseGlobalInference = fields.UseGlobalInference
}
if fields.UsePromptCache != nil {
apiConfig.AwsBedrockUsePromptCache = fields.UsePromptCache
}
if fields.Authentication != nil {
apiConfig.AwsAuthentication = fields.Authentication
}
if fields.UseProfile != nil {
apiConfig.AwsUseProfile = fields.UseProfile
}
if fields.Profile != nil {
apiConfig.AwsProfile = fields.Profile
}
if fields.Endpoint != nil {
apiConfig.AwsBedrockEndpoint = fields.Endpoint
}
}
// setOcaOptionalFields sets optional Oca-specific fields in the API configuration
func setOcaOptionalFields(apiConfig *cline.ModelsApiConfiguration, fields *OcaOptionalFields) {
if fields == nil {
return
}
if fields.Mode != nil {
apiConfig.OcaMode = fields.Mode
}
if fields.BaseURL != nil {
apiConfig.OcaBaseUrl = fields.BaseURL
}
}
// buildBedrockOptionalFieldMask builds field mask paths for Bedrock optional fields that have values
func buildBedrockOptionalFieldMask(fields *BedrockOptionalFields) []string {
if fields == nil {
return nil
}
var fieldPaths []string
if fields.SessionToken != nil {
fieldPaths = append(fieldPaths, "awsSessionToken")
}
if fields.Region != nil {
fieldPaths = append(fieldPaths, "awsRegion")
}
if fields.UseCrossRegionInference != nil {
fieldPaths = append(fieldPaths, "awsUseCrossRegionInference")
}
if fields.UseGlobalInference != nil {
fieldPaths = append(fieldPaths, "awsUseGlobalInference")
}
if fields.UsePromptCache != nil {
fieldPaths = append(fieldPaths, "awsBedrockUsePromptCache")
}
if fields.Authentication != nil {
fieldPaths = append(fieldPaths, "awsAuthentication")
}
if fields.UseProfile != nil {
fieldPaths = append(fieldPaths, "awsUseProfile")
}
if fields.Profile != nil {
fieldPaths = append(fieldPaths, "awsProfile")
}
if fields.Endpoint != nil {
fieldPaths = append(fieldPaths, "awsBedrockEndpoint")
}
return fieldPaths
}
// buildOcaOptionalFieldMask builds field mask paths for Bedrock optional fields that have values
func buildOcaOptionalFieldMask(fields *OcaOptionalFields) []string {
if fields == nil {
return nil
}
var fieldPaths []string
if fields.Mode != nil {
fieldPaths = append(fieldPaths, "ocaMode")
}
if fields.BaseURL != nil {
fieldPaths = append(fieldPaths, "ocaBaseUrl")
}
return fieldPaths
}
+764
View File
@@ -0,0 +1,764 @@
package auth
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// ProviderWizard handles the interactive provider configuration process
type ProviderWizard struct {
ctx context.Context
manager *task.Manager
}
// NewProviderWizard prepares a new provider configuration wizard
func NewProviderWizard(ctx context.Context) (*ProviderWizard, error) {
// Create task manager using auth instance from context
manager, err := createTaskManager(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create task manager: %w", err)
}
return &ProviderWizard{
ctx: ctx,
manager: manager,
}, nil
}
// showMainMenu displays the main provider configuration menu
func (pw *ProviderWizard) showMainMenu() (string, error) {
var action string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("What would you like to do?").
Options(
huh.NewOption("Add or change an API provider", "add"),
huh.NewOption("Change model for API provider", "change-model"),
huh.NewOption("Remove a provider", "remove"),
huh.NewOption("List configured providers", "list"),
huh.NewOption("Return to main auth menu", "back"),
).
Value(&action),
),
)
if err := form.Run(); err != nil {
return "", fmt.Errorf("failed to get menu choice: %w", err)
}
return action, nil
}
// Run runs the provider configuration wizard
func (pw *ProviderWizard) Run() error {
for {
action, err := pw.showMainMenu()
if err != nil {
return err
}
switch action {
case "add":
if err := pw.handleAddProvider(); err != nil {
return err
}
case "change-model":
if err := pw.handleChangeModel(); err != nil {
return err
}
case "remove":
if err := pw.handleRemoveProvider(); err != nil {
return err
}
case "list":
if err := pw.handleListProviders(); err != nil {
return err
}
case "back":
// Return to main auth menu
return HandleAuthMenuNoArgs(pw.ctx)
}
fmt.Println()
}
}
// "Add a new provider" > handleAddProvider
func (pw *ProviderWizard) handleAddProvider() error {
// Step 1: Select provider
provider, err := SelectBYOProvider()
if err != nil {
if strings.Contains(err.Error(), "cancelled") {
return nil
}
return fmt.Errorf("provider selection failed: %w", err)
}
// Step 2: Special handling for Bedrock provider
if provider == cline.ApiProvider_BEDROCK {
return pw.handleAddBedrockProvider()
}
// Step 2b: Special handling for OCA provider
if provider == cline.ApiProvider_OCA {
return pw.handleAddOcaProvider()
}
// Step 3: Get API key first (for non-Bedrock providers)
apiKey, baseURL, err := PromptForAPIKey(provider)
if err != nil {
return fmt.Errorf("failed to get API key: %w", err)
}
// Step 4: Try to fetch models and let user select (with fallback to manual entry for providers that don't support fetch)
modelID, modelInfo, err := pw.selectModel(provider, apiKey)
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Step 5: Apply configuration using AddProviderPartial
if err := AddProviderPartial(pw.ctx, pw.manager, provider, modelID, apiKey, baseURL, modelInfo); err != nil {
return fmt.Errorf("failed to save configuration: %w", err)
}
if err := setWelcomeViewCompleted(pw.ctx, pw.manager); err != nil {
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
}
fmt.Println("✓ Provider configured successfully!")
return nil
}
// handleAddBedrockProvider handles the special case of adding Bedrock provider with its multi-field form
func (pw *ProviderWizard) handleAddBedrockProvider() error {
// Step 1: Get Bedrock configuration (all credentials and optional fields)
config, err := PromptForBedrockConfig(pw.ctx, pw.manager)
if err != nil {
if strings.Contains(err.Error(), "user declined profile authentication") {
return nil
}
return fmt.Errorf("failed to get Bedrock configuration: %w", err)
}
// Step 2: Select model
modelID, modelInfo, err := pw.selectModel(cline.ApiProvider_BEDROCK, "")
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Step 3: Apply Bedrock configuration
if err := ApplyBedrockConfig(pw.ctx, pw.manager, config, modelID, modelInfo); err != nil {
return fmt.Errorf("failed to save Bedrock configuration: %w", err)
}
if err := setWelcomeViewCompleted(pw.ctx, pw.manager); err != nil {
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
}
fmt.Println("✓ Bedrock provider configured successfully!")
return nil
}
// handleAddOcaProvider handles adding Oracle Code Assist provider with optional settings and auth
func (pw *ProviderWizard) handleAddOcaProvider() error {
// Step 1: Get OCA configuration (base URL and mode)
config, err := PromptForOcaConfig(pw.ctx, pw.manager)
if err != nil {
if strings.Contains(err.Error(), "user aborted") || strings.Contains(err.Error(), "cancelled") {
return nil
}
return fmt.Errorf("failed to get OCA configuration: %w", err)
}
// Apply OCA configuration (base URL and mode)
if err := ApplyOcaConfig(pw.ctx, pw.manager, config); err != nil {
return fmt.Errorf("failed to save OCA configuration: %w", err)
}
// Step 2: Ensure OCA authentication
if err := ensureOcaAuthenticated(pw.ctx); err != nil {
return fmt.Errorf("failed to authenticate with OCA: %w", err)
}
// Step 3: Select model
modelID, _, err := pw.selectModel(cline.ApiProvider_OCA, "")
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Step 4: Apply the OCA model configuration and set as active
updates := ProviderUpdatesPartial{
ModelID: &modelID,
ModelInfo: nil,
}
if err := UpdateProviderPartial(pw.ctx, pw.manager, cline.ApiProvider_OCA, updates, true); err != nil {
return fmt.Errorf("failed to save OCA configuration: %w", err)
}
if err := setWelcomeViewCompleted(pw.ctx, pw.manager); err != nil {
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
}
fmt.Println("✓ OCA provider configured successfully!")
return nil
}
// handleListProviders retrieves and displays configured providers
func (pw *ProviderWizard) handleListProviders() error {
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
if err != nil {
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
}
output := FormatProviderList(result)
fmt.Println(output)
return nil
}
// selectModel attempts to fetch available models and let user select, or falls back to manual entry
func (pw *ProviderWizard) selectModel(provider cline.ApiProvider, apiKey string) (string, interface{}, error) {
// For providers that support model fetching, try to fetch and display models
canFetchModels := pw.supportsModelFetching(provider)
if canFetchModels {
fmt.Println("Fetching available models...")
models, modelInfoMap, err := pw.fetchModelsForProvider(provider, apiKey)
if err != nil {
fmt.Println("\n⚠ Unable to fetch model list from the provider. Please enter the model ID manually instead.")
if global.Config.Verbose {
fmt.Printf(" Error details: %v\n", err)
}
return pw.manualModelEntry(provider)
}
if len(models) == 0 {
fmt.Println("\n⚠ No models found from the provider. Please enter the model ID manually instead.")
return pw.manualModelEntry(provider)
}
// Let user select from available models (includes manual entry option)
modelID, err := pw.selectFromAvailableModels(models)
if err != nil {
return "", nil, fmt.Errorf("model selection failed: %w", err)
}
// Check if user chose manual entry
const manualEntryKey = "__MANUAL_ENTRY__"
if modelID == manualEntryKey {
return pw.manualModelEntry(provider)
}
// Get the model info for the selected model
var modelInfo interface{}
if modelInfoMap != nil {
modelInfo = modelInfoMap[modelID]
}
return modelID, modelInfo, nil
}
// For providers without model fetching support, use manual entry
return pw.manualModelEntry(provider)
}
// supportsModelFetching returns true if the provider supports fetching models
func (pw *ProviderWizard) supportsModelFetching(provider cline.ApiProvider) bool {
return SupportsBYOModelFetching(provider)
}
// fetchModelsForProvider fetches models for a given provider
// Supports both dynamic API fetching (OpenRouter, OpenAI, Ollama) and static model lists (Anthropic, Bedrock, Gemini, X AI)
func (pw *ProviderWizard) fetchModelsForProvider(provider cline.ApiProvider, apiKey string) ([]string, map[string]interface{}, error) {
// Try dynamic/remote model fetching first
switch provider {
case cline.ApiProvider_OPENROUTER:
models, err := FetchOpenRouterModels(pw.ctx, pw.manager)
if err != nil {
return nil, nil, err
}
interfaceMap := ConvertOpenRouterModelsToInterface(models)
return ConvertModelsMapToSlice(interfaceMap), interfaceMap, nil
case cline.ApiProvider_OPENAI:
// For OpenAI, we need to pass the base URL and API key
baseURL := "https://api.openai.com/v1" // Default OpenAI API base URL
modelIDs, err := FetchOpenAiModels(pw.ctx, pw.manager, baseURL, apiKey)
if err != nil {
return nil, nil, err
}
// OpenAI returns just model IDs without additional info, so modelInfo map is nil
return modelIDs, nil, nil
case cline.ApiProvider_OLLAMA:
// For Ollama, apiKey actually contains the base URL (or empty for default)
baseURL := apiKey // The "API key" field for Ollama is actually the base URL
modelIDs, err := FetchOllamaModels(pw.ctx, pw.manager, baseURL)
if err != nil {
return nil, nil, err
}
// Ollama returns just model IDs without additional info, so modelInfo map is nil
return modelIDs, nil, nil
case cline.ApiProvider_OCA:
// OCA supports dynamic model fetching
models, err := FetchOcaModels(pw.ctx, pw.manager)
if err != nil {
return nil, nil, err
}
interfaceMap := ConvertOcaModelsToInterface(models)
return ConvertModelsMapToSlice(interfaceMap), interfaceMap, nil
}
// Fall back to static models for providers that don't support dynamic fetching
if SupportsStaticModelList(provider) {
modelIDs, _, err := FetchStaticModels(provider)
if err != nil {
return nil, nil, err
}
// Static models don't have detailed info maps for now, so modelInfo map is nil
return modelIDs, nil, nil
}
return nil, nil, fmt.Errorf("model fetching not supported for provider: %v", provider)
}
// selectFromAvailableModels displays available models and lets user select one.
// Includes an option to enter a model ID manually in case the desired model isn't listed.
func (pw *ProviderWizard) selectFromAvailableModels(models []string) (string, error) {
if len(models) == 0 {
return "", fmt.Errorf("no models available")
}
// Add a special "manual entry" option at the end
const manualEntryKey = "__MANUAL_ENTRY__"
// Use model ID as the value (not index)
var selectedModel string
options := make([]huh.Option[string], len(models)+1)
for i, model := range models {
options[i] = huh.NewOption(model, model)
}
// Add manual entry option at the end
options[len(models)] = huh.NewOption("Enter model ID manually...", manualEntryKey)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Select a model").
Options(options...).
Height(calculateSelectHeight()).
Filtering(true).
Value(&selectedModel),
),
)
if err := form.Run(); err != nil {
return "", fmt.Errorf("failed to select model: %w", err)
}
// If user selected manual entry, return special key to trigger manual input
if selectedModel == manualEntryKey {
return manualEntryKey, nil
}
return selectedModel, nil
}
// manualModelEntry prompts user to manually enter a model ID.
// Returns the model ID and an error. The modelInfo is always nil for manual entry.
func (pw *ProviderWizard) manualModelEntry(provider cline.ApiProvider) (string, interface{}, error) {
var modelID string
modelPlaceholder := GetBYOProviderPlaceholder(provider)
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Model ID").
Placeholder(modelPlaceholder).
Value(&modelID).
Validate(func(s string) error {
// Trim whitespace and validate
trimmed := strings.TrimSpace(s)
if trimmed == "" {
return fmt.Errorf("model ID cannot be empty")
}
return nil
}),
),
)
if err := form.Run(); err != nil {
return "", nil, fmt.Errorf("failed to get model ID: %w", err)
}
// Trim whitespace from the final value
modelID = strings.TrimSpace(modelID)
// modelInfo is always nil for manual entry
return modelID, nil, nil
}
// 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)
if err != nil {
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
}
// Step 2: Get all configured providers with models
readyProviders := result.GetAllReadyProviders()
// Filter out Cline provider (it has its own model changer in the main menu)
var configurableProviders []*ProviderDisplay
for _, provider := range readyProviders {
if provider.Provider != cline.ApiProvider_CLINE {
configurableProviders = append(configurableProviders, provider)
}
}
// Step 3: Check if there are any configurable providers
if len(configurableProviders) == 0 {
fmt.Println("\nNo configurable providers found.")
fmt.Println("Note: Cline provider has its own model selection in the main menu.")
return nil
}
// Step 4: Let user select which provider to change the model for
var selectedIndex int
options := make([]huh.Option[int], len(configurableProviders)+1)
for i, providerDisplay := range configurableProviders {
displayName := fmt.Sprintf("%s (current: %s)",
GetProviderDisplayName(providerDisplay.Provider),
providerDisplay.ModelID)
options[i] = huh.NewOption(displayName, i)
}
options[len(configurableProviders)] = huh.NewOption("(Cancel)", -1)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[int]().
Title("Select provider to change model for").
Options(options...).
Value(&selectedIndex),
),
)
if err := form.Run(); err != nil {
return fmt.Errorf("failed to select provider: %w", err)
}
if selectedIndex == -1 {
return nil
}
selectedProvider := configurableProviders[selectedIndex]
provider := selectedProvider.Provider
fmt.Printf("\nChanging model for %s\n", GetProviderDisplayName(provider))
fmt.Printf("Current model: %s\n\n", selectedProvider.ModelID)
// Step 5: Retrieve API key if needed for model fetching
var apiKey string
if pw.supportsModelFetching(provider) {
// For providers that support fetching, we need to retrieve the API key from state
state, err := pw.manager.GetClient().State.GetLatestState(pw.ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to get state: %w", err)
}
var stateData map[string]interface{}
if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
return fmt.Errorf("failed to parse state JSON: %w", err)
}
apiConfig, ok := stateData["apiConfiguration"].(map[string]interface{})
if !ok {
return fmt.Errorf("no API configuration found in state")
}
apiKey = getProviderAPIKeyFromState(apiConfig, provider)
if apiKey == "" {
return fmt.Errorf("no API key found for provider %s", GetProviderDisplayName(provider))
}
}
modelID, modelInfo, err := pw.selectModel(provider, apiKey)
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Step 6: Apply the model change (for both Plan and Act modes)
if err := pw.applyModelChange(provider, modelID, modelInfo); err != nil {
return fmt.Errorf("failed to apply model change: %w", err)
}
fmt.Printf("✓ Model changed successfully to: %s\n", modelID)
fmt.Println(" (Applied to both Plan and Act modes)")
return nil
}
// applyModelChange applies a model change for both Plan and Act modes using UpdateProviderPartial
func (pw *ProviderWizard) applyModelChange(provider cline.ApiProvider, modelID string, modelInfo interface{}) error {
updates := ProviderUpdatesPartial{
ModelID: &modelID,
ModelInfo: modelInfo,
}
return UpdateProviderPartial(pw.ctx, pw.manager, provider, updates, true)
}
// SwitchToBYOProvider switches to a BYO provider that's already configured.
// It retrieves the existing model configuration and sets it as the active provider for both Plan and Act modes.
func SwitchToBYOProvider(ctx context.Context, manager *task.Manager, provider cline.ApiProvider) error {
// Get the current state to retrieve the model ID and model info for this provider
state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to get state: %w", err)
}
// Parse state JSON
var stateData map[string]interface{}
if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
return fmt.Errorf("failed to parse state JSON: %w", err)
}
// Extract apiConfiguration
apiConfig, ok := stateData["apiConfiguration"].(map[string]interface{})
if !ok {
return fmt.Errorf("no API configuration found in state")
}
// Get the model ID for the selected provider
modelID := getProviderModelIDFromState(apiConfig, provider)
if modelID == "" {
return fmt.Errorf("no model configured for provider %s", GetProviderDisplayName(provider))
}
// Get model info if available (for OpenRouter/Cline)
var modelInfo interface{}
if provider == cline.ApiProvider_OPENROUTER || provider == cline.ApiProvider_CLINE {
if modelInfoData, ok := apiConfig["planModeOpenRouterModelInfo"].(map[string]interface{}); ok {
modelInfo = convertMapToOpenRouterModelInfo(modelInfoData)
}
}
// Use UpdateProviderPartial to switch to this provider
updates := ProviderUpdatesPartial{
ModelID: &modelID,
ModelInfo: modelInfo,
}
if err := UpdateProviderPartial(ctx, manager, provider, updates, true); err != nil {
return fmt.Errorf("failed to switch provider: %w", err)
}
verboseLog("✓ Switched to %s\n", GetProviderDisplayName(provider))
verboseLog(" Using model: %s\n", modelID)
return HandleAuthMenuNoArgs(ctx)
}
// getProviderModelIDFromState retrieves the model ID for a specific provider from state
func getProviderModelIDFromState(stateData map[string]interface{}, provider cline.ApiProvider) string {
modelKey, err := GetModelIDFieldName(provider, "plan")
if err != nil {
return ""
}
if modelID, ok := stateData[modelKey].(string); ok {
return modelID
}
return ""
}
// 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 {
// Return a sentinel non-empty string so upstream checks pass.
return "OCA_AUTH_VERIFIED"
}
return ""
}
fields, err := GetProviderFields(provider)
if err != nil {
return ""
}
if apiKey, ok := stateData[fields.APIKeyField].(string); ok {
return apiKey
}
return ""
}
// convertMapToOpenRouterModelInfo converts a map to OpenRouterModelInfo
func convertMapToOpenRouterModelInfo(data map[string]interface{}) *cline.OpenRouterModelInfo {
info := &cline.OpenRouterModelInfo{}
if val, ok := data["description"].(string); ok {
info.Description = &val
}
if val, ok := data["contextWindow"].(float64); ok {
contextWindow := int64(val)
info.ContextWindow = &contextWindow
}
if val, ok := data["maxTokens"].(float64); ok {
maxTokens := int64(val)
info.MaxTokens = &maxTokens
}
if val, ok := data["inputPrice"].(float64); ok {
info.InputPrice = &val
}
if val, ok := data["outputPrice"].(float64); ok {
info.OutputPrice = &val
}
if val, ok := data["cacheWritesPrice"].(float64); ok {
info.CacheWritesPrice = &val
}
if val, ok := data["cacheReadsPrice"].(float64); ok {
info.CacheReadsPrice = &val
}
if val, ok := data["supportsImages"].(bool); ok {
info.SupportsImages = &val
}
if val, ok := data["supportsPromptCache"].(bool); ok {
info.SupportsPromptCache = val
}
return info
}
// 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)
if err != nil {
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
}
// Step 2: Get all ready providers
readyProviders := result.GetAllReadyProviders()
// Filter out Cline provider (uses account auth, not API keys)
var removableProviders []*ProviderDisplay
for _, provider := range readyProviders {
if provider.Provider != cline.ApiProvider_CLINE {
removableProviders = append(removableProviders, provider)
}
}
// Step 3: Check if there are providers to remove
if len(removableProviders) == 0 {
fmt.Println("\nNo providers available to remove.")
fmt.Println("Note: Cline provider cannot be removed via this menu.")
return nil
}
// Step 4: Display selection menu
var selectedIndex int
options := make([]huh.Option[int], len(removableProviders))
for i, provider := range removableProviders {
// Mark active provider
displayName := GetProviderDisplayName(provider.Provider)
if result.ActProvider != nil && provider.Provider == result.ActProvider.Provider {
displayName += " (ACTIVE)"
}
options[i] = huh.NewOption(displayName, i)
}
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[int]().
Title("Select provider to remove").
Options(options...).
Value(&selectedIndex),
),
)
if err := form.Run(); err != nil {
return fmt.Errorf("failed to select provider: %w", err)
}
selectedProvider := removableProviders[selectedIndex]
// Step 5: Check if trying to remove the active provider
if result.ActProvider != nil && selectedProvider.Provider == result.ActProvider.Provider {
fmt.Printf("\nCannot remove %s because it is currently active.\n", GetProviderDisplayName(selectedProvider.Provider))
fmt.Println("Please switch to a different provider first, then try again.")
return nil
}
// Step 6: Confirm removal
var confirm bool
confirmForm := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(fmt.Sprintf("Are you sure you want to remove %s?", GetProviderDisplayName(selectedProvider.Provider))).
Description("This will clear the API key but preserve the model configuration.").
Value(&confirm),
),
)
if err := confirmForm.Run(); err != nil {
return fmt.Errorf("failed to get confirmation: %w", err)
}
if !confirm {
fmt.Println("Removal cancelled.")
return nil
}
// Step 7: If removing OCA, sign out first
if selectedProvider.Provider == cline.ApiProvider_OCA {
if err := signOutOca(pw.ctx); err != nil {
fmt.Printf("Warning: Failed to sign out of OCA: %v\n", err)
} else {
fmt.Println("Signed out of OCA.")
}
}
// Step 8: Clear the API key for the selected provider
if err := pw.clearProviderAPIKey(selectedProvider.Provider); err != nil {
return fmt.Errorf("failed to remove provider: %w", err)
}
fmt.Printf("\n✓ %s removed successfully\n", GetProviderDisplayName(selectedProvider.Provider))
return nil
}
// clearProviderAPIKey clears the API key field for a specific provider using RemoveProviderPartial
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 {
return err
}
_, err = client.Ocaaccount.OcaAccountLogoutClicked(ctx, &cline.EmptyRequest{})
return err
}
func setWelcomeViewCompleted(ctx context.Context, manager *task.Manager) error {
_, err := manager.GetClient().State.SetWelcomeViewCompleted(ctx, &cline.BooleanRequest{Value: true})
return err
}
+193
View File
@@ -0,0 +1,193 @@
package auth
import (
"context"
"fmt"
"strings"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// BedrockConfig holds all AWS Bedrock-specific configuration fields
type BedrockConfig struct {
// Profile authentication fields
UseProfile bool // Always true for successful config
Profile string // Optional: AWS profile name (empty = default)
Region string // Required: AWS region
Endpoint string // Optional: Custom VPC endpoint URL
// Optional features
UseCrossRegionInference bool // Optional: Enable cross-region inference
UseGlobalInference bool // Optional: Use global inference endpoint
UsePromptCache bool // Optional: Enable prompt caching
// Authentication method (always "profile")
Authentication string // Always set to "profile"
// Legacy fields (no longer used in profile-only flow)
AccessKey string // No longer used
SecretKey string // No longer used
SessionToken string // No longer used
}
// PromptForBedrockConfig displays a profile-first authentication form for Bedrock configuration
func PromptForBedrockConfig(ctx context.Context, manager *task.Manager) (*BedrockConfig, error) {
config := &BedrockConfig{}
// First, ask if user wants to use AWS profile authentication
var useProfile bool
profileQuestion := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("Do you want to use an AWS profile for authentication?").
Description("AWS profiles are managed via 'aws configure'").
Value(&useProfile).
Affirmative("Yes").
Negative("No").
Inline(true),
),
)
if err := profileQuestion.Run(); err != nil {
return nil, fmt.Errorf("failed to get authentication method: %w", err)
}
// If user declines profile authentication, show message and return error
if !useProfile {
fmt.Println("\nAWS profile authentication is currently the only supported method in the CLI.")
fmt.Println("Please configure an AWS profile using 'aws configure' and try again.")
return nil, fmt.Errorf("user declined profile authentication")
}
// User wants profile auth - collect profile configuration
config.UseProfile = true
config.Authentication = "profile"
// Collect profile name, region, and optional settings
configForm := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("AWS Profile Name (optional, press Enter for default profile)").
Value(&config.Profile).
Description("Leave empty to use default AWS profile"),
huh.NewInput().
Title("AWS Region (required, e.g., us-east-1)").
Value(&config.Region).
Validate(func(s string) error {
if strings.TrimSpace(s) == "" {
return fmt.Errorf("AWS Region is required")
}
return nil
}),
huh.NewInput().
Title("Custom VPC Endpoint URL (optional)").
Value(&config.Endpoint).
Description("Press Enter to skip"),
huh.NewConfirm().
Title("Enable Prompt Cache? ").
Value(&config.UsePromptCache).
Affirmative("Yes").
Negative("No").
Inline(true),
huh.NewConfirm().
Title("Enable Cross-Region Inference? ").
Value(&config.UseCrossRegionInference).
Affirmative("Yes").
Negative("No").
Inline(true),
huh.NewConfirm().
Title("Use Global Inference Endpoint? ").
Value(&config.UseGlobalInference).
Affirmative("Yes").
Negative("No").
Inline(true),
),
)
if err := configForm.Run(); err != nil {
return nil, fmt.Errorf("failed to get Bedrock configuration: %w", err)
}
// Trim whitespace from string fields
config.Profile = strings.TrimSpace(config.Profile)
config.Region = strings.TrimSpace(config.Region)
config.Endpoint = strings.TrimSpace(config.Endpoint)
return config, nil
}
// ApplyBedrockConfig applies Bedrock configuration using partial updates (profile-only)
func ApplyBedrockConfig(ctx context.Context, manager *task.Manager, config *BedrockConfig, modelID string, modelInfo interface{}) error {
// Build the API configuration with all Bedrock fields
apiConfig := &cline.ModelsApiConfiguration{}
// Set model ID fields
apiConfig.PlanModeApiModelId = proto.String(modelID)
apiConfig.ActModeApiModelId = proto.String(modelID)
apiConfig.PlanModeAwsBedrockCustomModelBaseId = proto.String(modelID)
apiConfig.ActModeAwsBedrockCustomModelBaseId = proto.String(modelID)
// Set profile authentication fields (always required)
optionalFields := &BedrockOptionalFields{}
optionalFields.Authentication = proto.String("profile")
optionalFields.UseProfile = proto.Bool(true)
optionalFields.Region = proto.String(config.Region)
// Set profile name (can be empty for default profile)
if config.Profile != "" {
optionalFields.Profile = proto.String(config.Profile)
}
// Set optional fields if provided
if config.Endpoint != "" {
optionalFields.Endpoint = proto.String(config.Endpoint)
}
if config.UseCrossRegionInference {
optionalFields.UseCrossRegionInference = proto.Bool(true)
}
if config.UseGlobalInference {
optionalFields.UseGlobalInference = proto.Bool(true)
}
if config.UsePromptCache {
optionalFields.UsePromptCache = proto.Bool(true)
}
// Apply all fields to the config
setBedrockOptionalFields(apiConfig, optionalFields)
// Build field mask including all fields we're setting (excluding access keys)
fieldPaths := []string{
"planModeApiModelId",
"actModeApiModelId",
"planModeAwsBedrockCustomModelBaseId",
"actModeAwsBedrockCustomModelBaseId",
}
// Add profile authentication field paths
optionalPaths := buildBedrockOptionalFieldMask(optionalFields)
fieldPaths = append(fieldPaths, optionalPaths...)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
// Apply the partial update
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to apply Bedrock configuration: %w", err)
}
return nil
}
+366
View File
@@ -0,0 +1,366 @@
package auth
import (
"context"
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// OcaConfig holds Oracle Code Assist (OCA) configuration fields
type OcaConfig struct {
BaseURL string
Mode string
}
// PromptForOcaConfig displays a form for OCA configuration (base URL and mode)
func PromptForOcaConfig(ctx context.Context, manager *task.Manager) (*OcaConfig, error) {
config := &OcaConfig{}
var mode string
// Collect optional settings
configForm := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Base URL").
Value(&config.BaseURL).
Description("Leave empty to use default Base URL"),
huh.NewSelect[string]().
Title("Choose OCA mode (used for authentication)").
Description("Select 'Internal' to use Cline's internal OCA, or 'External' for your own OCA instance").
Options(
huh.NewOption("Internal", "internal"),
huh.NewOption("External", "external"),
).
Value(&mode),
),
)
if err := configForm.Run(); err != nil {
return nil, fmt.Errorf("failed to get OCA configuration: %w", err)
}
// Trim whitespace from string fields
config.BaseURL = strings.TrimSpace(config.BaseURL)
config.Mode = strings.TrimSpace(mode)
return config, nil
}
// ApplyOcaConfig applies OCA configuration using partial updates
func ApplyOcaConfig(ctx context.Context, manager *task.Manager, config *OcaConfig) error {
// Build the API configuration with all OCA fields
apiConfig := &cline.ModelsApiConfiguration{}
// Set profile authentication fields (always required)
optionalFields := &OcaOptionalFields{}
// Set profile name (can be empty for default profile)
if config.BaseURL != "" {
optionalFields.BaseURL = proto.String(config.BaseURL)
}
// Set optional fields if provided
if config.Mode != "" {
optionalFields.Mode = proto.String(config.Mode)
}
// Apply all fields to the config
setOcaOptionalFields(apiConfig, optionalFields)
// Add profile authentication field paths
optionalPaths := buildOcaOptionalFieldMask(optionalFields)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: optionalPaths}
// Apply the partial update
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to apply OCA configuration: %w", err)
}
return nil
}
// ===========================
// OCA Auth Listener Singleton
// ===========================
type ocaAuthStream interface {
Recv() (*cline.OcaAuthState, error)
}
// OcaAuthStatusListener manages subscription to OCA auth status updates
type OcaAuthStatusListener struct {
stream ocaAuthStream
updatesCh chan *cline.OcaAuthState
errCh chan error
ctx context.Context
cancel context.CancelFunc
mu sync.RWMutex
lastState *cline.OcaAuthState
firstEventCh chan struct{}
firstEventOnce sync.Once
}
// NewOcaAuthStatusListener creates a new OCA auth status listener
func NewOcaAuthStatusListener(parentCtx context.Context) (*OcaAuthStatusListener, error) {
client, err := global.GetDefaultClient(parentCtx)
if err != nil {
return nil, fmt.Errorf("failed to get client: %w", err)
}
// Keep the listener alive independently of short-lived caller contexts
ctx, cancel := context.WithCancel(context.Background())
// Subscribe to OCA auth status updates
stream, err := client.Ocaaccount.OcaSubscribeToAuthStatusUpdate(ctx, &cline.EmptyRequest{})
if err != nil {
cancel()
return nil, fmt.Errorf("failed to subscribe to OCA auth updates: %w", err)
}
return &OcaAuthStatusListener{
stream: stream,
updatesCh: make(chan *cline.OcaAuthState, 10),
errCh: make(chan error, 1),
ctx: ctx,
cancel: cancel,
firstEventCh: make(chan struct{}),
}, nil
}
// Start begins listening to the auth status update stream
func (l *OcaAuthStatusListener) Start() error {
go l.readStream()
return nil
}
func (l *OcaAuthStatusListener) readStream() {
defer close(l.updatesCh)
defer close(l.errCh)
for {
select {
case <-l.ctx.Done():
return
default:
state, err := l.stream.Recv()
if err != nil {
// Propagate error and exit
if err == io.EOF {
// Treat as error to notify waiters
err = fmt.Errorf("OCA auth status stream closed")
}
select {
case l.errCh <- err:
case <-l.ctx.Done():
}
return
}
l.mu.Lock()
l.lastState = state
l.mu.Unlock()
// Notify first event waiters
l.firstEventOnce.Do(func() { close(l.firstEventCh) })
select {
case l.updatesCh <- state:
case <-l.ctx.Done():
return
}
}
}
}
// WaitForFirstEvent blocks until the first event is received or timeout occurs
func (l *OcaAuthStatusListener) WaitForFirstEvent(timeout time.Duration) error {
// Fast-path if already have a state
l.mu.RLock()
ready := l.lastState != nil
l.mu.RUnlock()
if ready {
return nil
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-l.firstEventCh:
return nil
case <-timer.C:
return fmt.Errorf("timeout waiting for initial OCA auth event")
case <-l.ctx.Done():
return fmt.Errorf("OCA auth listener cancelled")
}
}
// IsAuthenticated returns true if the last known OCA auth state is authenticated
func (l *OcaAuthStatusListener) IsAuthenticated() bool {
l.mu.RLock()
defer l.mu.RUnlock()
return isOCAStateAuthenticated(l.lastState)
}
// WaitForAuthentication waits until OCA authentication succeeds or timeout occurs
func (l *OcaAuthStatusListener) WaitForAuthentication(timeout time.Duration) error {
timer := time.NewTimer(timeout)
defer timer.Stop()
// If already authenticated, return immediately
if l.IsAuthenticated() {
return nil
}
for {
select {
case <-timer.C:
return fmt.Errorf("OCA authentication timeout after %v - please try again", timeout)
case <-l.ctx.Done():
return fmt.Errorf("OCA authentication cancelled")
case err := <-l.errCh:
return fmt.Errorf("OCA authentication stream error: %w", err)
case state := <-l.updatesCh:
if isOCAStateAuthenticated(state) {
return nil
}
}
}
}
// Stop closes the stream and cleans up resources
func (l *OcaAuthStatusListener) Stop() {
l.cancel()
}
func isOCAStateAuthenticated(state *cline.OcaAuthState) bool {
return state != nil && state.User != nil
}
// Singleton holder
var (
ocaListener *OcaAuthStatusListener
ocaListenerOnce sync.Once
ocaListenerErr error
)
// GetOcaAuthListener returns the OCA auth listener singleton
func GetOcaAuthListener(ctx context.Context) (*OcaAuthStatusListener, error) {
// Allow optional ctx: if nil, use context.TODO(). If already initialized, return singleton.
if ctx == nil {
ctx = context.TODO()
}
ocaListenerOnce.Do(func() {
l, err := NewOcaAuthStatusListener(ctx)
if err != nil {
ocaListenerErr = err
return
}
if err := l.Start(); err != nil {
ocaListenerErr = err
return
}
ocaListener = l
})
return ocaListener, ocaListenerErr
}
// IsOCAAuthenticated returns true if the global OCA auth status is authenticated.
// It attempts a brief wait for the first event to avoid stale reads.
func IsOCAAuthenticated(ctx context.Context) bool {
l, err := GetOcaAuthListener(ctx)
if err != nil {
return false
}
_ = l.WaitForFirstEvent(1 * time.Second) // best-effort
return l.IsAuthenticated()
}
// LatestState returns the last received OCA auth state (may be nil)
func (l *OcaAuthStatusListener) LatestState() *cline.OcaAuthState {
l.mu.RLock()
defer l.mu.RUnlock()
return l.lastState
}
// GetLatestOCAState returns the latest known OCA auth state, optionally waiting for the first event
func GetLatestOCAState(ctx context.Context, timeout time.Duration) (*cline.OcaAuthState, error) {
l, err := GetOcaAuthListener(ctx)
if err != nil {
return nil, err
}
if timeout > 0 {
if err := l.WaitForFirstEvent(timeout); err != nil {
return nil, err
}
}
return l.LatestState(), nil
}
// ensureOcaAuthenticated initiates OCA login (if needed) and waits for success using the singleton listener
func ensureOcaAuthenticated(ctx context.Context) error {
// Ensure listener exists
listener, err := GetOcaAuthListener(ctx)
if err != nil {
return fmt.Errorf("failed to initialize OCA auth listener: %w", err)
}
// Briefly wait for first event to know current state
_ = listener.WaitForFirstEvent(1 * time.Second)
// If already authenticated, nothing to do
if listener.IsAuthenticated() {
fmt.Println("✓ OCA authentication already active.")
return nil
}
// Create gRPC client for initiating login
client, err := global.GetDefaultClient(ctx)
if err != nil {
return fmt.Errorf("failed to obtain client: %w", err)
}
// Start login and wait for authentication
waitCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
// Initiate login (opens the browser with a callback URL from Cline Core)
response, err := client.Ocaaccount.OcaAccountLoginClicked(waitCtx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to initiate OCA login: %w", err)
}
fmt.Println("\nOpening browser for OCA authentication...")
if response != nil && response.Value != "" {
fmt.Printf("If the browser doesn't open automatically, visit this URL:\n%s\n\n", response.Value)
}
fmt.Println("Waiting for you to complete OCA authentication in your browser...")
fmt.Println("(This may take a few moments. Timeout: 5 minutes)")
// Block until authenticated or timeout
if err := listener.WaitForAuthentication(5 * time.Minute); err != nil {
return err
}
fmt.Println("✓ OCA authentication successful!")
return nil
}
+187
View File
@@ -0,0 +1,187 @@
package clerror
import (
"encoding/json"
"fmt"
"strings"
)
// ClineErrorType represents the category of error
type ClineErrorType string
const (
ErrorTypeAuth ClineErrorType = "auth"
ErrorTypeNetwork ClineErrorType = "network"
ErrorTypeRateLimit ClineErrorType = "rateLimit"
ErrorTypeBalance ClineErrorType = "balance"
ErrorTypeUnknown ClineErrorType = "unknown"
)
// ClineError represents a parsed error from Cline API
type ClineError struct {
Message string `json:"message"`
Status int `json:"status"`
RequestID string `json:"request_id"`
Code interface{} `json:"code"` // Can be string or int
ModelID string `json:"modelId"`
ProviderID string `json:"providerId"`
Details map[string]interface{} `json:"details"`
}
// GetCodeString returns the code as a string regardless of its type
func (e *ClineError) GetCodeString() string {
if e == nil || e.Code == nil {
return ""
}
switch v := e.Code.(type) {
case string:
return v
case float64:
return fmt.Sprintf("%.0f", v)
case int:
return fmt.Sprintf("%d", v)
default:
return fmt.Sprintf("%v", v)
}
}
// Rate limit patterns from webview
var rateLimitPatterns = []string{
"status code 429",
"rate limit",
"too many requests",
"quota exceeded",
"resource exhausted",
}
// ParseClineError parses a JSON error string into a ClineError
func ParseClineError(errorJSON string) (*ClineError, error) {
if errorJSON == "" {
return nil, nil
}
var err ClineError
if parseErr := json.Unmarshal([]byte(errorJSON), &err); parseErr != nil {
// If JSON parsing fails, create a simple error with the message
return &ClineError{
Message: errorJSON,
}, nil
}
return &err, nil
}
// GetErrorType determines the type of error based on code, status, and message
func (e *ClineError) GetErrorType() ClineErrorType {
if e == nil {
return ErrorTypeUnknown
}
// Check balance error first (most specific)
codeStr := e.GetCodeString()
if codeStr == "insufficient_credits" {
return ErrorTypeBalance
}
// Check auth errors
if codeStr == "ERR_BAD_REQUEST" || e.Status == 401 {
return ErrorTypeAuth
}
// Check for auth message
if strings.Contains(e.Message, "Authentication required") ||
strings.Contains(e.Message, "Invalid API key") ||
strings.Contains(e.Message, "Unauthorized") {
return ErrorTypeAuth
}
// Check rate limit patterns
messageLower := strings.ToLower(e.Message)
for _, pattern := range rateLimitPatterns {
if strings.Contains(messageLower, pattern) {
return ErrorTypeRateLimit
}
}
return ErrorTypeUnknown
}
// IsBalanceError returns true if this is a balance/credits error
func (e *ClineError) IsBalanceError() bool {
return e.GetErrorType() == ErrorTypeBalance
}
// IsAuthError returns true if this is an authentication error
func (e *ClineError) IsAuthError() bool {
return e.GetErrorType() == ErrorTypeAuth
}
// IsRateLimitError returns true if this is a rate limit error
func (e *ClineError) IsRateLimitError() bool {
return e.GetErrorType() == ErrorTypeRateLimit
}
// GetCurrentBalance returns the current balance if available
func (e *ClineError) GetCurrentBalance() *float64 {
if e == nil || e.Details == nil {
return nil
}
if balance, ok := e.Details["current_balance"].(float64); ok {
return &balance
}
return nil
}
// GetBuyCreditsURL returns the URL to buy credits if available
func (e *ClineError) GetBuyCreditsURL() string {
if e == nil || e.Details == nil {
return ""
}
if url, ok := e.Details["buy_credits_url"].(string); ok {
return url
}
return ""
}
// GetTotalSpent returns the total spent amount if available
func (e *ClineError) GetTotalSpent() *float64 {
if e == nil || e.Details == nil {
return nil
}
if spent, ok := e.Details["total_spent"].(float64); ok {
return &spent
}
return nil
}
// GetTotalPromotions returns the total promotions amount if available
func (e *ClineError) GetTotalPromotions() *float64 {
if e == nil || e.Details == nil {
return nil
}
if promos, ok := e.Details["total_promotions"].(float64); ok {
return &promos
}
return nil
}
// GetDetailMessage returns the detail message from error.details if available
func (e *ClineError) GetDetailMessage() string {
if e == nil || e.Details == nil {
return ""
}
if msg, ok := e.Details["message"].(string); ok {
return msg
}
return ""
}
+152
View File
@@ -0,0 +1,152 @@
package cli
import (
"context"
"fmt"
"github.com/cline/cli/pkg/cli/config"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/spf13/cobra"
)
var configManager *config.Manager
func ensureConfigManager(ctx context.Context, address string) error {
if configManager == nil || (address != "" && configManager.GetCurrentInstance() != address) {
var err error
var instanceAddress string
if address != "" {
// Ensure instance exists at the specified address
if err := ensureInstanceAtAddress(ctx, address); err != nil {
return fmt.Errorf("failed to ensure instance at address %s: %w", address, err)
}
configManager, err = config.NewManager(ctx, address)
instanceAddress = address
} else {
// Ensure default instance exists
if err := global.EnsureDefaultInstance(ctx); err != nil {
return fmt.Errorf("failed to ensure default instance: %w", err)
}
configManager, err = config.NewManager(ctx, "")
if err == nil {
instanceAddress = configManager.GetCurrentInstance()
}
}
if err != nil {
return fmt.Errorf("failed to create config manager: %w", err)
}
// Always set the instance we're using as the default
registry := global.Clients.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)
}
}
return nil
}
func NewConfigCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Aliases: []string{"c"},
Short: "Manage Cline configuration",
Long: `Set and manage global Cline configuration variables.`,
}
cmd.AddCommand(newConfigListCommand())
cmd.AddCommand(newConfigGetCommand())
cmd.AddCommand(setCommand())
return cmd
}
func newConfigGetCommand() *cobra.Command {
var address string
cmd := &cobra.Command{
Use: "get <key>",
Aliases: []string{"g"},
Short: "Get a specific configuration value",
Long: `Get the value of a specific configuration setting. Supports nested keys using dot notation (e.g., auto-approval-settings.actions.read-files).`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
key := args[0]
// Ensure config manager
if err := ensureConfigManager(ctx, address); err != nil {
return err
}
// Get the setting
return configManager.GetSetting(ctx, key)
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
func newConfigListCommand() *cobra.Command {
var address string
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
Short: "List all configuration settings",
Long: `List all configuration settings from the Cline instance.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Ensure config manager
if err := ensureConfigManager(ctx, address); err != nil {
return err
}
// List settings
return configManager.ListSettings(ctx)
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
func setCommand() *cobra.Command {
var address string
cmd := &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.
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),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Parse using existing task parser
settings, secrets, err := task.ParseTaskSettings(args)
if err != nil {
return fmt.Errorf("failed to parse settings: %w", err)
}
// Ensure config manager
if err := ensureConfigManager(ctx, address); err != nil {
return err
}
// Update settings (server-side merge handles preserving existing values)
return configManager.UpdateSettings(ctx, settings, secrets)
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
+208
View File
@@ -0,0 +1,208 @@
package config
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/grpc-go/client"
"github.com/cline/grpc-go/cline"
)
type Manager struct {
client *client.ClineClient
clientAddress string
}
func NewManager(ctx context.Context, address string) (*Manager, error) {
var c *client.ClineClient
var err error
if address != "" {
c, err = global.GetClientForAddress(ctx, address)
} else {
c, err = global.GetDefaultClient(ctx)
}
if err != nil {
return nil, fmt.Errorf("failed to get client: %w", err)
}
// Get the actual address being used
clientAddress := address
if address == "" && global.Clients != nil {
clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
}
return &Manager{
client: c,
clientAddress: clientAddress,
}, nil
}
// GetCurrentInstance returns the address of the current instance
func (m *Manager) GetCurrentInstance() string {
return m.clientAddress
}
func (m *Manager) UpdateSettings(ctx context.Context, settings *cline.Settings, secrets *cline.Secrets) error {
request := &cline.UpdateSettingsRequestCli{
Metadata: &cline.Metadata{},
Settings: settings,
Secrets: secrets,
}
// Call the updateSettingsCli RPC
_, err := m.client.State.UpdateSettingsCli(ctx, request)
if err != nil {
return fmt.Errorf("failed to update settings: %w", err)
}
fmt.Println("Settings updated successfully")
fmt.Printf("Instance: %s\n", m.clientAddress)
return nil
}
func (m *Manager) GetState(ctx context.Context) (map[string]interface{}, error) {
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return nil, fmt.Errorf("failed to get state: %w", err)
}
var stateData map[string]interface{}
if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
return nil, fmt.Errorf("failed to parse state: %w", err)
}
return stateData, nil
}
func (m *Manager) ListSettings(ctx context.Context) error {
// Get state
stateData, err := m.GetState(ctx)
if err != nil {
return err
}
// Subset of fields we will print the values for
settingsFields := []string{
"apiConfiguration",
"telemetrySetting",
"planActSeparateModelsSetting",
"enableCheckpointsSetting",
"mcpMarketplaceEnabled",
"shellIntegrationTimeout",
"terminalReuseEnabled",
"mcpResponsesCollapsed",
"mcpDisplayMode",
"terminalOutputLineLimit",
"mode",
"preferredLanguage",
"openaiReasoningEffort",
"strictPlanModeEnabled",
"focusChainSettings",
"useAutoCondense",
"customPrompt",
"browserSettings",
"defaultTerminalProfile",
"yoloModeToggled",
"dictationSettings",
"autoCondenseThreshold",
"autoApprovalSettings",
}
// Render each field using the renderer
for _, field := range settingsFields {
if value, ok := stateData[field]; ok {
if err := RenderField(field, value, true); err != nil {
fmt.Printf("Error rendering %s: %v\n", field, err)
}
fmt.Println()
}
}
return nil
}
func (m *Manager) GetSetting(ctx context.Context, key string) error {
// Get state
stateData, err := m.GetState(ctx)
if err != nil {
return err
}
// Convert kebab-case to camelCase path
parts := kebabToCamelPath(key)
rootField := parts[0]
// Get the value
value, found := getNestedValue(stateData, parts)
if !found {
return fmt.Errorf("setting '%s' not found", key)
}
// Render the value
if len(parts) == 1 {
// Top-level field: use RenderField for nice formatting
return RenderField(rootField, value, false)
} else {
// Nested field: simple print
fmt.Printf("%s: %s\n", key, formatValue(value, rootField, true))
}
return nil
}
// kebabToCamelPath converts a kebab-case path to camelCase
// e.g., "auto-approval-settings.actions.read-files" -> "autoApprovalSettings.actions.readFiles"
func kebabToCamelPath(path string) []string {
parts := strings.Split(path, ".")
for i, part := range parts {
parts[i] = kebabToCamel(part)
}
return parts
}
// kebabToCamel converts a single kebab-case string to camelCase
// e.g., "auto-approval-settings" -> "autoApprovalSettings"
func kebabToCamel(s string) string {
if s == "" {
return s
}
parts := strings.Split(s, "-")
if len(parts) == 1 {
return s
}
// First part stays lowercase, rest are capitalized
result := parts[0]
for i := 1; i < len(parts); i++ {
if parts[i] != "" {
result += strings.ToUpper(parts[i][:1]) + parts[i][1:]
}
}
return result
}
// getNestedValue retrieves a value from a nested map using dot notation
// e.g., "autoApprovalSettings.actions.readFiles"
func getNestedValue(data map[string]interface{}, parts []string) (interface{}, bool) {
current := interface{}(data)
for _, part := range parts {
// Try to access as map
if m, ok := current.(map[string]interface{}); ok {
if val, exists := m[part]; exists {
current = val
continue
}
return nil, false
}
return nil, false
}
return current, true
}
+198
View File
@@ -0,0 +1,198 @@
package config
import (
"fmt"
"strings"
)
// sensitiveKeywords defines field name patterns that should be censored
var sensitiveKeywords = []string{"key", "secret", "password", "cline-account-id"}
// camelToKebab converts camelCase to kebab-case
// e.g., "autoApprovalSettings" -> "auto-approval-settings"
func camelToKebab(s string) string {
if s == "" {
return s
}
var result []rune
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
result = append(result, '-')
}
result = append(result, r|32) // Convert to lowercase (works for A-Z)
}
return string(result)
}
// isSensitiveField checks if a field name contains sensitive keywords
func isSensitiveField(fieldName string) bool {
if fieldName == "" {
return false
}
lowerName := strings.ToLower(fieldName)
for _, keyword := range sensitiveKeywords {
if strings.Contains(lowerName, keyword) {
return true
}
}
return false
}
// formatValue formats a value for display, handling empty strings and censoring sensitive fields
func formatValue(val interface{}, fieldName string, censor bool) string {
// Handle empty strings specifically
if str, ok := val.(string); ok && str == "" {
return "''"
}
if censor && isSensitiveField(fieldName) {
valStr := fmt.Sprintf("%v", val)
if valStr != "" && valStr != "''" {
return "********"
}
}
return fmt.Sprintf("%v", val)
}
// RenderField renders a single config field with proper formatting
func RenderField(key string, value interface{}, censor bool) error {
switch key {
// Nested objects - render with header + nested fields
case "apiConfiguration":
return renderApiConfiguration(value, censor)
case "browserSettings":
return renderBrowserSettings(value, censor)
case "focusChainSettings":
return renderFocusChainSettings(value, censor)
case "dictationSettings":
return renderDictationSettings(value, censor)
case "autoApprovalSettings":
return renderAutoApprovalSettings(value, censor)
// Simple values - just print key: value
case "mode", "telemetrySetting", "preferredLanguage", "customPrompt",
"defaultTerminalProfile", "mcpDisplayMode", "openaiReasoningEffort",
"planActSeparateModelsSetting", "enableCheckpointsSetting",
"mcpMarketplaceEnabled", "terminalReuseEnabled",
"mcpResponsesCollapsed", "strictPlanModeEnabled",
"useAutoCondense", "yoloModeToggled", "shellIntegrationTimeout",
"terminalOutputLineLimit", "autoCondenseThreshold":
fmt.Printf("%s: %s\n", camelToKebab(key), formatValue(value, key, censor))
return nil
default:
return fmt.Errorf("unknown config field: %s", key)
}
}
// renderApiConfiguration renders the API configuration object
func renderApiConfiguration(value interface{}, censor bool) error {
fmt.Println("api-configuration:")
configMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid api-configuration format")
}
// Print each field directly
for key, val := range configMap {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
return nil
}
// renderBrowserSettings renders browser settings
func renderBrowserSettings(value interface{}, censor bool) error {
fmt.Println("browser-settings:")
settingsMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid browser-settings format")
}
// Handle nested viewport if present
if viewport, ok := settingsMap["viewport"].(map[string]interface{}); ok {
fmt.Println(" viewport:")
for key, val := range viewport {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
}
// Print other fields
for key, val := range settingsMap {
if key != "viewport" {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
}
return nil
}
// renderFocusChainSettings renders focus chain settings
func renderFocusChainSettings(value interface{}, censor bool) error {
fmt.Println("focus-chain-settings:")
settingsMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid focus-chain-settings format")
}
for key, val := range settingsMap {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
return nil
}
// renderDictationSettings renders dictation settings
func renderDictationSettings(value interface{}, censor bool) error {
fmt.Println("dictation-settings:")
settingsMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid dictation-settings format")
}
for key, val := range settingsMap {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
return nil
}
// renderAutoApprovalSettings renders auto approval settings
func renderAutoApprovalSettings(value interface{}, censor bool) error {
fmt.Println("auto-approval-settings:")
settingsMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid auto-approval-settings format")
}
// Print top-level fields (skip version, handle actions specially)
for key, val := range settingsMap {
if key == "version" {
continue // Skip version
}
if key == "actions" {
// Handle nested actions with double indentation
fmt.Println(" actions:")
if actionsMap, ok := val.(map[string]interface{}); ok {
for actionKey, actionVal := range actionsMap {
fmt.Printf(" %s: %s\n", camelToKebab(actionKey), formatValue(actionVal, actionKey, censor))
}
}
} else {
// Print other fields normally (enabled, enableNotifications, favorites)
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
}
return nil
}
+27
View File
@@ -0,0 +1,27 @@
package display
import (
"fmt"
"os"
"golang.org/x/term"
)
func isTTY() bool {
return term.IsTerminal(int(os.Stdout.Fd()))
}
func ClearLine() {
if !isTTY() {
return
}
fmt.Print("\r\033[K")
}
// ClearToEnd clears from cursor to end of screen
func ClearToEnd() {
if !isTTY() {
return
}
fmt.Print("\033[J")
}
+211
View File
@@ -0,0 +1,211 @@
package display
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/charmbracelet/lipgloss"
)
// BannerInfo contains information to display in the session banner
type BannerInfo struct {
Version string
Provider string
ModelID string
Workdir string
Mode string
}
// RenderSessionBanner renders a nice banner showing version, model, and workspace info
func RenderSessionBanner(info BannerInfo) string {
// Bright white for title
titleStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("15")). // Bright white
Bold(true)
// Dim gray for regular text (same as huh placeholder)
dimStyle := lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "248", Dark: "238"})
// Border color matches mode
borderColor := lipgloss.Color("3") // Yellow for plan
if info.Mode == "act" {
borderColor = lipgloss.Color("39") // Blue for act
}
boxStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(borderColor).
Padding(1, 4)
var lines []string
// Format version with "v" prefix if it starts with a number
versionStr := info.Version
if len(versionStr) > 0 && versionStr[0] >= '0' && versionStr[0] <= '9' {
versionStr = "v" + versionStr
}
// First line: "cline cli vX.X.X" on left, "plan mode" on right
leftSide := titleStyle.Render("cline cli preview") + " " + dimStyle.Render(versionStr)
if info.Mode != "" {
modeColor := lipgloss.Color("3") // Yellow for plan
if info.Mode == "act" {
modeColor = lipgloss.Color("39") // Blue for act
}
modeStyle := lipgloss.NewStyle().Foreground(modeColor).Bold(true)
rightSide := modeStyle.Render(info.Mode + " mode")
// Calculate spacing to push mode to the right
// Assume a reasonable width (we'll adjust based on content)
lineWidth := 50
leftWidth := lipgloss.Width(leftSide)
rightWidth := lipgloss.Width(rightSide)
spacing := lineWidth - leftWidth - rightWidth
if spacing > 0 {
titleLine := leftSide + strings.Repeat(" ", spacing) + rightSide
lines = append(lines, titleLine)
} else {
// If too narrow, just put them on same line with a space
lines = append(lines, leftSide+" "+rightSide)
}
} else {
// No mode, just show title
lines = append(lines, leftSide)
}
// Model line - dim gray
if info.Provider != "" && info.ModelID != "" {
lines = append(lines, dimStyle.Render(info.Provider+"/"+shortenPath(info.ModelID, 30)))
}
// Workspace line - dim gray
if info.Workdir != "" {
lines = append(lines, dimStyle.Render(shortenPath(info.Workdir, 45)))
}
content := lipgloss.JoinVertical(lipgloss.Left, lines...)
return boxStyle.Render(content)
}
// shortenPath shortens a filesystem path to fit within maxLen
func shortenPath(path string, maxLen int) string {
// Try to replace home directory with ~ (cross-platform)
if homeDir, err := os.UserHomeDir(); err == nil {
if strings.HasPrefix(path, homeDir) {
shortened := "~" + path[len(homeDir):]
// Always use ~ version if we can
path = shortened
}
}
if len(path) <= maxLen {
return path
}
// If still too long, show last few path components
if len(path) > maxLen {
parts := strings.Split(path, string(filepath.Separator))
if len(parts) > 2 {
// Show last 2-3 components
lastParts := parts[len(parts)-2:]
shortened := "..." + string(filepath.Separator) + strings.Join(lastParts, string(filepath.Separator))
if len(shortened) <= maxLen {
return shortened
}
}
}
// Last resort: truncate with ellipsis
if len(path) > maxLen {
return "..." + path[len(path)-maxLen+3:]
}
return path
}
// ExtractBannerInfoFromState extracts banner info from state JSON
func ExtractBannerInfoFromState(stateJSON, version string) (BannerInfo, error) {
var state map[string]interface{}
if err := json.Unmarshal([]byte(stateJSON), &state); err != nil {
return BannerInfo{}, fmt.Errorf("failed to parse state JSON: %w", err)
}
info := BannerInfo{
Version: version,
}
// Extract mode
if mode, ok := state["mode"].(string); ok {
info.Mode = mode
}
// Extract workspace roots
if workspaceRoots, ok := state["workspaceRoots"].([]interface{}); ok && len(workspaceRoots) > 0 {
if root, ok := workspaceRoots[0].(map[string]interface{}); ok {
if path, ok := root["path"].(string); ok {
info.Workdir = path
}
}
}
// Extract API configuration to get provider/model
if apiConfig, ok := state["apiConfiguration"].(map[string]interface{}); ok {
// Try common keys for provider and model (both camelCase and lowercase variants)
providerKeys := []string{"apiProvider", "api_provider"}
modelKeys := []string{"apiModelId", "api_model_id"}
// Try to extract provider
for _, key := range providerKeys {
if provider, ok := apiConfig[key].(string); ok && provider != "" {
info.Provider = provider
break
}
}
// Try to extract model ID
for _, key := range modelKeys {
if modelID, ok := apiConfig[key].(string); ok && modelID != "" {
info.ModelID = shortenModelID(modelID)
break
}
}
}
return info, nil
}
// shortenModelID shortens long model IDs for display
func shortenModelID(modelID string) string {
// Remove date suffixes only if they're at the end (e.g., -20241022)
// Check if the model ID ends with -YYYYMMDD pattern
if len(modelID) > 9 {
suffix := modelID[len(modelID)-9:] // Last 9 chars: -20241022
if suffix[0] == '-' &&
(strings.HasPrefix(suffix[1:], "202") || strings.HasPrefix(suffix[1:], "201")) {
// Verify all remaining chars are digits
allDigits := true
for _, c := range suffix[1:] {
if c < '0' || c > '9' {
allDigits = false
break
}
}
if allDigits {
return modelID[:len(modelID)-9]
}
}
}
// If still too long, show first 40 chars
if len(modelID) > 40 {
return modelID[:37] + "..."
}
return modelID
}
+139
View File
@@ -0,0 +1,139 @@
package display
import (
"os"
"strconv"
"strings"
"fmt"
"github.com/charmbracelet/glamour"
"golang.org/x/term"
)
type MarkdownRenderer struct {
renderer *glamour.TermRenderer
width int
}
// i went back and forth on whether or not to enable word wrap
// setting line width to 0 enables the terminal to handle wrapping
// setting it to a terminal width enables glamour's word wrap
// the thing is, glamour's nice indentation looks really good, and
// won't work without glamour's word wrap - if you use the terminal's
// word wrap, the indentation looks weird so you have to turn it off
// and everything will be right next to the left margin
// but if you DO use glamours word wrap, it also means if you resize the terminal,
// it will scuff everything. but given that this is the case for the input anyway,
// i figure we just make things as beautiful as possible
// and if you resize the terminal, you'll learn real quick.
// anyway, you can set this to true or false to experiment
const USETERMINALWORDWRAP = true
// seems like a reliable way to check for terminals
// for now i'm keeping everything as auto
// eventually we can define a custom glamour style for ghostty / iterm
// https://github.com/charmbracelet/glamour/blob/master/styles/README.md)
func detectTerminalTheme() string {
switch os.Getenv("TERM_PROGRAM") {
case "iTerm.app", "Ghostty":
return "dark"
}
if os.Getenv("GHOSTTY_VERSION") != "" {
return "dark"
}
return "dark"
}
func glamourStyleJSON(terminalWrap bool) string {
const tmpl = `{
"document": {
"block_prefix": "\n",
"block_suffix": "\n",
"color": "252",
"margin": %s
},
"code_block": {
"margin": 0
}
}`
if terminalWrap {
return fmt.Sprintf(tmpl, "0")
}
return fmt.Sprintf(tmpl, "2")
}
func NewMarkdownRenderer() (*MarkdownRenderer, error) {
var wordWrap int
if USETERMINALWORDWRAP {
// terminal handles wrapping -> disable glamour wrap
wordWrap = 0
} else {
// glamour handles wrapping -> set to current width
wordWrap = terminalWidthOr(0)
}
r, err := glamour.NewTermRenderer(
glamour.WithStandardStyle(detectTerminalTheme()), // Load full auto style first
glamour.WithStylesFromJSONBytes([]byte(glamourStyleJSON(USETERMINALWORDWRAP))), // Then override just margins
glamour.WithWordWrap(wordWrap),
glamour.WithPreservedNewLines(),
)
if err != nil {
return nil, err
}
return &MarkdownRenderer{
renderer: r,
width: 0, // Unlimited width
}, nil
}
// terminalWidthOr returns the terminal width or the provided fallback.
// It first tries term.GetSize, then falls back to $COLUMNS if set.
func terminalWidthOr(fallback int) int {
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 {
return w
}
if cols := os.Getenv("COLUMNS"); cols != "" {
if n, err := strconv.Atoi(cols); err == nil && n > 0 {
return n
}
}
return fallback
}
// NewMarkdownRendererWithWidth creates a markdown renderer with a specific width.
// Useful for tables and other content that should fit within terminal bounds.
func NewMarkdownRendererWithWidth(width int) (*MarkdownRenderer, error) {
r, err := glamour.NewTermRenderer(
glamour.WithStandardStyle(detectTerminalTheme()),
glamour.WithStylesFromJSONBytes([]byte(glamourStyleJSON(false))),
glamour.WithWordWrap(width),
glamour.WithPreservedNewLines(),
)
if err != nil {
return nil, err
}
return &MarkdownRenderer{renderer: r, width: width}, nil
}
// NewMarkdownRendererForTerminal creates a markdown renderer using the actual terminal width.
// Falls back to 120 if terminal width cannot be determined.
func NewMarkdownRendererForTerminal() (*MarkdownRenderer, error) {
width := terminalWidthOr(120)
return NewMarkdownRendererWithWidth(width)
}
func (mr *MarkdownRenderer) Render(markdown string) (string, error) {
rendered, err := mr.renderer.Render(markdown)
if err != nil {
return "", err
}
return strings.TrimLeft(strings.TrimRight(rendered, "\n"), "\n"), nil
}
+168 -40
View File
@@ -3,44 +3,69 @@ package display
import (
"fmt"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/output"
"github.com/cline/cli/pkg/cli/types"
"github.com/cline/grpc-go/cline"
)
type Renderer struct {
typewriter *TypewriterPrinter
typewriter *TypewriterPrinter
mdRenderer *MarkdownRenderer
outputFormat string
// Lipgloss styles that respect outputFormat
dimStyle lipgloss.Style
greenStyle lipgloss.Style
redStyle lipgloss.Style
yellowStyle lipgloss.Style
blueStyle lipgloss.Style
whiteStyle lipgloss.Style
boldStyle lipgloss.Style
successStyle lipgloss.Style
}
func NewRenderer() *Renderer {
return &Renderer{
typewriter: NewTypewriterPrinter(DefaultTypewriterConfig()),
func NewRenderer(outputFormat string) *Renderer {
mdRenderer, err := NewMarkdownRenderer()
if err != nil {
mdRenderer = nil
}
r := &Renderer{
typewriter: NewTypewriterPrinter(DefaultTypewriterConfig()),
mdRenderer: mdRenderer,
outputFormat: outputFormat,
}
// Initialize lipgloss styles (will respect the global color profile)
r.dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
r.greenStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2"))
r.redStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
r.yellowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("3"))
r.blueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
r.whiteStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("7"))
r.boldStyle = lipgloss.NewStyle().Bold(true)
r.successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true)
return r
}
// RenderMessage renders a message with timestamp and prefix
func (r *Renderer) RenderMessage(timestamp, prefix, text string) error {
func (r *Renderer) RenderMessage(prefix, text string, newline bool) error {
if text == "" {
return nil
}
cleanText := r.sanitizeText(text)
if cleanText == "" {
clean := r.sanitizeText(text)
if clean == "" {
return nil
}
r.typewriter.PrintMessageLine(timestamp, prefix, cleanText)
return nil
}
// RenderCommand renders a command execution
func (r *Renderer) RenderCommand(timestamp, command string, isExecuting bool) error {
if isExecuting {
r.typewriter.PrintMessageLine(timestamp, "EXEC", command)
if newline {
output.Printf("%s: %s\n", prefix, clean)
} else {
r.typewriter.PrintMessageLine(timestamp, "CMD", command)
output.Printf("%s: %s", prefix, clean)
}
return nil
}
@@ -57,34 +82,58 @@ func formatNumber(n int) string {
// formatUsageInfo formats token usage information (extracted from RenderAPI)
func (r *Renderer) formatUsageInfo(tokensIn, tokensOut, cacheReads, cacheWrites int, cost float64) string {
tokenDetails := fmt.Sprintf("[tokens in: %s, out: %s; cache read: %s, write: %s]",
formatNumber(tokensIn),
formatNumber(tokensOut),
formatNumber(cacheReads),
formatNumber(cacheWrites))
parts := make([]string, 0, 4)
return fmt.Sprintf("%s ($%.4f)", tokenDetails, cost)
if tokensIn != 0 {
parts = append(parts, fmt.Sprintf("↑ %s", formatNumber(tokensIn)))
}
if tokensOut != 0 {
parts = append(parts, fmt.Sprintf("↓ %s", formatNumber(tokensOut)))
}
if cacheReads != 0 {
parts = append(parts, fmt.Sprintf("→ %s", formatNumber(cacheReads)))
}
if cacheWrites != 0 {
parts = append(parts, fmt.Sprintf("← %s", formatNumber(cacheWrites)))
}
if len(parts) == 0 {
return fmt.Sprintf("$%.4f", cost)
}
return fmt.Sprintf("%s $%.4f", strings.Join(parts, " "), cost)
}
// RenderAPI renders API request information
func (r *Renderer) RenderAPI(timestamp, status string, apiInfo *types.APIRequestInfo) error {
func (r *Renderer) RenderAPI(status string, apiInfo *types.APIRequestInfo) error {
if apiInfo.Cost >= 0 {
message := fmt.Sprintf("%s %s", status, r.formatUsageInfo(apiInfo.TokensIn, apiInfo.TokensOut, apiInfo.CacheReads, apiInfo.CacheWrites, apiInfo.Cost))
r.typewriter.PrintMessageLine(timestamp, "API INFO", message)
usageInfo := r.formatUsageInfo(apiInfo.TokensIn, apiInfo.TokensOut, apiInfo.CacheReads, apiInfo.CacheWrites, apiInfo.Cost)
markdown := fmt.Sprintf("## API %s `%s`", status, usageInfo)
rendered := r.RenderMarkdown(markdown)
output.Print(rendered)
} else {
r.typewriter.PrintMessageLine(timestamp, "API INFO", status)
// honestly i see no point in showing "### API processing request" here...
// markdown := fmt.Sprintf("## API %s", status)
// rendered := r.RenderMarkdown(markdown)
// output.Printf("\n%s\n", rendered)
}
return nil
}
// RenderRetry renders retry information
func (r *Renderer) RenderRetry(timestamp string, attempt, maxAttempts, delaySec int) error {
func (r *Renderer) RenderRetry(attempt, maxAttempts, delaySec int) error {
message := fmt.Sprintf("Retrying failed attempt %d/%d", attempt, maxAttempts)
if delaySec > 0 {
message += fmt.Sprintf(" in %d seconds", delaySec)
}
message += "..."
r.typewriter.PrintMessageLine(timestamp, "API INFO", message)
r.typewriter.PrintMessageLine("API INFO", message)
return nil
}
func (r *Renderer) RenderTaskCancelled() error {
markdown := "## Task cancelled"
rendered := r.RenderMarkdown(markdown)
output.Printf("\n%s\n", rendered)
return nil
}
@@ -101,16 +150,16 @@ func (r *Renderer) RenderTaskList(tasks []*cline.TaskItem) error {
r.typewriter.PrintfLn("=== Task History (showing last %d of %d total tasks) ===\n", len(recentTasks), len(tasks))
for i, task := range recentTasks {
r.typewriter.PrintfLn("Task ID: %s", task.Id)
for i, taskItem := range recentTasks {
r.typewriter.PrintfLn("Task ID: %s", taskItem.Id)
description := task.Task
description := taskItem.Task
if len(description) > 1000 {
description = description[:1000] + "..."
}
r.typewriter.PrintfLn("Message: %s", description)
usageInfo := r.formatUsageInfo(int(task.TokensIn), int(task.TokensOut), int(task.CacheReads), int(task.CacheWrites), task.TotalCost)
usageInfo := r.formatUsageInfo(int(taskItem.TokensIn), int(taskItem.TokensOut), int(taskItem.CacheReads), int(taskItem.CacheWrites), taskItem.TotalCost)
r.typewriter.PrintfLn("Usage : %s", usageInfo)
// Single space between tasks (except last)
@@ -124,19 +173,18 @@ func (r *Renderer) RenderTaskList(tasks []*cline.TaskItem) error {
func (r *Renderer) RenderDebug(format string, args ...interface{}) error {
if global.Config.Verbose {
timestamp := time.Now().Format("15:04:05")
message := fmt.Sprintf(format, args...)
r.typewriter.PrintMessageLine(timestamp, "[DEBUG]", message)
r.typewriter.PrintMessageLine("[DEBUG]", message)
}
return nil
}
func (r *Renderer) ClearLine() {
fmt.Print("\r\033[K")
output.Print("\r\033[K")
}
func (r *Renderer) MoveCursorUp(n int) {
fmt.Printf("\033[%dA", n)
output.Printf("\033[%dA", n)
}
func (r *Renderer) sanitizeText(text string) string {
@@ -174,3 +222,83 @@ func (r *Renderer) SetTypewriterSpeed(multiplier float64) {
func (r *Renderer) GetTypewriter() *TypewriterPrinter {
return r.typewriter
}
func (r *Renderer) GetMdRenderer() *MarkdownRenderer {
return r.mdRenderer
}
// RenderMarkdown renders markdown text to terminal format with ANSI codes
// Falls back to plaintext if markdown rendering is unavailable or fails
// Respects output format - skips rendering in plain mode or non-TTY contexts
func (r *Renderer) RenderMarkdown(markdown string) string {
// Skip markdown rendering if:
// 1. Output format is explicitly "plain"
// 2. Not in a TTY (piped output, file redirect, CI, etc.)
if r.outputFormat == "plain" || !isTTY() {
return markdown
}
if r.mdRenderer == nil {
return markdown
}
rendered, err := r.mdRenderer.Render(markdown)
if err != nil {
return markdown
}
return rendered
}
// Lipgloss-based color rendering methods
// These automatically respect the output format via lipgloss color profile
// Dim renders text in dim gray (bright black)
func (r *Renderer) Dim(text string) string {
return r.dimStyle.Render(text)
}
// Green renders text in green
func (r *Renderer) Green(text string) string {
return r.greenStyle.Render(text)
}
// Red renders text in red
func (r *Renderer) Red(text string) string {
return r.redStyle.Render(text)
}
// Yellow renders text in yellow
func (r *Renderer) Yellow(text string) string {
return r.yellowStyle.Render(text)
}
// Blue renders text in 256-color blue (index 39)
func (r *Renderer) Blue(text string) string {
return r.blueStyle.Render(text)
}
// White renders text in white
func (r *Renderer) White(text string) string {
return r.whiteStyle.Render(text)
}
// Bold renders text in bold
func (r *Renderer) Bold(text string) string {
return r.boldStyle.Render(text)
}
// Success renders text in green with bold
func (r *Renderer) Success(text string) string {
return r.successStyle.Render(text)
}
// SuccessWithCheckmark renders text in green with bold and a checkmark prefix
func (r *Renderer) SuccessWithCheckmark(text string) string {
return r.Success("✓ " + text)
}
// ErrorWithX renders text in red with an X prefix
func (r *Renderer) ErrorWithX(text string) string {
return r.Red("✗ " + text)
}
+212
View File
@@ -0,0 +1,212 @@
package display
import (
"encoding/json"
"fmt"
"strings"
"sync"
"github.com/cline/cli/pkg/cli/output"
"github.com/cline/cli/pkg/cli/types"
)
type StreamingSegment struct {
mu sync.Mutex
sayType string
prefix string
buffer strings.Builder
frozen bool
mdRenderer *MarkdownRenderer
toolRenderer *ToolRenderer
shouldMarkdown bool
outputFormat string
msg *types.ClineMessage
toolParser *ToolResultParser
}
func NewStreamingSegment(sayType, prefix string, mdRenderer *MarkdownRenderer, shouldMarkdown bool, msg *types.ClineMessage, outputFormat string) *StreamingSegment {
ss := &StreamingSegment{
sayType: sayType,
prefix: prefix,
mdRenderer: mdRenderer,
toolRenderer: NewToolRenderer(mdRenderer, outputFormat),
shouldMarkdown: shouldMarkdown,
outputFormat: outputFormat,
msg: msg,
toolParser: NewToolResultParser(mdRenderer),
}
// Render rich header immediately when creating segment (if in rich mode and TTY)
if shouldMarkdown && outputFormat != "plain" && isTTY() {
header := ss.generateRichHeader()
rendered, _ := mdRenderer.Render(header)
output.Println("")
output.Print(rendered)
}
return ss
}
func (ss *StreamingSegment) AppendText(text string) {
ss.mu.Lock()
defer ss.mu.Unlock()
if ss.frozen {
return
}
// Replace buffer with FULL text - msg.Text contains complete accumulated content
ss.buffer.Reset()
ss.buffer.WriteString(text)
// No rendering during streaming - we'll render once on Freeze()
}
func (ss *StreamingSegment) Freeze() {
ss.mu.Lock()
defer ss.mu.Unlock()
if ss.frozen {
return
}
ss.frozen = true
currentBuffer := ss.buffer.String()
// Render and print the final markdown
ss.renderFinal(currentBuffer)
}
func (ss *StreamingSegment) renderFinal(currentBuffer string) {
var bodyContent string
// Use ToolRenderer for all body rendering to centralize logic
if ss.sayType == "ask" {
// Handle ASK messages
if ss.msg.Ask == string(types.AskTypeTool) {
// Tool approval: use ToolRenderer for body
var tool types.ToolMessage
if err := json.Unmarshal([]byte(currentBuffer), &tool); err == nil {
// For approval requests in streaming, use the preview method
bodyContent = ss.toolRenderer.GenerateToolContentPreview(&tool)
}
} else if ss.msg.Ask == string(types.AskTypeFollowup) {
// Followup question: use ToolRenderer
bodyContent = ss.toolRenderer.GenerateAskFollowupBody(currentBuffer)
} else if ss.msg.Ask == string(types.AskTypePlanModeRespond) {
// Plan mode respond: use ToolRenderer
bodyContent = ss.toolRenderer.GeneratePlanModeRespondBody(currentBuffer)
} else if ss.msg.Ask == string(types.AskTypeCommand) {
// Command approval: no body needed - header shows command, output shown separately later
bodyContent = ""
} else {
// For other ask types, render as-is
bodyContent = currentBuffer
}
} else if ss.sayType == string(types.SayTypeTool) {
// Tool execution (SAY): use ToolRenderer for body
var tool types.ToolMessage
if err := json.Unmarshal([]byte(currentBuffer), &tool); err == nil {
bodyContent = ss.toolRenderer.GenerateToolContentBody(&tool)
}
} else if ss.sayType == string(types.SayTypeCommand) {
// Command output
bodyContent = "```shell\n" + currentBuffer + "\n```"
// Render markdown only in rich mode and TTY
if ss.shouldMarkdown && ss.outputFormat != "plain" && isTTY() {
rendered, err := ss.mdRenderer.Render(bodyContent)
if err == nil {
bodyContent = rendered
}
}
} else {
// For other types (reasoning, text, etc.), render markdown as-is
if ss.shouldMarkdown && ss.outputFormat != "plain" && isTTY() {
rendered, err := ss.mdRenderer.Render(currentBuffer)
if err == nil {
bodyContent = rendered
} else {
bodyContent = currentBuffer
}
} else {
bodyContent = currentBuffer
}
}
// Print the body content
if bodyContent != "" {
if !strings.HasSuffix(bodyContent, "\n") {
output.Print(bodyContent)
output.Println("")
} else {
output.Print(bodyContent)
}
}
}
// generateRichHeader generates a contextual header for the segment
func (ss *StreamingSegment) generateRichHeader() string {
switch ss.sayType {
case string(types.SayTypeReasoning):
return "### Cline is thinking\n"
case string(types.SayTypeText):
return "### Cline responds\n"
case string(types.SayTypeCompletionResult):
return "### Task completed\n"
case string(types.SayTypeTool):
return ss.generateToolHeader()
case "ask":
// Check the specific ask type
if ss.msg.Ask == string(types.AskTypePlanModeRespond) {
return ss.toolRenderer.GeneratePlanModeRespondHeader()
}
// For tool approvals, show proper tool header
if ss.msg.Ask == string(types.AskTypeTool) {
var tool types.ToolMessage
if err := json.Unmarshal([]byte(ss.msg.Text), &tool); err == nil {
// Use ToolRenderer for approval header with "wants to" verbs
return ss.toolRenderer.RenderToolApprovalHeader(&tool)
}
}
// For command approvals, show command header
if ss.msg.Ask == string(types.AskTypeCommand) {
command := strings.TrimSpace(ss.msg.Text)
if strings.HasSuffix(command, "REQ_APP") {
command = strings.TrimSuffix(command, "REQ_APP")
command = strings.TrimSpace(command)
}
return fmt.Sprintf("### Cline wants to run `%s`\n", command)
}
// For followup questions, show question header
if ss.msg.Ask == string(types.AskTypeFollowup) {
return ss.toolRenderer.GenerateAskFollowupHeader()
}
// For other ask types, show generic message
return fmt.Sprintf("### Cline is asking (%s)\n", ss.msg.Ask)
default:
return fmt.Sprintf("### %s\n", ss.prefix)
}
}
// generateToolHeader generates a contextual header for tool operations
func (ss *StreamingSegment) generateToolHeader() string {
// Parse tool JSON from message text
var tool types.ToolMessage
if err := json.Unmarshal([]byte(ss.msg.Text), &tool); err != nil {
return "### Tool operation\n"
}
// Use unified ToolRenderer for header
return ss.toolRenderer.RenderToolExecutionHeader(&tool)
}
+86 -401
View File
@@ -1,7 +1,6 @@
package display
import (
"encoding/json"
"fmt"
"strings"
"sync"
@@ -11,18 +10,26 @@ import (
// StreamingDisplay manages streaming message display with deduplication
type StreamingDisplay struct {
mu sync.RWMutex
state *types.ConversationState
renderer *Renderer
dedupe *MessageDeduplicator
mu sync.RWMutex
state *types.ConversationState
renderer *Renderer
dedupe *MessageDeduplicator
activeSegment *StreamingSegment
mdRenderer *MarkdownRenderer
}
// NewStreamingDisplay creates a new streaming display manager
func NewStreamingDisplay(state *types.ConversationState, renderer *Renderer) *StreamingDisplay {
mdRenderer, err := NewMarkdownRenderer()
if err != nil {
panic(fmt.Sprintf("Failed to initialize markdown renderer: %v", err))
}
return &StreamingDisplay{
state: state,
renderer: renderer,
dedupe: NewMessageDeduplicator(),
state: state,
renderer: renderer,
dedupe: NewMessageDeduplicator(),
mdRenderer: mdRenderer,
}
}
@@ -31,418 +38,96 @@ func (s *StreamingDisplay) HandlePartialMessage(msg *types.ClineMessage) error {
s.mu.Lock()
defer s.mu.Unlock()
messageKey := fmt.Sprintf("%d", msg.Timestamp)
timestamp := msg.GetTimestamp()
// Check for deduplication
if s.dedupe.IsDuplicate(msg) {
return nil
}
// Get current streaming state
streamingMsg := s.state.GetStreamingMessage()
// Segment-based header-only streaming
// Partial stream only shows headers immediately, state stream will handle content bodies
sayType := msg.Say
if msg.Type == types.MessageTypeAsk {
sayType = "ask"
}
switch msg.Type {
case types.MessageTypeAsk:
return s.handleStreamingAsk(msg, messageKey, timestamp, streamingMsg)
case types.MessageTypeSay:
return s.handleStreamingSay(msg, messageKey, timestamp, streamingMsg)
// Detect segment boundary
if s.activeSegment != nil && s.activeSegment.sayType != sayType {
// Just cleanup, don't freeze (no body to print)
s.activeSegment = nil
}
// On first partial message for a new segment type, create segment (prints header)
if s.activeSegment == nil && msg.Partial {
shouldMd := s.shouldRenderMarkdown(sayType)
prefix := s.getPrefix(sayType)
// NewStreamingSegment prints the header immediately
s.activeSegment = NewStreamingSegment(sayType, prefix, s.mdRenderer, shouldMd, msg, s.renderer.outputFormat)
// Header printed, done - don't append text or freeze
return nil
}
// For subsequent partial messages, do nothing (header already shown)
if msg.Partial {
return nil
}
// When message is complete (partial=false), render the content body
if s.activeSegment != nil {
// Had an active segment from partial messages - freeze to render body
s.activeSegment.AppendText(msg.Text)
s.activeSegment.Freeze()
s.activeSegment = nil
} else if !msg.Partial {
// Message arrived complete without partial phase - create segment and render immediately
shouldMd := s.shouldRenderMarkdown(sayType)
prefix := s.getPrefix(sayType)
segment := NewStreamingSegment(sayType, prefix, s.mdRenderer, shouldMd, msg, s.renderer.outputFormat)
segment.AppendText(msg.Text)
segment.Freeze()
}
return nil
}
func (s *StreamingDisplay) shouldRenderMarkdown(sayType string) bool {
switch sayType {
case string(types.SayTypeReasoning), string(types.SayTypeText), string(types.SayTypeCompletionResult), string(types.SayTypeTool), "ask":
return true
default:
return s.renderer.RenderMessage(timestamp, "🤖", msg.Text)
}
}
// handleStreamingAsk handles streaming ASK messages
func (s *StreamingDisplay) handleStreamingAsk(msg *types.ClineMessage, messageKey, timestamp string, streamingMsg *types.StreamingMessage) error {
if msg.Text == "" {
return nil
}
cleanText := s.renderer.sanitizeText(msg.Text)
if cleanText == "" {
return nil
}
// Check if this is an update to the same ASK message
if streamingMsg.CurrentKey == messageKey {
// This is an update to the same ASK message - stream the changes
if cleanText != streamingMsg.LastText {
s.streamAskMessageUpdate(cleanText, streamingMsg.LastText, timestamp)
s.state.SetStreamingMessage(messageKey, cleanText)
}
} else {
// This is a new ASK message
s.finishCurrentStream()
s.streamAskMessage(cleanText, timestamp, true)
s.state.SetStreamingMessage(messageKey, cleanText)
}
return nil
}
// handleStreamingSay handles streaming SAY messages
func (s *StreamingDisplay) handleStreamingSay(msg *types.ClineMessage, messageKey, timestamp string, streamingMsg *types.StreamingMessage) error {
switch msg.Say {
case string(types.SayTypeText), string(types.SayTypeCompletionResult):
return s.handleStreamingText(msg, messageKey, timestamp, streamingMsg)
case string(types.SayTypeCommand):
return s.handleStreamingCommand(msg, messageKey, timestamp, streamingMsg)
case string(types.SayTypeCommandOutput):
return s.handleStreamingCommandOutput(msg, messageKey, timestamp, streamingMsg)
case string(types.SayTypeTool):
return s.handleStreamingTool(msg, messageKey, timestamp, streamingMsg)
case string(types.SayTypeShellIntegrationWarning):
return s.handleShellIntegrationWarning(msg, messageKey, timestamp, streamingMsg)
default:
// For non-streaming message types, use regular display
return s.renderer.RenderMessage(timestamp, s.getMessagePrefix(msg.Say), msg.Text)
}
}
// handleStreamingText handles streaming text messages
func (s *StreamingDisplay) handleStreamingText(msg *types.ClineMessage, messageKey, timestamp string, streamingMsg *types.StreamingMessage) error {
cleanText := s.renderer.sanitizeText(msg.Text)
if cleanText == "" {
return nil
}
// Check if we've already displayed this exact message
if streamingMsg.CurrentKey == messageKey && streamingMsg.LastText == cleanText {
return nil // Duplicate - ignore it
}
// Check if this is an update to the same message
if streamingMsg.CurrentKey == messageKey {
// Show incremental changes
if len(cleanText) > len(streamingMsg.LastText) && strings.HasPrefix(cleanText, streamingMsg.LastText) {
// Show only the new characters with typewriter effect
newChars := cleanText[len(streamingMsg.LastText):]
s.typewriterPrint(newChars)
s.state.SetStreamingMessage(messageKey, cleanText)
} else {
// Text changed in a non-incremental way - replace the line
s.renderer.ClearLine()
prefix := s.getMessagePrefix(msg.Say)
s.renderer.typewriter.PrintfInstant("[%s] %s: ", timestamp, prefix)
s.typewriterPrint(cleanText)
s.state.SetStreamingMessage(messageKey, cleanText)
}
} else {
// This is a new message
s.finishCurrentStream()
prefix := s.getMessagePrefix(msg.Say)
s.renderer.typewriter.PrintfInstant("[%s] %s: ", timestamp, prefix)
// Add typewriter animation for new messages
s.typewriterPrint(cleanText)
s.state.SetStreamingMessage(messageKey, cleanText)
}
// If message is complete, add newline
if !msg.Partial {
fmt.Println()
s.state.SetStreamingMessage("", "")
}
return nil
}
// handleStreamingCommand handles command execution messages
func (s *StreamingDisplay) handleStreamingCommand(msg *types.ClineMessage, messageKey, timestamp string, streamingMsg *types.StreamingMessage) error {
cleanText := s.renderer.sanitizeText(msg.Text)
if cleanText == "" {
return nil
}
// Show command being executed with typewriter effect
s.finishCurrentStream()
s.renderer.typewriter.PrintfInstant("[%s] 🖥️ CMD: ", timestamp)
s.typewriterPrint(cleanText)
fmt.Println()
return nil
}
// handleStreamingCommandOutput handles streaming command output
func (s *StreamingDisplay) handleStreamingCommandOutput(msg *types.ClineMessage, messageKey, timestamp string, streamingMsg *types.StreamingMessage) error {
cleanText := s.renderer.sanitizeText(msg.Text)
if cleanText == "" {
return nil
}
// Check if we've already displayed this exact message
if streamingMsg.CurrentKey == messageKey && streamingMsg.LastText == cleanText {
return nil
}
// Check if this is an update to the same message
if streamingMsg.CurrentKey == messageKey {
// Show incremental changes with typewriter effect
if len(cleanText) > len(streamingMsg.LastText) && strings.HasPrefix(cleanText, streamingMsg.LastText) {
newChars := cleanText[len(streamingMsg.LastText):]
s.typewriterPrint(newChars)
s.state.SetStreamingMessage(messageKey, cleanText)
} else {
// Non-incremental change - replace the line
s.renderer.ClearLine()
s.renderer.typewriter.PrintfInstant("[%s] 🖥️ OUT: ", timestamp)
s.typewriterPrint(cleanText)
s.state.SetStreamingMessage(messageKey, cleanText)
}
} else {
// New command output message
s.finishCurrentStream()
s.renderer.typewriter.PrintfInstant("[%s] 🖥️ OUT: ", timestamp)
s.typewriterPrint(cleanText)
s.state.SetStreamingMessage(messageKey, cleanText)
}
// If message is complete, add newline
if !msg.Partial {
fmt.Println()
s.state.SetStreamingMessage("", "")
}
return nil
}
// handleShellIntegrationWarning handles shell integration warning messages
func (s *StreamingDisplay) handleShellIntegrationWarning(msg *types.ClineMessage, messageKey, timestamp string, streamingMsg *types.StreamingMessage) error {
cleanText := s.renderer.sanitizeText(msg.Text)
if cleanText == "" {
return nil
}
// Show a more concise shell integration warning
s.finishCurrentStream()
s.renderer.typewriter.PrintfInstant("[%s] ️ NOTE: ", timestamp)
s.typewriterPrint("Command executed (output not streamed due to shell integration)")
fmt.Println()
return nil
}
// handleStreamingTool handles streaming tool messages with deduplication
func (s *StreamingDisplay) handleStreamingTool(msg *types.ClineMessage, messageKey, timestamp string, streamingMsg *types.StreamingMessage) error {
cleanText := s.renderer.sanitizeText(msg.Text)
if cleanText == "" {
return nil
}
formattedTool := s.formatToolMessage(cleanText)
// Check if this is the exact same tool message we just displayed
if streamingMsg.LastToolMessage == formattedTool {
return nil // Exact duplicate - ignore it
}
// Check if this is a very similar tool message
if streamingMsg.LastToolMessage != "" && s.isSimilarToolMessage(streamingMsg.LastToolMessage, formattedTool) {
return nil // Similar duplicate - ignore it
}
// This is a genuinely new/different tool message
s.finishCurrentStream()
fmt.Printf("[%s] 🔧 TOOL: %s\n", timestamp, formattedTool)
// Store the formatted tool message for deduplication
s.state.StreamingMessage.LastToolMessage = formattedTool
return nil
}
// streamAskMessage streams an ASK message in a natural format
func (s *StreamingDisplay) streamAskMessage(text, timestamp string, isNew bool) {
// Try to parse as JSON
var askData types.AskData
if err := s.parseJSON(text, &askData); err != nil {
// Display as text but sanitized
fmt.Printf("[%s] 🤖 ASK: %s", timestamp, text)
return
}
fmt.Printf("[%s] 🤖 ASK: %s", timestamp, askData.Response)
// Display options if available
if len(askData.Options) > 0 {
fmt.Print("\n\nOptions:")
for i, option := range askData.Options {
fmt.Printf("\n%d. %s", i+1, option)
}
}
}
// streamAskMessageUpdate handles updates to an existing ASK message
func (s *StreamingDisplay) streamAskMessageUpdate(newText, oldText, timestamp string) {
var oldAskData, newAskData types.AskData
oldErr := s.parseJSON(oldText, &oldAskData)
newErr := s.parseJSON(newText, &newAskData)
if oldErr != nil || newErr != nil {
// Handle plain text incremental updates
if len(newText) > len(oldText) && strings.HasPrefix(newText, oldText) {
newChars := newText[len(oldText):]
fmt.Print(newChars)
} else {
// Non-incremental change - clear line and reprint everything
s.renderer.ClearLine()
fmt.Printf("[%s] 🤖 ASK: %s", timestamp, newText)
}
return
}
// Handle structured updates
if len(newAskData.Response) > len(oldAskData.Response) && strings.HasPrefix(newAskData.Response, oldAskData.Response) {
newChars := newAskData.Response[len(oldAskData.Response):]
fmt.Print(newChars)
} else if oldAskData.Response != newAskData.Response {
s.renderer.ClearLine()
fmt.Printf("[%s] 🤖 ASK: %s", timestamp, newAskData.Response)
}
// Handle options changes
if len(newAskData.Options) > len(oldAskData.Options) {
if len(oldAskData.Options) == 0 {
fmt.Print("\n\nOptions:")
}
for i := len(oldAskData.Options); i < len(newAskData.Options); i++ {
fmt.Printf("\n%d. %s", i+1, newAskData.Options[i])
}
}
}
// typewriterPrint displays text with a typewriter animation effect
func (s *StreamingDisplay) typewriterPrint(text string) {
// Use the renderer's typewriter for consistent animation
s.renderer.typewriter.Print(text)
}
// finishCurrentStream completes any ongoing streaming message
func (s *StreamingDisplay) finishCurrentStream() {
streamingMsg := s.state.GetStreamingMessage()
if streamingMsg.CurrentKey != "" {
//fmt.Println() // Add newline to finish the current streaming message
s.state.SetStreamingMessage("", "")
}
}
// getMessagePrefix returns the appropriate prefix for a message type
func (s *StreamingDisplay) getMessagePrefix(say string) string {
switch say {
case string(types.SayTypeCompletionResult):
return "✅ RESULT"
case string(types.SayTypeText):
return "🤖"
default:
return "🤖"
}
}
// formatToolMessage formats tool call messages for better readability
func (s *StreamingDisplay) formatToolMessage(text string) string {
var toolCall map[string]interface{}
if err := s.parseJSON(text, &toolCall); err == nil {
if tool, ok := toolCall["tool"].(string); ok {
parts := []string{tool}
if path, ok := toolCall["path"].(string); ok && path != "" {
parts = append(parts, fmt.Sprintf("path=%s", path))
}
if content, ok := toolCall["content"].(string); ok && content != "" {
if len(content) > 50 {
parts = append(parts, fmt.Sprintf("content=%s...", content[:50]))
} else {
parts = append(parts, fmt.Sprintf("content=%s", content))
}
}
return strings.Join(parts, " ")
}
}
// If not JSON or doesn't have expected structure, return truncated
if len(text) > 100 {
return text[:100] + "..."
}
return text
}
// isSimilarToolMessage checks if two tool messages are similar enough to be considered duplicates
func (s *StreamingDisplay) isSimilarToolMessage(msg1, msg2 string) bool {
parts1 := strings.Fields(msg1)
parts2 := strings.Fields(msg2)
if len(parts1) == 0 || len(parts2) == 0 {
return false
}
// If the first word (tool name) is the same, check for similarity
if parts1[0] == parts2[0] {
// For file operations, check if the path is the same
if strings.Contains(msg1, "path=") && strings.Contains(msg2, "path=") {
path1 := s.extractPathFromToolMessage(msg1)
path2 := s.extractPathFromToolMessage(msg2)
if path1 != "" && path1 == path2 {
return true
}
}
// For very similar content (>80% similarity), consider them duplicates
similarity := s.calculateStringSimilarity(msg1, msg2)
return similarity > 0.8
}
return false
}
// extractPathFromToolMessage extracts the path parameter from a tool message
func (s *StreamingDisplay) extractPathFromToolMessage(msg string) string {
parts := strings.Fields(msg)
for _, part := range parts {
if strings.HasPrefix(part, "path=") {
return strings.TrimPrefix(part, "path=")
}
func (s *StreamingDisplay) getPrefix(sayType string) string {
switch sayType {
case string(types.SayTypeReasoning):
return "THINKING"
case string(types.SayTypeText):
return "CLINE"
case string(types.SayTypeCompletionResult):
return "RESULT"
case "ask":
return "ASK"
case string(types.SayTypeCommand):
return "TERMINAL"
default:
return strings.ToUpper(sayType)
}
return ""
}
// calculateStringSimilarity calculates a simple similarity ratio between two strings
func (s *StreamingDisplay) calculateStringSimilarity(s1, s2 string) float64 {
if s1 == s2 {
return 1.0
func (s *StreamingDisplay) FreezeActiveSegment() {
s.mu.Lock()
defer s.mu.Unlock()
if s.activeSegment != nil {
s.activeSegment.Freeze()
s.activeSegment = nil
}
if len(s1) == 0 || len(s2) == 0 {
return 0.0
}
shorter, longer := s1, s2
if len(s1) > len(s2) {
shorter, longer = s2, s1
}
matches := 0
for i, r := range shorter {
if i < len(longer) && rune(longer[i]) == r {
matches++
}
}
return float64(matches) / float64(len(longer))
}
// parseJSON is a helper function to parse JSON with error handling
func (s *StreamingDisplay) parseJSON(text string, v interface{}) error {
return json.Unmarshal([]byte(text), v)
}
// Cleanup cleans up streaming display resources
func (s *StreamingDisplay) Cleanup() {
s.FreezeActiveSegment()
if s.dedupe != nil {
s.dedupe.Stop()
}
+269
View File
@@ -0,0 +1,269 @@
package display
import (
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/clerror"
)
// ErrorSeverity represents the severity level of an error
type ErrorSeverity string
const (
SeverityCritical ErrorSeverity = "critical"
SeverityWarning ErrorSeverity = "warning"
SeverityInfo ErrorSeverity = "info"
)
// SystemMessageRenderer handles rendering of system messages (errors, warnings, info)
type SystemMessageRenderer struct {
renderer *Renderer
mdRenderer *MarkdownRenderer
outputFormat string
}
// NewSystemMessageRenderer creates a new system message renderer
func NewSystemMessageRenderer(renderer *Renderer, mdRenderer *MarkdownRenderer, outputFormat string) *SystemMessageRenderer {
return &SystemMessageRenderer{
renderer: renderer,
mdRenderer: mdRenderer,
outputFormat: outputFormat,
}
}
// RenderError renders a beautiful error message with optional details
func (sr *SystemMessageRenderer) RenderError(severity ErrorSeverity, title, body string, details map[string]string) error {
var colorMarkdown string
switch severity {
case SeverityCritical:
colorMarkdown = "**[ERROR]**"
case SeverityWarning:
colorMarkdown = "**[WARNING]**"
case SeverityInfo:
colorMarkdown = "**[INFO]**"
}
// Build the error message in markdown
var parts []string
// Header
header := fmt.Sprintf("### %s %s", colorMarkdown, title)
parts = append(parts, header)
// Body
if body != "" {
parts = append(parts, "", body)
}
// Details
if len(details) > 0 {
parts = append(parts, "", "**Details:**")
for key, value := range details {
parts = append(parts, fmt.Sprintf("- %s: `%s`", key, value))
}
}
markdown := strings.Join(parts, "\n")
rendered := sr.renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
return nil
}
// RenderBalanceError renders a special balance/credits error with helpful info
func (sr *SystemMessageRenderer) RenderBalanceError(err *clerror.ClineError) error {
var parts []string
// Header
parts = append(parts, "### **[ERROR]** Credit Limit Reached")
parts = append(parts, "")
// Message - prefer detail message from error.details, fallback to main message
message := err.Message
if detailMsg := err.GetDetailMessage(); detailMsg != "" {
message = detailMsg
}
parts = append(parts, message)
parts = append(parts, "")
// Account Balance section
parts = append(parts, "**Account Balance:**")
// Current balance
if balance := err.GetCurrentBalance(); balance != nil {
parts = append(parts, fmt.Sprintf("- Current Balance: **$%.2f**", *balance))
}
// Total spent
if spent := err.GetTotalSpent(); spent != nil {
parts = append(parts, fmt.Sprintf("- Total Spent: $%.2f", *spent))
}
// Promotions applied
if promos := err.GetTotalPromotions(); promos != nil {
parts = append(parts, fmt.Sprintf("- Promotions Applied: $%.2f", *promos))
}
parts = append(parts, "")
// Buy credits link
if url := err.GetBuyCreditsURL(); url != "" {
parts = append(parts, fmt.Sprintf("**→ Buy credits:** %s", url))
} else {
// Fallback - show both personal and org URLs
parts = append(parts, "**→ Buy credits:**")
parts = append(parts, " - Personal: https://app.cline.bot/dashboard/account?tab=credits")
parts = append(parts, " - Organization: https://app.cline.bot/dashboard/organization?tab=credits")
}
// Request ID (less prominent at the end)
if err.RequestID != "" {
parts = append(parts, "")
parts = append(parts, fmt.Sprintf("*Request ID: %s*", err.RequestID))
}
markdown := strings.Join(parts, "\n")
rendered := sr.renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
return nil
}
// RenderAuthError renders an authentication error with helpful guidance
func (sr *SystemMessageRenderer) RenderAuthError(err *clerror.ClineError) error {
var parts []string
// Header
parts = append(parts, "### **[ERROR]** Authentication Failed")
parts = append(parts, "")
// Message - prefer detail message if available
message := err.Message
if detailMsg := err.GetDetailMessage(); detailMsg != "" {
message = detailMsg
}
parts = append(parts, message)
parts = append(parts, "")
// Guidance
parts = append(parts, "**Next Steps:**")
parts = append(parts, "- Check your API key configuration")
parts = append(parts, "- Run `cline auth` to authenticate")
parts = append(parts, "- Verify your account status at https://app.cline.bot")
// Request ID
if err.RequestID != "" {
parts = append(parts, "")
parts = append(parts, fmt.Sprintf("*Request ID: `%s`*", err.RequestID))
}
markdown := strings.Join(parts, "\n")
rendered := sr.renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
return nil
}
// RenderRateLimitError renders a rate limit error with request ID
func (sr *SystemMessageRenderer) RenderRateLimitError(err *clerror.ClineError) error {
var parts []string
// Header
parts = append(parts, "### **[WARNING]** Rate Limit Reached")
parts = append(parts, "")
// Message - prefer detail message if available
message := err.Message
if detailMsg := err.GetDetailMessage(); detailMsg != "" {
message = detailMsg
}
parts = append(parts, message)
parts = append(parts, "")
// Guidance
parts = append(parts, "The API will automatically retry this request.")
// Request ID
if err.RequestID != "" {
parts = append(parts, "")
parts = append(parts, fmt.Sprintf("*Request ID: `%s`*", err.RequestID))
}
markdown := strings.Join(parts, "\n")
rendered := sr.renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
return nil
}
// RenderAPIError renders a generic API error with all available details
func (sr *SystemMessageRenderer) RenderAPIError(err *clerror.ClineError) error {
var parts []string
// Header
parts = append(parts, "### **[ERROR]** API Request Failed")
parts = append(parts, "")
// Message - prefer detail message if available
message := err.Message
if detailMsg := err.GetDetailMessage(); detailMsg != "" {
message = detailMsg
}
parts = append(parts, message)
// Details
var details []string
if err.RequestID != "" {
details = append(details, fmt.Sprintf("- Request ID: `%s`", err.RequestID))
}
if code := err.GetCodeString(); code != "" {
details = append(details, fmt.Sprintf("- Error Code: `%s`", code))
}
if err.Status > 0 {
details = append(details, fmt.Sprintf("- HTTP Status: `%d`", err.Status))
}
if err.ModelID != "" {
details = append(details, fmt.Sprintf("- Model: `%s`", err.ModelID))
}
if err.ProviderID != "" {
details = append(details, fmt.Sprintf("- Provider: `%s`", err.ProviderID))
}
if len(details) > 0 {
parts = append(parts, "")
parts = append(parts, "**Details:**")
parts = append(parts, strings.Join(details, "\n"))
}
markdown := strings.Join(parts, "\n")
rendered := sr.renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
return nil
}
// RenderWarning renders a warning message
func (sr *SystemMessageRenderer) RenderWarning(title, message string) error {
markdown := fmt.Sprintf("### **[WARNING]** %s\n\n%s", title, message)
rendered := sr.renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
return nil
}
// RenderInfo renders an info message
func (sr *SystemMessageRenderer) RenderInfo(title, message string) error {
markdown := fmt.Sprintf("### **[INFO]** %s\n\n%s", title, message)
rendered := sr.renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n", rendered)
return nil
}
// RenderCheckpoint renders a checkpoint creation message
func (sr *SystemMessageRenderer) RenderCheckpoint(timestamp string, id int64) error {
markdown := fmt.Sprintf("## [%s] Checkpoint created `%d`", timestamp, id)
rendered := sr.renderer.RenderMarkdown(markdown)
fmt.Printf(rendered)
return nil
}
+455
View File
@@ -0,0 +1,455 @@
package display
import (
"encoding/json"
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/types"
)
// ToolRenderer provides unified rendering for tool and command messages
type ToolRenderer struct {
mdRenderer *MarkdownRenderer
outputFormat string
}
// NewToolRenderer creates a new tool renderer
func NewToolRenderer(mdRenderer *MarkdownRenderer, outputFormat string) *ToolRenderer {
return &ToolRenderer{
mdRenderer: mdRenderer,
outputFormat: outputFormat,
}
}
// RenderToolApprovalRequest renders a tool approval request ("Cline wants to...")
func (tr *ToolRenderer) RenderToolApprovalRequest(tool *types.ToolMessage) string {
var output strings.Builder
// Generate header
header := tr.generateToolHeader(tool, "wants to")
rendered := tr.renderMarkdown(header)
output.WriteString(rendered)
output.WriteString("\n")
// Add content preview for relevant tools
contentPreview := tr.GenerateToolContentPreview(tool)
if contentPreview != "" {
output.WriteString("\n")
output.WriteString(contentPreview)
}
return output.String()
}
// RenderToolExecution renders a completed tool execution ("Cline is ...ing")
func (tr *ToolRenderer) RenderToolExecution(tool *types.ToolMessage) string {
var output strings.Builder
// Generate header
header := tr.generateToolHeader(tool, "is")
rendered := tr.renderMarkdown(header)
output.WriteString("\n")
output.WriteString(rendered)
output.WriteString("\n")
// Add content body for relevant tools
contentBody := tr.GenerateToolContentBody(tool)
if contentBody != "" {
output.WriteString("\n")
output.WriteString(contentBody)
output.WriteString("\n")
}
return output.String()
}
// RenderToolExecutionHeader renders just the header for streaming (no body)
func (tr *ToolRenderer) RenderToolExecutionHeader(tool *types.ToolMessage) string {
header := tr.generateToolHeader(tool, "is")
return header
}
// RenderToolApprovalHeader renders just the header for approval requests (no body)
func (tr *ToolRenderer) RenderToolApprovalHeader(tool *types.ToolMessage) string {
header := tr.generateToolHeader(tool, "wants to")
return header
}
// generateToolHeader generates the markdown header for a tool message
func (tr *ToolRenderer) generateToolHeader(tool *types.ToolMessage, verbTense string) string {
var verb string
var action string
switch tool.Tool {
case string(types.ToolTypeEditedExistingFile):
if verbTense == "wants to" {
action = "wants to edit"
} else {
action = "is editing"
}
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
case string(types.ToolTypeNewFileCreated):
if verbTense == "wants to" {
action = "wants to write"
} else {
action = "is writing"
}
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
case string(types.ToolTypeReadFile):
if verbTense == "wants to" {
action = "wants to read"
} else {
action = "is reading"
}
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
case string(types.ToolTypeFileDeleted):
if verbTense == "wants to" {
action = "wants to delete"
} else {
action = "is deleting"
}
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
case string(types.ToolTypeListFilesTopLevel):
if verbTense == "wants to" {
action = "wants to list files in"
} else {
action = "is listing files in"
}
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
case string(types.ToolTypeListFilesRecursive):
if verbTense == "wants to" {
action = "wants to recursively list files in"
} else {
action = "is recursively listing files in"
}
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
case string(types.ToolTypeSearchFiles):
if tool.Regex != "" && tool.Path != "" {
if verbTense == "wants to" {
action = "wants to search for"
} else {
action = "is searching for"
}
return fmt.Sprintf("### Cline %s `%s` in `%s`", action, tool.Regex, tool.Path)
} else if tool.Regex != "" {
if verbTense == "wants to" {
action = "wants to search for"
} else {
action = "is searching for"
}
return fmt.Sprintf("### Cline %s `%s`", action, tool.Regex)
} else {
if verbTense == "wants to" {
return "### Cline wants to search files"
} else {
return "### Cline is searching files"
}
}
case string(types.ToolTypeWebFetch):
if verbTense == "wants to" {
action = "wants to fetch"
} else {
action = "is fetching"
}
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
case string(types.ToolTypeListCodeDefinitionNames):
if verbTense == "wants to" {
action = "wants to list code definitions in"
} else {
action = "is listing code definitions in"
}
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
case string(types.ToolTypeSummarizeTask):
if verbTense == "wants to" {
return "### Cline wants to condense the conversation"
} else {
return "### Cline condensed the conversation"
}
default:
if verbTense == "wants to" {
verb = "wants to use"
} else {
verb = "is using"
}
return fmt.Sprintf("### Cline %s tool: %s", verb, tool.Tool)
}
}
// GenerateToolContentPreview generates content preview for approval requests
func (tr *ToolRenderer) GenerateToolContentPreview(tool *types.ToolMessage) string {
if tool.Content == "" {
return ""
}
switch tool.Tool {
case string(types.ToolTypeEditedExistingFile):
// Show diff for edits
diffMarkdown := fmt.Sprintf("```diff\n%s\n```", tool.Content)
return tr.renderMarkdown(diffMarkdown)
case string(types.ToolTypeNewFileCreated):
// Show content preview for new files (truncated)
preview := strings.TrimSpace(tool.Content)
if len(preview) > 500 {
preview = preview[:500] + "..."
}
previewMd := fmt.Sprintf("```\n%s\n```", preview)
return tr.renderMarkdown(previewMd)
case string(types.ToolTypeReadFile), string(types.ToolTypeWebFetch), string(types.ToolTypeFileDeleted):
// No preview for read/fetch operations
return ""
default:
// For other tools, show truncated content if available
preview := strings.TrimSpace(tool.Content)
if len(preview) > 200 {
preview = preview[:200] + "..."
}
if preview != "" {
return fmt.Sprintf("Preview: %s", preview)
}
return ""
}
}
// GenerateToolContentBody generates full content for completed executions
func (tr *ToolRenderer) GenerateToolContentBody(tool *types.ToolMessage) string {
if tool.Content == "" {
return ""
}
// Use enhanced tool result parser for supported tools
toolParser := NewToolResultParser(tr.mdRenderer)
switch tool.Tool {
case string(types.ToolTypeReadFile),
string(types.ToolTypeFileDeleted):
// readFile: show header only, no body
return ""
case string(types.ToolTypeListFilesTopLevel),
string(types.ToolTypeListFilesRecursive),
string(types.ToolTypeListCodeDefinitionNames),
string(types.ToolTypeSearchFiles),
string(types.ToolTypeWebFetch):
// Use parser for structured output
preview := toolParser.ParseToolResult(tool)
return tr.renderMarkdown(preview)
case string(types.ToolTypeEditedExistingFile):
// Show the diff
diffMarkdown := fmt.Sprintf("```diff\n%s\n```", tool.Content)
return tr.renderMarkdown(diffMarkdown)
case string(types.ToolTypeNewFileCreated):
// Show file content preview
preview := strings.TrimSpace(tool.Content)
if len(preview) > 1000 {
preview = preview[:1000] + "..."
}
contentMd := fmt.Sprintf("```\n%s\n```", preview)
return tr.renderMarkdown(contentMd)
default:
// For unknown tools, show content as-is
if len(tool.Content) > 500 {
return tool.Content[:500] + "..."
}
return tool.Content
}
}
// RenderCommandApprovalRequest renders a command approval request
func (tr *ToolRenderer) RenderCommandApprovalRequest(command string, autoApprovalConflict bool) string {
var output strings.Builder
// Clean command
command = strings.TrimSpace(command)
if strings.HasSuffix(command, "REQ_APP") {
command = strings.TrimSuffix(command, "REQ_APP")
command = strings.TrimSpace(command)
autoApprovalConflict = true
}
// Generate header
header := fmt.Sprintf("### Cline wants to run `%s`", command)
rendered := tr.renderMarkdown(header)
output.WriteString(rendered)
output.WriteString("\n")
// Show command in code block
cmdBlock := fmt.Sprintf("```shell\n%s\n```", command)
cmdRendered := tr.renderMarkdown(cmdBlock)
output.WriteString("\n")
output.WriteString(cmdRendered)
// Add warning if needed
if autoApprovalConflict {
output.WriteString("\nWARNING: The model has determined this command requires explicit approval.\n")
}
return output.String()
}
// RenderCommandExecution renders a command execution announcement
func (tr *ToolRenderer) RenderCommandExecution(command string) string {
command = strings.TrimSpace(command)
header := fmt.Sprintf("### Cline is running `%s`", command)
rendered := tr.renderMarkdown(header)
return "\n" + rendered + "\n"
}
// RenderCommandOutput renders command output
func (tr *ToolRenderer) RenderCommandOutput(output string) string {
var result strings.Builder
header := "### Terminal output"
rendered := tr.renderMarkdown(header)
result.WriteString("\n")
result.WriteString(rendered)
result.WriteString("\n\n")
// Show output in code block
outputBlock := fmt.Sprintf("```\n%s\n```", strings.TrimSpace(output))
outputRendered := tr.renderMarkdown(outputBlock)
result.WriteString(outputRendered)
result.WriteString("\n")
return result.String()
}
// RenderUserResponse renders user approval/rejection feedback
func (tr *ToolRenderer) RenderUserResponse(approved bool, feedback string) string {
var symbol, status string
if approved {
symbol = "✓"
status = "Approved"
} else {
symbol = "✗"
status = "Rejected"
}
if feedback != "" {
return fmt.Sprintf("%s %s with feedback: %s\n", symbol, status, feedback)
}
return fmt.Sprintf("%s %s\n", symbol, status)
}
// renderMarkdown renders markdown if not in plain mode and in a TTY
func (tr *ToolRenderer) renderMarkdown(markdown string) string {
// Skip markdown rendering if plain mode or not in TTY
if tr.outputFormat == "plain" || !isTTY() {
return markdown
}
if tr.mdRenderer == nil {
return markdown
}
rendered, err := tr.mdRenderer.Render(markdown)
if err != nil {
return markdown
}
return rendered
}
// GenerateAskFollowupHeader generates the header for followup questions
func (tr *ToolRenderer) GenerateAskFollowupHeader() string {
return "### Cline has a question\n"
}
// GenerateAskFollowupBody generates the body content for followup questions
func (tr *ToolRenderer) GenerateAskFollowupBody(messageText string) string {
var question string
var options []string
// Try to parse as JSON
var askData types.AskData
if err := json.Unmarshal([]byte(messageText), &askData); err == nil {
question = askData.Question
options = askData.Options
} else {
question = messageText
}
if question == "" {
return ""
}
// Build the body
var body strings.Builder
// Render the question
rendered := tr.renderMarkdown(question)
body.WriteString(rendered)
// Add options if available
if len(options) > 0 {
body.WriteString("\n\nOptions:\n")
for i, option := range options {
body.WriteString(fmt.Sprintf("%d. %s\n", i+1, option))
}
}
return body.String()
}
// GeneratePlanModeRespondHeader generates the header for plan mode responses
func (tr *ToolRenderer) GeneratePlanModeRespondHeader() string {
return "### Cline has a plan\n"
}
// GeneratePlanModeRespondBody generates the body content for plan mode responses
func (tr *ToolRenderer) GeneratePlanModeRespondBody(messageText string) string {
var response string
var options []string
// Try to parse as JSON
type PlanModeResponse struct {
Response string `json:"response"`
Options []string `json:"options,omitempty"`
}
var planData PlanModeResponse
if err := json.Unmarshal([]byte(messageText), &planData); err == nil {
response = planData.Response
options = planData.Options
} else {
response = messageText
}
if response == "" {
return ""
}
// Build the body
var body strings.Builder
// Render the response
rendered := tr.renderMarkdown(response)
body.WriteString(rendered)
// Add options if available
if len(options) > 0 {
body.WriteString("\n\nOptions:\n")
for i, option := range options {
body.WriteString(fmt.Sprintf("%d. %s\n", i+1, option))
}
}
return body.String()
}
+295
View File
@@ -0,0 +1,295 @@
package display
import (
"fmt"
"path/filepath"
"strings"
"github.com/cline/cli/pkg/cli/types"
)
// ToolResultParser handles parsing and formatting tool results for display
type ToolResultParser struct {
maxPreviewLines int
maxPreviewChars int
mdRenderer *MarkdownRenderer
}
// NewToolResultParser creates a new tool result parser
func NewToolResultParser(mdRenderer *MarkdownRenderer) *ToolResultParser {
return &ToolResultParser{
maxPreviewLines: 15,
maxPreviewChars: 500,
mdRenderer: mdRenderer,
}
}
// ParseReadFile formats readFile tool results with smart preview
func (p *ToolResultParser) ParseReadFile(content, path string) string {
lines := strings.Split(content, "\n")
totalLines := len(lines)
// Get file extension for syntax highlighting
ext := filepath.Ext(path)
lang := p.detectLanguage(ext)
var preview strings.Builder
// Show header with line count
preview.WriteString(fmt.Sprintf("*%d lines*\n\n", totalLines))
// Show preview of content
previewLines := p.maxPreviewLines
if totalLines < previewLines {
previewLines = totalLines
}
preview.WriteString(fmt.Sprintf("```%s\n", lang))
for i := 0; i < previewLines; i++ {
preview.WriteString(lines[i])
preview.WriteString("\n")
}
if totalLines > previewLines {
preview.WriteString("...\n")
}
preview.WriteString("```\n")
if totalLines > previewLines {
preview.WriteString(fmt.Sprintf("\n*[Content truncated - showing %d of %d lines]*", previewLines, totalLines))
}
return preview.String()
}
// ParseListFiles formats listFiles tool results with directory tree
func (p *ToolResultParser) ParseListFiles(content, path string) string {
if content == "" || content == "No files found." {
return "*No files found*"
}
lines := strings.Split(strings.TrimSpace(content), "\n")
// Check for truncation message
var truncationMsg string
lastLine := lines[len(lines)-1]
if strings.Contains(lastLine, "File list truncated") {
truncationMsg = lastLine
lines = lines[:len(lines)-1]
}
totalFiles := len(lines)
var result strings.Builder
result.WriteString(fmt.Sprintf("*%d %s*\n\n", totalFiles, p.pluralize(totalFiles, "file", "files")))
// Show up to 20 files in tree format
maxShow := 20
if totalFiles < maxShow {
maxShow = totalFiles
}
result.WriteString("```\n")
for i := 0; i < maxShow; i++ {
line := lines[i]
// Add tree characters for better visualization
if strings.HasPrefix(line, "🔒 ") {
result.WriteString("├── 🔒 ")
result.WriteString(strings.TrimPrefix(line, "🔒 "))
} else {
result.WriteString("├── ")
result.WriteString(line)
}
result.WriteString("\n")
}
if totalFiles > maxShow {
result.WriteString("└── ...\n")
}
result.WriteString("```\n")
if totalFiles > maxShow {
result.WriteString(fmt.Sprintf("\n*[Showing %d of %d files]*", maxShow, totalFiles))
}
if truncationMsg != "" {
result.WriteString(fmt.Sprintf("\n\n*%s*", truncationMsg))
}
return result.String()
}
// ParseSearchFiles formats searchFiles tool results with context
func (p *ToolResultParser) ParseSearchFiles(content string) string {
if content == "" || content == "Found 0 results." {
return "*No results found*"
}
lines := strings.Split(content, "\n")
if len(lines) == 0 {
return "*No results found*"
}
// Extract result count from first line
firstLine := lines[0]
var result strings.Builder
result.WriteString(fmt.Sprintf("*%s*\n\n", firstLine))
// Parse and group results by file
var currentFile string
var fileResults []string
filesShown := 0
maxFiles := 5
matchesShown := 0
maxMatches := 15
for i := 1; i < len(lines) && filesShown < maxFiles && matchesShown < maxMatches; i++ {
line := lines[i]
if line == "" {
continue
}
// Check if this is a file path (doesn't start with whitespace or line number)
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") && strings.Contains(line, ":") {
// Save previous file results
if currentFile != "" && len(fileResults) > 0 {
result.WriteString(p.formatFileMatches(currentFile, fileResults))
filesShown++
}
currentFile = line
fileResults = []string{}
} else if currentFile != "" {
// This is a match line
fileResults = append(fileResults, strings.TrimSpace(line))
matchesShown++
}
}
// Add last file's results
if currentFile != "" && len(fileResults) > 0 && filesShown < maxFiles {
result.WriteString(p.formatFileMatches(currentFile, fileResults))
filesShown++
}
// Add truncation notice
totalMatches := strings.Count(content, "\n") - 1 // Rough estimate
if matchesShown < totalMatches {
result.WriteString(fmt.Sprintf("\n*[Showing %d results - see full output for all matches]*", matchesShown))
}
return result.String()
}
// formatFileMatches formats matches for a single file
func (p *ToolResultParser) formatFileMatches(file string, matches []string) string {
var result strings.Builder
// Parse file path and extension for syntax highlighting
ext := filepath.Ext(file)
lang := p.detectLanguage(ext)
result.WriteString(fmt.Sprintf("**%s** (%d %s)\n", file, len(matches), p.pluralize(len(matches), "match", "matches")))
result.WriteString(fmt.Sprintf("```%s\n", lang))
maxMatches := 5
for i, match := range matches {
if i >= maxMatches {
result.WriteString("...\n")
break
}
result.WriteString(match)
result.WriteString("\n")
}
result.WriteString("```\n\n")
return result.String()
}
// ParseCodeDefinitions formats listCodeDefinitionNames tool results
func (p *ToolResultParser) ParseCodeDefinitions(content string) string {
if content == "" || content == "No source code definitions found." {
return "*No code definitions found*"
}
// Return the full content as-is
return content
}
// ParseWebFetch formats webFetch tool results with content preview
func (p *ToolResultParser) ParseWebFetch(content, url string) string {
return ""
}
// detectLanguage returns syntax highlighting language based on file extension
func (p *ToolResultParser) detectLanguage(ext string) string {
langMap := map[string]string{
".ts": "typescript",
".tsx": "tsx",
".js": "javascript",
".jsx": "jsx",
".go": "go",
".py": "python",
".rb": "ruby",
".java": "java",
".c": "c",
".cpp": "cpp",
".cs": "csharp",
".php": "php",
".sh": "bash",
".bash": "bash",
".zsh": "bash",
".json": "json",
".yaml": "yaml",
".yml": "yaml",
".xml": "xml",
".html": "html",
".css": "css",
".scss": "scss",
".md": "markdown",
".sql": "sql",
".rs": "rust",
}
if lang, ok := langMap[ext]; ok {
return lang
}
return ""
}
// pluralize returns the correct plural form
func (p *ToolResultParser) pluralize(count int, singular, plural string) string {
if count == 1 {
return singular
}
return plural
}
// formatWordCount formats word count with appropriate unit
func (p *ToolResultParser) formatWordCount(count int) string {
if count < 1000 {
return fmt.Sprintf("%d words", count)
}
return fmt.Sprintf("%.1fk words", float64(count)/1000.0)
}
// ParseToolResult is the main entry point for parsing tool results
func (p *ToolResultParser) ParseToolResult(tool *types.ToolMessage) string {
switch tool.Tool {
case "readFile":
return p.ParseReadFile(tool.Content, tool.Path)
case "listFilesTopLevel", "listFilesRecursive":
return p.ParseListFiles(tool.Content, tool.Path)
case "searchFiles":
return p.ParseSearchFiles(tool.Content)
case "listCodeDefinitionNames":
return p.ParseCodeDefinitions(tool.Content)
case "webFetch":
return p.ParseWebFetch(tool.Content, tool.Path)
default:
return tool.Content
}
}
+4 -8
View File
@@ -160,11 +160,8 @@ func (tp *TypewriterPrinter) SetSpeed(multiplier float64) {
tp.config.PauseDelay = time.Duration(float64(150*time.Millisecond) / multiplier)
}
// PrintMessageLine prints a complete message line with typewriter effect
func (tp *TypewriterPrinter) PrintMessageLine(timestamp, prefix, text string) {
// Print the timestamp and prefix with 10-char padding
tp.PrintfInstant("[%s] %-10s: ", timestamp, prefix)
// Print the message text with typewriter effect
func (tp *TypewriterPrinter) PrintMessageLine(prefix, text string) {
tp.PrintfInstant("%s: ", prefix)
tp.Println(text)
}
@@ -193,9 +190,8 @@ func TypewriterPrintfLn(format string, args ...interface{}) {
globalTypewriter.PrintfLn(format, args...)
}
// TypewriterPrintMessageLine prints a message line with typewriter effect using the global instance
func TypewriterPrintMessageLine(timestamp, prefix, text string) {
globalTypewriter.PrintMessageLine(timestamp, prefix, text)
func TypewriterPrintMessageLine(prefix, text string) {
globalTypewriter.PrintMessageLine(prefix, text)
}
// SetGlobalTypewriterEnabled enables or disables the global typewriter effect
+65
View File
@@ -0,0 +1,65 @@
package cli
import (
"fmt"
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/terminal"
"github.com/cline/cli/pkg/cli/updater"
"github.com/spf13/cobra"
)
// NewDoctorCommand creates the doctor command
func NewDoctorCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "doctor",
Aliases: []string{"d"},
Short: "Check system health and diagnose problems",
Long: `Check the health of your Cline CLI installation and diagnose problems.
Currently this command performs the following checks and fixes:
Terminal Configuration:
- Detects your terminal emulator (VS Code, Cursor, Ghostty, Kitty, WezTerm, Alacritty)
- Configures shift+enter to insert newlines in multiline input
- Creates backups before modifying configuration files
- Supported terminals: VS Code, Cursor, Ghostty, Kitty, WezTerm, Alacritty
- iTerm2 works by default, Terminal.app requires manual setup
CLI Updates:
- Checks npm registry for the latest version
- Automatically installs updates via npm if available
- Respects NO_AUTO_UPDATE environment variable
- Skipped in CI environments
Note: Future versions will include additional health checks for Node.js version,
npm availability, Cline Core connectivity, database integrity, and more.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runDoctorChecks()
},
}
return cmd
}
// runDoctorChecks performs all doctor diagnostics and configuration
func runDoctorChecks() error {
renderer := display.NewRenderer(global.Config.OutputFormat)
fmt.Printf("\n%s\n\n", renderer.Bold("Cline Doctor - System Health Check"))
// Configure terminal keybindings (terminal.go prints its own status)
fmt.Printf("%s\n\n", renderer.Dim("━━━ Terminal Configuration ━━━"))
terminal.SetupKeyboardSync()
// Check for updates (updater.go prints its own status)
fmt.Printf("\n%s\n\n", renderer.Dim("━━━ CLI Updates ━━━"))
updater.CheckAndUpdateSync(global.Config.Verbose, true)
// Summary
fmt.Printf("\n%s\n", renderer.Dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"))
fmt.Printf("\n%s\n\n", renderer.SuccessWithCheckmark("Health check complete"))
return nil
}
+265 -38
View File
@@ -5,9 +5,13 @@ import (
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"syscall"
"time"
"github.com/cline/cli/pkg/common"
"github.com/cline/grpc-go/cline"
)
// ClineClients manages Cline instances using the new registry system
@@ -39,7 +43,9 @@ func (c *ClineClients) StartNewInstance(ctx context.Context) (*common.CoreInstan
return nil, fmt.Errorf("failed to find available ports: %w", err)
}
fmt.Printf("Starting new Cline instance on ports %d (core) and %d (host bridge)\n", corePort, hostPort)
if Config.Verbose {
fmt.Printf("Starting new Cline instance on ports %d (core) and %d (host bridge)\n", corePort, hostPort)
}
// Start cline-host first
hostCmd, err := startClineHost(hostPort, corePort)
@@ -58,7 +64,9 @@ func (c *ClineClients) StartNewInstance(ctx context.Context) (*common.CoreInstan
}
fullAddress := fmt.Sprintf("localhost:%d", corePort)
fmt.Println("Waiting for services to start and self-register in SQLite...")
if Config.Verbose {
fmt.Println("Waiting for services to start and self-register in SQLite...")
}
// Use RetryOperation to wait for instance to be ready
var instance *common.CoreInstanceInfo
@@ -92,11 +100,22 @@ func (c *ClineClients) StartNewInstance(ctx context.Context) (*common.CoreInstan
return nil, fmt.Errorf("failed to start instance: %w", err)
}
fmt.Println("✅ Services started and registered successfully!")
fmt.Printf(" Address: %s\n", instance.Address)
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)
if Config.Verbose {
fmt.Println("Services started and registered successfully!")
fmt.Printf(" Address: %s\n", instance.Address)
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)
}
// If this is the first instance, set it as default
instances := c.registry.ListInstances()
if err := c.registry.EnsureDefaultInstance(instances); err != nil {
if Config.Verbose {
fmt.Printf("Warning: Failed to set default instance: %v\n", err)
}
}
return instance, nil
}
@@ -111,7 +130,9 @@ func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int)
return nil, fmt.Errorf("port %d is already in use by another Cline instance", corePort)
}
fmt.Printf("Starting new Cline instance on ports %d (core) and %d (host bridge)\n", corePort, hostPort)
if Config.Verbose {
fmt.Printf("Starting new Cline instance on ports %d (core) and %d (host bridge)\n", corePort, hostPort)
}
// Start cline-host first
hostCmd, err := startClineHost(hostPort, corePort)
@@ -130,7 +151,9 @@ func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int)
}
fullAddress := fmt.Sprintf("localhost:%d", corePort)
fmt.Println("Waiting for services to start and self-register in SQLite...")
if Config.Verbose {
fmt.Println("Waiting for services to start and self-register in SQLite...")
}
// Use RetryOperation to wait for instance to be ready
var instance *common.CoreInstanceInfo
@@ -164,11 +187,22 @@ func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int)
return nil, fmt.Errorf("failed to start instance at port %d: %w", corePort, err)
}
fmt.Println("✅ Services started and registered successfully!")
fmt.Printf(" Address: %s\n", instance.Address)
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)
if Config.Verbose {
fmt.Println("Services started and registered successfully!")
fmt.Printf(" Address: %s\n", instance.Address)
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)
}
// If this is the first instance, set it as default
instances := c.registry.ListInstances()
if err := c.registry.EnsureDefaultInstance(instances); err != nil {
if Config.Verbose {
fmt.Printf("Warning: Failed to set default instance: %v\n", err)
}
}
return instance, nil
}
@@ -209,66 +243,259 @@ func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address stri
}
func startClineHost(hostPort, corePort int) (*exec.Cmd, error) {
fmt.Printf("Starting cline-host on port %d\n", hostPort)
if Config.Verbose {
fmt.Printf("Starting cline-host on port %d\n", hostPort)
}
// Get the directory where the cline binary is located
execPath, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("failed to get executable path: %w", err)
}
binDir := path.Dir(execPath)
clineHostPath := path.Join(binDir, "cline-host")
// Start the cline-host process
cmd := exec.Command("./cli/bin/cline-host",
cmd := exec.Command(clineHostPath,
"--verbose",
"--port", fmt.Sprintf("%d", hostPort))
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to start cline-host: %w", err)
// Create logs directory in ~/.cline/logs
logsDir := path.Join(Config.ConfigPath, "logs")
if err := os.MkdirAll(logsDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create logs directory: %w", err)
}
fmt.Printf("Started cline-host (PID: %d)\n", cmd.Process.Pid)
return cmd, nil
}
func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
fmt.Printf("Starting cline-core on port %d (with hostbridge on %d)\n", corePort, hostPort)
// Create port-tagged log file in OS temp directory with full address
logFileName := fmt.Sprintf("cline-core-debug-localhost-%d.log", corePort)
logFilePath := fmt.Sprintf("%s/%s", os.TempDir(), logFileName)
// Create timestamped log file
timestamp := time.Now().Format("2006-01-02-15-04-05")
logFileName := fmt.Sprintf("cline-host-%s-localhost-%d.log", timestamp, hostPort)
logFilePath := path.Join(logsDir, logFileName)
logFile, err := os.Create(logFilePath)
if err != nil {
return nil, fmt.Errorf("failed to create log file: %w", err)
}
// Start the cline-core process with --config flag instead of CLINE_DIR env var
args := []string{"cline-core.js",
// Redirect stdout and stderr to log file
cmd.Stdout = logFile
cmd.Stderr = logFile
// Put the child process in a new process group so Ctrl+C doesn't kill it
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
if err := cmd.Start(); err != nil {
logFile.Close()
return nil, fmt.Errorf("failed to start cline-host: %w", err)
}
if Config.Verbose {
fmt.Printf("Started cline-host (PID: %d)\n", cmd.Process.Pid)
fmt.Printf("Logging cline-host output to: %s\n", logFilePath)
}
return cmd, nil
}
// KillInstanceByAddress kills a Cline instance by its address
func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, address string) error {
// Check if the instance exists in the registry
_, err := registry.GetInstance(address)
if err != nil {
return fmt.Errorf("instance %s not found in registry", address)
}
if Config.Verbose {
fmt.Printf("Killing instance: %s\n", address)
}
// Get gRPC client and process info
client, err := registry.GetClient(ctx, address)
if err != nil {
return fmt.Errorf("failed to connect to instance %s: %w", address, err)
}
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to get process info for instance %s: %w", address, err)
}
pid := int(processInfo.ProcessId)
if Config.Verbose {
fmt.Printf("Terminating process PID %d...\n", pid)
}
// Kill the process
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
return fmt.Errorf("failed to kill process %d: %w", pid, err)
}
// Wait for the instance to remove itself from registry
if Config.Verbose {
fmt.Printf("Waiting for instance to clean up registry entry...\n")
}
for i := 0; i < 5; i++ {
time.Sleep(1 * time.Second)
if !registry.HasInstanceAtAddress(address) {
if Config.Verbose {
fmt.Printf("Instance %s successfully killed and removed from registry.\n", address)
}
// Update default instance if needed
instances, err := registry.ListInstancesCleaned(ctx)
if err == nil && len(instances) > 0 {
// ensureDefaultInstance logic will handle setting a new default
defaultInstance := registry.GetDefaultInstance()
if defaultInstance == address || defaultInstance == "" {
if len(instances) > 0 {
if err := registry.SetDefaultInstance(instances[0].Address); err == nil {
if Config.Verbose {
fmt.Printf("Updated default instance to: %s\n", instances[0].Address)
}
}
}
}
}
return nil
}
}
return fmt.Errorf("instance killed but failed to remove itself from registry within 5 seconds")
}
func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
if Config.Verbose {
fmt.Printf("Starting cline-core on port %d (with hostbridge on %d)\n", corePort, hostPort)
}
// Get the executable path and resolve symlinks (for npm global installs)
execPath, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("failed to get executable path: %w", err)
}
// Resolve symlinks to get the real path
// For npm global installs, execPath might be a symlink like:
// /opt/homebrew/bin/cline -> /opt/homebrew/lib/node_modules/cline/bin/cline
realPath, err := filepath.EvalSymlinks(execPath)
if err != nil {
// If we can't resolve symlinks, fall back to the original path
realPath = execPath
if Config.Verbose {
fmt.Printf("Warning: Could not resolve symlinks for %s: %v\n", execPath, err)
}
}
binDir := path.Dir(realPath)
installDir := path.Dir(binDir)
clineCorePath := path.Join(installDir, "cline-core.js")
if Config.Verbose {
fmt.Printf("Executable path: %s\n", execPath)
if realPath != execPath {
fmt.Printf("Real path (after resolving symlinks): %s\n", realPath)
}
fmt.Printf("Bin directory: %s\n", binDir)
fmt.Printf("Install directory: %s\n", installDir)
fmt.Printf("Looking for cline-core.js at: %s\n", clineCorePath)
}
// Check if cline-core.js exists at the primary location
var finalClineCorePath string
var finalInstallDir string
if _, err := os.Stat(clineCorePath); os.IsNotExist(err) {
// Development mode: Try ../../dist-standalone/cline-core.js
// This handles the case where we're running from cli/bin/cline
devClineCorePath := path.Join(binDir, "..", "..", "dist-standalone", "cline-core.js")
devInstallDir := path.Join(binDir, "..", "..", "dist-standalone")
if Config.Verbose {
fmt.Printf("Primary location not found, trying development path: %s\n", devClineCorePath)
}
if _, err := os.Stat(devClineCorePath); os.IsNotExist(err) {
return nil, fmt.Errorf("cline-core.js not found at '%s' or '%s'. Please ensure you're running from the correct location or reinstall with 'npm install -g cline'", clineCorePath, devClineCorePath)
}
finalClineCorePath = devClineCorePath
finalInstallDir = devInstallDir
if Config.Verbose {
fmt.Printf("Using development mode: cline-core.js found at %s\n", finalClineCorePath)
}
} else {
finalClineCorePath = clineCorePath
finalInstallDir = installDir
if Config.Verbose {
fmt.Printf("Using production mode: cline-core.js found at %s\n", finalClineCorePath)
}
}
// Create logs directory in ~/.cline/logs
logsDir := path.Join(Config.ConfigPath, "logs")
if err := os.MkdirAll(logsDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create logs directory: %w", err)
}
// Create timestamped log file
timestamp := time.Now().Format("2006-01-02-15-04-05")
logFileName := fmt.Sprintf("cline-core-%s-localhost-%d.log", timestamp, corePort)
logFilePath := path.Join(logsDir, logFileName)
logFile, err := os.Create(logFilePath)
if err != nil {
return nil, fmt.Errorf("failed to create log file: %w", err)
}
// Start the cline-core process with --config flag using system node
args := []string{finalClineCorePath,
"--port", fmt.Sprintf("%d", corePort),
"--host-bridge-port", fmt.Sprintf("%d", hostPort),
"--config", Config.ConfigPath}
fmt.Printf("DEBUG: Starting cline-core with command: node %v\n", args)
fmt.Printf("DEBUG: Working directory: ./dist-standalone\n")
fmt.Printf("DEBUG: Config path: %s\n", Config.ConfigPath)
if Config.Verbose {
fmt.Printf("Using system node\n")
}
cmd := exec.Command("node", args...)
// Set working directory to dist-standalone (relative to project root)
cmd.Dir = "./dist-standalone"
// Set working directory to installation root
cmd.Dir = finalInstallDir
// Redirect stdout and stderr to log file
cmd.Stdout = logFile
cmd.Stderr = logFile
// Set environment variables (removed CLINE_DIR)
// Put the child process in a new process group so Ctrl+C doesn't kill it
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
// Set environment variables with NODE_PATH for both real and fake node_modules
// The fake node_modules contains the vscode stub that can't be in the real node_modules
env := os.Environ()
realNodeModules := path.Join(finalInstallDir, "node_modules")
fakeNodeModules := path.Join(finalInstallDir, "fake_node_modules")
nodePath := fmt.Sprintf("%s%c%s", realNodeModules, os.PathListSeparator, fakeNodeModules)
env = append(env,
fmt.Sprintf("NODE_PATH=%s", nodePath),
"GRPC_TRACE=all",
"GRPC_VERBOSITY=DEBUG",
"NODE_ENV=development",
)
cmd.Env = env
if Config.Verbose {
fmt.Printf("NODE_PATH set to: %s\n", nodePath)
}
if err := cmd.Start(); err != nil {
logFile.Close()
return nil, fmt.Errorf("failed to start cline-core: %w", err)
}
fmt.Printf("Started cline-core (PID: %d)\n", cmd.Process.Pid)
fmt.Printf("Logging cline-core output to: %s\n", logFilePath)
if Config.Verbose {
fmt.Printf("Started cline-core (PID: %d)\n", cmd.Process.Pid)
fmt.Printf("Logging cline-core output to: %s\n", logFilePath)
}
return cmd, nil
}
+46
View File
@@ -6,8 +6,10 @@ import (
"os"
"path/filepath"
"github.com/charmbracelet/lipgloss"
"github.com/cline/cli/pkg/common"
"github.com/cline/grpc-go/client"
"github.com/muesli/termenv"
)
type Port uint16
@@ -22,6 +24,15 @@ type GlobalConfig struct {
var (
Config *GlobalConfig
Clients *ClineClients
// Version info - set at build time via ldflags
// Version is the Cline Core version (from root package.json)
Version = "dev"
// CliVersion is the CLI package version (from cli/package.json)
CliVersion = "dev"
Commit = "unknown"
Date = "unknown"
BuiltBy = "unknown"
)
func InitializeGlobalConfig(cfg *GlobalConfig) error {
@@ -38,6 +49,12 @@ func InitializeGlobalConfig(cfg *GlobalConfig) error {
return fmt.Errorf("failed to create config directory: %w", err)
}
// Configure lipgloss color profile based on output format
if cfg.OutputFormat == "plain" {
lipgloss.SetColorProfile(termenv.Ascii) // NO COLOR mode
}
// Otherwise lipgloss auto-detects terminal capabilities (default behavior)
Config = cfg
Clients = NewClineClients(cfg.ConfigPath)
@@ -65,3 +82,32 @@ func GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
func GetClientForAddress(ctx context.Context, address string) (*client.ClineClient, error) {
return Clients.GetRegistry().GetClient(ctx, address)
}
// EnsureDefaultInstance ensures a default instance exists
func EnsureDefaultInstance(ctx context.Context) error {
if Clients == nil {
return fmt.Errorf("global clients not initialized")
}
registry := Clients.GetRegistry()
// First, check if there are any instances already registered in SQLite
instances := registry.ListInstances()
// Use the registry's EnsureDefaultInstance to auto-set first instance as default if needed
if err := registry.EnsureDefaultInstance(instances); err != nil {
return fmt.Errorf("failed to ensure default from existing instances: %w", err)
}
// Now check if we have a default set
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)
if err != nil {
return fmt.Errorf("failed to start new default instance: %w", err)
}
}
return nil
}
+40 -3
View File
@@ -110,6 +110,43 @@ func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineCli
return nil, fmt.Errorf("no default instance configured")
}
// Check if the default instance actually exists in the database
if r.lockManager != nil {
exists, err := r.lockManager.HasInstanceAtAddress(defaultAddr)
if err != nil {
// 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
settingsPath := filepath.Join(r.configPath, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
if removeErr := os.Remove(settingsPath); removeErr != nil && !os.IsNotExist(removeErr) {
fmt.Printf("Warning: Failed to remove stale default instance config: %v\n", removeErr)
} 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 != "" {
fmt.Printf("Set new default instance: %s\n", newDefaultAddr)
return r.GetClient(ctx, newDefaultAddr)
}
}
return nil, fmt.Errorf("no default instance configured")
}
}
return r.GetClient(ctx, defaultAddr)
}
@@ -223,15 +260,15 @@ func (r *ClientRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.Co
instances := r.ListInstances()
// 3. Ensure default is set if instances exist
if err := r.ensureDefaultInstance(instances); err != nil {
if err := r.EnsureDefaultInstance(instances); err != nil {
fmt.Printf("Warning: Failed to ensure default instance: %v\n", err)
}
return instances, nil
}
// ensureDefaultInstance ensures a default instance is set if instances exist but no default is configured
func (r *ClientRegistry) ensureDefaultInstance(instances []*common.CoreInstanceInfo) error {
// EnsureDefaultInstance ensures a default instance is set if instances exist but no default is configured
func (r *ClientRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceInfo) error {
currentDefault := r.GetDefaultInstance()
// If we have no instances, clear any stale default and remove settings file
+158 -173
View File
@@ -5,6 +5,8 @@ import (
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/clerror"
"github.com/cline/cli/pkg/cli/output"
"github.com/cline/cli/pkg/cli/types"
)
@@ -25,262 +27,242 @@ func (h *AskHandler) CanHandle(msg *types.ClineMessage) bool {
return msg.IsAsk()
}
// Handle processes ASK messages
func (h *AskHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
timestamp := msg.GetTimestamp()
// Always display approval messages so user can see what they're approving
// The input handler will show the approval prompt form after the content is displayed
switch msg.Ask {
case string(types.AskTypeFollowup):
return h.handleFollowup(msg, dc, timestamp)
return h.handleFollowup(msg, dc)
case string(types.AskTypePlanModeRespond):
return h.handlePlanModeRespond(msg, dc, timestamp)
return h.handlePlanModeRespond(msg, dc)
case string(types.AskTypeCommand):
return h.handleCommand(msg, dc, timestamp)
return h.handleCommand(msg, dc)
case string(types.AskTypeCommandOutput):
return h.handleCommandOutput(msg, dc, timestamp)
return h.handleCommandOutput(msg, dc)
case string(types.AskTypeCompletionResult):
return h.handleCompletionResult(msg, dc, timestamp)
return h.handleCompletionResult(msg, dc)
case string(types.AskTypeTool):
return h.handleTool(msg, dc, timestamp)
return h.handleTool(msg, dc)
case string(types.AskTypeAPIReqFailed):
return h.handleAPIReqFailed(msg, dc, timestamp)
return h.handleAPIReqFailed(msg, dc)
case string(types.AskTypeResumeTask):
return h.handleResumeTask(msg, dc, timestamp)
return h.handleResumeTask(msg, dc)
case string(types.AskTypeResumeCompletedTask):
return h.handleResumeCompletedTask(msg, dc, timestamp)
return h.handleResumeCompletedTask(msg, dc)
case string(types.AskTypeMistakeLimitReached):
return h.handleMistakeLimitReached(msg, dc, timestamp)
case string(types.AskTypeAutoApprovalMaxReached):
return h.handleAutoApprovalMaxReached(msg, dc, timestamp)
return h.handleMistakeLimitReached(msg, dc)
case string(types.AskTypeBrowserActionLaunch):
return h.handleBrowserActionLaunch(msg, dc, timestamp)
return h.handleBrowserActionLaunch(msg, dc)
case string(types.AskTypeUseMcpServer):
return h.handleUseMcpServer(msg, dc, timestamp)
return h.handleUseMcpServer(msg, dc)
case string(types.AskTypeNewTask):
return h.handleNewTask(msg, dc, timestamp)
return h.handleNewTask(msg, dc)
case string(types.AskTypeCondense):
return h.handleCondense(msg, dc, timestamp)
return h.handleCondense(msg, dc)
case string(types.AskTypeReportBug):
return h.handleReportBug(msg, dc, timestamp)
return h.handleReportBug(msg, dc)
default:
return h.handleDefault(msg, dc, timestamp)
return h.handleDefault(msg, dc)
}
}
// handleFollowup handles followup questions
func (h *AskHandler) handleFollowup(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
var question string
var options []string
func (h *AskHandler) handleFollowup(msg *types.ClineMessage, dc *DisplayContext) error {
body := dc.ToolRenderer.GenerateAskFollowupBody(msg.Text)
var askData types.AskData
if err := json.Unmarshal([]byte(msg.Text), &askData); err == nil {
question = askData.Question
options = askData.Options
} else {
question = msg.Text
}
if question == "" {
if body == "" {
return nil
}
err := dc.Renderer.RenderMessage(timestamp, "QUESTION", question)
if err != nil {
return err
}
// Display options if available
if len(options) > 0 {
fmt.Println("\nOptions:")
for i, option := range options {
fmt.Printf("%d. %s\n", i+1, option)
}
if dc.IsStreamingMode {
// In streaming mode, header was already shown by partial stream
// Just render the body content
output.Print(body)
} else {
// Non-streaming mode: render header + body together
header := dc.ToolRenderer.GenerateAskFollowupHeader()
rendered := dc.Renderer.RenderMarkdown(header)
output.Print("\n")
output.Print(rendered)
output.Print("\n")
output.Print(body)
}
return nil
}
// handlePlanModeRespond handles plan mode responses
func (h *AskHandler) handlePlanModeRespond(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
var response string
var options []string
// Try to parse as JSON
type PlanModeResponse struct {
Response string `json:"response"`
Options []string `json:"options,omitempty"`
}
var planData PlanModeResponse
if err := json.Unmarshal([]byte(msg.Text), &planData); err == nil {
response = planData.Response
options = planData.Options
} else {
response = msg.Text
}
if response == "" {
return nil
}
err := dc.Renderer.RenderMessage(timestamp, "ASST PLAN", response)
if err != nil {
return err
}
// Display options if available
if len(options) > 0 {
fmt.Println("\nOptions:")
for i, option := range options {
fmt.Printf("%d. %s\n", i+1, option)
func (h *AskHandler) handlePlanModeRespond(msg *types.ClineMessage, dc *DisplayContext) error {
if dc.IsStreamingMode {
// In streaming mode, header was already shown by partial stream
// Just render the body content
body := dc.ToolRenderer.GeneratePlanModeRespondBody(msg.Text)
if body != "" {
output.Print(body)
}
} else {
// In non-streaming mode, render header + body together
header := dc.ToolRenderer.GeneratePlanModeRespondHeader()
body := dc.ToolRenderer.GeneratePlanModeRespondBody(msg.Text)
if body == "" {
return nil
}
// Render header
rendered := dc.Renderer.RenderMarkdown(header)
output.Print("\n")
output.Print(rendered)
output.Print("\n")
// Render body
output.Print(body)
}
return nil
}
// showApprovalHint displays a hint in non-interactive mode about how to approve/deny
func (h *AskHandler) showApprovalHint(dc *DisplayContext) {
if !dc.IsInteractive {
output.Printf("\n%s\n", dc.Renderer.Dim("Cline is requesting approval to use this tool"))
output.Printf("%s\n", dc.Renderer.Dim("Use cline task send --approve or --deny to respond"))
}
}
// handleCommand handles command execution requests
func (h *AskHandler) handleCommand(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *AskHandler) handleCommand(msg *types.ClineMessage, dc *DisplayContext) error {
if msg.Text == "" {
return nil
}
command := msg.Text
// Check if this command was flagged despite auto-approval settings
autoApprovalConflict := strings.HasSuffix(msg.Text, "REQ_APP")
// Check if this command was flagged despite auto-approval settings turned on for safe commands
hasAutoApprovalConflict := strings.HasSuffix(command, "REQ_APP")
if hasAutoApprovalConflict {
command = strings.TrimSuffix(command, "REQ_APP")
}
err := dc.Renderer.RenderMessage(timestamp, "TERMINAL", "Cline wants to execute this command:")
if err != nil {
return fmt.Errorf("failed to render handleCommand: %w", err)
}
fmt.Printf("\n```shell\n%s\n```\n", strings.TrimSpace(command))
if hasAutoApprovalConflict {
fmt.Printf("\nThe model has determined this command requires explicit approval.\n")
} else {
fmt.Printf("\nApproval required for this command.\n")
}
// Use unified ToolRenderer
rendered := dc.ToolRenderer.RenderCommandApprovalRequest(msg.Text, autoApprovalConflict)
output.Print(rendered)
h.showApprovalHint(dc)
return nil
}
// handleCommandOutput handles command output requests
func (h *AskHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *AskHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayContext) error {
if msg.Text == "" {
return nil
}
commandOutput := msg.Text
err := dc.Renderer.RenderMessage(timestamp, "TERMINAL", fmt.Sprintf("Current terminal output: %s", commandOutput))
if err != nil {
return fmt.Errorf("failed to render handleCommandOutput: %w", err)
}
markdown := fmt.Sprintf("```\n%s\n```", commandOutput)
rendered := dc.Renderer.RenderMarkdown(markdown)
fmt.Printf("\nApprove to proceed while this command runs in the background.\n")
fmt.Printf("%s", rendered)
return nil
}
// handleCompletionResult handles completion result requests
func (h *AskHandler) handleCompletionResult(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *AskHandler) handleCompletionResult(msg *types.ClineMessage, dc *DisplayContext) error {
return nil
}
// handleTool handles tool execution requests
func (h *AskHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *AskHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext) error {
// Parse tool message
var tool types.ToolMessage
if err := json.Unmarshal([]byte(msg.Text), &tool); err != nil {
// Fallback to simple display
return dc.Renderer.RenderMessage(timestamp, "TOOL", msg.Text)
return dc.Renderer.RenderMessage("TOOL", msg.Text, true)
}
return h.renderToolMessage(&tool, dc, timestamp)
}
// renderToolMessage renders a tool message with appropriate formatting
func (h *AskHandler) renderToolMessage(tool *types.ToolMessage, dc *DisplayContext, timestamp string) error {
switch tool.Tool {
case string(types.ToolTypeEditedExistingFile):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to edit file: %s", tool.Path))
case string(types.ToolTypeNewFileCreated):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to create file: %s", tool.Path))
case string(types.ToolTypeReadFile):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to read file: %s", tool.Path))
case string(types.ToolTypeListFilesTopLevel):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to list files in: %s", tool.Path))
case string(types.ToolTypeListFilesRecursive):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to recursively list files in: %s", tool.Path))
case string(types.ToolTypeSearchFiles):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to search for '%s' in: %s", tool.Regex, tool.Path))
case string(types.ToolTypeWebFetch):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to fetch URL: %s", tool.Path))
case string(types.ToolTypeListCodeDefinitionNames):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to list code definitions for: %s", tool.Path))
default:
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline wants to use tool: %s", tool.Tool))
}
// Skip content preview for readFile and webFetch tools
if tool.Tool == string(types.ToolTypeReadFile) || tool.Tool == string(types.ToolTypeWebFetch) {
return nil
}
// Show content preview, truncating if necessary
preview := tool.Content
if preview != "" {
preview = strings.TrimSpace(tool.Content)
if len(preview) > 1000 {
preview = preview[:1000] + "..."
if dc.IsStreamingMode {
// In streaming mode, header was already shown by partial stream
// Just render the content preview
contentPreview := dc.ToolRenderer.GenerateToolContentPreview(&tool)
if contentPreview != "" {
output.Print("\n")
output.Print(contentPreview)
}
fmt.Printf("Preview: %s\n", preview)
} else {
// Non-streaming mode: render full approval (header + preview)
rendered := dc.ToolRenderer.RenderToolApprovalRequest(&tool)
output.Print(rendered)
}
fmt.Printf("\nApproval required.\n")
h.showApprovalHint(dc)
return nil
}
// handleAPIReqFailed handles API request failures
func (h *AskHandler) handleAPIReqFailed(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "ERROR", fmt.Sprintf("API Request Failed: %s. Approve to retry request.", msg.Text))
func (h *AskHandler) handleAPIReqFailed(msg *types.ClineMessage, dc *DisplayContext) error {
// Try to parse as ClineError for better error display
clineErr, _ := clerror.ParseClineError(msg.Text)
if clineErr != nil {
if dc.SystemRenderer != nil {
// Render the error with system renderer
switch clineErr.GetErrorType() {
case clerror.ErrorTypeBalance:
dc.SystemRenderer.RenderBalanceError(clineErr)
case clerror.ErrorTypeAuth:
dc.SystemRenderer.RenderAuthError(clineErr)
case clerror.ErrorTypeRateLimit:
dc.SystemRenderer.RenderRateLimitError(clineErr)
default:
dc.SystemRenderer.RenderAPIError(clineErr)
}
return nil
}
// Fallback: render with basic renderer using parsed message
return dc.Renderer.RenderMessage("ERROR", fmt.Sprintf("API Request Failed: %s. Approve to retry request.", clineErr.Message), true)
}
// Last resort: display raw text if parsing completely failed
return dc.Renderer.RenderMessage("ERROR", fmt.Sprintf("API Request Failed: %s. Approve to retry request.", msg.Text), true)
}
// handleResumeTask handles resume task requests
func (h *AskHandler) handleResumeTask(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "GEN INFO", "Resuming interrupted task.")
func (h *AskHandler) handleResumeTask(msg *types.ClineMessage, dc *DisplayContext) error {
// Don't render - this is metadata only, user already knows they're resuming
return nil
}
// handleResumeCompletedTask handles resume completed task requests
func (h *AskHandler) handleResumeCompletedTask(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "GEN INFO", "Resuming completed task.")
func (h *AskHandler) handleResumeCompletedTask(msg *types.ClineMessage, dc *DisplayContext) error {
// Don't render - this is metadata only, user already knows they're resuming
return nil
}
// handleMistakeLimitReached handles mistake limit reached
func (h *AskHandler) handleMistakeLimitReached(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "ERROR", fmt.Sprintf("Mistake Limit Reached: %s. Approval required.", msg.Text))
}
// handleAutoApprovalMaxReached handles auto-approval max reached
func (h *AskHandler) handleAutoApprovalMaxReached(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "WARNING", fmt.Sprintf("Auto-approval limit reached: %s. Approval required.", msg.Text))
func (h *AskHandler) handleMistakeLimitReached(msg *types.ClineMessage, dc *DisplayContext) error {
if dc.SystemRenderer != nil {
details := make(map[string]string)
if msg.Text != "" {
details["details"] = msg.Text
}
dc.SystemRenderer.RenderError(
"critical",
"Mistake Limit Reached",
"Cline has made too many consecutive mistakes and needs your guidance to proceed.",
details,
)
fmt.Printf("\n**Approval required to continue.**\n")
return nil
}
return dc.Renderer.RenderMessage("ERROR", fmt.Sprintf("Mistake Limit Reached: %s. Approval required.", msg.Text), true)
}
// handleBrowserActionLaunch handles browser action launch requests
func (h *AskHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *AskHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext) error {
url := strings.TrimSpace(msg.Text)
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Cline wants to launch browser and navigate to: %s. Approval required.", url))
err := dc.Renderer.RenderMessage("BROWSER", fmt.Sprintf("Cline wants to launch browser and navigate to: %s. Approval required.", url), true)
h.showApprovalHint(dc)
return err
}
// handleUseMcpServer handles MCP server usage requests
func (h *AskHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *AskHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayContext) error {
// Parse MCP server usage request
type McpServerRequest struct {
ServerName string `json:"serverName"`
@@ -292,7 +274,7 @@ func (h *AskHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayCont
var mcpReq McpServerRequest
if err := json.Unmarshal([]byte(msg.Text), &mcpReq); err != nil {
return dc.Renderer.RenderMessage(timestamp, "MCP", msg.Text)
return dc.Renderer.RenderMessage("MCP", msg.Text, true)
}
var operation string
@@ -305,22 +287,25 @@ func (h *AskHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayCont
}
}
return dc.Renderer.RenderMessage(timestamp, "MCP",
fmt.Sprintf("Cline wants to %s on the %s MCP server", operation, mcpReq.ServerName))
err := dc.Renderer.RenderMessage("MCP",
fmt.Sprintf("Cline wants to %s on the %s MCP server", operation, mcpReq.ServerName), true)
h.showApprovalHint(dc)
return err
}
// handleNewTask handles new task creation requests
func (h *AskHandler) handleNewTask(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "NEW TASK", fmt.Sprintf("Cline wants to start a new task: %s. Approval required.", msg.Text))
func (h *AskHandler) handleNewTask(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("NEW TASK", fmt.Sprintf("Cline wants to start a new task: %s. Approval required.", msg.Text), true)
}
// handleCondense handles conversation condensing requests
func (h *AskHandler) handleCondense(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "CONDENSE", fmt.Sprintf("Cline wants to condense the conversation: %s. Approval required.", msg.Text))
func (h *AskHandler) handleCondense(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("CONDENSE", fmt.Sprintf("Cline wants to condense the conversation: %s. Approval required.", msg.Text), true)
}
// handleReportBug handles bug report requests
func (h *AskHandler) handleReportBug(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *AskHandler) handleReportBug(msg *types.ClineMessage, dc *DisplayContext) error {
var bugData struct {
Title string `json:"title"`
WhatHappened string `json:"what_happened"`
@@ -330,10 +315,10 @@ func (h *AskHandler) handleReportBug(msg *types.ClineMessage, dc *DisplayContext
}
if err := json.Unmarshal([]byte(msg.Text), &bugData); err != nil {
return dc.Renderer.RenderMessage(timestamp, "BUG REPORT", fmt.Sprintf("Cline wants to create a GitHub issue: %s. Approval required.", msg.Text))
return dc.Renderer.RenderMessage("BUG REPORT", fmt.Sprintf("Cline wants to create a GitHub issue: %s. Approval required.", msg.Text), true)
}
err := dc.Renderer.RenderMessage(timestamp, "BUG REPORT", "Cline wants to create a GitHub issue:")
err := dc.Renderer.RenderMessage("BUG REPORT", "Cline wants to create a GitHub issue:", true)
if err != nil {
return fmt.Errorf("failed to render handleReportBug: %w", err)
}
@@ -349,6 +334,6 @@ func (h *AskHandler) handleReportBug(msg *types.ClineMessage, dc *DisplayContext
}
// handleDefault handles unknown ASK message types
func (h *AskHandler) handleDefault(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "ASK", msg.Text)
func (h *AskHandler) handleDefault(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("ASK", msg.Text, true)
}
+11 -9
View File
@@ -22,12 +22,16 @@ type MessageHandler interface {
// DisplayContext provides context and utilities for message handlers
type DisplayContext struct {
State *types.ConversationState
Renderer *display.Renderer
IsLast bool
IsPartial bool
Verbose bool
MessageIndex int
State *types.ConversationState
Renderer *display.Renderer
ToolRenderer *display.ToolRenderer
SystemRenderer *display.SystemMessageRenderer
IsLast bool
IsPartial bool
Verbose bool
MessageIndex int
IsStreamingMode bool
IsInteractive bool
}
// BaseHandler provides common functionality for message handlers
@@ -94,15 +98,13 @@ func (r *HandlerRegistry) Handle(msg *types.ClineMessage, dc *DisplayContext) er
// handleDefault provides default handling for unrecognized messages
func (r *HandlerRegistry) handleDefault(msg *types.ClineMessage, dc *DisplayContext) error {
timestamp := msg.GetTimestamp()
if msg.Text == "" {
return nil
}
prefix := "RESPONSE:"
return dc.Renderer.RenderMessage(timestamp, prefix, msg.Text)
return dc.Renderer.RenderMessage(prefix, msg.Text, true)
}
// GetHandlers returns all registered handlers
+253 -150
View File
@@ -5,7 +5,9 @@ import (
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/clerror"
"github.com/cline/cli/pkg/cli/types"
"github.com/cline/cli/pkg/cli/output"
)
// SayHandler handles SAY type messages
@@ -31,269 +33,346 @@ func (h *SayHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
switch msg.Say {
case string(types.SayTypeTask):
return h.handleTask(msg, dc, timestamp)
return h.handleTask(msg, dc)
case string(types.SayTypeError):
return h.handleError(msg, dc, timestamp)
return h.handleError(msg, dc)
case string(types.SayTypeAPIReqStarted):
return h.handleAPIReqStarted(msg, dc, timestamp)
return h.handleAPIReqStarted(msg, dc)
case string(types.SayTypeAPIReqFinished):
return h.handleAPIReqFinished(msg, dc, timestamp)
return h.handleAPIReqFinished(msg, dc)
case string(types.SayTypeText):
return h.handleText(msg, dc, timestamp)
return h.handleText(msg, dc)
case string(types.SayTypeReasoning):
return h.handleReasoning(msg, dc, timestamp)
return h.handleReasoning(msg, dc)
case string(types.SayTypeCompletionResult):
return h.handleCompletionResult(msg, dc, timestamp)
return h.handleCompletionResult(msg, dc)
case string(types.SayTypeUserFeedback):
return h.handleUserFeedback(msg, dc, timestamp)
return h.handleUserFeedback(msg, dc)
case string(types.SayTypeUserFeedbackDiff):
return h.handleUserFeedbackDiff(msg, dc, timestamp)
return h.handleUserFeedbackDiff(msg, dc)
case string(types.SayTypeAPIReqRetried):
return h.handleAPIReqRetried(msg, dc, timestamp)
return h.handleAPIReqRetried(msg, dc)
case string(types.SayTypeErrorRetry):
return h.handleErrorRetry(msg, dc)
case string(types.SayTypeCommand):
return h.handleCommand(msg, dc, timestamp)
return h.handleCommand(msg, dc)
case string(types.SayTypeCommandOutput):
return h.handleCommandOutput(msg, dc, timestamp)
return h.handleCommandOutput(msg, dc)
case string(types.SayTypeTool):
return h.handleTool(msg, dc, timestamp)
return h.handleTool(msg, dc)
case string(types.SayTypeShellIntegrationWarning):
return h.handleShellIntegrationWarning(msg, dc, timestamp)
return h.handleShellIntegrationWarning(msg, dc)
case string(types.SayTypeBrowserActionLaunch):
return h.handleBrowserActionLaunch(msg, dc, timestamp)
return h.handleBrowserActionLaunch(msg, dc)
case string(types.SayTypeBrowserAction):
return h.handleBrowserAction(msg, dc, timestamp)
return h.handleBrowserAction(msg, dc)
case string(types.SayTypeBrowserActionResult):
return h.handleBrowserActionResult(msg, dc, timestamp)
return h.handleBrowserActionResult(msg, dc)
case string(types.SayTypeMcpServerRequestStarted):
return h.handleMcpServerRequestStarted(msg, dc, timestamp)
return h.handleMcpServerRequestStarted(msg, dc)
case string(types.SayTypeMcpServerResponse):
return h.handleMcpServerResponse(msg, dc, timestamp)
return h.handleMcpServerResponse(msg, dc)
case string(types.SayTypeMcpNotification):
return h.handleMcpNotification(msg, dc, timestamp)
return h.handleMcpNotification(msg, dc)
case string(types.SayTypeUseMcpServer):
return h.handleUseMcpServer(msg, dc, timestamp)
return h.handleUseMcpServer(msg, dc)
case string(types.SayTypeDiffError):
return h.handleDiffError(msg, dc, timestamp)
return h.handleDiffError(msg, dc)
case string(types.SayTypeDeletedAPIReqs):
return h.handleDeletedAPIReqs(msg, dc, timestamp)
return h.handleDeletedAPIReqs(msg, dc)
case string(types.SayTypeClineignoreError):
return h.handleClineignoreError(msg, dc, timestamp)
return h.handleClineignoreError(msg, dc)
case string(types.SayTypeCheckpointCreated):
return h.handleCheckpointCreated(msg, dc, timestamp)
case string(types.SayTypeLoadMcpDocumentation):
return h.handleLoadMcpDocumentation(msg, dc, timestamp)
return h.handleLoadMcpDocumentation(msg, dc)
case string(types.SayTypeInfo):
return h.handleInfo(msg, dc, timestamp)
return h.handleInfo(msg, dc)
case string(types.SayTypeTaskProgress):
return h.handleTaskProgress(msg, dc, timestamp)
return h.handleTaskProgress(msg, dc)
default:
return h.handleDefault(msg, dc, timestamp)
return h.handleDefault(msg, dc)
}
}
// handleTask handles task messages
func (h *SayHandler) handleTask(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleTask(msg *types.ClineMessage, dc *DisplayContext) error {
return nil
}
// handleError handles error messages
func (h *SayHandler) handleError(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "ERROR", msg.Text)
func (h *SayHandler) handleError(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("ERROR", msg.Text, true)
}
// handleAPIReqStarted handles API request started messages
func (h *SayHandler) handleAPIReqStarted(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleAPIReqStarted(msg *types.ClineMessage, dc *DisplayContext) error {
// Parse API request info
apiInfo := types.APIRequestInfo{Cost: -1}
if err := json.Unmarshal([]byte(msg.Text), &apiInfo); err != nil {
return dc.Renderer.RenderMessage(timestamp, "API INFO", msg.Text)
return dc.Renderer.RenderMessage("API INFO", msg.Text, true)
}
// Check for streaming failed message with error details
if apiInfo.StreamingFailedMessage != "" {
clineErr, _ := clerror.ParseClineError(apiInfo.StreamingFailedMessage)
if clineErr != nil {
return h.renderClineError(clineErr, dc)
}
}
// Handle different API request states
if apiInfo.CancelReason != "" {
if apiInfo.CancelReason == "user_cancelled" {
return dc.Renderer.RenderMessage(timestamp, "API INFO", "Request Cancelled")
return dc.Renderer.RenderMessage("API INFO", "Request Cancelled", true)
} else if apiInfo.CancelReason == "retries_exhausted" {
return dc.Renderer.RenderMessage(timestamp, "API INFO", "Request Failed (Retries Exhausted)")
return dc.Renderer.RenderMessage("API INFO", "Request Failed (Retries Exhausted)", true)
}
return dc.Renderer.RenderMessage(timestamp, "API INFO", "Streaming Failed")
return dc.Renderer.RenderMessage("API INFO", "Streaming Failed", true)
}
if apiInfo.Cost >= 0 {
return dc.Renderer.RenderAPI(timestamp, "Request completed", &apiInfo)
return dc.Renderer.RenderAPI("request completed", &apiInfo)
}
// Check for retry status
if apiInfo.RetryStatus != nil {
return dc.Renderer.RenderRetry(timestamp,
return dc.Renderer.RenderRetry(
apiInfo.RetryStatus.Attempt,
apiInfo.RetryStatus.MaxAttempts,
apiInfo.RetryStatus.DelaySec)
}
return dc.Renderer.RenderAPI(timestamp, "Processing request", &apiInfo)
return dc.Renderer.RenderAPI("processing request", &apiInfo)
}
// renderClineError renders a ClineError with appropriate formatting based on type
func (h *SayHandler) renderClineError(err *clerror.ClineError, dc *DisplayContext) error {
if dc.SystemRenderer == nil {
return dc.Renderer.RenderMessage("ERROR", err.Message, true)
}
switch err.GetErrorType() {
case clerror.ErrorTypeBalance:
return dc.SystemRenderer.RenderBalanceError(err)
case clerror.ErrorTypeAuth:
return dc.SystemRenderer.RenderAuthError(err)
case clerror.ErrorTypeRateLimit:
return dc.SystemRenderer.RenderRateLimitError(err)
default:
return dc.SystemRenderer.RenderAPIError(err)
}
}
// handleAPIReqFinished handles API request finished messages
func (h *SayHandler) handleAPIReqFinished(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleAPIReqFinished(msg *types.ClineMessage, dc *DisplayContext) error {
// This message type is typically not displayed as it's handled by the started message
return nil
}
// handleText handles regular text messages
func (h *SayHandler) handleText(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleText(msg *types.ClineMessage, dc *DisplayContext) error {
if msg.Text == "" {
return nil
}
// Special case for the user's task input
prefix := "ASST TEXT"
if dc.MessageIndex == 0 {
prefix = "USER"
markdown := formatUserMessage(msg.Text)
rendered := dc.Renderer.RenderMarkdown(markdown)
output.Printf("%s", rendered)
output.Printf("\n")
return nil
}
return dc.Renderer.RenderMessage(timestamp, prefix, msg.Text)
// Regular Cline text response
var rendered string
if dc.IsStreamingMode {
// In streaming mode, header already shown by partial stream
rendered = dc.Renderer.RenderMarkdown(msg.Text)
output.Printf("%s\n", rendered)
} else {
// In non-streaming mode, render header + body together
markdown := fmt.Sprintf("### Cline responds\n\n%s", msg.Text)
rendered = dc.Renderer.RenderMarkdown(markdown)
output.Printf("\n%s\n", rendered)
}
return nil
}
// handleReasoning handles reasoning messages
func (h *SayHandler) handleReasoning(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleReasoning(msg *types.ClineMessage, dc *DisplayContext) error {
if msg.Text == "" {
return nil
}
return dc.Renderer.RenderMessage(timestamp, "THINKING", msg.Text)
var rendered string
if dc.IsStreamingMode {
// In streaming mode, header already shown by partial stream
rendered = dc.Renderer.RenderMarkdown(msg.Text)
output.Printf("%s\n", rendered)
} else {
// In non-streaming mode, render header + body together
markdown := fmt.Sprintf("### Cline is thinking\n\n%s", msg.Text)
rendered = dc.Renderer.RenderMarkdown(markdown)
output.Printf("\n%s\n", rendered)
}
return nil
}
func (h *SayHandler) handleCompletionResult(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleCompletionResult(msg *types.ClineMessage, dc *DisplayContext) error {
text := msg.Text
if strings.HasSuffix(text, "HAS_CHANGES") {
text = strings.TrimSuffix(text, "HAS_CHANGES")
}
return dc.Renderer.RenderMessage(timestamp, "RESULT", text)
var rendered string
if dc.IsStreamingMode {
// In streaming mode, header already shown by partial stream
rendered = dc.Renderer.RenderMarkdown(text)
output.Printf("%s\n", rendered)
} else {
// In non-streaming mode, render header + body together
markdown := fmt.Sprintf("### Task completed\n\n%s", text)
rendered = dc.Renderer.RenderMarkdown(markdown)
output.Printf("\n%s\n", rendered)
}
return nil
}
func formatUserMessage(text string) string {
lines := strings.Split(text, "\n")
// Wrap each line in backticks
for i, line := range lines {
if line != "" {
lines[i] = fmt.Sprintf("`%s`", line)
}
}
return strings.Join(lines, "\n")
}
// handleUserFeedback handles user feedback messages
func (h *SayHandler) handleUserFeedback(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleUserFeedback(msg *types.ClineMessage, dc *DisplayContext) error {
if msg.Text != "" {
return dc.Renderer.RenderMessage(timestamp, "USER", msg.Text)
markdown := formatUserMessage(msg.Text)
rendered := dc.Renderer.RenderMarkdown(markdown)
output.Printf("%s", rendered)
return nil
} else {
return dc.Renderer.RenderMessage(timestamp, "USER", "[Provided feedback without text]")
return dc.Renderer.RenderMessage("USER", "[Provided feedback without text]", true)
}
}
// handleUserFeedbackDiff handles user feedback diff messages
func (h *SayHandler) handleUserFeedbackDiff(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleUserFeedbackDiff(msg *types.ClineMessage, dc *DisplayContext) error {
var toolMsg types.ToolMessage
if err := json.Unmarshal([]byte(msg.Text), &toolMsg); err != nil {
return dc.Renderer.RenderMessage(timestamp, "USER DIFF", msg.Text)
return dc.Renderer.RenderMessage("USER DIFF", msg.Text, true)
}
message := fmt.Sprintf("User manually edited: %s\n\nDiff:\n%s",
toolMsg.Path,
toolMsg.Diff)
return dc.Renderer.RenderMessage(timestamp, "USER DIFF", message)
return dc.Renderer.RenderMessage("USER DIFF", message, true)
}
// handleAPIReqRetried handles API request retry messages
func (h *SayHandler) handleAPIReqRetried(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "API INFO", "Retrying request")
func (h *SayHandler) handleAPIReqRetried(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("API INFO", "Retrying request", true)
}
// handleErrorRetry handles error retry status messages
func (h *SayHandler) handleErrorRetry(msg *types.ClineMessage, dc *DisplayContext) error {
// Parse retry info from message text
type ErrorRetryInfo struct {
Attempt int `json:"attempt"`
MaxAttempts int `json:"maxAttempts"`
DelaySeconds int `json:"delaySeconds"`
Failed bool `json:"failed"`
}
var retryInfo ErrorRetryInfo
if err := json.Unmarshal([]byte(msg.Text), &retryInfo); err != nil {
// Fallback to simple message if parsing fails
return dc.Renderer.RenderMessage("API INFO", "Auto-retry in progress", true)
}
if retryInfo.Failed {
// Retry failed after max attempts
message := fmt.Sprintf("Auto-retry failed after %d attempts. Manual intervention required.", retryInfo.MaxAttempts)
if dc.SystemRenderer != nil {
return dc.SystemRenderer.RenderWarning("Auto-Retry Failed", message)
}
return dc.Renderer.RenderMessage("WARNING", message, true)
}
// Retry in progress
message := fmt.Sprintf("Attempt %d/%d - Retrying in %d seconds...",
retryInfo.Attempt, retryInfo.MaxAttempts, retryInfo.DelaySeconds)
return dc.Renderer.RenderMessage("API INFO", message, true)
}
// handleCommand handles command execution announcements
func (h *SayHandler) handleCommand(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleCommand(msg *types.ClineMessage, dc *DisplayContext) error {
if msg.Text == "" {
return nil
}
command := strings.TrimSpace(msg.Text)
err := dc.Renderer.RenderMessage(timestamp, "TERMINAL", "Running command:")
if err != nil {
return fmt.Errorf("failed to render handleCommand: %w", err)
}
fmt.Printf("\n```shell\n%s\n```\n", command)
// Use unified ToolRenderer
rendered := dc.ToolRenderer.RenderCommandExecution(msg.Text)
output.Print(rendered)
return nil
}
// handleCommandOutput handles command output messages
func (h *SayHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
commandOutput := msg.Text
return dc.Renderer.RenderMessage(timestamp, "TERMINAL", fmt.Sprintf("Current terminal output: %s", commandOutput))
}
func (h *SayHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
var tool types.ToolMessage
if err := json.Unmarshal([]byte(msg.Text), &tool); err != nil {
return dc.Renderer.RenderMessage(timestamp, "TOOL", msg.Text)
}
return h.renderToolMessage(&tool, dc, timestamp)
}
func (h *SayHandler) renderToolMessage(tool *types.ToolMessage, dc *DisplayContext, timestamp string) error {
switch tool.Tool {
case string(types.ToolTypeEditedExistingFile):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline edited file: %s", tool.Path))
case string(types.ToolTypeNewFileCreated):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline created file: %s", tool.Path))
case string(types.ToolTypeReadFile):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline read file: %s", tool.Path))
case string(types.ToolTypeListFilesTopLevel):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline listed files in: %s", tool.Path))
case string(types.ToolTypeListFilesRecursive):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline recursively listed files in: %s", tool.Path))
case string(types.ToolTypeSearchFiles):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline searched for '%s' in: %s", tool.Regex, tool.Path))
case string(types.ToolTypeWebFetch):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline fetched URL: %s", tool.Path))
case string(types.ToolTypeListCodeDefinitionNames):
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline listed code definitions for: %s", tool.Path))
case string(types.ToolTypeSummarizeTask):
dc.Renderer.RenderMessage(timestamp, "TOOL", "Cline condensed the conversation")
default:
dc.Renderer.RenderMessage(timestamp, "TOOL", fmt.Sprintf("Cline executed tool: %s", tool.Tool))
}
// Skip content preview for readFile and webFetch tools
if tool.Tool == string(types.ToolTypeReadFile) || tool.Tool == string(types.ToolTypeWebFetch) {
func (h *SayHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayContext) error {
if msg.Text == "" {
return nil
}
// Show content preview, truncating if necessary
preview := tool.Content
if preview != "" {
preview = strings.TrimSpace(tool.Content)
if len(preview) > 1000 {
preview = preview[:1000] + "..."
}
fmt.Printf("Content: %s\n", preview)
// Use unified ToolRenderer
rendered := dc.ToolRenderer.RenderCommandOutput(msg.Text)
output.Print(rendered)
return nil
}
func (h *SayHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext) error {
var tool types.ToolMessage
if err := json.Unmarshal([]byte(msg.Text), &tool); err != nil {
return dc.Renderer.RenderMessage("TOOL", msg.Text, true)
}
// Use unified ToolRenderer
rendered := dc.ToolRenderer.RenderToolExecution(&tool)
output.Print(rendered)
return nil
}
// handleShellIntegrationWarning handles shell integration warning messages
func (h *SayHandler) handleShellIntegrationWarning(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "WARNING", "Shell Integration Unavailable - Cline won't be able to view the command's output.")
func (h *SayHandler) handleShellIntegrationWarning(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("WARNING", "Shell Integration Unavailable - Cline won't be able to view the command's output.", true)
}
// handleBrowserActionLaunch handles browser action launch messages
func (h *SayHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext) error {
url := msg.Text
if url == "" {
return nil
}
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Launching browser at: %s", url))
return dc.Renderer.RenderMessage("BROWSER", fmt.Sprintf("Launching browser at: %s", url), true)
}
// handleBrowserAction handles browser action messages
func (h *SayHandler) handleBrowserAction(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleBrowserAction(msg *types.ClineMessage, dc *DisplayContext) error {
if msg.Text == "" {
return nil
}
@@ -306,27 +385,27 @@ func (h *SayHandler) handleBrowserAction(msg *types.ClineMessage, dc *DisplayCon
var actionData BrowserActionData
if err := json.Unmarshal([]byte(msg.Text), &actionData); err != nil {
return dc.Renderer.RenderMessage(timestamp, "BROWSER", msg.Text)
return dc.Renderer.RenderMessage("BROWSER", msg.Text, true)
}
// Special handling for type action
if actionData.Action == "type" && actionData.Text != "" {
actionText := fmt.Sprintf("type '%s'", actionData.Text)
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Next action: %s", actionText))
return dc.Renderer.RenderMessage("BROWSER", fmt.Sprintf("Next action: %s", actionText), true)
}
// Special handling for click action
if actionData.Action == "click" && actionData.Coordinate != "" {
actionText := fmt.Sprintf("click (%s)", actionData.Coordinate)
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Next action: %s", actionText))
return dc.Renderer.RenderMessage("BROWSER", fmt.Sprintf("Next action: %s", actionText), true)
}
// Generic handling for all other actions
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Next action: %s", actionData.Action))
return dc.Renderer.RenderMessage("BROWSER", fmt.Sprintf("Next action: %s", actionData.Action), true)
}
// handleBrowserActionResult handles browser action result messages
func (h *SayHandler) handleBrowserActionResult(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleBrowserActionResult(msg *types.ClineMessage, dc *DisplayContext) error {
if msg.Text == "" {
return nil
}
@@ -340,79 +419,103 @@ func (h *SayHandler) handleBrowserActionResult(msg *types.ClineMessage, dc *Disp
var result BrowserActionResult
if err := json.Unmarshal([]byte(msg.Text), &result); err != nil {
return dc.Renderer.RenderMessage(timestamp, "BROWSER", "Action completed")
return dc.Renderer.RenderMessage("BROWSER", "Action completed", true)
}
// If we have logs, include them in the message
if result.Logs != "" {
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Action completed with logs: '%s'", result.Logs))
return dc.Renderer.RenderMessage("BROWSER", fmt.Sprintf("Action completed with logs: '%s'", result.Logs), true)
}
// Default case
return dc.Renderer.RenderMessage(timestamp, "BROWSER", "Action completed")
return dc.Renderer.RenderMessage("BROWSER", "Action completed", true)
}
// handleMcpServerRequestStarted handles MCP server request started messages
func (h *SayHandler) handleMcpServerRequestStarted(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "MCP", "Sending request to server")
func (h *SayHandler) handleMcpServerRequestStarted(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("MCP", "Sending request to server", true)
}
// handleMcpServerResponse handles MCP server response messages
func (h *SayHandler) handleMcpServerResponse(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "MCP", fmt.Sprintf("Server response: %s", msg.Text))
func (h *SayHandler) handleMcpServerResponse(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("MCP", fmt.Sprintf("Server response: %s", msg.Text), true)
}
// handleMcpNotification handles MCP notification messages
func (h *SayHandler) handleMcpNotification(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "MCP", fmt.Sprintf("Server notification: %s", msg.Text))
func (h *SayHandler) handleMcpNotification(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("MCP", fmt.Sprintf("Server notification: %s", msg.Text), true)
}
// handleUseMcpServer handles MCP server usage messages
func (h *SayHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "MCP", "Server operation approved")
func (h *SayHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("MCP", "Server operation approved", true)
}
// handleDiffError handles diff error messages
func (h *SayHandler) handleDiffError(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "WARNING", "Diff Edit Failure - The model used an invalid diff edit format or used search patterns that don't match anything in the file.")
func (h *SayHandler) handleDiffError(msg *types.ClineMessage, dc *DisplayContext) error {
if dc.SystemRenderer != nil {
return dc.SystemRenderer.RenderWarning(
"Diff Edit Failure",
"The model used search patterns that don't match anything in the file. Retrying...",
)
}
return dc.Renderer.RenderMessage("WARNING", "Diff Edit Failure - The model used an invalid diff edit format or used search patterns that don't match anything in the file.", true)
}
// handleDeletedAPIReqs handles deleted API requests messages
func (h *SayHandler) handleDeletedAPIReqs(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
// This message includes api metrics of deleted messages, which we do not log
return dc.Renderer.RenderMessage(timestamp, "GEN INFO", "Checkpoint restored")
func (h *SayHandler) handleDeletedAPIReqs(msg *types.ClineMessage, dc *DisplayContext) error {
// Don't render - this is internal metadata (aggregated API metrics from deleted checkpoint messages)
return nil
}
// handleClineignoreError handles .clineignore error messages
func (h *SayHandler) handleClineignoreError(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "WARNING", fmt.Sprintf("Access Denied - Cline tried to access %s which is blocked by the .clineignore file", msg.Text))
func (h *SayHandler) handleClineignoreError(msg *types.ClineMessage, dc *DisplayContext) error {
if dc.SystemRenderer != nil {
return dc.SystemRenderer.RenderInfo(
"Access Denied",
fmt.Sprintf("Cline tried to access `%s` which is blocked by the .clineignore file.", msg.Text),
)
}
return dc.Renderer.RenderMessage("WARNING", fmt.Sprintf("Access Denied - Cline tried to access %s which is blocked by the .clineignore file", msg.Text), true)
}
// handleCheckpointCreated handles checkpoint created messages
func (h *SayHandler) handleCheckpointCreated(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "GEN INFO", "Checkpoint created")
if dc.SystemRenderer != nil {
return dc.SystemRenderer.RenderCheckpoint(timestamp, msg.Timestamp)
}
// Fallback to basic renderer if SystemRenderer not available
markdown := fmt.Sprintf("## [%s] Checkpoint created `%d`", timestamp, msg.Timestamp)
rendered := dc.Renderer.RenderMarkdown(markdown)
output.Print(rendered)
return nil
}
// handleLoadMcpDocumentation handles load MCP documentation messages
func (h *SayHandler) handleLoadMcpDocumentation(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "GEN INFO", "Loading MCP documentation")
func (h *SayHandler) handleLoadMcpDocumentation(msg *types.ClineMessage, dc *DisplayContext) error {
if dc.SystemRenderer != nil {
return dc.SystemRenderer.RenderInfo("MCP", "Loading MCP documentation")
}
return dc.Renderer.RenderMessage("INFO", "Loading MCP documentation", true)
}
// handleInfo handles info messages
func (h *SayHandler) handleInfo(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleInfo(msg *types.ClineMessage, dc *DisplayContext) error {
return nil
}
// handleTaskProgress handles task progress messages
func (h *SayHandler) handleTaskProgress(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
func (h *SayHandler) handleTaskProgress(msg *types.ClineMessage, dc *DisplayContext) error {
if msg.Text == "" {
return nil
}
return dc.Renderer.RenderMessage(timestamp, "PROGRESS", fmt.Sprintf("Task Checklist: %s", msg.Text))
markdown := fmt.Sprintf("### Progress\n\n%s", msg.Text)
rendered := dc.Renderer.RenderMarkdown(markdown)
output.Printf("\n%s\n", rendered)
return nil
}
// handleDefault handles unknown SAY message types
func (h *SayHandler) handleDefault(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return dc.Renderer.RenderMessage(timestamp, "SAY", msg.Text)
func (h *SayHandler) handleDefault(msg *types.ClineMessage, dc *DisplayContext) error {
return dc.Renderer.RenderMessage("SAY", msg.Text, true)
}
+228 -110
View File
@@ -4,16 +4,60 @@ import (
"context"
"fmt"
"os"
"strings"
"syscall"
"text/tabwriter"
"time"
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/common"
client2 "github.com/cline/grpc-go/client"
"github.com/cline/grpc-go/cline"
"github.com/spf13/cobra"
"google.golang.org/grpc/health/grpc_health_v1"
)
const (
platformCLI = "CLI"
platformJetBrains = "JetBrains"
platformNA = "N/A"
hostPlatformCLI = "Cline CLI" // Value returned by host bridge for CLI instances
)
// detectInstancePlatform connects to an instance's host bridge and determines its platform
func detectInstancePlatform(ctx context.Context, instance *common.CoreInstanceInfo) (string, error) {
hostTarget, err := common.NormalizeAddressForGRPC(instance.HostServiceAddress)
if err != nil {
return platformNA, err
}
hostClient, err := client2.NewClineClient(hostTarget)
if err != nil {
return platformNA, err
}
defer hostClient.Disconnect()
if err := hostClient.Connect(ctx); err != nil {
return platformNA, err
}
hostVersion, err := hostClient.Env.GetHostVersion(ctx, &cline.EmptyRequest{})
if err != nil {
return platformNA, err
}
if hostVersion.Platform == nil {
return platformNA, fmt.Errorf("host returned nil platform")
}
platformStr := *hostVersion.Platform
if platformStr == hostPlatformCLI {
return platformCLI, nil
}
return platformJetBrains, nil
}
func NewInstanceCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "instance",
@@ -23,7 +67,7 @@ func NewInstanceCommand() *cobra.Command {
}
cmd.AddCommand(newInstanceListCommand())
cmd.AddCommand(newInstanceUseCommand())
cmd.AddCommand(newInstanceDefaultCommand())
cmd.AddCommand(newInstanceNewCommand())
cmd.AddCommand(newInstanceKillCommand())
@@ -31,7 +75,7 @@ func NewInstanceCommand() *cobra.Command {
}
func newInstanceKillCommand() *cobra.Command {
var killAll bool
var killAllCLI bool
cmd := &cobra.Command{
Use: "kill <address>",
@@ -39,11 +83,11 @@ func newInstanceKillCommand() *cobra.Command {
Short: "Kill a Cline instance by address",
Long: `Kill a running Cline instance and clean up its registry entry.`,
Args: func(cmd *cobra.Command, args []string) error {
if killAll && len(args) > 0 {
return fmt.Errorf("cannot specify both --all flag and address argument")
if killAllCLI && len(args) > 0 {
return fmt.Errorf("cannot specify both --all-cli flag and address argument")
}
if !killAll && len(args) != 1 {
return fmt.Errorf("requires exactly one address argument when --all is not specified")
if !killAllCLI && len(args) != 1 {
return fmt.Errorf("requires exactly one address argument when --all-cli is not specified")
}
return nil
},
@@ -55,76 +99,20 @@ func newInstanceKillCommand() *cobra.Command {
ctx := cmd.Context()
registry := global.Clients.GetRegistry()
if killAll {
return killAllInstances(ctx, registry)
if killAllCLI {
return killAllCLIInstances(ctx, registry)
} else {
return killSingleInstance(ctx, registry, args[0])
return global.KillInstanceByAddress(ctx, registry, args[0])
}
},
}
cmd.Flags().BoolVar(&killAll, "all", false, "kill all running instances")
cmd.Flags().BoolVarP(&killAllCLI, "all-cli", "a", false, "kill all running CLI instances (excludes JetBrains)")
return cmd
}
func killSingleInstance(ctx context.Context, registry *global.ClientRegistry, address string) error {
// Check if the instance exists in the registry
_, err := registry.GetInstance(address)
if err != nil {
return fmt.Errorf("instance %s not found in registry", address)
}
fmt.Printf("Killing instance: %s\n", address)
// Get gRPC client and process info
client, err := registry.GetClient(ctx, address)
if err != nil {
return fmt.Errorf("failed to connect to instance %s: %w", address, err)
}
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to get process info for instance %s: %w", address, err)
}
pid := int(processInfo.ProcessId)
fmt.Printf("Terminating process PID %d...\n", pid)
// Kill the process
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
return fmt.Errorf("failed to kill process %d: %w", pid, err)
}
// Wait for the instance to remove itself from registry
fmt.Printf("Waiting for instance to clean up registry entry...\n")
for i := 0; i < 5; i++ {
time.Sleep(1 * time.Second)
if !registry.HasInstanceAtAddress(address) {
fmt.Printf("Instance %s successfully killed and removed from registry.\n", address)
// Update default instance if needed
instances, err := registry.ListInstancesCleaned(ctx)
if err == nil && len(instances) > 0 {
// ensureDefaultInstance logic will handle setting a new default
defaultInstance := registry.GetDefaultInstance()
if defaultInstance == address || defaultInstance == "" {
if len(instances) > 0 {
if err := registry.SetDefaultInstance(instances[0].Address); err == nil {
fmt.Printf("Updated default instance to: %s\n", instances[0].Address)
}
}
}
}
return nil
}
}
return fmt.Errorf("instance killed but failed to remove itself from registry within 5 seconds")
}
func killAllInstances(ctx context.Context, registry *global.ClientRegistry) error {
func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) error {
// Get all instances from registry
instances, err := registry.ListInstancesCleaned(ctx)
if err != nil {
@@ -136,12 +124,42 @@ func killAllInstances(ctx context.Context, registry *global.ClientRegistry) erro
return nil
}
fmt.Printf("Killing %d instances...\n", len(instances))
// Filter to only CLI instances
var cliInstances []*common.CoreInstanceInfo
var skippedNonCLI int
for _, instance := range instances {
if instance.Status == grpc_health_v1.HealthCheckResponse_SERVING {
platform, err := detectInstancePlatform(ctx, instance)
if err == nil {
if platform == platformCLI {
cliInstances = append(cliInstances, instance)
} else {
skippedNonCLI++
fmt.Printf("⊘ Skipping %s instance: %s\n", platform, instance.Address)
}
}
}
}
if len(cliInstances) == 0 {
if skippedNonCLI > 0 {
fmt.Printf("No CLI instances to kill. Skipped %d JetBrains instance(s).\n", skippedNonCLI)
} else {
fmt.Println("No CLI instances found to kill.")
}
return nil
}
fmt.Printf("Killing %d CLI instance(s)...\n", len(cliInstances))
if skippedNonCLI > 0 {
fmt.Printf("Skipping %d JetBrains instance(s).\n", skippedNonCLI)
}
var killResults []killResult
killedAddresses := make(map[string]bool)
// Kill all instances
for _, instance := range instances {
// Kill all CLI instances
for _, instance := range cliInstances {
result := killInstanceProcess(ctx, registry, instance.Address)
killResults = append(killResults, result)
@@ -151,31 +169,42 @@ func killAllInstances(ctx context.Context, registry *global.ClientRegistry) erro
fmt.Printf("⚠ Instance %s appears to be already dead\n", instance.Address)
} else {
fmt.Printf("✓ Killed %s (PID %d)\n", instance.Address, result.pid)
killedAddresses[instance.Address] = true
}
}
// Wait for all instances to clean up their registry entries
fmt.Printf("Waiting for instances to clean up registry entries...\n")
// Wait for killed instances to clean up their registry entries
if len(killedAddresses) > 0 {
fmt.Printf("Waiting for instances to clean up registry entries...\n")
maxWaitTime := 10 // seconds
for i := 0; i < maxWaitTime; i++ {
time.Sleep(1 * time.Second)
maxWaitTime := 10 // seconds
for i := 0; i < maxWaitTime; i++ {
time.Sleep(1 * time.Second)
remainingInstances, err := registry.ListInstancesCleaned(ctx)
if err != nil {
fmt.Printf("Warning: failed to check registry status: %v\n", err)
continue
}
remainingInstances, err := registry.ListInstancesCleaned(ctx)
if err != nil {
fmt.Printf("Warning: failed to check registry status: %v\n", err)
continue
}
if len(remainingInstances) == 0 {
fmt.Printf("✓ All instances successfully removed from registry.\n")
break
}
if i == maxWaitTime-1 {
fmt.Printf("⚠ %d instances still in registry after %d seconds\n", len(remainingInstances), maxWaitTime)
// Check if any of the killed instances are still in the registry
stillPresent := []string{}
for _, remaining := range remainingInstances {
fmt.Printf(" - %s\n", remaining.Address)
if killedAddresses[remaining.Address] {
stillPresent = append(stillPresent, remaining.Address)
}
}
if len(stillPresent) == 0 {
fmt.Printf("✓ All killed instances successfully removed from registry.\n")
break
}
if i == maxWaitTime-1 {
fmt.Printf("⚠ %d killed instance(s) still in registry after %d seconds\n", len(stillPresent), maxWaitTime)
for _, addr := range stillPresent {
fmt.Printf(" - %s\n", addr)
}
}
}
}
@@ -267,14 +296,22 @@ func newInstanceListCommand() *cobra.Command {
return nil
}
// Always output a table
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ADDRESS\tSTATUS\tVERSION\tLAST SEEN\tPID\tDEFAULT")
// Build instance data
type instanceRow struct {
address string
status string
version string
lastSeen string
pid string
platform string
isDefault string
}
var rows []instanceRow
for _, instance := range instances {
isDefault := ""
if instance.Address == defaultInstance {
isDefault = "*"
isDefault = ""
}
lastSeen := instance.LastSeen.Format("15:04:05")
@@ -282,9 +319,11 @@ func newInstanceListCommand() *cobra.Command {
lastSeen = instance.LastSeen.Format("2006-01-02")
}
// Get PID via RPC if instance is healthy
pid := "N/A"
// Get PID and platform via RPC if instance is healthy
pid := platformNA
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 processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{}); err == nil {
pid = fmt.Sprintf("%d", processInfo.ProcessId)
@@ -294,19 +333,84 @@ func newInstanceListCommand() *cobra.Command {
}
}
}
// Get platform from host bridge
if detectedPlatform, err := detectInstancePlatform(ctx, instance); err == nil {
platform = detectedPlatform
}
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
instance.Address,
instance.Status,
instance.Version,
lastSeen,
pid,
isDefault,
)
rows = append(rows, instanceRow{
address: instance.Address,
status: instance.Status.String(),
version: instance.Version,
lastSeen: lastSeen,
pid: pid,
platform: platform,
isDefault: isDefault,
})
}
// Check output format
if global.Config.OutputFormat == "plain" {
// Use tabwriter for plain output
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ADDRESS\tSTATUS\tVERSION\tLAST SEEN\tPID\tPLATFORM\tDEFAULT")
for _, row := range rows {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
row.address,
row.status,
row.version,
row.lastSeen,
row.pid,
row.platform,
row.isDefault,
)
}
w.Flush()
} else {
// Use markdown table for rich output
var markdown strings.Builder
markdown.WriteString("| **ADDRESS (ID)** | **STATUS** | **VERSION** | **LAST SEEN** | **PID** | **PLATFORM** | **DEFAULT** |\n")
markdown.WriteString("|---------|--------|---------|-----------|-----|----------|---------|")
for _, row := range rows {
markdown.WriteString(fmt.Sprintf("\n| %s | %s | %s | %s | %s | %s | %s |",
row.address,
row.status,
row.version,
row.lastSeen,
row.pid,
row.platform,
row.isDefault,
))
}
// Render the markdown table with terminal width for nice table layout
mdRenderer, err := display.NewMarkdownRendererForTerminal()
if err != nil {
// Fallback to plain table if markdown renderer fails
fmt.Println(markdown.String())
} else {
rendered, err := mdRenderer.Render(markdown.String())
if err != nil {
fmt.Println(markdown.String())
} else {
// Post-process to colorize status values
colorRenderer := display.NewRenderer(global.Config.OutputFormat)
rendered = strings.ReplaceAll(rendered, "SERVING", colorRenderer.Green("SERVING"))
rendered = strings.ReplaceAll(rendered, "✓", colorRenderer.Green("✓"))
rendered = strings.ReplaceAll(rendered, "NOT_SERVING", colorRenderer.Red("NOT_SERVING"))
rendered = strings.ReplaceAll(rendered, "UNKNOWN", colorRenderer.Yellow("UNKNOWN"))
fmt.Print(strings.TrimLeft(rendered, "\n"))
}
fmt.Println("\n")
}
}
w.Flush()
return nil
},
}
@@ -314,10 +418,10 @@ func newInstanceListCommand() *cobra.Command {
return cmd
}
func newInstanceUseCommand() *cobra.Command {
func newInstanceDefaultCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "use <address>",
Aliases: []string{"u"},
Use: "default <address>",
Aliases: []string{"d"},
Short: "Set the default Cline instance",
Long: `Set the default Cline instance to use for subsequent commands.`,
Args: cobra.ExactArgs(1),
@@ -350,6 +454,8 @@ func newInstanceUseCommand() *cobra.Command {
}
func newInstanceNewCommand() *cobra.Command {
var setDefault bool
cmd := &cobra.Command{
Use: "new",
Aliases: []string{"n"},
@@ -374,15 +480,27 @@ func newInstanceNewCommand() *cobra.Command {
fmt.Printf(" Core Port: %d\n", instance.CorePort())
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
// Check if this is now the default instance
registry := global.Clients.GetRegistry()
if registry.GetDefaultInstance() == instance.Address {
fmt.Printf(" Status: Default instance\n")
// If --default flag provided, set this instance as the default
if setDefault {
if err := registry.SetDefaultInstance(instance.Address); 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 {
fmt.Printf(" Status: Default instance\n")
}
}
return nil
},
}
cmd.Flags().BoolVarP(&setDefault, "default", "d", false, "set as default instance")
return cmd
}
}
+382
View File
@@ -0,0 +1,382 @@
package cli
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"text/tabwriter"
"time"
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/global"
"github.com/spf13/cobra"
)
type logFileInfo struct {
name string
path string
size int64
created time.Time
}
func NewLogsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "logs",
Aliases: []string{"log", "l"},
Short: "Manage Cline log files",
Long: `List and manage log files created by Cline instances.`,
}
cmd.AddCommand(newLogsListCommand())
cmd.AddCommand(newLogsCleanCommand())
cmd.AddCommand(newLogsPathCommand())
return cmd
}
func newLogsListCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l", "ls"},
Short: "List all log files",
Long: `List all log files in the Cline logs directory with their sizes and ages.`,
RunE: func(cmd *cobra.Command, args []string) error {
if global.Config == nil {
return fmt.Errorf("config not initialized")
}
logsDir := filepath.Join(global.Config.ConfigPath, "logs")
logs, err := listLogFiles(logsDir)
if err != nil {
return fmt.Errorf("failed to list log files: %w", err)
}
if len(logs) == 0 {
fmt.Println("No log files found.")
fmt.Printf("Log files will be created in: %s\n", logsDir)
return nil
}
return renderLogsTable(logs, false)
},
}
return cmd
}
func newLogsCleanCommand() *cobra.Command {
var olderThan int
var all bool
var dryRun bool
cmd := &cobra.Command{
Use: "clean",
Aliases: []string{"c"},
Short: "Delete old log files",
Long: `Delete log files older than a specified number of days.`,
RunE: func(cmd *cobra.Command, args []string) error {
if global.Config == nil {
return fmt.Errorf("config not initialized")
}
logsDir := filepath.Join(global.Config.ConfigPath, "logs")
logs, err := listLogFiles(logsDir)
if err != nil {
return fmt.Errorf("failed to list log files: %w", err)
}
var toDelete []logFileInfo
if all {
toDelete = logs
} else {
toDelete = filterOldLogs(logs, olderThan)
}
if len(toDelete) == 0 {
if all {
fmt.Println("No log files to delete.")
} else {
fmt.Printf("No log files older than %d days found.\n", olderThan)
}
return nil
}
// Calculate total size
var totalSize int64
for _, log := range toDelete {
totalSize += log.size
}
if dryRun {
fmt.Println("The following log files will be deleted:\n")
if err := renderLogsTable(toDelete, true); err != nil {
return err
}
fileWord := "files"
if len(toDelete) == 1 {
fileWord = "file"
}
fmt.Printf("\nSummary: %d %s will be deleted (%s freed)\n", len(toDelete), fileWord, formatFileSize(totalSize))
fmt.Println("\nRun without --dry-run to actually delete these files.")
return nil
}
// Actually delete the files
count, bytesFreed, err := deleteLogFiles(toDelete)
if err != nil {
return fmt.Errorf("failed to delete log files: %w", err)
}
fileWord := "files"
if count == 1 {
fileWord = "file"
}
fmt.Printf("Deleted %d log %s (%s freed)\n", count, fileWord, formatFileSize(bytesFreed))
return nil
},
}
cmd.Flags().IntVar(&olderThan, "older-than", 7, "delete logs older than N days")
cmd.Flags().BoolVar(&all, "all", false, "delete all log files")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show what would be deleted without deleting")
return cmd
}
func newLogsPathCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "path",
Short: "Print the logs directory path",
Long: `Print the absolute path to the Cline logs directory.`,
RunE: func(cmd *cobra.Command, args []string) error {
if global.Config == nil {
return fmt.Errorf("config not initialized")
}
logsDir := filepath.Join(global.Config.ConfigPath, "logs")
fmt.Println(logsDir)
return nil
},
}
return cmd
}
// Helper functions
func listLogFiles(logsDir string) ([]logFileInfo, error) {
// Check if logs directory exists
if _, err := os.Stat(logsDir); os.IsNotExist(err) {
return []logFileInfo{}, nil
}
entries, err := os.ReadDir(logsDir)
if err != nil {
return nil, err
}
var logs []logFileInfo
for _, entry := range entries {
if entry.IsDir() {
continue
}
// Only process .log files
if !strings.HasSuffix(entry.Name(), ".log") {
continue
}
// Parse timestamp from filename
created, err := parseTimestampFromFilename(entry.Name())
if err != nil {
// Skip files we can't parse
continue
}
info, err := entry.Info()
if err != nil {
continue
}
logs = append(logs, logFileInfo{
name: entry.Name(),
path: filepath.Join(logsDir, entry.Name()),
size: info.Size(),
created: created,
})
}
// Sort by created time (oldest first)
sort.Slice(logs, func(i, j int) bool {
return logs[i].created.Before(logs[j].created)
})
return logs, nil
}
func parseTimestampFromFilename(filename string) (time.Time, error) {
// Expected format: cline-core-2025-10-12-21-30-45-localhost-51051.log
// or: cline-host-2025-10-12-21-30-45-localhost-52051.log
parts := strings.Split(filename, "-")
if len(parts) < 8 {
return time.Time{}, fmt.Errorf("invalid filename format")
}
// Extract timestamp parts: YYYY-MM-DD-HH-mm-ss
// They should be at indices 2-7
timestampStr := strings.Join(parts[2:8], "-")
// Parse as local time since the filename timestamp is created in local time
parsedTime, err := time.ParseInLocation("2006-01-02-15-04-05", timestampStr, time.Local)
if err != nil {
return time.Time{}, err
}
return parsedTime, nil
}
func filterOldLogs(logs []logFileInfo, olderThanDays int) []logFileInfo {
cutoff := time.Now().AddDate(0, 0, -olderThanDays)
var filtered []logFileInfo
for _, log := range logs {
if log.created.Before(cutoff) {
filtered = append(filtered, log)
}
}
return filtered
}
func deleteLogFiles(files []logFileInfo) (int, int64, error) {
var count int
var bytesFreed int64
for _, file := range files {
if err := os.Remove(file.path); err != nil {
return count, bytesFreed, err
}
count++
bytesFreed += file.size
}
return count, bytesFreed, nil
}
func formatFileSize(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
func formatAge(t time.Time) string {
duration := time.Since(t)
if duration < time.Hour {
minutes := int(duration.Minutes())
return fmt.Sprintf("%dm ago", minutes)
}
if duration < 24*time.Hour {
hours := int(duration.Hours())
return fmt.Sprintf("%dh ago", hours)
}
if duration < 7*24*time.Hour {
days := int(duration.Hours() / 24)
return fmt.Sprintf("%dd ago", days)
}
weeks := int(duration.Hours() / 24 / 7)
return fmt.Sprintf("%dw ago", weeks)
}
func renderLogsTable(logs []logFileInfo, markForDeletion bool) error {
// Build table data
type tableRow struct {
filename string
size string
created string
age string
}
var rows []tableRow
for _, log := range logs {
rows = append(rows, tableRow{
filename: log.name,
size: formatFileSize(log.size),
created: log.created.Format("2006-01-02 15:04:05"),
age: formatAge(log.created),
})
}
// Check output format
if global.Config.OutputFormat == "plain" {
// Use tabwriter for plain output
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "FILENAME\tSIZE\tCREATED\tAGE")
for _, row := range rows {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
row.filename,
row.size,
row.created,
row.age,
)
}
w.Flush()
return nil
}
// Use markdown table for rich output
colorRenderer := display.NewRenderer(global.Config.OutputFormat)
var markdown strings.Builder
markdown.WriteString("| **FILENAME** | **SIZE** | **CREATED** | **AGE** |\n")
markdown.WriteString("|--------------|----------|-------------|---------|")
for _, row := range rows {
line := fmt.Sprintf("\n| %s | %s | %s | %s |",
row.filename,
row.size,
row.created,
row.age,
)
// If marking for deletion, wrap in red
if markForDeletion {
line = colorRenderer.Red(line)
}
markdown.WriteString(line)
}
// Render the markdown table
renderer, err := display.NewMarkdownRendererForTerminal()
if err != nil {
// Fallback to plain markdown if renderer fails
fmt.Println(markdown.String())
return nil
}
rendered, err := renderer.Render(markdown.String())
if err != nil {
fmt.Println(markdown.String())
return nil
}
fmt.Print(strings.TrimLeft(rendered, "\n"))
fmt.Println()
return nil
}
+167
View File
@@ -0,0 +1,167 @@
package output
import (
"fmt"
"sync"
"sync/atomic"
"time"
tea "github.com/charmbracelet/bubbletea"
)
// SuspendInputMsg tells the input model to suspend and hide
type SuspendInputMsg struct{}
// ResumeInputMsg tells the input model to resume and show
type ResumeInputMsg struct{}
// OutputCoordinator manages terminal output and coordinates with interactive input
type OutputCoordinator struct {
mu sync.Mutex
program *tea.Program
inputVisible atomic.Bool
inputModel *InputModel // Reference to current input model for state restoration
restartCallback func(*InputModel) // Callback to restart the program with preserved state
}
var (
globalCoordinator *OutputCoordinator
coordinatorMu sync.Mutex
)
// GetCoordinator returns the global output coordinator instance
func GetCoordinator() *OutputCoordinator {
coordinatorMu.Lock()
defer coordinatorMu.Unlock()
if globalCoordinator == nil {
globalCoordinator = &OutputCoordinator{}
}
return globalCoordinator
}
// SetProgram sets the bubbletea program for input coordination
func (oc *OutputCoordinator) SetProgram(program *tea.Program) {
oc.mu.Lock()
defer oc.mu.Unlock()
oc.program = program
}
// SetInputModel sets the current input model reference for state preservation
func (oc *OutputCoordinator) SetInputModel(model *InputModel) {
oc.mu.Lock()
defer oc.mu.Unlock()
oc.inputModel = model
}
// SetRestartCallback sets the callback for restarting the program
func (oc *OutputCoordinator) SetRestartCallback(callback func(*InputModel)) {
oc.mu.Lock()
defer oc.mu.Unlock()
oc.restartCallback = callback
}
// SetInputVisible sets whether input is currently visible
func (oc *OutputCoordinator) SetInputVisible(visible bool) {
oc.inputVisible.Store(visible)
}
// IsInputVisible returns whether input is currently visible
func (oc *OutputCoordinator) IsInputVisible() bool {
return oc.inputVisible.Load()
}
// Printf prints formatted output, suspending input if necessary
func (oc *OutputCoordinator) Printf(format string, args ...interface{}) {
oc.mu.Lock()
prog := oc.program
model := oc.inputModel
restart := oc.restartCallback
visible := oc.inputVisible.Load()
oc.mu.Unlock()
if visible && prog != nil && restart != nil && model != nil {
// Kill/restart approach: completely stop the program, print, restart with state
// 1. Save the current input state (text, cursor position, etc.)
savedModel := model.Clone()
// 2. Manually clear the form from terminal BEFORE quitting
clearCodes := model.ClearScreen()
if clearCodes != "" {
fmt.Print(clearCodes)
}
// 3. Quit the program
prog.Send(Quit())
// Small delay to let program actually quit
time.Sleep(20 * time.Millisecond)
// 4. Print the output
fmt.Printf(format, args...)
// 5. Restart with preserved state
restart(savedModel)
} else {
// No input showing, just print normally
fmt.Printf(format, args...)
}
}
// Println prints a line with newline, suspending input if necessary
func (oc *OutputCoordinator) Println(args ...interface{}) {
oc.Printf("%s\n", fmt.Sprint(args...))
}
// Print prints output, suspending input if necessary
func (oc *OutputCoordinator) Print(args ...interface{}) {
oc.Printf("%s", fmt.Sprint(args...))
}
// Package-level convenience functions
// Printf prints formatted output via the global coordinator
func Printf(format string, args ...interface{}) {
GetCoordinator().Printf(format, args...)
}
// Println prints a line with newline via the global coordinator
func Println(args ...interface{}) {
GetCoordinator().Println(args...)
}
// Print prints output via the global coordinator
func Print(args ...interface{}) {
GetCoordinator().Print(args...)
}
// SetProgram sets the bubbletea program on the global coordinator
func SetProgram(program *tea.Program) {
GetCoordinator().SetProgram(program)
}
// SetInputVisible sets input visibility on the global coordinator
func SetInputVisible(visible bool) {
GetCoordinator().SetInputVisible(visible)
}
// IsInputVisible checks input visibility on the global coordinator
func IsInputVisible() bool {
return GetCoordinator().IsInputVisible()
}
// SetInputModel sets the input model on the global coordinator
func SetInputModel(model *InputModel) {
GetCoordinator().SetInputModel(model)
}
// SetRestartCallback sets the restart callback on the global coordinator
func SetRestartCallback(callback func(*InputModel)) {
GetCoordinator().SetRestartCallback(callback)
}
// Quit returns a Bubble Tea quit message
func Quit() tea.Msg {
return tea.Quit()
}
+497
View File
@@ -0,0 +1,497 @@
package output
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// InputType represents the type of input being collected
type InputType int
const INPUT_WIDTH = 46
const (
InputTypeMessage InputType = iota
InputTypeApproval
InputTypeFeedback
)
// InputSubmitMsg is sent when the user submits input
type InputSubmitMsg struct {
Value string
InputType InputType
Approved bool // For approval type
NeedsFeedback bool // For approval type
NoAskAgain bool // For approval type - indicates "don't ask again" was selected
}
// InputCancelMsg is sent when the user cancels input (Ctrl+C)
type InputCancelMsg struct{}
// ChangeInputTypeMsg changes the current input type
type ChangeInputTypeMsg struct {
InputType InputType
Title string
Placeholder string
}
// editorFinishedMsg is sent when the external editor finishes
type editorFinishedMsg struct {
content []byte
err error
}
// InputModel is the bubbletea model for interactive input
type InputModel struct {
textarea textarea.Model
suspended bool
savedValue string
inputType InputType
title string
placeholder string
currentMode string // "plan" or "act"
width int
lastHeight int // Track height for cleanup on submit
// For approval type
approvalOptions []string
selectedOption int
pendingApproval bool // Stores approval decision when transitioning to feedback input
// Styles (huh-inspired theme)
styles fieldStyles
}
// fieldStyles holds the styling for the input field
type fieldStyles struct {
base lipgloss.Style
title lipgloss.Style
textArea lipgloss.Style
cursor lipgloss.Style
placeholder lipgloss.Style
selector lipgloss.Style
selectedOption lipgloss.Style
option lipgloss.Style
}
// newFieldStyles creates huh-inspired styles (Charm theme)
func newFieldStyles() fieldStyles {
// Charm theme colors
indigo := lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"}
fuchsia := lipgloss.Color("#F780E2")
normalFg := lipgloss.AdaptiveColor{Light: "235", Dark: "252"}
green := lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"}
return fieldStyles{
base: lipgloss.NewStyle().
PaddingLeft(1).
BorderStyle(lipgloss.ThickBorder()).
BorderLeft(true).
BorderForeground(lipgloss.Color("238")),
title: lipgloss.NewStyle().
Foreground(indigo).
Bold(true),
textArea: lipgloss.NewStyle().
Foreground(normalFg),
cursor: lipgloss.NewStyle().
Foreground(green),
placeholder: lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "248", Dark: "238"}),
selector: lipgloss.NewStyle().
Foreground(fuchsia).
SetString("> "),
selectedOption: lipgloss.NewStyle().
Foreground(normalFg),
option: lipgloss.NewStyle().
Foreground(normalFg),
}
}
// NewInputModel creates a new input model
func NewInputModel(inputType InputType, title, placeholder, currentMode string) InputModel {
ta := textarea.New()
ta.Placeholder = placeholder
ta.Focus()
ta.CharLimit = 0
ta.ShowLineNumbers = false
ta.Prompt = "" // Remove prompt prefix (this is what adds the inner border!)
ta.SetHeight(5)
// Don't set width here - let WindowSizeMsg handle it
ta.SetWidth(INPUT_WIDTH)
// Configure keybindings like huh does:
// alt+enter and ctrl+j for newlines (textarea will handle these)
ta.KeyMap.InsertNewline.SetKeys("alt+enter", "ctrl+j")
// Apply huh-like styling
styles := newFieldStyles()
// Set cursor color based on mode
cursorColor := lipgloss.Color("3") // Yellow for plan
if currentMode == "act" {
cursorColor = lipgloss.Color("39") // Blue for act
}
ta.FocusedStyle.CursorLine = lipgloss.NewStyle() // No cursor line highlighting
ta.FocusedStyle.EndOfBuffer = lipgloss.NewStyle() // No end-of-buffer styling
ta.FocusedStyle.Placeholder = styles.placeholder
ta.FocusedStyle.Text = styles.textArea
ta.FocusedStyle.Prompt = lipgloss.NewStyle() // No prompt styling
ta.Cursor.Style = lipgloss.NewStyle().Foreground(cursorColor)
ta.Cursor.TextStyle = styles.textArea
m := InputModel{
textarea: ta,
inputType: inputType,
title: title,
placeholder: placeholder,
currentMode: currentMode,
width: 0, // Will be set by first WindowSizeMsg
styles: styles,
}
// For approval type, set up options
if inputType == InputTypeApproval {
m.approvalOptions = []string{
"Yes",
"Yes, and don't ask again for this task",
"No, with feedback",
}
m.selectedOption = 0
}
return m
}
// Init initializes the model
func (m *InputModel) Init() tea.Cmd {
return textarea.Blink
}
// Update handles messages
func (m *InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case editorFinishedMsg:
// External editor finished
if msg.err == nil && len(msg.content) > 0 {
m.textarea.SetValue(string(msg.content))
}
return m, nil
case SuspendInputMsg:
// Save current value and suspend
m.savedValue = m.textarea.Value()
m.suspended = true
return m, tea.ClearScreen
case ResumeInputMsg:
// Restore value and resume
m.textarea.SetValue(m.savedValue)
m.suspended = false
return m, nil
case ChangeInputTypeMsg:
// Change input type (e.g., from approval to feedback)
m.inputType = msg.InputType
m.title = msg.Title
m.placeholder = msg.Placeholder
m.textarea.Placeholder = msg.Placeholder
m.textarea.SetValue("")
m.textarea.Focus()
if msg.InputType == InputTypeApproval {
m.approvalOptions = []string{
"Yes",
"Yes, and don't ask again for this task",
"No, with feedback",
}
m.selectedOption = 0
}
return m, nil
default:
// Forward all other messages to textarea (including blink ticks)
if !m.suspended && (m.inputType == InputTypeMessage || m.inputType == InputTypeFeedback) {
m.textarea, cmd = m.textarea.Update(msg)
return m, cmd
}
case tea.KeyMsg:
if m.suspended {
return m, nil
}
// Handle keys for text input types (Message/Feedback)
if m.inputType == InputTypeMessage || m.inputType == InputTypeFeedback {
switch msg.String() {
case "ctrl+c":
return m, func() tea.Msg { return InputCancelMsg{} }
case "ctrl+e":
// Open external editor (like huh does)
return m, m.openEditor()
case "enter":
// Intercept enter for submit (textarea handles alt+enter and ctrl+j for newlines)
return m.handleSubmit()
case "up", "down", "left", "right":
// Let textarea handle navigation
m.textarea, cmd = m.textarea.Update(msg)
return m, cmd
}
// Pass all other keys to textarea (including alt+enter, ctrl+j for newlines)
m.textarea, cmd = m.textarea.Update(msg)
return m, cmd
}
// Handle keys for approval type
if m.inputType == InputTypeApproval {
switch msg.String() {
case "ctrl+c":
return m, func() tea.Msg { return InputCancelMsg{} }
case "enter":
return m.handleSubmit()
case "up":
if m.selectedOption > 0 {
m.selectedOption--
}
return m, nil
case "down":
if m.selectedOption < len(m.approvalOptions)-1 {
m.selectedOption++
}
return m, nil
}
}
}
return m, nil
}
// handleSubmit handles submission based on input type
func (m *InputModel) handleSubmit() (tea.Model, tea.Cmd) {
switch m.inputType {
case InputTypeMessage:
value := strings.TrimSpace(m.textarea.Value())
return m, func() tea.Msg {
return InputSubmitMsg{
Value: value,
InputType: InputTypeMessage,
}
}
case InputTypeApproval:
selected := m.approvalOptions[m.selectedOption]
approved := strings.HasPrefix(selected, "Yes")
needsFeedback := strings.Contains(selected, "feedback")
noAskAgain := strings.Contains(selected, "don't ask again")
if needsFeedback {
// Store the approval decision before switching to feedback input
m.pendingApproval = approved
// Switch to feedback input
return m, func() tea.Msg {
return ChangeInputTypeMsg{
InputType: InputTypeFeedback,
Title: "Your feedback",
Placeholder: "/plan or /act to switch modes\nctrl+e to open editor",
}
}
}
return m, func() tea.Msg {
return InputSubmitMsg{
Value: "",
InputType: InputTypeApproval,
Approved: approved,
NeedsFeedback: false,
NoAskAgain: noAskAgain,
}
}
case InputTypeFeedback:
value := strings.TrimSpace(m.textarea.Value())
return m, func() tea.Msg {
return InputSubmitMsg{
Value: value,
InputType: InputTypeFeedback,
Approved: m.pendingApproval, // Pass the stored approval decision
}
}
}
return m, nil
}
// View renders the model
func (m *InputModel) View() string {
if m.suspended {
return ""
}
var parts []string
// Render title with mode indicator
yellow := lipgloss.Color("3")
blue := lipgloss.Color("39")
modeStyle := lipgloss.NewStyle().Bold(true)
if m.currentMode == "plan" {
modeStyle = modeStyle.Foreground(yellow)
} else {
modeStyle = modeStyle.Foreground(blue)
}
modeIndicator := modeStyle.Render(fmt.Sprintf("[%s mode]", m.currentMode))
titleText := m.styles.title.Render(m.title)
fullTitle := fmt.Sprintf("%s %s", modeIndicator, titleText)
parts = append(parts, fullTitle)
// Render based on input type
switch m.inputType {
case InputTypeMessage, InputTypeFeedback:
parts = append(parts, m.textarea.View())
case InputTypeApproval:
var options []string
for i, option := range m.approvalOptions {
if i == m.selectedOption {
options = append(options, m.styles.selector.Render("")+m.styles.selectedOption.Render(option))
} else {
options = append(options, " "+m.styles.option.Render(option))
}
}
parts = append(parts, strings.Join(options, "\n"))
}
// Wrap everything in the base style with border
content := strings.Join(parts, "\n")
rendered := m.styles.base.Render(content)
// Add newline before the form (outside the border)
rendered = "\n" + rendered
// Track height for cleanup
m.lastHeight = lipgloss.Height(rendered)
return rendered
}
// ClearScreen returns the ANSI codes to clear the input from the terminal
// This is used when submitting to remove the form cleanly
func (m *InputModel) ClearScreen() string {
if m.lastHeight == 0 {
return ""
}
// Move cursor up by lastHeight lines and clear from cursor to end of screen
return fmt.Sprintf("\033[%dA\033[J", m.lastHeight)
}
// Clone creates a deep copy of the InputModel with all state preserved
func (m *InputModel) Clone() *InputModel {
// Create new textarea with same configuration
ta := textarea.New()
ta.SetValue(m.textarea.Value())
ta.Placeholder = m.placeholder
ta.CharLimit = 0
ta.ShowLineNumbers = false
ta.Prompt = ""
ta.SetHeight(5)
ta.SetWidth(INPUT_WIDTH)
ta.Focus()
// Configure keybindings
ta.KeyMap.InsertNewline.SetKeys("alt+enter", "ctrl+j")
// Apply styles (including mode-based cursor color)
cursorColor := lipgloss.Color("3") // Yellow for plan
if m.currentMode == "act" {
cursorColor = lipgloss.Color("39") // Blue for act
}
ta.FocusedStyle.CursorLine = lipgloss.NewStyle()
ta.FocusedStyle.EndOfBuffer = lipgloss.NewStyle()
ta.FocusedStyle.Placeholder = m.styles.placeholder
ta.FocusedStyle.Text = m.styles.textArea
ta.FocusedStyle.Prompt = lipgloss.NewStyle()
ta.Cursor.Style = lipgloss.NewStyle().Foreground(cursorColor)
ta.Cursor.TextStyle = m.styles.textArea
// Create cloned model
clone := &InputModel{
textarea: ta,
suspended: false, // New program starts unsuspended
savedValue: m.savedValue,
inputType: m.inputType,
title: m.title,
placeholder: m.placeholder,
currentMode: m.currentMode,
width: m.width,
lastHeight: m.lastHeight,
approvalOptions: m.approvalOptions,
selectedOption: m.selectedOption,
pendingApproval: m.pendingApproval, // Preserve approval decision
styles: m.styles,
}
return clone
}
// openEditor opens an external editor for composing the message
func (m *InputModel) openEditor() tea.Cmd {
// Get editor from environment or use nano as default
editorCmd := "nano"
editorArgs := []string{}
if editor := os.Getenv("EDITOR"); editor != "" {
editorFields := strings.Fields(editor)
if len(editorFields) > 0 {
editorCmd = editorFields[0]
if len(editorFields) > 1 {
editorArgs = editorFields[1:]
}
}
}
// Create temp file with current content
tmpFile, err := os.CreateTemp(os.TempDir(), "*.md")
if err != nil {
return func() tea.Msg {
return editorFinishedMsg{err: err}
}
}
// Write current textarea value to temp file
if err := os.WriteFile(tmpFile.Name(), []byte(m.textarea.Value()), 0o644); err != nil {
return func() tea.Msg {
return editorFinishedMsg{err: err}
}
}
// Open the editor
cmd := exec.Command(editorCmd, append(editorArgs, tmpFile.Name())...)
return tea.ExecProcess(cmd, func(err error) tea.Msg {
content, readErr := os.ReadFile(tmpFile.Name())
_ = os.Remove(tmpFile.Name())
if readErr != nil {
return editorFinishedMsg{err: readErr}
}
return editorFinishedMsg{content: content, err: err}
})
}
+4 -4
View File
@@ -11,7 +11,7 @@ import (
"time"
"github.com/cline/cli/pkg/common"
_ "github.com/mattn/go-sqlite3"
_ "github.com/glebarez/go-sqlite"
"google.golang.org/grpc/health/grpc_health_v1"
)
@@ -60,7 +60,7 @@ func NewLockManager(clineDir string) (*LockManager, error) {
}
// Database exists - open it normally (no schema creation)
db, err := sql.Open("sqlite3", dbPath)
db, err := sql.Open("sqlite", dbPath)
if err != nil {
// If we can't open existing database, return nil db manager
return &LockManager{dbPath: dbPath, db: nil}, nil
@@ -92,7 +92,7 @@ func (lm *LockManager) ensureConnection() error {
}
// Database exists, try to connect
db, err := sql.Open("sqlite3", lm.dbPath)
db, err := sql.Open("sqlite", lm.dbPath)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
@@ -160,7 +160,7 @@ func (lm *LockManager) RemoveInstanceLock(address string) error {
// HasInstanceAtAddress checks if an instance exists at the given address
func (lm *LockManager) HasInstanceAtAddress(address string) (bool, error) {
if err := lm.ensureConnection(); err != nil {
return false, nil
return false, err
}
query := common.CountInstanceLockSQL
+346 -112
View File
@@ -2,16 +2,33 @@ package cli
import (
"context"
"errors"
"fmt"
"io"
"os"
"slices"
"strconv"
"strings"
"github.com/cline/cli/pkg/cli/config"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/cli/pkg/cli/updater"
"github.com/cline/grpc-go/cline"
"github.com/spf13/cobra"
)
// TaskOptions contains options for creating a task
type TaskOptions struct {
Images []string
Files []string
Mode string
Settings []string
Yolo bool
Address string
Verbose bool
}
func NewTaskCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "task",
@@ -21,12 +38,13 @@ func NewTaskCommand() *cobra.Command {
}
cmd.AddCommand(newTaskNewCommand())
cmd.AddCommand(newTaskCancelCommand())
cmd.AddCommand(newTaskFollowCommand())
cmd.AddCommand(newTaskPauseCommand())
cmd.AddCommand(newTaskChatCommand())
cmd.AddCommand(newTaskSendCommand())
cmd.AddCommand(newTaskViewCommand())
cmd.AddCommand(newTaskListCommand())
cmd.AddCommand(newTaskResumeCommand())
cmd.AddCommand(newTaskOpenCommand())
cmd.AddCommand(newTaskRestoreCommand())
return cmd
}
@@ -47,7 +65,7 @@ func ensureTaskManager(ctx context.Context, address string) error {
instanceAddress = address
} else {
// Ensure default instance exists
if err := ensureDefaultInstance(ctx); err != nil {
if err := global.EnsureDefaultInstance(ctx); err != nil {
return fmt.Errorf("failed to ensure default instance: %w", err)
}
taskManager, err = task.NewManagerForDefault(ctx)
@@ -78,38 +96,14 @@ func ensureInstanceAtAddress(ctx context.Context, address string) error {
return global.Clients.EnsureInstanceAtAddress(ctx, address)
}
// ensureDefaultInstance ensures a default instance exists
func ensureDefaultInstance(ctx context.Context) error {
if global.Clients == nil {
return fmt.Errorf("global clients not initialized")
}
// Check if we have any instances in the registry
registry := global.Clients.GetRegistry()
if registry.GetDefaultInstance() == "" {
// No default instance, start a new one
instance, err := global.Clients.StartNewInstance(ctx)
if err != nil {
return fmt.Errorf("failed to start new default instance: %w", err)
}
// Set the new instance as default
if err := registry.SetDefaultInstance(instance.Address); err != nil {
return fmt.Errorf("failed to set default instance: %w", err)
}
}
return nil
}
func newTaskNewCommand() *cobra.Command {
var (
images []string
files []string
wait bool
workspaces []string
address string
mode string
images []string
files []string
address string
mode string
settings []string
yolo bool
)
cmd := &cobra.Command{
@@ -121,6 +115,12 @@ func newTaskNewCommand() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Check if an instance exists when no address specified
if address == "" && global.Clients.GetRegistry().GetDefaultInstance() == "" {
fmt.Println("No instances available for creating tasks")
return nil
}
// Get content from both args and stdin
prompt, err := getContentFromStdinAndArgs(args)
if err != nil {
@@ -142,22 +142,27 @@ func newTaskNewCommand() *cobra.Command {
if err := taskManager.SetMode(ctx, mode, nil, nil, nil); err != nil {
return fmt.Errorf("failed to set mode: %w", err)
}
fmt.Printf("Mode set to: %s\n", mode)
if global.Config.Verbose {
fmt.Printf("Mode set to: %s\n", mode)
}
}
// Inject yolo_mode_toggled setting if --yolo flag is set
// Will append to the -s settings to be parsed by the settings parser logic.
// If the yoloMode is also set in the settings, this will override that, since it will be set last.
if yolo {
settings = append(settings, "yolo_mode_toggled=true")
}
// Create the task
taskID, err := taskManager.CreateTask(ctx, prompt, images, files, workspaces)
taskID, err := taskManager.CreateTask(ctx, prompt, images, files, settings)
if err != nil {
return fmt.Errorf("failed to create task: %w", err)
}
fmt.Printf("Task created successfully with ID: %s\n", taskID)
fmt.Printf("Using instance: %s\n", taskManager.GetCurrentInstance())
// Wait for completion if requested
if wait {
fmt.Println("Following task conversation...")
return taskManager.FollowConversation(ctx)
if global.Config.Verbose {
fmt.Printf("Task created successfully with ID: %s\n", taskID)
}
return nil
@@ -166,21 +171,22 @@ func newTaskNewCommand() *cobra.Command {
cmd.Flags().StringSliceVarP(&images, "image", "i", nil, "attach image files")
cmd.Flags().StringSliceVarP(&files, "file", "f", nil, "attach files")
cmd.Flags().BoolVar(&wait, "wait", false, "wait for task completion")
cmd.Flags().StringSliceVarP(&workspaces, "workdir", "w", nil, "workdir directory paths")
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
cmd.Flags().StringVarP(&mode, "mode", "m", "", "mode (act|plan)")
cmd.Flags().StringSliceVarP(&settings, "setting", "s", nil, "task settings (key=value format, e.g., -s aws-region=us-west-2 -s mode=act)")
cmd.Flags().BoolVarP(&yolo, "yolo", "y", false, "enable yolo mode (non-interactive)")
cmd.Flags().BoolVar(&yolo, "no-interactive", false, "enable yolo mode (non-interactive)")
return cmd
}
func newTaskCancelCommand() *cobra.Command {
func newTaskPauseCommand() *cobra.Command {
var address string
cmd := &cobra.Command{
Use: "cancel",
Aliases: []string{"c"},
Short: "Cancel the current task",
Use: "pause",
Aliases: []string{"p"},
Short: "Pause the current task",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
@@ -192,7 +198,7 @@ func newTaskCancelCommand() *cobra.Command {
return err
}
fmt.Println("Task cancelled successfully")
fmt.Println("Task paused successfully")
fmt.Printf("Instance: %s\n", taskManager.GetCurrentInstance())
return nil
},
@@ -208,7 +214,9 @@ func newTaskSendCommand() *cobra.Command {
files []string
address string
mode string
approve string
approve bool
deny bool
yolo bool
)
cmd := &cobra.Command{
@@ -220,22 +228,28 @@ func newTaskSendCommand() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Check if an instance exists when no address specified
if address == "" && global.Clients.GetRegistry().GetDefaultInstance() == "" {
fmt.Println("No instances available for sending messages")
return nil
}
// Get content from both args and stdin
message, err := getContentFromStdinAndArgs(args)
if err != nil {
return fmt.Errorf("failed to read message: %w", err)
}
if message == "" && len(images) == 0 && len(files) == 0 && mode == "" && approve == "" {
return fmt.Errorf("content (message, files, images) required unless using --mode or --approve flags")
if message == "" && len(images) == 0 && len(files) == 0 && mode == "" && !approve && !deny {
return fmt.Errorf("content (message, files, images) required unless using --mode, --approve, or --deny flags")
}
if approve != "" && approve != "true" && approve != "false" {
return fmt.Errorf("--approve must be 'true' or 'false'")
if approve && deny {
return fmt.Errorf("cannot use both --approve and --deny flags")
}
if approve != "" && mode != "" {
return fmt.Errorf("cannot use --approve and --mode together")
if (approve || deny) && mode != "" {
return fmt.Errorf("cannot use --approve/--deny and --mode together")
}
// Ensure task manager is initialized
@@ -243,15 +257,38 @@ func newTaskSendCommand() *cobra.Command {
return err
}
sendDisabled, err := taskManager.CheckSendDisabled(ctx)
// Check if we can send a message
err = taskManager.CheckSendEnabled(ctx)
if err != nil {
// Handle specific error cases
if errors.Is(err, task.ErrNoActiveTask) {
fmt.Println("Cannot send message: no active task")
return nil
}
if errors.Is(err, task.ErrTaskBusy) {
fmt.Println("Cannot send message: task is currently busy")
return nil
}
// All other errors are unexpected
return fmt.Errorf("failed to check if message can be sent: %w", err)
}
if sendDisabled {
fmt.Println("Cannot send message: task is currently busy")
return nil
// Process yolo flag and apply settings
if yolo {
settings := []string{"yolo_mode_toggled=true"}
parsedSettings, secrets, err := task.ParseTaskSettings(settings)
if err != nil {
return fmt.Errorf("failed to parse settings: %w", err)
}
configManager, err := config.NewManager(ctx, taskManager.GetCurrentInstance())
if err != nil {
return fmt.Errorf("failed to create config manager: %w", err)
}
if err := configManager.UpdateSettings(ctx, parsedSettings, secrets); err != nil {
return fmt.Errorf("failed to apply settings: %w", err)
}
}
if mode != "" {
@@ -261,7 +298,16 @@ func newTaskSendCommand() *cobra.Command {
fmt.Printf("Mode set to %s and message sent successfully.\n", mode)
} else {
if err := taskManager.SendMessage(ctx, message, images, files, approve); err != nil {
// Convert approve/deny booleans to string
approveStr := ""
if approve {
approveStr = "true"
}
if deny {
approveStr = "false"
}
if err := taskManager.SendMessage(ctx, message, images, files, approveStr); err != nil {
return err
}
fmt.Printf("Message sent successfully.\n")
@@ -276,19 +322,22 @@ func newTaskSendCommand() *cobra.Command {
cmd.Flags().StringSliceVarP(&files, "file", "f", nil, "attach files")
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
cmd.Flags().StringVarP(&mode, "mode", "m", "", "mode (act|plan)")
cmd.Flags().StringVarP(&approve, "approve", "a", "", "approve (true) or deny (false) pending request")
cmd.Flags().BoolVarP(&approve, "approve", "a", false, "approve pending request")
cmd.Flags().BoolVarP(&deny, "deny", "d", false, "deny pending request")
cmd.Flags().BoolVarP(&yolo, "yolo", "y", false, "enable yolo mode (non-interactive)")
cmd.Flags().BoolVar(&yolo, "no-interactive", false, "enable yolo mode (non-interactive)")
return cmd
}
func newTaskFollowCommand() *cobra.Command {
func newTaskChatCommand() *cobra.Command {
var address string
cmd := &cobra.Command{
Use: "follow",
Aliases: []string{"f"},
Short: "Follow current task conversation in real-time",
Long: `Follow the current task conversation, displaying new messages as they arrive in real-time.`,
Use: "chat",
Aliases: []string{"c"},
Short: "Chat with the current task in interactive mode",
Long: `Chat with the current task, displaying messages in real-time with interactive input enabled.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
@@ -297,9 +346,19 @@ func newTaskFollowCommand() *cobra.Command {
return err
}
fmt.Printf("Using instance: %s\n", taskManager.GetCurrentInstance())
// Check if there's an active task before entering follow mode
err := taskManager.CheckSendEnabled(ctx)
if err != nil {
// Handle specific error cases
if errors.Is(err, task.ErrNoActiveTask) {
fmt.Println("No active task found. Use 'cline task new' to create a task first.")
return nil
}
// For other errors (like task busy), we can still enter follow mode
// as the user may want to observe the task
}
return taskManager.FollowConversation(ctx)
return taskManager.FollowConversation(ctx, taskManager.GetCurrentInstance(), true)
},
}
@@ -310,16 +369,16 @@ func newTaskFollowCommand() *cobra.Command {
func newTaskViewCommand() *cobra.Command {
var (
current bool
summary bool
address string
follow bool
followComplete bool
address string
)
cmd := &cobra.Command{
Use: "view",
Aliases: []string{"v"},
Short: "View task conversation",
Long: `Output conversation until next completion, with options for current state or summary only.`,
Long: `Output conversation snapshot by default, or follow with flags.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
@@ -330,26 +389,27 @@ func newTaskViewCommand() *cobra.Command {
fmt.Printf("Using instance: %s\n", taskManager.GetCurrentInstance())
if current {
return taskManager.ShowConversation(ctx)
} else if summary {
return taskManager.GatherFinalSummary(ctx)
if follow {
// Follow conversation forever (non-interactive)
return taskManager.FollowConversation(ctx, taskManager.GetCurrentInstance(), false)
} else if followComplete {
// Follow until completion
return taskManager.FollowConversationUntilCompletion(ctx, task.DefaultFollowOptions())
} else {
return taskManager.FollowConversationUntilCompletion(ctx)
// Default: show snapshot
return taskManager.ShowConversation(ctx)
}
},
}
cmd.Flags().BoolVarP(&current, "current", "c", false, "output current conversation without following")
cmd.Flags().BoolVarP(&summary, "summary", "s", false, "outputs only the completion summary")
cmd.Flags().BoolVarP(&follow, "follow", "f", false, "follow conversation forever")
cmd.Flags().BoolVarP(&followComplete, "follow-complete", "c", false, "follow until completion")
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
func newTaskListCommand() *cobra.Command {
var address string
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
@@ -357,31 +417,27 @@ func newTaskListCommand() *cobra.Command {
Long: `Display recent tasks from task history.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Ensure task manager is initialized
if err := ensureTaskManager(ctx, address); err != nil {
return err
}
fmt.Printf("Using instance: %s\n", taskManager.GetCurrentInstance())
return taskManager.ListTasks(ctx)
// Read directly from disk
return task.ListTasksFromDisk()
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
func newTaskResumeCommand() *cobra.Command {
var address string
func newTaskOpenCommand() *cobra.Command {
var (
address string
mode string
settings []string
yolo bool
)
cmd := &cobra.Command{
Use: "resume <task-id>",
Aliases: []string{"r"},
Short: "Resume a task by ID",
Long: `Resume an existing task by ID.`,
Use: "open <task-id>",
Aliases: []string{"o"},
Short: "Open a task by ID",
Long: `Open an existing task by ID and optionally update settings or mode.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
@@ -394,11 +450,128 @@ func newTaskResumeCommand() *cobra.Command {
fmt.Printf("Using instance: %s\n", taskManager.GetCurrentInstance())
return taskManager.ResumeTask(ctx, taskID)
// Resume the task
if err := taskManager.ResumeTask(ctx, taskID); err != nil {
return err
}
// Apply mode if provided
if mode != "" {
if err := taskManager.SetMode(ctx, mode, nil, nil, nil); err != nil {
return fmt.Errorf("failed to set mode: %w", err)
}
if global.Config.Verbose {
fmt.Printf("Mode set to: %s\n", mode)
}
}
// Process yolo flag and apply settings
if yolo {
settings = append(settings, "yolo_mode_toggled=true")
}
if len(settings) > 0 {
// Parse settings using existing parser
parsedSettings, secrets, err := task.ParseTaskSettings(settings)
if err != nil {
return fmt.Errorf("failed to parse settings: %w", err)
}
// Apply task-specific settings using UpdateTaskSettings RPC
if parsedSettings != nil {
_, err = taskManager.GetClient().State.UpdateTaskSettings(ctx, &cline.UpdateTaskSettingsRequest{
Settings: parsedSettings,
TaskId: &taskID,
})
if err != nil {
return fmt.Errorf("failed to apply task settings: %w", err)
}
if global.Config.Verbose {
fmt.Println("Task-specific settings applied successfully")
}
}
// Handle secrets separately if provided (they must go to global config)
if secrets != nil {
// Secrets are always global, not task-specific
configManager, err := config.NewManager(ctx, taskManager.GetCurrentInstance())
if err != nil {
return fmt.Errorf("failed to create config manager: %w", err)
}
if err := configManager.UpdateSettings(ctx, nil, secrets); err != nil {
return fmt.Errorf("failed to apply secrets: %w", err)
}
if global.Config.Verbose {
fmt.Println("Global secrets applied successfully")
}
}
}
return nil
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
cmd.Flags().StringVarP(&mode, "mode", "m", "", "mode (act|plan)")
cmd.Flags().StringSliceVarP(&settings, "setting", "s", nil, "task settings (key=value format, e.g., -s model=claude)")
cmd.Flags().BoolVarP(&yolo, "yolo", "y", false, "enable yolo mode (non-interactive)")
cmd.Flags().BoolVar(&yolo, "no-interactive", false, "enable yolo mode (non-interactive)")
return cmd
}
func newTaskRestoreCommand() *cobra.Command {
var (
restoreType string
address string
)
cmd := &cobra.Command{
Use: "restore <checkpoint-id>",
Short: "Restore task to a specific checkpoint",
Long: `Restore the current task to a specific checkpoint by checkpoint ID (timestamp) and by type.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
checkpointID := args[0]
// Convert checkpoint ID string to int64
id, err := strconv.ParseInt(checkpointID, 10, 64)
if err != nil {
return fmt.Errorf("invalid checkpoint ID '%s': must be a valid number", checkpointID)
}
validTypes := []string{"task", "workspace", "taskAndWorkspace"}
if !slices.Contains(validTypes, restoreType) {
return fmt.Errorf("invalid restore type '%s': must be one of [task, workspace, taskAndWorkspace]", restoreType)
}
// Ensure task manager is initialized
if err := ensureTaskManager(ctx, address); err != nil {
return err
}
// Validate checkpoint exists before attempting restore
if err := taskManager.ValidateCheckpointExists(ctx, id); err != nil {
return err
}
fmt.Printf("Using instance: %s\n", taskManager.GetCurrentInstance())
fmt.Printf("Restoring to checkpoint %d (type: %s)\n", id, restoreType)
if err := taskManager.RestoreCheckpoint(ctx, id, restoreType); err != nil {
return fmt.Errorf("failed to restore checkpoint: %w", err)
}
fmt.Println("Checkpoint restored successfully")
return nil
},
}
cmd.Flags().StringVarP(&restoreType, "type", "t", "task", "Restore type (task, workspace, taskAndWorkspace)")
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
@@ -419,17 +592,20 @@ func getContentFromStdinAndArgs(args []string) (string, error) {
// Check if data is being piped to stdin
if (stat.Mode() & os.ModeCharDevice) == 0 {
stdinBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return "", fmt.Errorf("failed to read from stdin: %w", err)
}
stdinContent := strings.TrimSpace(string(stdinBytes))
if stdinContent != "" {
if content.Len() > 0 {
content.WriteString(" ")
// Only try to read if there's actually data available
if stat.Size() > 0 {
stdinBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return "", fmt.Errorf("failed to read from stdin: %w", err)
}
stdinContent := strings.TrimSpace(string(stdinBytes))
if stdinContent != "" {
if content.Len() > 0 {
content.WriteString(" ")
}
content.WriteString(stdinContent)
}
content.WriteString(stdinContent)
}
}
@@ -442,3 +618,61 @@ func CleanupTaskManager() {
taskManager.Cleanup()
}
}
// 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 {
// Initialize task manager with the provided instance address
if err := ensureTaskManager(ctx, opts.Address); err != nil {
return err
}
// Set mode to plan by default if not specified
if opts.Mode == "" {
opts.Mode = "plan"
}
// Set mode if provided
if opts.Mode != "" {
if err := taskManager.SetMode(ctx, opts.Mode, nil, nil, nil); err != nil {
return fmt.Errorf("failed to set mode: %w", err)
}
if global.Config.Verbose {
fmt.Printf("Mode set to: %s\n", opts.Mode)
}
}
// Inject yolo_mode_toggled setting if --yolo flag is set
if opts.Yolo {
opts.Settings = append(opts.Settings, "yolo_mode_toggled=true")
}
// Create the task
taskID, err := taskManager.CreateTask(ctx, prompt, opts.Images, opts.Files, opts.Settings)
if err != nil {
return fmt.Errorf("failed to create task: %w", err)
}
if global.Config.Verbose {
fmt.Printf("Task created successfully with ID: %s\n\n", taskID)
}
// Check for updates in background after task is created
updater.CheckAndUpdate(opts.Verbose)
// If yolo mode is enabled, follow until completion (non-interactive)
// Otherwise, follow in interactive mode
if opts.Yolo {
// Skip active task check since we just created the task
return taskManager.FollowConversationUntilCompletion(ctx, task.FollowOptions{
SkipActiveTaskCheck: true,
})
} else {
return taskManager.FollowConversation(ctx, taskManager.GetCurrentInstance(), true)
}
}
+15
View File
@@ -0,0 +1,15 @@
package task
// FollowOptions contains options for following a conversation
type FollowOptions struct {
// SkipActiveTaskCheck skips the check for an active task
// This is useful when following a task that was just created to avoid race conditions
SkipActiveTaskCheck bool
}
// DefaultFollowOptions returns the default options for following a conversation
func DefaultFollowOptions() FollowOptions {
return FollowOptions{
SkipActiveTaskCheck: false,
}
}
+72
View File
@@ -0,0 +1,72 @@
package task
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/types"
"github.com/cline/grpc-go/cline"
)
// ListTasksFromDisk reads task history directly from disk
func ListTasksFromDisk() error {
// Get the task history file path
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
filePath := filepath.Join(homeDir, ".cline", "data", "state", "taskHistory.json")
// Read the file
data, err := os.ReadFile(filePath)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("No task history found.")
return nil
}
return fmt.Errorf("failed to read task history: %w", err)
}
// Parse JSON into intermediate struct
var historyItems []types.HistoryItem
if err := json.Unmarshal(data, &historyItems); err != nil {
return fmt.Errorf("failed to parse task history: %w", err)
}
if len(historyItems) == 0 {
fmt.Println("No task history found.")
return nil
}
// Sort by timestamp ascending (oldest first, newest last)
sort.Slice(historyItems, func(i, j int) bool {
return historyItems[i].Ts < historyItems[j].Ts
})
// Convert to protobuf TaskItem format for rendering
tasks := make([]*cline.TaskItem, len(historyItems))
for i, item := range historyItems {
tasks[i] = &cline.TaskItem{
Id: item.Id,
Task: item.Task,
Ts: item.Ts,
IsFavorited: item.IsFavorited,
Size: item.Size,
TotalCost: item.TotalCost,
TokensIn: item.TokensIn,
TokensOut: item.TokensOut,
CacheWrites: item.CacheWrites,
CacheReads: item.CacheReads,
}
}
// Use existing renderer
renderer := display.NewRenderer(global.Config.OutputFormat)
return renderer.RenderTaskList(tasks)
}
+577
View File
@@ -0,0 +1,577 @@
package task
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/output"
"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
}
// 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),
}
}
// Start begins monitoring for input opportunities
func (ih *InputHandler) Start(ctx context.Context, errChan chan error) {
ih.mu.Lock()
ih.isRunning = true
ih.mu.Unlock()
defer func() {
ih.mu.Lock()
ih.isRunning = false
ih.mu.Unlock()
ih.pollTicker.Stop()
if ih.program != nil {
ih.program.Quit()
}
}()
for {
select {
case <-ctx.Done():
return
case <-ih.pollTicker.C:
// First check if approval is needed
needsApproval, approvalMsg, err := ih.manager.CheckNeedsApproval(ctx)
if err != nil {
if global.Config.Verbose {
output.Printf("\nDebug: CheckNeedsApproval error: %v\n", err)
}
continue
}
if needsApproval {
ih.coordinator.SetInputAllowed(true)
// Show approval prompt
approved, feedback, err := ih.promptForApproval(ctx, approvalMsg)
if err != nil {
// Check if the error is due to interrupt (Ctrl+C) or context cancellation
if errors.Is(err, context.Canceled) || ctx.Err() != nil {
// User pressed Ctrl+C - cancel context to exit FollowConversation
ih.cancelFunc()
return
}
if global.Config.Verbose {
output.Printf("\nDebug: Approval prompt error: %v\n", err)
}
continue
}
ih.coordinator.SetInputAllowed(false)
// Send approval response
approveStr := "false"
if approved {
approveStr = "true"
}
if err := ih.manager.SendMessage(ctx, feedback, nil, nil, approveStr); err != nil {
output.Printf("\nError sending approval: %v\n", err)
continue
}
if global.Config.Verbose {
output.Printf("\nDebug: Approval sent (approved=%s, feedback=%q)\n", approveStr, feedback)
}
// Give the system a moment to process before re-polling
time.Sleep(1 * time.Second)
continue
}
// Check if we can send a regular message
err = ih.manager.CheckSendEnabled(ctx)
if err != nil {
// Handle specific error cases
if errors.Is(err, ErrNoActiveTask) {
// No active task - don't show input prompt
ih.coordinator.SetInputAllowed(false)
continue
}
if errors.Is(err, ErrTaskBusy) {
// Task is busy - don't show input prompt
ih.coordinator.SetInputAllowed(false)
continue
}
// Unexpected error
if global.Config.Verbose {
output.Printf("\nDebug: CheckSendEnabled error: %v\n", err)
}
continue
}
// If we reach here, we can send a message
ih.coordinator.SetInputAllowed(true)
// Show prompt and get input
message, shouldSend, err := ih.promptForInput(ctx)
if err != nil {
// Check if the error is due to interrupt (Ctrl+C) or context cancellation
if errors.Is(err, context.Canceled) || ctx.Err() != nil {
// User pressed Ctrl+C - cancel context to exit FollowConversation
ih.cancelFunc()
return
}
if global.Config.Verbose {
output.Printf("\nDebug: Input prompt error: %v\n", err)
}
continue
}
ih.coordinator.SetInputAllowed(false)
if shouldSend {
// Check for mode switch commands first
newMode, remainingMessage, isModeSwitch := ih.parseModeSwitch(message)
if isModeSwitch {
// Create styles for mode switch messages (respect global color profile)
actStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true)
planStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("3")).Bold(true)
if remainingMessage != "" {
// Switching with a message - behavior differs by mode
if newMode == "act" {
// Act mode: can send mode + message in one call
if err := ih.manager.SetMode(ctx, newMode, &remainingMessage, nil, nil); err != nil {
output.Printf("\nError switching to act mode with message: %v\n", err)
continue
}
output.Printf("\n%s\n", actStyle.Render("Switched to act mode"))
} else {
// Plan mode: must switch first, then send message separately
if err := ih.manager.SetMode(ctx, newMode, nil, nil, nil); err != nil {
output.Printf("\nError switching to plan mode: %v\n", err)
continue
}
output.Printf("\n%s\n", planStyle.Render("Switched to plan mode"))
// Now send the message separately
time.Sleep(500 * time.Millisecond) // Give mode switch time to process
if err := ih.manager.SendMessage(ctx, remainingMessage, nil, nil, ""); err != nil {
output.Printf("\nError sending message after mode switch: %v\n", err)
continue
}
}
} else {
// Just switch mode, no message
if err := ih.manager.SetMode(ctx, newMode, nil, nil, nil); err != nil {
output.Printf("\nError switching to %s mode: %v\n", newMode, err)
continue
}
// Color based on mode
if newMode == "act" {
output.Printf("\n%s\n", actStyle.Render("Switched to act mode"))
} else {
output.Printf("\n%s\n", planStyle.Render("Switched to plan mode"))
}
}
// Mode switch handled, continue to next poll
time.Sleep(1 * time.Second)
continue
}
// Handle special commands
if handled := ih.handleSpecialCommand(ctx, message); handled {
continue
}
// Send the message
if err := ih.manager.SendMessage(ctx, message, nil, nil, ""); err != nil {
output.Printf("\nError sending message: %v\n", err)
continue
}
if global.Config.Verbose {
output.Printf("\nDebug: Message sent successfully\n")
}
// Give the system a moment to process before re-polling
time.Sleep(1 * time.Second)
}
}
}
}
// determineAutoApprovalAction determines which auto-approval action to enable based on the ask type
func determineAutoApprovalAction(msg *types.ClineMessage) (string, error) {
switch types.AskType(msg.Ask) {
case types.AskTypeTool:
// Parse tool message to determine if it's a read or edit operation
var toolMsg types.ToolMessage
if err := json.Unmarshal([]byte(msg.Text), &toolMsg); err != nil {
return "", fmt.Errorf("failed to parse tool message: %w", err)
}
// Determine action based on tool type
switch types.ToolType(toolMsg.Tool) {
case types.ToolTypeReadFile,
types.ToolTypeListFilesTopLevel,
types.ToolTypeListFilesRecursive,
types.ToolTypeListCodeDefinitionNames,
types.ToolTypeSearchFiles,
types.ToolTypeWebFetch:
return "read_files", nil
case types.ToolTypeEditedExistingFile,
types.ToolTypeNewFileCreated:
return "edit_files", nil
case types.ToolTypeFileDeleted:
return "apply_patch", nil
default:
return "", fmt.Errorf("unsupported tool type: %s", toolMsg.Tool)
}
case types.AskTypeCommand:
return "execute_all_commands", nil
case types.AskTypeBrowserActionLaunch:
return "use_browser", nil
case types.AskTypeUseMcpServer:
return "use_mcp", nil
default:
return "", fmt.Errorf("unsupported ask type: %s", msg.Ask)
}
}
// promptForInput displays an interactive prompt and waits for user input
func (ih *InputHandler) promptForInput(ctx context.Context) (string, bool, error) {
currentMode := ih.manager.GetCurrentMode()
model := output.NewInputModel(
output.InputTypeMessage,
"Cline is ready for your message...",
"/plan or /act to switch modes\nctrl+e to open editor",
currentMode,
)
return ih.runInputProgram(ctx, model)
}
// promptForApproval displays an approval prompt for tool/command requests
func (ih *InputHandler) promptForApproval(ctx context.Context, msg *types.ClineMessage) (bool, string, error) {
// Store the approval message for later use in determining auto-approval action
ih.approvalMessage = msg
model := output.NewInputModel(
output.InputTypeApproval,
"Let Cline use this tool?",
"",
ih.manager.GetCurrentMode(),
)
message, shouldSend, err := ih.runInputProgram(ctx, model)
if err != nil {
return false, "", err
}
if !shouldSend {
return false, "", nil
}
// The approval and feedback are handled via the model state
return ih.feedbackApproved, message, nil
}
// runInputProgram runs the bubbletea program and waits for result
func (ih *InputHandler) runInputProgram(ctx context.Context, model output.InputModel) (string, bool, error) {
ih.mu.Lock()
// Create the program with custom update wrapper
wrappedModel := &inputProgramWrapper{
model: &model,
resultChan: ih.resultChan,
cancelChan: ih.cancelChan,
handler: ih,
}
ih.program = tea.NewProgram(wrappedModel)
ih.programDoneChan = make(chan struct{})
ih.ctx = ctx
// Set up coordinator references
output.SetProgram(ih.program)
output.SetInputModel(wrappedModel.model)
output.SetRestartCallback(ih.restartProgram)
output.SetInputVisible(true)
ih.programRunning = true
ih.mu.Unlock()
// Run program in goroutine
programErrChan := make(chan error, 1)
go func() {
if _, err := ih.program.Run(); err != nil {
programErrChan <- err
}
// Signal that program is done
close(ih.programDoneChan)
}()
// Wait for result, cancellation, or context done
select {
case <-ctx.Done():
ih.mu.Lock()
output.SetInputVisible(false)
if ih.program != nil {
ih.program.Quit()
}
ih.programRunning = false
ih.mu.Unlock()
return "", false, ctx.Err()
case <-ih.cancelChan:
ih.mu.Lock()
output.SetInputVisible(false)
ih.programRunning = false
ih.mu.Unlock()
return "", false, context.Canceled
case err := <-programErrChan:
ih.mu.Lock()
output.SetInputVisible(false)
ih.programRunning = false
ih.mu.Unlock()
return "", false, err
case result := <-ih.resultChan:
ih.mu.Lock()
output.SetInputVisible(false)
ih.programRunning = false
ih.mu.Unlock()
// Handle different input types
switch result.InputType {
case output.InputTypeMessage:
if result.Value == "" {
return "", false, nil
}
return result.Value, true, nil
case output.InputTypeApproval:
if result.NeedsFeedback {
// Need to collect feedback - will be handled by model state change
return "", false, nil
}
// Check if NoAskAgain was selected
if result.NoAskAgain && result.Approved && ih.approvalMessage != nil {
// Determine which auto-approval action to enable
action, err := determineAutoApprovalAction(ih.approvalMessage)
if err != nil {
output.Printf("\nWarning: Could not determine auto-approval action: %v\n", err)
} else {
// Enable the auto-approval action
if err := ih.manager.UpdateTaskAutoApprovalAction(ctx, action); err != nil {
output.Printf("\nWarning: Could not update auto-approval: %v\n", err)
} else {
output.Printf("\nAuto-approval enabled for %s\n", action)
}
}
}
// Store approval state for when feedback comes back
ih.feedbackApproval = false
ih.feedbackApproved = result.Approved
return "", true, nil
case output.InputTypeFeedback:
// This came from approval flow
ih.feedbackApproval = true
ih.feedbackApproved = result.Approved // Use the approval decision from the feedback
return result.Value, true, nil
}
return "", false, nil
}
}
// inputProgramWrapper wraps the InputModel to handle message routing
type inputProgramWrapper struct {
model *output.InputModel
resultChan chan output.InputSubmitMsg
cancelChan chan struct{}
handler *InputHandler
}
func (w *inputProgramWrapper) Init() tea.Cmd {
return w.model.Init()
}
func (w *inputProgramWrapper) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case output.InputSubmitMsg:
// Handle input submission - clear the screen before quitting
w.resultChan <- msg
clearCodes := w.model.ClearScreen()
if clearCodes != "" {
fmt.Print(clearCodes)
}
return w, tea.Quit
case output.InputCancelMsg:
// Handle cancellation - clear the screen before quitting
w.cancelChan <- struct{}{}
clearCodes := w.model.ClearScreen()
if clearCodes != "" {
fmt.Print(clearCodes)
}
return w, tea.Quit
case output.ChangeInputTypeMsg:
// Change input type (approval -> feedback)
_, cmd := w.model.Update(msg)
return w, cmd
}
// Forward to wrapped model
_, cmd := w.model.Update(msg)
return w, cmd
}
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)) {
case "/cancel":
ih.manager.GetRenderer().RenderTaskCancelled()
if err := ih.manager.CancelTask(ctx); err != nil {
output.Printf("Error cancelling task: %v\n", err)
} else {
output.Println("Task cancelled successfully")
}
return true
case "/exit", "/quit":
output.Println("\nExiting follow mode...")
return true
default:
return false
}
}
// Stop stops the input handler
func (ih *InputHandler) Stop() {
ih.mu.Lock()
defer ih.mu.Unlock()
if ih.pollTicker != nil {
ih.pollTicker.Stop()
}
if ih.program != nil && ih.programRunning {
ih.program.Quit()
}
ih.isRunning = false
}
// IsRunning returns whether the input handler is currently running
func (ih *InputHandler) IsRunning() bool {
ih.mu.RLock()
defer ih.mu.RUnlock()
return ih.isRunning
}
// restartProgram restarts the Bubble Tea program with preserved state
func (ih *InputHandler) restartProgram(savedModel *output.InputModel) {
ih.mu.Lock()
// Wait for old program to actually quit
if ih.programDoneChan != nil {
select {
case <-ih.programDoneChan:
// Program quit successfully
case <-time.After(100 * time.Millisecond):
// Timeout - continue anyway
}
}
// Create new wrapper with the saved model
wrappedModel := &inputProgramWrapper{
model: savedModel,
resultChan: ih.resultChan,
cancelChan: ih.cancelChan,
handler: ih,
}
// Start new program
ih.program = tea.NewProgram(wrappedModel)
ih.programDoneChan = make(chan struct{})
// Update coordinator references
output.SetProgram(ih.program)
output.SetInputModel(savedModel)
output.SetInputVisible(true)
ih.programRunning = true
ih.mu.Unlock()
// Run in goroutine
go func() {
if _, err := ih.program.Run(); err != nil {
// Log error if needed
if global.Config.Verbose {
output.Printf("\nDebug: Program restart error: %v\n", err)
}
}
close(ih.programDoneChan)
}()
}
+471 -120
View File
@@ -3,8 +3,12 @@ package task
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/cline/cli/pkg/cli/display"
@@ -15,6 +19,12 @@ import (
"github.com/cline/grpc-go/cline"
)
// Sentinel errors for CheckSendEnabled
var (
ErrNoActiveTask = fmt.Errorf("no active task")
ErrTaskBusy = fmt.Errorf("task is currently busy")
)
// Manager handles task execution and message display
type Manager struct {
mu sync.RWMutex
@@ -22,14 +32,21 @@ type Manager struct {
clientAddress string
state *types.ConversationState
renderer *display.Renderer
toolRenderer *display.ToolRenderer
systemRenderer *display.SystemMessageRenderer
streamingDisplay *display.StreamingDisplay
handlerRegistry *handlers.HandlerRegistry
isStreamingMode bool
isInteractive bool
currentMode string // "plan" or "act"
}
// NewManager creates a new task manager
func NewManager(client *client.ClineClient) *Manager {
state := types.NewConversationState()
renderer := display.NewRenderer()
renderer := display.NewRenderer(global.Config.OutputFormat)
toolRenderer := display.NewToolRenderer(renderer.GetMdRenderer(), global.Config.OutputFormat)
systemRenderer := display.NewSystemMessageRenderer(renderer, renderer.GetMdRenderer(), global.Config.OutputFormat)
streamingDisplay := display.NewStreamingDisplay(state, renderer)
// Create handler registry and register handlers
@@ -42,8 +59,11 @@ func NewManager(client *client.ClineClient) *Manager {
clientAddress: "", // Will be set when client is provided
state: state,
renderer: renderer,
toolRenderer: toolRenderer,
systemRenderer: systemRenderer,
streamingDisplay: streamingDisplay,
handlerRegistry: registry,
currentMode: "plan", // Default mode
}
}
@@ -106,7 +126,7 @@ func (m *Manager) GetCurrentInstance() string {
}
// CreateTask creates a new task
func (m *Manager) CreateTask(ctx context.Context, prompt string, images, files []string, workspacePaths []string) (string, error) {
func (m *Manager) CreateTask(ctx context.Context, prompt string, images, files []string, settingsFlags []string) (string, error) {
m.mu.Lock()
defer m.mu.Unlock()
@@ -118,8 +138,8 @@ func (m *Manager) CreateTask(ctx context.Context, prompt string, images, files [
if len(images) > 0 {
m.renderer.RenderDebug("Images: %v", images)
}
if len(workspacePaths) > 0 {
m.renderer.RenderDebug("Workspaces: %v", workspacePaths)
if len(settingsFlags) > 0 {
m.renderer.RenderDebug("Settings: %v", settingsFlags)
}
}
@@ -128,11 +148,22 @@ func (m *Manager) CreateTask(ctx context.Context, prompt string, images, files [
return "", fmt.Errorf("failed to cancel existing task: %w", err)
}
// Parse task settings if provided
var taskSettings *cline.Settings
if len(settingsFlags) > 0 {
var err error
taskSettings, _, err = ParseTaskSettings(settingsFlags)
if err != nil {
return "", fmt.Errorf("failed to parse task settings: %w", err)
}
}
// Create task request
req := &cline.NewTaskRequest{
Text: prompt,
Images: images,
Files: files,
Text: prompt,
Images: images,
Files: files,
TaskSettings: taskSettings,
}
resp, err := m.client.Task.NewTask(ctx, req)
@@ -184,26 +215,64 @@ func (m *Manager) cancelExistingTaskIfNeeded(ctx context.Context) error {
fmt.Println("Cancelled existing task to start new one")
}
}
}
}
return nil
}
// CheckSendDisabled determines if we can send a message to the current task
// We duplicate the logic from buttonConfig::getButtonConfig
func (m *Manager) CheckSendDisabled(ctx context.Context) (bool, error) {
// ValidateCheckpointExists checks if a checkpoint ID is valid
func (m *Manager) ValidateCheckpointExists(ctx context.Context, checkpointID int64) error {
// Get current state
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return false, fmt.Errorf("failed to get latest state: %w", err)
return fmt.Errorf("failed to get state: %w", err)
}
// Extract messages
messages, err := m.extractMessagesFromState(state.StateJson)
if err != nil {
return fmt.Errorf("failed to extract messages: %w", err)
}
// Find and validate the checkpoint message
for _, msg := range messages {
if msg.Timestamp == checkpointID {
if msg.Say != string(types.SayTypeCheckpointCreated) {
return fmt.Errorf("timestamp %d is not a checkpoint (type: %s)", checkpointID, msg.Type)
}
return nil // Valid checkpoint
}
}
return fmt.Errorf("checkpoint ID %d not found in task history", checkpointID)
}
// CheckSendEnabled checks if we can send a message to the current task
// Returns nil if sending is allowed, or an error indicating why it's not allowed
// We duplicate the logic from buttonConfig::getButtonConfig
func (m *Manager) CheckSendEnabled(ctx context.Context) error {
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to get latest state: %w", err)
}
var stateData types.ExtensionState
if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
return fmt.Errorf("failed to parse state: %w", err)
}
// Check if there is an active task
if stateData.CurrentTaskItem == nil {
return ErrNoActiveTask
}
messages, err := m.extractMessagesFromState(state.StateJson)
if err != nil {
return false, fmt.Errorf("failed to extract messages: %w", err)
return fmt.Errorf("failed to extract messages: %w", err)
}
if len(messages) == 0 {
return false, nil
return nil
}
// Use final message to perform validation
@@ -211,9 +280,8 @@ func (m *Manager) CheckSendDisabled(ctx context.Context) (bool, error) {
// Error types which we allow sending on
errorTypes := []string{
string(types.AskTypeAPIReqFailed), // "api_req_failed"
string(types.AskTypeMistakeLimitReached), // "mistake_limit_reached"
string(types.AskTypeAutoApprovalMaxReached), // "auto_approval_max_req_reached"
string(types.AskTypeAPIReqFailed), // "api_req_failed"
string(types.AskTypeMistakeLimitReached), // "mistake_limit_reached"
}
isError := false
@@ -233,15 +301,25 @@ func (m *Manager) CheckSendDisabled(ctx context.Context) (bool, error) {
if global.Config.Verbose {
m.renderer.RenderDebug("Send disabled: task is streaming and non-error")
}
return true, nil
return ErrTaskBusy
}
// All ask messages allow sending
// All ask messages allow sending, EXCEPT command_output
if lastMessage.Type == types.MessageTypeAsk {
// Special case: command_output means command is actively streaming
// In the CLI, we don't want to show input during streaming output (too messy)
// The webview can show "Proceed While Running" button, but CLI should wait
if lastMessage.Ask == string(types.AskTypeCommandOutput) {
if global.Config.Verbose {
m.renderer.RenderDebug("Send disabled: command output is streaming")
}
return ErrTaskBusy
}
if global.Config.Verbose {
m.renderer.RenderDebug("Send enabled: ask message")
}
return false, nil
return nil
}
// Technically unnecessary but implements getButtonConfig 1-1
@@ -249,14 +327,58 @@ func (m *Manager) CheckSendDisabled(ctx context.Context) (bool, error) {
if global.Config.Verbose {
m.renderer.RenderDebug("Send disabled: API request is active")
}
return true, nil
return ErrTaskBusy
}
if global.Config.Verbose {
m.renderer.RenderDebug("Send disabled: default fallback")
}
return true, nil
return ErrTaskBusy
}
// CheckNeedsApproval determines if the current task is waiting for approval
// Returns (needsApproval, lastMessage, error)
func (m *Manager) CheckNeedsApproval(ctx context.Context) (bool, *types.ClineMessage, error) {
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return false, nil, fmt.Errorf("failed to get latest state: %w", err)
}
messages, err := m.extractMessagesFromState(state.StateJson)
if err != nil {
return false, nil, fmt.Errorf("failed to extract messages: %w", err)
}
if len(messages) == 0 {
return false, nil, nil
}
// Use final message to check if approval is needed
lastMessage := messages[len(messages)-1]
// Only check non-partial ask messages
if lastMessage.Partial {
return false, nil, nil
}
// Check if this is an approval-required ask type
if lastMessage.Type == types.MessageTypeAsk {
approvalTypes := []string{
string(types.AskTypeTool),
string(types.AskTypeCommand),
string(types.AskTypeBrowserActionLaunch),
string(types.AskTypeUseMcpServer),
}
for _, approvalType := range approvalTypes {
if lastMessage.Ask == approvalType {
return true, lastMessage, nil
}
}
}
return false, nil, nil
}
// SendMessage sends a followup message to the current task
@@ -449,6 +571,27 @@ func (m *Manager) ResumeTask(ctx context.Context, taskID string) error {
return nil
}
// RestoreCheckpoint restores the task to a specific checkpoint
func (m *Manager) RestoreCheckpoint(ctx context.Context, checkpointID int64, restoreType string) error {
if global.Config.Verbose {
m.renderer.RenderDebug("Restoring checkpoint: %d (type: %s)", checkpointID, restoreType)
}
// Create the checkpoint restore request
req := &cline.CheckpointRestoreRequest{
Metadata: &cline.Metadata{},
Number: checkpointID,
RestoreType: restoreType,
}
_, err := m.client.Checkpoints.CheckpointRestore(ctx, req)
if err != nil {
return fmt.Errorf("failed to restore checkpoint %d: %w", checkpointID, err)
}
return nil
}
// CancelTask cancels the current task
func (m *Manager) CancelTask(ctx context.Context) error {
m.mu.Lock()
@@ -462,60 +605,24 @@ func (m *Manager) CancelTask(ctx context.Context) error {
return nil
}
// ListTasks retrieves and displays task history
func (m *Manager) ListTasks(ctx context.Context) error {
m.mu.RLock()
defer m.mu.RUnlock()
req := &cline.GetTaskHistoryRequest{
FavoritesOnly: false,
SearchQuery: "",
SortBy: "oldest",
CurrentWorkspaceOnly: false,
}
resp, err := m.client.Task.GetTaskHistory(ctx, req)
if err != nil {
return fmt.Errorf("failed to get task history: %w", err)
}
if len(resp.Tasks) == 0 {
fmt.Println("No task history found.")
return nil
}
return m.renderer.RenderTaskList(resp.Tasks)
}
// GatherFinalSummary attempts to gather the latest completion_result output and display it
func (m *Manager) GatherFinalSummary(ctx context.Context) error {
m.mu.RLock()
defer m.mu.RUnlock()
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to get state: %w", err)
}
messages, err := m.extractMessagesFromState(state.StateJson)
if err != nil {
return fmt.Errorf("failed to extract messages: %w", err)
}
for i := len(messages) - 1; i >= 0; i-- {
msg := messages[i]
// Check if this is a completion result SAY message
if msg.IsSay() && msg.Say == string(types.SayTypeCompletionResult) {
return m.displayMessage(msg, false, false, i)
}
}
return nil
}
// ShowConversation displays the current conversation
func (m *Manager) ShowConversation(ctx context.Context) error {
// Check if there's an active task before showing conversation
err := m.CheckSendEnabled(ctx)
if err != nil {
// Handle specific error cases
if errors.Is(err, ErrNoActiveTask) {
fmt.Println("No active task found. Use 'cline task new' to create a task first.")
return nil
}
// For other errors (like task busy), we can still show the conversation
}
// Disable streaming mode for static view
m.mu.Lock()
m.isStreamingMode = false
m.mu.Unlock()
m.mu.RLock()
defer m.mu.RUnlock()
@@ -536,16 +643,35 @@ func (m *Manager) ShowConversation(ctx context.Context) error {
return nil
}
// Display messages
for i, msg := range messages {
if msg.Partial {
continue
}
m.displayMessage(msg, false, false, i)
}
return nil
}
func (m *Manager) FollowConversation(ctx context.Context) error {
fmt.Println("Following task conversation... (Press Ctrl+C to exit)")
func (m *Manager) FollowConversation(ctx context.Context, instanceAddress string, interactive bool) error {
// Enable streaming mode
m.mu.Lock()
m.isStreamingMode = true
m.isInteractive = interactive
m.mu.Unlock()
if global.Config.OutputFormat != "plain" {
markdown := fmt.Sprintf("*Using instance: %s*\n*Press Ctrl+C to exit*", instanceAddress)
rendered := m.renderer.RenderMarkdown(markdown)
fmt.Printf("%s", rendered)
} else {
fmt.Printf("Using instance: %s\n", instanceAddress)
if interactive {
fmt.Println("Following task conversation in interactive mode... (Press Ctrl+C to exit)")
} else {
fmt.Println("Following task conversation... (Press Ctrl+C to exit)")
}
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
@@ -561,21 +687,64 @@ func (m *Manager) FollowConversation(ctx context.Context) error {
}
coordinator.SetConversationTurnStartIndex(totalMessageCount)
fmt.Println("\n--- Live updates ---")
// Start both streams concurrently
errChan := make(chan error, 2)
errChan := make(chan error, 3)
if global.Config.OutputFormat == "json" {
go m.handleStateStream(ctx, coordinator, errChan, nil)
} else {
go m.handleStateStream(ctx, coordinator, errChan, nil)
go m.handlePartialMessageStream(ctx, coordinator, errChan)
// Start input handler if interactive mode is enabled
if interactive {
inputHandler := NewInputHandler(m, coordinator, cancel)
go inputHandler.Start(ctx, errChan)
}
}
// Handle Ctrl+C signals
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
defer signal.Stop(sigChan) // Clean up signal handler when goroutine exits
for {
select {
case <-ctx.Done():
return
case <-sigChan:
if interactive {
// Interactive mode (task chat)
// Check if input is currently being shown
if coordinator.IsInputAllowed() {
// Input form is showing - huh will handle the signal via ErrUserAborted
// Do nothing here, let the input handler deal with it
} else {
// Streaming mode - cancel the task and stay in follow mode
m.renderer.RenderTaskCancelled()
if err := m.CancelTask(context.Background()); err != nil {
fmt.Printf("Error cancelling task: %v\n", err)
}
// Don't cancel main context - stay in follow mode
}
} else {
// Non-interactive mode (task view --follow)
// Just exit without canceling the task
cancel()
return // Exit the loop after canceling in non-interactive mode
}
}
}
}()
// Wait for either stream to error or context cancellation
select {
case <-ctx.Done():
// Check if this was a user-initiated cancellation (Ctrl+C)
// Return nil for clean exit instead of context.Canceled error
if ctx.Err() == context.Canceled {
return nil
}
return ctx.Err()
case err := <-errChan:
cancel()
@@ -584,8 +753,27 @@ func (m *Manager) FollowConversation(ctx context.Context) error {
}
// FollowConversationUntilCompletion streams conversation updates until task completion
func (m *Manager) FollowConversationUntilCompletion(ctx context.Context) error {
fmt.Println("Streaming conversation until completion... (Press Ctrl+C to exit)")
func (m *Manager) FollowConversationUntilCompletion(ctx context.Context, opts FollowOptions) error {
// Check if there's an active task before entering follow mode
// Skip this check if we just created a task (to avoid race condition where task isn't active yet)
if !opts.SkipActiveTaskCheck {
err := m.CheckSendEnabled(ctx)
if err != nil {
if errors.Is(err, ErrNoActiveTask) {
fmt.Println("No task is currently running.")
return nil
}
// For other errors (like task busy), we can still enter follow mode
// as the user may want to observe the task
}
}
// Enable streaming mode
m.mu.Lock()
m.isStreamingMode = true
m.mu.Unlock()
fmt.Println("Following task conversation until completion... (Press Ctrl+C to exit)")
ctx, cancel := context.WithCancel(ctx)
defer cancel()
@@ -593,10 +781,10 @@ func (m *Manager) FollowConversationUntilCompletion(ctx context.Context) error {
// Create stream coordinator
coordinator := NewStreamCoordinator()
// Get current message count without displaying history
totalMessageCount, err := m.getCurrentMessageCount(ctx)
// Load history first
totalMessageCount, err := m.loadAndDisplayRecentHistory(ctx)
if err != nil {
m.renderer.RenderDebug("Warning: Failed to get current message count: %v", err)
m.renderer.RenderDebug("Warning: Failed to load conversation history: %v", err)
totalMessageCount = 0
}
coordinator.SetConversationTurnStartIndex(totalMessageCount)
@@ -726,6 +914,9 @@ func (m *Manager) processStateUpdateJsonMode(stateUpdate *cline.State, coordinat
// processStateUpdate processes state updates and supports logic for handling task competion markers
func (m *Manager) processStateUpdate(stateUpdate *cline.State, coordinator *StreamCoordinator, completionChan chan bool) error {
// Update current mode from state
m.updateMode(stateUpdate.StateJson)
messages, err := m.extractMessagesFromState(stateUpdate.StateJson)
if err != nil {
return err
@@ -749,27 +940,96 @@ func (m *Manager) processStateUpdate(stateUpdate *cline.State, coordinator *Stre
foundCompletion = true
}
// Currently handling a subset of message types for displaying
switch {
case msg.Say == string(types.SayTypeUserFeedback):
if !coordinator.IsProcessedInCurrentTurn("user_msg") {
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println()
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn("user_msg")
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Say == string(types.SayTypeCommand):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println()
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Say == string(types.SayTypeCommandOutput):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Say == string(types.SayTypeBrowserActionLaunch):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println()
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Say == string(types.SayTypeMcpServerRequestStarted):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println()
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Say == string(types.SayTypeCheckpointCreated):
if !coordinator.IsProcessedInCurrentTurn("checkpoint") {
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println()
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn("checkpoint")
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Say == string(types.SayTypeAPIReqStarted):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
apiInfo := types.APIRequestInfo{Cost: -1}
if err := json.Unmarshal([]byte(msg.Text), &apiInfo); err == nil && apiInfo.Cost >= 0 {
fmt.Println() // adds a separator between cline message and usage message
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println() // adds a separator between cline message and usage message
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
coordinator.CompleteTurn(len(messages))
displayedUsage = true
}
}
case msg.Ask == string(types.AskTypeCommandOutput):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
m.displayMessage(msg, false, false, i)
coordinator.CompleteTurn(len(messages))
displayedUsage = true
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Ask == string(types.AskTypePlanModeRespond):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
// Non-streaming mode: render normally when message is complete
if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Type == types.MessageTypeAsk:
msgKey := fmt.Sprintf("%d", msg.Timestamp)
// Only render if not already handled by partial stream
if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
}
}
@@ -790,6 +1050,10 @@ func (m *Manager) handlePartialMessageStream(ctx context.Context, coordinator *S
return
}
defer func() {
m.streamingDisplay.FreezeActiveSegment()
}()
for {
select {
case <-ctx.Done():
@@ -810,7 +1074,7 @@ func (m *Manager) handlePartialMessageStream(ctx context.Context, coordinator *S
msg.Type, msg.Partial, len(msg.Text))
// Handle the message with streaming support for de-dupping
if err := m.handleStreamingMessage(msg); err != nil {
if err := m.handleStreamingMessage(msg, coordinator); err != nil {
m.renderer.RenderDebug("Error handling streaming message: %v", err)
}
}
@@ -818,7 +1082,7 @@ func (m *Manager) handlePartialMessageStream(ctx context.Context, coordinator *S
}
// handleStreamingMessage handles a streaming message
func (m *Manager) handleStreamingMessage(msg *types.ClineMessage) error {
func (m *Manager) handleStreamingMessage(msg *types.ClineMessage, coordinator *StreamCoordinator) error {
// Debug: Always log what we're processing
m.renderer.RenderDebug("Processing message: timestamp=%d, partial=%v, type=%s, text_preview=%s",
msg.Timestamp, msg.Partial, msg.Type, m.truncateText(msg.Text, 50))
@@ -827,7 +1091,7 @@ func (m *Manager) handleStreamingMessage(msg *types.ClineMessage) error {
if err := m.streamingDisplay.HandlePartialMessage(msg); err != nil {
m.renderer.RenderDebug("Streaming display failed, using fallback: %v", err)
// Fallback to regular display
return m.displayMessage(msg, true, false, -1)
m.displayMessage(msg, true, false, -1)
}
return nil
@@ -846,12 +1110,21 @@ func (m *Manager) displayMessage(msg *types.ClineMessage, isLast, isPartial bool
if global.Config.OutputFormat == "json" {
return m.outputMessageAsJSON(msg)
} else {
m.mu.RLock()
isStreaming := m.isStreamingMode
isInteractive := m.isInteractive
m.mu.RUnlock()
dc := &handlers.DisplayContext{
State: m.state,
Renderer: m.renderer,
IsLast: isLast,
IsPartial: isPartial,
MessageIndex: messageIndex,
State: m.state,
Renderer: m.renderer,
ToolRenderer: m.toolRenderer,
SystemRenderer: m.systemRenderer,
IsLast: isLast,
IsPartial: isPartial,
MessageIndex: messageIndex,
IsStreamingMode: isStreaming,
IsInteractive: isInteractive,
}
return m.handlerRegistry.Handle(msg, dc)
@@ -869,21 +1142,6 @@ func (m *Manager) outputMessageAsJSON(msg *types.ClineMessage) error {
return nil
}
// getCurrentMessageCount gets the current message count without displaying messages
func (m *Manager) getCurrentMessageCount(ctx context.Context) (int, error) {
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return 0, fmt.Errorf("failed to get state: %w", err)
}
messages, err := m.extractMessagesFromState(state.StateJson)
if err != nil {
return 0, fmt.Errorf("failed to extract messages: %w", err)
}
return len(messages), nil
}
// loadAndDisplayRecentHistory loads and displays recent conversation history and returns the total number of existing messages
func (m *Manager) loadAndDisplayRecentHistory(ctx context.Context) (int, error) {
// Get the latest state which contains messages
@@ -910,16 +1168,30 @@ func (m *Manager) loadAndDisplayRecentHistory(ctx context.Context) (int, error)
if totalMessages > maxHistoryMessages {
startIndex = totalMessages - maxHistoryMessages
fmt.Printf("--- Conversation history (%d of %d messages) ---\n", maxHistoryMessages, totalMessages)
if global.Config.OutputFormat != "plain" {
markdown := fmt.Sprintf("*Conversation history (%d of %d messages)*", maxHistoryMessages, totalMessages)
rendered := m.renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n\n", rendered)
} else {
fmt.Printf("--- Conversation history (%d of %d messages) ---\n", maxHistoryMessages, totalMessages)
}
} else {
fmt.Printf("--- Conversation history (%d messages) ---\n", totalMessages)
if global.Config.OutputFormat != "plain" {
markdown := fmt.Sprintf("*Conversation history (%d messages)*", totalMessages)
rendered := m.renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n\n", rendered)
} else {
fmt.Printf("--- Conversation history (%d messages) ---\n", totalMessages)
}
}
// Display recent messages
for i := startIndex; i < len(messages); i++ {
msg := messages[i]
// Display the message
if msg.Partial {
continue
}
m.displayMessage(msg, false, false, i)
}
@@ -937,6 +1209,85 @@ func (m *Manager) GetState() *types.ConversationState {
return m.state
}
// GetClient returns the underlying ClineClient for direct gRPC calls
func (m *Manager) GetClient() *client.ClineClient {
m.mu.RLock()
defer m.mu.RUnlock()
return m.client
}
// GetRenderer returns the renderer for formatting output
func (m *Manager) GetRenderer() *display.Renderer {
return m.renderer
}
// GetCurrentMode returns the current plan/act mode
func (m *Manager) GetCurrentMode() string {
m.mu.RLock()
defer m.mu.RUnlock()
return m.currentMode
}
// extractModeFromState extracts the current mode from state JSON
func (m *Manager) extractModeFromState(stateJson string) string {
var rawState map[string]interface{}
if err := json.Unmarshal([]byte(stateJson), &rawState); err != nil {
return m.currentMode // Return current mode if parsing fails
}
if mode, ok := rawState["mode"].(string); ok {
return mode
}
return m.currentMode // Return current mode if not found in state
}
// updateMode updates the current mode from state
func (m *Manager) updateMode(stateJson string) {
mode := m.extractModeFromState(stateJson)
m.mu.Lock()
m.currentMode = mode
m.mu.Unlock()
}
// UpdateTaskAutoApprovalAction enables a specific auto-approval action for the current task
func (m *Manager) UpdateTaskAutoApprovalAction(ctx context.Context, actionKey string) error {
boolPtr := func(b bool) *bool { return &b }
settings := &cline.Settings{
AutoApprovalSettings: &cline.AutoApprovalSettings{
Actions: &cline.AutoApprovalActions{},
},
}
// Set the specific action to true based on actionKey
truePtr := boolPtr(true)
switch actionKey {
case "read_files":
settings.AutoApprovalSettings.Actions.ReadFiles = truePtr
case "edit_files":
settings.AutoApprovalSettings.Actions.EditFiles = truePtr
case "execute_all_commands":
settings.AutoApprovalSettings.Actions.ExecuteAllCommands = truePtr
case "use_browser":
settings.AutoApprovalSettings.Actions.UseBrowser = truePtr
case "use_mcp":
settings.AutoApprovalSettings.Actions.UseMcp = truePtr
default:
return fmt.Errorf("unknown auto-approval action: %s", actionKey)
}
_, err := m.client.State.UpdateTaskSettings(ctx, &cline.UpdateTaskSettingsRequest{
Settings: settings,
})
if err != nil {
return fmt.Errorf("failed to update task settings: %w", err)
}
return nil
}
// Cleanup cleans up resources
func (m *Manager) Cleanup() {
// Clean up streaming display resources if needed
+754
View File
@@ -0,0 +1,754 @@
package task
import (
"fmt"
"strconv"
"strings"
"github.com/cline/grpc-go/cline"
)
func ParseTaskSettings(settingsFlags []string) (*cline.Settings, *cline.Secrets, error) {
if len(settingsFlags) == 0 {
return nil, nil, nil
}
settings := &cline.Settings{}
secrets := &cline.Secrets{}
nestedSettings := make(map[string]map[string]string)
for _, flag := range settingsFlags {
// Parse key=value
parts := strings.SplitN(flag, "=", 2)
if len(parts) != 2 {
return nil, nil, fmt.Errorf("invalid setting format '%s': expected key=value", flag)
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
// Convert kebab-case to snake_case
key = kebabToSnake(key)
// Check if this is a nested setting (contains a dot)
if strings.Contains(key, ".") {
dotParts := strings.SplitN(key, ".", 2)
parentField := dotParts[0]
childField := dotParts[1]
if nestedSettings[parentField] == nil {
nestedSettings[parentField] = make(map[string]string)
}
nestedSettings[parentField][childField] = value
} else {
// Check if it's a secret field first, then settings field
if err := setSecretField(secrets, key, value); err == nil {
// Successfully set as secret, continue
continue
}
// Not a secret, try as a settings field
if err := setSimpleField(settings, key, value); err != nil {
return nil, nil, fmt.Errorf("error setting field '%s': %w", key, err)
}
}
}
// Process nested settings
for parentField, childFields := range nestedSettings {
if err := setNestedField(settings, parentField, childFields); err != nil {
return nil, nil, fmt.Errorf("error setting nested field '%s': %w", parentField, err)
}
}
return settings, secrets, nil
}
// kebabToSnake converts kebab-case to snake_case
func kebabToSnake(s string) string {
return strings.ReplaceAll(s, "-", "_")
}
// Pointer helper functions for optional protobuf fields
func strPtr(s string) *string { return &s }
func boolPtr(b bool) *bool { return &b }
func int32Ptr(i int32) *int32 { return &i }
func int64Ptr(i int64) *int64 { return &i }
func float64Ptr(f float64) *float64 { return &f }
// setSimpleField sets a simple (non-nested) field on Settings
func setSimpleField(settings *cline.Settings, key, value string) error {
switch key {
// String fields
case "aws_region":
settings.AwsRegion = strPtr(value)
case "aws_bedrock_endpoint":
settings.AwsBedrockEndpoint = strPtr(value)
case "aws_profile":
settings.AwsProfile = strPtr(value)
case "aws_authentication":
settings.AwsAuthentication = strPtr(value)
case "vertex_project_id":
settings.VertexProjectId = strPtr(value)
case "vertex_region":
settings.VertexRegion = strPtr(value)
case "requesty_base_url":
settings.RequestyBaseUrl = strPtr(value)
case "open_ai_base_url":
settings.OpenAiBaseUrl = strPtr(value)
case "ollama_base_url":
settings.OllamaBaseUrl = strPtr(value)
case "ollama_api_options_ctx_num":
settings.OllamaApiOptionsCtxNum = strPtr(value)
case "lm_studio_base_url":
settings.LmStudioBaseUrl = strPtr(value)
case "lm_studio_max_tokens":
settings.LmStudioMaxTokens = strPtr(value)
case "anthropic_base_url":
settings.AnthropicBaseUrl = strPtr(value)
case "gemini_base_url":
settings.GeminiBaseUrl = strPtr(value)
case "azure_api_version":
settings.AzureApiVersion = strPtr(value)
case "open_router_provider_sorting":
settings.OpenRouterProviderSorting = strPtr(value)
case "lite_llm_base_url":
settings.LiteLlmBaseUrl = strPtr(value)
case "qwen_api_line":
settings.QwenApiLine = strPtr(value)
case "moonshot_api_line":
settings.MoonshotApiLine = strPtr(value)
case "zai_api_line":
settings.ZaiApiLine = strPtr(value)
case "telemetry_setting":
settings.TelemetrySetting = strPtr(value)
case "asksage_api_url":
settings.AsksageApiUrl = strPtr(value)
case "default_terminal_profile":
settings.DefaultTerminalProfile = strPtr(value)
case "sap_ai_core_token_url":
settings.SapAiCoreTokenUrl = strPtr(value)
case "sap_ai_core_base_url":
settings.SapAiCoreBaseUrl = strPtr(value)
case "sap_ai_resource_group":
settings.SapAiResourceGroup = strPtr(value)
case "claude_code_path":
settings.ClaudeCodePath = strPtr(value)
case "qwen_code_oauth_path":
settings.QwenCodeOauthPath = strPtr(value)
case "preferred_language":
settings.PreferredLanguage = strPtr(value)
case "custom_prompt":
settings.CustomPrompt = strPtr(value)
case "dify_base_url":
settings.DifyBaseUrl = strPtr(value)
case "oca_base_url":
settings.OcaBaseUrl = strPtr(value)
case "plan_mode_api_model_id":
settings.PlanModeApiModelId = strPtr(value)
case "plan_mode_reasoning_effort":
settings.PlanModeReasoningEffort = strPtr(value)
case "plan_mode_aws_bedrock_custom_model_base_id":
settings.PlanModeAwsBedrockCustomModelBaseId = strPtr(value)
case "plan_mode_open_router_model_id":
settings.PlanModeOpenRouterModelId = strPtr(value)
case "plan_mode_open_ai_model_id":
settings.PlanModeOpenAiModelId = strPtr(value)
case "plan_mode_ollama_model_id":
settings.PlanModeOllamaModelId = strPtr(value)
case "plan_mode_lm_studio_model_id":
settings.PlanModeLmStudioModelId = strPtr(value)
case "plan_mode_lite_llm_model_id":
settings.PlanModeLiteLlmModelId = strPtr(value)
case "plan_mode_requesty_model_id":
settings.PlanModeRequestyModelId = strPtr(value)
case "plan_mode_together_model_id":
settings.PlanModeTogetherModelId = strPtr(value)
case "plan_mode_fireworks_model_id":
settings.PlanModeFireworksModelId = strPtr(value)
case "plan_mode_sap_ai_core_model_id":
settings.PlanModeSapAiCoreModelId = strPtr(value)
case "plan_mode_sap_ai_core_deployment_id":
settings.PlanModeSapAiCoreDeploymentId = strPtr(value)
case "plan_mode_groq_model_id":
settings.PlanModeGroqModelId = strPtr(value)
case "plan_mode_baseten_model_id":
settings.PlanModeBasetenModelId = strPtr(value)
case "plan_mode_hugging_face_model_id":
settings.PlanModeHuggingFaceModelId = strPtr(value)
case "plan_mode_huawei_cloud_maas_model_id":
settings.PlanModeHuaweiCloudMaasModelId = strPtr(value)
case "plan_mode_oca_model_id":
settings.PlanModeOcaModelId = strPtr(value)
case "act_mode_api_model_id":
settings.ActModeApiModelId = strPtr(value)
case "act_mode_reasoning_effort":
settings.ActModeReasoningEffort = strPtr(value)
case "act_mode_aws_bedrock_custom_model_base_id":
settings.ActModeAwsBedrockCustomModelBaseId = strPtr(value)
case "act_mode_open_router_model_id":
settings.ActModeOpenRouterModelId = strPtr(value)
case "act_mode_open_ai_model_id":
settings.ActModeOpenAiModelId = strPtr(value)
case "act_mode_ollama_model_id":
settings.ActModeOllamaModelId = strPtr(value)
case "act_mode_lm_studio_model_id":
settings.ActModeLmStudioModelId = strPtr(value)
case "act_mode_lite_llm_model_id":
settings.ActModeLiteLlmModelId = strPtr(value)
case "act_mode_requesty_model_id":
settings.ActModeRequestyModelId = strPtr(value)
case "act_mode_together_model_id":
settings.ActModeTogetherModelId = strPtr(value)
case "act_mode_fireworks_model_id":
settings.ActModeFireworksModelId = strPtr(value)
case "act_mode_sap_ai_core_model_id":
settings.ActModeSapAiCoreModelId = strPtr(value)
case "act_mode_sap_ai_core_deployment_id":
settings.ActModeSapAiCoreDeploymentId = strPtr(value)
case "act_mode_groq_model_id":
settings.ActModeGroqModelId = strPtr(value)
case "act_mode_baseten_model_id":
settings.ActModeBasetenModelId = strPtr(value)
case "act_mode_hugging_face_model_id":
settings.ActModeHuggingFaceModelId = strPtr(value)
case "act_mode_huawei_cloud_maas_model_id":
settings.ActModeHuaweiCloudMaasModelId = strPtr(value)
case "act_mode_oca_model_id":
settings.ActModeOcaModelId = strPtr(value)
// Boolean fields
case "aws_use_cross_region_inference":
val, err := parseBool(value)
if err != nil {
return err
}
settings.AwsUseCrossRegionInference = boolPtr(val)
case "aws_bedrock_use_prompt_cache":
val, err := parseBool(value)
if err != nil {
return err
}
settings.AwsBedrockUsePromptCache = boolPtr(val)
case "aws_use_profile":
val, err := parseBool(value)
if err != nil {
return err
}
settings.AwsUseProfile = boolPtr(val)
case "lite_llm_use_prompt_cache":
val, err := parseBool(value)
if err != nil {
return err
}
settings.LiteLlmUsePromptCache = boolPtr(val)
case "plan_act_separate_models_setting":
val, err := parseBool(value)
if err != nil {
return err
}
settings.PlanActSeparateModelsSetting = boolPtr(val)
case "enable_checkpoints_setting":
val, err := parseBool(value)
if err != nil {
return err
}
settings.EnableCheckpointsSetting = boolPtr(val)
case "sap_ai_core_use_orchestration_mode":
val, err := parseBool(value)
if err != nil {
return err
}
settings.SapAiCoreUseOrchestrationMode = boolPtr(val)
case "strict_plan_mode_enabled":
val, err := parseBool(value)
if err != nil {
return err
}
settings.StrictPlanModeEnabled = boolPtr(val)
case "yolo_mode_toggled":
val, err := parseBool(value)
if err != nil {
return err
}
settings.YoloModeToggled = boolPtr(val)
case "use_auto_condense":
val, err := parseBool(value)
if err != nil {
return err
}
settings.UseAutoCondense = boolPtr(val)
case "plan_mode_aws_bedrock_custom_selected":
val, err := parseBool(value)
if err != nil {
return err
}
settings.PlanModeAwsBedrockCustomSelected = boolPtr(val)
case "act_mode_aws_bedrock_custom_selected":
val, err := parseBool(value)
if err != nil {
return err
}
settings.ActModeAwsBedrockCustomSelected = boolPtr(val)
// Integer fields
case "request_timeout_ms":
val, err := parseInt32(value)
if err != nil {
return err
}
settings.RequestTimeoutMs = int32Ptr(val)
case "shell_integration_timeout":
val, err := parseInt32(value)
if err != nil {
return err
}
settings.ShellIntegrationTimeout = int32Ptr(val)
case "terminal_output_line_limit":
val, err := parseInt32(value)
if err != nil {
return err
}
settings.TerminalOutputLineLimit = int32Ptr(val)
case "max_consecutive_mistakes":
val, err := parseInt32(value)
if err != nil {
return err
}
settings.MaxConsecutiveMistakes = int32Ptr(val)
case "fireworks_model_max_completion_tokens":
val, err := parseInt32(value)
if err != nil {
return err
}
settings.FireworksModelMaxCompletionTokens = int32Ptr(val)
case "fireworks_model_max_tokens":
val, err := parseInt32(value)
if err != nil {
return err
}
settings.FireworksModelMaxTokens = int32Ptr(val)
// Int64 fields
case "plan_mode_thinking_budget_tokens":
val, err := parseInt64(value)
if err != nil {
return err
}
settings.PlanModeThinkingBudgetTokens = int64Ptr(val)
case "act_mode_thinking_budget_tokens":
val, err := parseInt64(value)
if err != nil {
return err
}
settings.ActModeThinkingBudgetTokens = int64Ptr(val)
// Double fields
case "auto_condense_threshold":
val, err := parseFloat64(value)
if err != nil {
return err
}
settings.AutoCondenseThreshold = float64Ptr(val)
// Enum fields
// Note: We can use &val directly for enums because the parser functions return a new local variable.
// This is different from using &value (the loop variable), which would cause all fields to share
// the same memory address.
case "openai_reasoning_effort":
val, err := parseOpenaiReasoningEffort(value)
if err != nil {
return err
}
settings.OpenaiReasoningEffort = &val
case "mode":
val, err := parsePlanActMode(value)
if err != nil {
return err
}
settings.Mode = &val
case "plan_mode_api_provider":
val, err := parseApiProvider(value)
if err != nil {
return err
}
settings.PlanModeApiProvider = &val
case "act_mode_api_provider":
val, err := parseApiProvider(value)
if err != nil {
return err
}
settings.ActModeApiProvider = &val
default:
return fmt.Errorf("unsupported field '%s'", key)
}
return nil
}
// setNestedField sets a nested field on Settings
// Currently supports: auto_approval_settings, browser_settings
func setNestedField(settings *cline.Settings, parentField string, childFields map[string]string) error {
switch parentField {
case "auto_approval_settings":
if settings.AutoApprovalSettings == nil {
settings.AutoApprovalSettings = &cline.AutoApprovalSettings{}
}
return setAutoApprovalSettings(settings.AutoApprovalSettings, childFields)
case "browser_settings":
if settings.BrowserSettings == nil {
settings.BrowserSettings = &cline.BrowserSettings{}
}
return setBrowserSettings(settings.BrowserSettings, childFields)
default:
return fmt.Errorf("unsupported nested field '%s' (complex nested types are not supported via -s flags)", parentField)
}
}
// setAutoApprovalSettings sets fields on AutoApprovalSettings
func setAutoApprovalSettings(settings *cline.AutoApprovalSettings, fields map[string]string) error {
for key, value := range fields {
switch key {
case "enable_notifications":
val, err := parseBool(value)
if err != nil {
return err
}
settings.EnableNotifications = boolPtr(val)
case "actions":
return fmt.Errorf("auto_approval_settings.actions requires nested dot notation (e.g., auto-approval-settings.actions.read-files=true)")
default:
// Check if this is an action field (actions.*)
if strings.HasPrefix(key, "actions.") {
actionField := strings.TrimPrefix(key, "actions.")
if settings.Actions == nil {
settings.Actions = &cline.AutoApprovalActions{}
}
if err := setAutoApprovalAction(settings.Actions, actionField, value); err != nil {
return err
}
// Continue processing other fields
} else {
return fmt.Errorf("unsupported auto_approval_settings field '%s'", key)
}
}
}
return nil
}
// setAutoApprovalAction sets fields on AutoApprovalActions
func setAutoApprovalAction(actions *cline.AutoApprovalActions, key, value string) error {
val, err := parseBool(value)
if err != nil {
return err
}
switch key {
case "read_files":
actions.ReadFiles = boolPtr(val)
case "read_files_externally":
actions.ReadFilesExternally = boolPtr(val)
case "edit_files":
actions.EditFiles = boolPtr(val)
case "edit_files_externally":
actions.EditFilesExternally = boolPtr(val)
case "execute_safe_commands":
actions.ExecuteSafeCommands = boolPtr(val)
case "execute_all_commands":
actions.ExecuteAllCommands = boolPtr(val)
case "use_browser":
actions.UseBrowser = boolPtr(val)
case "use_mcp":
actions.UseMcp = boolPtr(val)
default:
return fmt.Errorf("unsupported auto_approval_actions field '%s'", key)
}
return nil
}
// setBrowserSettings sets fields on BrowserSettings
func setBrowserSettings(settings *cline.BrowserSettings, fields map[string]string) error {
for key, value := range fields {
switch key {
case "viewport_width":
val, err := parseInt32(value)
if err != nil {
return err
}
if settings.Viewport == nil {
settings.Viewport = &cline.Viewport{}
}
settings.Viewport.Width = val
case "viewport_height":
val, err := parseInt32(value)
if err != nil {
return err
}
if settings.Viewport == nil {
settings.Viewport = &cline.Viewport{}
}
settings.Viewport.Height = val
case "remote_browser_host":
settings.RemoteBrowserHost = strPtr(value)
case "remote_browser_enabled":
val, err := parseBool(value)
if err != nil {
return err
}
settings.RemoteBrowserEnabled = boolPtr(val)
case "chrome_executable_path":
settings.ChromeExecutablePath = strPtr(value)
case "disable_tool_use":
val, err := parseBool(value)
if err != nil {
return err
}
settings.DisableToolUse = boolPtr(val)
case "custom_args":
settings.CustomArgs = strPtr(value)
default:
return fmt.Errorf("unsupported browser_settings field '%s'", key)
}
}
return nil
}
// Type parsing helpers
func parseBool(value string) (bool, error) {
lower := strings.ToLower(value)
switch lower {
case "true", "t", "yes", "y", "1":
return true, nil
case "false", "f", "no", "n", "0":
return false, nil
default:
return false, fmt.Errorf("invalid boolean value '%s': expected true/false", value)
}
}
func parseInt32(value string) (int32, error) {
val, err := strconv.ParseInt(value, 10, 32)
if err != nil {
return 0, fmt.Errorf("invalid integer value '%s': %w", value, err)
}
return int32(val), nil
}
func parseInt64(value string) (int64, error) {
val, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid integer value '%s': %w", value, err)
}
return val, nil
}
func parseFloat64(value string) (float64, error) {
val, err := strconv.ParseFloat(value, 64)
if err != nil {
return 0, fmt.Errorf("invalid float value '%s': %w", value, err)
}
return val, nil
}
// Enum parsing helpers
func parseOpenaiReasoningEffort(value string) (cline.OpenaiReasoningEffort, error) {
lower := strings.ToLower(value)
switch lower {
case "low":
return cline.OpenaiReasoningEffort_LOW, nil
case "medium":
return cline.OpenaiReasoningEffort_MEDIUM, nil
case "high":
return cline.OpenaiReasoningEffort_HIGH, nil
default:
return cline.OpenaiReasoningEffort_LOW, fmt.Errorf("invalid openai_reasoning_effort '%s': expected low/medium/high", value)
}
}
func parsePlanActMode(value string) (cline.PlanActMode, error) {
lower := strings.ToLower(value)
switch lower {
case "plan":
return cline.PlanActMode_PLAN, nil
case "act":
return cline.PlanActMode_ACT, nil
default:
return cline.PlanActMode_ACT, fmt.Errorf("invalid mode '%s': expected plan/act", value)
}
}
func parseApiProvider(value string) (cline.ApiProvider, error) {
lower := strings.ToLower(value)
switch lower {
case "anthropic":
return cline.ApiProvider_ANTHROPIC, nil
case "openrouter":
return cline.ApiProvider_OPENROUTER, nil
case "bedrock":
return cline.ApiProvider_BEDROCK, nil
case "vertex":
return cline.ApiProvider_VERTEX, nil
case "openai":
return cline.ApiProvider_OPENAI, nil
case "ollama":
return cline.ApiProvider_OLLAMA, nil
case "lmstudio":
return cline.ApiProvider_LMSTUDIO, nil
case "gemini":
return cline.ApiProvider_GEMINI, nil
case "openai_native":
return cline.ApiProvider_OPENAI_NATIVE, nil
case "requesty":
return cline.ApiProvider_REQUESTY, nil
case "together":
return cline.ApiProvider_TOGETHER, nil
case "deepseek":
return cline.ApiProvider_DEEPSEEK, nil
case "qwen":
return cline.ApiProvider_QWEN, nil
case "doubao":
return cline.ApiProvider_DOUBAO, nil
case "mistral":
return cline.ApiProvider_MISTRAL, nil
case "vscode_lm":
return cline.ApiProvider_VSCODE_LM, nil
case "cline":
return cline.ApiProvider_CLINE, nil
case "litellm":
return cline.ApiProvider_LITELLM, nil
case "nebius":
return cline.ApiProvider_NEBIUS, nil
case "fireworks":
return cline.ApiProvider_FIREWORKS, nil
case "asksage":
return cline.ApiProvider_ASKSAGE, nil
case "xai", "grok":
return cline.ApiProvider_XAI, nil
case "sambanova":
return cline.ApiProvider_SAMBANOVA, nil
case "cerebras":
return cline.ApiProvider_CEREBRAS, nil
case "groq":
return cline.ApiProvider_GROQ, nil
case "sapaicore", "sap_ai_core":
return cline.ApiProvider_SAPAICORE, nil
case "claude_code":
return cline.ApiProvider_CLAUDE_CODE, nil
case "moonshot":
return cline.ApiProvider_MOONSHOT, nil
case "huggingface":
return cline.ApiProvider_HUGGINGFACE, nil
case "huawei_cloud_maas":
return cline.ApiProvider_HUAWEI_CLOUD_MAAS, nil
case "baseten":
return cline.ApiProvider_BASETEN, nil
case "zai":
return cline.ApiProvider_ZAI, nil
case "vercel_ai_gateway":
return cline.ApiProvider_VERCEL_AI_GATEWAY, nil
case "qwen_code":
return cline.ApiProvider_QWEN_CODE, nil
case "dify":
return cline.ApiProvider_DIFY, nil
case "oca":
return cline.ApiProvider_OCA, nil
case "minimax":
return cline.ApiProvider_MINIMAX, nil
default:
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("invalid api_provider '%s'", value)
}
}
// setSecretField sets a secret field on Secrets
// All secret fields are optional strings
// Returns nil if field was successfully set, error otherwise
func setSecretField(secrets *cline.Secrets, key, value string) error {
switch key {
case "api_key":
secrets.ApiKey = strPtr(value)
case "open_router_api_key":
secrets.OpenRouterApiKey = strPtr(value)
case "aws_access_key":
secrets.AwsAccessKey = strPtr(value)
case "aws_secret_key":
secrets.AwsSecretKey = strPtr(value)
case "aws_session_token":
secrets.AwsSessionToken = strPtr(value)
case "aws_bedrock_api_key":
secrets.AwsBedrockApiKey = strPtr(value)
case "open_ai_api_key":
secrets.OpenAiApiKey = strPtr(value)
case "gemini_api_key":
secrets.GeminiApiKey = strPtr(value)
case "open_ai_native_api_key":
secrets.OpenAiNativeApiKey = strPtr(value)
case "ollama_api_key":
secrets.OllamaApiKey = strPtr(value)
case "deep_seek_api_key":
secrets.DeepSeekApiKey = strPtr(value)
case "requesty_api_key":
secrets.RequestyApiKey = strPtr(value)
case "together_api_key":
secrets.TogetherApiKey = strPtr(value)
case "fireworks_api_key":
secrets.FireworksApiKey = strPtr(value)
case "qwen_api_key":
secrets.QwenApiKey = strPtr(value)
case "doubao_api_key":
secrets.DoubaoApiKey = strPtr(value)
case "mistral_api_key":
secrets.MistralApiKey = strPtr(value)
case "lite_llm_api_key":
secrets.LiteLlmApiKey = strPtr(value)
case "auth_nonce":
secrets.AuthNonce = strPtr(value)
case "asksage_api_key":
secrets.AsksageApiKey = strPtr(value)
case "xai_api_key":
secrets.XaiApiKey = strPtr(value)
case "moonshot_api_key":
secrets.MoonshotApiKey = strPtr(value)
case "zai_api_key":
secrets.ZaiApiKey = strPtr(value)
case "hugging_face_api_key":
secrets.HuggingFaceApiKey = strPtr(value)
case "nebius_api_key":
secrets.NebiusApiKey = strPtr(value)
case "sambanova_api_key":
secrets.SambanovaApiKey = strPtr(value)
case "cerebras_api_key":
secrets.CerebrasApiKey = strPtr(value)
case "sap_ai_core_client_id":
secrets.SapAiCoreClientId = strPtr(value)
case "sap_ai_core_client_secret":
secrets.SapAiCoreClientSecret = strPtr(value)
case "groq_api_key":
secrets.GroqApiKey = strPtr(value)
case "huawei_cloud_maas_api_key":
secrets.HuaweiCloudMaasApiKey = strPtr(value)
case "baseten_api_key":
secrets.BasetenApiKey = strPtr(value)
case "dify_api_key":
secrets.DifyApiKey = strPtr(value)
case "oca_api_key":
secrets.OcaApiKey = strPtr(value)
case "oca_refresh_token":
secrets.OcaRefreshToken = strPtr(value)
case "hicap_api_key":
secrets.HicapApiKey = strPtr(value)
default:
return fmt.Errorf("unsupported secret field '%s'", key)
}
return nil
}
// Note: message types not supported via -s flags:
// - OpenRouterModelInfo, OpenAiCompatibleModelInfo, LiteLLMModelInfo, OcaModelInfo
// - LanguageModelChatSelector
// - DictationSettings
// - FocusChainSettings
+21 -2
View File
@@ -1,9 +1,13 @@
package task
import "sync"
// StreamCoordinator manages coordination between SubscribeToState and SubscribeToPartialMessage streams
type StreamCoordinator struct {
conversationTurnStartIndex int // First message index of current turn
processedInCurrentTurn map[string]bool // What we've handled in THIS turn
inputAllowed bool // Whether user input is currently allowed
mu sync.RWMutex // Protects inputAllowed
}
// NewStreamCoordinator creates a new stream coordinator
@@ -34,8 +38,23 @@ func (sc *StreamCoordinator) IsProcessedInCurrentTurn(key string) bool {
return sc.processedInCurrentTurn[key]
}
// CompleteTurn resets the coordinator for the next conversation turn
// CompleteTurn updates the start index for the next batch of messages
// Note: Does NOT reset the processed map - that persists across state updates
func (sc *StreamCoordinator) CompleteTurn(totalMessages int) {
sc.conversationTurnStartIndex = totalMessages
sc.processedInCurrentTurn = make(map[string]bool)
// Don't reset processedInCurrentTurn - it should persist across state updates
}
// SetInputAllowed sets whether user input is currently allowed
func (sc *StreamCoordinator) SetInputAllowed(allowed bool) {
sc.mu.Lock()
defer sc.mu.Unlock()
sc.inputAllowed = allowed
}
// IsInputAllowed returns whether user input is currently allowed
func (sc *StreamCoordinator) IsInputAllowed() bool {
sc.mu.RLock()
defer sc.mu.RUnlock()
return sc.inputAllowed
}
+695
View File
@@ -0,0 +1,695 @@
package terminal
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/global"
)
// KeyboardProtocol manages enhanced keyboard protocol support for detecting
// modified keys like shift+enter across all major terminals.
type KeyboardProtocol struct {
enabled bool
mu sync.Mutex
}
var globalProtocol = &KeyboardProtocol{}
// EnableEnhancedKeyboard enables enhanced keyboard protocols to support
// shift+enter and other modified keys across all major terminals:
// - VS Code integrated terminal
// - iTerm2
// - Terminal.app
// - Ghostty
// - Kitty
// - WezTerm
// - Alacritty
// - foot
// - xterm
//
// This function is safe to call multiple times and handles cleanup automatically.
// It enables both modifyOtherKeys (xterm protocol) and Kitty keyboard protocol
// for maximum compatibility.
func EnableEnhancedKeyboard() {
globalProtocol.mu.Lock()
defer globalProtocol.mu.Unlock()
if globalProtocol.enabled {
return // Already enabled
}
// Check if we're in a TTY (not piped/redirected)
if !isatty(os.Stdin.Fd()) {
return
}
// Enable modifyOtherKeys mode 2
// This tells xterm-compatible terminals (VS Code, iTerm2, Terminal.app, etc.)
// to send escape sequences for modified keys including shift+enter
// Format: CSI > 4 ; 2 m
// - Mode 2 enables for ALL keys including well-known ones
fmt.Print("\x1b[>4;2m")
// Also enable Kitty keyboard protocol for terminals that support it
// This is a more modern protocol supported by Kitty, Ghostty, WezTerm, foot, etc.
// Format: CSI = <flags> u where flags=1 means "disambiguate escape codes"
// This makes shift+enter distinguishable from plain enter
fmt.Print("\x1b[=1u")
globalProtocol.enabled = true
}
// DisableEnhancedKeyboard restores the terminal to its default keyboard mode.
// This should be called on program exit to be a good citizen.
func DisableEnhancedKeyboard() {
globalProtocol.mu.Lock()
defer globalProtocol.mu.Unlock()
if !globalProtocol.enabled {
return
}
// Disable modifyOtherKeys (restore to mode 0)
fmt.Print("\x1b[>4;0m")
// Disable Kitty keyboard protocol
fmt.Print("\x1b[<u")
globalProtocol.enabled = false
}
// isatty checks if a file descriptor is a terminal
func isatty(fd uintptr) bool {
// Use the standard library's terminal package
// This works across all platforms (Unix, Windows, etc.)
fileInfo, err := os.Stdin.Stat()
if err != nil {
return false
}
return (fileInfo.Mode() & os.ModeCharDevice) != 0
}
// SetupKeyboard detects the current terminal and configures keybindings if needed.
// Runs in background and doesn't block. Prints status when configs are modified.
func SetupKeyboard() {
go func() {
renderer := display.NewRenderer(global.Config.OutputFormat)
setupKeyboardInternal(renderer)
}()
}
// SetupKeyboardSync is the synchronous version used by doctor command.
// Blocks until complete and prints status for all terminals.
func SetupKeyboardSync() {
renderer := display.NewRenderer(global.Config.OutputFormat)
setupKeyboardInternal(renderer)
}
func setupKeyboardInternal(renderer *display.Renderer) {
terminalName := DetectTerminal()
switch terminalName {
case "vscode":
// VS Code and Cursor use the same TERM_PROGRAM value
modified, path := SetupVSCodeKeybindings()
if modified {
fmt.Printf("%s VS Code %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
} else if path != "" {
fmt.Printf("%s\n", renderer.Dim("✓ VS Code shift+enter already configured"))
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
}
modified, path = SetupCursorKeybindings()
if modified {
fmt.Printf("%s Cursor %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
} else if path != "" {
fmt.Printf("%s\n", renderer.Dim("✓ Cursor shift+enter already configured"))
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
}
case "ghostty":
modified, path := SetupGhosttyKeybindings()
if modified {
fmt.Printf("%s Ghostty %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
fmt.Printf("%s\n", renderer.Dim(" Fully restart Ghostty (quit all windows) for changes to take effect"))
} else if path != "" {
fmt.Printf("%s\n", renderer.Dim("✓ Ghostty shift+enter already configured"))
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
}
case "wezterm":
modified, path := SetupWezTermKeybindings()
if modified {
fmt.Printf("%s WezTerm %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
} else if path != "" {
fmt.Printf("%s\n", renderer.Dim("✓ WezTerm shift+enter already configured"))
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
}
case "alacritty":
modified, path := SetupAlacrittyKeybindings()
if modified {
fmt.Printf("%s Alacritty %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
} else if path != "" {
fmt.Printf("%s\n", renderer.Dim("✓ Alacritty shift+enter already configured"))
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
}
case "kitty":
modified, path := SetupKittyKeybindings()
if modified {
fmt.Printf("%s Kitty %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
} else if path != "" {
fmt.Printf("%s\n", renderer.Dim("✓ Kitty shift+enter already configured"))
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
}
case "iterm2":
fmt.Printf("%s\n", renderer.Dim("✓ iTerm2 shift+enter works by default (maps to alt+enter)"))
case "terminal.app":
fmt.Printf("%s\n", renderer.Dim("⚠ Terminal.app requires manual configuration"))
fmt.Printf("%s\n", renderer.Dim(" See: Terminal → Preferences → Profiles → Keyboard"))
case "unknown":
fmt.Printf("%s\n", renderer.Dim(" Terminal not detected - use alt+enter or ctrl+j for newlines"))
}
}
// getVSCodeConfigPath returns the platform-specific path to VS Code's User directory
func getVSCodeConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
switch runtime.GOOS {
case "darwin":
return filepath.Join(home, "Library", "Application Support", "Code", "User"), nil
case "windows":
appData := os.Getenv("APPDATA")
if appData == "" {
appData = filepath.Join(home, "AppData", "Roaming")
}
return filepath.Join(appData, "Code", "User"), nil
default: // linux, freebsd, etc.
return filepath.Join(home, ".config", "Code", "User"), nil
}
}
// getCursorConfigPath returns the platform-specific path to Cursor's User directory
func getCursorConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
switch runtime.GOOS {
case "darwin":
return filepath.Join(home, "Library", "Application Support", "Cursor", "User"), nil
case "windows":
appData := os.Getenv("APPDATA")
if appData == "" {
appData = filepath.Join(home, "AppData", "Roaming")
}
return filepath.Join(appData, "Cursor", "User"), nil
default: // linux, freebsd, etc.
return filepath.Join(home, ".config", "Cursor", "User"), nil
}
}
// DetectTerminal identifies which terminal emulator is currently running
func DetectTerminal() string {
// Check TERM_PROGRAM (works for most terminals)
termProgram := os.Getenv("TERM_PROGRAM")
switch termProgram {
case "vscode":
return "vscode" // Also covers Cursor (uses same value)
case "WezTerm":
return "wezterm"
case "ghostty":
return "ghostty"
case "iTerm.app":
return "iterm2"
case "Apple_Terminal":
return "terminal.app"
}
// Kitty doesn't set TERM_PROGRAM, check KITTY_WINDOW_ID
if os.Getenv("KITTY_WINDOW_ID") != "" {
return "kitty"
}
// Alacritty doesn't set TERM_PROGRAM, check ALACRITTY_SOCKET
if os.Getenv("ALACRITTY_SOCKET") != "" {
return "alacritty"
}
// Ghostty fallback (cross-platform - more reliable than TERM_PROGRAM)
if os.Getenv("GHOSTTY_RESOURCES_DIR") != "" {
return "ghostty"
}
// Alacritty fallback
if os.Getenv("ALACRITTY_LOG") != "" {
return "alacritty"
}
// Check TERM variable as last resort
term := os.Getenv("TERM")
if strings.Contains(term, "kitty") {
return "kitty"
}
if term == "alacritty" {
return "alacritty"
}
if term == "xterm-ghostty" {
return "ghostty"
}
return "unknown"
}
// VSCodeKeybinding represents a VS Code keyboard shortcut
type VSCodeKeybinding struct {
Key string `json:"key"`
Command string `json:"command"`
Args map[string]interface{} `json:"args,omitempty"`
When string `json:"when,omitempty"`
}
// SetupVSCodeKeybindings adds shift+enter support to VS Code's integrated terminal
// by modifying the user's keybindings.json file.
// Returns (wasModified, configPath) to allow caller to log the change.
func SetupVSCodeKeybindings() (bool, string) {
// Get platform-specific VS Code config path
configDir, err := getVSCodeConfigPath()
if err != nil {
return false, ""
}
keybindingsPath := filepath.Join(configDir, "keybindings.json")
// Check if VS Code is installed (keybindings file or parent dir exists)
if _, err := os.Stat(filepath.Dir(keybindingsPath)); os.IsNotExist(err) {
// VS Code not installed, skip silently
return false, ""
}
// Read existing keybindings
var keybindings []VSCodeKeybinding
data, err := os.ReadFile(keybindingsPath)
if err != nil {
if !os.IsNotExist(err) {
return false, ""
}
// File doesn't exist, start with empty array
keybindings = []VSCodeKeybinding{}
} else {
// Parse existing keybindings
if err := json.Unmarshal(data, &keybindings); err != nil {
// If parse fails, don't modify the file
return false, ""
}
}
// Check if shift+enter binding already exists
for _, kb := range keybindings {
if kb.Key == "shift+enter" && kb.Command == "workbench.action.terminal.sendSequence" {
// Already configured
return false, keybindingsPath
}
}
// Add shift+enter keybinding
newBinding := VSCodeKeybinding{
Key: "shift+enter",
Command: "workbench.action.terminal.sendSequence",
Args: map[string]interface{}{
"text": "\u001b\n", // ESC + newline (alt+enter sequence)
},
When: "terminalFocus",
}
keybindings = append(keybindings, newBinding)
// Create backup
if data != nil {
backupPath := keybindingsPath + ".backup"
_ = os.WriteFile(backupPath, data, 0644)
}
// Write updated keybindings
updatedData, err := json.MarshalIndent(keybindings, "", " ")
if err != nil {
return false, ""
}
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(keybindingsPath), 0755); err != nil {
return false, ""
}
if err := os.WriteFile(keybindingsPath, updatedData, 0644); err != nil {
return false, ""
}
return true, keybindingsPath
}
// SetupCursorKeybindings adds shift+enter support to Cursor's integrated terminal
// by modifying the user's keybindings.json file.
// Cursor is a fork of VS Code, so it uses the same keybinding format.
// Returns (wasModified, configPath) to allow caller to log the change.
func SetupCursorKeybindings() (bool, string) {
// Get platform-specific Cursor config path
configDir, err := getCursorConfigPath()
if err != nil {
return false, ""
}
keybindingsPath := filepath.Join(configDir, "keybindings.json")
// Check if Cursor is installed (keybindings file or parent dir exists)
if _, err := os.Stat(filepath.Dir(keybindingsPath)); os.IsNotExist(err) {
// Cursor not installed, skip silently
return false, ""
}
// Read existing keybindings
var keybindings []VSCodeKeybinding
data, err := os.ReadFile(keybindingsPath)
if err != nil {
if !os.IsNotExist(err) {
return false, ""
}
// File doesn't exist, start with empty array
keybindings = []VSCodeKeybinding{}
} else {
// Parse existing keybindings
if err := json.Unmarshal(data, &keybindings); err != nil {
// If parse fails, don't modify the file
return false, ""
}
}
// Check if shift+enter binding already exists
for _, kb := range keybindings {
if kb.Key == "shift+enter" && kb.Command == "workbench.action.terminal.sendSequence" {
// Already configured
return false, keybindingsPath
}
}
// Add shift+enter keybinding
newBinding := VSCodeKeybinding{
Key: "shift+enter",
Command: "workbench.action.terminal.sendSequence",
Args: map[string]interface{}{
"text": "\u001b\n", // ESC + newline (alt+enter sequence)
},
When: "terminalFocus",
}
keybindings = append(keybindings, newBinding)
// Create backup
if data != nil {
backupPath := keybindingsPath + ".backup"
_ = os.WriteFile(backupPath, data, 0644)
}
// Write updated keybindings
updatedData, err := json.MarshalIndent(keybindings, "", " ")
if err != nil {
return false, ""
}
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(keybindingsPath), 0755); err != nil {
return false, ""
}
if err := os.WriteFile(keybindingsPath, updatedData, 0644); err != nil {
return false, ""
}
return true, keybindingsPath
}
// SetupGhosttyKeybindings adds shift+enter support to Ghostty terminal
// by appending to the user's config file.
// Returns (wasModified, configPath) to allow caller to log the change.
func SetupGhosttyKeybindings() (bool, string) {
home, err := os.UserHomeDir()
if err != nil {
return false, ""
}
// Ghostty config location: ~/.config/ghostty/config
configPath := filepath.Join(home, ".config", "ghostty", "config")
// Check if config directory exists
configDir := filepath.Dir(configPath)
if _, err := os.Stat(configDir); os.IsNotExist(err) {
// Ghostty not installed, skip silently
return false, ""
}
// Read existing config if it exists
var existingContent []byte
if data, err := os.ReadFile(configPath); err == nil {
existingContent = data
// Check if shift+enter already configured
if strings.Contains(string(data), "keybind = shift+enter") {
return false, configPath
}
}
// Keybinding to add - send newline character (0x0a)
// Ghostty requires \x0a hex escape syntax, verified working
keybinding := "keybind = shift+enter=text:\\x0a\n"
// Append to config
newContent := append(existingContent, []byte(keybinding)...)
// Ensure directory exists
if err := os.MkdirAll(configDir, 0755); err != nil {
return false, ""
}
// Create backup if file exists
if existingContent != nil {
backupPath := configPath + ".backup"
_ = os.WriteFile(backupPath, existingContent, 0644)
}
// Write updated config
if err := os.WriteFile(configPath, newContent, 0644); err != nil {
return false, ""
}
return true, configPath
}
// SetupWezTermKeybindings adds shift+enter support to WezTerm
// by appending to the user's .wezterm.lua file.
// Returns (wasModified, configPath)
func SetupWezTermKeybindings() (bool, string) {
home, err := os.UserHomeDir()
if err != nil {
return false, ""
}
configPath := filepath.Join(home, ".wezterm.lua")
// Check if WezTerm config exists
if _, err := os.Stat(configPath); os.IsNotExist(err) {
// WezTerm not configured, skip silently
return false, ""
}
// Read existing config
data, err := os.ReadFile(configPath)
if err != nil {
return false, ""
}
// Check if shift+enter already configured
if strings.Contains(string(data), "key = 'Enter'") && strings.Contains(string(data), "mods = 'SHIFT'") {
return false, configPath
}
// Create backup
backupPath := configPath + ".backup"
_ = os.WriteFile(backupPath, data, 0644)
// Keybinding to add (insert before final return statement)
keybinding := `
-- Shift+Enter for newlines (added by Cline CLI)
config.keys = config.keys or {}
table.insert(config.keys, {
key = 'Enter',
mods = 'SHIFT',
action = wezterm.action.SendString '\x1b\n',
})
`
content := string(data)
// Try to insert before the final return statement
if strings.Contains(content, "return config") {
content = strings.Replace(content, "return config", keybinding+"\nreturn config", 1)
} else {
// No return statement, append at end
content += keybinding
}
// Write updated config
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
return false, ""
}
return true, configPath
}
// SetupAlacrittyKeybindings adds shift+enter support to Alacritty
// by appending to the user's alacritty.yml file.
// Returns (wasModified, configPath)
func SetupAlacrittyKeybindings() (bool, string) {
home, err := os.UserHomeDir()
if err != nil {
return false, ""
}
// Try both possible locations
configPaths := []string{
filepath.Join(home, ".config", "alacritty", "alacritty.yml"),
filepath.Join(home, ".config", "alacritty", "alacritty.toml"),
filepath.Join(home, ".alacritty.yml"),
}
var configPath string
for _, path := range configPaths {
if _, err := os.Stat(path); err == nil {
configPath = path
break
}
}
if configPath == "" {
// Alacritty not configured, skip silently
return false, ""
}
// Read existing config
data, err := os.ReadFile(configPath)
if err != nil {
return false, ""
}
// Check if shift+enter already configured
if strings.Contains(string(data), "key: Return") && strings.Contains(string(data), "mods: Shift") {
return false, configPath
}
// Create backup
backupPath := configPath + ".backup"
_ = os.WriteFile(backupPath, data, 0644)
// Keybinding to add
var keybinding string
if strings.HasSuffix(configPath, ".yml") || strings.HasSuffix(configPath, ".yaml") {
keybinding = `
# Shift+Enter for newlines (added by Cline CLI)
key_bindings:
- { key: Return, mods: Shift, chars: "\x1b\n" }
`
} else {
// TOML format
keybinding = `
# Shift+Enter for newlines (added by Cline CLI)
[[keyboard.bindings]]
key = "Return"
mods = "Shift"
chars = "\x1b\n"
`
}
// Append to config
newContent := append(data, []byte(keybinding)...)
// Write updated config
if err := os.WriteFile(configPath, newContent, 0644); err != nil {
return false, ""
}
return true, configPath
}
// SetupKittyKeybindings adds shift+enter support to Kitty terminal
// by appending to the user's kitty.conf file.
// Returns (wasModified, configPath)
func SetupKittyKeybindings() (bool, string) {
home, err := os.UserHomeDir()
if err != nil {
return false, ""
}
configPath := filepath.Join(home, ".config", "kitty", "kitty.conf")
// Check if config directory exists
configDir := filepath.Dir(configPath)
if _, err := os.Stat(configDir); os.IsNotExist(err) {
// Kitty not installed, skip silently
return false, ""
}
// Read existing config if it exists
var existingContent []byte
if data, err := os.ReadFile(configPath); err == nil {
existingContent = data
// Check if shift+enter already configured
if strings.Contains(string(data), "map shift+enter") {
return false, configPath
}
}
// Keybinding to add
keybinding := "# Shift+Enter for newlines (added by Cline CLI)\nmap shift+enter send_text all \\x1b\\n\n"
// Append to config
newContent := append(existingContent, []byte(keybinding)...)
// Ensure directory exists
if err := os.MkdirAll(configDir, 0755); err != nil {
return false, ""
}
// Create backup if file exists
if existingContent != nil {
backupPath := configPath + ".backup"
_ = os.WriteFile(backupPath, existingContent, 0644)
}
// Write updated config
if err := os.WriteFile(configPath, newContent, 0644); err != nil {
return false, ""
}
return true, configPath
}
+1
View File
@@ -0,0 +1 @@
if you can make a beautiful tui in go, please help!
+17
View File
@@ -0,0 +1,17 @@
package types
// HistoryItem represents a task history item from taskHistory.json
// This struct matches the JSON format stored on disk
type HistoryItem struct {
Id string `json:"id"`
Ulid string `json:"ulid,omitempty"`
Ts int64 `json:"ts"`
Task string `json:"task"`
TokensIn int32 `json:"tokensIn"`
TokensOut int32 `json:"tokensOut"`
CacheWrites int32 `json:"cacheWrites,omitempty"`
CacheReads int32 `json:"cacheReads,omitempty"`
TotalCost float64 `json:"totalCost"`
Size int64 `json:"size,omitempty"`
IsFavorited bool `json:"isFavorited,omitempty"`
}
+38 -30
View File
@@ -3,22 +3,26 @@ package types
import (
"encoding/json"
"fmt"
"strconv"
"time"
"strconv"
"github.com/cline/grpc-go/cline"
)
// ClineMessage represents a conversation message in the CLI
type ClineMessage struct {
Type MessageType `json:"type"`
Text string `json:"text"`
Timestamp int64 `json:"ts"`
Reasoning string `json:"reasoning,omitempty"`
Say string `json:"say,omitempty"`
Ask string `json:"ask,omitempty"`
Partial bool `json:"partial,omitempty"`
Images []string `json:"images,omitempty"`
Files []string `json:"files,omitempty"`
Type MessageType `json:"type"`
Text string `json:"text"`
Timestamp int64 `json:"ts"`
Reasoning string `json:"reasoning,omitempty"`
Say string `json:"say,omitempty"`
Ask string `json:"ask,omitempty"`
Partial bool `json:"partial,omitempty"`
Images []string `json:"images,omitempty"`
Files []string `json:"files,omitempty"`
LastCheckpointHash string `json:"lastCheckpointHash,omitempty"`
IsCheckpointCheckedOut bool `json:"isCheckpointCheckedOut,omitempty"`
IsOperationOutsideWorkspace bool `json:"isOperationOutsideWorkspace,omitempty"`
}
// MessageType represents the type of message
@@ -33,17 +37,16 @@ const (
type AskType string
const (
AskTypeFollowup AskType = "followup"
AskTypePlanModeRespond AskType = "plan_mode_respond"
AskTypeCommand AskType = "command"
AskTypeCommandOutput AskType = "command_output"
AskTypeCompletionResult AskType = "completion_result"
AskTypeTool AskType = "tool"
AskTypeAPIReqFailed AskType = "api_req_failed"
AskTypeResumeTask AskType = "resume_task"
AskTypeResumeCompletedTask AskType = "resume_completed_task"
AskTypeMistakeLimitReached AskType = "mistake_limit_reached"
AskTypeAutoApprovalMaxReached AskType = "auto_approval_max_req_reached"
AskTypeFollowup AskType = "followup"
AskTypePlanModeRespond AskType = "plan_mode_respond"
AskTypeCommand AskType = "command"
AskTypeCommandOutput AskType = "command_output"
AskTypeCompletionResult AskType = "completion_result"
AskTypeTool AskType = "tool"
AskTypeAPIReqFailed AskType = "api_req_failed"
AskTypeResumeTask AskType = "resume_task"
AskTypeResumeCompletedTask AskType = "resume_completed_task"
AskTypeMistakeLimitReached AskType = "mistake_limit_reached"
AskTypeBrowserActionLaunch AskType = "browser_action_launch"
AskTypeUseMcpServer AskType = "use_mcp_server"
AskTypeNewTask AskType = "new_task"
@@ -65,6 +68,7 @@ const (
SayTypeUserFeedback SayType = "user_feedback"
SayTypeUserFeedbackDiff SayType = "user_feedback_diff"
SayTypeAPIReqRetried SayType = "api_req_retried"
SayTypeErrorRetry SayType = "error_retry"
SayTypeCommand SayType = "command"
SayTypeCommandOutput SayType = "command_output"
SayTypeTool SayType = "tool"
@@ -103,6 +107,7 @@ const (
ToolTypeEditedExistingFile ToolType = "editedExistingFile"
ToolTypeNewFileCreated ToolType = "newFileCreated"
ToolTypeReadFile ToolType = "readFile"
ToolTypeFileDeleted ToolType = "fileDeleted"
ToolTypeListFilesTopLevel ToolType = "listFilesTopLevel"
ToolTypeListFilesRecursive ToolType = "listFilesRecursive"
ToolTypeListCodeDefinitionNames ToolType = "listCodeDefinitionNames"
@@ -206,13 +211,16 @@ func ConvertProtoToMessage(protoMsg *cline.ClineMessage) *ClineMessage {
}
return &ClineMessage{
Type: msgType,
Text: protoMsg.Text,
Timestamp: protoMsg.Ts,
Reasoning: protoMsg.Reasoning,
Say: say,
Ask: ask,
Partial: protoMsg.Partial,
Type: msgType,
Text: protoMsg.Text,
Timestamp: protoMsg.Ts,
Reasoning: protoMsg.Reasoning,
Say: say,
Ask: ask,
Partial: protoMsg.Partial,
LastCheckpointHash: protoMsg.LastCheckpointHash,
IsCheckpointCheckedOut: protoMsg.IsCheckpointCheckedOut,
IsOperationOutsideWorkspace: protoMsg.IsOperationOutsideWorkspace,
}
}
@@ -239,8 +247,6 @@ func convertProtoAskType(askType cline.ClineAsk) string {
return string(AskTypeResumeCompletedTask)
case cline.ClineAsk_MISTAKE_LIMIT_REACHED:
return string(AskTypeMistakeLimitReached)
case cline.ClineAsk_AUTO_APPROVAL_MAX_REQ_REACHED:
return string(AskTypeAutoApprovalMaxReached)
case cline.ClineAsk_BROWSER_ACTION_LAUNCH:
return string(AskTypeBrowserActionLaunch)
case cline.ClineAsk_USE_MCP_SERVER:
@@ -279,6 +285,8 @@ func convertProtoSayType(sayType cline.ClineSay) string {
return string(SayTypeUserFeedbackDiff)
case cline.ClineSay_API_REQ_RETRIED:
return string(SayTypeAPIReqRetried)
case cline.ClineSay_ERROR_RETRY:
return string(SayTypeErrorRetry)
case cline.ClineSay_COMMAND_SAY:
return string(SayTypeCommand)
case cline.ClineSay_COMMAND_OUTPUT_SAY:
+4 -6
View File
@@ -12,9 +12,8 @@ type ConversationState struct {
// StreamingMessage manages state for streaming message display
type StreamingMessage struct {
CurrentKey string `json:"currentKey"`
LastText string `json:"lastText"`
LastToolMessage string `json:"lastToolMessage,omitempty"`
CurrentKey string `json:"currentKey"`
LastText string `json:"lastText"`
}
// NewConversationState creates a new conversation state
@@ -37,9 +36,8 @@ func (cs *ConversationState) GetStreamingMessage() *StreamingMessage {
cs.mu.RLock()
defer cs.mu.RUnlock()
return &StreamingMessage{
CurrentKey: cs.StreamingMessage.CurrentKey,
LastText: cs.StreamingMessage.LastText,
LastToolMessage: cs.StreamingMessage.LastToolMessage,
CurrentKey: cs.StreamingMessage.CurrentKey,
LastText: cs.StreamingMessage.LastText,
}
}
+409
View File
@@ -0,0 +1,409 @@
package updater
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/output"
)
type cacheData struct {
LastCheck time.Time `json:"last_check"`
LatestVersion string `json:"latest_version"`
}
type npmRegistryResponse struct {
DistTags struct {
Latest string `json:"latest"`
Nightly string `json:"nightly"`
} `json:"dist-tags"`
}
const (
checkInterval = 24 * time.Hour
requestTimeout = 3 * time.Second
)
var (
successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true)
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true)
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
)
var verbose bool
// CheckAndUpdate performs a background update check and attempts to auto-update if needed.
// This is non-blocking and safe to call on CLI startup.
func CheckAndUpdate(isVerbose bool) {
verbose = isVerbose
// Skip in CI environments
if os.Getenv("CI") != "" {
if verbose {
output.Printf("[updater] Skipping update check (CI environment)\n")
}
return
}
// Skip if user disabled auto-updates
if os.Getenv("NO_AUTO_UPDATE") != "" {
if verbose {
output.Printf("[updater] Skipping update check (NO_AUTO_UPDATE set)\n")
}
return
}
if verbose {
output.Printf("[updater] Starting background update check...\n")
}
// Run in background so we don't block CLI startup
go func() {
if err := checkAndUpdateInternal(false); err != nil {
if verbose {
output.Printf("[updater] Update check failed: %v\n", err)
}
}
}()
}
// CheckAndUpdateSync performs a synchronous update check (blocks until complete).
// If bypassCache is true, ignores the 24-hour cache and always checks npm registry.
// This is used by the doctor command.
func CheckAndUpdateSync(isVerbose bool, bypassCache bool) {
verbose = isVerbose
// Skip in CI environments
if os.Getenv("CI") != "" {
if verbose {
output.Printf("[updater] Skipping update check (CI environment)\n")
}
return
}
// Skip if user disabled auto-updates
if os.Getenv("NO_AUTO_UPDATE") != "" {
if verbose {
output.Printf("[updater] Skipping update check (NO_AUTO_UPDATE set)\n")
}
return
}
if verbose {
output.Printf("[updater] Starting update check...\n")
}
// Run synchronously
if err := checkAndUpdateInternal(bypassCache); err != nil {
if verbose {
output.Printf("[updater] Update check failed: %v\n", err)
}
}
}
func checkAndUpdateInternal(bypassCache bool) error {
if verbose {
output.Printf("[updater] Loading update cache...\n")
}
// Load cache
cache, err := loadCache()
if !bypassCache && err == nil && time.Since(cache.LastCheck) < checkInterval {
// Checked recently, skip (unless cache is bypassed)
if verbose {
output.Printf("[updater] Cache is fresh (last checked %v ago), skipping\n", time.Since(cache.LastCheck))
}
return nil
}
if err != nil && verbose {
output.Printf("[updater] Cache load failed or doesn't exist: %v\n", err)
}
// Determine channel
distTag := "latest"
if strings.Contains(global.CliVersion, "nightly") {
distTag = "nightly"
}
if verbose {
output.Printf("[updater] Current version: %s (channel: %s)\n", global.CliVersion, distTag)
output.Printf("[updater] Fetching latest version from npm registry...\n")
}
// Fetch latest version from npm
latestVersion, err := fetchLatestVersion()
if err != nil {
if verbose {
output.Printf("[updater] Failed to fetch latest version: %v\n", err)
}
return err
}
if verbose {
output.Printf("[updater] Latest version on npm: %s\n", latestVersion)
}
// Update cache
cache = cacheData{
LastCheck: time.Now(),
LatestVersion: latestVersion,
}
saveCache(cache)
if verbose {
output.Printf("[updater] Updated cache\n")
}
// Compare versions
currentVersion := strings.TrimPrefix(global.CliVersion, "v")
latestVersion = strings.TrimPrefix(latestVersion, "v")
if verbose {
output.Printf("[updater] Comparing versions: current=%s latest=%s\n", currentVersion, latestVersion)
}
if !isNewer(latestVersion, currentVersion) {
// Already up to date
if verbose {
output.Printf("[updater] Already on latest version, no update needed\n")
}
return nil
}
if verbose {
output.Printf("[updater] Update available! Attempting to install...\n")
}
// Determine channel for update command
channel := "latest"
if strings.Contains(global.CliVersion, "nightly") {
channel = "nightly"
}
// Attempt update
if verbose {
output.Printf("[updater] Running: npm install -g cline%s\n",
map[bool]string{true: "@"+channel, false: ""}[channel == "nightly"])
}
if err := attemptUpdate(channel); err != nil {
if verbose {
output.Printf("[updater] Update failed: %v\n", err)
}
showFailureMessage(channel)
return err
}
if verbose {
output.Printf("[updater] Update completed successfully!\n")
}
showSuccessMessage(latestVersion)
return nil
}
func fetchLatestVersion() (string, error) {
// Determine dist-tag from current version
distTag := "latest"
if strings.Contains(global.CliVersion, "nightly") {
distTag = "nightly"
}
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://registry.npmjs.org/cline", nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("npm registry returned status %d", resp.StatusCode)
}
var data npmRegistryResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return "", err
}
if distTag == "nightly" {
return data.DistTags.Nightly, nil
}
return data.DistTags.Latest, nil
}
func attemptUpdate(channel string) error {
packageName := "cline"
if channel == "nightly" {
packageName = "cline@nightly"
}
cmd := exec.Command("npm", "install", "-g", packageName)
cmd.Stdout = nil
cmd.Stderr = nil
return cmd.Run()
}
func isNewer(latest, current string) bool {
// Parse version strings (e.g., "1.0.0-nightly.19")
latestBase, latestSuffix := parseVersion(latest)
currentBase, currentSuffix := parseVersion(current)
// Compare base versions (1.0.0)
comparison := compareVersionParts(latestBase, currentBase)
if comparison != 0 {
return comparison > 0
}
// Base versions are equal, compare suffixes (nightly.19)
return compareSuffix(latestSuffix, currentSuffix) > 0
}
func parseVersion(version string) (string, string) {
parts := strings.SplitN(version, "-", 2)
if len(parts) == 2 {
return parts[0], parts[1]
}
return parts[0], ""
}
func compareVersionParts(v1, v2 string) int {
parts1 := strings.Split(v1, ".")
parts2 := strings.Split(v2, ".")
for i := 0; i < len(parts1) && i < len(parts2); i++ {
// Convert to int for proper numeric comparison
n1 := parseInt(parts1[i])
n2 := parseInt(parts2[i])
if n1 > n2 {
return 1
}
if n1 < n2 {
return -1
}
}
// If all parts are equal, longer version is newer
if len(parts1) > len(parts2) {
return 1
}
if len(parts1) < len(parts2) {
return -1
}
return 0
}
func compareSuffix(s1, s2 string) int {
// If one has no suffix, stable > prerelease
if s1 == "" && s2 == "" {
return 0
}
if s1 == "" {
return 1 // Stable is newer than prerelease
}
if s2 == "" {
return -1 // Prerelease is older than stable
}
// Both have suffixes (e.g., "nightly.19" vs "nightly.18")
// Extract the numeric part after the last dot
n1 := extractBuildNumber(s1)
n2 := extractBuildNumber(s2)
if n1 > n2 {
return 1
}
if n1 < n2 {
return -1
}
return 0
}
func extractBuildNumber(suffix string) int {
// Extract number from "nightly.19" -> 19
parts := strings.Split(suffix, ".")
if len(parts) > 1 {
return parseInt(parts[len(parts)-1])
}
return 0
}
func parseInt(s string) int {
var result int
fmt.Sscanf(s, "%d", &result)
return result
}
func showSuccessMessage(version string) {
output.Printf("\n%s Updated to %s %s Changes will take effect next session\n\n",
successStyle.Render("✓"),
successStyle.Render("v"+version),
dimStyle.Render("→"),
)
}
func showFailureMessage(channel string) {
packageName := "cline"
if channel == "nightly" {
packageName = "cline@nightly"
}
output.Printf("\n%s Auto-update failed %s Try: %s\n\n",
errorStyle.Render("✗"),
dimStyle.Render("·"),
"npm install -g "+packageName,
)
}
func getCacheFilePath() string {
configDir := filepath.Join(os.Getenv("HOME"), ".cline", "data")
return filepath.Join(configDir, "cli-update-cache")
}
func loadCache() (cacheData, error) {
var cache cacheData
cacheFile := getCacheFilePath()
data, err := os.ReadFile(cacheFile)
if err != nil {
return cache, err
}
err = json.Unmarshal(data, &cache)
return cache, err
}
func saveCache(cache cacheData) error {
cacheFile := getCacheFilePath()
// Ensure config directory exists
configDir := filepath.Dir(cacheFile)
if err := os.MkdirAll(configDir, 0755); err != nil {
return err
}
data, err := json.Marshal(cache)
if err != nil {
return err
}
return os.WriteFile(cacheFile, data, 0644)
}
+16 -20
View File
@@ -4,38 +4,34 @@ import (
"fmt"
"runtime"
"github.com/cline/cli/pkg/cli/global"
"github.com/spf13/cobra"
)
var (
// These will be set at build time via ldflags
Version = "dev"
Commit = "unknown"
Date = "unknown"
BuiltBy = "unknown"
)
// NewVersionCommand creates the version command
func NewVersionCommand() *cobra.Command {
var short bool
cmd := &cobra.Command{
Use: "version",
Short: "Show version information",
Long: `Display version information for the Cline Go host.`,
Use: "version",
Aliases: []string{"v"},
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(Version)
fmt.Println(global.CliVersion)
return nil
}
fmt.Printf("Cline Go Host\n")
fmt.Printf("Version: %s\n", Version)
fmt.Printf("Commit: %s\n", Commit)
fmt.Printf("Built: %s\n", Date)
fmt.Printf("Built by: %s\n", BuiltBy)
fmt.Printf("Go version: %s\n", runtime.Version())
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
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)
return nil
},
@@ -44,4 +40,4 @@ func NewVersionCommand() *cobra.Command {
cmd.Flags().BoolVar(&short, "short", false, "show only version number")
return cmd
}
}

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