Compare commits

..

184 Commits

Author SHA1 Message Date
celestial-vault b36fe247fd add dev command for cline-core changes for cli 2025-10-04 10:16:12 -07:00
pashpashpash 097f8e6239 cline cli super alpha (#6644)
* super sketchy big merge with main

* gitignore

* gitignore

* Delete cli/bin/air

* Delete cli/bin directory

* Delete cli/cline-host

* Fix missing package.json in cli

Copy the package JSON into the dist-standalone dir during compilation.
Remove workaround for missing package.json

* Update scripts/build-cli.sh

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

* Remove reference to watchservice, it has been removed

* Update scripts/build-go-proto.mjs

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

* Fix timestamp to string conversion

* diff.go ellipsis fix

* COMMON_TYPES

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-03 15:04:00 -07:00
Sarah Fortune a037ba8965 Make the results of the GH test workflow easier to understand & fix windows tests (#6628)
* Dont use continue-on-error in the GH workflow

Using continue-on-error makes the tests appear as passed even when they failed and this is confusing

* Increase timeout for getOpenTabs test

* Dont run any tests if the build step failed

* Update .github/workflows/test.yml

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

* Fix getOpenTabs on tabs

```
Extension host test runner error 1 test failed.
  1 failing
  1) Hostbridge - Window - getOpenTabs
       should return all tabs including deleted files:
     Error: EBUSY: resource busy or locked, rmdir 'C:\Users\RUNNER~1\AppData\Local\Temp\vscode-test-O06Qnd'
```

The test is failing because the clean can't delete the temp directory it created, just surround it with try/catch.

* Add debug logs to the openTabsTest

* Increase the timeout on the e2e tests

I see this test timing out, so try increasing the timeout
https://github.com/cline/cline/actions/runs/18209633465/job/51847553463?pr=6628

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-10-03 13:49:00 -07:00
Ara f9f96b0e8a Bug Fix: replace featureFlagsService with isMultiRootEnabled utility (#6631)
* refactor: replace featureFlagsService with isMultiRootEnabled utility

Replace direct usage of featureFlagsService.getMultiRootEnabled() with the
isMultiRootEnabled() utility function throughout the codebase. This change:

- Centralizes multi-root feature flag logic in a single utility function
- Removes dependency on featureFlagsService in task and tool modules
- Passes stateManager to isMultiRootEnabled() for consistent state access
- Improves code maintainability by reducing coupling to the feature flags service

The refactoring affects Task class initialization, system prompt generation,
workspace root formatting, and auto-approve functionality.

* cancel button
2025-10-03 11:48:05 -07:00
celestial-vault a5699e883d Make statemanager global singleton (#6619)
* do not initialize duplicate statemanager in migration; use vscode api directly

* make state manager a singleton

* move statemanager init to common.ts
2025-10-02 21:18:57 -07:00
Bee 1cee9f33a7 feat: feature flag for cline auth provider (#6629) 2025-10-03 04:01:12 +02:00
pashpashpash 1b1e66f9d1 cli <--> main sync (#6576)
* adding cli args to cline core for ports and cline directory

* added locking

* data dir is correct

* final touches

* added better sqlite3 dep

* fs

* making help text more accurate

* touch instance at the end

* shutdown impl

* moving SETTINGS_SUBFOLDER const to vscode-context

* not calling process exit in protobus or hostbridge, handling that in cline core with proper graceful shutdown

* hostbridge port default

* package lock

* sigh, biome auto updated and daniels mcp hub change caused issues. locking biome

* fixed package-lock
2025-10-02 16:45:19 -07:00
Nick Baumann 888968d387 Update local models docs: remove emojis, add Qwen3 Coder recommendations, improve guidance (#6624) 2025-10-02 16:28:18 -07:00
canvrno 3fa41068ba Added checkpoints warning for multiroot scenarios (#6615)
* Added checkpoints warning for multiroot scenarios

* changeset

---------

Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-10-02 15:01:11 -07:00
celestial-vault 4c820db231 do not initialize duplicate statemanager in migration; use vscode api directly (#6614) 2025-10-02 14:26:47 -07:00
Sarah Fortune afa3eed7eb Update scripts/runclinecore.sh to include platform specific node module path (#6618)
With the introduction of better sqlite, this script needs to set the correct path for platform specific node modules. The logic is copied from the JB plugin (the vscode extension does not set the node modules path).
2025-10-02 14:21:39 -07:00
Daniel Steigman 9b6a120da4 feat(focus-chain): Render checklist items as markdown (#6452)
* feat(focus-chain): Render checklist items as markdown

This change introduces markdown rendering for both the header and individual items in the focus chain checklist. It leverages the existing MarkdownBlock component and adds a 'compact' mode to ensure proper alignment and spacing.

* feat: Add LightMarkdown component for memory-efficient rendering

- Create ultra-lightweight LightMarkdown component as memory-efficient alternative to MarkdownBlock
- Support bold (**text**), italic (*text*), and headers (# text) with cached regex patterns
- Replace MarkdownBlock with LightMarkdown in ChecklistRenderer and FocusChain components
- Eliminate heavy remark/rehype pipeline, AST parsing, and syntax highlighting for simple text formatting
- Significantly reduce memory usage and GC pressure in focus chain and checklist rendering
- Fix linting errors and ensure proper React element handling

This optimizes memory usage for components that only need basic markdown formatting,
while preserving full MarkdownBlock functionality where advanced features are needed.

* LightMarkdown: super-lightweight emphasis parser (bold/italic only); single-pass O(n) scan with memoization; remove headers; disable underscore emphasis to avoid snake_case false positives
2025-10-02 13:38:02 -07:00
Sarah Fortune 80f84b551e Log env vars in cline-core (#6595) 2025-10-01 16:00:39 -07:00
github-actions[bot] 8456f45a80 v3.32.6 Release Notes (#6579)
* 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

Updated version to 3.32.6

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-01 15:18:16 -07:00
canvrno e288bb9e4f Adjusted position and wording of multiroot settings (#6590)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-10-01 11:24:49 -07:00
celestial-vault a369551a9e fix: resolve CSS @import ordering issue for Tailwind CSS v4 compatibility (#6463)
- Move external @import statements before Tailwind imports to prevent PostCSS errors
- Add comprehensive documentation explaining import order requirements
- Fix build failure caused by '@import must precede all other statements' error

The issue occurred because Tailwind CSS v4 expands its imports into thousands of
lines of generated CSS, causing external imports to appear after CSS rules in
the final compiled output, violating CSS specifications.
2025-10-01 11:24:07 -07:00
celestial-vault b0fa1be2df Accept task settings in init task (#6342)
* v3.28.5 Release Notes

* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for patch release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>

* Add task-specific settings caching to StateManager (#6171)

* Add task-specific settings caching to StateManager

Implement per-task settings storage with automatic loading/clearing:
- Add taskStateCache for isolated task settings
- Load task settings on task creation and reinitialize
- Clear task settings cache when tasks end
- Prioritize task cache over global state in getters

* update error wording for taskSettings read failure

* add missing return statement in promise.all

* fix: persist pending task settings before clearing cache

Changed clearTaskSettings to be async and ensure any pending task state
changes are persisted to storage before clearing the in-memory cache.
This prevents potential data loss when a task ends with unpersisted
settings still in the pending state queue.

- Made clearTaskSettings async and added optional taskId parameter
- Added logic to persist pending task state batch before clearing
- Updated controller to await clearTaskSettings and pass taskId

* add ability to set task setting overrides in newtask rpc

* spread proto object keys that don't need to be transformed and filter undefined values

* use most Settings fields in TaskSettings proto and convert fields that do not directly map to the application typescripts type of Settings

* accept taskSettings in newTask RPC and save them to task state

* remove test taskSettings in useMessageHandlers

---------

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: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-10-01 11:00:21 -07:00
Ara 793db91291 Adding support for GLM-4.6 (#6589) 2025-10-01 10:42:40 -07:00
canvrno 9cb0e51ffc Feat: Exclude deleted files from environment_details visible/open tabs context (#6014)
* Change to hide deleted files from the visible files/tabs environment_details

* Moved filtering logic to extension

---------

Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-10-01 10:11:44 -07:00
canvrno d68bf862a8 Provide model with better environment details in multiroot scenarios (#6585)
* Provide model with better env details in multiroot scenarios

---------

Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-10-01 01:24:18 -07:00
Sarah Fortune c2f69737aa Improve the git commit message generator (#6583)
* Commit message generator shouldn't create it's own StateManager, just use the one from the webview.
* Use the correct Cline logo
* Move it into the vscode specific directory, it uses too many VSCode APIs to work cross-platform.
2025-09-30 23:56:04 -07:00
Ara 99b8b92b27 Adding Telemetry for multi root workspace (#6580)
* Adding Telemetry for multi root workspace

* Taking out command runner

* Refactoring
2025-09-30 18:42:15 -07:00
Ara 1239fe1b33 Fixing parsing issues for workspace hints (#6559)
* Fixing regex parsing for workspace hints

* Fixing regex parsing for workspace hints

* test

---------

Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-30 17:01:33 -07:00
Tomás Barreiro 062e597b74 feat: Add Claude Sonnet 4.5 to Claude Code (#6573)
* feat: Add Claude Sonnet 4.5 to Claude Code

* add changeset

* Add support for the latest opus and sonnet models

* fix tests
2025-09-30 14:48:44 -07:00
Ara d099be10fd Adding Telemetry for @ mention usage (#6499)
* cjage

* Refactoring
2025-09-30 14:23:51 -07:00
canvrno 3e8268605f Update Cline provider headers (#6575)
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-30 13:50:04 -07:00
Saoud Rizwan 1615bf3f82 fix: set max thinking budgets to avoid overflow when switching models with different budget limits 2025-09-30 13:11:17 -07:00
kvyb 15f713d26a Pass host telemetry requests (#6444)
* feat: add ideId to headers

* Add IDE-id to telemetry and request headers

* feat: send X-IDE-ID/X-IDE-VERSION headers; cache host version in EnvService

* feat: send X-IDE-ID/X-IDE-VERSION/X-CLIENT-VERSION

* Address PR feedback: single RPC call, remove cache, use ExtensionRegistryInfo, remove redundant ide_id

* feat: align headers with backend

* fix: PR feedback; EnvUtils and aligned headers with backend;
2025-09-29 22:52:06 -07:00
kvyb 5babd9e061 fix: keep fuzzy search active in chat input after selecting Add File/… (#6078)
* fix: keep fuzzy search active in chat input after selecting Add File/Folder

* fix: mentions, strip leading '/' and enforce file/folder-only results

* fix: improve readability of filteredDynamic

* fix: improve readability of selectedTypeValue
2025-09-30 07:47:58 +03:00
celestial-vault ba12672600 Remove open in editor button (#6462)
* first pass of removing all related code

# Conflicts:
#	src/hosts/external/ExternalWebviewProvider.ts
#	src/hosts/vscode/VscodeWebviewProvider.ts

* remove more things

* rename all sidebarWebviews to webview

* remove null from getInstance and remove null checks

* remove client id logic and update RPC subscriptions to match this

* add back extension.test.ts without irrelevant webview panel tests

* Refresh CI cache - fix proto compilation

* add comment to try to invalidate CI cache

* add clean script to test.yml for test-platform-integration

* remove clean script didn't work

* finally found the error in the code - linter wasn't highlighting it

* Don't delete unrelated tests in this PR

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2025-09-29 21:15:17 -07:00
Sarah Fortune 163bf77677 Add a way for cline-core to get the version of the client. (#6553)
Add fields to the getHostVersion RPC that return the version of the cline-core client (e.g. the extension or plugin version).
2025-09-29 21:08:30 -07:00
Saoud Rizwan edf5ea00f6 Change task timeline item hover style 2025-09-29 20:18:47 -07:00
github-actions[bot] c3c80ffc4d v3.32.5 Release Notes (#6552)
* changeset version bump

* Updating CHANGELOG.md format

* 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-09-29 18:17:17 -07:00
Saoud Rizwan f14ed8506d Use state to manage NewModelBanner presentation 2025-09-29 18:17:17 -07:00
Saoud Rizwan 41202df74e Add prompt to encourage Sonnet 4.5 to use multiple search/replace blocks in a diff edit call rather than as separate requests 2025-09-29 18:17:14 -07:00
Saoud Rizwan ab88599e05 Add ContextWindowSwitcher component to easily switch between 200k and 1m context models 2025-09-29 16:03:23 -07:00
celestial-vault c2dc0e531c make task constructor typed object (#6558) 2025-09-29 15:45:17 -07:00
Juan Pablo Flores 246b0fa999 Add documentation for changes introduced in 3.30 (#6512)
* docs: update what-is-cline to use generic IDE references

Updated documentation to replace VS Code-specific references with generic "IDE" terms, making it applicable to modern IDEs beyond VS Code for broader compatibility.

* docs: expand Cline installation guide with comprehensive setup instructions

- Add prerequisites section with account creation and editor compatibility
- Include detailed installation steps for VS Code/Cursor and JetBrains IDEs
- Add troubleshooting sections for common installation issues
- Expand editor support information and setup guidance
- Improve documentation structure with tabs and accordions for better UX

* docs: remove font weight and reorder getting started pages

- Remove font weight property from fonts configuration
- Reorder getting started pages to place installation before model selection guide

* fixes title font-weight to original values

* docs: improve JetBrains plugin installation link text

Replace generic URL text with descriptive "JetBrains Marketplace" link text for better user experience and accessibility in the Cline installation guide.

* docs: add voice mode feature documentation and cross-references

Add comprehensive documentation for Voice Mode feature including setup instructions, use cases, and technical requirements. Also add cross-reference tip in plan-and-act.mdx to promote voice mode usage during planning discussions.

* docs: rename voice-mode to dictation for clarity

Rename voice-mode.mdx to dictation.mdx and update all references throughout the documentation to use "Dictation" instead of "Voice Mode" for more accurate terminology and better user understanding.

* feat(docs): add YOLO mode documentation

Add comprehensive documentation for YOLO mode feature, covering auto-approval functionality, safety warnings, use cases, and best practices for autonomous operation.

* docs: reorganize navigation structure and add redirect for JetBrains install page

- Remove JetBrains installation page from getting-started section
- Reorder Features section with @ Mentions first, followed by alphabetically sorted individual features
- Move Slash Commands group after individual features and add workflows page
- Add yolo-mode feature to the end
- Add redirect from old JetBrains install path to main installation guide

* docs: improve JetBrains logo visibility and simplify dictation instructions

- Remove Frame wrapper around JetBrains logo for cleaner markup
- Add CSS styling to ensure JetBrains logo visibility in dark mode with background, border, and hover effects
- Simplify dictation instructions by removing redundant recording state description
2025-09-29 15:33:48 -07:00
Nick Baumann 6c099fbe12 docs: update documentation for Claude Sonnet 4.5 release (#6556)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-29 15:33:29 -07:00
lcs-bdr 00bf05c26c Fix repeated API error 400 in SAP AI Core provider (#6537)
* only add reasoning_details if available

* add changeset

* simplify patch
2025-09-29 15:28:24 -07:00
Walter Korman da99e2bf4b fix: update vercel provider cost note and sign-up url (#6551) 2025-09-29 15:27:31 -07:00
nihar-oracle 963e2c00eb fix: Fixing refresh logic (#6542)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-29 15:26:37 -07:00
Jose Castelli 9355d61bc1 updating .github codeowners (#6539) 2025-09-29 15:26:01 -07:00
Saoud Rizwan a9e17fee57 Fix thinking slider starting at min rather than 0 2025-09-29 15:22:51 -07:00
Saoud Rizwan 3e8548a341 Improve thinking budget slider UI to take up less space 2025-09-29 14:58:09 -07:00
Saoud Rizwan 76b86ff0c0 v3.32.4 Release Notes 2025-09-29 14:15:02 -07:00
Saoud Rizwan 684438b44c Add Sonnet 4.5 to GCP Vertex 2025-09-29 14:15:02 -07:00
AJ Juaire 3c84388fb2 feat: Add Amazon Bedrock us-west-1 support. (#6550)
docs: https://docs.aws.amazon.com/bedrock/latest/userguide/models-regions.html
2025-09-29 14:01:33 -07:00
Saoud Rizwan b0f86201d2 Add prompt caching support for OpenRouter's anthropic/claude-4.5-sonnet model 2025-09-29 13:56:35 -07:00
Jose Castelli d1fc59758e Reduce Test Workflow Time by 45% and Enable Qlty Coverage on Main (#6374)
* improving test workflow

* testing pipeline improvement

* testing new run

* adding missing protos

* adding previous cache + restoring dev dep version

* restoring webview package lock

* scripts update

* fixing old flaky test

* fixing old flaky test

* changeset update

* adding test-platform-integration again

* adding quality check for integration platform
2025-09-29 13:50:56 -07:00
Saoud Rizwan 87c9f58902 Add 1m context window support to Sonnet 4.5 2025-09-29 13:49:31 -07:00
Saoud Rizwan cda3eb8236 fix: task timeline showing text and reasoning items 2025-09-29 12:31:41 -07:00
John Costa 3c21d2be1f fix: returning undefined when URL is not valid (#6377)
* fix: returning undefined when URL is not valid

This can happen when the user is typing, and once it was set there was
no way of changing it.

* adding change set
2025-09-29 12:19:23 -07:00
Saoud Rizwan f3adf68775 v3.32.3 Release Notes 2025-09-29 11:50:11 -07:00
Saoud Rizwan 6bd3181133 Add Sonnet 4.5 to Bedrock; add model banner announcing Sonnet 4.5; modify bug report 2025-09-29 11:38:02 -07:00
github-actions[bot] dd3a234a69 v3.32.2 Release Notes (#6511)
* changeset version bump

* Updating CHANGELOG.md format

* 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-09-29 10:53:49 -07:00
Saoud Rizwan c496f8a90d Fix OpenRouter model id 2025-09-29 10:47:00 -07:00
Saoud Rizwan 25b1cf91fc Add fixed Sonnet 4.5 support 2025-09-29 10:40:15 -07:00
Saoud Rizwan b54e2043fe Revert "Add Claude Sonnet 4.5 (#6544)"
This reverts commit 98e5ccc547.
2025-09-29 10:26:31 -07:00
Saoud Rizwan 98e5ccc547 Add Claude Sonnet 4.5 (#6544) 2025-09-29 10:10:04 -07:00
Sarah Fortune 813a9589d0 Replace VSCode API with the host bridge. (#6529)
Add a method to the host bridge to open and focus the terminal panel.
2025-09-28 19:12:09 -07:00
Sarah Fortune 64eb66d49a Move getGlobalStorageDir out of the HostProvider into disk.ts (#6530) 2025-09-28 19:11:59 -07:00
Sarah Fortune 8c17d864c8 Replace VSCode API with Host Provider (#6527)
VSCode API doesn't work cross-platform, this should use the host provider instead.
2025-09-28 18:18:55 -07:00
Sarah Fortune 4f931c2d9d Handle quotes and other special chars in the curl request (#6525)
This workflow didn't work if the PR title contained quotes, e.g.
https://github.com/cline/cline/pull/6523 - Don't show VS Code LM provider on non-VSCode platforms
2025-09-28 17:50:46 -07:00
Sarah Fortune 5836db3093 [disk.ts] Replace uses of context.globalStorageUri with HostProvider.get().globalStorageFsPath (#6507)
* Fix ellipsis warning

# Conflicts:
#	src/services/test/TestServer.ts

* [disk.ts] Replace uses of context.globalStorageUri with HostProvider.get().globalStorageFsPath

This is part of removing dependencies on the VSCode API fom the codebase except for in platform specific code in src/hosts/vscode and src/extension.ts.

Remove unused vscode context param.

* Remove constructors that just call super()
2025-09-28 17:39:19 -07:00
Sarah Fortune 18879edf9f Don't include VS Code LM API for non-VSCode platforms (#6523) 2025-09-28 17:38:52 -07:00
celestial-vault 1cc702c8b9 remove getCurrentMode function in favor of state manager (#6418) 2025-09-28 15:34:52 -07:00
Saoud Rizwan c8caa6f9b9 fix: command execution not sending output to webview and incorrectly showing 'Proceed while Running' when finished (#6522) 2025-09-28 15:29:21 -07:00
Saoud Rizwan 01f61b6765 Make first checkpoint async to unblock UI 2025-09-28 12:20:47 -07:00
Saoud Rizwan b8dd6abe61 Add /task deep link handler (#6513)
* Add /task deep link handler

* Add deep link handler for /task
2025-09-28 02:23:09 -07:00
github-actions[bot] fb7f0e36fe v3.32.1 Release Notes (#6488)
* changeset version bump

* Updating CHANGELOG.md format

* 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-09-28 00:00:26 -07:00
Saoud Rizwan 403a32b69c Disable TaskFeedbackButtons 2025-09-27 23:18:22 -07:00
Saoud Rizwan ee6daed7bb Change task timeline items border radius 2025-09-27 23:13:30 -07:00
Saoud Rizwan 55ffe9e5dd Update task timeline colors 2025-09-27 23:07:16 -07:00
Saoud Rizwan c2ec5fd17d Update Settings design and 'About' section 2025-09-27 22:59:07 -07:00
Saoud Rizwan 5a84cb1145 Use SlidersHorizontal icon for API Configuration tab in Settings 2025-09-27 22:41:09 -07:00
Saoud Rizwan aec2fd5a7b fix: planActSeparateModelsSetting defaulting to true for new users after reload 2025-09-27 22:33:57 -07:00
Tomás Barreiro 5826cb584e fix: cline accounts using stale id token at refresh response (#6509) 2025-09-27 22:16:25 -07:00
Sarah Fortune 76232639c3 [Tasks directory] Replace uses of context.globalStorageUri with HostProvider.get().globalStorageFsPath (#6420)
* Replace uses of context.globalStorageUri with HostProvider.get().globalStorageFsPath for task directory

This is part of removing dependencies on the VSCode API fom the codebase except for in platform specific code in src/hosts/vscode and src/extension.ts.

Remove unused vscode context param.

* Fix test ModelContextTracker.test.ts

* Fix test FileContextTracker.test.ts
2025-09-27 17:47:27 -07:00
celestial-vault 3173483c0f remove task class enable checkpoints variable and directly use statemanager (#6491) 2025-09-27 16:38:23 -07:00
Sarah Fortune 8caf37600b Fix problem compiling integration tests (#6498)
Fixes the error below. The integraion tests are compiled to CommonJS and cannot import an .mjs directly.

```
> claude-dev@3.32.0 compile-tests
> node ./scripts/build-tests.js

node:child_process:957
    throw err;
    ^

Error: Command failed: tsc -p ./tsconfig.test.json --outDir out
    at genericNodeError (node:internal/errors:983:15)
    at wrappedFn (node:internal/errors:537:14)
    at checkExecSyncError (node:child_process:882:11)
    at execSync (node:child_process:954:15)
    at Object.<anonymous> (/Users/sjf/cline/scripts/build-tests.js:56:1)
    at Module._compile (node:internal/modules/cjs/loader:1734:14)
    at Object..js (node:internal/modules/cjs/loader:1899:10)
    at Module.load (node:internal/modules/cjs/loader:1469:32)
    at Function._load (node:internal/modules/cjs/loader:1286:12)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14) {
  status: 2,
  signal: null,
  output: [
    null,
    "src/core/task/index.ts(3,65): error TS7016: Could not find a declaration file for module '@anthropic-ai/sdk/resources/index.mjs'. '/Users/sjf/cline/node_modules/@anthropic-ai/sdk/resources/index.mjs' implicitly has an 'any' type.\n" +
      "  There are types at '/Users/sjf/cline/node_modules/@anthropic-ai/sdk/resources/index.d.ts', but this result could not be resolved under your current 'moduleResolution' setting. Consider updating to 'node16', 'nodenext', or 'bundler'.\n",
    ''
  ],
  pid: 22680,
  stdout: "src/core/task/index.ts(3,65): error TS7016: Could not find a declaration file for module '@anthropic-ai/sdk/resources/index.mjs'. '/Users/sjf/cline/node_modules/@anthropic-ai/sdk/resources/index.mjs' implicitly has an 'any' type.\n" +
    "  There are types at '/Users/sjf/cline/node_modules/@anthropic-ai/sdk/resources/index.d.ts', but this result could not be resolved under your current 'moduleResolution' setting. Consider updating to 'node16', 'nodenext', or 'bundler'.\n",
  stderr: ''
}

Node.js v23.11.0
```
2025-09-27 13:07:29 -07:00
Igor Tceglevskii 58b14c69b1 CLINE_ACTIVE usage (#6471) 2025-09-27 10:02:05 -07:00
Saoud Rizwan b831100f20 feat: preserve reasoning traces for cline/openrouter/anthropic providers to maintain conversation integrity (#6495)
* feat: preserve reasoning traces for cline/openrouter/anthropic providers to maintain conversation integrity

* Add reasoning trace preservation for providers

This change introduces a feature to preserve reasoning traces for specific providers, enhancing conversation integrity.

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

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-27 06:17:10 -07:00
Saoud Rizwan 23fa305eac Enable thinking by default for models that support it to reduce text verbosity in chat (#6493)
* Enable thinking by default for models that support it to reduce text verbosity in chat

* Refactor
2025-09-27 05:42:57 -07:00
Saoud Rizwan 0762e6406d fix: sending message during interactive command would show double checkpoints 2025-09-27 00:55:15 -07:00
Sarah Fortune 94da9f6669 Only show the info banner messages about the right sidebar in VSCode (#6492)
Add a `type` field to the PlatConfig with type of the IDE: VSCode, standalone, etc.
Make the info banner conditional on the type.
Quiet the gRPC logs on startup.
2025-09-26 19:37:58 -07:00
Saoud Rizwan fdeef9cece fix: uses homedir instead of hardcoded tilde for MCP path 2025-09-26 17:51:54 -07:00
lcs-bdr dd055b3327 fix: add retry logic to SAP AI Core provider (#6453)
* Use retry behavior for SAP AI Core provider

* add changeset
2025-09-26 12:47:08 -07:00
Igor Tceglevskii e852c6953b force no proxy for hostbridge connection (#6474) 2025-09-26 12:06:02 -07:00
github-actions[bot] aef27eb1c4 v3.32.0 Release Notes (#6487)
* Added the new code-supernova-1-million stealth model, available for free and delivering a 1 million token context window
* Changes to inform Cline about commands that are available on your system

Updated version to 3.32.0 

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2025-09-26 10:32:37 -07:00
Ara 863572031f Empty PR to bump changeset (#6485) 2025-09-26 09:45:12 -07:00
pashpashpash 65eee1ac6a add code-supernova-1m (#6458)
* add code-supernova-1m

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

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

* fixing cache price

* wording

* Update webview-ui/src/components/chat/Announcement.tsx

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

* Fix announcement content

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-09-26 08:19:39 -07:00
Bee 8213b0b910 Update E2E Test for Chat input (#6470)
* Update E2E Test for Chat input

* update

* typo
2025-09-26 10:00:32 +08:00
Bee f5fc3fed6f test: skip API request failure check to prevent Windows timeout (#6468)
Remove API request failure assertions that cause test timeouts on Windows due to longer API request failure times, while preserving other test functionality
2025-09-25 17:51:33 -07:00
celestial-vault 42df03177f feat: add HeroTooltip to conversation history button (#6466)
- Rename OpenDiskTaskHistoryButton to OpenDiskConversationHistoryButton
- Add HeroTooltip for consistent UI experience
- Update protobuf service and backend handler
2025-09-25 17:22:43 -07:00
Bee 648ae1b1fd feat(chat): add scroll to top functionality (#6423)
* feat(chat): add scroll to top functionality

- Add scroll to top button when action buttons are not visible
- Pass virtuosoRef to ActionButtons component for scroll control
- Enhance scroll button logic to handle both up and down directions

* Update aria-label
2025-09-25 17:10:25 -07:00
Saoud Rizwan d3cff6ac47 fix: search tool long regex string causing overflow in chatview 2025-09-24 23:38:25 -07:00
github-actions[bot] 13af435103 v3.31.1 Release Notes (#6451)
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for version 3.31.1

Added details about installed CLI tools and renamed MCP tab.

---------

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-09-24 23:08:59 -07:00
Saoud Rizwan 3b1477bc24 Rename MCP tab 'Installed' to 'Configure' (#5966)
* Rename MCP tab 'Installed' to 'Configure'

* Fix component name

* Fix errors; update docs

* Create green-wasps-brush.md
2025-09-24 23:05:23 -07:00
Saoud Rizwan 688f93db6d Show ToS update for cline account users 2025-09-24 23:00:41 -07:00
Saoud Rizwan a702270e85 Fix import linter error 2025-09-24 21:57:30 -07:00
Saoud Rizwan 03acada1b9 Fix padding in checkpoints error 2025-09-24 21:56:43 -07:00
adam jones 95cca05f5e Add CLI tools detection to environment details (#5471)
- Auto-detect available CLI tools in system PATH
- Add detected tools to environment context for AI models
- Include comprehensive list of common developer tools (gh, docker, aws, etc.)
- Cross-platform support using 'which' on Unix and 'where' on Windows

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-24 21:49:25 -07:00
Saoud Rizwan 33d77eb095 Copy changes 2025-09-24 21:16:25 -07:00
github-actions[bot] 3e0c39acbb v3.31.0 Release Notes (#6379)
* changeset version bump

* Updating CHANGELOG.md format

* Update package.json

* Update changelog for version 3.31.0

Updated version to 3.31.0 and added new features and improvements.

---------

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-09-24 18:33:47 -07:00
Saoud Rizwan 366d8a5411 Fix failing integration test due to duplicate test id 2025-09-24 18:25:13 -07:00
Saoud Rizwan d22596e5c2 Update announcement banner content (#6448) 2025-09-24 18:06:25 -07:00
Saoud Rizwan 7cc612dbce Fix right sidebar banner and docs (#6446) 2025-09-24 18:02:44 -07:00
Ara f0cab63a43 Fixing icons and positioning for UI for voice mode (#6445)
* Minor UX fixes for voice mode

* Minor UX fixes for voice mode

* Minor UX fixes for voice mode

* Fix alignment of stop icon

---------

Co-authored-by: frostbournesb <frostbournesb@protonmail.com>
2025-09-24 16:20:27 -07:00
Saoud Rizwan 3c1b670b73 fix: cline not knowing plan/act mode if compact mode is enabled (#6443) 2025-09-24 15:31:42 -07:00
Ara 5de05d68ef Restricting voice mode to mac os only (#6442) 2025-09-24 15:15:44 -07:00
Saoud Rizwan 9b6b1c376c Revert focus chain design changes (#6441)
This reverts commit e5e26f45fa.

Revert "Update colors and width of focus chain (#6437)"

This reverts commit b794583b7a.

Revert "Update task header and focus chain UI design (#6415)"

This reverts commit 011b19225e.

Fix

Fix
2025-09-24 14:50:30 -07:00
Saoud Rizwan e5e26f45fa Decrease focus chain height 2025-09-24 13:55:31 -07:00
Saoud Rizwan b794583b7a Update colors and width of focus chain (#6437) 2025-09-24 13:44:54 -07:00
celestial-vault ca96bd8f62 add infobanner (#6294)
* add infobanner

* fix type and update docs url

* restructure test to not include brittle elements; it is not clear why this banner was even added to auth tests

* move update info banner logic into rpc
2025-09-24 13:37:40 -07:00
Bee 0ecdf8d0cc update CompactTaskButton tooltip content and styling (#6424) 2025-09-24 13:10:44 -07:00
Jose Castelli 5be6ba68a3 Testing Platform - Support partial response validation via meta.expected (#6399)
Testing Platform - Support partial response validation via meta.expected
2025-09-24 20:03:32 +02:00
celestial-vault 1bdd1e943f Add docs for opening cline in right sidebar (#6292)
* Add docs for opening cline in right sidebar

* use frame tags

* change to gifs

* docs(customization): refactor sidebar instructions with Steps component

Updated opening-cline-in-sidebar.mdx to replace numbered lists with structured <Steps> components for improved readability. Also updated image sources and added a link for Cursor alignment guidance.

---------

Co-authored-by: Juan Pablo <juan@cline.bot>
2025-09-24 09:22:02 -07:00
Saoud Rizwan c85a4abeb4 Fix bg color of API configuration in settings (#6422) 2025-09-24 02:13:29 -07:00
Saoud Rizwan 011b19225e Update task header and focus chain UI design (#6415)
* Fix chatfield buttons positioning

* Fix task header icons being cut off at bottom

* Fix copy for focus chain

* Fix focus chain text overflowing and being hidden

* Remove transition animation

* Fix token stats

* Tweak task header styles

* Fix focus chain progress bar direction

* Hide checkpoint text unless hovered

* Make copy button smaller and muted

* Use new focus chain design

* Make timeline blocks circles

* Move focus chain down and fix pencil icon positioning
2025-09-24 02:06:06 -07:00
Sarah Fortune a3945dce7f Replace vscode context.globalStoragePath with HostProvider.globalStorageFsPath (#6419) 2025-09-24 01:19:06 -07:00
celestial-vault 8daca03996 add a script to reconstruct taskHistory (#6403)
* add a script to reconstruct taskHistory

* remove format setting unrelated
2025-09-23 23:23:44 -07:00
Bee 777b8576f2 fix: update fontsource import path to resolve 401 errors (#6414)
* fix: update fontsource import path to resolve 401 errors

Replace specific weight imports with node_modules path reference to fix accessibility issues with @fontsource/azeret-mono font files in webview

* update path
2025-09-23 18:47:56 -07:00
Ara 880755ec89 feat: add multi-root workspace support for auto-approve file reads (#6412)
* feat: add multi-root workspace support for auto-approve file reads

Add logic to handle auto-approval of file read operations in multi-root workspace scenarios. When multi-root is enabled and multiple workspaces are present, the system now checks if a file is located in any workspace rather than just the current working directory. This ensures proper auto-approval behavior across all workspace roots while maintaining backward compatibility for single-root workspaces.

* Fixing pulsing border
2025-09-23 17:57:12 -07:00
Bee 1c70089521 initialize compact mode to prevent UI glitching on mount (#6413)
- Set isCompactMode initial state to true instead of false
- Replace CSS variable with Tailwind class for settings title
- Prevents layout shifts and UI glitching during component initialization
2025-09-23 17:05:48 -07:00
Bee fe2a8a9477 dev: disable auto-condense threshold configuration in task header (#6407)
Remove autoCondenseThreshold prop and hardcode useAutoCondense to false in TaskHeader component to temporarily disable the configurable auto-condense threshold from UI

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-09-23 16:41:57 -07:00
nihar-oracle eb2550ec3d fix(oca): add auth guard, unify axios config, and rotate default IDCS client id (#6410)
- refreshOcaModels:

  - Add explicit auth guard: show a user-facing error if no OCA access token is present and return a typed error via OcaCompatibleModelInfo.
  - Switch axios invocation to use getAxiosSettings() (fetch adapter) instead of proxy agents.
  - Replace getProxyAgents import with getAxiosSettings.

- OcaAuthProvider:

  - Migrate all axios calls (discovery + token POSTs) to use getAxiosSettings().
  - Add explicit error when no id_token is returned from OCA during the auth code exchange to improve diagnostics.

- utils/constants:
  - Rotate DEFAULT_IDCS_CLIENT_ID to a new value.

- utils/utils:

  - Create getAxiosSettings() helper (uses axios fetch adapter) and remove proxy agent–specific helpers.

  - Revise createOcaHeaders to avoid direct vscode + package.json coupling:

    - Use HostProvider.env.getHostVersion for host/IDE details.
    - Use ExtensionRegistryInfo.version for the extension version.
    - Set headers: client=Cline, client-version, client-ide, client-ide-version, opc-request-id.

  - Note: import of HttpsProxyAgent remains but is now unused; consider removing to avoid lint/TS warnings.

Rationale

- Reliability/UX: Users now receive a clear error when attempting to refresh OCA models without being authenticated.
- Portability: Replacing direct vscode and package.json usage in headers with HostProvider + ExtensionRegistryInfo reduces coupling and makes code host-agnostic.
- Network config simplification: Standardize axios setup through a single getAxiosSettings() helper and the fetch adapter.
- Auth robustness: Explicitly surface the absence of id_token to speed up troubleshooting OIDC flows.

Potential behavior changes

- Proxy handling: getProxyAgents() was removed in favor of the axios fetch adapter via getAxiosSettings(). If explicit HTTP(S)_PROXY env-based proxying is required, follow-up work may be needed to reintroduce agent support or configure fetch-compatible proxying.

Files touched

- src/core/controller/models/refreshOcaModels.ts
- src/services/auth/oca/providers/OcaAuthProvider.ts
- src/services/auth/oca/utils/constants.ts
- src/services/auth/oca/utils/utils.ts
2025-09-23 15:36:31 -07:00
Bee 9c3ac14cab feat: add multi-root workspace setting with feature flag support (#6409)
* feat: add multi-root workspace setting with feature flag support

Add user-configurable multi-root workspace setting that works in conjunction with feature flags. Includes proto definition, state management, and UI toggle in settings panel.

- Add multi_root_enabled field to UpdateSettingsRequest proto
- Implement multiRootSetting with user preference and feature flag state
- Add ClineFeatureSetting interface for feature flag + user setting pattern
- Create settings UI toggle with feature flag override indication
- Update state helpers to properly handle boolean conversion

* remove docs

* remove env vars
2025-09-23 15:34:50 -07:00
canvrno 6a8f900d75 REmove VCS requirement from multiroot checkpoints (#6405)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-23 13:09:46 -07:00
Bee 407e472322 fix: poll feature flags for all users (#6404)
* fix: poll feature flags for all users instead of just authenticated users

Move feature flags polling outside the authenticated user check to ensure all users (logged in and anonymous) receive up-to-date feature flags. Reset flags only for authenticated users to maintain proper user-specific configuration.

* add changeset
2025-09-23 12:59:55 -07:00
Saoud Rizwan 580b2e35e2 Update CODEOWNERS to remove dcbartlett (#6408) 2025-09-23 12:54:50 -07:00
Sarah Fortune 30c121509f Add a workflow that will trigger the Jetbrains tests for PRs in the cline repo (#6402)
Changes in the cline repo have the potential to cause breakages in the JetBrains repo. The creator of the PR might not found out until some time later that they have made a change that causes problems for JetBrains.

This workflow will trigger the workflow .github/workflows/test-changes-from-cline-repo.yml in the JetBrains repo, that runs the JetBrains integration tests. When the tests complete, the workflow will leave a comment on the PR with the results of the tests.
2025-09-23 10:13:38 -07:00
Bee b3aee3857c feat: new Task Header UI with configurable auto condense threshold (#6049)
* Update TaskHeader UI

- Create new FocusChainContainer component for todo list management
- Add onSendMessage prop to TaskHeader and TaskSection components
- Refactor TaskHeader to use new FocusChainContainer
- Update tooltip styling and Tailwind configuration
- Improve task progress visualization and interaction handling

* use tailwind

* Remove edit thread btn

* Add interactive auto-compact marker to context window bar

- Add state management for auto-compact marker position (default 75%)
- Make context window bar clickable to reposition marker
- Wrap entire bar in tooltip instead of just marker
- Add click handler to calculate percentage from mouse position
- Fix cost display conditional and remove redundant CSS property

* Prevent event bubbling in task header button clicks

Add preventDefault and stopPropagation to all task header button click handlers to prevent unintended parent element interactions. Also standardize styling by replacing inline styles with Tailwind classes and update text color class for consistency.

* Clean up

* Add CheckpointError component and clean up task header UI

- Add new CheckpointError component for error handling
- Remove commented code from FocusChainContainer
- Add rounded corners to focus chain container
- Refactor formatLargeNumber function with default parameter handling
- Clean up token display formatting in TaskHeader

* Add configurable auto-condense threshold for context window management

- Add autoCondenseThreshold setting to control when context window compaction occurs
- Update ContextManager to accept threshold percentage parameter (0-100%)
- Add ContextWindowDetails component to display context usage in task header
- Extend protobuf schema and state management for new threshold setting
- Default threshold set to 75% when auto-condense is enabled

* v2 style

* Dynamic marker & remove HeroTooltip

* Update styles

* feat: enhance task header UX with expanded progress bar hitbox smart boundary detection timeline block hover dimming effects optimized spacing and standard confirmation dialog styling

* Extract TaskHeader styles to CSS module

Move inline styles from TaskHeader component to external CSS module file for better maintainability and separation of concerns.

* migrate to tailwind

* migrate inline styes to tailwind for FocusChain

* fix(ui): fix useEffect cleanup and add key prop to InfoRow

- Return cleanup function from useEffect instead of nested setTimeout
- Add key prop to InfoRow component to ensure proper re-rendering
- Improves component lifecycle management and prevents memory leaks

* price tag and warning positioning

* clean up & persist expand state

* debounce

* add keyboard navigation for auto condense threshold slider

Add arrow key controls to adjust auto condense threshold with 5% steps (10% with Shift). Include focus management, accessibility attributes, and click-outside handling for improved UX.

* feat: small vertical positioning adjustment

* add changeset

* Update Task Action Button Text

* clean up

* Update storybook

* Apply styling feedback

* clean up

* feat: accordian style metadata for context window bar tooltip

* fix: notch slide fix

* fix: add cleanup for animation frames and timeouts in AutoCondenseMarker

Add proper cleanup functions to useEffect hooks to prevent memory leaks by canceling animation frames and clearing timeouts when component unmounts or dependencies change.

* Fix animation on mount

* remove handleBlur

* fix: change the order of the instructional text

* fix: remove the color from the tooltip percentage value

* clean up

* simplify

* useAutoCondense

* remove highlights

* set maxAllowedSize

* Auto Compact

* remove fork button

* feat: add configurable auto-condense threshold setting

Add auto_condense_threshold parameter to control when context window compaction occurs. The threshold is configurable as a percentage (0-1 range) of the total context window size, allowing users to customize when automatic condensing triggers instead of using a fixed maximum size.

Changes:
- Add autoCondenseThreshold field to protobuf UpdateSettingsRequest
- Update ApiProviderInfo interface to include autoCondenseThreshold
- Modify shouldCompactContextWindow to accept threshold percentage parameter
- Add threshold validation and state management in updateSettings
- Include autoCondenseThreshold in controller state and UI data flow

* autoCondenseThreshold

* fix package

---------

Co-authored-by: Jose R. Perez <trupix@gmail.com>
2025-09-23 09:21:07 -07:00
Bee 10cbbb2c6b feat: Cline Auth Provider (#6131)
* Add ClineAuthProvider and integrate with AuthService

- Add new ClineAuthProvider class for Cline-specific authentication
- Update AuthService to support both Firebase and Cline auth providers
- Switch default provider from Firebase to Cline
- Add dynamic auth URL fetching for Cline provider
- Update type definitions to support multiple provider types

* Add API auth URL configuration and update Cline auth flow

- Add apiAuthUrl to environment configs for all environments
- Update ClineAuthProvider to use new token exchange API endpoint
- Add shared Cline API utilities and types
- Refactor auth service to handle access tokens with expiration
- Update mock auth service and test fixtures for new auth flow

* add changeset

* wip

* refactor authentication service and improve token handling

- Add null check for auth token in ClineAccountService
- Update authorization header format to use 'workos:' prefix
- Replace hardcoded API endpoints with CLINE_API_ENDPOINT constants
- Refactor ClineAuthInfo interface to use accessToken terminology
- Remove Firebase auth provider dependency
- Simplify auth callback handling and token storage
- Improve error handling for missing authentication tokens

* refactor(auth): standardize workos token prefix handling

Centralize workos: prefix application in AuthService.getAuthToken() method instead of duplicating across multiple API call sites. This ensures consistent authentication token formatting and simplifies maintenance by having a single source of truth for token prefixing.

* Remove unused code

* update mock responses

* Set Firebase Auth Provider as default

* Update src/shared/cline/api.ts

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

* remove docs

* refactor(auth): replace type union with IAuthProvider interface

- Replace AvailableAuthProvider type union with IAuthProvider interface for better extensibility
- Add ServiceConfig type for provider configuration
- Remove hardcoded providerName field in favor of provider.name property
- Update method signatures to use IAuthProvider interface
- Improve error messages and code comments for clarity
- Add TODO for mock auth provider implementation

---------

Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
2025-09-23 08:23:22 -07:00
Bee 08365b3e0b feat: add configurable auto-condense threshold setting (#6391)
* feat: add configurable auto-condense threshold setting

Add auto_condense_threshold parameter to control when context window compaction occurs. The threshold is configurable as a percentage (0-1 range) of the total context window size, allowing users to customize when automatic condensing triggers instead of using a fixed maximum size.

Changes:
- Add autoCondenseThreshold field to protobuf UpdateSettingsRequest
- Update ApiProviderInfo interface to include autoCondenseThreshold
- Modify shouldCompactContextWindow to accept threshold percentage parameter
- Add threshold validation and state management in updateSettings
- Include autoCondenseThreshold in controller state and UI data flow

* updateSettings
2025-09-23 06:48:12 -07:00
canvrno cb4c61b1ca Dependency changes (#6393)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-22 23:36:50 -07:00
canvrno 2d9ff863b7 Update dependencies (#6390)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-22 20:16:33 -07:00
Ara ae9b20a12b Adding Search tool for multi root workspace (#6288)
* Adding multi search

* Adding multi search

* Adding multi search

* Adding multi search

* feat: Cleanup

* feat: Cleanup

* feat: Cleanup

* feat: Cleanup

* Fixing search and workspace path stuff
2025-09-22 19:17:40 -07:00
Ara c16e271c14 Adding voice mode to Cline (#6208)
* Adding voice mode
2025-09-22 18:50:56 -07:00
Bee b940cef0e4 refactor: append stealth models on readOpenRouterModels & refreshOpenRouterModels (#6384)
* refactor: extract stealth models to reusable function

Move hardcoded stealth model addition from inline code to a dedicated `appendStealthModels` function. This improves code organization by centralizing stealth model management and ensures consistent application across both fresh API responses and cached model data.

* refactor(controller): integrate stealth models into OpenRouter handling

- Added import and integrated appendClineStealthModels in readOpenRouterModels
- Improved error handling with try-catch in readOpenRouterModels
- Refactored refreshOpenRouterModels to use controller method and renamed functions
- Renamed STEALTH_MODELS to CLINE_STEALTH_MODELS and made appendClineStealthModels exportable

* await cached models for immediate UI availability

Changed the initialization to synchronously await and post last cached OpenRouter models, improving UI responsiveness by making them available as soon as possible instead of relying on a promise chain.
2025-09-22 18:47:02 -07:00
tjandy98 bc228de20e Update SAP AI Core Provider Anthropic input token calculation (#6363)
* Update anthropic input token usage calculation

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

* changeset

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

---------

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>
2025-09-22 18:41:21 -07:00
Ara a9cac3206a feat: Fixing dictation settings (#6385) 2025-09-22 17:38:31 -07:00
Jose Castelli a9bc4c7d67 Testing platform coverage (#6332)
Testing platform coverage
2025-09-22 21:14:54 +02:00
celestial-vault 737dce09d1 Remove duplicate settings references in Task class (#6336)
* read settings from statemanager instead of keeping stale references in Task class and child classes

* removed unused vars
2025-09-22 12:02:17 -07:00
ZeroAurora a10d778bee fix: remove temperature settings in z.ai models (#6311)
To use the default temperature. (#6223 comment)
2025-09-22 12:00:13 -07:00
Jose Castelli 58b0ea9afa Run Testing platform within Test workflow [shadow] (#6273)
Run Testing platform within Test workflow [shadow]
2025-09-22 20:15:22 +02:00
canvrno 90fa3d7336 Dependency updates (#6096)
* Dependency updates

* package-lock

* Updated packages

---------

Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-22 10:39:11 -07:00
canvrno d0da22d5a9 Modify checkpoints to accept an array of workspaces (#6320)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-22 10:06:02 -07:00
nihar-oracle f2ce0b46a3 fix: Fixing oca provider utils (#6359) 2025-09-20 22:14:54 -07:00
github-actions[bot] 52ab767e44 v3.30.3 Release Notes (#6352)
* changeset version bump

* Updating CHANGELOG.md format

* Update version from 3.31.0 to 3.30.3

* 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-09-19 17:57:08 -07:00
Ara d858fb9360 Empty PR to bump changeset (#6351)
* Empty PR to bump changeset

* feat: Cleanup
2025-09-19 17:49:33 -07:00
nihar-oracle 428cdb670a feat(oca): add OCA provider with model picker and auth integration (#6339)
- Add webview UI components:

  - webview-ui/src/components/settings/providers/OcaProvider.tsx
  - webview-ui/src/components/settings/providers/OcaModelPicker.tsx

- Update settings to surface OCA:
  - webview-ui/src/components/settings/ApiOptions.tsx

- Wire backend for OCA auth and controller:

  - src/services/auth/oca/OcaAuthService.ts
  - src/services/auth/oca/providers/OcaAuthProvider.ts
  - src/core/controller/index.ts
2025-09-19 17:27:45 -07:00
Toshii aab002fe65 adding yolo mode telemetry which is triggered in grpc call updated (#6348) 2025-09-19 16:45:52 -07:00
Saoud Rizwan cc540d8158 Remove comment on cacheWritesPrice in clineCodeSupernovaModelInfo 2025-09-19 15:05:22 -07:00
Saoud Rizwan adc15c79d7 Fix copy 2025-09-19 14:57:54 -07:00
Saoud Rizwan af05d3497a v3.30.2 Release Notes 2025-09-19 14:51:19 -07:00
Saoud Rizwan 23fa1cb481 Fix Announcement banner UI tests 2025-09-19 14:50:33 -07:00
github-actions[bot] 800967d851 v3.30.1 Release Notes (#6343)
* changeset version bump

* Updating CHANGELOG.md format

* 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-09-19 14:25:10 -07:00
Saoud Rizwan 91deede3c3 fix: model list not being updated in time for user to use shortcut button to update model to stealth model (#6347)
* fix: model list not being updated in time for user to use shortcut button to update model to stealth model

* Create tame-rabbits-travel.md
2025-09-19 14:21:32 -07:00
pashpashpash b11e6171ff YOLO MODE (#6340)
* add yolo mode setting

* more explicit warning

* changeset
2025-09-19 13:43:50 -07:00
Bee 42666d9ca7 fix: use webview dependencies (#6344) 2025-09-19 13:40:32 -07:00
Bee 07f944b668 fix: update SectionHeader to prevent content overlap (#6337)
Remove sticky positioning and z-index styling from SectionHeader component to fix overlapping content issues during scroll. Also clean up unused imports and update description text styling to use semantic class.
2025-09-19 13:28:20 -07:00
canvrno c6e5b1509c Fix flicker issue when switching modes (#6341)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-09-19 13:04:29 -07:00
github-actions[bot] 6787dedf47 v3.30.0 Release Notes (#6338)
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for Oracle Code Assist integration

---------

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-09-19 12:04:04 -07:00
Saoud Rizwan d07648746a Add code-supernova stealth model (#6327)
* Update announcement banner

* Fix banner buttons

---------

Co-authored-by: pashpashpash <nik@cline.bot>
2025-09-19 11:53:07 -07:00
nihar-oracle 07b49baa2a feat: OCA provider (#5075)
* feat(oca): add Oracle Code Assist provider; auth, models, settings

- Implement OCA API handler (src/core/api/providers/oca.ts)

  - OpenAI-compatible chat.completions with custom fetch injecting OCA headers
  - Optional reasoning (thinking) budget and ephemeral prompt caching
  - LiteLLM session tracking and usage streaming; cost via /spend/calculate
  - Guarded client init; clear error when OCA access token is missing

- Wire provider into core API and controller

  - Register provider (src/core/api/index.ts)
  - Controller flows for OCA account login/logout and auth status subscription
  - refreshOcaModels command and model config plumbing

- Add protobuf surfaces

  - proto/cline/models.proto and proto/cline/ocaAccount.proto
  - Extend proto/cline/state.proto for settings/state

- Persist settings/state and helpers
  - Update StateManager, state-keys, state-helpers, updateSettings

- Misc

  - Add changeset entry
  - Minor .gitignore and commit-message generator tweak

feat: New redirect server

feat(oca): add Oracle Code Assist provider; auth, models, settings

- Implement OCA API handler (src/core/api/providers/oca.ts)

  - OpenAI-compatible chat.completions with custom fetch injecting OCA headers
  - Optional reasoning (thinking) budget and ephemeral prompt caching
  - LiteLLM session tracking and usage streaming; cost via /spend/calculate
  - Guarded client init; clear error when OCA access token is missing

- Wire provider into core API and controller

  - Register provider (src/core/api/index.ts)
  - Controller flows for OCA account login/logout and auth status subscription
  - refreshOcaModels command and model config plumbing

- Add protobuf surfaces

  - proto/cline/models.proto and proto/cline/ocaAccount.proto
  - Extend proto/cline/state.proto for settings/state

- Persist settings/state and helpers
  - Update StateManager, state-keys, state-helpers, updateSettings

- Misc

  - Add changeset entry
  - Minor .gitignore and commit-message generator tweak

feat: New redirect server

update UI and add NPS survey link

papercuts

fix model dropdown height

fix: Fix 1

fix: removing ui

feat(AuthManager): Adding an AuthManager

fix: fixing rebase

* fix: Removing AuthManager, simplifyng auth service initialization
2025-09-19 00:33:24 -07:00
Alex Ker 2668bcdbe0 added baseten link to docs.json (#6312)
* added baseten link to docs.json

* fixed docs formatting

---------

Co-authored-by: AlexKer <AlexKer@users.noreply.github.com>
2025-09-18 16:25:55 -07:00
github-actions[bot] e5e293c32b v3.29.2 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for v3.29.2 release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-18 15:56:16 -07:00
Bee fea8313695 Revert "fix: configure HeroUI setup (#6279)" (#6323)
* Revert "fix: configure HeroUI setup (#6279)"

This reverts commit 5c2d93617f.

* changeset
2025-09-18 15:44:01 -07:00
canvrno 4b450f4488 Pass max_tokens to moonshot provider (#6316)
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-18 14:21:26 -07:00
Sarah Fortune 2687ae149f Set telemetry env vars for JetBrains builds [esbuild] (#6299) 2025-09-18 20:45:05 +00:00
celestial-vault 3b93871b50 use consolidated type parameters for getter functions in state-helpers (#6262)
* use consolidated type parameters for getter functions in state-helpers

* fix types

* fix types
2025-09-18 12:55:43 -07:00
Jose Castelli 767b81b22b Improve standalone startup times (#6272)
Improve standalone startup times
2025-09-18 21:38:46 +02:00
Jose Castelli 5db4970c7d Enhance Testing Framework - Improve non-deterministic scenarios + fix flag (#6244)
Enhance Testing Framework - Improve non-deterministic scenarios + fix flag
2025-09-18 21:27:41 +02:00
Sarah Fortune 4f66126a8c Consolidate duplicated code in BrowserSession and UrlContextFetcher (#6289)
Move duplicated code to utils.ts

Replace context.globalStorageUri with the HostProvider.globalStorageFsPath.
This is part of removing dependencies on the VSCode API fom the codebase except for in platform specific code in src/hosts/vscode and src/extension.ts.
2025-09-18 11:40:44 -07:00
Sarah Fortune 43006ca401 Update runclinecore.sh script (#6300) 2025-09-18 10:12:56 -07:00
github-actions[bot] 1064c631c5 v3.29.1 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-17 20:15:56 -07:00
canvrno 981fe9cf09 Changeset bump and announcement update (#6290)
Co-authored-by: Kevin Bond <kevin@Mac.hsd1.ca.comcast.net>
2025-09-17 20:01:36 -07:00
Sarah Fortune 90ab59d8f5 Replace context.globalStorageUri with the HostProvider in CheckpointTracker (#6277)
* Replace context.globalStorageUri with the HostProvider in CheckpointTracker

Replace context.globalStorageUri with the HostProvider.globalStorageFsPath.

Remove globalStoragePath param, it doesn't need to be passed around anymore.

Remove unused param taskId from shadowGit

* Remove globalStoragePath param from checkpoint manager.

Now tht the global storage path is available from the HostProvider anywhere, it doesn't need to be passed around.
2025-09-17 17:03:30 -07:00
kvyb f237dda413 fix: test e2e click “Show Code Actions” button then wait for listbox on macOS (#6287)
* fix: test e2e click “Show Code Actions” button then wait for listbox on macOS

* fix: trigger code actions during test with shortcut

* fix: add reasonable timeout to the menu and listbox call in test
2025-09-18 02:30:54 +03:00
Bee f08b0499d5 test: refactor chat e2e tests to support multi-roots workspace types (#6282)
* test: refactor chat e2e tests to support multiple workspace types

- Convert single test functions to parameterized test suite using describe blocks
- Add workspace type iteration for both chat messaging and slash command tests
- Consolidate test structure to run against different workspace configurations
- Maintain existing test logic while improving test coverage and organization
- consolidate chat input tests into single comprehensive test

Merged three separate chat input tests (slash commands, @ mentions, and partial completion) into one comprehensive test to reduce test setup time and improve test efficiency. Simplified test structure while maintaining all original functionality checks.

* update keybindings
2025-09-17 16:00:56 -07:00
Bee 5c2d93617f fix: configure HeroUI setup (#6279)
* fix: configure HeroUI setup

- Simplify Storybook stories pattern to only include TypeScript files
- Update HeroUI to v2.8.4 with dependencies
- Refactor HeroUI configuration with VSCode theme integration
- Streamline Tailwind CSS imports and plugin setup

* darkmode

* Theme

* remove workflow
2025-09-17 14:35:22 -07:00
celestial-vault 99201f9944 split up global state and settings types; split apart methods for fetching state and settings (#6263) 2025-09-17 14:11:56 -07:00
Sarah Fortune 0983a8a4b9 Replace context.globalStorageUri with HostProvider.globalStorageFsPath (#6276) 2025-09-17 21:05:10 +00:00
397 changed files with 37333 additions and 19948 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Added checkpoints warning when users start a multiroot task
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Added markdown support to focus chain text, allowing the model to display more interesting focus chains
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Interactive playwright script
+2 -2
View File
@@ -1,3 +1,3 @@
/docs/
/.github/ @saoudrizwan @dcbartlett
/README.md @saoudrizwan @nickbaumann98
/.github/ @saoudrizwan @garoth @sjf
/README.md @saoudrizwan @nickbaumann98
+2 -2
View File
@@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
**Important:** All bug reports must be reproducible using Claude Sonnet 4. Cline uses complex prompts so less capable models may not work as expected.
**Important:** All bug reports must be reproducible using Claude Sonnet 4.5. Cline uses complex prompts so less capable models may not work as expected.
- type: dropdown
id: plugin-type
attributes:
@@ -49,7 +49,7 @@ body:
attributes:
label: Provider/Model
description: What provider and model were you using when the issue occurred?
placeholder: 'e.g., cline:anthropic/claude-sonnet-4, gemini:gemini-2.5-pro-exp-03-25'
placeholder: 'e.g., cline:anthropic/claude-sonnet-4.5, gemini:gemini-2.5-pro-exp-03-25'
validations:
required: false
- type: textarea
-43
View File
@@ -1,43 +0,0 @@
# Source: https://storybook.js.org/docs/sharing/publish-storybook#github-pages
name: Publish Storybook to GitHub Pages
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# Manual Checkout
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Set up Node
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
- name: Install root dependencies
run: npm ci --include=optional
- name: Install webview dependencies
run: cd webview-ui && npm ci --include=optional
- name: Build Tests and Extension
run: npm run pretest
#👇 Add Storybook build and deploy to GitHub Pages as a step in the workflow
- uses: bitovi/github-actions-storybook-to-github-pages@v1.0.3
with:
build_command: cd webview-ui && npm run build-storybook
path: webview-ui/storybook-static
checkout: false
+107 -116
View File
@@ -1,6 +1,9 @@
name: Tests
on:
push:
branches:
- main
workflow_dispatch:
pull_request:
branches:
@@ -14,7 +17,45 @@ permissions:
pull-requests: write # Needed to add comments/annotations to PRs
jobs:
quality-checks:
runs-on: ubuntu-latest
name: Quality Checks
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Run Quality Checks (Parallel)
run: npm run ci:check-all
test:
needs: quality-checks
strategy:
fail-fast: false
matrix:
@@ -33,18 +74,6 @@ jobs:
with:
node-version: 22
# Setup Python for coverage script
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install requests
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
@@ -52,7 +81,6 @@ jobs:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
@@ -68,60 +96,45 @@ jobs:
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Install xvfb on Linux
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y xvfb
- name: Set up NPM on Windows
if: runner.os == 'Windows'
run: |
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
- name: Type Check
run: npm run check-types
- name: Lint Check
run: npm run lint
- name: Format Check
run: npm run format
# Build the extension before running tests
# Build the extension and tests (without redundant checks)
- name: Build Tests and Extension
run: npm run pretest
id: build_step
run: npm run ci:build
- name: Unit Tests (with coverage on Linux)
- name: Unit Tests with coverage - Linux
id: unit_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
run: |
if [ "${{ runner.os }}" = "Linux" ]; then
npm install --no-save nyc
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
else
npm run test:unit
fi
# Run extension tests with coverage
- name: Extension Integration Tests with Coverage
id: extension_coverage
continue-on-error: true
- name: Unit Tests - Non-Linux
id: unit_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
run: |
node ./scripts/test-ci.js 2>&1 | tee extension_coverage.txt
# Default the encoding to UTF-8 - It's not the default on Windows
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
npm run test:unit
- name: Extension Integration Tests - Linux
id: integration_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
run: xvfb-run -a npm run test:coverage
- name: Extension Integration Tests - Non-Linux
id: integration_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
run: npm run test:integration
# Run webview tests with coverage
- name: Webview Tests with Coverage
id: webview_coverage
continue-on-error: true
id: webview_tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: |
cd webview-ui
# Ensure coverage dependency is installed
npm install --no-save @vitest/coverage-v8
npm run test:coverage 2>&1 | tee webview_coverage.txt
cd ..
# Default the encoding to UTF-8 - It's not the default on Windows
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage webview-ui/webview_coverage.txt --type=webview --github-output --verbose
npm run test:coverage
# Save coverage reports as artifacts (workflow-scoped)
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
# Only upload artifacts on Linux - We only need coverage from one OS
@@ -129,54 +142,21 @@ jobs:
with:
name: pr-coverage-reports
path: |
extension_coverage.txt
webview-ui/webview_coverage.txt
coverage-unit/lcov.info
webview-ui/coverage/lcov.info
# Set the check as failed if any of the tests failed
- name: Check for test failures
run: |
# Check if any of the test steps failed
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context
if [ "${{ steps.extension_coverage.outcome }}" != "success" ]; then
echo "Extension Integration Tests failed, see previous step for test output."
fi
if [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
echo "Webview Tests failed, see previous step for test output."
fi
if [ "${{ steps.extension_coverage.outcome }}" != "success" ] || [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
exit 1
fi
coverage:
needs: test
test-platform-integration:
needs: quality-checks
runs-on: ubuntu-latest
# Only run on PRs to main branch
if: github.event_name == 'pull_request' && github.base_ref == 'main'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for accurate comparison
# Setup Python for coverage script
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
@@ -184,7 +164,6 @@ jobs:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
@@ -192,6 +171,14 @@ jobs:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
# Cache testing-platform dependencies
- name: Cache testing-platform dependencies
uses: actions/cache@v4
id: testing-platform-cache
with:
path: testing-platform/node_modules
key: ${{ runner.os }}-npm-testing-platform-${{ hashFiles('testing-platform/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
@@ -200,46 +187,37 @@ jobs:
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
# Build the extension before running tests
- name: Build Extension
run: npm run compile
- name: Compile standalone
run: npm run compile-standalone
# Download coverage artifacts from test job
- name: Download Coverage Reports
uses: actions/download-artifact@v4
with:
name: pr-coverage-reports
path: . # Download to root directory to match expected paths
- name: Install testing platform dependencies
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
run: cd testing-platform && npm ci
# Process coverage workflow
- name: Process coverage workflow
id: coverage
- name: Running testing platform integration spec tests
continue-on-error: true
timeout-minutes: 7
# Temporarily wrapping the test command to always return a neutral exit code.
# This prevents the job from showing as failed and avoids distracting developers
# until the integration tests are ready to be enforced.
run: |
# Extract PR number from GITHUB_REF
PR_NUMBER=$(echo "$GITHUB_REF" | sed -e 's/refs\/pull\///' -e 's/\/merge//')
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage || true
# Run the coverage workflow from root directory
PYTHONPATH=.github/scripts python -m coverage_check process-workflow \
--base-branch ${{ github.base_ref }} \
--pr-number $PR_NUMBER \
--repo $GITHUB_REPOSITORY \
--token ${{ secrets.GITHUB_TOKEN }} \
--verbose
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
with:
name: test-platform-integration-core-coverage
path: coverage/**/lcov.info
qlty:
needs: test
needs: [test, test-platform-integration]
runs-on: ubuntu-latest
# Run on PRs to main, pushes to main, and manual dispatches
if: always() && needs.test.result == 'success'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for accurate comparison
- name: Download Coverage Reports
- name: Download unit tests coverage reports
uses: actions/download-artifact@v4
with:
name: pr-coverage-reports
@@ -263,3 +241,16 @@ jobs:
webview-ui/coverage/lcov.info
tag: unit:webview-ui
add-prefix: webview-ui/
- name: Download test platform integration core coverage artifact
uses: actions/download-artifact@v4
with:
name: test-platform-integration-core-coverage
path: integration-core-coverage-reports
- name: Upload core integration tests coverage to Qlty
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
files: integration-core-coverage-reports/**/lcov.info
tag: integration:core
@@ -0,0 +1,53 @@
name: Trigger Jetbrains Plugin <-> Cline Tests
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
concurrency:
group: jetbrains-trigger-${{ github.event.number }}
cancel-in-progress: true
jobs:
trigger-integration-test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: 1998650
private-key: ${{ secrets.CLINE_JETBRAINS_WORKFLOW_KEY }}
owner: cline
repositories: intellij-plugin
- name: Trigger IntelliJ Plugin Integration Test
run: |
curl -X POST \
-H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \
-H "Accept: application/vnd.github.v3+json" \
-H "User-Agent: cline-pr-trigger" \
-H "Content-Type: application/json" \
https://api.github.com/repos/cline/intellij-plugin/dispatches \
-d @- <<EOF
{
"event_type": "cline-pr-check",
"client_payload": {
"pr_number": "${{ github.event.number }}",
"branch_name": "${{ github.head_ref }}",
"action": "${{ github.event.action }}",
"sha": "${{ github.event.pull_request.head.sha }}",
"pr_title": ${{ toJSON(github.event.pull_request.title) }},
"pr_url": "${{ github.event.pull_request.html_url }}"
}
}
EOF
- name: Log trigger details
run: |
echo "Triggered IntelliJ Plugin integration test for:"
echo " PR #${{ github.event.number }}: ${{ github.event.pull_request.title }}"
echo " Branch: ${{ github.head_ref }}"
echo " Action: ${{ github.event.action }}"
echo " SHA: ${{ github.event.pull_request.head.sha }}"
-3
View File
@@ -34,6 +34,3 @@ webview-ui/src/services/grpc-client.ts
# E2E Tests
test-results
## CLI pre-release ##
/cli
+81
View File
@@ -1,5 +1,86 @@
# Changelog
## [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
## [3.32.5]
- Improve thinking budget slider UI to take up less space
- Fix Vercel provider cost note and sign-up url
- Fix repeated API error 400 in SAP AI Core provider
- Add us-west-1 to Amazon Bedrock regions
- Fix OCA provider refresh logic
## [3.32.4]
- Add 1m context window support to Claude Sonnet 4.5
- Add Claude Sonnet 4.5 to GCP Vertex
- Add prompt caching support for OpenRouter accidental `anthropic/claude-4.5-sonnet` model ID
## [3.32.3]
- Add Claude Sonnet 4.5 to Bedrock provider
- Add Alert banner for new Claude Sonnet 4.5 model
## [3.32.2]
- Add Claude Sonnet 4.5 to Cline/OpenRouter/Anthropic providers
- Add /task deep link handler
## [3.32.1]
- Preserve reasoning traces for Cline/OpenRouter/Anthropic providers to maintain conversation integrity
- Add automatically retry on rate limit errors with SAP AI Core provider
- Fix Cline accounts using stale id token at refresh response
- Minor UI improvements to Settings and Task Header
## [3.32.0]
- Added the new code-supernova-1-million stealth model, available for free and delivering a 1 million token context window
- Changes to inform Cline about commands that are available on your system
## [3.31.1]
- Version bump
## [3.31.0]
- UI Improvements: New task header and focus chain design to take up less space for a cleaner experience
- Voice Mode: Experimental feature that must be enabled in settings for hands-free coding
- YOLO Mode: Enable in settings to let Cline approve all actions and automatically switch between plan/act mode
- Fix Oracle Code Assist provider issues
## [3.30.3]
- Add Oracle Code Assist provider
## [3.30.2]
- Fix UI tests
## [3.30.1]
- Fix model list not being updated in time for user to use shortcut button to update model to stealth model
- Fix flicker issue when switching modes
- Fix Sticky header in settings view overlaping with content on scroll
- Add experimental yolo mode feature that disables all user approvals and automatically executes a task and navigates through plan to act mode until the task is complete
## [3.30.0]
- Add code-supernova stealth model
## [3.29.2]
- Fix: Reverted change that caused formatting issues
- Fix: Moonshot - Pass max_tokens value to provider
## [3.29.1]
- Changeset bump + Announcement banner update
## [3.29.0]
- Updated Baseten provider to fetch models from server
-1
View File
@@ -74,7 +74,6 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
4. Testing
- Run `npm run test` to run tests locally.
- Before submitting PR, run `npm run format:fix` to format your code
- Run `npm run test:ci` to run tests locally
### Extension
+2
View File
@@ -0,0 +1,2 @@
cline-core-debug.log
bin/*
+6
View File
@@ -0,0 +1,6 @@
/_____/\ /_/\ /_______/\/__/\ /__/\ /_____/\
\:::__\/ \:\ \ \__.::._\/\::\_\\ \ \\::::_\/_
\:\ \ __\:\ \ \::\ \ \:. `-\ \ \\:\/___/\
\:\ \/_/\\:\ \____ _\::\ \__\:. _ \ \\::___\/_
\:\_\ \ \\:\/___/\/__\::\__/\\. \`-\ \ \\:\____/\
\_____\/ \_____\/\________\/ \__\/ \__\/ \_____\/
+71
View File
@@ -0,0 +1,71 @@
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/spf13/cobra"
"github.com/cline/cli/pkg/hostbridge"
)
var (
port int
verbose bool
)
func main() {
rootCmd := &cobra.Command{
Use: "cline-host",
Short: "Cline Host Bridge Service",
Long: `A simple host bridge service that provides host operations for Cline Core.`,
RunE: runServer,
}
rootCmd.Flags().IntVarP(&port, "port", "p", 51052, "port to listen on")
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "verbose logging")
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func runServer(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Create gRPC hostbridge service
service := hostbridge.NewGrpcServer(port, verbose)
// Handle graceful shutdown
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
if verbose {
log.Println("Shutting down hostbridge server...")
}
cancel()
}()
// Start server
if verbose {
log.Printf("Starting Cline Host Bridge on port %d", port)
}
// Run the service
if err := service.Start(ctx); err != nil {
return fmt.Errorf("failed to run service: %w", err)
}
return nil
}
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"context"
"fmt"
"os"
"github.com/cline/cli/pkg/cli"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/common"
"github.com/spf13/cobra"
)
var (
coreAddress string
cfgFile string
verbose bool
outputFormat string
)
func main() {
rootCmd := &cobra.Command{
Use: "cline",
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.`,
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,
})
},
}
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.AddCommand(cli.NewTaskCommand())
rootCmd.AddCommand(cli.NewInstanceCommand())
rootCmd.AddCommand(cli.NewVersionCommand())
rootCmd.AddCommand(cli.NewAuthCommand())
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
+154
View File
@@ -0,0 +1,154 @@
package e2e
import (
"context"
"encoding/json"
"os"
"path/filepath"
"syscall"
"testing"
"github.com/cline/cli/pkg/common"
)
// 2. Multi-instance start: default_instance remains the first started.
func TestMultiInstanceDefaultUnchanged(t *testing.T) {
_ = setTempClineDir(t)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Start first instance and wait healthy
_ = mustRunCLI(ctx, t, "instance", "new")
out1 := listInstancesJSON(ctx, t)
if len(out1.CoreInstances) != 1 {
t.Fatalf("expected 1 instance, got %d", len(out1.CoreInstances))
}
firstAddr := out1.CoreInstances[0].Address
waitForAddressHealthy(t, firstAddr, defaultTimeout)
// Start second instance
_ = mustRunCLI(ctx, t, "instance", "new")
out2 := listInstancesJSON(ctx, t)
if len(out2.CoreInstances) < 2 {
t.Fatalf("expected at least 2 instances, got %d", len(out2.CoreInstances))
}
// Default should remain the first started address
if out2.DefaultInstance != firstAddr {
t.Fatalf("default changed; expected %s, got %s", firstAddr, out2.DefaultInstance)
}
}
// 6. Default.json update after removal of current default
func TestDefaultJsonUpdateAfterRemoval(t *testing.T) {
_ = setTempClineDir(t)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Start two instances
_ = mustRunCLI(ctx, t, "instance", "new")
_ = mustRunCLI(ctx, t, "instance", "new")
out := listInstancesJSON(ctx, t)
if len(out.CoreInstances) < 2 {
t.Fatalf("expected at least 2 instances, got %d", len(out.CoreInstances))
}
// Choose second as new default
target := out.CoreInstances[1]
waitForAddressHealthy(t, target.Address, defaultTimeout)
// Set as default
_ = mustRunCLI(ctx, t, "instance", "use", target.Address)
// Verify default switched
out = listInstancesJSON(ctx, t)
if out.DefaultInstance != target.Address {
t.Fatalf("default_instance not updated to %s (got %s)", target.Address, out.DefaultInstance)
}
// Kill the default instance using runtime PID discovery
corePID := getCorePID(t, target.Address)
if corePID <= 0 {
t.Fatalf("could not find PID for core process at %s", target.Address)
}
t.Logf("Killing cline-core process PID %d for instance %s", corePID, target.Address)
if err := syscall.Kill(corePID, syscall.SIGKILL); err != nil {
t.Fatalf("kill pid %d: %v", corePID, err)
}
// Wait for removal
waitForAddressRemoved(t, target.Address, longTimeout)
// Clean up dangling host process (SIGKILL leaves these behind by design)
t.Logf("Cleaning up dangling host process on port %d", target.HostPort())
findAndKillHostProcess(t, target.HostPort())
// Ensure default_instance updated to another available instance (or removed if none remain)
out = listInstancesJSON(ctx, t)
// If there are instances left, default_instance must be one of them
if len(out.CoreInstances) > 0 {
found := false
for _, it := range out.CoreInstances {
if out.DefaultInstance == it.Address {
found = true
break
}
}
if !found {
t.Fatalf("default_instance %s not set to an existing instance after removal", out.DefaultInstance)
}
} else {
// No instances remain; cli-default-instance.json should be removed
clineDir := getClineDir(t)
defPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
if _, err := os.Stat(defPath); err == nil {
t.Fatalf("expected cli-default-instance.json removed when no instances remain")
}
}
// Also verify cli-default-instance.json on disk reflects the in-memory default (if any)
clineDir := getClineDir(t)
defPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
if len(out.CoreInstances) > 0 {
raw, err := os.ReadFile(defPath)
if err != nil {
t.Fatalf("read cli-default-instance.json: %v", err)
}
var tmp struct {
DefaultInstance string `json:"default_instance"`
}
if err := json.Unmarshal(raw, &tmp); err != nil {
t.Fatalf("unmarshal cli-default-instance.json: %v", err)
}
if tmp.DefaultInstance != out.DefaultInstance {
t.Fatalf("cli-default-instance.json mismatch: file=%s list=%s", tmp.DefaultInstance, out.DefaultInstance)
}
}
}
// 11. SQLite database missing (edge): list succeeds and returns empty set
func TestRegistryDirMissingEdge(t *testing.T) {
clineDir := setTempClineDir(t)
// Remove the settings directory entirely (which contains locks.db)
settingsDir := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER)
if err := os.RemoveAll(settingsDir); err != nil {
t.Fatalf("RemoveAll(%s): %v", common.SETTINGS_SUBFOLDER, err)
}
// Listing should succeed and return empty results
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
out := listInstancesJSON(ctx, t)
if len(out.CoreInstances) != 0 {
t.Fatalf("expected 0 instances after removing %s dir, got %d", common.SETTINGS_SUBFOLDER, len(out.CoreInstances))
}
// Ensure cli-default-instance.json not present
defPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
if _, err := os.Stat(defPath); err == nil {
t.Fatalf("expected no cli-default-instance.json after removing %s dir", common.SETTINGS_SUBFOLDER)
}
}
+378
View File
@@ -0,0 +1,378 @@
package e2e
import (
"context"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"testing"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/common"
"github.com/cline/grpc-go/cline"
)
const (
defaultTimeout = 30 * time.Second
longTimeout = 60 * time.Second
pollInterval = 250 * time.Millisecond
instancesBinRel = "../bin/cline"
)
func repoAwareBinPath(t *testing.T) string {
// Tests live in repoRoot/cli/e2e. Binary is at repoRoot/cli/bin/cline
t.Helper()
wd, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd error: %v", err)
}
// cli/e2e -> cli/bin/cline
p := filepath.Clean(filepath.Join(wd, instancesBinRel))
if _, err := os.Stat(p); err != nil {
t.Fatalf("CLI binary not found at %s; run `npm run compile-cli` first: %v", p, err)
}
return p
}
func setTempClineDir(t *testing.T) string {
t.Helper()
dir := t.TempDir()
clineDir := filepath.Join(dir, ".cline")
if err := os.MkdirAll(clineDir, 0o755); err != nil {
t.Fatalf("mkdir clineDir: %v", err)
}
t.Setenv("CLINE_DIR", clineDir)
return clineDir
}
func runCLI(ctx context.Context, t *testing.T, args ...string) (string, string, int) {
t.Helper()
bin := repoAwareBinPath(t)
// Ensure CLI uses the same CLINE_DIR as the tests by passing --config=<CLINE_DIR>
// (InitializeGlobalConfig uses ConfigPath as the base directory for registry.)
if clineDir := os.Getenv("CLINE_DIR"); clineDir != "" && !contains(args, "--config") {
// Prepend persistent flag so Cobra sees it regardless of subcommand position
args = append([]string{"--config", clineDir}, args...)
}
cmd := exec.CommandContext(ctx, bin, args...)
// Run CLI from repo root so relative paths inside CLI (./cli/bin/...) resolve
if wd, err := os.Getwd(); err == nil {
repoRoot := filepath.Clean(filepath.Join(wd, "..", ".."))
cmd.Dir = repoRoot
}
// propagate env including CLINE_DIR
cmd.Env = os.Environ()
outB, errB := &strings.Builder{}, &strings.Builder{}
cmd.Stdout = outB
cmd.Stderr = errB
err := cmd.Run()
exit := 0
if err != nil {
// Extract exit code if possible
if ee, ok := err.(*exec.ExitError); ok {
exit = ee.ExitCode()
} else {
exit = -1
}
}
return outB.String(), errB.String(), exit
}
func mustRunCLI(ctx context.Context, t *testing.T, args ...string) string {
t.Helper()
out, errOut, exit := runCLI(ctx, t, args...)
if exit != 0 {
t.Fatalf("cline %v failed (exit=%d)\nstdout:\n%s\nstderr:\n%s", args, exit, out, errOut)
}
return out
}
func listInstancesJSON(ctx context.Context, t *testing.T) common.InstancesOutput {
t.Helper()
// Trigger CLI to perform cleanup/health by invoking list (table output is ignored)
_ = mustRunCLI(ctx, t, "instance", "list")
// Read from SQLite locks database to build structured output
clineDir := getClineDir(t)
// Load default instance from settings file
defaultInstance := readDefaultInstanceFromSettings(t, clineDir)
// Load instances from SQLite
instances := readInstancesFromSQLite(t, clineDir)
return common.InstancesOutput{
DefaultInstance: defaultInstance,
CoreInstances: instances,
}
}
func hasAddress(in common.InstancesOutput, addr string) bool {
for _, it := range in.CoreInstances {
if it.Address == addr {
return true
}
}
return false
}
func getByAddress(in common.InstancesOutput, addr string) (common.CoreInstanceInfo, bool) {
for _, it := range in.CoreInstances {
if it.Address == addr {
return it, true
}
}
return common.CoreInstanceInfo{}, false
}
func waitFor(t *testing.T, timeout time.Duration, cond func() (bool, string)) {
t.Helper()
deadline := time.Now().Add(timeout)
for {
ok, msg := cond()
if ok {
return
}
if time.Now().After(deadline) {
t.Fatalf("waitFor timeout: %s", msg)
}
time.Sleep(pollInterval)
}
}
func waitForAddressHealthy(t *testing.T, addr string, timeout time.Duration) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
t.Logf("Waiting for gRPC health check on %s...", addr)
waitFor(t, timeout, func() (bool, string) {
if common.IsInstanceHealthy(ctx, addr) {
return true, ""
}
return false, fmt.Sprintf("gRPC health check failed for %s", addr)
})
t.Logf("gRPC health check passed for %s", addr)
}
func waitForAddressRemoved(t *testing.T, addr string, timeout time.Duration) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
waitFor(t, timeout, func() (bool, string) {
out := listInstancesJSON(ctx, t)
if hasAddress(out, addr) {
return false, fmt.Sprintf("address %s still present", addr)
}
return true, ""
})
}
func findFreePort(t *testing.T) int {
t.Helper()
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen 127.0.0.1:0: %v", err)
}
defer l.Close()
_, portStr, _ := net.SplitHostPort(l.Addr().String())
var port int
fmt.Sscanf(portStr, "%d", &port)
return port
}
func getClineDir(t *testing.T) string {
t.Helper()
clineDir := os.Getenv("CLINE_DIR")
if clineDir == "" {
t.Fatalf("CLINE_DIR not set")
}
return clineDir
}
// isPortInUse checks if a port is currently in use by any process
func isPortInUse(port int) bool {
conn, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return true // Port is in use
}
conn.Close()
return false // Port is free
}
// waitForPortClosed waits for a port to become free (no process listening)
func waitForPortClosed(t *testing.T, port int, timeout time.Duration) {
t.Helper()
waitFor(t, timeout, func() (bool, string) {
if isPortInUse(port) {
return false, fmt.Sprintf("port %d still in use", port)
}
return true, ""
})
}
// waitForPortsClosed waits for both core and host ports to become free
func waitForPortsClosed(t *testing.T, corePort, hostPort int, timeout time.Duration) {
t.Helper()
waitFor(t, timeout, func() (bool, string) {
if isPortInUse(corePort) {
return false, fmt.Sprintf("core port %d still in use", corePort)
}
if isPortInUse(hostPort) {
return false, fmt.Sprintf("host port %d still in use", hostPort)
}
return true, ""
})
}
// findAndKillHostProcess finds and kills any process listening on the host port
// This is used to clean up dangling host processes after SIGKILL tests
func findAndKillHostProcess(t *testing.T, hostPort int) {
t.Helper()
// Use lsof to find process listening on the host port
cmd := exec.Command("lsof", "-ti", fmt.Sprintf(":%d", hostPort))
output, err := cmd.Output()
if err != nil {
// No process found on port - that's fine
return
}
pidStr := strings.TrimSpace(string(output))
if pidStr == "" {
return
}
var pid int
if _, err := fmt.Sscanf(pidStr, "%d", &pid); err != nil {
t.Logf("Warning: could not parse PID from lsof output: %s", pidStr)
return
}
if pid > 0 {
t.Logf("Cleaning up dangling host process PID %d on port %d", pid, hostPort)
if err := syscall.Kill(pid, syscall.SIGKILL); err != nil {
t.Logf("Warning: failed to kill dangling host process %d: %v", pid, err)
}
}
}
// getPIDByPort returns the PID of the process listening on the specified port (fallback method)
func getPIDByPort(t *testing.T, port int) int {
t.Helper()
cmd := exec.Command("lsof", "-ti", fmt.Sprintf(":%d", port))
output, err := cmd.Output()
if err != nil {
return 0 // Process not found
}
pidStr := strings.TrimSpace(string(output))
if pidStr == "" {
return 0
}
pid, err := strconv.Atoi(pidStr)
if err != nil {
t.Logf("Warning: could not parse PID from lsof output: %s", pidStr)
return 0
}
return pid
}
// getCorePIDViaRPC returns the PID of the cline-core process using RPC (preferred method)
func getCorePIDViaRPC(t *testing.T, address string) int {
t.Helper()
// Initialize global config to access registry
clineDir := os.Getenv("CLINE_DIR")
if clineDir == "" {
t.Logf("Warning: CLINE_DIR not set, falling back to lsof")
return getCorePIDViaLsof(t, address)
}
cfg := &global.GlobalConfig{
ConfigPath: clineDir,
}
if err := global.InitializeGlobalConfig(cfg); err != nil {
t.Logf("Warning: failed to initialize global config, falling back to lsof: %v", err)
return getCorePIDViaLsof(t, address)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Get client for the address
client, err := global.Clients.GetRegistry().GetClient(ctx, address)
if err != nil {
t.Logf("Warning: failed to get client for %s, falling back to lsof: %v", address, err)
return getCorePIDViaLsof(t, address)
}
// Call GetProcessInfo RPC
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
if err != nil {
t.Logf("Warning: GetProcessInfo RPC failed for %s, falling back to lsof: %v", address, err)
return getCorePIDViaLsof(t, address)
}
return int(processInfo.ProcessId)
}
// getCorePIDViaLsof returns the PID using lsof (fallback method)
func getCorePIDViaLsof(t *testing.T, address string) int {
t.Helper()
_, portStr, err := net.SplitHostPort(address)
if err != nil {
t.Logf("Warning: invalid address format %s", address)
return 0
}
port, err := strconv.Atoi(portStr)
if err != nil {
t.Logf("Warning: invalid port in address %s", address)
return 0
}
return getPIDByPort(t, port)
}
// getCorePID returns the PID of the cline-core process for the given address
// Uses RPC first, falls back to lsof if RPC fails
func getCorePID(t *testing.T, address string) int {
t.Helper()
// Try RPC first (preferred method)
if pid := getCorePIDViaRPC(t, address); pid > 0 {
return pid
}
// Fall back to lsof if RPC fails
return getCorePIDViaLsof(t, address)
}
// getHostPID returns the PID of the cline-host process for the given host port
func getHostPID(t *testing.T, hostPort int) int {
t.Helper()
return getPIDByPort(t, hostPort)
}
// contains reports whether slice has the target string.
func contains(slice []string, target string) bool {
for _, s := range slice {
if s == target {
return true
}
}
return false
}
+47
View File
@@ -0,0 +1,47 @@
package e2e
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
// TestMain validates required artifacts exist before running E2E tests.
// It does NOT build artifacts. Build manually via:
//
// npm run compile-standalone
// npm run compile-cli
func TestMain(m *testing.M) {
// Determine repo root from cli/e2e
wd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "getwd: %v\n", err)
os.Exit(2)
}
repoRoot := filepath.Clean(filepath.Join(wd, "..", ".."))
cliBin := filepath.Join(repoRoot, "cli", "bin", "cline")
coreJS := filepath.Join(repoRoot, "dist-standalone", "cline-core.js")
missing := []string{}
if _, err := os.Stat(cliBin); err != nil {
missing = append(missing, cliBin)
}
if _, err := os.Stat(coreJS); err != nil {
missing = append(missing, coreJS)
}
if len(missing) > 0 {
if testing.Short() {
// Optional quality-of-life: allow skipping with -short when artifacts are absent
fmt.Fprintf(os.Stderr, "[e2e] skipping (-short) due to missing artifacts:\n %s\n", strings.Join(missing, "\n "))
os.Exit(0)
}
fmt.Fprintf(os.Stderr, "Missing required build artifacts for E2E tests:\n %s\n\nPlease build them first:\n npm run compile-standalone\n npm run compile-cli\n", strings.Join(missing, "\n "))
os.Exit(2)
}
os.Exit(m.Run())
}
+120
View File
@@ -0,0 +1,120 @@
package e2e
import (
"context"
"fmt"
"os"
"path/filepath"
"syscall"
"testing"
"github.com/cline/cli/pkg/common"
)
// 9. Mixed localhost vs 127.0.0.1 addresses coexist and are both healthy
func TestMixedLocalhostVs127Coexist(t *testing.T) {
clineDir := setTempClineDir(t)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Start one instance
_ = mustRunCLI(ctx, t, "instance", "new")
// Get the running instance and its port/PID
out := listInstancesJSON(ctx, t)
if len(out.CoreInstances) == 0 {
t.Fatalf("expected at least 1 instance")
}
inst := out.CoreInstances[0]
waitForAddressHealthy(t, inst.Address, defaultTimeout)
// Manually add a SQLite entry for the same port but 127.0.0.1 host
addr127 := fmt.Sprintf("127.0.0.1:%d", inst.CorePort())
dbPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "locks.db")
if err := insertRemoteInstanceIntoSQLite(t, dbPath, addr127, inst.CorePort(), inst.HostPort()); err != nil {
t.Fatalf("insert 127 alias entry: %v", err)
}
// Verify both addresses appear and are healthy
waitForAddressHealthy(t, inst.Address, defaultTimeout)
waitForAddressHealthy(t, addr127, defaultTimeout)
out = listInstancesJSON(ctx, t)
if !hasAddress(out, inst.Address) || !hasAddress(out, addr127) {
t.Fatalf("expected both %s and %s present", inst.Address, addr127)
}
}
// 10. Start-stop stress: loop starting then killing instances; ensure no leftovers
func TestStartStopStress(t *testing.T) {
_ = setTempClineDir(t)
for i := 0; i < 3; i++ { // keep small for CI time
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Snapshot current addresses
before := listInstancesJSON(ctx, t)
beforeSet := map[string]struct{}{}
for _, it := range before.CoreInstances {
beforeSet[it.Address] = struct{}{}
}
// Start a new instance
_ = mustRunCLI(ctx, t, "instance", "new")
// Find the new instance address
var newAddr string
waitFor(t, defaultTimeout, func() (bool, string) {
after := listInstancesJSON(ctx, t)
for _, it := range after.CoreInstances {
if _, ok := beforeSet[it.Address]; !ok {
newAddr = it.Address
return true, ""
}
}
return false, "new instance address not detected yet"
})
// Wait healthy
waitForAddressHealthy(t, newAddr, defaultTimeout)
// Get PID using runtime discovery and kill it
after := listInstancesJSON(ctx, t)
info, ok := getByAddress(after, newAddr)
if !ok {
t.Fatalf("new instance %s missing", newAddr)
}
// Get PID using runtime discovery
corePID := getCorePID(t, info.Address)
if corePID <= 0 {
t.Fatalf("could not find PID for new instance at %s", info.Address)
}
t.Logf("Killing new instance %s (PID %d) for iteration %d", info.Address, corePID, i)
if err := syscall.Kill(corePID, syscall.SIGKILL); err != nil {
t.Fatalf("kill pid %d: %v", corePID, err)
}
// Wait removed from SQLite database
waitForAddressRemoved(t, newAddr, longTimeout)
// Verify instance is removed from SQLite database
clineDir := os.Getenv("CLINE_DIR")
if clineDir != "" {
dbPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "locks.db")
if verifyInstanceExistsInSQLite(t, dbPath, newAddr) {
t.Fatalf("expected instance removed from SQLite database: %s", newAddr)
}
}
// Clean up dangling host process (SIGKILL leaves these behind by design)
t.Logf("Cleaning up dangling host process on port %d for iteration %d", info.HostPort(), i)
findAndKillHostProcess(t, info.HostPort())
// Verify both ports are now free
waitForPortsClosed(t, info.CorePort(), info.HostPort(), defaultTimeout)
}
}
+161
View File
@@ -0,0 +1,161 @@
package e2e
import (
"database/sql"
"encoding/json"
"os"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/cline/cli/pkg/common"
_ "github.com/mattn/go-sqlite3"
"google.golang.org/grpc/health/grpc_health_v1"
)
// readInstancesFromSQLite reads instances directly from the SQLite database for testing
func readInstancesFromSQLite(t *testing.T, clineDir string) []common.CoreInstanceInfo {
t.Helper()
dbPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "locks.db")
// Check if database exists
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
return []common.CoreInstanceInfo{}
}
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
t.Logf("Warning: Failed to open SQLite database: %v", err)
return []common.CoreInstanceInfo{}
}
defer db.Close()
// Query instance locks
query := common.SelectInstanceLockHoldersAscSQL
rows, err := db.Query(query)
if err != nil {
t.Logf("Warning: Failed to query instance locks: %v", err)
return []common.CoreInstanceInfo{}
}
defer rows.Close()
var instances []common.CoreInstanceInfo
for rows.Next() {
var heldBy, lockTarget string
var lockedAt int64
err := rows.Scan(&heldBy, &lockTarget, &lockedAt)
if err != nil {
t.Logf("Warning: Failed to scan lock row: %v", err)
continue
}
// Create InstanceInfo
info := common.CoreInstanceInfo{
Address: heldBy,
HostServiceAddress: lockTarget,
Status: grpc_health_v1.HealthCheckResponse_UNKNOWN, // Will be updated by health check
LastSeen: time.Unix(lockedAt/1000, 0), // Convert from milliseconds
}
instances = append(instances, info)
}
return instances
}
// readDefaultInstanceFromSettings reads the default instance from the settings file
func readDefaultInstanceFromSettings(t *testing.T, clineDir string) string {
t.Helper()
settingsPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
data, err := os.ReadFile(settingsPath)
if err != nil {
if os.IsNotExist(err) {
return ""
}
t.Logf("Warning: Failed to read default instance file: %v", err)
return ""
}
var tmp struct {
DefaultInstance string `json:"default_instance"`
}
if err := json.Unmarshal(data, &tmp); err != nil {
t.Logf("Warning: Failed to parse default instance file: %v", err)
return ""
}
return tmp.DefaultInstance
}
// insertRemoteInstanceIntoSQLite inserts a remote instance entry directly into SQLite for testing
func insertRemoteInstanceIntoSQLite(t *testing.T, dbPath, address string, corePort, hostPort int) error {
t.Helper()
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return err
}
defer db.Close()
// Initialize database schema for testing
createTableSQL := `
CREATE TABLE IF NOT EXISTS locks (
id INTEGER PRIMARY KEY,
held_by TEXT NOT NULL,
lock_type TEXT NOT NULL CHECK (lock_type IN ('file', 'instance', 'folder')),
lock_target TEXT NOT NULL,
locked_at INTEGER NOT NULL,
UNIQUE(lock_type, lock_target)
);
`
createIndexesSQL := `
CREATE INDEX IF NOT EXISTS idx_locks_held_by ON locks(held_by);
CREATE INDEX IF NOT EXISTS idx_locks_type ON locks(lock_type);
CREATE INDEX IF NOT EXISTS idx_locks_target ON locks(lock_target);
`
if _, err := db.Exec(createTableSQL); err != nil {
return err
}
if _, err := db.Exec(createIndexesSQL); err != nil {
return err
}
// Insert the remote instance
hostAddress := "remote.example.com:0"
if hostPort != 0 {
hostAddress = "remote.example.com:" + strconv.Itoa(hostPort)
}
insertSQL := `INSERT INTO locks (held_by, lock_type, lock_target, locked_at) VALUES (?, 'instance', ?, ?)`
_, err = db.Exec(insertSQL, address, hostAddress, time.Now().Unix()*1000)
return err
}
// verifyInstanceExistsInSQLite checks if an instance exists in the SQLite database
func verifyInstanceExistsInSQLite(t *testing.T, dbPath, address string) bool {
t.Helper()
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
t.Logf("Failed to open database: %v", err)
return false
}
defer db.Close()
query := `SELECT COUNT(*) FROM locks WHERE held_by = ? AND lock_type = 'instance'`
var count int
err = db.QueryRow(query, address).Scan(&count)
if err != nil {
t.Logf("Failed to query database: %v", err)
return false
}
return count > 0
}
+178
View File
@@ -0,0 +1,178 @@
package e2e
import (
"context"
"fmt"
"syscall"
"testing"
)
// TestStartAndList verifies self-registration and default.json semantics in a fresh CLINE_DIR.
func TestStartAndList(t *testing.T) {
clineDir := setTempClineDir(t)
t.Logf("Using temp CLINE_DIR: %s", clineDir)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
t.Logf("Starting new instance...")
// Start a new instance
startOutput := mustRunCLI(ctx, t, "instance", "new")
t.Logf("Instance start output: %s", startOutput)
t.Logf("Listing instances to check registration...")
// It should appear healthy in list JSON and be the default.
out := listInstancesJSON(ctx, t)
t.Logf("Found %d instances after start", len(out.CoreInstances))
if len(out.CoreInstances) != 1 {
t.Fatalf("expected 1 instance, got %d", len(out.CoreInstances))
}
addr := out.CoreInstances[0].Address
t.Logf("Instance address: %s, status: %s", addr, out.CoreInstances[0].Status)
t.Logf("Waiting for address %s to become healthy...", addr)
waitForAddressHealthy(t, addr, defaultTimeout)
t.Logf("Address %s is now healthy", addr)
t.Logf("Checking default instance configuration...")
// Default should be set to the new instance.
out = listInstancesJSON(ctx, t)
t.Logf("Default instance: %s", out.DefaultInstance)
if out.DefaultInstance == "" {
t.Fatalf("default_instance not set")
}
if out.DefaultInstance != out.CoreInstances[0].Address {
t.Fatalf("expected default_instance=%s, got %s", out.CoreInstances[0].Address, out.DefaultInstance)
}
t.Logf("TestStartAndList completed successfully")
}
// TestTaskNewDefault ensures tasks route to default instance.
func TestTaskNewDefault(t *testing.T) {
_ = setTempClineDir(t)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Start one instance and wait for healthy
_ = mustRunCLI(ctx, t, "instance", "new")
out := listInstancesJSON(ctx, t)
if len(out.CoreInstances) != 1 {
t.Fatalf("expected 1 instance, got %d", len(out.CoreInstances))
}
addr := out.CoreInstances[0].Address
waitForAddressHealthy(t, addr, defaultTimeout)
// Create a new task at default (success is sufficient)
_ = mustRunCLI(ctx, t, "task", "new", "hello world")
}
// TestExplicitAddressAutoStart verifies that giving an explicit address auto-starts an instance and routes the task.
func TestExplicitAddressAutoStart(t *testing.T) {
_ = setTempClineDir(t)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Find a free port and use explicit address. This should auto-start an instance.
port := findFreePort(t)
addr := "localhost:" + itoa(port)
// Run a task at explicit address (auto-start path)
_ = mustRunCLI(ctx, t, "task", "new", "--address", "localhost:"+itoa(port), "explicit address task")
// Verify the instance is present and healthy
waitForAddressHealthy(t, addr, defaultTimeout)
}
// TestCrashCleanup verifies that after SIGKILL of a local core, the cleanup removes the registry entry.
// Also tests graceful shutdown (SIGTERM) vs crash cleanup and ensures no dangling host processes.
func TestCrashCleanup(t *testing.T) {
_ = setTempClineDir(t)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Start two instances for testing both graceful and crash scenarios
_ = mustRunCLI(ctx, t, "instance", "new")
_ = mustRunCLI(ctx, t, "instance", "new")
out := listInstancesJSON(ctx, t)
if len(out.CoreInstances) < 2 {
t.Fatalf("expected at least 2 instances, got %d", len(out.CoreInstances))
}
// Test 1: Graceful shutdown (SIGTERM) - should clean up both processes
gracefulTarget := out.CoreInstances[0]
waitForAddressHealthy(t, gracefulTarget.Address, defaultTimeout)
// Get PID using runtime discovery
gracefulPID := getCorePID(t, gracefulTarget.Address)
if gracefulPID <= 0 {
t.Fatalf("could not find PID for graceful target at %s", gracefulTarget.Address)
}
t.Logf("Testing graceful shutdown (SIGTERM) for instance %s (PID %d)", gracefulTarget.Address, gracefulPID)
if err := syscall.Kill(gracefulPID, syscall.SIGTERM); err != nil {
t.Fatalf("kill SIGTERM pid %d: %v", gracefulPID, err)
}
// Wait for registry cleanup
waitForAddressRemoved(t, gracefulTarget.Address, longTimeout)
// Verify both core and host ports are freed (no dangling processes)
waitForPortsClosed(t, gracefulTarget.CorePort(), gracefulTarget.HostPort(), defaultTimeout)
// Verify the instance is removed from SQLite (no file to check anymore)
// The waitForAddressRemoved already confirms the instance is gone from the registry
// Test 2: Crash cleanup (SIGKILL) - creates dangling host process that we must clean up
crashTarget := out.CoreInstances[1]
waitForAddressHealthy(t, crashTarget.Address, defaultTimeout)
// Get PID using runtime discovery
crashPID := getCorePID(t, crashTarget.Address)
if crashPID <= 0 {
t.Fatalf("could not find PID for crash target at %s", crashTarget.Address)
}
t.Logf("Testing crash cleanup (SIGKILL) for instance %s (PID %d)", crashTarget.Address, crashPID)
if err := syscall.Kill(crashPID, syscall.SIGKILL); err != nil {
t.Fatalf("kill SIGKILL pid %d: %v", crashPID, err)
}
// Wait for registry cleanup
waitForAddressRemoved(t, crashTarget.Address, longTimeout)
// Verify the instance is removed from SQLite (no file to check anymore)
// The waitForAddressRemoved already confirms the instance is gone from the registry
// Clean up dangling host process (SIGKILL leaves these behind by design)
t.Logf("Cleaning up dangling host process %s", crashTarget.HostServiceAddress)
findAndKillHostProcess(t, crashTarget.HostPort())
// Verify both ports are now free
waitForPortsClosed(t, crashTarget.CorePort(), crashTarget.HostPort(), defaultTimeout)
}
// itoa is a small helper for readability
func itoa(i int) string {
return strconvItoa(i)
}
// minimal inline int->string to avoid extra imports in helpers
func strconvItoa(i int) string {
// simple fast path
return fmtInt(i)
}
func fmtInt(i int) string {
// allocate small buffer; ints here are short
return (func(n int) string {
return fmt.Sprintf("%d", n)
})(i)
}
+22
View File
@@ -0,0 +1,22 @@
module github.com/cline/cli
go 1.23.0
require (
github.com/cline/grpc-go v0.0.0
github.com/mattn/go-sqlite3 v1.14.24
github.com/spf13/cobra v1.8.0
google.golang.org/grpc v1.75.0
)
replace github.com/cline/grpc-go => ../src/generated/grpc-go
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.41.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
)
+48
View File
@@ -0,0 +1,48 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/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=
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=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
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/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
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=
+125
View File
@@ -0,0 +1,125 @@
package cli
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/grpc-go/cline"
"github.com/spf13/cobra"
)
var isSessionAuthenticated bool
func NewAuthCommand() *cobra.Command {
return &cobra.Command{
Use: "auth",
Short: "Sign in to Cline",
Long: `Complete the authentication flow in browser to sign in to Cline.`,
RunE: func(cmd *cobra.Command, args []string) error {
return handleAuthCommand(cmd.Context())
},
}
}
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...)
}
}
+95
View File
@@ -0,0 +1,95 @@
package display
import (
"crypto/md5"
"fmt"
"sync"
"time"
"github.com/cline/cli/pkg/cli/types"
)
// MessageDeduplicator handles message deduplication to prevent duplicate displays
type MessageDeduplicator struct {
mu sync.RWMutex
seenMessages map[string]time.Time
maxAge time.Duration
cleanupTicker *time.Ticker
}
// NewMessageDeduplicator creates a new message deduplicator
func NewMessageDeduplicator() *MessageDeduplicator {
d := &MessageDeduplicator{
seenMessages: make(map[string]time.Time),
maxAge: 5 * time.Minute, // Keep messages for 5 minutes
cleanupTicker: time.NewTicker(1 * time.Minute), // Cleanup every minute
}
// Start cleanup goroutine
go d.cleanup()
return d
}
// IsDuplicate checks if a message is a duplicate
func (d *MessageDeduplicator) IsDuplicate(msg *types.ClineMessage) bool {
d.mu.Lock()
defer d.mu.Unlock()
// Create a hash of the message content
hash := d.hashMessage(msg)
// Check if we've seen this message recently
if lastSeen, exists := d.seenMessages[hash]; exists {
// If we've seen it within the last few seconds, it's a duplicate
if time.Since(lastSeen) < 2*time.Second {
return true
}
}
// Mark this message as seen
d.seenMessages[hash] = time.Now()
return false
}
// hashMessage creates a hash of the message for deduplication
func (d *MessageDeduplicator) hashMessage(msg *types.ClineMessage) string {
// Create a hash based on message content, type, and timestamp
content := fmt.Sprintf("%s|%s|%s|%d",
string(msg.Type),
msg.Say,
msg.Ask,
msg.Timestamp)
// For partial messages, include the text content in the hash
if msg.Partial {
content += "|" + msg.Text
}
hash := md5.Sum([]byte(content))
return fmt.Sprintf("%x", hash)
}
// cleanup removes old entries from the seen messages map
func (d *MessageDeduplicator) cleanup() {
for range d.cleanupTicker.C {
d.mu.Lock()
now := time.Now()
// Remove entries older than maxAge
for hash, timestamp := range d.seenMessages {
if now.Sub(timestamp) > d.maxAge {
delete(d.seenMessages, hash)
}
}
d.mu.Unlock()
}
}
// Stop stops the cleanup goroutine
func (d *MessageDeduplicator) Stop() {
if d.cleanupTicker != nil {
d.cleanupTicker.Stop()
}
}
+176
View File
@@ -0,0 +1,176 @@
package display
import (
"fmt"
"strings"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/types"
"github.com/cline/grpc-go/cline"
)
type Renderer struct {
typewriter *TypewriterPrinter
}
func NewRenderer() *Renderer {
return &Renderer{
typewriter: NewTypewriterPrinter(DefaultTypewriterConfig()),
}
}
// RenderMessage renders a message with timestamp and prefix
func (r *Renderer) RenderMessage(timestamp, prefix, text string) error {
if text == "" {
return nil
}
cleanText := r.sanitizeText(text)
if cleanText == "" {
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)
} else {
r.typewriter.PrintMessageLine(timestamp, "CMD", command)
}
return nil
}
// formatNumber formats numbers with k/m abbreviations
func formatNumber(n int) string {
if n >= 1000000 {
return fmt.Sprintf("%.1fm", float64(n)/1000000.0)
} else if n >= 1000 {
return fmt.Sprintf("%.1fk", float64(n)/1000.0)
}
return fmt.Sprintf("%d", n)
}
// 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))
return fmt.Sprintf("%s ($%.4f)", tokenDetails, cost)
}
// RenderAPI renders API request information
func (r *Renderer) RenderAPI(timestamp, 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)
} else {
r.typewriter.PrintMessageLine(timestamp, "API INFO", status)
}
return nil
}
// RenderRetry renders retry information
func (r *Renderer) RenderRetry(timestamp string, 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)
return nil
}
// RenderTaskList displays task history with improved formatting
func (r *Renderer) RenderTaskList(tasks []*cline.TaskItem) error {
const maxTasks = 20
startIndex := 0
if len(tasks) > maxTasks {
startIndex = len(tasks) - maxTasks
}
recentTasks := tasks[startIndex:]
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)
description := task.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)
r.typewriter.PrintfLn("Usage : %s", usageInfo)
// Single space between tasks (except last)
if i < len(recentTasks)-1 {
r.typewriter.PrintfLn("")
}
}
return nil
}
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)
}
return nil
}
func (r *Renderer) ClearLine() {
fmt.Print("\r\033[K")
}
func (r *Renderer) MoveCursorUp(n int) {
fmt.Printf("\033[%dA", n)
}
func (r *Renderer) sanitizeText(text string) string {
text = strings.TrimSpace(text)
if text == "" {
return ""
}
// Remove control characters and escape sequences
var result strings.Builder
for _, r := range text {
// Keep printable characters, spaces, tabs, and newlines
if r >= 32 || r == '\t' || r == '\n' || r == '\r' {
result.WriteRune(r)
}
// Skip control characters (0-31 except tab, newline, carriage return)
}
return result.String()
}
func (r *Renderer) SetTypewriterEnabled(enabled bool) {
r.typewriter.SetEnabled(enabled)
}
func (r *Renderer) IsTypewriterEnabled() bool {
return r.typewriter.IsEnabled()
}
func (r *Renderer) SetTypewriterSpeed(multiplier float64) {
r.typewriter.SetSpeed(multiplier)
}
func (r *Renderer) GetTypewriter() *TypewriterPrinter {
return r.typewriter
}
+449
View File
@@ -0,0 +1,449 @@
package display
import (
"encoding/json"
"fmt"
"strings"
"sync"
"github.com/cline/cli/pkg/cli/types"
)
// StreamingDisplay manages streaming message display with deduplication
type StreamingDisplay struct {
mu sync.RWMutex
state *types.ConversationState
renderer *Renderer
dedupe *MessageDeduplicator
}
// NewStreamingDisplay creates a new streaming display manager
func NewStreamingDisplay(state *types.ConversationState, renderer *Renderer) *StreamingDisplay {
return &StreamingDisplay{
state: state,
renderer: renderer,
dedupe: NewMessageDeduplicator(),
}
}
// HandlePartialMessage processes partial messages with streaming support
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()
switch msg.Type {
case types.MessageTypeAsk:
return s.handleStreamingAsk(msg, messageKey, timestamp, streamingMsg)
case types.MessageTypeSay:
return s.handleStreamingSay(msg, messageKey, timestamp, streamingMsg)
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=")
}
}
return ""
}
// calculateStringSimilarity calculates a simple similarity ratio between two strings
func (s *StreamingDisplay) calculateStringSimilarity(s1, s2 string) float64 {
if s1 == s2 {
return 1.0
}
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() {
if s.dedupe != nil {
s.dedupe.Stop()
}
}
+214
View File
@@ -0,0 +1,214 @@
package display
import (
"fmt"
"os"
"time"
)
// TypewriterConfig holds configuration for the typewriter effect
type TypewriterConfig struct {
BaseDelay time.Duration // Base delay between characters
FastDelay time.Duration // Faster delay for common characters
SlowDelay time.Duration // Slower delay for punctuation
PauseDelay time.Duration // Pause after sentences
Enabled bool // Whether typewriter effect is enabled
RandomFactor float64 // Randomness factor (0.0 to 1.0)
}
// DefaultTypewriterConfig returns the default typewriter configuration
func DefaultTypewriterConfig() *TypewriterConfig {
return &TypewriterConfig{
BaseDelay: 15 * time.Millisecond,
FastDelay: 8 * time.Millisecond,
SlowDelay: 25 * time.Millisecond,
PauseDelay: 150 * time.Millisecond,
Enabled: false,
RandomFactor: 0.3,
}
}
// TypewriterPrinter handles typewriter-style output
type TypewriterPrinter struct {
config *TypewriterConfig
}
// NewTypewriterPrinter creates a new typewriter printer
func NewTypewriterPrinter(config *TypewriterConfig) *TypewriterPrinter {
if config == nil {
config = DefaultTypewriterConfig()
}
return &TypewriterPrinter{
config: config,
}
}
// Print prints text with typewriter effect
func (tp *TypewriterPrinter) Print(text string) {
if !tp.config.Enabled {
fmt.Print(text)
return
}
tp.typewriterPrint(text)
}
// Printf prints formatted text with typewriter effect
func (tp *TypewriterPrinter) Printf(format string, args ...interface{}) {
text := fmt.Sprintf(format, args...)
tp.Print(text)
}
// Println prints text with typewriter effect and adds a newline
func (tp *TypewriterPrinter) Println(text string) {
tp.Print(text + "\n")
}
// PrintfLn prints formatted text with typewriter effect and adds a newline
func (tp *TypewriterPrinter) PrintfLn(format string, args ...interface{}) {
text := fmt.Sprintf(format, args...)
tp.Println(text)
}
// PrintInstant prints text immediately without typewriter effect
func (tp *TypewriterPrinter) PrintInstant(text string) {
fmt.Print(text)
}
// PrintfInstant prints formatted text immediately without typewriter effect
func (tp *TypewriterPrinter) PrintfInstant(format string, args ...interface{}) {
fmt.Printf(format, args...)
}
// typewriterPrint displays text with a typewriter animation effect
func (tp *TypewriterPrinter) typewriterPrint(text string) {
// Convert string to runes to handle Unicode properly
runes := []rune(text)
for i, r := range runes {
// Print the character
fmt.Print(string(r))
os.Stdout.Sync() // Force immediate output
// Don't add delay after the last character
if i == len(runes)-1 {
break
}
// Determine delay based on character type
delay := tp.getDelayForCharacter(r, i)
// Sleep for the calculated delay
time.Sleep(delay)
}
}
// getDelayForCharacter returns the appropriate delay for a character
func (tp *TypewriterPrinter) getDelayForCharacter(r rune, position int) time.Duration {
var baseDelay time.Duration
switch {
case r == '.' || r == '!' || r == '?':
// Longer pause after sentence endings
baseDelay = tp.config.PauseDelay
case r == ',' || r == ';' || r == ':':
// Medium pause after punctuation
baseDelay = tp.config.SlowDelay
case r == ' ':
// Slightly faster for spaces
baseDelay = tp.config.FastDelay
case r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z':
// Fast for common letters
baseDelay = tp.config.FastDelay
case r == '\n':
// No delay for newlines
return 0
default:
// Base delay for other characters
baseDelay = tp.config.BaseDelay
}
// Add randomness to make it feel more natural
if tp.config.RandomFactor > 0 {
// Simple pseudo-random based on position to ensure consistency
randomFactor := 0.7 + (tp.config.RandomFactor * float64(position%7) / 6.0)
baseDelay = time.Duration(float64(baseDelay) * randomFactor)
}
return baseDelay
}
// SetEnabled enables or disables the typewriter effect
func (tp *TypewriterPrinter) SetEnabled(enabled bool) {
tp.config.Enabled = enabled
}
// IsEnabled returns whether the typewriter effect is enabled
func (tp *TypewriterPrinter) IsEnabled() bool {
return tp.config.Enabled
}
// SetSpeed adjusts the typewriter speed (multiplier: 0.1 = very slow, 1.0 = normal, 2.0 = fast)
func (tp *TypewriterPrinter) SetSpeed(multiplier float64) {
if multiplier <= 0 {
multiplier = 1.0
}
tp.config.BaseDelay = time.Duration(float64(15*time.Millisecond) / multiplier)
tp.config.FastDelay = time.Duration(float64(8*time.Millisecond) / multiplier)
tp.config.SlowDelay = time.Duration(float64(25*time.Millisecond) / multiplier)
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
tp.Println(text)
}
// Global typewriter printer instance
var globalTypewriter = NewTypewriterPrinter(DefaultTypewriterConfig())
// Global convenience functions that use the global typewriter instance
// TypewriterPrint prints text with typewriter effect using the global instance
func TypewriterPrint(text string) {
globalTypewriter.Print(text)
}
// TypewriterPrintf prints formatted text with typewriter effect using the global instance
func TypewriterPrintf(format string, args ...interface{}) {
globalTypewriter.Printf(format, args...)
}
// TypewriterPrintln prints text with typewriter effect and newline using the global instance
func TypewriterPrintln(text string) {
globalTypewriter.Println(text)
}
// TypewriterPrintfLn prints formatted text with typewriter effect and newline using the global instance
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)
}
// SetGlobalTypewriterEnabled enables or disables the global typewriter effect
func SetGlobalTypewriterEnabled(enabled bool) {
globalTypewriter.SetEnabled(enabled)
}
// SetGlobalTypewriterSpeed sets the speed of the global typewriter effect
func SetGlobalTypewriterSpeed(multiplier float64) {
globalTypewriter.SetSpeed(multiplier)
}
// GetGlobalTypewriter returns the global typewriter instance
func GetGlobalTypewriter() *TypewriterPrinter {
return globalTypewriter
}
+274
View File
@@ -0,0 +1,274 @@
package global
import (
"context"
"fmt"
"os"
"os/exec"
"time"
"github.com/cline/cli/pkg/common"
)
// ClineClients manages Cline instances using the new registry system
type ClineClients struct {
registry *ClientRegistry
}
// NewClineClients creates a new ClineClients instance
func NewClineClients(configPath string) *ClineClients {
registry := NewClientRegistry(configPath)
return &ClineClients{
registry: registry,
}
}
// Initialize performs cleanup of stale instances
func (c *ClineClients) Initialize(ctx context.Context) error {
// Clean up stale entries (direct SQLite operations)
_ = c.registry.CleanupStaleInstances(ctx)
return nil
}
// StartNewInstance starts a new Cline instance and waits for cline-core to self-register
func (c *ClineClients) StartNewInstance(ctx context.Context) (*common.CoreInstanceInfo, error) {
// Find available ports
corePort, hostPort, err := common.FindAvailablePortPair()
if err != nil {
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)
// Start cline-host first
hostCmd, err := startClineHost(hostPort, corePort)
if err != nil {
return nil, fmt.Errorf("failed to start cline-host: %w", err)
}
// Start cline-core (it will register itself in SQLite locks database)
coreCmd, err := startClineCore(corePort, hostPort)
if err != nil {
// Clean up host process if core fails to start
if hostCmd != nil && hostCmd.Process != nil {
hostCmd.Process.Kill()
}
return nil, fmt.Errorf("failed to start cline-core: %w", err)
}
fullAddress := fmt.Sprintf("localhost:%d", corePort)
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
err = common.RetryOperation(12, 5*time.Second, func() error {
// Check if instance registered itself in SQLite
foundInstance, err := c.registry.GetInstance(fullAddress)
if err != nil || foundInstance == nil {
return fmt.Errorf("instance not found in registry: %v", err)
}
// Verify instance is healthy
if !common.IsInstanceHealthy(ctx, fullAddress) {
return fmt.Errorf("instance is registered but not healthy")
}
// Success - store the instance for return
instance = foundInstance
return nil
})
if err != nil {
// Clean up both processes on failure
if coreCmd != nil && coreCmd.Process != nil {
fmt.Printf("Cleaning up core process (PID: %d)\n", coreCmd.Process.Pid)
coreCmd.Process.Kill()
}
if hostCmd != nil && hostCmd.Process != nil {
fmt.Printf("Cleaning up host process (PID: %d)\n", hostCmd.Process.Pid)
hostCmd.Process.Kill()
}
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)
return instance, nil
}
// StartNewInstanceAtPort starts a new Cline instance at the specified port and waits for self-registration
func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int) (*common.CoreInstanceInfo, error) {
// Find available host port (core port + 1000)
hostPort := corePort + 1000
coreAddress := fmt.Sprintf("localhost:%d", corePort)
// Check if the specified core port is available
if common.IsInstanceHealthy(ctx, coreAddress) {
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)
// Start cline-host first
hostCmd, err := startClineHost(hostPort, corePort)
if err != nil {
return nil, fmt.Errorf("failed to start cline-host: %w", err)
}
// Start cline-core (it will register itself in SQLite locks database)
coreCmd, err := startClineCore(corePort, hostPort)
if err != nil {
// Clean up host process if core fails to start
if hostCmd != nil && hostCmd.Process != nil {
hostCmd.Process.Kill()
}
return nil, fmt.Errorf("failed to start cline-core: %w", err)
}
fullAddress := fmt.Sprintf("localhost:%d", corePort)
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
err = common.RetryOperation(12, 5*time.Second, func() error {
// Check if instance registered itself in SQLite
foundInstance, err := c.registry.GetInstance(fullAddress)
if err != nil || foundInstance == nil {
return fmt.Errorf("instance not found in registry: %v", err)
}
// Verify instance is healthy
if !common.IsInstanceHealthy(ctx, fullAddress) {
return fmt.Errorf("instance is registered but not healthy")
}
// Success - store the instance for return
instance = foundInstance
return nil
})
if err != nil {
// Clean up both processes on failure
if coreCmd != nil && coreCmd.Process != nil {
fmt.Printf("Cleaning up core process (PID: %d)\n", coreCmd.Process.Pid)
coreCmd.Process.Kill()
}
if hostCmd != nil && hostCmd.Process != nil {
fmt.Printf("Cleaning up host process (PID: %d)\n", hostCmd.Process.Pid)
hostCmd.Process.Kill()
}
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)
return instance, nil
}
// GetRegistry returns the client registry
func (c *ClineClients) GetRegistry() *ClientRegistry {
return c.registry
}
// EnsureInstanceAtAddress ensures an instance exists at the given address, starting one if needed
func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address string) error {
// Expect host:port everywhere
normalized := address
if normalized == "" {
normalized = fmt.Sprintf("localhost:%d", common.DEFAULT_CLINE_CORE_PORT)
}
// Check if instance already exists at this address
if c.registry.HasInstanceAtAddress(normalized) {
return nil
}
// Parse host:port
host, port, err := common.ParseHostPort(normalized)
if err != nil {
return fmt.Errorf("invalid address format %s", address)
}
// Use IPv6-compatible localhost detection
if common.IsLocalAddress(host) {
_, err := c.StartNewInstanceAtPort(ctx, port)
if err != nil {
return fmt.Errorf("failed to start new instance at %s: %w", normalized, err)
}
return nil
}
return fmt.Errorf("cannot start remote instance at %s", normalized)
}
func startClineHost(hostPort, corePort int) (*exec.Cmd, error) {
fmt.Printf("Starting cline-host on port %d\n", hostPort)
// Start the cline-host process
cmd := exec.Command("./cli/bin/cline-host",
"--verbose",
"--port", fmt.Sprintf("%d", hostPort))
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to start cline-host: %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)
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",
"--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)
cmd := exec.Command("node", args...)
// Set working directory to dist-standalone (relative to project root)
cmd.Dir = "./dist-standalone"
// Redirect stdout and stderr to log file
cmd.Stdout = logFile
cmd.Stderr = logFile
// Set environment variables (removed CLINE_DIR)
env := os.Environ()
env = append(env,
"GRPC_TRACE=all",
"GRPC_VERBOSITY=DEBUG",
"NODE_ENV=development",
)
cmd.Env = env
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)
return cmd, nil
}
+67
View File
@@ -0,0 +1,67 @@
package global
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/cline/cli/pkg/common"
"github.com/cline/grpc-go/client"
)
type Port uint16
type GlobalConfig struct {
ConfigPath string
Verbose bool
OutputFormat string
CoreAddress string
}
var (
Config *GlobalConfig
Clients *ClineClients
)
func InitializeGlobalConfig(cfg *GlobalConfig) error {
if cfg.ConfigPath == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
cfg.ConfigPath = filepath.Join(homeDir, ".cline")
}
// Ensure .cline directory exists
if err := os.MkdirAll(cfg.ConfigPath, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
Config = cfg
Clients = NewClineClients(cfg.ConfigPath)
// Initialize the clients registry
ctx := context.Background()
if err := Clients.Initialize(ctx); err != nil {
return fmt.Errorf("failed to initialize clients: %w", err)
}
return nil
}
// GetDefaultClient returns a client for the default instance or the address override
func GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
if Config.CoreAddress != "" && Config.CoreAddress != fmt.Sprintf("localhost:%d", common.DEFAULT_CLINE_CORE_PORT) {
// User specified a specific address, use that
return Clients.GetRegistry().GetClient(ctx, Config.CoreAddress)
}
// Use the default instance from registry
return Clients.GetRegistry().GetDefaultClient(ctx)
}
// GetClientForAddress returns a client for a specific address
func GetClientForAddress(ctx context.Context, address string) (*client.ClineClient, error) {
return Clients.GetRegistry().GetClient(ctx, address)
}
+267
View File
@@ -0,0 +1,267 @@
package global
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
"github.com/cline/cli/pkg/cli/sqlite"
"github.com/cline/cli/pkg/common"
"github.com/cline/grpc-go/client"
"github.com/cline/grpc-go/cline"
"github.com/cline/grpc-go/host"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/health/grpc_health_v1"
)
// ClientRegistry manages Cline client connections using direct SQLite operations
type ClientRegistry struct {
lockManager *sqlite.LockManager
configPath string
}
// NewClientRegistry creates a new client registry
func NewClientRegistry(configPath string) *ClientRegistry {
lockManager, err := sqlite.NewLockManager(configPath)
if err != nil {
// Log error but continue - we can still function without SQLite
log.Fatalf("Warning: Failed to initialize SQLite lock manager: %v\n", err)
}
return &ClientRegistry{
lockManager: lockManager,
configPath: configPath,
}
}
// GetDefaultInstance returns the default instance address from settings file
func (r *ClientRegistry) GetDefaultInstance() string {
defaultAddr, err := sqlite.GetDefaultInstance(r.configPath)
if err != nil {
return ""
}
return defaultAddr
}
// SetDefaultInstance sets the default instance (writes default.json)
func (r *ClientRegistry) SetDefaultInstance(address string) error {
// Verify the instance exists in SQLite
if r.lockManager != nil {
exists, err := r.lockManager.HasInstanceAtAddress(address)
if err != nil {
return fmt.Errorf("failed to check instance existence: %w", err)
}
if !exists {
return fmt.Errorf("instance %s not found in registry", address)
}
}
return sqlite.SetDefaultInstance(r.configPath, address)
}
// GetInstance returns instance information directly from SQLite
func (r *ClientRegistry) GetInstance(address string) (*common.CoreInstanceInfo, error) {
if r.lockManager == nil {
return nil, fmt.Errorf("lock manager not available")
}
return r.lockManager.GetInstanceInfo(address)
}
// GetClient returns a connected client for the given address (created on-demand)
func (r *ClientRegistry) GetClient(ctx context.Context, address string) (*client.ClineClient, error) {
// Verify instance exists in SQLite
if r.lockManager != nil {
exists, err := r.lockManager.HasInstanceAtAddress(address)
if err != nil {
return nil, fmt.Errorf("failed to check instance existence: %w", err)
}
if !exists {
return nil, fmt.Errorf("instance %s not found", address)
}
}
// Create client on-demand (no caching)
target, err := common.NormalizeAddressForGRPC(address)
if err != nil {
return nil, fmt.Errorf("invalid address %s: %w", address, err)
}
cl, err := client.NewClineClient(target)
if err != nil {
return nil, fmt.Errorf("failed to create client for %s: %w", target, err)
}
if err := cl.Connect(ctx); err != nil {
return nil, fmt.Errorf("failed to connect to %s: %w", target, err)
}
return cl, nil
}
// GetDefaultClient returns a client for the default instance
func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
defaultAddr := r.GetDefaultInstance()
if defaultAddr == "" {
return nil, fmt.Errorf("no default instance configured")
}
return r.GetClient(ctx, defaultAddr)
}
// ListInstances returns all registered instances directly from SQLite
func (r *ClientRegistry) ListInstances() []*common.CoreInstanceInfo {
if r.lockManager == nil {
return []*common.CoreInstanceInfo{}
}
// Use context with timeout for health checks
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
instances, err := r.lockManager.ListInstancesWithHealthCheck(ctx)
if err != nil {
fmt.Printf("Warning: Failed to list instances: %v\n", err)
return []*common.CoreInstanceInfo{}
}
return instances
}
// HasInstanceAtAddress checks if an instance exists at the given address (delegates to SQLite)
func (r *ClientRegistry) HasInstanceAtAddress(address string) bool {
if r.lockManager == nil {
return false
}
exists, err := r.lockManager.HasInstanceAtAddress(address)
if err != nil {
fmt.Printf("Warning: Failed to check instance existence: %v\n", err)
return false
}
return exists
}
// CleanupStaleInstances removes stale instances using direct SQLite operations
func (r *ClientRegistry) CleanupStaleInstances(ctx context.Context) error {
if r.lockManager == nil {
return nil
}
// Get all instances with health checks
instances, err := r.lockManager.ListInstancesWithHealthCheck(ctx)
if err != nil {
return fmt.Errorf("failed to list instances for cleanup: %w", err)
}
// Clean up all stale instances
for _, instance := range instances {
if instance.Status != grpc_health_v1.HealthCheckResponse_SERVING {
// Try to gracefully shutdown the paired host process before cleanup
fmt.Printf("Attempting to shutdown dangling host service %s for stale cline core instance %s\n",
instance.HostServiceAddress, instance.Address)
r.tryShutdownHostProcess(instance.HostServiceAddress)
// Remove from SQLite database
if err := r.lockManager.RemoveInstanceLock(instance.Address); err != nil {
return fmt.Errorf("failed to remove stale instance %s: %w", instance.Address, err)
}
fmt.Printf("Removed stale instance: %s\n", instance.Address)
}
}
return nil
}
// tryShutdownHostProcess attempts to gracefully shutdown a host process via RPC
// Best effort, don't throw errors i guess
func (r *ClientRegistry) tryShutdownHostProcess(hostServiceAddress string) {
err := common.RetryOperation(3, 2*time.Second, func() error {
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// Create gRPC connection to host bridge
conn, err := grpc.DialContext(ctx, hostServiceAddress,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock())
if err != nil {
return fmt.Errorf("connection failed: %w", err)
}
defer conn.Close()
// Create env service client and call shutdown
envClient := host.NewEnvServiceClient(conn)
_, err = envClient.Shutdown(ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("RPC failed: %w", err)
}
return nil
})
if err != nil {
fmt.Printf("Warning: Failed to request host bridge shutdown on port %s: %v\n", hostServiceAddress, err)
} else {
fmt.Printf("Host bridge shutdown requested successfully on port %s\n", hostServiceAddress)
}
}
// ListInstancesCleaned performs cleanup and returns instances with health checks
func (r *ClientRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.CoreInstanceInfo, error) {
// 1. Clean up stale entries (best-effort)
_ = r.CleanupStaleInstances(ctx)
// 2. Get all instances with real-time health checks
instances := r.ListInstances()
// 3. Ensure default is set if instances exist
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 {
currentDefault := r.GetDefaultInstance()
// If we have no instances, clear any stale default and remove settings file
if len(instances) == 0 {
if currentDefault != "" {
// Remove the settings file since no instances exist
settingsPath := filepath.Join(r.configPath, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
_ = os.Remove(settingsPath)
}
return nil
}
// If we have instances but no default, pick the first one
if currentDefault == "" {
return sqlite.SetDefaultInstance(r.configPath, instances[0].Address)
}
// Validate current default still exists in the instances
defaultExists := false
for _, instance := range instances {
if instance.Address == currentDefault {
defaultExists = true
break
}
}
if !defaultExists {
// Current default doesn't exist, pick a new one from available instances
return sqlite.SetDefaultInstance(r.configPath, instances[0].Address)
}
return nil
}
+354
View File
@@ -0,0 +1,354 @@
package handlers
import (
"encoding/json"
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/types"
)
// AskHandler handles ASK type messages
type AskHandler struct {
*BaseHandler
}
// NewAskHandler creates a new ASK handler
func NewAskHandler() *AskHandler {
return &AskHandler{
BaseHandler: NewBaseHandler("ask", PriorityHigh),
}
}
// CanHandle returns true if this is an ASK message
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()
switch msg.Ask {
case string(types.AskTypeFollowup):
return h.handleFollowup(msg, dc, timestamp)
case string(types.AskTypePlanModeRespond):
return h.handlePlanModeRespond(msg, dc, timestamp)
case string(types.AskTypeCommand):
return h.handleCommand(msg, dc, timestamp)
case string(types.AskTypeCommandOutput):
return h.handleCommandOutput(msg, dc, timestamp)
case string(types.AskTypeCompletionResult):
return h.handleCompletionResult(msg, dc, timestamp)
case string(types.AskTypeTool):
return h.handleTool(msg, dc, timestamp)
case string(types.AskTypeAPIReqFailed):
return h.handleAPIReqFailed(msg, dc, timestamp)
case string(types.AskTypeResumeTask):
return h.handleResumeTask(msg, dc, timestamp)
case string(types.AskTypeResumeCompletedTask):
return h.handleResumeCompletedTask(msg, dc, timestamp)
case string(types.AskTypeMistakeLimitReached):
return h.handleMistakeLimitReached(msg, dc, timestamp)
case string(types.AskTypeAutoApprovalMaxReached):
return h.handleAutoApprovalMaxReached(msg, dc, timestamp)
case string(types.AskTypeBrowserActionLaunch):
return h.handleBrowserActionLaunch(msg, dc, timestamp)
case string(types.AskTypeUseMcpServer):
return h.handleUseMcpServer(msg, dc, timestamp)
case string(types.AskTypeNewTask):
return h.handleNewTask(msg, dc, timestamp)
case string(types.AskTypeCondense):
return h.handleCondense(msg, dc, timestamp)
case string(types.AskTypeReportBug):
return h.handleReportBug(msg, dc, timestamp)
default:
return h.handleDefault(msg, dc, timestamp)
}
}
// handleFollowup handles followup questions
func (h *AskHandler) handleFollowup(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
var question string
var options []string
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 == "" {
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)
}
}
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)
}
}
return nil
}
// handleCommand handles command execution requests
func (h *AskHandler) handleCommand(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text == "" {
return nil
}
command := msg.Text
// 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")
}
return nil
}
// handleCommandOutput handles command output requests
func (h *AskHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayContext, timestamp string) 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)
}
fmt.Printf("\nApprove to proceed while this command runs in the background.\n")
return nil
}
// handleCompletionResult handles completion result requests
func (h *AskHandler) handleCompletionResult(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return nil
}
// handleTool handles tool execution requests
func (h *AskHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext, timestamp string) 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 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] + "..."
}
fmt.Printf("Preview: %s\n", preview)
}
fmt.Printf("\nApproval required.\n")
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))
}
// 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.")
}
// 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.")
}
// 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))
}
// handleBrowserActionLaunch handles browser action launch requests
func (h *AskHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext, timestamp string) 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))
}
// handleUseMcpServer handles MCP server usage requests
func (h *AskHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
// Parse MCP server usage request
type McpServerRequest struct {
ServerName string `json:"serverName"`
Type string `json:"type"`
ToolName string `json:"toolName,omitempty"`
Arguments string `json:"arguments,omitempty"`
URI string `json:"uri,omitempty"`
}
var mcpReq McpServerRequest
if err := json.Unmarshal([]byte(msg.Text), &mcpReq); err != nil {
return dc.Renderer.RenderMessage(timestamp, "MCP", msg.Text)
}
var operation string
if mcpReq.Type == "access_mcp_resource" {
operation = "access a resource"
} else {
operation = fmt.Sprintf("use a tool (%s)", mcpReq.ToolName)
if mcpReq.Arguments != "" {
operation = fmt.Sprintf("%s with args (%s)", operation, mcpReq.Arguments)
}
}
return dc.Renderer.RenderMessage(timestamp, "MCP",
fmt.Sprintf("Cline wants to %s on the %s MCP server", operation, mcpReq.ServerName))
}
// 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))
}
// 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))
}
// handleReportBug handles bug report requests
func (h *AskHandler) handleReportBug(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
var bugData struct {
Title string `json:"title"`
WhatHappened string `json:"what_happened"`
StepsToReproduce string `json:"steps_to_reproduce"`
APIRequestOutput string `json:"api_request_output"`
AdditionalContext string `json:"additional_context"`
}
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))
}
err := dc.Renderer.RenderMessage(timestamp, "BUG REPORT", "Cline wants to create a GitHub issue:")
if err != nil {
return fmt.Errorf("failed to render handleReportBug: %w", err)
}
fmt.Printf("\n**Title**: %s\n", bugData.Title)
fmt.Printf("**What Happened**: %s\n", bugData.WhatHappened)
fmt.Printf("**Steps to Reproduce**: %s\n", bugData.StepsToReproduce)
fmt.Printf("**API Request Output**: %s\n", bugData.APIRequestOutput)
fmt.Printf("**Additional Context**: %s\n", bugData.AdditionalContext)
fmt.Printf("\nApprove to create a GitHub issue.\n")
return nil
}
// 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)
}
+128
View File
@@ -0,0 +1,128 @@
package handlers
import (
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/types"
)
// MessageHandler defines the interface for handling different message types
type MessageHandler interface {
// CanHandle returns true if this handler can process the given message
CanHandle(msg *types.ClineMessage) bool
// Handle processes the message and renders it using the display context
Handle(msg *types.ClineMessage, dc *DisplayContext) error
// GetPriority returns the priority of this handler (higher = more priority)
GetPriority() int
// GetName returns a human-readable name for this handler
GetName() string
}
// 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
}
// BaseHandler provides common functionality for message handlers
type BaseHandler struct {
name string
priority int
}
// NewBaseHandler creates a new base handler
func NewBaseHandler(name string, priority int) *BaseHandler {
return &BaseHandler{
name: name,
priority: priority,
}
}
// GetName returns the handler name
func (h *BaseHandler) GetName() string {
return h.name
}
// GetPriority returns the handler priority
func (h *BaseHandler) GetPriority() int {
return h.priority
}
// HandlerRegistry manages a collection of message handlers
type HandlerRegistry struct {
handlers []MessageHandler
}
// NewHandlerRegistry creates a new handler registry
func NewHandlerRegistry() *HandlerRegistry {
return &HandlerRegistry{
handlers: make([]MessageHandler, 0),
}
}
// Register adds a handler to the registry
func (r *HandlerRegistry) Register(handler MessageHandler) {
r.handlers = append(r.handlers, handler)
// Sort handlers by priority (highest first)
for i := len(r.handlers) - 1; i > 0; i-- {
if r.handlers[i].GetPriority() > r.handlers[i-1].GetPriority() {
r.handlers[i], r.handlers[i-1] = r.handlers[i-1], r.handlers[i]
} else {
break
}
}
}
// Handle finds the appropriate handler and processes the message
func (r *HandlerRegistry) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
for _, handler := range r.handlers {
if handler.CanHandle(msg) {
return handler.Handle(msg, dc)
}
}
// If no specific handler found, use default text handler
return r.handleDefault(msg, dc)
}
// 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)
}
// GetHandlers returns all registered handlers
func (r *HandlerRegistry) GetHandlers() []MessageHandler {
return r.handlers
}
// GetHandlerByName finds a handler by name
func (r *HandlerRegistry) GetHandlerByName(name string) MessageHandler {
for _, handler := range r.handlers {
if handler.GetName() == name {
return handler
}
}
return nil
}
// HandlerPriorities defines standard priority levels for handlers
const (
PriorityHigh = 100
PriorityNormal = 50
PriorityLow = 10
)
+418
View File
@@ -0,0 +1,418 @@
package handlers
import (
"encoding/json"
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/types"
)
// SayHandler handles SAY type messages
type SayHandler struct {
*BaseHandler
}
// NewSayHandler creates a new SAY handler
func NewSayHandler() *SayHandler {
return &SayHandler{
BaseHandler: NewBaseHandler("say", PriorityNormal),
}
}
// CanHandle returns true if this is a SAY message
func (h *SayHandler) CanHandle(msg *types.ClineMessage) bool {
return msg.IsSay()
}
// Handle processes SAY messages
func (h *SayHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
timestamp := msg.GetTimestamp()
switch msg.Say {
case string(types.SayTypeTask):
return h.handleTask(msg, dc, timestamp)
case string(types.SayTypeError):
return h.handleError(msg, dc, timestamp)
case string(types.SayTypeAPIReqStarted):
return h.handleAPIReqStarted(msg, dc, timestamp)
case string(types.SayTypeAPIReqFinished):
return h.handleAPIReqFinished(msg, dc, timestamp)
case string(types.SayTypeText):
return h.handleText(msg, dc, timestamp)
case string(types.SayTypeReasoning):
return h.handleReasoning(msg, dc, timestamp)
case string(types.SayTypeCompletionResult):
return h.handleCompletionResult(msg, dc, timestamp)
case string(types.SayTypeUserFeedback):
return h.handleUserFeedback(msg, dc, timestamp)
case string(types.SayTypeUserFeedbackDiff):
return h.handleUserFeedbackDiff(msg, dc, timestamp)
case string(types.SayTypeAPIReqRetried):
return h.handleAPIReqRetried(msg, dc, timestamp)
case string(types.SayTypeCommand):
return h.handleCommand(msg, dc, timestamp)
case string(types.SayTypeCommandOutput):
return h.handleCommandOutput(msg, dc, timestamp)
case string(types.SayTypeTool):
return h.handleTool(msg, dc, timestamp)
case string(types.SayTypeShellIntegrationWarning):
return h.handleShellIntegrationWarning(msg, dc, timestamp)
case string(types.SayTypeBrowserActionLaunch):
return h.handleBrowserActionLaunch(msg, dc, timestamp)
case string(types.SayTypeBrowserAction):
return h.handleBrowserAction(msg, dc, timestamp)
case string(types.SayTypeBrowserActionResult):
return h.handleBrowserActionResult(msg, dc, timestamp)
case string(types.SayTypeMcpServerRequestStarted):
return h.handleMcpServerRequestStarted(msg, dc, timestamp)
case string(types.SayTypeMcpServerResponse):
return h.handleMcpServerResponse(msg, dc, timestamp)
case string(types.SayTypeMcpNotification):
return h.handleMcpNotification(msg, dc, timestamp)
case string(types.SayTypeUseMcpServer):
return h.handleUseMcpServer(msg, dc, timestamp)
case string(types.SayTypeDiffError):
return h.handleDiffError(msg, dc, timestamp)
case string(types.SayTypeDeletedAPIReqs):
return h.handleDeletedAPIReqs(msg, dc, timestamp)
case string(types.SayTypeClineignoreError):
return h.handleClineignoreError(msg, dc, timestamp)
case string(types.SayTypeCheckpointCreated):
return h.handleCheckpointCreated(msg, dc, timestamp)
case string(types.SayTypeLoadMcpDocumentation):
return h.handleLoadMcpDocumentation(msg, dc, timestamp)
case string(types.SayTypeInfo):
return h.handleInfo(msg, dc, timestamp)
case string(types.SayTypeTaskProgress):
return h.handleTaskProgress(msg, dc, timestamp)
default:
return h.handleDefault(msg, dc, timestamp)
}
}
// handleTask handles task messages
func (h *SayHandler) handleTask(msg *types.ClineMessage, dc *DisplayContext, timestamp string) 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)
}
// handleAPIReqStarted handles API request started messages
func (h *SayHandler) handleAPIReqStarted(msg *types.ClineMessage, dc *DisplayContext, timestamp string) 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)
}
// Handle different API request states
if apiInfo.CancelReason != "" {
if apiInfo.CancelReason == "user_cancelled" {
return dc.Renderer.RenderMessage(timestamp, "API INFO", "Request Cancelled")
} else if apiInfo.CancelReason == "retries_exhausted" {
return dc.Renderer.RenderMessage(timestamp, "API INFO", "Request Failed (Retries Exhausted)")
}
return dc.Renderer.RenderMessage(timestamp, "API INFO", "Streaming Failed")
}
if apiInfo.Cost >= 0 {
return dc.Renderer.RenderAPI(timestamp, "Request completed", &apiInfo)
}
// Check for retry status
if apiInfo.RetryStatus != nil {
return dc.Renderer.RenderRetry(timestamp,
apiInfo.RetryStatus.Attempt,
apiInfo.RetryStatus.MaxAttempts,
apiInfo.RetryStatus.DelaySec)
}
return dc.Renderer.RenderAPI(timestamp, "Processing request", &apiInfo)
}
// handleAPIReqFinished handles API request finished messages
func (h *SayHandler) handleAPIReqFinished(msg *types.ClineMessage, dc *DisplayContext, timestamp string) 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 {
if msg.Text == "" {
return nil
}
// Special case for the user's task input
prefix := "ASST TEXT"
if dc.MessageIndex == 0 {
prefix = "USER"
}
return dc.Renderer.RenderMessage(timestamp, prefix, msg.Text)
}
// handleReasoning handles reasoning messages
func (h *SayHandler) handleReasoning(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text == "" {
return nil
}
return dc.Renderer.RenderMessage(timestamp, "THINKING", msg.Text)
}
func (h *SayHandler) handleCompletionResult(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
text := msg.Text
if strings.HasSuffix(text, "HAS_CHANGES") {
text = strings.TrimSuffix(text, "HAS_CHANGES")
}
return dc.Renderer.RenderMessage(timestamp, "RESULT", text)
}
// handleUserFeedback handles user feedback messages
func (h *SayHandler) handleUserFeedback(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text != "" {
return dc.Renderer.RenderMessage(timestamp, "USER", msg.Text)
} else {
return dc.Renderer.RenderMessage(timestamp, "USER", "[Provided feedback without text]")
}
}
// handleUserFeedbackDiff handles user feedback diff messages
func (h *SayHandler) handleUserFeedbackDiff(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
var toolMsg types.ToolMessage
if err := json.Unmarshal([]byte(msg.Text), &toolMsg); err != nil {
return dc.Renderer.RenderMessage(timestamp, "USER DIFF", msg.Text)
}
message := fmt.Sprintf("User manually edited: %s\n\nDiff:\n%s",
toolMsg.Path,
toolMsg.Diff)
return dc.Renderer.RenderMessage(timestamp, "USER DIFF", message)
}
// 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")
}
// handleCommand handles command execution announcements
func (h *SayHandler) handleCommand(msg *types.ClineMessage, dc *DisplayContext, timestamp string) 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)
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) {
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)
}
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.")
}
// handleBrowserActionLaunch handles browser action launch messages
func (h *SayHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
url := msg.Text
if url == "" {
return nil
}
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Launching browser at: %s", url))
}
// handleBrowserAction handles browser action messages
func (h *SayHandler) handleBrowserAction(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text == "" {
return nil
}
type BrowserActionData struct {
Action string `json:"action"`
Coordinate string `json:"coordinate,omitempty"`
Text string `json:"text,omitempty"`
}
var actionData BrowserActionData
if err := json.Unmarshal([]byte(msg.Text), &actionData); err != nil {
return dc.Renderer.RenderMessage(timestamp, "BROWSER", msg.Text)
}
// 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))
}
// 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))
}
// Generic handling for all other actions
return dc.Renderer.RenderMessage(timestamp, "BROWSER", fmt.Sprintf("Next action: %s", actionData.Action))
}
// handleBrowserActionResult handles browser action result messages
func (h *SayHandler) handleBrowserActionResult(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text == "" {
return nil
}
type BrowserActionResult struct {
Screenshot string `json:"screenshot,omitempty"`
Logs string `json:"logs,omitempty"`
CurrentUrl string `json:"currentUrl,omitempty"`
CurrentMousePosition string `json:"currentMousePosition,omitempty"`
}
var result BrowserActionResult
if err := json.Unmarshal([]byte(msg.Text), &result); err != nil {
return dc.Renderer.RenderMessage(timestamp, "BROWSER", "Action completed")
}
// 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))
}
// Default case
return dc.Renderer.RenderMessage(timestamp, "BROWSER", "Action completed")
}
// 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")
}
// 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))
}
// 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))
}
// 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")
}
// 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.")
}
// 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")
}
// 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))
}
// 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")
}
// 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")
}
// handleInfo handles info messages
func (h *SayHandler) handleInfo(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
return nil
}
// handleTaskProgress handles task progress messages
func (h *SayHandler) handleTaskProgress(msg *types.ClineMessage, dc *DisplayContext, timestamp string) error {
if msg.Text == "" {
return nil
}
return dc.Renderer.RenderMessage(timestamp, "PROGRESS", fmt.Sprintf("Task Checklist: %s", msg.Text))
}
// 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)
}
+388
View File
@@ -0,0 +1,388 @@
package cli
import (
"context"
"fmt"
"os"
"syscall"
"text/tabwriter"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/grpc-go/cline"
"github.com/spf13/cobra"
"google.golang.org/grpc/health/grpc_health_v1"
)
func NewInstanceCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "instance",
Aliases: []string{"i"},
Short: "Manage Cline instances",
Long: `List and manage multiple Cline instances similar to kubectl contexts.`,
}
cmd.AddCommand(newInstanceListCommand())
cmd.AddCommand(newInstanceUseCommand())
cmd.AddCommand(newInstanceNewCommand())
cmd.AddCommand(newInstanceKillCommand())
return cmd
}
func newInstanceKillCommand() *cobra.Command {
var killAll bool
cmd := &cobra.Command{
Use: "kill <address>",
Aliases: []string{"k"},
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 !killAll && len(args) != 1 {
return fmt.Errorf("requires exactly one address argument when --all is not specified")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if global.Clients == nil {
return fmt.Errorf("clients not initialized")
}
ctx := cmd.Context()
registry := global.Clients.GetRegistry()
if killAll {
return killAllInstances(ctx, registry)
} else {
return killSingleInstance(ctx, registry, args[0])
}
},
}
cmd.Flags().BoolVar(&killAll, "all", false, "kill all running instances")
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 {
// Get all instances from registry
instances, err := registry.ListInstancesCleaned(ctx)
if err != nil {
return fmt.Errorf("failed to list instances: %w", err)
}
if len(instances) == 0 {
fmt.Println("No Cline instances found to kill.")
return nil
}
fmt.Printf("Killing %d instances...\n", len(instances))
var killResults []killResult
// Kill all instances
for _, instance := range instances {
result := killInstanceProcess(ctx, registry, instance.Address)
killResults = append(killResults, result)
if result.err != nil {
fmt.Printf("✗ Failed to kill %s: %v\n", instance.Address, result.err)
} else if result.alreadyDead {
fmt.Printf("⚠ Instance %s appears to be already dead\n", instance.Address)
} else {
fmt.Printf("✓ Killed %s (PID %d)\n", instance.Address, result.pid)
}
}
// Wait for all instances to clean up their registry entries
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)
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)
for _, remaining := range remainingInstances {
fmt.Printf(" - %s\n", remaining.Address)
}
}
}
// Print summary
successful := 0
failed := 0
alreadyDead := 0
for _, result := range killResults {
if result.err != nil {
failed++
} else if result.alreadyDead {
alreadyDead++
} else {
successful++
}
}
fmt.Printf("\nSummary: ")
if successful > 0 {
fmt.Printf("Successfully killed %d instances. ", successful)
}
if alreadyDead > 0 {
fmt.Printf("%d were already dead. ", alreadyDead)
}
if failed > 0 {
fmt.Printf("%d failures.", failed)
return fmt.Errorf("failed to kill %d out of %d instances", failed, len(instances))
}
fmt.Println()
return nil
}
type killResult struct {
address string
pid int
alreadyDead bool
err error
}
func killInstanceProcess(ctx context.Context, registry *global.ClientRegistry, address string) killResult {
// Get gRPC client and process info
client, err := registry.GetClient(ctx, address)
if err != nil {
return killResult{address: address, alreadyDead: true, err: nil}
}
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
if err != nil {
return killResult{address: address, alreadyDead: true, err: nil}
}
pid := int(processInfo.ProcessId)
// Kill the process
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
return killResult{address: address, pid: pid, err: err}
}
return killResult{address: address, pid: pid, err: nil}
}
func newInstanceListCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
Short: "List all registered Cline instances",
Long: `List all registered Cline instances with their status and connection details.`,
RunE: func(cmd *cobra.Command, args []string) error {
if global.Clients == nil {
return fmt.Errorf("clients not initialized")
}
ctx := cmd.Context()
registry := global.Clients.GetRegistry()
// Load, cleanup stale local entries, and update health
instances, err := registry.ListInstancesCleaned(ctx)
if err != nil {
return fmt.Errorf("failed to list instances: %w", err)
}
defaultInstance := registry.GetDefaultInstance()
if len(instances) == 0 {
fmt.Println("No Cline instances found.")
fmt.Println("Run 'cline instance new' to start a new instance, or 'cline task new \"...\"' to auto-start one.")
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")
for _, instance := range instances {
isDefault := ""
if instance.Address == defaultInstance {
isDefault = "*"
}
lastSeen := instance.LastSeen.Format("15:04:05")
if time.Since(instance.LastSeen) > 24*time.Hour {
lastSeen = instance.LastSeen.Format("2006-01-02")
}
// Get PID via RPC if instance is healthy
pid := "N/A"
if instance.Status == grpc_health_v1.HealthCheckResponse_SERVING {
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)
// Update version from RPC if available
if processInfo.Version != nil && *processInfo.Version != "" && *processInfo.Version != "unknown" {
instance.Version = *processInfo.Version
}
}
}
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
instance.Address,
instance.Status,
instance.Version,
lastSeen,
pid,
isDefault,
)
}
w.Flush()
return nil
},
}
return cmd
}
func newInstanceUseCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "use <address>",
Aliases: []string{"u"},
Short: "Set the default Cline instance",
Long: `Set the default Cline instance to use for subsequent commands.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
address := args[0]
if global.Clients == nil {
return fmt.Errorf("clients not initialized")
}
registry := global.Clients.GetRegistry()
// Verify the instance exists
_, err := registry.GetInstance(address)
if err != nil {
return fmt.Errorf("instance %s not found. Run 'cline instance list' to see available instances", address)
}
// Set as default
if err := registry.SetDefaultInstance(address); err != nil {
return fmt.Errorf("failed to set default instance: %w", err)
}
fmt.Printf("Switched to instance: %s\n", address)
return nil
},
}
return cmd
}
func newInstanceNewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "new",
Aliases: []string{"n"},
Short: "Create a new Cline instance",
Long: `Create a new Cline instance with automatically assigned ports.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if global.Clients == nil {
return fmt.Errorf("clients not initialized")
}
fmt.Println("Starting new Cline instance...")
instance, err := global.Clients.StartNewInstance(ctx)
if err != nil {
return fmt.Errorf("failed to start instance: %w", err)
}
fmt.Printf("Successfully started new instance:\n")
fmt.Printf(" Address: %s\n", instance.Address)
fmt.Printf(" 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")
}
return nil
},
}
return cmd
}
+366
View File
@@ -0,0 +1,366 @@
package sqlite
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"time"
"github.com/cline/cli/pkg/common"
_ "github.com/mattn/go-sqlite3"
"google.golang.org/grpc/health/grpc_health_v1"
)
// normalizeAddressVariants returns address variants to try when querying SQLite.
// Handles localhost/127.0.0.1 equivalence by returning both forms.
func normalizeAddressVariants(address string) []string {
variants := []string{address}
// Extract host and port
host, port, err := net.SplitHostPort(address)
if err != nil {
return variants
}
// Add the alternate form for localhost/127.0.0.1
if host == "localhost" {
variants = append(variants, net.JoinHostPort("127.0.0.1", port))
} else if host == "127.0.0.1" {
variants = append(variants, net.JoinHostPort("localhost", port))
}
return variants
}
// LockManager provides access to the SQLite locks database
type LockManager struct {
dbPath string
db *sql.DB
}
// NewLockManager creates a new lock manager
func NewLockManager(clineDir string) (*LockManager, error) {
dbPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "locks.db")
// Ensure the directory exists (for future DB creation by cline-core)
dbDir := filepath.Dir(dbPath)
if err := os.MkdirAll(dbDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create database directory: %w", err)
}
// Check if database exists
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
// Database doesn't exist - return manager with nil db
// All methods already handle this gracefully!
return &LockManager{dbPath: dbPath, db: nil}, nil
}
// Database exists - open it normally (no schema creation)
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
// If we can't open existing database, return nil db manager
return &LockManager{dbPath: dbPath, db: nil}, nil
}
// Test the connection
if err := db.Ping(); err != nil {
db.Close()
// If connection fails, return nil db manager
return &LockManager{dbPath: dbPath, db: nil}, nil
}
return &LockManager{
dbPath: dbPath,
db: db,
}, nil
}
// ensureConnection attempts to establish a database connection if one doesn't exist
func (lm *LockManager) ensureConnection() error {
// If we already have a connection, we're done
if lm.db != nil {
return nil
}
// Check if database exists now (created by cline-core)
if _, err := os.Stat(lm.dbPath); os.IsNotExist(err) {
return fmt.Errorf("database not available")
}
// Database exists, try to connect
db, err := sql.Open("sqlite3", lm.dbPath)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
if err := db.Ping(); err != nil {
db.Close()
return fmt.Errorf("database connection failed: %w", err)
}
// Success! Update our connection permanently
lm.db = db
return nil
}
// Close closes the database connection
func (lm *LockManager) Close() error {
if lm.db != nil {
return lm.db.Close()
}
return nil
}
// GetInstanceLocks returns all instance locks
func (lm *LockManager) GetInstanceLocks() ([]common.LockRow, error) {
if err := lm.ensureConnection(); err != nil {
return []common.LockRow{}, nil
}
query := common.SelectInstanceLocksSQL
rows, err := lm.db.Query(query)
if err != nil {
return nil, fmt.Errorf("failed to query instance locks: %w", err)
}
defer rows.Close()
var locks []common.LockRow
for rows.Next() {
var lock common.LockRow
err := rows.Scan(&lock.ID, &lock.HeldBy, &lock.LockType, &lock.LockTarget, &lock.LockedAt)
if err != nil {
return nil, fmt.Errorf("failed to scan lock row: %w", err)
}
locks = append(locks, lock)
}
return locks, nil
}
// RemoveInstanceLock removes an instance lock by address
func (lm *LockManager) RemoveInstanceLock(address string) error {
if err := lm.ensureConnection(); err != nil {
return nil // Gracefully handle missing database for cleanup operations
}
query := common.DeleteInstanceLockSQL
_, err := lm.db.Exec(query, address)
if err != nil {
return fmt.Errorf("failed to remove instance lock: %w", err)
}
return nil
}
// 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
}
query := common.CountInstanceLockSQL
var count int
err := lm.db.QueryRow(query, address).Scan(&count)
if err != nil {
return false, fmt.Errorf("failed to check instance existence: %w", err)
}
return count > 0, nil
}
// GetInstanceInfo returns instance information directly from SQLite.
// Handles localhost/127.0.0.1 equivalence by trying both variants.
func (lm *LockManager) GetInstanceInfo(address string) (*common.CoreInstanceInfo, error) {
if err := lm.ensureConnection(); err != nil {
return nil, err
}
query := common.SelectInstanceLockByHolderSQL
variants := normalizeAddressVariants(address)
var heldBy, lockTarget string
var lockedAt int64
var lastErr error
// Try each address variant (e.g., localhost:50607 and 127.0.0.1:50607)
for _, variant := range variants {
err := lm.db.QueryRow(query, variant).Scan(&heldBy, &lockTarget, &lockedAt)
if err == nil {
// Found it!
return &common.CoreInstanceInfo{
Address: heldBy,
HostServiceAddress: lockTarget,
Status: grpc_health_v1.HealthCheckResponse_UNKNOWN,
LastSeen: time.Unix(lockedAt/1000, 0),
}, nil
}
if err != sql.ErrNoRows {
// Real error (not just "not found"), save it
lastErr = err
}
}
// None of the variants were found
if lastErr != nil {
return nil, fmt.Errorf("failed to query instance: %w", lastErr)
}
return nil, fmt.Errorf("instance %s not found", address)
}
// ListInstancesWithHealthCheck returns all instances with real-time health checks
func (lm *LockManager) ListInstancesWithHealthCheck(ctx context.Context) ([]*common.CoreInstanceInfo, error) {
if err := lm.ensureConnection(); err != nil {
return []*common.CoreInstanceInfo{}, nil
}
// Get all instance locks
locks, err := lm.GetInstanceLocks()
if err != nil {
return nil, fmt.Errorf("failed to get instance locks: %w", err)
}
var instances []*common.CoreInstanceInfo
for _, lock := range locks {
// Create instance info using actual SQLite data
status, err := common.PerformHealthCheck(ctx, lock.HeldBy)
if status != grpc_health_v1.HealthCheckResponse_SERVING || err != nil {
time.Sleep(1 * time.Second)
status, err = common.PerformHealthCheck(ctx, lock.HeldBy)
}
info := &common.CoreInstanceInfo{
Address: lock.HeldBy,
HostServiceAddress: lock.LockTarget,
Status: status,
LastSeen: time.Unix(lock.LockedAt/1000, 0),
}
instances = append(instances, info)
}
return instances, nil
}
// GetDefaultInstance reads the default instance from the settings file
func GetDefaultInstance(clineDir string) (string, error) {
settingsPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
data, err := os.ReadFile(settingsPath)
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", fmt.Errorf("failed to read default instance file: %w", err)
}
var defaultInstance common.DefaultCoreInstance
if err := json.Unmarshal(data, &defaultInstance); err != nil {
return "", fmt.Errorf("failed to parse default instance JSON: %w", err)
}
if defaultInstance.Address == "" {
return "", fmt.Errorf("default instance not set in settings file")
}
return defaultInstance.Address, nil
}
// SetDefaultInstance writes the default instance to the settings file with proper locking
func SetDefaultInstance(clineDir, address string) error {
// Create lock manager for this operation
lockManager, err := NewLockManager(clineDir)
if err != nil {
return fmt.Errorf("Warning: SQLite unavailable, writing without lock: %v\n", err)
}
defer lockManager.Close()
settingsPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
// Generate a unique identifier for this CLI process
heldBy := fmt.Sprintf("cli-process-%d", os.Getpid())
// Use file lock for the write operation
return lockManager.WithFileLock(settingsPath, heldBy, func() error {
return writeDefaultInstanceJSONToDisk(clineDir, address)
})
}
func writeDefaultInstanceJSONToDisk(clineDir, address string) error {
settingsDir := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings")
if err := os.MkdirAll(settingsDir, 0755); err != nil {
return fmt.Errorf("failed to create settings directory: %w", err)
}
settingsPath := filepath.Join(settingsDir, "cli-default-instance.json")
payload := common.DefaultCoreInstance{
Address: address,
LastUpdated: time.Now().Format(time.RFC3339),
}
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal default instance JSON: %w", err)
}
if err := os.WriteFile(settingsPath, data, 0644); err != nil {
return fmt.Errorf("failed to write default instance file: %w", err)
}
return nil
}
// AcquireFileLock attempts to acquire a file lock
func (lm *LockManager) AcquireFileLock(filePath, heldBy string) error {
if err := lm.ensureConnection(); err != nil {
return err
}
now := time.Now().Unix() * 1000 // Convert to milliseconds
query := common.InsertFileLockSQL
_, err := lm.db.Exec(query, heldBy, filePath, now)
if err != nil {
return fmt.Errorf("failed to acquire file lock for %s: %w", filePath, err)
}
return nil
}
// ReleaseFileLock releases a file lock
func (lm *LockManager) ReleaseFileLock(filePath, heldBy string) error {
if lm.db == nil {
return nil
}
query := common.DeleteFileLockSQL
_, err := lm.db.Exec(query, heldBy, filePath)
if err != nil {
return fmt.Errorf("failed to release file lock for %s: %w", filePath, err)
}
return nil
}
// WithFileLock executes a function while holding a file lock
func (lm *LockManager) WithFileLock(filePath, heldBy string, fn func() error) error {
if err := lm.AcquireFileLock(filePath, heldBy); err != nil {
return err
}
defer func() {
if releaseErr := lm.ReleaseFileLock(filePath, heldBy); releaseErr != nil {
fmt.Printf("Warning: Failed to release file lock for %s: %v\n", filePath, releaseErr)
}
}()
return fn()
}
+444
View File
@@ -0,0 +1,444 @@
package cli
import (
"context"
"fmt"
"io"
"os"
"strings"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/spf13/cobra"
)
func NewTaskCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "task",
Aliases: []string{"t"},
Short: "Manage Cline tasks",
Long: `Create, monitor, and manage Cline AI tasks.`,
}
cmd.AddCommand(newTaskNewCommand())
cmd.AddCommand(newTaskCancelCommand())
cmd.AddCommand(newTaskFollowCommand())
cmd.AddCommand(newTaskSendCommand())
cmd.AddCommand(newTaskViewCommand())
cmd.AddCommand(newTaskListCommand())
cmd.AddCommand(newTaskResumeCommand())
return cmd
}
var taskManager *task.Manager
func ensureTaskManager(ctx context.Context, address string) error {
if taskManager == nil || (address != "" && taskManager.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)
}
taskManager, err = task.NewManagerForAddress(ctx, address)
instanceAddress = address
} else {
// Ensure default instance exists
if err := ensureDefaultInstance(ctx); err != nil {
return fmt.Errorf("failed to ensure default instance: %w", err)
}
taskManager, err = task.NewManagerForDefault(ctx)
if err == nil {
instanceAddress = taskManager.GetCurrentInstance()
}
}
if err != nil {
return fmt.Errorf("failed to create task 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
}
// ensureInstanceAtAddress ensures an instance exists at the given address
func ensureInstanceAtAddress(ctx context.Context, address string) error {
if global.Clients == nil {
return fmt.Errorf("global clients not initialized")
}
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
)
cmd := &cobra.Command{
Use: "new <prompt>",
Aliases: []string{"n"},
Short: "Create a new task",
Long: `Create a new Cline task with the specified prompt. If no Cline instance exists at the specified address, a new one will be started automatically.`,
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Get content from both args and stdin
prompt, err := getContentFromStdinAndArgs(args)
if err != nil {
return fmt.Errorf("failed to read prompt: %w", err)
}
// Validate that prompt is passed in call
if prompt == "" {
return fmt.Errorf("prompt required: provide as argument or pipe via stdin")
}
// Ensure task manager is initialized
if err := ensureTaskManager(ctx, address); err != nil {
return err
}
// Set 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)
}
fmt.Printf("Mode set to: %s\n", mode)
}
// Create the task
taskID, err := taskManager.CreateTask(ctx, prompt, images, files, workspaces)
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)
}
return nil
},
}
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)")
return cmd
}
func newTaskCancelCommand() *cobra.Command {
var address string
cmd := &cobra.Command{
Use: "cancel",
Aliases: []string{"c"},
Short: "Cancel the current task",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if err := ensureTaskManager(ctx, address); err != nil {
return err
}
if err := taskManager.CancelTask(ctx); err != nil {
return err
}
fmt.Println("Task cancelled successfully")
fmt.Printf("Instance: %s\n", taskManager.GetCurrentInstance())
return nil
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
func newTaskSendCommand() *cobra.Command {
var (
images []string
files []string
address string
mode string
approve string
)
cmd := &cobra.Command{
Use: "send [message]",
Aliases: []string{"s"},
Short: "Send a followup message to the current task and/or update mode/approve",
Long: `Send a followup message to continue the conversation with the current task and/or update mode/approve.`,
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// 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 approve != "" && approve != "true" && approve != "false" {
return fmt.Errorf("--approve must be 'true' or 'false'")
}
if approve != "" && mode != "" {
return fmt.Errorf("cannot use --approve and --mode together")
}
// Ensure task manager is initialized
if err := ensureTaskManager(ctx, address); err != nil {
return err
}
sendDisabled, err := taskManager.CheckSendDisabled(ctx)
if err != nil {
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
}
if mode != "" {
if err := taskManager.SetModeAndSendMessage(ctx, mode, message, images, files); err != nil {
return fmt.Errorf("failed to set mode and send message: %w", err)
}
fmt.Printf("Mode set to %s and message sent successfully.\n", mode)
} else {
if err := taskManager.SendMessage(ctx, message, images, files, approve); err != nil {
return err
}
fmt.Printf("Message sent successfully.\n")
}
fmt.Printf("Instance: %s\n", taskManager.GetCurrentInstance())
return nil
},
}
cmd.Flags().StringSliceVarP(&images, "image", "i", nil, "attach image files")
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")
return cmd
}
func newTaskFollowCommand() *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.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if err := ensureTaskManager(ctx, address); err != nil {
return err
}
fmt.Printf("Using instance: %s\n", taskManager.GetCurrentInstance())
return taskManager.FollowConversation(ctx)
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
func newTaskViewCommand() *cobra.Command {
var (
current bool
summary 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.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if err := ensureTaskManager(ctx, address); err != nil {
return err
}
fmt.Printf("Using instance: %s\n", taskManager.GetCurrentInstance())
if current {
return taskManager.ShowConversation(ctx)
} else if summary {
return taskManager.GatherFinalSummary(ctx)
} else {
return taskManager.FollowConversationUntilCompletion(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().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"},
Short: "List recent task history",
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)
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
func newTaskResumeCommand() *cobra.Command {
var address string
cmd := &cobra.Command{
Use: "resume <task-id>",
Aliases: []string{"r"},
Short: "Resume a task by ID",
Long: `Resume an existing task by ID.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
taskID := args[0]
// Ensure task manager is initialized
if err := ensureTaskManager(ctx, address); err != nil {
return err
}
fmt.Printf("Using instance: %s\n", taskManager.GetCurrentInstance())
return taskManager.ResumeTask(ctx, taskID)
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
// 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 {
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
}
// CleanupTaskManager cleans up the task manager resources
func CleanupTaskManager() {
if taskManager != nil {
taskManager.Cleanup()
}
}
+946
View File
@@ -0,0 +1,946 @@
package task
import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/handlers"
"github.com/cline/cli/pkg/cli/types"
"github.com/cline/grpc-go/client"
"github.com/cline/grpc-go/cline"
)
// Manager handles task execution and message display
type Manager struct {
mu sync.RWMutex
client *client.ClineClient
clientAddress string
state *types.ConversationState
renderer *display.Renderer
streamingDisplay *display.StreamingDisplay
handlerRegistry *handlers.HandlerRegistry
}
// NewManager creates a new task manager
func NewManager(client *client.ClineClient) *Manager {
state := types.NewConversationState()
renderer := display.NewRenderer()
streamingDisplay := display.NewStreamingDisplay(state, renderer)
// Create handler registry and register handlers
registry := handlers.NewHandlerRegistry()
registry.Register(handlers.NewAskHandler())
registry.Register(handlers.NewSayHandler())
return &Manager{
client: client,
clientAddress: "", // Will be set when client is provided
state: state,
renderer: renderer,
streamingDisplay: streamingDisplay,
handlerRegistry: registry,
}
}
// NewManagerForAddress creates a new task manager for a specific instance address
func NewManagerForAddress(ctx context.Context, address string) (*Manager, error) {
client, err := global.GetClientForAddress(ctx, address)
if err != nil {
return nil, fmt.Errorf("failed to get client for address %s: %w", address, err)
}
manager := NewManager(client)
manager.clientAddress = address
return manager, nil
}
// NewManagerForDefault creates a new task manager using the default instance
func NewManagerForDefault(ctx context.Context) (*Manager, error) {
client, err := global.GetDefaultClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get default client: %w", err)
}
manager := NewManager(client)
// Get the default instance address
if global.Clients != nil {
manager.clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
}
return manager, nil
}
// SwitchToInstance switches the manager to use a different Cline instance
func (m *Manager) SwitchToInstance(ctx context.Context, address string) error {
m.mu.Lock()
defer m.mu.Unlock()
// Get client for the new address
newClient, err := global.GetClientForAddress(ctx, address)
if err != nil {
return fmt.Errorf("failed to get client for address %s: %w", address, err)
}
// Update the client and address
m.client = newClient
m.clientAddress = address
if global.Config.Verbose {
m.renderer.RenderDebug("Switched to instance: %s", address)
}
return nil
}
// GetCurrentInstance returns the address of the current instance
func (m *Manager) GetCurrentInstance() string {
m.mu.RLock()
defer m.mu.RUnlock()
return m.clientAddress
}
// CreateTask creates a new task
func (m *Manager) CreateTask(ctx context.Context, prompt string, images, files []string, workspacePaths []string) (string, error) {
m.mu.Lock()
defer m.mu.Unlock()
if global.Config.Verbose {
m.renderer.RenderDebug("Creating task: %s", prompt)
if len(files) > 0 {
m.renderer.RenderDebug("Files: %v", files)
}
if len(images) > 0 {
m.renderer.RenderDebug("Images: %v", images)
}
if len(workspacePaths) > 0 {
m.renderer.RenderDebug("Workspaces: %v", workspacePaths)
}
}
// Check if there's an active task and cancel it first
if err := m.cancelExistingTaskIfNeeded(ctx); err != nil {
return "", fmt.Errorf("failed to cancel existing task: %w", err)
}
// Create task request
req := &cline.NewTaskRequest{
Text: prompt,
Images: images,
Files: files,
}
resp, err := m.client.Task.NewTask(ctx, req)
if err != nil {
return "", fmt.Errorf("failed to create task: %w", err)
}
taskID := resp.Value
return taskID, nil
}
// cancelExistingTaskIfNeeded checks if there's an active task and cancels it
func (m *Manager) cancelExistingTaskIfNeeded(ctx context.Context) error {
// Try to get the current state to check if there's an active task
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
// If we can't get state, assume no active task and continue
if global.Config.Verbose {
m.renderer.RenderDebug("Could not get state to check for active task: %v", err)
}
return nil
}
// Properly parse the state to check if there's actually an active task
if state.StateJson != "" {
var stateData types.ExtensionState
if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
// If we can't parse state, assume no active task
if global.Config.Verbose {
m.renderer.RenderDebug("Could not parse state JSON: %v", err)
}
return nil
}
// Check if there's actually an active task
if stateData.CurrentTaskItem != nil && stateData.CurrentTaskItem.Id != "" {
if global.Config.Verbose {
m.renderer.RenderDebug("Found active task %s, cancelling...", stateData.CurrentTaskItem.Id)
}
// Cancel the existing task
_, err := m.client.Task.CancelTask(ctx, &cline.EmptyRequest{})
if err != nil {
if global.Config.Verbose {
m.renderer.RenderDebug("Cancel task returned error: %v", err)
}
} else {
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) {
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return false, fmt.Errorf("failed to get latest state: %w", err)
}
messages, err := m.extractMessagesFromState(state.StateJson)
if err != nil {
return false, fmt.Errorf("failed to extract messages: %w", err)
}
if len(messages) == 0 {
return false, nil
}
// Use final message to perform validation
lastMessage := messages[len(messages)-1]
// 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"
}
isError := false
// Check if message is an error type
if lastMessage.Type == types.MessageTypeAsk {
for _, errType := range errorTypes {
if lastMessage.Ask == errType {
isError = true
break
}
}
}
// Streaming and error check
if lastMessage.Partial && !isError {
if global.Config.Verbose {
m.renderer.RenderDebug("Send disabled: task is streaming and non-error")
}
return true, nil
}
// All ask messages allow sending
if lastMessage.Type == types.MessageTypeAsk {
if global.Config.Verbose {
m.renderer.RenderDebug("Send enabled: ask message")
}
return false, nil
}
// Technically unnecessary but implements getButtonConfig 1-1
if lastMessage.Type == types.MessageTypeSay && lastMessage.Say == string(types.SayTypeAPIReqStarted) {
if global.Config.Verbose {
m.renderer.RenderDebug("Send disabled: API request is active")
}
return true, nil
}
if global.Config.Verbose {
m.renderer.RenderDebug("Send disabled: default fallback")
}
return true, nil
}
// SendMessage sends a followup message to the current task
func (m *Manager) SendMessage(ctx context.Context, message string, images, files []string, approve string) error {
responseType := "messageResponse"
if approve == "true" {
responseType = "yesButtonClicked"
}
if approve == "false" {
responseType = "noButtonClicked"
}
if global.Config.Verbose {
m.renderer.RenderDebug("Sending message: %s", message)
if len(files) > 0 {
m.renderer.RenderDebug("Files: %v", files)
}
if len(images) > 0 {
m.renderer.RenderDebug("Images: %v", images)
}
}
// Send the followup message using AskResponse
req := &cline.AskResponseRequest{
ResponseType: responseType,
Text: message,
Images: images,
Files: files,
}
_, err := m.client.Task.AskResponse(ctx, req)
if err != nil {
return fmt.Errorf("failed to send message: %w", err)
}
return nil
}
// SetMode sets the Plan/Act mode for the current Cline instance and optionally sends message
func (m *Manager) SetMode(ctx context.Context, mode string, message *string, images, files []string) error {
if mode != "act" && mode != "plan" {
return fmt.Errorf("invalid mode '%s': must be 'act' or 'plan'", mode)
}
var protoMode cline.PlanActMode
if mode == "plan" {
protoMode = cline.PlanActMode_PLAN
} else {
protoMode = cline.PlanActMode_ACT
}
req := &cline.TogglePlanActModeRequest{
Metadata: &cline.Metadata{},
Mode: protoMode,
}
if message != nil {
req.ChatContent = &cline.ChatContent{
Message: message,
Images: images,
Files: files,
}
}
_, err := m.client.State.TogglePlanActModeProto(ctx, req)
if err != nil {
return fmt.Errorf("failed to set mode to '%s': %w", mode, err)
}
return nil
}
// SetModeAndSendMessage sets the mode and sends a message in one operation
// Handles task restoration internally if the mode switch cancels the current task
func (m *Manager) SetModeAndSendMessage(ctx context.Context, mode, message string, images, files []string) error {
if mode != "act" && mode != "plan" {
return fmt.Errorf("invalid mode '%s': must be 'act' or 'plan'", mode)
}
taskId, err := m.getCurrentTaskId(ctx)
if err != nil {
return fmt.Errorf("failed to get current task ID: %w", err)
}
fmt.Printf("Current task ID: %s\n", taskId)
var protoMode cline.PlanActMode
if mode == "plan" {
protoMode = cline.PlanActMode_PLAN
} else {
protoMode = cline.PlanActMode_ACT
}
req := &cline.TogglePlanActModeRequest{
Metadata: &cline.Metadata{},
Mode: protoMode,
ChatContent: &cline.ChatContent{
Message: &message,
Images: images,
Files: files,
},
}
result, err := m.client.State.TogglePlanActModeProto(ctx, req)
if err != nil {
return fmt.Errorf("failed to set mode to '%s': %w", mode, err)
}
taskPreserved := result.Value
if taskPreserved {
fmt.Printf("Message sent as part of mode change\n")
return nil
} else {
if message != "" || len(images) > 0 || len(files) > 0 {
fmt.Printf("Task was cancelled, restoring task ID: %s\n", taskId)
err = m.ReinitExistingTaskFromId(ctx, taskId)
if err != nil {
return fmt.Errorf("Failed to restore task: %w", err)
}
fmt.Printf("Task restored successfully\n")
// Hardcoded sleep should be replaced with a way to fetch whether task is ready algorithmically
time.Sleep(1 * time.Second)
err = m.SendMessage(ctx, message, images, files, "")
if err != nil {
return fmt.Errorf("Failed to send message: %w", err)
}
fmt.Printf("Message sent to restored task\n")
}
}
return nil
}
// getCurrentTaskId extracts the current task ID from the server state
func (m *Manager) getCurrentTaskId(ctx context.Context) (string, error) {
// Get the latest state
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return "", fmt.Errorf("failed to get state: %w", err)
}
// Parse the server state JSON
var stateData types.ExtensionState
if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
return "", fmt.Errorf("failed to parse state JSON: %w", err)
}
// Extract current task ID
if stateData.CurrentTaskItem != nil && stateData.CurrentTaskItem.Id != "" {
return stateData.CurrentTaskItem.Id, nil
}
return "", fmt.Errorf("no current task found in state")
}
// ReinitExistingTaskFromId reinitializes an existing task from the given task ID
func (m *Manager) ReinitExistingTaskFromId(ctx context.Context, taskId string) error {
req := &cline.StringRequest{Value: taskId}
resp, err := m.client.Task.ShowTaskWithId(ctx, req)
if err != nil {
return fmt.Errorf("Failed to reinitialize task %s: %w", taskId, err)
}
fmt.Printf("Successfully reinitialized task: %s (ID: %s)\n", taskId, resp.Id)
return nil
}
// ResumeTask resumes an existing task by ID
func (m *Manager) ResumeTask(ctx context.Context, taskID string) error {
m.mu.Lock()
defer m.mu.Unlock()
if global.Config.Verbose {
m.renderer.RenderDebug("Resuming task: %s", taskID)
}
// This call handles cancellation of any active task
if err := m.ReinitExistingTaskFromId(ctx, taskID); err != nil {
return fmt.Errorf("failed to resume task %s: %w", taskID, err)
}
fmt.Printf("Task %s resumed successfully\n", taskID)
return nil
}
// CancelTask cancels the current task
func (m *Manager) CancelTask(ctx context.Context) error {
m.mu.Lock()
defer m.mu.Unlock()
_, err := m.client.Task.CancelTask(ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to cancel task: %w", err)
}
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 {
m.mu.RLock()
defer m.mu.RUnlock()
// Get the latest state which contains messages
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to get state: %w", err)
}
// Parse the state JSON to extract messages
messages, err := m.extractMessagesFromState(state.StateJson)
if err != nil {
return fmt.Errorf("failed to extract messages: %w", err)
}
if len(messages) == 0 {
fmt.Println("No conversation history found.")
return nil
}
// Display messages
for i, msg := range messages {
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)")
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Create stream coordinator
coordinator := NewStreamCoordinator()
// Load history first
totalMessageCount, err := m.loadAndDisplayRecentHistory(ctx)
if err != nil {
m.renderer.RenderDebug("Warning: Failed to load conversation history: %v", err)
totalMessageCount = 0
}
coordinator.SetConversationTurnStartIndex(totalMessageCount)
fmt.Println("\n--- Live updates ---")
// Start both streams concurrently
errChan := make(chan error, 2)
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)
}
// Wait for either stream to error or context cancellation
select {
case <-ctx.Done():
return ctx.Err()
case err := <-errChan:
cancel()
return err
}
}
// 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)")
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Create stream coordinator
coordinator := NewStreamCoordinator()
// Get current message count without displaying history
totalMessageCount, err := m.getCurrentMessageCount(ctx)
if err != nil {
m.renderer.RenderDebug("Warning: Failed to get current message count: %v", err)
totalMessageCount = 0
}
coordinator.SetConversationTurnStartIndex(totalMessageCount)
// Start both streams concurrently
errChan := make(chan error, 2)
completionChan := make(chan bool, 1)
if global.Config.OutputFormat == "json" {
go m.handleStateStream(ctx, coordinator, errChan, completionChan)
} else {
go m.handleStateStream(ctx, coordinator, errChan, completionChan)
go m.handlePartialMessageStream(ctx, coordinator, errChan)
}
// Wait for completion, error, or context cancellation
select {
case <-ctx.Done():
return ctx.Err()
case <-completionChan:
cancel()
return nil
case err := <-errChan:
cancel()
return err
}
}
// handleStateStream handles the SubscribeToState stream
func (m *Manager) handleStateStream(ctx context.Context, coordinator *StreamCoordinator, errChan chan error, completionChan chan bool) {
stateStream, err := m.client.State.SubscribeToState(ctx, &cline.EmptyRequest{})
if err != nil {
errChan <- fmt.Errorf("failed to subscribe to state: %w", err)
return
}
for {
select {
case <-ctx.Done():
return
default:
stateUpdate, err := stateStream.Recv()
if err != nil {
m.renderer.RenderDebug("State stream receive error: %v", err)
errChan <- fmt.Errorf("failed to receive state update: %w", err)
return
}
var pErr error
if global.Config.OutputFormat == "json" {
pErr = m.processStateUpdateJsonMode(stateUpdate, coordinator, completionChan)
} else {
pErr = m.processStateUpdate(stateUpdate, coordinator, completionChan)
}
if pErr != nil {
m.renderer.RenderDebug("State processing error: %v", pErr)
}
}
}
}
func (m *Manager) processStateUpdateJsonMode(stateUpdate *cline.State, coordinator *StreamCoordinator, completionChan chan bool) error {
messages, err := m.extractMessagesFromState(stateUpdate.StateJson)
if err != nil {
return err
}
// Process messages from current conversation turn onwards
startIndex := coordinator.GetConversationTurnStartIndex()
var foundCompletion bool
var displayedUsage bool
for i := startIndex; i < len(messages); i++ {
msg := messages[i]
if global.Config.Verbose {
m.renderer.RenderDebug("State message %d: type=%s, say=%s", i, msg.Type, msg.Say)
}
// Exit after we've seen a task completion & printed out the usage info
if msg.Say == string(types.SayTypeCompletionResult) {
foundCompletion = true
}
// Determine if message is ready to be displayed now
shouldDisplay := true
switch {
case msg.Say == string(types.SayTypeAPIReqStarted):
shouldDisplay = false
apiInfo := types.APIRequestInfo{Cost: -1}
if err := json.Unmarshal([]byte(msg.Text), &apiInfo); err == nil && apiInfo.Cost >= 0 {
shouldDisplay = true
displayedUsage = true
}
}
// Skip if message is partial, except for a specific edge case
if msg.Partial {
// Exception: display if type=say, text="", say="text"
if msg.IsSay() && msg.Text == "" && msg.Say == string(types.SayTypeText) {
shouldDisplay = true
} else {
shouldDisplay = false
}
}
// Display valid messages, exit as soon as we hit a non-valid message
if shouldDisplay {
coordinator.CompleteTurn(i + 1) // Mark the message as complete as soon as we print it
m.displayMessage(msg, false, false, i)
} else {
break
}
}
// We only want to exit after we've displayed the usage, for the case of seeing completion result
if completionChan != nil && foundCompletion && displayedUsage {
completionChan <- true
}
return nil
}
// 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 {
messages, err := m.extractMessagesFromState(stateUpdate.StateJson)
if err != nil {
return err
}
// Process messages from current conversation turn onwards
startIndex := coordinator.GetConversationTurnStartIndex()
var foundCompletion bool
var displayedUsage bool
for i := startIndex; i < len(messages); i++ {
msg := messages[i]
if global.Config.Verbose {
m.renderer.RenderDebug("State message %d: type=%s, say=%s", i, msg.Type, msg.Say)
}
// Exit after we've seen a task completion & printed out the usage info
if msg.Say == string(types.SayTypeCompletionResult) {
foundCompletion = true
}
// Currently handling a subset of message types for displaying
switch {
case msg.Say == string(types.SayTypeUserFeedback):
if !coordinator.IsProcessedInCurrentTurn("user_msg") {
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn("user_msg")
}
case msg.Say == string(types.SayTypeCheckpointCreated):
if !coordinator.IsProcessedInCurrentTurn("checkpoint") {
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn("checkpoint")
}
case msg.Say == string(types.SayTypeAPIReqStarted):
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
m.displayMessage(msg, false, false, i)
coordinator.CompleteTurn(len(messages))
displayedUsage = true
}
}
}
// We only want to exit after we've displayed the usage, for the case of seeing completion result
if completionChan != nil && foundCompletion && displayedUsage {
completionChan <- true
}
return nil
}
// handlePartialMessageStream handles the SubscribeToPartialMessage stream for streaming assistant text
func (m *Manager) handlePartialMessageStream(ctx context.Context, coordinator *StreamCoordinator, errChan chan error) {
partialStream, err := m.client.Ui.SubscribeToPartialMessage(ctx, &cline.EmptyRequest{})
if err != nil {
errChan <- fmt.Errorf("failed to subscribe to partial messages: %w", err)
return
}
for {
select {
case <-ctx.Done():
return
default:
protoMsg, err := partialStream.Recv()
if err != nil {
m.renderer.RenderDebug("Partial stream receive error: %v", err)
errChan <- fmt.Errorf("failed to receive partial message: %w", err)
return
}
// Convert proto message to our Message struct
msg := types.ConvertProtoToMessage(protoMsg)
// Debug: Log received message (always show for debugging)
m.renderer.RenderDebug("Received streaming message: type=%s, partial=%v, text_len=%d",
msg.Type, msg.Partial, len(msg.Text))
// Handle the message with streaming support for de-dupping
if err := m.handleStreamingMessage(msg); err != nil {
m.renderer.RenderDebug("Error handling streaming message: %v", err)
}
}
}
}
// handleStreamingMessage handles a streaming message
func (m *Manager) handleStreamingMessage(msg *types.ClineMessage) 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))
// Use streaming display which handles deduplication internally
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)
}
return nil
}
// truncateText truncates text for debug display
func (m *Manager) truncateText(text string, maxLen int) string {
if len(text) <= maxLen {
return text
}
return text[:maxLen] + "..."
}
// displayMessage displays a single message using the handler system
func (m *Manager) displayMessage(msg *types.ClineMessage, isLast, isPartial bool, messageIndex int) error {
if global.Config.OutputFormat == "json" {
return m.outputMessageAsJSON(msg)
} else {
dc := &handlers.DisplayContext{
State: m.state,
Renderer: m.renderer,
IsLast: isLast,
IsPartial: isPartial,
MessageIndex: messageIndex,
}
return m.handlerRegistry.Handle(msg, dc)
}
}
// outputMessageAsJSON prints a single cline message as json
func (m *Manager) outputMessageAsJSON(msg *types.ClineMessage) error {
jsonBytes, err := json.MarshalIndent(msg, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal message as JSON: %w", err)
}
fmt.Println(string(jsonBytes))
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
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return 0, fmt.Errorf("failed to get state: %w", err)
}
// Parse the state JSON to extract messages
messages, err := m.extractMessagesFromState(state.StateJson)
if err != nil {
return 0, fmt.Errorf("failed to extract messages: %w", err)
}
if len(messages) == 0 {
fmt.Println("No conversation history found.")
return 0, nil
}
// Show only the last 100 messages by default
const maxHistoryMessages = 100
totalMessages := len(messages)
startIndex := 0
if totalMessages > maxHistoryMessages {
startIndex = totalMessages - maxHistoryMessages
fmt.Printf("--- Conversation history (%d of %d messages) ---\n", maxHistoryMessages, totalMessages)
} 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
m.displayMessage(msg, false, false, i)
}
// Return the total number of messages in the conversation
return totalMessages, nil
}
// extractMessagesFromState parses the state JSON and extracts messages
func (m *Manager) extractMessagesFromState(stateJson string) ([]*types.ClineMessage, error) {
return types.ExtractMessagesFromStateJSON(stateJson)
}
// GetState returns the current conversation state
func (m *Manager) GetState() *types.ConversationState {
return m.state
}
// Cleanup cleans up resources
func (m *Manager) Cleanup() {
// Clean up streaming display resources if needed
if m.streamingDisplay != nil {
m.streamingDisplay.Cleanup()
}
}
+41
View File
@@ -0,0 +1,41 @@
package task
// 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
}
// NewStreamCoordinator creates a new stream coordinator
func NewStreamCoordinator() *StreamCoordinator {
return &StreamCoordinator{
conversationTurnStartIndex: 0,
processedInCurrentTurn: make(map[string]bool),
}
}
// SetConversationTurnStartIndex sets the starting index for the current conversation turn
func (sc *StreamCoordinator) SetConversationTurnStartIndex(index int) {
sc.conversationTurnStartIndex = index
}
// GetConversationTurnStartIndex returns the starting index for the current conversation turn
func (sc *StreamCoordinator) GetConversationTurnStartIndex() int {
return sc.conversationTurnStartIndex
}
// MarkProcessedInCurrentTurn marks an item as processed in the current turn
func (sc *StreamCoordinator) MarkProcessedInCurrentTurn(key string) {
sc.processedInCurrentTurn[key] = true
}
// IsProcessedInCurrentTurn checks if an item has been processed in the current turn
func (sc *StreamCoordinator) IsProcessedInCurrentTurn(key string) bool {
return sc.processedInCurrentTurn[key]
}
// CompleteTurn resets the coordinator for the next conversation turn
func (sc *StreamCoordinator) CompleteTurn(totalMessages int) {
sc.conversationTurnStartIndex = totalMessages
sc.processedInCurrentTurn = make(map[string]bool)
}
+321
View File
@@ -0,0 +1,321 @@
package types
import (
"encoding/json"
"fmt"
"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"`
}
// MessageType represents the type of message
type MessageType string
const (
MessageTypeAsk MessageType = "ask"
MessageTypeSay MessageType = "say"
)
// AskType represents different types of ASK messages
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"
AskTypeBrowserActionLaunch AskType = "browser_action_launch"
AskTypeUseMcpServer AskType = "use_mcp_server"
AskTypeNewTask AskType = "new_task"
AskTypeCondense AskType = "condense"
AskTypeReportBug AskType = "report_bug"
)
// SayType represents different types of SAY messages
type SayType string
const (
SayTypeTask SayType = "task"
SayTypeError SayType = "error"
SayTypeAPIReqStarted SayType = "api_req_started"
SayTypeAPIReqFinished SayType = "api_req_finished"
SayTypeText SayType = "text"
SayTypeReasoning SayType = "reasoning"
SayTypeCompletionResult SayType = "completion_result"
SayTypeUserFeedback SayType = "user_feedback"
SayTypeUserFeedbackDiff SayType = "user_feedback_diff"
SayTypeAPIReqRetried SayType = "api_req_retried"
SayTypeCommand SayType = "command"
SayTypeCommandOutput SayType = "command_output"
SayTypeTool SayType = "tool"
SayTypeShellIntegrationWarning SayType = "shell_integration_warning"
SayTypeBrowserActionLaunch SayType = "browser_action_launch"
SayTypeBrowserAction SayType = "browser_action"
SayTypeBrowserActionResult SayType = "browser_action_result"
SayTypeMcpServerRequestStarted SayType = "mcp_server_request_started"
SayTypeMcpServerResponse SayType = "mcp_server_response"
SayTypeMcpNotification SayType = "mcp_notification"
SayTypeUseMcpServer SayType = "use_mcp_server"
SayTypeDiffError SayType = "diff_error"
SayTypeDeletedAPIReqs SayType = "deleted_api_reqs"
SayTypeClineignoreError SayType = "clineignore_error"
SayTypeCheckpointCreated SayType = "checkpoint_created"
SayTypeLoadMcpDocumentation SayType = "load_mcp_documentation"
SayTypeInfo SayType = "info"
SayTypeTaskProgress SayType = "task_progress"
)
// ToolMessage represents a tool-related message
type ToolMessage struct {
Tool string `json:"tool"`
Path string `json:"path,omitempty"`
Content string `json:"content,omitempty"`
Diff string `json:"diff,omitempty"`
Regex string `json:"regex,omitempty"`
FilePattern string `json:"filePattern,omitempty"`
OperationIsLocatedInWorkspace *bool `json:"operationIsLocatedInWorkspace,omitempty"`
}
// ToolType represents different types of tools
type ToolType string
const (
ToolTypeEditedExistingFile ToolType = "editedExistingFile"
ToolTypeNewFileCreated ToolType = "newFileCreated"
ToolTypeReadFile ToolType = "readFile"
ToolTypeListFilesTopLevel ToolType = "listFilesTopLevel"
ToolTypeListFilesRecursive ToolType = "listFilesRecursive"
ToolTypeListCodeDefinitionNames ToolType = "listCodeDefinitionNames"
ToolTypeSearchFiles ToolType = "searchFiles"
ToolTypeWebFetch ToolType = "webFetch"
ToolTypeSummarizeTask ToolType = "summarizeTask"
)
// AskData represents the parsed structure of an ASK message
type AskData struct {
Question string `json:"question"`
Response string `json:"response"`
Options []string `json:"options,omitempty"`
}
// APIRequestInfo represents API request information
type APIRequestInfo struct {
Request string `json:"request,omitempty"`
TokensIn int `json:"tokensIn,omitempty"`
TokensOut int `json:"tokensOut,omitempty"`
CacheWrites int `json:"cacheWrites,omitempty"`
CacheReads int `json:"cacheReads,omitempty"`
Cost float64 `json:"cost,omitempty"`
CancelReason string `json:"cancelReason,omitempty"`
StreamingFailedMessage string `json:"streamingFailedMessage,omitempty"`
RetryStatus *APIRequestRetryStatus `json:"retryStatus,omitempty"`
}
// APIRequestRetryStatus represents retry status information
type APIRequestRetryStatus struct {
Attempt int `json:"attempt"`
MaxAttempts int `json:"maxAttempts"`
DelaySec int `json:"delaySec"`
ErrorSnippet string `json:"errorSnippet,omitempty"`
}
// GetTimestamp returns a formatted timestamp string
func (m *ClineMessage) GetTimestamp() string {
return time.Unix(m.Timestamp/1000, 0).Format("15:04:05")
}
// IsAsk returns true if this is an ASK message
func (m *ClineMessage) IsAsk() bool {
return m.Type == MessageTypeAsk
}
// IsSay returns true if this is a SAY message
func (m *ClineMessage) IsSay() bool {
return m.Type == MessageTypeSay
}
// GetMessageKey returns a unique key for this message based on timestamp
func (m *ClineMessage) GetMessageKey() string {
return strconv.FormatInt(m.Timestamp, 10)
}
// ExtractMessagesFromStateJSON parses the state JSON and extracts messages
func ExtractMessagesFromStateJSON(stateJson string) ([]*ClineMessage, error) {
// Parse the state JSON to extract clineMessages
var rawState map[string]interface{}
if err := json.Unmarshal([]byte(stateJson), &rawState); err != nil {
return nil, fmt.Errorf("failed to parse state JSON: %w", err)
}
// Try to extract clineMessages
clineMessagesRaw, exists := rawState["clineMessages"]
if !exists {
return []*ClineMessage{}, nil
}
// Convert to JSON and back to get proper Message structs
clineMessagesJson, err := json.Marshal(clineMessagesRaw)
if err != nil {
return nil, fmt.Errorf("failed to marshal clineMessages: %w", err)
}
var messages []*ClineMessage
if err := json.Unmarshal(clineMessagesJson, &messages); err != nil {
return nil, fmt.Errorf("failed to unmarshal clineMessages: %w", err)
}
return messages, nil
}
// ConvertProtoToMessage converts a protobuf ClineMessage to our local Message struct
func ConvertProtoToMessage(protoMsg *cline.ClineMessage) *ClineMessage {
var msgType MessageType
var say, ask string
// Convert message type
switch protoMsg.Type {
case cline.ClineMessageType_ASK:
msgType = MessageTypeAsk
ask = convertProtoAskType(protoMsg.Ask)
case cline.ClineMessageType_SAY:
msgType = MessageTypeSay
say = convertProtoSayType(protoMsg.Say)
default:
msgType = MessageTypeSay
say = "unknown"
}
return &ClineMessage{
Type: msgType,
Text: protoMsg.Text,
Timestamp: protoMsg.Ts,
Reasoning: protoMsg.Reasoning,
Say: say,
Ask: ask,
Partial: protoMsg.Partial,
}
}
// convertProtoAskType converts protobuf ask type to string
func convertProtoAskType(askType cline.ClineAsk) string {
switch askType {
case cline.ClineAsk_FOLLOWUP:
return string(AskTypeFollowup)
case cline.ClineAsk_PLAN_MODE_RESPOND:
return string(AskTypePlanModeRespond)
case cline.ClineAsk_COMMAND:
return string(AskTypeCommand)
case cline.ClineAsk_COMMAND_OUTPUT:
return string(AskTypeCommandOutput)
case cline.ClineAsk_COMPLETION_RESULT:
return string(AskTypeCompletionResult)
case cline.ClineAsk_TOOL:
return string(AskTypeTool)
case cline.ClineAsk_API_REQ_FAILED:
return string(AskTypeAPIReqFailed)
case cline.ClineAsk_RESUME_TASK:
return string(AskTypeResumeTask)
case cline.ClineAsk_RESUME_COMPLETED_TASK:
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:
return string(AskTypeUseMcpServer)
case cline.ClineAsk_NEW_TASK:
return string(AskTypeNewTask)
case cline.ClineAsk_CONDENSE:
return string(AskTypeCondense)
case cline.ClineAsk_REPORT_BUG:
return string(AskTypeReportBug)
default:
return "unknown"
}
}
// convertProtoSayType converts protobuf say type to string
func convertProtoSayType(sayType cline.ClineSay) string {
switch sayType {
case cline.ClineSay_TASK:
return string(SayTypeTask)
case cline.ClineSay_ERROR:
return string(SayTypeError)
case cline.ClineSay_API_REQ_STARTED:
return string(SayTypeAPIReqStarted)
case cline.ClineSay_API_REQ_FINISHED:
return string(SayTypeAPIReqFinished)
case cline.ClineSay_TEXT:
return string(SayTypeText)
case cline.ClineSay_REASONING:
return string(SayTypeReasoning)
case cline.ClineSay_COMPLETION_RESULT_SAY:
return string(SayTypeCompletionResult)
case cline.ClineSay_USER_FEEDBACK:
return string(SayTypeUserFeedback)
case cline.ClineSay_USER_FEEDBACK_DIFF:
return string(SayTypeUserFeedbackDiff)
case cline.ClineSay_API_REQ_RETRIED:
return string(SayTypeAPIReqRetried)
case cline.ClineSay_COMMAND_SAY:
return string(SayTypeCommand)
case cline.ClineSay_COMMAND_OUTPUT_SAY:
return string(SayTypeCommandOutput)
case cline.ClineSay_TOOL_SAY:
return string(SayTypeTool)
case cline.ClineSay_SHELL_INTEGRATION_WARNING:
return string(SayTypeShellIntegrationWarning)
case cline.ClineSay_BROWSER_ACTION_LAUNCH_SAY:
return string(SayTypeBrowserActionLaunch)
case cline.ClineSay_BROWSER_ACTION:
return string(SayTypeBrowserAction)
case cline.ClineSay_BROWSER_ACTION_RESULT:
return string(SayTypeBrowserActionResult)
case cline.ClineSay_MCP_SERVER_REQUEST_STARTED:
return string(SayTypeMcpServerRequestStarted)
case cline.ClineSay_MCP_SERVER_RESPONSE:
return string(SayTypeMcpServerResponse)
case cline.ClineSay_MCP_NOTIFICATION:
return string(SayTypeMcpNotification)
case cline.ClineSay_USE_MCP_SERVER_SAY:
return string(SayTypeUseMcpServer)
case cline.ClineSay_DIFF_ERROR:
return string(SayTypeDiffError)
case cline.ClineSay_DELETED_API_REQS:
return string(SayTypeDeletedAPIReqs)
case cline.ClineSay_CLINEIGNORE_ERROR:
return string(SayTypeClineignoreError)
case cline.ClineSay_CHECKPOINT_CREATED:
return string(SayTypeCheckpointCreated)
case cline.ClineSay_LOAD_MCP_DOCUMENTATION:
return string(SayTypeLoadMcpDocumentation)
case cline.ClineSay_INFO:
return string(SayTypeInfo)
case cline.ClineSay_TASK_PROGRESS:
return string(SayTypeTaskProgress)
default:
return "unknown"
}
}
+61
View File
@@ -0,0 +1,61 @@
package types
import (
"sync"
)
// ConversationState manages the state of the conversation
type ConversationState struct {
mu sync.RWMutex
StreamingMessage *StreamingMessage `json:"streamingMessage,omitempty"`
}
// StreamingMessage manages state for streaming message display
type StreamingMessage struct {
CurrentKey string `json:"currentKey"`
LastText string `json:"lastText"`
LastToolMessage string `json:"lastToolMessage,omitempty"`
}
// NewConversationState creates a new conversation state
func NewConversationState() *ConversationState {
return &ConversationState{
StreamingMessage: &StreamingMessage{},
}
}
// SetStreamingMessage updates the streaming message state
func (cs *ConversationState) SetStreamingMessage(key, text string) {
cs.mu.Lock()
defer cs.mu.Unlock()
cs.StreamingMessage.CurrentKey = key
cs.StreamingMessage.LastText = text
}
// GetStreamingMessage returns the current streaming message state
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,
}
}
// Clear resets state
func (cs *ConversationState) Clear() {
cs.mu.Lock()
defer cs.mu.Unlock()
cs.StreamingMessage = &StreamingMessage{}
}
// ExtensionState represents the server-side extension state structure
type ExtensionState struct {
CurrentTaskItem *CurrentTaskItem `json:"currentTaskItem,omitempty"`
}
// CurrentTaskItem - minimal struct with just what we need
type CurrentTaskItem struct {
Id string `json:"id"`
}
+47
View File
@@ -0,0 +1,47 @@
package cli
import (
"fmt"
"runtime"
"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.`,
RunE: func(cmd *cobra.Command, args []string) error {
if short {
fmt.Println(Version)
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)
return nil
},
}
cmd.Flags().BoolVar(&short, "short", false, "show only version number")
return cmd
}
+6
View File
@@ -0,0 +1,6 @@
package common
// WE WILL HAVE TO MIGRATE THIS FROM DATA TO v1 LATER
const SETTINGS_SUBFOLDER = "data"
const DEFAULT_CLINE_CORE_PORT = 50052
+54
View File
@@ -0,0 +1,54 @@
package common
// Database query constants for the SQLite locks database
const (
// SelectInstanceLocksSQL selects all instance locks ordered by creation time
SelectInstanceLocksSQL = `
SELECT id, held_by, lock_type, lock_target, locked_at
FROM locks
WHERE lock_type = 'instance'
ORDER BY locked_at ASC
`
SelectInstanceLockByHolderSQL = `
SELECT held_by, lock_target, locked_at
FROM locks
WHERE held_by = ? AND lock_type = 'instance'
`
SelectInstanceLockHoldersAscSQL = `
SELECT held_by, lock_target, locked_at
FROM locks
WHERE lock_type = 'instance'
ORDER BY locked_at ASC
`
// DeleteInstanceLockSQL deletes an instance lock by address
DeleteInstanceLockSQL = `
DELETE FROM locks
WHERE held_by = ? AND lock_type = 'instance'
`
InsertFileLockSQL = `
INSERT INTO locks (held_by, lock_type, lock_target, locked_at)
VALUES (?, 'file', ?, ?)
`
// DeleteFileLockSQL deletes a file lock by holder and target
DeleteFileLockSQL = `
DELETE FROM locks
WHERE held_by = ? AND lock_type = 'file' AND lock_target = ?
`
// CountInstanceLockSQL counts instance locks for a given address
CountInstanceLockSQL = `
SELECT COUNT(*) FROM locks
WHERE held_by = ? AND lock_type = 'instance'
`
// InsertInstanceLockSQL inserts or replaces an instance lock
InsertInstanceLockSQL = `
INSERT OR REPLACE INTO locks (held_by, lock_type, lock_target, locked_at)
VALUES (?, 'instance', ?, ?)
`
)
+54
View File
@@ -0,0 +1,54 @@
package common
import (
"time"
"google.golang.org/grpc/health/grpc_health_v1"
)
// CoreInstanceInfo represents a discovered Cline instance
// This is the canonical definition used across all CLI packages
type CoreInstanceInfo struct {
// Full core address including port
Address string `json:"address"`
// Host bridge service address that core holds (host is ALWAYS running on localhost FYI)
HostServiceAddress string `json:"host_port"`
Status grpc_health_v1.HealthCheckResponse_ServingStatus `json:"status"`
LastSeen time.Time `json:"last_seen"`
ProcessPID int `json:"process_pid,omitempty"`
Version string `json:"version,omitempty"`
}
func (c *CoreInstanceInfo) CorePort() int {
_, port, _ := ParseHostPort(c.Address)
return port
}
func (c *CoreInstanceInfo) HostPort() int {
_, port, _ := ParseHostPort(c.HostServiceAddress)
return port
}
func (c *CoreInstanceInfo) StatusString() string {
return c.Status.String()
}
// LockRow represents a row in the locks table
type LockRow struct {
ID int64 `json:"id"`
HeldBy string `json:"held_by"`
LockType string `json:"lock_type"`
LockTarget string `json:"lock_target"`
LockedAt int64 `json:"locked_at"`
}
// InstancesOutput represents the JSON output format for instance listing
type InstancesOutput struct {
DefaultInstance string `json:"default_instance"`
CoreInstances []CoreInstanceInfo `json:"instances"`
}
type DefaultCoreInstance struct {
Address string `json:"default_instance"`
LastUpdated string `json:"last_updated"`
}
+159
View File
@@ -0,0 +1,159 @@
package common
import (
"context"
"fmt"
"net"
"strconv"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/health/grpc_health_v1"
)
// ParseHostPort parses a host:port address and returns the host and port separately
func ParseHostPort(address string) (string, int, error) {
host, portStr, err := net.SplitHostPort(address)
if err != nil {
return "", 0, err
}
port, err := strconv.Atoi(portStr)
if err != nil {
return "", 0, err
}
return host, port, nil
}
// IsLocalAddress checks if the given host is a local/loopback address
// Supports both IPv4 (localhost, 127.0.0.1) and IPv6 (::1) addresses
func IsLocalAddress(host string) bool {
// Handle common localhost names
if host == "localhost" {
return true
}
// Parse as IP and check if it's a loopback
if ip := net.ParseIP(host); ip != nil {
return ip.IsLoopback()
}
return false
}
// PerformHealthCheck performs a gRPC health check on the given address
// Will return UNKNOWN if the service is unreachable (error)
func PerformHealthCheck(ctx context.Context, address string) (grpc_health_v1.HealthCheckResponse_ServingStatus, error) {
conn, err := grpc.DialContext(ctx, address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return grpc_health_v1.HealthCheckResponse_UNKNOWN, err
}
defer conn.Close()
healthClient := grpc_health_v1.NewHealthClient(conn)
resp, err := healthClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{})
if err != nil {
return grpc_health_v1.HealthCheckResponse_UNKNOWN, err
}
return resp.Status, nil
}
// It's healthy if we can reach it and it responds with SERVING
func IsInstanceHealthy(ctx context.Context, address string) bool {
status, err := PerformHealthCheck(ctx, address)
return err == nil && status == grpc_health_v1.HealthCheckResponse_SERVING
}
// It's (likely) our instance if we can reach it and it responds to health checks
func IsInstanceOurs(ctx context.Context, address string) bool {
_, err := PerformHealthCheck(ctx, address)
return err != nil
}
// (unreachable or not serving)
func IsInstanceStale(ctx context.Context, address string) (grpc_health_v1.HealthCheckResponse_ServingStatus, bool, error) {
status, err := PerformHealthCheck(ctx, address)
isStale := err != nil || status != grpc_health_v1.HealthCheckResponse_SERVING
return status, isStale, err
}
// IsPortAvailable checks if a port is available for binding
func IsPortAvailable(port int) bool {
address := fmt.Sprintf("localhost:%d", port)
listener, err := net.Listen("tcp", address)
if err != nil {
return false
}
listener.Close()
return true
}
// FindAvailablePortPair finds two available ports by letting the OS allocate them
func FindAvailablePortPair() (corePort, hostPort int, err error) {
coreListener, err := net.Listen("tcp", ":0")
if err != nil {
return 0, 0, err
}
defer coreListener.Close()
hostListener, err := net.Listen("tcp", ":0")
if err != nil {
return 0, 0, err
}
defer hostListener.Close()
corePort = coreListener.Addr().(*net.TCPAddr).Port
hostPort = hostListener.Addr().(*net.TCPAddr).Port
return corePort, hostPort, nil
}
// NormalizeAddressForGRPC converts address to host:port for grpc client with proper normalization
func NormalizeAddressForGRPC(address string) (string, error) {
host, port, err := ParseHostPort(address)
if err != nil {
return "", err
}
// Normalize local addresses to localhost for gRPC compatibility
if IsLocalAddress(host) {
return fmt.Sprintf("localhost:%d", port), nil
}
return address, nil
}
// RetryOperation performs an operation with retry logic
func RetryOperation(maxRetries int, timeoutPerAttempt time.Duration, operation func() error) error {
var lastErr error
for attempt := 1; attempt <= maxRetries; attempt++ {
ctx, cancel := context.WithTimeout(context.Background(), timeoutPerAttempt)
// Create a channel to capture the operation result
done := make(chan error, 1)
go func() {
done <- operation()
}()
select {
case err := <-done:
cancel()
if err == nil {
return nil // Success
}
lastErr = err
case <-ctx.Done():
cancel()
lastErr = ctx.Err()
}
// Add delay between attempts (except for the last one)
if attempt < maxRetries {
time.Sleep(1 * time.Second)
}
}
return fmt.Errorf("operation failed after %d attempts: %w", maxRetries, lastErr)
}
+351
View File
@@ -0,0 +1,351 @@
package hostbridge
import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
proto "github.com/cline/grpc-go/host"
)
// diffSession represents an in-memory diff editing session
type diffSession struct {
originalPath string // File path from OpenDiff request
originalContent []byte // Original file content (for comparison)
currentContent []byte // Current modified content
lines []string // Current content split into lines
encoding string // File encoding (default: utf8)
}
// DiffService implements the proto.DiffServiceServer interface
type DiffService struct {
proto.UnimplementedDiffServiceServer
verbose bool
sessions *sync.Map // thread-safe: diffId -> *diffSession
counter *int64 // atomic counter for unique IDs
}
// NewDiffService creates a new DiffService
func NewDiffService(verbose bool) *DiffService {
counter := int64(0)
return &DiffService{
verbose: verbose,
sessions: &sync.Map{},
counter: &counter,
}
}
// generateDiffID creates a unique diff ID
func (s *DiffService) generateDiffID() string {
id := atomic.AddInt64(s.counter, 1)
return fmt.Sprintf("diff_%d_%d", os.Getpid(), id)
}
// splitLines splits content into lines, preserving line ending information
func splitLines(content string) []string {
if content == "" {
return []string{}
}
lines := []string{}
current := ""
for _, char := range content {
if char == '\n' {
lines = append(lines, current)
current = ""
} else if char != '\r' { // Skip \r characters, handle \r\n as \n
current += string(char)
}
}
// Add the last line if it doesn't end with newline
if current != "" {
lines = append(lines, current)
}
return lines
}
// joinLines joins lines back into content with newlines
func joinLines(lines []string) string {
if len(lines) == 0 {
return ""
}
return strings.Join(lines, "\n")
}
// OpenDiff opens a diff view for the specified file
func (s *DiffService) OpenDiff(ctx context.Context, req *proto.OpenDiffRequest) (*proto.OpenDiffResponse, error) {
if s.verbose {
log.Printf("OpenDiff called for path: %s", req.GetPath())
}
diffID := s.generateDiffID()
var originalContent []byte
// Check if file exists and read original content
if req.GetPath() != "" {
if _, err := os.Stat(req.GetPath()); err == nil {
// File exists, read its content
var readErr error
originalContent, readErr = ioutil.ReadFile(req.GetPath())
if readErr != nil {
return nil, fmt.Errorf("failed to read original file: %w", readErr)
}
} else {
// File doesn't exist, use empty content
originalContent = []byte{}
}
}
// Use provided content as the initial current content
currentContent := []byte(req.GetContent())
// Create the diff session
session := &diffSession{
originalPath: req.GetPath(),
originalContent: originalContent,
currentContent: currentContent,
lines: splitLines(req.GetContent()),
encoding: "utf8", // Default encoding
}
// Store the session
s.sessions.Store(diffID, session)
if s.verbose {
log.Printf("Created diff session: %s (original: %d bytes, current: %d bytes)",
diffID, len(originalContent), len(currentContent))
}
return &proto.OpenDiffResponse{
DiffId: &diffID,
}, nil
}
// GetDocumentText returns the current content of the diff document
func (s *DiffService) GetDocumentText(ctx context.Context, req *proto.GetDocumentTextRequest) (*proto.GetDocumentTextResponse, error) {
if s.verbose {
log.Printf("GetDocumentText called for diff ID: %s", req.GetDiffId())
}
sessionInterface, exists := s.sessions.Load(req.GetDiffId())
if !exists {
return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
}
session := sessionInterface.(*diffSession)
content := string(session.currentContent)
return &proto.GetDocumentTextResponse{
Content: &content,
}, nil
}
// ReplaceText replaces text in the diff document using line-based operations
func (s *DiffService) ReplaceText(ctx context.Context, req *proto.ReplaceTextRequest) (*proto.ReplaceTextResponse, error) {
if s.verbose {
log.Printf("ReplaceText called for diff ID: %s, lines %d-%d",
req.GetDiffId(), req.GetStartLine(), req.GetEndLine())
}
sessionInterface, exists := s.sessions.Load(req.GetDiffId())
if !exists {
return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
}
session := sessionInterface.(*diffSession)
startLine := int(req.GetStartLine())
endLine := int(req.GetEndLine())
newContent := req.GetContent()
// Validate line ranges
if startLine < 0 {
startLine = 0
}
if endLine < startLine {
endLine = startLine
}
// Split new content into lines
newLines := splitLines(newContent)
// Ensure we have enough lines in the current content
for len(session.lines) < endLine {
session.lines = append(session.lines, "")
}
// Replace the specified line range
if endLine > len(session.lines) {
// Extending beyond current content - append new lines
session.lines = append(session.lines[:startLine], newLines...)
} else {
// Replace within existing content
result := make([]string, 0, len(session.lines)-endLine+startLine+len(newLines))
result = append(result, session.lines[:startLine]...)
result = append(result, newLines...)
result = append(result, session.lines[endLine:]...)
session.lines = result
}
// Update current content
session.currentContent = []byte(joinLines(session.lines))
// Store the updated session
s.sessions.Store(req.GetDiffId(), session)
if s.verbose {
log.Printf("Updated diff session %s: %d lines, %d bytes",
req.GetDiffId(), len(session.lines), len(session.currentContent))
}
return &proto.ReplaceTextResponse{}, nil
}
// ScrollDiff scrolls the diff view to a specific line (no-op for CLI)
func (s *DiffService) ScrollDiff(ctx context.Context, req *proto.ScrollDiffRequest) (*proto.ScrollDiffResponse, error) {
if s.verbose {
log.Printf("ScrollDiff called for diff ID: %s, line: %d", req.GetDiffId(), req.GetLine())
}
// Verify session exists
if _, exists := s.sessions.Load(req.GetDiffId()); !exists {
return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
}
// In a CLI implementation, scrolling is a no-op
// In a GUI implementation, this would scroll the view to the specified line
return &proto.ScrollDiffResponse{}, nil
}
// TruncateDocument truncates the diff document at the specified line
func (s *DiffService) TruncateDocument(ctx context.Context, req *proto.TruncateDocumentRequest) (*proto.TruncateDocumentResponse, error) {
if s.verbose {
log.Printf("TruncateDocument called for diff ID: %s, end line: %d", req.GetDiffId(), req.GetEndLine())
}
sessionInterface, exists := s.sessions.Load(req.GetDiffId())
if !exists {
return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
}
session := sessionInterface.(*diffSession)
endLine := int(req.GetEndLine())
// Truncate lines at the specified position
if endLine >= 0 && endLine < len(session.lines) {
session.lines = session.lines[:endLine]
session.currentContent = []byte(joinLines(session.lines))
// Store the updated session
s.sessions.Store(req.GetDiffId(), session)
if s.verbose {
log.Printf("Truncated diff session %s to %d lines", req.GetDiffId(), len(session.lines))
}
}
return &proto.TruncateDocumentResponse{}, nil
}
// SaveDocument saves the diff document to the original file
func (s *DiffService) SaveDocument(ctx context.Context, req *proto.SaveDocumentRequest) (*proto.SaveDocumentResponse, error) {
if s.verbose {
log.Printf("SaveDocument called for diff ID: %s", req.GetDiffId())
}
sessionInterface, exists := s.sessions.Load(req.GetDiffId())
if !exists {
return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
}
session := sessionInterface.(*diffSession)
if session.originalPath == "" {
return nil, fmt.Errorf("no file path specified for diff session: %s", req.GetDiffId())
}
// Create parent directories if they don't exist
dir := filepath.Dir(session.originalPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("failed to create directories: %w", err)
}
// Write the current content to the original file
if err := ioutil.WriteFile(session.originalPath, session.currentContent, 0644); err != nil {
return nil, fmt.Errorf("failed to save file: %w", err)
}
if s.verbose {
log.Printf("Saved diff session %s to file: %s (%d bytes)",
req.GetDiffId(), session.originalPath, len(session.currentContent))
}
return &proto.SaveDocumentResponse{}, nil
}
// CloseAllDiffs closes all diff views and cleans up all sessions
func (s *DiffService) CloseAllDiffs(ctx context.Context, req *proto.CloseAllDiffsRequest) (*proto.CloseAllDiffsResponse, error) {
if s.verbose {
log.Printf("CloseAllDiffs called")
}
var count int64
s.sessions.Range(func(key, value any) bool {
// Optional: attempt to close if the value supports it
if c, ok := value.(interface{ Close() error }); ok {
_ = c.Close() // best-effort; ignore error
}
s.sessions.Delete(key)
atomic.AddInt64(&count, 1)
return true
})
if s.verbose {
log.Printf("Closed %d diff sessions", count)
}
return &proto.CloseAllDiffsResponse{}, nil
}
// OpenMultiFileDiff displays a diff view comparing before/after states for multiple files
func (s *DiffService) OpenMultiFileDiff(ctx context.Context, req *proto.OpenMultiFileDiffRequest) (*proto.OpenMultiFileDiffResponse, error) {
if s.verbose {
log.Printf("OpenMultiFileDiff called with title: %s, %d files", req.GetTitle(), len(req.GetDiffs()))
}
// In a CLI implementation, we could display the diffs to console
// For now, we'll just log the information
title := req.GetTitle()
if title == "" {
title = "Multi-file diff"
}
if s.verbose {
log.Printf("=== %s ===", title)
for i, diff := range req.GetDiffs() {
log.Printf("File %d: %s", i+1, diff.GetFilePath())
log.Printf(" Left content: %d bytes", len(diff.GetLeftContent()))
log.Printf(" Right content: %d bytes", len(diff.GetRightContent()))
}
}
// In a more sophisticated CLI implementation, we could:
// 1. Use a diff library to generate unified diffs
// 2. Display them with colors
// 3. Allow navigation between files
// For now, this is a no-op that just acknowledges the request
return &proto.OpenMultiFileDiffResponse{}, nil
}
+39
View File
@@ -0,0 +1,39 @@
package hostbridge
import (
"log"
"github.com/cline/grpc-go/host"
)
// WatchService implements the host.WatchServiceServer interface
type WatchService struct {
host.UnimplementedWatchServiceServer
coreAddress string
verbose bool
}
// NewWatchService creates a new WatchService
func NewWatchService(coreAddress string, verbose bool) *WatchService {
return &WatchService{
coreAddress: coreAddress,
verbose: verbose,
}
}
// SubscribeToFile subscribes to file change notifications
func (s *WatchService) SubscribeToFile(req *host.SubscribeToFileRequest, stream host.WatchService_SubscribeToFileServer) error {
if s.verbose {
log.Printf("SubscribeToFile called for path: %s", req.GetPath())
}
// For console implementation, we'll just log that we would watch the file
// In a real implementation, we'd use fsnotify or similar to watch file changes
log.Printf("[Cline] Would watch file: %s", req.GetPath())
// Keep the stream open but don't send any events for now
// In a real implementation, we'd send FileChangeEvent messages when files change
<-stream.Context().Done()
return nil
}
+63
View File
@@ -0,0 +1,63 @@
package hostbridge
import (
"context"
"fmt"
"log"
proto "github.com/cline/grpc-go/host"
)
// WindowService implements the proto.WindowServiceServer interface
type WindowService struct {
proto.UnimplementedWindowServiceServer
coreAddress string
verbose bool
}
// NewWindowService creates a new WindowService
func NewWindowService(coreAddress string, verbose bool) *WindowService {
return &WindowService{
coreAddress: coreAddress,
verbose: verbose,
}
}
// ShowTextDocument opens a text document for viewing/editing
func (s *WindowService) ShowTextDocument(ctx context.Context, req *proto.ShowTextDocumentRequest) (*proto.TextEditorInfo, error) {
if s.verbose {
log.Printf("ShowTextDocument called for path: %s", req.GetPath())
}
// For console implementation, we'll just log that we would open the document
fmt.Printf("[Cline] Would open document: %s\n", req.GetPath())
return &proto.TextEditorInfo{
DocumentPath: req.GetPath(),
IsActive: true,
}, nil
}
// ShowOpenDialogue shows a file open dialog
func (s *WindowService) ShowOpenDialogue(ctx context.Context, req *proto.ShowOpenDialogueRequest) (*proto.SelectedResources, error) {
if s.verbose {
log.Printf("ShowOpenDialogue called")
}
// For console implementation, return empty list (user cancelled)
return &proto.SelectedResources{
Paths: []string{},
}, nil
}
// ShowMessage displays a message to the user
func (s *WindowService) ShowMessage(ctx context.Context, req *proto.ShowMessageRequest) (*proto.SelectedResponse, error) {
if s.verbose {
log.Printf("ShowMessage called: %s", req.GetMessage())
}
// Display message to console
fmt.Printf("[Cline] %s\n", req.GetMessage())
return &proto.SelectedResponse{}, nil
}
+66
View File
@@ -0,0 +1,66 @@
package hostbridge
import (
"context"
"log"
"os"
"github.com/cline/grpc-go/host"
)
// WorkspaceService implements the host.WorkspaceServiceServer interface
type WorkspaceService struct {
host.UnimplementedWorkspaceServiceServer
coreAddress string
verbose bool
}
// NewWorkspaceService creates a new WorkspaceService
func NewWorkspaceService(coreAddress string, verbose bool) *WorkspaceService {
return &WorkspaceService{
coreAddress: coreAddress,
verbose: verbose,
}
}
// GetWorkspacePaths returns the workspace directory paths
func (s *WorkspaceService) GetWorkspacePaths(ctx context.Context, req *host.GetWorkspacePathsRequest) (*host.GetWorkspacePathsResponse, error) {
if s.verbose {
log.Printf("GetWorkspacePaths called")
}
// Get current working directory as the workspace
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
return &host.GetWorkspacePathsResponse{
Paths: []string{cwd},
}, nil
}
// SaveOpenDocumentIfDirty saves an open document if it has unsaved changes
func (s *WorkspaceService) SaveOpenDocumentIfDirty(ctx context.Context, req *host.SaveOpenDocumentIfDirtyRequest) (*host.SaveOpenDocumentIfDirtyResponse, error) {
if s.verbose {
log.Printf("SaveOpenDocumentIfDirty called for path: %s", req.GetPath())
}
// For console implementation, we'll assume the document is already saved
// In a real implementation, we'd check if the file has unsaved changes
return &host.SaveOpenDocumentIfDirtyResponse{
WasSaved: false, // Assume no changes to save
}, nil
}
// GetDiagnostics returns diagnostic information for a file
func (s *WorkspaceService) GetDiagnostics(ctx context.Context, req *host.GetDiagnosticsRequest) (*host.GetDiagnosticsResponse, error) {
if s.verbose {
log.Printf("GetDiagnostics called for path: %s", req.GetPath())
}
// For console implementation, return empty diagnostics
return &host.GetDiagnosticsResponse{
Diagnostics: []*host.Diagnostic{},
}, nil
}
+98
View File
@@ -0,0 +1,98 @@
package hostbridge
import (
"context"
"log"
"github.com/cline/grpc-go/cline"
"github.com/cline/grpc-go/host"
)
// Global shutdown channel - simple approach
var globalShutdownCh chan struct{}
func init() {
globalShutdownCh = make(chan struct{})
}
// EnvService implements the host.EnvServiceServer interface
type EnvService struct {
host.UnimplementedEnvServiceServer
verbose bool
}
// NewEnvService creates a new EnvService
func NewEnvService(verbose bool) *EnvService {
return &EnvService{
verbose: verbose,
}
}
// ClipboardWriteText writes text to the system clipboard
func (s *EnvService) ClipboardWriteText(ctx context.Context, req *cline.StringRequest) (*cline.Empty, error) {
if s.verbose {
log.Printf("ClipboardWriteText called with: %s", req.GetValue())
}
// TODO: Implement actual clipboard functionality
// For now, just return success
return &cline.Empty{}, nil
}
// ClipboardReadText reads text from the system clipboard
func (s *EnvService) ClipboardReadText(ctx context.Context, req *cline.EmptyRequest) (*cline.String, error) {
if s.verbose {
log.Printf("ClipboardReadText called")
}
// TODO: Implement actual clipboard functionality
// For now, return empty string
return &cline.String{
Value: "",
}, nil
}
// GetMachineId returns a stable machine identifier for telemetry distinctId purposes
func (s *EnvService) GetMachineId(ctx context.Context, req *cline.EmptyRequest) (*cline.String, error) {
if s.verbose {
log.Printf("GetMachineId called")
}
// TODO: Implement actual machine ID functionality
// For now, return empty string
return &cline.String{
Value: "",
}, nil
}
// GetHostVersion returns the host platform name and version
func (s *EnvService) GetHostVersion(ctx context.Context, req *cline.EmptyRequest) (*host.GetHostVersionResponse, error) {
if s.verbose {
log.Printf("GetHostVersion called")
}
// TODO: Implement actual host version functionality
// For now, return empty response
return &host.GetHostVersionResponse{}, nil
}
// Shutdown initiates a graceful shutdown of the host bridge service
func (s *EnvService) Shutdown(ctx context.Context, req *cline.EmptyRequest) (*cline.Empty, error) {
if s.verbose {
log.Printf("Shutdown requested via RPC")
}
// Trigger global shutdown signal
select {
case globalShutdownCh <- struct{}{}:
if s.verbose {
log.Printf("Shutdown signal sent successfully")
}
default:
if s.verbose {
log.Printf("Shutdown signal already pending")
}
}
return &cline.Empty{}, nil
}
+113
View File
@@ -0,0 +1,113 @@
package hostbridge
import (
"context"
"fmt"
"log"
"net"
"github.com/cline/grpc-go/host"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
)
// GrpcServer provides gRPC hostbridge functionality
type GrpcServer struct {
port int
verbose bool
server *grpc.Server
shutdownCh chan struct{}
}
// NewGrpcServer creates a new GrpcServer
func NewGrpcServer(port int, verbose bool) *GrpcServer {
return &GrpcServer{
port: port,
verbose: verbose,
shutdownCh: make(chan struct{}),
}
}
// Start starts the gRPC hostbridge server
func (s *GrpcServer) Start(ctx context.Context) error {
if s.verbose {
log.Printf("Starting gRPC hostbridge server on port %d", s.port)
}
// Create listener
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.port))
if err != nil {
return fmt.Errorf("failed to listen on port %d: %w", s.port, err)
}
// Create gRPC server
s.server = grpc.NewServer()
// Register health service
healthServer := health.NewServer()
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
grpc_health_v1.RegisterHealthServer(s.server, healthServer)
// Register services
workspaceService := NewSimpleWorkspaceService(s.verbose)
host.RegisterWorkspaceServiceServer(s.server, workspaceService)
windowService := NewWindowService(s.verbose)
host.RegisterWindowServiceServer(s.server, windowService)
diffService := NewDiffService(s.verbose)
host.RegisterDiffServiceServer(s.server, diffService)
envService := NewEnvService(s.verbose)
host.RegisterEnvServiceServer(s.server, envService)
if s.verbose {
log.Printf("Registered HealthService")
log.Printf("Registered WorkspaceService")
log.Printf("Registered WindowService")
log.Printf("Registered DiffService")
log.Printf("Registered EnvService")
}
// Start server in goroutine
go func() {
if s.verbose {
log.Printf("gRPC server listening on :%d", s.port)
}
if err := s.server.Serve(lis); err != nil {
log.Printf("gRPC server error: %v", err)
}
}()
// Wait for context cancellation or global shutdown signal
select {
case <-ctx.Done():
if s.verbose {
log.Println("Context cancelled, shutting down gRPC hostbridge server...")
}
case <-globalShutdownCh:
if s.verbose {
log.Println("Shutdown requested via RPC, shutting down gRPC hostbridge server...")
}
}
// Graceful shutdown
s.server.GracefulStop()
if s.verbose {
log.Println("gRPC hostbridge server stopped")
}
return nil
}
// TriggerShutdown triggers a graceful shutdown of the server
func (s *GrpcServer) TriggerShutdown() {
select {
case s.shutdownCh <- struct{}{}:
// Shutdown signal sent
default:
// Channel already has a signal or is closed
}
}
+43
View File
@@ -0,0 +1,43 @@
package hostbridge
import (
"context"
"fmt"
"log"
)
// Simple implementations that don't rely on proto files for now
// This allows us to test the basic hostbridge structure
// SimpleService provides basic hostbridge functionality
type SimpleService struct {
coreAddress string
verbose bool
}
// NewSimpleService creates a new SimpleService
func NewSimpleService(coreAddress string, verbose bool) *SimpleService {
return &SimpleService{
coreAddress: coreAddress,
verbose: verbose,
}
}
// Start starts the simple hostbridge service
func (s *SimpleService) Start(ctx context.Context) error {
if s.verbose {
log.Printf("Starting simple hostbridge service (connecting to core at %s)", s.coreAddress)
}
// For now, just log that we're running
fmt.Printf("[Cline Host Bridge] Service started on core address: %s\n", s.coreAddress)
// Keep running until context is cancelled
<-ctx.Done()
if s.verbose {
log.Println("Simple hostbridge service stopped")
}
return nil
}
+65
View File
@@ -0,0 +1,65 @@
package hostbridge
import (
"context"
"log"
"os"
"github.com/cline/grpc-go/cline"
"github.com/cline/grpc-go/host"
)
// SimpleWorkspaceService implements a basic workspace service without complex dependencies
type SimpleWorkspaceService struct {
host.UnimplementedWorkspaceServiceServer
verbose bool
}
// NewSimpleWorkspaceService creates a new SimpleWorkspaceService
func NewSimpleWorkspaceService(verbose bool) *SimpleWorkspaceService {
return &SimpleWorkspaceService{
verbose: verbose,
}
}
// GetWorkspacePaths returns the workspace directory paths
func (s *SimpleWorkspaceService) GetWorkspacePaths(ctx context.Context, req *host.GetWorkspacePathsRequest) (*host.GetWorkspacePathsResponse, error) {
if s.verbose {
log.Printf("GetWorkspacePaths called")
}
// Get current working directory as the workspace
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
return &host.GetWorkspacePathsResponse{
Paths: []string{cwd},
}, nil
}
// SaveOpenDocumentIfDirty saves an open document if it has unsaved changes
func (s *SimpleWorkspaceService) SaveOpenDocumentIfDirty(ctx context.Context, req *host.SaveOpenDocumentIfDirtyRequest) (*host.SaveOpenDocumentIfDirtyResponse, error) {
if s.verbose {
log.Printf("SaveOpenDocumentIfDirty called for path: %s", req.GetFilePath())
}
// For console implementation, we'll assume the document is already saved
wasSaved := false
return &host.SaveOpenDocumentIfDirtyResponse{
WasSaved: &wasSaved,
}, nil
}
// GetDiagnostics returns diagnostic information for a file - simplified version
func (s *SimpleWorkspaceService) GetDiagnostics(ctx context.Context, req *host.GetDiagnosticsRequest) (*host.GetDiagnosticsResponse, error) {
if s.verbose {
log.Printf("GetDiagnostics called")
}
// For console implementation, return empty diagnostics
return &host.GetDiagnosticsResponse{
FileDiagnostics: []*cline.FileDiagnostics{},
}, nil
}
+129
View File
@@ -0,0 +1,129 @@
package hostbridge
import (
"context"
"fmt"
"log"
proto "github.com/cline/grpc-go/host"
)
// WindowService implements the proto.WindowServiceServer interface
type WindowService struct {
proto.UnimplementedWindowServiceServer
verbose bool
}
// NewWindowService creates a new WindowService
func NewWindowService(verbose bool) *WindowService {
return &WindowService{
verbose: verbose,
}
}
// ShowTextDocument opens a text document for viewing/editing
func (s *WindowService) ShowTextDocument(ctx context.Context, req *proto.ShowTextDocumentRequest) (*proto.TextEditorInfo, error) {
if s.verbose {
log.Printf("ShowTextDocument called for path: %s", req.GetPath())
}
// For console implementation, we'll just log that we would open the document
fmt.Printf("[Cline] Would open document: %s\n", req.GetPath())
return &proto.TextEditorInfo{
DocumentPath: req.GetPath(),
IsActive: true,
}, nil
}
// ShowOpenDialogue shows a file open dialog
func (s *WindowService) ShowOpenDialogue(ctx context.Context, req *proto.ShowOpenDialogueRequest) (*proto.SelectedResources, error) {
if s.verbose {
log.Printf("ShowOpenDialogue called")
}
// For console implementation, return empty list (user cancelled)
return &proto.SelectedResources{
Paths: []string{},
}, nil
}
// ShowMessage displays a message to the user
func (s *WindowService) ShowMessage(ctx context.Context, req *proto.ShowMessageRequest) (*proto.SelectedResponse, error) {
if s.verbose {
log.Printf("ShowMessage called: %s", req.GetMessage())
}
// Display message to console
fmt.Printf("[Cline] %s\n", req.GetMessage())
return &proto.SelectedResponse{}, nil
}
// ShowInputBox shows an input dialog to the user
func (s *WindowService) ShowInputBox(ctx context.Context, req *proto.ShowInputBoxRequest) (*proto.ShowInputBoxResponse, error) {
if s.verbose {
log.Printf("ShowInputBox called: %s", req.GetTitle())
}
// For console implementation, return empty response (user cancelled)
return &proto.ShowInputBoxResponse{}, nil
}
// ShowSaveDialog shows a save file dialog
func (s *WindowService) ShowSaveDialog(ctx context.Context, req *proto.ShowSaveDialogRequest) (*proto.ShowSaveDialogResponse, error) {
if s.verbose {
log.Printf("ShowSaveDialog called")
}
// For console implementation, return empty response (user cancelled)
return &proto.ShowSaveDialogResponse{}, nil
}
// OpenFile opens a file in the editor
func (s *WindowService) OpenFile(ctx context.Context, req *proto.OpenFileRequest) (*proto.OpenFileResponse, error) {
if s.verbose {
log.Printf("OpenFile called for path: %s", req.GetFilePath())
}
// For console implementation, just log that we would open the file
fmt.Printf("[Cline] Would open file: %s\n", req.GetFilePath())
return &proto.OpenFileResponse{}, nil
}
// GetOpenTabs returns a list of currently open tabs
func (s *WindowService) GetOpenTabs(ctx context.Context, req *proto.GetOpenTabsRequest) (*proto.GetOpenTabsResponse, error) {
if s.verbose {
log.Printf("GetOpenTabs called")
}
// For console implementation, return empty list
return &proto.GetOpenTabsResponse{
Paths: []string{},
}, nil
}
// GetVisibleTabs returns a list of currently visible tabs
func (s *WindowService) GetVisibleTabs(ctx context.Context, req *proto.GetVisibleTabsRequest) (*proto.GetVisibleTabsResponse, error) {
if s.verbose {
log.Printf("GetVisibleTabs called")
}
// For console implementation, return empty list
return &proto.GetVisibleTabsResponse{
Paths: []string{},
}, nil
}
// GetActiveEditor returns information about the current active editor
func (s *WindowService) GetActiveEditor(ctx context.Context, req *proto.GetActiveEditorRequest) (*proto.GetActiveEditorResponse, error) {
if s.verbose {
log.Printf("GetActiveEditor called")
}
// Return empty response (no active file)
return &proto.GetActiveEditorResponse{
FilePath: nil,
}, nil
}
+40 -25
View File
@@ -25,15 +25,15 @@
},
"styling": {
"eyebrows": "breadcrumbs",
"codeblocks": "system"
"codeblocks": "system",
"css": "styles.css"
},
"appearance": {
"default": "system",
"strict": false
},
"fonts": {
"family": "Roboto",
"weight": 400
"family": "Roboto"
},
"navbar": {
"links": [
@@ -58,9 +58,8 @@
"group": "Getting Started",
"pages": [
"getting-started/what-is-cline",
"getting-started/model-selection-guide",
"getting-started/installing-cline",
"getting-started/installing-cline-jetbrains",
"getting-started/model-selection-guide",
"getting-started/task-management",
"getting-started/understanding-context-management",
{
@@ -82,15 +81,6 @@
{
"group": "Features",
"pages": [
"features/auto-approve",
"features/checkpoints",
"features/cline-rules",
"features/drag-and-drop",
"features/plan-and-act",
"features/slash-commands/workflows",
"features/focus-chain",
"features/auto-compact",
"features/editing-messages",
{
"group": "@ Mentions",
"pages": [
@@ -102,6 +92,32 @@
"features/at-mentions/url-mentions"
]
},
"features/auto-approve",
"features/auto-compact",
"features/checkpoints",
"features/cline-rules",
{
"group": "Commands & Shortcuts",
"pages": [
"features/commands-and-shortcuts/overview",
"features/commands-and-shortcuts/code-commands",
"features/commands-and-shortcuts/terminal-integration",
"features/commands-and-shortcuts/git-integration",
"features/commands-and-shortcuts/keyboard-shortcuts"
]
},
{
"group": "Customization",
"pages": [
"features/customization/opening-cline-in-sidebar",
"features/customization/disable-terminal-pagers"
]
},
"features/dictation",
"features/drag-and-drop",
"features/editing-messages",
"features/focus-chain",
"features/plan-and-act",
{
"group": "Slash Commands",
"pages": [
@@ -112,16 +128,8 @@
"features/slash-commands/deep-planning"
]
},
{
"group": "Commands & Shortcuts",
"pages": [
"features/commands-and-shortcuts/overview",
"features/commands-and-shortcuts/code-commands",
"features/commands-and-shortcuts/terminal-integration",
"features/commands-and-shortcuts/git-integration",
"features/commands-and-shortcuts/keyboard-shortcuts"
]
}
"features/slash-commands/workflows",
"features/yolo-mode"
]
},
{
@@ -183,7 +191,8 @@
"provider-config/openrouter",
"provider-config/sap-aicore",
"provider-config/vercel-ai-gateway",
"provider-config/requesty"
"provider-config/requesty",
"provider-config/baseten"
]
},
{
@@ -223,6 +232,12 @@
"url": "getting-started/what-is-cline"
}
],
"redirects": [
{
"source": "/getting-started/installing-cline-jetbrains",
"destination": "/getting-started/installing-cline"
}
],
"search": {
"prompt": "Search Cline documentation..."
},
@@ -105,11 +105,11 @@ Below is an example `.clinerules` file focused specifically on using `new_task`
````markdown
# You MUST use the `new_task` tool: Task Handoff Strategy Guide
**⚠️ CRITICAL INSTRUCTIONS - YOU MUST FOLLOW THESE GUIDELINES ⚠️**
**CRITICAL INSTRUCTIONS - YOU MUST FOLLOW THESE GUIDELINES**
This guide provides **MANDATORY** instructions for effectively breaking down complex tasks and implementing a smooth handoff process between tasks. You **MUST** follow these guidelines to ensure continuity, context preservation, and efficient task completion.
## ⚠️ CONTEXT WINDOW MONITORING - MANDATORY ACTION REQUIRED ⚠️
## CONTEXT WINDOW MONITORING - MANDATORY ACTION REQUIRED
You **MUST** monitor the context window usage displayed in the environment details. When usage exceeds 50% of the available context window, you **MUST** initiate a task handoff using the `new_task` tool.
@@ -370,7 +370,7 @@ You **MUST** initiate task handoffs in these scenarios:
5. **When switching focus areas** within a larger project
6. **When different expertise** might be beneficial for different parts of the task
**⚠️ FINAL REMINDER - CRITICAL INSTRUCTION ⚠️**
**FINAL REMINDER - CRITICAL INSTRUCTION**
You **MUST** monitor the context window usage in the environment details section. When it exceeds 50% (e.g., "105,000 / 200,000 tokens (53%)"), you **MUST** proactively initiate the task handoff process using the `ask_followup_question` tool followed by the `new_task` tool. You MUST use the `new_task` tool.
@@ -21,7 +21,6 @@ While Cline has only a few default keyboard shortcuts, you can assign your own s
| Command ID | Description |
| ---------------------------------------------------------------------------------------- | --------------------------------------------- |
| [`cline.openInNewTab`](/features/commands-and-shortcuts/overview) | Opens Cline in a new editor tab |
| [`cline.addToChat`](/features/commands-and-shortcuts/code-commands) | Adds selected code to Cline chat |
| [`cline.addTerminalOutputToChat`](/features/commands-and-shortcuts/terminal-integration) | Adds terminal output to Cline |
| `cline.focusChatInput` | Focuses the Cline chat input field |
@@ -0,0 +1,79 @@
---
title: "Disable Terminal Pagers During Cline Sessions"
description: "Make CLI output non-interactive when Cline runs commands by detecting the CLINE_ACTIVE environment variable and disabling pagers like less."
---
Many CLI tools (like Git) use a pager such as `less` for interactive, scrollable output. When Cline runs commands in your terminal, that interactivity gets in the way — the pager can pause on the first page and block progress. You can configure your shell so that when a terminal is spawned by Cline, pagers are disabled and output streams through normally.
## How it works
Cline sets an environment variable for terminals it opens to run commands:
- `CLINE_ACTIVE` — non-empty when the shell is running under Cline
You can detect this variable in your shell startup file and adjust environment variables or aliases only for Cline-run sessions. This keeps your normal interactive terminals unchanged.
## Quick setup (Zsh/Bash)
Add the following to your `~/.zshrc`, `~/.bashrc`, or `~/.bash_profile`:
```bash
# Disable pagers when the terminal is launched by Cline
if [[ -n "$CLINE_ACTIVE" ]]; then
export PAGER=cat
export GIT_PAGER=cat
export SYSTEMD_PAGER=cat
export LESS="-FRX"
fi
```
<Note>
- `PAGER=cat` ensures generic pager-aware tools print directly to stdout
- `GIT_PAGER=cat` prevents Git from invoking `less`
- `SYSTEMD_PAGER=cat` disables paging in systemd tools (if present)
- `LESS="-FRX"` makes `less` behave more like streaming output if a tool still calls it
</Note>
This configuration only applies when `CLINE_ACTIVE` is set, so your normal terminals keep their usual interactive behavior.
## Verify
- Open a task in Cline that runs terminal commands and check:
- `echo "$CLINE_ACTIVE"` prints a non-empty value
- `git log` or other long outputs should stream without pausing
- If changes don't take effect:
- Make sure you updated the correct startup file for your shell
- Restart VS Code/Cursor so integrated terminals reload your shell config
- Confirm your terminal profile sources your `~/.zshrc` or `~/.bashrc`
## Optional tweaks
- Prefer command-line options when you don't want to rely on env vars:
```bash
# One-off usage (no aliases)
git --no-pager log -n 50 --decorate --oneline
systemctl --no-pager status nginx
journalctl --no-pager -u nginx -n 200
less -FRX README.md
```
- You can also override paging via shell aliases scoped to Cline sessions using options rather than env vars:
```bash
if [[ -n "$CLINE_ACTIVE" ]]; then
# Make 'less' non-interactive by default
alias less='less -FRX'
# Disable paging for common tools via CLI flags
alias git='command git --no-pager'
alias systemctl='command systemctl --no-pager'
alias journalctl='command journalctl --no-pager'
fi
```
- If you prefer environment variables, many CLIs also respect a generic or tool-specific pager variable:
- Git: `GIT_PAGER=cat`
- Systemd: `SYSTEMD_PAGER=cat`
- Man pages: `MANPAGER=cat` (not typically needed for Cline-driven commands)
- Aliases affect the current interactive shell, while environment variables propagate to child processes. Choose the approach that best fits your workflow.
@@ -0,0 +1,60 @@
---
title: "Opening Cline in the Right Sidebar"
description: "Learn how to open Cline in the right sidebar in VS Code and Cursor"
---
By default, when you first install Cline, it appears in VS Code's left sidebar alongside your file explorer and other extensions. However, for a better coding experience, we recommend moving Cline to the right sidebar. This allows you to keep your project files visible in the left sidebar while chatting with Cline on the right, giving you full visibility of your codebase as Cline works on your project.
## VS Code
To open Cline in the right sidebar:
<Steps>
<Step title="Align Extension View">
Make sure your extension view is aligned vertically to the left
</Step>
<Step title="Open Right Side View">
Click the button that opens the right side panel in VS Code (typically used to open GitHub Copilot chat). Optionally use the `Option + CMD/Ctrl + B` shortcut.
</Step>
<Step title="Drag Cline Icon">
Drag the Cline icon over to the nav panel at the top of that right view
</Step>
</Steps>
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/vscode_right_view.gif"
alt="VS Code Right Sidebar Setup"
/>
</Frame>
## Cursor
To open Cline in the right sidebar:
<Steps>
<Step title="Align Extensions">
Cursor uses a horizontal activity bar by default to optimize space for the AI chat interface ([see here for details](https://cursor.com/docs/configuration/migrations/vscode#activity-bar-orientation)). To switch to vertical:
1. Open the Command Palette (`CMD/Ctrl + Shift + P`)
2. Search for "Preferences: Open Settings (UI)"
3. Search for `workbench.activityBar.orientation`
4. Set the value to `vertical`
5. Restart Cursor for the changes to take effect
</Step>
<Step title="Open Agent Panel">
Click the Cursor cube icon button that opens Cursor's agent (right side view panel)
</Step>
<Step title="Drag to Three Dots">
Drag the Cline icon directly onto the three dots button - it doesn't work if you just drag it to the top, it has to be the three dots
</Step>
</Steps>
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/cursor-side-bar.gif"
alt="Cursor Right Sidebar Setup"
/>
</Frame>
Once set up, Cline will load on the right side and you can use it as normal.
+149
View File
@@ -0,0 +1,149 @@
---
title: "Dictation"
description: "Communicate with Cline using your voice for faster, more natural AI collaboration"
---
Dictation transforms how you work with AI. Instead of typing out complex thoughts, you speak naturally and share your complete intent. This isn't just about speed - though voice is faster - it's about unlocking the kind of fluid collaboration that typing can't match.
## Why Voice Changes Everything
When you type, you edit yourself. You simplify complex ideas, skip context, and lose nuance. When you speak, you share everything on your mind - the full problem, the constraints, the edge cases you're worried about.
Use Dictation constantly in [Plan mode](/features/plan-and-act) for rapid back-and-forth discussions. Instead of typing careful, structured prompts, think about a problem. Cline asks clarifying questions, respond immediately, and iterate until having a solid plan.
The friction of typing was holding back real collaboration. Voice removes that friction.
## Getting Started
**Enable Dictation:**
1. Go to Settings → Features → Dictation
2. Toggle "Enable Dictation" on
3. Sign into your Cline account when prompted
4. Install FFmpeg if you haven't already (Cline will guide you)
Once enabled, you'll see a microphone button in the chat input area.
**Using Dictation:**
- Click the microphone button to start recording
- Speak naturally
- Click again to stop recording
- Wait for transcription to appear in the chat
<Tip>
Dictation works with any AI model you've configured. The transcription happens through Cline's service, but your conversation continues with whatever model you're using.
</Tip>
## System Requirements
Dictation uses FFmpeg to capture your voice across all platforms:
- **macOS**: FFmpeg (via Homebrew: `brew install ffmpeg`)
- **Linux**: FFmpeg (via apt: `sudo apt-get install ffmpeg`)
- **Windows**: FFmpeg (via winget: `winget install Gyan.FFmpeg`)
If you don't have FFmpeg installed, Cline will automatically detect this and prompt you to install it with a single click.
## Where Dictation Shines
### Plan Mode Conversations
Dictation is perfect for [Plan mode](/features/plan-and-act) discussions. Instead of carefully crafting prompts, you can:
- Dictate your entire problem context in one go
- Respond to Cline's questions immediately
- Iterate on ideas without typing friction
- Think out loud while Cline listens
Start a planning session by speaking for 2-3 minutes straight, explaining the full context of what you're trying to build, the constraints you're working with, and the specific challenges you're facing.
### Complex Problem Explanation
Some problems are hard to type out. When you're dealing with:
- Multi-step workflows with edge cases
- Integration challenges across multiple systems
- Performance issues with specific reproduction steps
- UI/UX problems that need detailed context
Speaking lets you explain the full situation naturally, including all the "oh, and also..." details that matter.
### Code Review and Debugging
When reviewing code or explaining bugs, voice lets you walk through your thought process:
- "This function looks fine, but I'm worried about what happens when..."
- "The issue might be in this section, or possibly this other area..."
- "I tried X and Y, but neither worked because..."
You can share your complete debugging journey instead of just the final question.
## Technical Requirements
**System Requirements:**
- FFmpeg installed on your system
- Active internet connection
- Cline account with transcription credits
**Audio Quality:**
- Records in WebM format with Opus codec
- Mono audio at 16kHz sample rate
- Optimized for voice recognition
**Privacy:**
- Audio recorded locally on your machine
- Only audio files sent for transcription
- No audio stored after transcription
- Temporary files automatically cleaned up
## Cost and Credits
Voice transcription costs $0.006 per minute through your Cline account. For most users, this works out to pennies per session.
A typical 5-minute planning conversation costs about 3 cents. Even heavy voice users rarely spend more than a few dollars per month.
<Note>
Pricing is experimental and may change as we refine the service.
</Note>
## Best Practices
**Speak Naturally**
Don't try to speak like you type. Use your normal conversational tone and don't worry about perfect grammar.
**Give Context First**
Start with the big picture, then drill down into specifics. "I'm building a React app that needs to handle real-time data, and I'm running into performance issues with the WebSocket connection..."
**Use Voice for Exploration**
Dictation is perfect for exploratory conversations where you're not sure exactly what you need. Start talking through the problem and let the conversation evolve.
**Combine with Text**
You don't have to use voice for everything. Use voice for complex explanations and context, then switch to text for quick follow-ups or code snippets.
## Troubleshooting
**Microphone Not Working**
- Check your IDE permissions for microphone access
- Ensure FFmpeg is properly installed
- Try refreshing VSCode/your editor
**Poor Transcription Quality**
- Speak clearly and at normal volume
- Reduce background noise if possible
- Check your microphone settings
**Connection Issues**
- Verify internet connection
- Check if firewall is blocking Cline's servers
- Try signing out and back into your Cline account
**Authentication Issues**
- Sign out and back into your Cline account if you see authentication errors
- Check that your account has sufficient transcription credits
- Verify your internet connection is stable
**Audio Recording Issues**
- Ensure FFmpeg is properly installed and accessible
- Check that your browser/IDE has microphone permissions
- Try restarting your editor if audio capture fails
## The Future of AI Collaboration
When you can speak your thoughts as fast as you think them, you stop self-editing. You share the full context, the edge cases, the "what if" scenarios that matter. This leads to better solutions and fewer back-and-forth clarifications.
+4
View File
@@ -24,6 +24,10 @@ Plan mode is where you and Cline figure out what you're trying to build and how
- Focuses on understanding requirements and creating a strategy
- Helps identify potential issues before you write a single line of code
<Tip>
Try [Dictation](/features/dictation) in Plan mode - instead of typing out complex requirements, you can speak naturally and share your complete thought process. It's perfect for rapid back-and-forth planning discussions.
</Tip>
#### Act Mode: Build It
Once you've got a plan, you switch to Act mode. Now Cline:
+83
View File
@@ -0,0 +1,83 @@
---
title: "YOLO Mode"
sidebarTitle: "YOLO Mode"
---
YOLO mode is exactly what it sounds like - Cline auto-approves everything. Check the box in feature settings and he'll execute file changes, terminal commands, even transitions from Plan to Act mode without asking.
Think of it as [Auto Approve](/features/auto-approve) on steroids - instead of granular permissions, YOLO mode gives Cline complete autonomy.
<Note>
**Warning: This is dangerous.** YOLO mode disables all safety checks. Cline will execute whatever he decides without asking permission.
</Note>
## What Gets Auto-Approved
When YOLO mode is enabled, Cline automatically approves:
- **All file operations** - reading, writing, and modifying files anywhere on your system
- **All terminal commands** - including potentially destructive operations
- **Browser actions** - web scraping, form submissions, navigation
- **MCP server tools** - external integrations and API calls
- **Mode transitions** - automatic switching from Plan to Act mode
Essentially, every safety guardrail is removed. Cline operates with complete autonomy.
## How to Enable YOLO Mode
Navigate to Cline Settings → Features and check the "YOLO Mode" box. That's it - no confirmation dialogs, no additional warnings. Once enabled, Cline will start auto-approving all actions immediately.
To disable it, simply uncheck the box. Any pending actions will still require your approval once YOLO mode is turned off.
## When You Might Use This
YOLO mode was built primarily for our upcoming scriptable CLI where fully autonomous execution makes sense. In the GUI, you might consider it for:
**Rapid prototyping** where you want zero friction and don't care about potential mistakes. Perfect for throwaway experiments or exploring new ideas quickly.
**Trusted, repetitive tasks** where you've already validated Cline's approach and want to eliminate approval overhead. Think routine refactoring or well-established patterns.
**Demonstration purposes** where you want to show Cline's capabilities without constant interruptions.
## What Could Go Wrong
Since YOLO mode removes all safety checks, Cline could:
- Delete important files without warning
- Execute commands that modify system settings
- Make network requests to external services
- Overwrite configuration files
- Install or uninstall software packages
- Commit and push changes to version control
The risk level depends entirely on what you ask Cline to do. Simple tasks remain relatively safe, but complex requests can have unpredictable consequences.
## Best Practices
If you decide to use YOLO mode:
**Start with isolated environments.** Use it in throwaway projects or sandboxed environments first. Never enable it on production codebases until you understand the risks.
**Be specific with requests.** Vague instructions combined with unlimited permissions can lead to unexpected results. The clearer your requirements, the more predictable Cline's actions.
**Monitor the output.** Even though Cline doesn't ask for permission, he still shows you what he's doing. Watch the terminal output and file changes as they happen.
**Keep version control handy.** Make sure you can easily revert changes if something goes wrong. Git becomes your safety net when YOLO mode is your workflow.
## Inspiration: What Becomes Possible
With YOLO mode enabled, you can:
**Build entire applications** from a single prompt. Describe what you want and let Cline handle everything - file creation, dependency installation, configuration setup, even deployment scripts.
**Automate complex workflows** that normally require dozens of approval clicks. Data processing pipelines, build system setup, or multi-step refactoring operations become seamless.
**Rapid iteration cycles** where you can quickly test ideas without approval friction. Perfect for exploring different approaches or experimenting with new technologies.
**Live demonstrations** where you can show Cline's full capabilities without stopping to approve every action. Great for presentations or teaching scenarios.
The key is understanding that YOLO mode transforms Cline from an interactive assistant into an autonomous agent. Use that power wisely.
---
Questions or feedback? Reach us in our [Discord](https://discord.gg/cline) or [r/cline](https://reddit.com/r/cline).
@@ -1,129 +0,0 @@
---
title: "Installing Cline for JetBrains"
description: "Install Cline in your favorite JetBrains IDE with the same powerful AI assistance you know from VSCode."
---
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/jetbrains-logo.svg"
alt="JetBrains logo"
style={{ width: "200px", height: "auto", margin: "0 auto 20px auto", display: "block" }}
/>
</Frame>
Cline for JetBrains works almost identically to Cline in VSCode. All the core features work properly: diff editing, using tools, logging in with different providers, MCP servers, Cline rules and workflows, and more.
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/jetbrains-demo-hifi.gif"
alt="Cline running in JetBrains IDE showing AI assistance"
/>
</Frame>
Cline is now available on the [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/28247-cline). Works in IntelliJ IDEA, PyCharm, WebStorm, DataSpell, PhpStorm, and all other JetBrains IDEs.
## Installation
**Method 1: Direct from your IDE**
1. Open your JetBrains IDE
2. Press `Ctrl+Alt+S` (Windows/Linux) or `Cmd+,` (macOS)
3. Go to Plugins → Marketplace tab
4. Search "Cline" and click Install
5. Restart your IDE
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-marketplace-search.png"
alt="JetBrains marketplace showing Cline plugin search results"
/>
</Frame>
**Method 2: Browser install**
Visit [plugins.jetbrains.com/plugin/28247-cline](https://plugins.jetbrains.com/plugin/28247-cline) and click the "Install to IDE" button. Your IDE will open and prompt you to install.
<details>
<summary>Method 3: Manual installation</summary>
Download the plugin from the [marketplace page](https://plugins.jetbrains.com/plugin/28247-cline), then:
1. Go to Settings → Plugins
2. Click the gear icon → Install Plugin from Disk
3. Select the downloaded `.zip` file
4. Restart your IDE
</details>
## Getting Started with Cline
After installation, you'll find Cline in your IDE. Look for the Cline tool window (usually on the right side) or go to **View** → **Tool Windows** → **Cline**.
Sign in is optional - you can also bring your own API key. If you want to sign in, click **Sign In** in the Cline panel. You'll be taken to [app.cline.bot](https://app.cline.bot) to create your account. No credit card needed to get started with free credits.
Try this first prompt: "Hey Cline! Can you help me create a simple Hello World program in this project?"
## Key Differences from VSCode
The terminal integration works differently in JetBrains. Unlike VSCode where terminal output streams directly to the chat, JetBrains shows command output in a collapsible section. Commands still execute successfully - you just need to expand the **Command Output** section to see results.
## What Works
Everything else works exactly like VSCode. Cline can read, write, and edit files with the same precision. All his tools work identically - file operations, web browsing, you name it.
You get full support for:
- API providers (Anthropic, OpenAI, local models)
- MCP servers and custom tools
- Cline rules and workflows
- @ mentions for files, folders, and problems
- Drag & drop for files and images
## Tips for JetBrains Users
Cline automatically understands your project structure, just like in VSCode. He works with any language your JetBrains IDE supports - Java, Python, JavaScript, Go, whatever you're building.
I find it helpful to share error messages and stack traces directly in the chat when debugging. You can also ask him to review your code changes before committing.
## Troubleshooting
### Plugin Installation Issues
If you can't find Cline in the marketplace:
- Make sure you're searching in the **Marketplace** tab (not Installed)
- Try searching for "Cline AI" or just "Cline"
- Check that your IDE version is compatible (2023.1 or later recommended)
If installation fails:
- Restart your IDE and try again
- Check your internet connection
- Try installing from disk as an alternative
### Plugin Not Appearing
If you don't see the Cline tool window after installation:
- Restart your IDE completely (File → Exit and reopen)
- Check **View** → **Tool Windows** → **Cline**
- Verify the plugin is enabled in **Settings** → **Plugins** → **Installed** tab
- Look for the Cline icon in your IDE's tool window bar (usually on the right side)
### Common Issues
**Plugin appears to be installed but doesn't work:**
- Ensure you've restarted your IDE after installation
- Check if there are any error messages in the IDE's event log
- Try disabling and re-enabling the plugin in Settings
**Performance or compatibility issues:**
- Make sure you're using a supported JetBrains IDE version
- Check for IDE updates that might improve compatibility
- Consider allocating more memory to your IDE if needed
Having other issues? Join our [Discord community](https://discord.gg/cline) for help from the team and other users.
## Next Steps
Now that you have Cline installed, you might want to:
- Learn about [model selection](/getting-started/model-selection-guide) to choose the best AI provider
- Explore [@ mentions](/features/at-mentions/overview) to reference files and context efficiently
- Set up [Cline rules](/features/cline-rules) for your specific workflow
- Try [MCP servers](/mcp/mcp-overview) to extend Cline's capabilities
+226 -53
View File
@@ -1,81 +1,254 @@
---
title: "Installing Cline"
description: "Cline brings AI-powered coding assistance to your editor. Available for VS Code and JetBrains IDEs."
description: "Get Cline set up in your editor and start building projects with AI assistance."
---
## Prerequisites
Before installing Cline, make sure you have the following:
### Create a Cline Account
Create a Cline account for the best experience. Creating a Cline account is completely free and you can [sign up here](https://app.cline.bot/signup). A Cline account provides:
- Access to multiple AI models including stealth models
- Seamless setup without needing to manage API keys
- At times, we partner with model providers to offer inferencing at no cost through your Cline account
### Compatible Editor
Cline works with the following IDEs:
- **VS Code** - Microsoft's popular code editor
- **Cursor** - AI-powered code editor based on VS Code
- **JetBrains IDEs** - IntelliJ IDEA, PyCharm, WebStorm, DataSpell, PhpStorm, and other JetBrains products
- **VSCodium** - Open-source version of VS Code
- **Windsurf** - VS Code-compatible editor
Make sure you have one of these editors installed before proceeding with the Cline installation.
## Choose Your Editor
Cline works across multiple development environments:
Cline works across multiple IDEs. Select your preferred editor below for installation instructions:
- **VS Code/Cursor:** Install from VS Code Marketplace (most popular)
- **JetBrains IDEs:** Install from JetBrains Marketplace - works in IntelliJ IDEA, PyCharm, WebStorm, and more
- **VSCodium/Windsurf:** Install from Open VSX Registry
<Tabs>
<Tab title="VS Code/Cursor" icon="code">
### Installation Steps
## VS Code Installation
1. **Open VS Code** and navigate to the Extensions view (`Ctrl/Cmd + Shift + X`)
2. **Search for "Cline"** in the Extensions marketplace
3. **Click Install** on the Cline extension
### VS Code Marketplace: Step-by-Step Setup
<Frame caption="VS Code marketplace with Cline extension ready to install">
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(20).png"
alt="VS Code marketplace showing Cline extension"
/>
</Frame>
Follow these steps to get Cline up and running:
4. **Access Cline** after installation:
- Click the Cline icon in the Activity Bar, or
- Use Command Palette (`Ctrl/Cmd + Shift + P`) → "Cline: Open In New Tab"
1. **Open VS Code:** Launch the VS Code application.
> **Note:** If VS Code shows "Running extensions might..." dialog, click "Allow". If you don't see the Cline icon, restart VS Code.
> **Note:** If VS Code shows "Running extensions might...", click "Allow".
<Accordion title="Troubleshooting">
2. **Open Your Cline Folder:** In VS Code, open the Cline folder you created in Documents.
3. **Navigate to Extensions:** Click on the Extensions icon in the Activity Bar on the side of VS Code (`Ctrl + Shift + X` or `Cmd + Shift + X`).
4. **Search for 'Cline':** In the Extensions search bar, type `Cline`.
**Plugin Installation Issues**
<Frame caption="VS Code marketplace with Cline extension ready to install">
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(20).png"
alt="VS Code marketplace showing Cline extension"
/>
</Frame>
If you can't find Cline in the marketplace:
- Make sure you're searching in the **Marketplace** tab (not Installed)
- Try searching for "Cline AI" or just "Cline"
- Check that your VS Code version is compatible
1. **Install the Extension:** Click the "Install" button next to the Cline extension.
2. **Open Cline:**
- Click the Cline icon in the Activity Bar.
- Or, use the command palette (`Ctrl/Cmd + Shift + P`) and type "Cline: Open In New Tab" for a better view.
3. **Troubleshooting:** If you don't see the Cline icon, try restarting VS Code.
If installation fails:
- Restart your VS Code and try again
- Check your internet connection
- Try installing from VSIX file as an alternative
> **Pro Tip:** You should see the Cline chat window appear in your VS Code editor!
**Plugin Not Appearing**
### Open VSX Registry
If you don't see the Cline tool window after installation:
- Restart VS Code completely (File → Exit and reopen)
- Check **View** → **Command Palette** → "Cline: Open In New Tab"
- Verify the plugin is enabled in **Extensions** view
- Look for the Cline icon in your Activity Bar (usually on the left side)
For VS Code-compatible editors without Marketplace access (like VSCodium and Windsurf):
**Common Issues**
1. Open your editor.
2. Access the Extensions view.
3. Search for "Cline".
4. Select "Cline" by saoudrizwan and click **Install**.
5. Reload if prompted.
Plugin appears to be installed but doesn't work:
- Ensure you've restarted VS Code after installation
- Check if there are any error messages in the Developer Console
- Try disabling and re-enabling the extension
## JetBrains Installation
Performance or compatibility issues:
- Make sure you're using a supported VS Code version
- Check for VS Code updates that might improve compatibility
- Consider closing other resource-intensive extensions if needed
For IntelliJ IDEA, PyCharm, WebStorm, DataSpell, and other JetBrains IDEs:
Need help? Join our [Discord community](https://discord.gg/cline).
1. Open your JetBrains IDE
2. Press `Ctrl+Alt+S` (Windows/Linux) or `Cmd+,` (macOS) to open Settings
3. Go to Plugins → Marketplace tab
4. Search "Cline" and click Install
5. Restart your IDE
</Accordion>
</Tab>
<Tab title="JetBrains IDEs" icon="brain">
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/jetbrains-logo.svg"
alt="JetBrains logo"
style={{ width: "200px", height: "auto", margin: "0 auto 20px auto", display: "block" }}
/>
**Need more help?** See our [complete JetBrains installation guide](/getting-started/installing-cline-jetbrains) for screenshots and troubleshooting.
Cline for JetBrains works almost identically to Cline in VSCode. All the core features work properly: diff editing, using tools, logging in with different providers, MCP servers, Cline rules and workflows, and more.
### Creating Your Cline Account
Now that you have Cline installed, let's get you set up with your account:
### Installation Steps
**Method 1: From IDE (Recommended)**
1. Open your JetBrains IDE
2. Go to **Settings** (`Ctrl+Alt+S` on Windows/Linux, `Cmd+,` on macOS)
3. Navigate to **Plugins** → **Marketplace**
4. Search for "Cline" and click **Install**
5. Restart your IDE
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-marketplace-search.png"
alt="JetBrains marketplace showing Cline plugin search results"
/>
</Frame>
**Method 2: Browser Install**
Visit the [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/28247-cline) and click **Install to IDE**.
<Accordion title="Method 3: Manual Installation">
1. Download the plugin from the [marketplace page](https://plugins.jetbrains.com/plugin/28247-cline)
2. Go to **Settings** → **Plugins**
3. Click the gear icon → **Install Plugin from Disk**
4. Select the downloaded `.zip` file
5. Restart your IDE
</Accordion>
### Using the Plugin
After installation, youll find Cline in your IDE. Look for the Cline tool window (usually on the right side) or go to View → Tool Windows → Cline.
### Key Features
Cline for JetBrains includes all core features:
- Diff editing and file modifications
- Multiple API providers (Anthropic, OpenAI, local models)
- MCP servers and custom tools
- Cline rules and workflows
- @ mentions for files, folders, and problems
- Drag & drop support
> **Note:** Terminal output appears in collapsible sections rather than streaming directly to chat.
### Key Differences from VSCode
The terminal integration works differently in JetBrains. Unlike VSCode where terminal output streams directly to the chat, JetBrains shows command output in a collapsible section. Commands still execute successfully - you just need to expand the Command Output section to see results.
<Accordion title="Troubleshooting">
**Plugin Installation Issues**
If you can't find Cline in the marketplace:
- Make sure you're searching in the **Marketplace** tab (not Installed)
- Try searching for "Cline AI" or just "Cline"
- Check that your IDE version is compatible (2023.1 or later recommended)
If installation fails:
- Restart your IDE and try again
- Check your internet connection
- Try installing from disk as an alternative
**Plugin Not Appearing**
If you don't see the Cline tool window after installation:
- Restart your IDE completely (File → Exit and reopen)
- Check **View** → **Tool Windows** → **Cline**
- Verify the plugin is enabled in **Settings** → **Plugins** → **Installed** tab
- Look for the Cline icon in your IDE's tool window bar (usually on the right side)
**Common Issues**
Plugin appears to be installed but doesn't work:
- Ensure you've restarted your IDE after installation
- Check if there are any error messages in the IDE's event log
- Try disabling and re-enabling the plugin in Settings
Performance or compatibility issues:
- Make sure you're using a supported JetBrains IDE version
- Check for IDE updates that might improve compatibility
- Consider allocating more memory to your IDE if needed
Need help? Join our [Discord community](https://discord.gg/cline).
</Accordion>
</Tab>
<Tab title="VSCodium/Windsurf" icon="terminal">
### Installation Steps
For VS Code-compatible editors using Open VSX Registry:
1. **Open your editor** (VSCodium, Windsurf, etc.)
2. **Navigate to Extensions view** (`Ctrl/Cmd + Shift + X`)
3. **Search for "Cline"** in the marketplace
4. **Select "Cline" by saoudrizwan** and click **Install**
5. **Reload** if prompted
> **Note:** These editors use the Open VSX Registry instead of the VS Code Marketplace.
<Accordion title="Troubleshooting">
**Plugin Installation Issues**
If you can't find Cline in the marketplace:
- Make sure you're searching in the **Marketplace** tab (not Installed)
- Try searching for "Cline AI" or just "Cline"
- Check that your editor version is compatible
If installation fails:
- Restart your editor and try again
- Check your internet connection
- Try installing from disk as an alternative
**Plugin Not Appearing**
If you don't see the Cline tool window after installation:
- Restart your editor completely (File → Exit and reopen)
- Check **View** → **Command Palette** → "Cline: Open In New Tab"
- Verify the plugin is enabled in **Extensions** view
- Look for the Cline icon in your Activity Bar (usually on the left side)
**Common Issues**
Plugin appears to be installed but doesn't work:
- Ensure you've restarted your editor after installation
- Check if there are any error messages in the Developer Console
- Try disabling and re-enabling the extension
Performance or compatibility issues:
- Make sure you're using a supported editor version
- Check for editor updates that might improve compatibility
- Consider closing other resource-intensive extensions if needed
Need help? Join our [Discord community](https://discord.gg/cline).
</Accordion>
</Tab>
</Tabs>
### Sign In to Your Cline Account
Now that you have Cline installed, sign in to access your account:
1. **Open Cline** in your editor (click the Cline icon in the Activity Bar or Tool Windows)
2. **Click "Sign In"** - you'll see this button in the Cline interface
3. **Complete authentication** - you'll be redirected to [app.cline.bot](https://app.cline.bot) to sign in
4. **Return to your editor** - once signed in, you'll be automatically redirected back
1. **Sign In to Cline:**
- Click the **Sign In** button in the Cline extension.
- You'll be taken to [app.cline.bot](https://app.cline.bot) to create your account.
2. **Start with Free Credits:**
- No credit card needed!
3. **Available AI Models:**
- Anthropic Claude 3.5-Sonnet (recommended for coding)
- DeepSeek Chat (cost-effective alternative)
- Google Gemini 2.0 Flash
- And more — all through your Cline account.
### Your First Interaction with Cline
@@ -96,4 +269,4 @@ Hey Cline! Could you help me create a new project folder called "hello-world" in
### Still Struggling?
Join our Discord community and engage with our team and other Cline users directly.
Join our [Discord community](https://discord.gg/cline) and engage with our team and other Cline users directly.
@@ -9,7 +9,7 @@ New models drop constantly, so this guide focuses on what's working well with Cl
| Model | Context Window | Input Price* | Output Price* | Best For |
|-------|---------------|--------------|---------------|----------|
| **Claude Sonnet 4** | 1M tokens | $3-6 | $15-22.50 | Reliable tool usage, complex codebases |
| **Claude Sonnet 4.5** | 1M tokens | $3-6 | $15-22.50 | Reliable tool usage, complex codebases |
| **Qwen3 Coder** | 256K tokens | $0.20 | $0.80 | Coding tasks, open source flexibility |
| **Gemini 2.5 Pro** | 1M+ tokens | TBD | TBD | Large codebases, document analysis |
| **GPT-5** | 400K tokens | $1.25 | $10 | Latest OpenAI tech, three modes |
@@ -57,9 +57,9 @@ New models drop constantly, so this guide focuses on what's working well with Cl
| If you want... | Use this |
|----------------|----------|
| Something that just works | Claude Sonnet 4 |
| Something that just works | Claude Sonnet 4.5 |
| To save money | DeepSeek V3 or Qwen3 variants |
| Huge context windows | Gemini 2.5 Pro or Claude Sonnet 4 |
| Huge context windows | Gemini 2.5 Pro or Claude Sonnet 4.5 |
| Open source | Qwen3 Coder, Z AI GLM 4.5, or Kimi K2 |
| Latest tech | GPT-5 |
| Speed | Qwen3 Coder on Cerebras (fastest available) |
@@ -74,6 +74,6 @@ Cline automatically handles context limits with [auto-compact](/features/auto-co
## The Bottom Line
Start with **Claude Sonnet 4** if you want reliability. Experiment with **open source options** once you're comfortable to find the best fit for your workflow and budget.
Start with **Claude Sonnet 4.5** if you want reliability. Experiment with **open source options** once you're comfortable to find the best fit for your workflow and budget.
The landscape moves fast - these recommendations reflect what's working now, but keep an eye on new releases.
@@ -53,7 +53,7 @@ Think of context like a whiteboard you and Cline share:
- **Context Window** is the size of the whiteboard itself:
- Measured in tokens (1 token ≈ 3/4 of an English word)
- Each model has a fixed size:
- Claude Sonnet 4: 1,000,000 tokens
- Claude Sonnet 4.5: 1,000,000 tokens
- Qwen3 Coder: 256,000 tokens
- Gemini 2.5 Pro: 1,000,000+ tokens
- GPT-5: 400,000 tokens
@@ -77,7 +77,7 @@ Cline provides a visual way to monitor your context window usage through a progr
- ↑ shows input tokens (what you've sent to the LLM)
- ↓ shows output tokens (what the LLM has generated)
- The progress bar visualizes how much of your context window you've used
- The total shows your model's maximum capacity (e.g., 1M for Claude Sonnet 4)
- The total shows your model's maximum capacity (e.g., 1M for Claude Sonnet 4.5)
### When to Watch the Bar
+3 -3
View File
@@ -1,9 +1,9 @@
---
title: "What is Cline?"
description: "An introduction to Cline, your AI-powered development assistant in VS Code."
description: "An introduction to Cline, your AI-powered development assistant for modern IDEs."
---
Cline is an open source AI coding agent that brings frontier AI models directly to your VS Code editor. Unlike autocomplete tools, Cline is a true coding agent that can understand entire codebases, plan complex changes, and execute multi-step tasks.
Cline is an open source AI coding agent that brings frontier AI models directly to your IDE. Unlike autocomplete tools, Cline is a true coding agent that can understand entire codebases, plan complex changes, and execute multi-step tasks.
## Open Source AI Coding, Uncompromised
@@ -69,4 +69,4 @@ Define project-specific instructions that Cline follows including coding standar
## Getting Started
Ready to experience AI coding without limits? [Install Cline](/getting-started/installing-cline) and start with our [Model Selection Guide](/getting-started/model-selection-guide) to choose the right AI model for your needs.
Ready to experience AI coding without limits? [Install Cline](/getting-started/installing-cline) for your preferred IDE and start with our [Model Selection Guide](/getting-started/model-selection-guide) to choose the right AI model for your needs.
+2 -2
View File
@@ -7,7 +7,7 @@ title: "Configuring MCP Servers"
Utilizing MCP servers will increase your token usage. Cline offers the ability to restrict or disable MCP server functionality as desired.
1. Click the "MCP Servers" icon in the top navigation bar of the Cline extension.
2. Select the "Installed" tab, and then Click the "Advanced MCP Settings" link at the bottom of that pane.
2. Select the "Configure" tab, and then Click the "Advanced MCP Settings" link at the bottom of that pane.
3. Cline will open a new settings window. find `Cline>Mcp:Mode` and make your selection from the dropdown menu.
<Frame>
@@ -56,7 +56,7 @@ To set the maximum time to wait for a response after a tool call to the MCP serv
Settings for all installed MCP servers are located in the `cline_mcp_settings.json` file:
1. Click the MCP Servers icon at the top navigation bar of the Cline pane.
2. Select the "Installed" tab.
2. Select the "Configure" tab.
3. Click the "Configure MCP Servers" button at the bottom of the pane.
The file uses a JSON format with a `mcpServers` object containing named server configurations:
+2 -2
View File
@@ -54,7 +54,7 @@ Please note: Smithery is maintained independently and is not affiliated with our
### Managing Installed MCP Servers
Once added, your MCP servers appear in the "Installed" tab where you can:
Once added, your MCP servers appear in the "Configure" tab where you can:
#### View Server Status
@@ -98,7 +98,7 @@ If a server fails to connect:
For advanced users, Cline stores MCP server configurations in a JSON file that can be modified:
1. In the "Installed" tab, click "Configure MCP Servers" to access the settings file
1. In the "Configure" tab, click "Configure MCP Servers" to access the settings file
2. The configuration for each server follows this format:
```json
+5 -5
View File
@@ -3,7 +3,7 @@ title: "MCP Server Development Protocol"
description: "This protocol is designed to streamline the development process of building MCP servers with Cline."
---
> 🚀 **Build and share your MCP servers with the world.** Once you've created a great MCP server, submit it to the [Cline MCP Marketplace](https://github.com/cline/mcp-marketplace) to make it discoverable and one-click installable by thousands of developers.
> **Build and share your MCP servers with the world.** Once you've created a great MCP server, submit it to the [Cline MCP Marketplace](https://github.com/cline/mcp-marketplace) to make it discoverable and one-click installable by thousands of developers.
## What Are MCP Servers?
@@ -35,7 +35,7 @@ Here's the complete MCP Server Development Protocol that should be placed in you
````markdown
# MCP Server Development Protocol
⚠️ CRITICAL: DO NOT USE attempt_completion BEFORE TESTING ⚠️
CRITICAL: DO NOT USE attempt_completion BEFORE TESTING
## Step 1: Planning (PLAN MODE)
@@ -141,7 +141,7 @@ If ANY answer is "no", I MUST NOT use attempt_completion.
1. Test Each Tool (REQUIRED)
□ Test each tool with valid inputs
□ Verify output format is correct
⚠️ DO NOT PROCEED UNTIL ALL TOOLS TESTED
DO NOT PROCEED UNTIL ALL TOOLS TESTED
## Step 4: Completion
@@ -157,7 +157,7 @@ Only after ALL tools have been tested can attempt_completion be used.
- ✓ Must have comprehensive logging
- ✓ Must test each tool individually
- ✓ Must handle errors gracefully
- ⛔️ NEVER skip testing before completion
- NEVER skip testing before completion
````
When this `.clinerules` file is present in your working directory, Cline will:
@@ -171,7 +171,7 @@ When this `.clinerules` file is present in your working directory, Cline will:
Creating an MCP server requires just a few simple steps to get started:
### 1. Create a `.clinerules` file (🚨 IMPORTANT)
### 1. Create a `.clinerules` file (IMPORTANT)
First, add a `.clinerules` file to the root of your MCP working directory using the protocol above. This file configures Cline to use the MCP development protocol when working in this folder.
+4 -4
View File
@@ -1386,7 +1386,7 @@
"integrity": "sha512-/uR4hAwpcJW9+zbmZL48kKFnWLkOxhIqoGWvZzjg0CniVhR4emtQJAps80WqLAhz0iJgCQxg/axtA7leaznDzQ==",
"license": "Elastic-2.0",
"dependencies": {
"axios": "^1.8.3",
"axios": "^1.12.0",
"openapi-types": "^12.0.0"
},
"engines": {
@@ -2688,9 +2688,9 @@
}
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
+8 -8
View File
@@ -47,7 +47,7 @@ node_modules/
*.xlsx
```
## Prompting Cline 💬
## Prompting Cline
**Prompting is how you communicate your needs for a given task in the back-and-forth chat with Cline.** Cline understands natural language, so write conversationally.
@@ -88,9 +88,9 @@ Effective prompting involves:
Here are some prompting tips that users have found helpful for working with Cline:
## Our Community's Favorite Prompts 🌟
## Our Community's Favorite Prompts
### Memory and Confidence Checks 🧠
### Memory and Confidence Checks
- **Memory Check** - _pacnpal_
@@ -108,7 +108,7 @@ Here are some prompting tips that users have found helpful for working with Clin
Encourages critical thinking and makes decision-making transparent.
### Code Quality Prompts 💻
### Code Quality Prompts
- **Prevent Code Truncation**
@@ -126,7 +126,7 @@ Here are some prompting tips that users have found helpful for working with Clin
Reinforces adherence to your settings dial ⚙️ configuration.
### Code Organization 📋
### Code Organization
- **Large File Refactoring** - _icklebil_
@@ -144,7 +144,7 @@ Here are some prompting tips that users have found helpful for working with Clin
Ensures documentation stays in sync with code changes.
### Analysis and Planning 🔍
### Analysis and Planning
- **Structured Development** - _yellow_bat_coffee_
@@ -174,7 +174,7 @@ Here are some prompting tips that users have found helpful for working with Clin
Identifies potential issues early in development.
### Thoughtful Development 🤔
### Thoughtful Development
- **Pause and Reflect** - _nickbaumann98_
@@ -200,7 +200,7 @@ Here are some prompting tips that users have found helpful for working with Clin
Maintains quality through self-assessment.
### Best Practices 🎯
### Best Practices
- **Project Structure** - _kvs007_
+3 -6
View File
@@ -18,11 +18,8 @@ Cline supports the following Anthropic Claude models:
- `claude-opus-4-1-20250805`
- `claude-opus-4-20250514`
- `claude-opus-4-20250514:thinking` (Extended Thinking variant)
- `claude-sonnet-4-20250514` (Recommended)
- `claude-sonnet-4-20250514:thinking` (Extended Thinking variant)
- `anthropic/claude-sonnet-4.5` (Recommended)
- `claude-3-7-sonnet-20250219`
- `claude-3-7-sonnet-20250219:thinking` (Extended Thinking variant)
- `claude-3-5-sonnet-20241022`
- `claude-3-5-haiku-20241022`
- `claude-3-opus-20240229`
@@ -47,8 +44,8 @@ Cline users can leverage this by checking the `Enable Extended Thinking` box bel
**Key Aspects of Extended Thinking:**
- **Supported Models:** This feature is available for select models, including variants of Claude Opus 4, Claude Sonnet 4, and Claude Sonnet 3.7. The specific models listed in the "Supported Models" section above with the `:thinking` suffix are pre-configured in Cline to utilize this.
- **Summarized Thinking (Claude 4):** For Claude 4 models, the API returns a summary of the full thinking process to balance insight with efficiency and prevent misuse. You are billed for the full thinking tokens, not just the summary.
- **Supported Models:** This feature is available for select models, including Claude Opus 4, Claude Sonnet 4.5, and Claude Sonnet 3.7.
- **Summarized Thinking (Claude 4):** For Claude 4 and 4.5 models, the API returns a summary of the full thinking process to balance insight with efficiency and prevent misuse. You are billed for the full thinking tokens, not just the summary.
- **Streaming:** Extended thinking responses, including the `thinking` blocks, can be streamed.
- **Tool Use & Prompt Caching:** Extended thinking interacts with tool use (requiring thinking blocks to be passed back) and prompt caching (with specific behaviors around cache invalidation and context).
+13 -12
View File
@@ -17,25 +17,26 @@ Baseten provides on-demand frontier model APIs designed for production applicati
### Supported Models
Cline supports all current models under Baseten Model APIs, including:
For the most updated pricing, please visit: https://www.baseten.co/products/model-apis/
**Reasoning Models:**
- `deepseek-ai/DeepSeek-R1` - DeepSeek's first-generation reasoning model (163K context) - $2.55/$5.95 per 1M tokens
- `deepseek-ai/DeepSeek-R1-0528` - Latest revision of DeepSeek's reasoning model (163K context) - $2.55/$5.95 per 1M tokens
- `deepseek-ai/DeepSeek-V3.1` - Hybrid reasoning with advanced tool calling (163K context) - $0.50/$1.50 per 1M tokens
- `deepseek-ai/DeepSeek-V3-0324` - Fast general-purpose with enhanced reasoning (163K context) - $0.77/$0.77 per 1M tokens
- `deepseek-ai/DeepSeek-R1` - DeepSeek's first-generation reasoning model (163K context) - \$2.55/\$5.95 per 1M tokens
- `deepseek-ai/DeepSeek-R1-0528` - Latest revision of DeepSeek's reasoning model (163K context) - \$2.55/\$5.95 per 1M tokens
- `deepseek-ai/DeepSeek-V3.1` - Hybrid reasoning with advanced tool calling (163K context) - \$0.50/\$1.50 per 1M tokens
- `deepseek-ai/DeepSeek-V3-0324` - Fast general-purpose with enhanced reasoning (163K context) - \$0.77/\$0.77 per 1M tokens
**Flagship Models:**
- `openai/gpt-oss-120b` (OpenAI) - 120B MoE with strong reasoning capabilities (128K context) - $0.10/$0.50 per 1M tokens
- `moonshotai/Kimi-K2-Instruct` (Moonshot AI) - 1 trillion parameter model for agentic tasks (131K context) - $0.60/$2.50 per 1M tokens
- `moonshotai/Kimi-K2-Instruct-0905` (Moonshot AI) - September update with enhanced capabilities (262K context) - $0.60/$2.50 per 1M tokens
- `openai/gpt-oss-120b` (OpenAI) - 120B MoE with strong reasoning capabilities (128K context) - \$0.10/\$0.50 per 1M tokens
- `moonshotai/Kimi-K2-Instruct` (Moonshot AI) - 1 trillion parameter model for agentic tasks (131K context) - \$0.60/\$2.50 per 1M tokens
- `moonshotai/Kimi-K2-Instruct-0905` (Moonshot AI) - September update with enhanced capabilities (262K context) - \$0.60/\$2.50 per 1M tokens
**Meta Llama 4 Series:**
- `meta-llama/Llama-4-Maverick-17B-128E-Instruct` - High-efficiency processing (1M context!) - $0.19/$0.72 per 1M tokens
- `meta-llama/Llama-4-Scout-17B-16E-Instruct` - Precise context understanding (1M context!) - $0.13/$0.50 per 1M tokens
- `meta-llama/Llama-4-Maverick-17B-128E-Instruct` - High-efficiency processing (1M context!) - \$0.19/\$0.72 per 1M tokens
- `meta-llama/Llama-4-Scout-17B-16E-Instruct` - Precise context understanding (1M context!) - \$0.13/\$0.50 per 1M tokens
**Coding Specialists:**
- `Qwen/Qwen3-Coder-480B-A35B-Instruct` (Alibaba Cloud) - Advanced coding and reasoning (262K context) - $0.38/$1.53 per 1M tokens
- `Qwen/Qwen3-235B-A22B-Instruct-2507` (Alibaba Cloud) - Math and reasoning expert (262K context) - $0.22/$0.80 per 1M tokens
- `Qwen/Qwen3-Coder-480B-A35B-Instruct`- Advanced coding and reasoning (262K context) - \$0.38/\$1.53 per 1M tokens
- `Qwen/Qwen3-235B-A22B-Instruct-2507` - Math and reasoning expert (262K context) - \$0.22/\$0.80 per 1M tokens
### Configuration in Cline
@@ -111,4 +112,4 @@ Baseten's OpenAI compatibility makes migration straightforward:
### Pricing Information
Current pricing is highly competitive and transparent. For the most up-to-date pricing, visit the [Baseten Model APIs page](https://www.baseten.co/products/model-apis/). Prices typically range from $0.10-$6.00 per million tokens, making Baseten significantly more cost-effective than many closed-model alternatives while providing access to state-of-the-art open-source models.
Current pricing is highly competitive and transparent. For the most up-to-date pricing, visit the [Baseten Model APIs page](https://www.baseten.co/products/model-apis/). Prices typically range from \$0.10-\$6.00 per million tokens, making Baseten significantly more cost-effective than many closed-model alternatives while providing access to state-of-the-art open-source models.
+6 -8
View File
@@ -8,10 +8,9 @@ SAP AI Core and the generative AI hub help you to integrate LLMs and AI into new
**Website:** [SAP Help Portal](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/what-is-sap-ai-core)
> 💡 **Information**
>
> SAP AI Core, and Generative AI Hub, are offerings from SAP BTP.
> You need an active SAP BTP contract and a existing subaccount with a SAP AI Core instance with the `extended` service plan (For more details about SAP AI Core service plans and their capabilities, see the [Service Plans documentation](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/service-plans)) to perform these steps.
<Info>
SAP AI Core, and Generative AI Hub, are offerings from SAP BTP. You need an active SAP BTP contract and a existing subaccount with a SAP AI Core instance with the `extended` service plan (For more details about SAP AI Core service plans and their capabilities, see the [Service Plans documentation](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/service-plans)) to perform these steps.
</Info>
### Getting a Service Binding
@@ -70,7 +69,6 @@ When using OpenAI reasoning models (such as o1, o3, o3-mini, o4-mini) through SA
- **Medium:** Balanced performance and token usage for most tasks
- **High:** More thorough analysis with higher token usage, better for complex reasoning tasks
> 💡 **Note**
>
> This setting only applies when using OpenAI reasoning models (o1, o3, o3-mini, o4-mini, gpt-5, etc.) deployed through SAP AI Core. Other models will ignore this setting.
<Note>
This setting only applies when using OpenAI reasoning models (o1, o3, o3-mini, o4-mini, gpt-5, etc.) deployed through SAP AI Core. Other models will ignore this setting.
</Note>
+36 -5
View File
@@ -3,16 +3,16 @@ title: "LM Studio"
description: "A quick guide to setting up LM Studio for local AI model execution with Cline."
---
## 🤖 Setting Up LM Studio with Cline
## Setting Up LM Studio with Cline
Run AI models locally using LM Studio with Cline.
### 📋 Prerequisites
### Prerequisites
- Windows, macOS, or Linux computer with AVX2 support
- Cline installed in VS Code
### 🚀 Setup Steps
### Setup Steps
#### 1. Install LM Studio
@@ -75,14 +75,45 @@ Run AI models locally using LM Studio with Cline.
/>
</Frame>
### ⚠️ Important Notes
### Recommended Model and Settings
For the best experience with Cline, use **Qwen3 Coder 30B A3B Instruct**. This model delivers strong coding performance and reliable tool use.
#### Critical Settings
After loading your model in the Developer tab, configure these settings:
1. **Context Length**: Set to 262,144 (the model's maximum)
2. **KV Cache Quantization**: Leave unchecked (critical for consistent performance)
3. **Flash Attention**: Enable if available (improves performance)
#### Quantization Guide
Choose quantization based on your RAM:
- **32GB RAM**: Use 4-bit quantization (~17GB download)
- **64GB RAM**: Use 8-bit quantization (~32GB download) for better quality
- **128GB+ RAM**: Consider full precision or larger models
#### Model Format
- **Mac (Apple Silicon)**: Use MLX format for optimized performance
- **Windows/Linux**: Use GGUF format
### Enable Compact Prompts
For optimal performance with local models, enable compact prompts in Cline settings. This reduces the prompt size by 90% while maintaining core functionality.
Navigate to Cline Settings → Features → Use Compact Prompt and toggle it on.
### Important Notes
- Start LM Studio before using with Cline
- Keep LM Studio running in background
- First model download may take several minutes depending on size
- Models are stored locally after download
### 🔧 Troubleshooting
### Troubleshooting
1. If Cline can't connect to LM Studio:
2. Verify LM Studio server is running (check Developer tab)
+33 -14
View File
@@ -3,12 +3,12 @@ title: "Ollama"
description: "A quick guide to setting up Ollama for local AI model execution with Cline."
---
### 📋 Prerequisites
### Prerequisites
- Windows, macOS, or Linux computer
- Cline installed in VS Code
### 🚀 Setup Steps
### Setup Steps
#### 1. Install Ollama
@@ -53,31 +53,50 @@ description: "A quick guide to setting up Ollama for local AI model execution wi
/>
</Frame>
**✨ Your model is now ready to use within Cline!**
Your model is now ready to use within Cline.
#### 3. Configure Cline
1. Open VS Code
2. Click Cline settings icon
3. Select "Ollama" as API provider
4. Enter configuration:
- Base URL: `http://localhost:11434/` (default value, can be left as is)
- Select the model from your available options
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/selecting-ollama-model-cline%20(3).gif"
alt="Configuring Cline with Ollama"
src="https://storage.googleapis.com/cline_public_images/docs/assets/ollama-setup.gif"
alt="Complete Ollama setup process"
/>
</Frame>
### ⚠️ Important Notes
Open VS Code and configure Cline:
1. Click the Cline settings icon
2. Select "Ollama" as your API provider
3. Base URL: `http://localhost:11434/` (default, usually no need to change)
4. Select your model from the dropdown
### Recommended Models
For the best experience with Cline, use **Qwen3 Coder 30B**. This model provides strong coding capabilities and reliable tool use for local development.
To download it:
```bash
ollama run qwen3-coder-30b
```
Other capable models include:
- `mistral-small` - Good balance of performance and speed
- `devstral-small` - Optimized for coding tasks
### Important Notes
- Start Ollama before using with Cline
- Keep Ollama running in background
- First model download may take several minutes
### 🔧 Troubleshooting
### Enable Compact Prompts
For better performance with local models, enable compact prompts in Cline settings. This reduces the prompt size by 90% while maintaining core functionality.
Navigate to Cline Settings → Features → Use Compact Prompt and toggle it on.
### Troubleshooting
If Cline can't connect to Ollama:
+119 -74
View File
@@ -2,108 +2,153 @@
title: "Read Me First"
---
## Running Local Models with Cline: What You Need to Know 🤖
## Running Local Models with Cline
Cline is a powerful AI coding assistant that uses tool-calling to help you write, analyze, and modify code. While running models locally can save on API costs, there's an important trade-off: local models are significantly less reliable at using these essential tools.
Local models have reached a turning point. For the first time, you can run Cline completely offline with genuinely capable models. No API costs, no data leaving your machine, no internet dependency.
## Why Local Models Are Different 🔬
The key is choosing the right model for your hardware and configuring it properly.
When you run a "local version" of a model, you're actually running a drastically simplified copy of the original. This process, called distillation, is like trying to compress a professional chef's knowledge into a basic cookbook you keep the simple recipes but lose the complex techniques and intuition.
## What You Need to Know
Local models are created by training a smaller model to imitate a larger one, but they typically only retain 1-26% of the original model's capacity. This massive reduction means:
### Hardware Requirements
- Less ability to understand complex contexts
- Reduced capability for multi-step reasoning
- Limited tool-use abilities
- Simplified decision-making process
Your RAM determines which models you can run:
Think of it like running your development environment on a calculator instead of a computer it might handle basic tasks, but complex operations become unreliable or impossible.
| RAM Tier | Recommended Model | Quantization | What You Get |
| --- | --- | --- | --- |
| 32GB | Qwen3 Coder 30B | 4-bit | Entry-level local coding |
| 64GB | Qwen3 Coder 30B | 8-bit | Full Cline features |
| 128GB+ | GLM-4.5-Air | 4-bit | Cloud-competitive performance |
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(4).png"
alt="Local model comparison diagram"
/>
</Frame>
### The Model That Works: Qwen3 Coder 30B
### What Actually Happens
After extensive testing, **Qwen3 Coder 30B** is the only model under 70B parameters that reliably works with Cline. It brings:
When you run a local model with Cline:
- 256K native context window
- Strong tool-use capabilities
- Repository-scale understanding
- Reliable command execution
#### Performance Impact 📉
Most smaller models (7B-20B) fail with Cline. They produce broken outputs, refuse to execute commands, or can't handle tool use properly.
- Responses are 5-10x slower than cloud services
- System resources (CPU, GPU, RAM) get heavily utilized
- Your computer may become less responsive for other tasks
### Critical Configuration
#### Tool Reliability Issues 🛠️
Getting local models to work requires specific settings:
- Code analysis becomes less accurate
- File operations may be unreliable
- Browser automation capabilities are reduced
- Terminal commands might fail more often
- Complex multi-step tasks often break down
**For LM Studio:**
1. Context Length: 262,144 (maximum)
2. KV Cache Quantization: OFF (critical)
3. Flash Attention: ON (if available)
### Hardware Requirements 💻
**For All Local Models:**
- Enable "Use Compact Prompt" in Cline settings
- This reduces prompt size by 90% while maintaining core functionality
- Essential for local inference performance
You'll need at minimum:
### Quantization Explained
- Modern GPU with 8GB+ VRAM (RTX 3070 or better)
- 32GB+ system RAM
- Fast SSD storage
- Good cooling solution
Quantization reduces model precision to fit on consumer hardware. Think of it as compression:
Even with this hardware, you'll be running smaller, less capable versions of models:
- **4-bit**: ~75% size reduction. Completely usable for coding tasks.
- **8-bit**: ~50% size reduction. Better quality, more nuanced responses.
- **16-bit**: Full precision. Matches cloud APIs but requires 4x the memory.
| Model Size | What You Get |
| ---------- | ------------------------------------------------------- |
| 7B models | Basic coding, limited tool use |
| 14B models | Better coding, unstable tool use |
| 32B models | Good coding, inconsistent tool use |
| 70B models | Best local performance, but requires expensive hardware |
For Qwen3 Coder 30B:
- 4-bit: ~17GB download
- 8-bit: ~32GB download
- 16-bit: ~60GB download
Put simply, the cloud (API) versions of these models are the full-bore version of the model. The full version of DeepSeek-R1 is 671B. These distilled models are essentially "watered-down" versions of the cloud model.
### Model Format
### Practical Recommendations 💡
Choose based on your platform:
#### Consider This Approach
**MLX (Mac only)**
- Optimized for Apple Silicon
- Leverages Metal and AMX acceleration
- Faster inference on M1/M2/M3 chips
1. Use cloud models for:
- Complex development tasks
- When tool reliability is crucial
- Multi-step operations
- Critical code changes
2. Use local models for:
- Simple code completion
- Basic documentation
- When privacy is paramount
- Learning and experimentation
**GGUF (Universal)**
- Works on Windows, Linux, and Mac
- Extensive quantization options
- Broader tool compatibility
#### If You Must Go Local
## Performance Characteristics
- Start with smaller models
- Keep tasks simple and focused
- Save work frequently
- Be prepared to switch to cloud models for complex operations
- Monitor system resources
Local models perform differently than cloud APIs:
### Common Issues 🚨
**Expect:**
- Warmup time when first loading (normal, happens once)
- Slower inference than cloud models
- Context ingestion slows with very large repositories
- **"Tool execution failed":** Local models often struggle with complex tool chains. Simplify your prompt.
- **"No connection could be made because the target machine actively refused it":** This usually means that the Ollama or LM Studio server isn't running, or is running on a different port/address than Cline is configured to use. Double-check the Base URL address in your API Provider settings.
- **"Cline is having trouble...":** Increase your model's context length to its maximum size.
- **Slow or incomplete responses:** Local models can be slower than cloud-based models, especially on less powerful hardware. If performance is an issue, try using a smaller model. Expect significantly longer processing times.
- **System stability:** Watch for high GPU/CPU usage and temperature
- **Context limitations:** Local models often have smaller context windows than cloud models. Break tasks down into smaller pieces.
**Don't Expect:**
- Instant responses like cloud APIs
- Unlimited context processing speed
- Zero configuration
### Looking Ahead 🔮
## When Local Models Excel
Local model capabilities are improving, but they're not yet a complete replacement for cloud services, especially for Cline's tool-based functionality. Consider your specific needs and hardware capabilities carefully before committing to a local-only approach.
Use local models for:
### Need Help? 🤝
- Offline development where internet is unreliable
- Privacy-sensitive projects where code can't leave your environment
- Cost-conscious development where API usage would be prohibitive
- Learning and experimentation with unlimited usage
- Join our [Discord](https://discord.gg/cline) community and [r/cline](https://www.reddit.com/r/CLine/)
- Check the latest compatibility guides
- Share your experiences with other developers
## When to Use Cloud Models
Remember: When in doubt, prioritize reliability over cost savings for important development work.
Cloud models still have advantages for:
- Very large repositories exceeding local context limits
- Multi-hour refactoring sessions needing maximum context
- Teams requiring consistent performance across different hardware
- Tasks requiring the absolute latest model capabilities
## Common Issues
**"Shell integration unavailable" or command execution fails**
Switch to a simpler shell in Cline settings. Go to Cline Settings → Terminal → Default Terminal Profile and select "bash". This resolves 90% of terminal integration problems.
**"No connection could be made"**
Your local server (Ollama or LM Studio) isn't running, or is running on a different port. Check that:
- The server is actually running
- The Base URL in Cline settings matches your server's address
- No firewall is blocking the connection
**Slow or incomplete responses**
This is normal for local models. They're significantly slower than cloud APIs. If it's too slow:
- Try a smaller quantization (4-bit instead of 8-bit)
- Reduce context window size
- Enable compact prompts if you haven't already
**Model seems confused or makes errors**
Ensure you have:
- Compact prompts enabled
- KV Cache Quantization disabled (LM Studio)
- Context length set to maximum
- Sufficient RAM for your chosen quantization
## Getting Started
1. **Choose your runtime**: [LM Studio](/running-models-locally/lm-studio) or [Ollama](/running-models-locally/ollama)
2. **Download Qwen3 Coder 30B** in the appropriate quantization for your RAM
3. **Configure critical settings** as outlined above
4. **Enable compact prompts** in Cline settings
5. **Start coding** offline
## The Reality of Local Models
Local models are now genuinely useful for coding tasks, but they're not magic. You're trading some convenience and speed for privacy and cost savings. The setup requires attention to detail, and performance won't match top-tier cloud APIs.
But for the first time, you can run a capable coding agent entirely on your laptop. That's a significant milestone.
## Need Help?
- Join our [Discord](https://discord.gg/cline) community
- Visit [r/cline](https://www.reddit.com/r/CLine/) on Reddit
- Check the [LM Studio guide](/running-models-locally/lm-studio) for detailed setup
- See the [Ollama guide](/running-models-locally/ollama) for alternative setup
+47
View File
@@ -0,0 +1,47 @@
/* Custom styles for Cline documentation */
/* Make h1 titles lighter in font weight */
h1 {
font-weight: 500 !important;
}
/* Also apply to any h1 elements within content areas */
.content h1,
.markdown h1,
article h1,
main h1 {
font-weight: 500 !important;
}
/* JetBrains logo visibility fix for dark mode */
/* Add a subtle background and border to ensure visibility in both light and dark modes */
img[alt="JetBrains logo"] {
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 12px;
transition: all 0.3s ease;
}
/* Dark mode specific styling */
[data-theme="dark"] img[alt="JetBrains logo"],
.dark img[alt="JetBrains logo"] {
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(0, 0, 0, 0.2);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Media query for system dark mode preference */
@media (prefers-color-scheme: dark) {
img[alt="JetBrains logo"] {
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(0, 0, 0, 0.2);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
/* Hover effect for better interactivity */
img[alt="JetBrains logo"]:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
+18 -13
View File
@@ -6,7 +6,7 @@ import * as esbuild from "esbuild"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const production = process.argv.includes("--production")
const production = process.argv.includes("--production") || process.env["IS_DEBUG_BUILD"] === "false"
const watch = process.argv.includes("--watch")
const standalone = process.argv.includes("--standalone")
const e2eBuild = process.argv.includes("--e2e-build")
@@ -123,24 +123,29 @@ const copyWasmFiles = {
},
}
const buildEnvVars = { "import.meta.url": "_importMetaUrl" }
if (production) {
// IS_DEV is always disable in production builds.
buildEnvVars["process.env.IS_DEV"] = "false"
}
// Set the environment and telemetry env vars. The API key env vars need to be populated in the GitHub
// workflows from the secrets.
if (process.env.CLINE_ENVIRONMENT) {
buildEnvVars["process.env.CLINE_ENVIRONMENT"] = JSON.stringify(process.env.CLINE_ENVIRONMENT)
}
if (process.env.TELEMETRY_SERVICE_API_KEY) {
buildEnvVars["process.env.TELEMETRY_SERVICE_API_KEY"] = JSON.stringify(process.env.TELEMETRY_SERVICE_API_KEY)
}
if (process.env.ERROR_SERVICE_API_KEY) {
buildEnvVars["process.env.ERROR_SERVICE_API_KEY"] = JSON.stringify(process.env.ERROR_SERVICE_API_KEY)
}
// Base configuration shared between extension and standalone builds
const baseConfig = {
bundle: true,
minify: production,
sourcemap: !production,
logLevel: "silent",
define: production
? {
"import.meta.url": "_importMetaUrl",
"process.env.IS_DEV": JSON.stringify(!production),
...(process.env.TELEMETRY_SERVICE_API_KEY && process.env.ERROR_SERVICE_API_KEY
? {
"process.env.TELEMETRY_SERVICE_API_KEY": JSON.stringify(process.env.TELEMETRY_SERVICE_API_KEY),
"process.env.ERROR_SERVICE_API_KEY": JSON.stringify(process.env.ERROR_SERVICE_API_KEY),
}
: {}),
}
: { "import.meta.url": "_importMetaUrl" },
define: buildEnvVars,
tsconfig: path.resolve(__dirname, "tsconfig.json"),
plugins: [
copyWasmFiles,
-17
View File
@@ -148,11 +148,6 @@ export async function spawnVSCode(workspacePath: string, vsixPath?: string): Pro
command: "workbench.view.extension.saoudrizwan.claude-dev-ActivityBar",
when: "viewContainer.workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.enabled",
},
{
key: "alt+shift+c",
command: "cline.openInNewTab",
when: "viewContainer.workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.enabled",
},
]
fs.writeFileSync(keybindingsPath, JSON.stringify(keybindings, null, 2))
console.log(`Created keybindings.json to help with Cline activation`)
@@ -187,11 +182,6 @@ export async function spawnVSCode(workspacePath: string, vsixPath?: string): Pro
setTimeout(() => {
// Try to open Cline in the sidebar
require('vscode').commands.executeCommand('workbench.view.extension.saoudrizwan.claude-dev-ActivityBar');
// Also try to open Cline in a tab as a fallback
setTimeout(() => {
require('vscode').commands.executeCommand('cline.openInNewTab');
}, 5000);
}, 5000);
`
fs.writeFileSync(startupScriptPath, startupScript)
@@ -288,13 +278,6 @@ export async function spawnVSCode(workspacePath: string, vsixPath?: string): Pro
// Wait a moment for the sidebar to initialize
await new Promise(resolve => setTimeout(resolve, 2000));
// Also open Cline in a tab as a fallback
console.log('Opening Cline in a tab...');
await vscode.commands.executeCommand('cline.openInNewTab');
// Wait a moment for the tab to initialize
await new Promise(resolve => setTimeout(resolve, 2000));
// Create the test server if it doesn't exist
console.log('Creating test server...');
+82 -31
View File
@@ -9,9 +9,9 @@
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"axios": "^1.8.2",
"axios": "^1.12.0",
"better-sqlite3": "^11.10.0",
"chalk": "^4.1.2",
"chalk": "5.6.2",
"commander": "^9.4.1",
"dotenv": "^16.5.0",
"execa": "^5.1.1",
@@ -200,12 +200,13 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -292,15 +293,12 @@
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": ">=10"
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
@@ -607,9 +605,10 @@
}
},
"node_modules/form-data": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -708,6 +707,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -843,6 +843,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-symbols/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -1003,6 +1019,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -1251,6 +1283,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -1635,12 +1668,12 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -1695,13 +1728,9 @@
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="
},
"chownr": {
"version": "1.1.4",
@@ -1909,9 +1938,9 @@
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
},
"form-data": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -2054,6 +2083,17 @@
"requires": {
"chalk": "^4.1.0",
"is-unicode-supported": "^0.1.0"
},
"dependencies": {
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
}
}
},
"make-error": {
@@ -2164,6 +2204,17 @@
"log-symbols": "^4.1.0",
"strip-ansi": "^6.0.0",
"wcwidth": "^1.0.1"
},
"dependencies": {
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
}
}
},
"path-key": {
+2 -2
View File
@@ -19,9 +19,9 @@
"author": "",
"license": "MIT",
"dependencies": {
"axios": "^1.8.2",
"axios": "^1.12.0",
"better-sqlite3": "^11.10.0",
"chalk": "^4.1.2",
"chalk": "5.6.2",
"dotenv": "^16.5.0",
"commander": "^9.4.1",
"execa": "^5.1.1",
+3
View File
@@ -0,0 +1,3 @@
go 1.24.7
use ./cli
+23
View File
@@ -0,0 +1,23 @@
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
+1950 -208
View File
File diff suppressed because it is too large Load Diff
+60 -54
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.29.0",
"version": "3.32.6",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -133,11 +133,6 @@
"title": "History",
"icon": "$(history)"
},
{
"command": "cline.popoutButtonClicked",
"title": "Open in Editor",
"icon": "$(link-external)"
},
{
"command": "cline.accountButtonClicked",
"title": "Account",
@@ -148,11 +143,6 @@
"title": "Settings",
"icon": "$(settings-gear)"
},
{
"command": "cline.openInNewTab",
"title": "Open In New Tab",
"category": "Cline"
},
{
"command": "cline.dev.createTestTasks",
"title": "Create Test Tasks",
@@ -178,7 +168,10 @@
"command": "cline.generateGitCommitMessage",
"title": "Generate Commit Message with Cline",
"category": "Cline",
"icon": "$(robot)"
"icon": {
"light": "assets/icons/robot_panel_light.png",
"dark": "assets/icons/robot_panel_dark.png"
}
},
{
"command": "cline.abortGitCommitMessage",
@@ -200,6 +193,11 @@
"command": "cline.openWalkthrough",
"title": "Open Walkthrough",
"category": "Cline"
},
{
"command": "cline.reconstructTaskHistory",
"title": "Reconstruct Task History",
"category": "Cline"
}
],
"keybindings": [
@@ -241,11 +239,6 @@
"group": "navigation@3",
"when": "view == claude-dev.SidebarProvider"
},
{
"command": "cline.popoutButtonClicked",
"group": "navigation@4",
"when": "view == claude-dev.SidebarProvider"
},
{
"command": "cline.accountButtonClicked",
"group": "navigation@5",
@@ -257,38 +250,6 @@
"when": "view == claude-dev.SidebarProvider"
}
],
"editor/title": [
{
"command": "cline.plusButtonClicked",
"group": "navigation@1",
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
},
{
"command": "cline.mcpButtonClicked",
"group": "navigation@2",
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
},
{
"command": "cline.historyButtonClicked",
"group": "navigation@3",
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
},
{
"command": "cline.popoutButtonClicked",
"group": "navigation@4",
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
},
{
"command": "cline.accountButtonClicked",
"group": "navigation@5",
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
},
{
"command": "cline.settingsButtonClicked",
"group": "navigation@6",
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
}
],
"editor/context": [
{
"command": "cline.addToChat",
@@ -334,12 +295,15 @@
"vscode:prepublish": "npm run package",
"compile": "npm run check-types && npm run lint && node esbuild.mjs",
"compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
"compile-cli": "scripts/build-cli.sh",
"dev:cli": "npm run compile-standalone && npm run compile-cli && ./cli/bin/cline instance kill --all && ./cli/bin/cline instance new",
"postcompile-standalone": "node scripts/package-standalone.mjs",
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.mjs --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
"protos": "node scripts/build-proto.mjs",
"protos-go": "node scripts/build-go-proto.mjs",
"postprotos": "biome format src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --no-errors-on-unmatched",
"clean:build": "rimraf dist dist-standalone webview-ui/build src/generated out/",
"clean:deps": "rimraf node_modules webview-ui/node_modules",
@@ -351,11 +315,12 @@
"format": "biome format --changed --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error",
"format:fix": "biome check --changed --no-errors-on-unmatched --files-ignore-unknown=true --write",
"fix:all": "biome check --no-errors-on-unmatched --files-ignore-unknown=true --write --diagnostic-level=error --unsafe",
"ci:check-all": "npm-run-all -p check-types lint format",
"ci:build": "npm run protos && npm run build:webview && node esbuild.mjs && npm run compile-tests",
"pretest": "npm run compile && npm run compile-tests && npm run compile-standalone && npm run lint",
"test": "npm-run-all test:unit test:integration",
"test:ci": "node scripts/test-ci.js",
"test:integration": "vscode-test",
"test:unit": "TS_NODE_PROJECT='./tsconfig.unit-test.json' mocha",
"test:unit": "cross-env TS_NODE_PROJECT=./tsconfig.unit-test.json mocha",
"test:coverage": "vscode-test --coverage",
"test:sca-server": "npx tsx watch scripts/test-standalone-core-api-server.ts",
"test:tp-orchestrator": "npx tsx scripts/testing-platform-orchestrator.ts",
@@ -404,20 +369,24 @@
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.6.0",
"c8": "^10.1.3",
"chai": "^4.3.10",
"chalk": "^5.3.0",
"chalk": "5.6.2",
"cross-env": "^10.1.0",
"esbuild": "^0.25.0",
"grpc-tools": "^1.13.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
"minimatch": "^3.0.3",
"npm-run-all": "^4.1.5",
"nyc": "^17.1.0",
"prebuild-install": "^7.1.3",
"protoc-gen-ts": "^0.8.7",
"proxyquire": "^2.1.3",
"rimraf": "^6.0.1",
"should": "^13.2.3",
"sinon": "^19.0.2",
"tree-kill": "^1.2.2",
"ts-node": "^10.9.2",
"ts-proto": "^2.6.1",
"tsconfig-paths": "^4.2.0",
@@ -450,7 +419,8 @@
"@types/uuid": "^10.0.0",
"@vscode/codicons": "^0.0.36",
"archiver": "^7.0.1",
"axios": "^1.8.2",
"axios": "^1.12.0",
"better-sqlite3": "^12.4.1",
"cheerio": "^1.0.0",
"chokidar": "^4.0.1",
"chrome-launcher": "^1.1.2",
@@ -465,6 +435,7 @@
"get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"grpc-health-check": "^2.0.2",
"https-proxy-agent": "^7.0.6",
"iconv-lite": "^0.6.3",
"ignore": "^7.0.3",
"image-size": "^2.0.2",
@@ -487,7 +458,7 @@
"reconnecting-eventsource": "^1.6.4",
"serialize-error": "^11.0.3",
"simple-git": "^3.27.0",
"strip-ansi": "^7.1.0",
"strip-ansi": "^7.1.2",
"tree-sitter-wasms": "^0.1.11",
"ts-morph": "^25.0.1",
"turndown": "^7.2.0",
@@ -496,5 +467,40 @@
"vscode-uri": "^3.1.0",
"web-tree-sitter": "^0.22.6",
"zod": "^3.24.2"
},
"c8": {
"reporter": [
"lcov",
"html"
],
"exclude": [
"**/testing-platform/**",
"**/webview-ui/**",
"**/.vscode-test/**",
"**/node_modules/**",
"node_modules",
"**/dist-standalone/src/**",
"**/dist-standalone/vsce-extension/https:/**",
"**/dist-standalone/vsce-extension/**",
"**/dist-standalone/https:/**",
"**/dist-standalone/LIB/src/**",
"**/dist-standalone/pdfjs-dist/**",
"**/*.d.ts",
"**/*.{test,spec}.{js,jsx,ts,tsx,mjs,cjs}",
"**/__tests__/**",
"**/test/**",
"**/tests/**",
"**/.nyc_output/**",
"**/tests-results/**",
"src/test/**",
"**/src/xml/**",
"**/standalone/**",
"**/src/generated/**",
"**/evals/cli/dist/**",
"**/evals/cli/src/**",
"dist"
],
"all": true,
"exclude-after-remap": true
}
}
+1
View File
@@ -2,6 +2,7 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
+1
View File
@@ -3,6 +3,7 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
import "cline/state.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
+1
View File
@@ -2,6 +2,7 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
+1
View File
@@ -2,6 +2,7 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
+1
View File
@@ -1,6 +1,7 @@
syntax = "proto3";
package cline;
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
+42
View File
@@ -0,0 +1,42 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
service DictationService {
rpc startRecording(EmptyRequest) returns (RecordingResult);
rpc stopRecording(EmptyRequest) returns (RecordedAudio);
rpc cancelRecording(EmptyRequest) returns (RecordingResult);
rpc getRecordingStatus(EmptyRequest) returns (RecordingStatus);
rpc transcribeAudio(TranscribeAudioRequest) returns (Transcription);
}
message TranscribeAudioRequest {
string audio_base64 = 2;
string language = 3;
}
message RecordingResult {
bool success = 1;
string error = 2;
}
message RecordedAudio {
bool success = 1;
string audio_base64 = 2;
string error = 3;
}
message RecordingStatus {
bool is_recording = 1;
double duration_seconds = 2;
string error = 3;
}
message Transcription {
string text = 1;
string error = 2;
}
+2 -1
View File
@@ -2,6 +2,7 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
@@ -50,7 +51,7 @@ service FileService {
rpc refreshRules(EmptyRequest) returns (RefreshedRules);
// Opens a task's conversation history file on disk
rpc openTaskHistory(StringRequest) returns (Empty);
rpc openDiskConversationHistory(StringRequest) returns (Empty);
// Toggles a workflow on or off
rpc toggleWorkflow(ToggleWorkflowRequest) returns (ClineRulesToggles);
+1
View File
@@ -2,6 +2,7 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
+53
View File
@@ -2,6 +2,7 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
@@ -33,6 +34,8 @@ service ModelsService {
rpc refreshBasetenModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Fetches available models from SAP AI Core
rpc getSapAiCoreModels(SapAiCoreModelsRequest) returns (SapAiCoreModelsResponse);
// Fetches available models from OCA
rpc refreshOcaModels(StringRequest) returns (OcaCompatibleModelInfo);
}
// List of VS Code LM models
@@ -127,6 +130,48 @@ message UpdateApiConfigurationRequest {
ModelsApiConfiguration api_configuration = 2;
}
// Model info for OCA (OpenAI-compatible) models exposed by the OCA provider
message OcaModelInfo {
// Maximum completion tokens per request supported by this model
optional int64 max_tokens = 1;
// Total context window in tokens (input + output)
optional int64 context_window = 2;
// Whether the model supports image inputs
optional bool supports_images = 3;
// Whether prompt caching is supported for this model
bool supports_prompt_cache = 4;
// Price per million input tokens (USD unless otherwise specified by provider)
optional double input_price = 5;
// Price per million output tokens (USD unless otherwise specified by provider)
optional double output_price = 6;
// Thinking/reasoning configuration if the model supports it
optional ThinkingConfig thinking_config = 7;
// Price per million tokens for prompt cache writes
optional double cache_writes_price = 9;
// Price per million tokens for prompt cache reads
optional double cache_reads_price = 10;
// Human-readable model description
optional string description = 11;
// Recommended default temperature for this model
optional double temperature = 13;
// Optional survey content to display in the UI
optional string survey_content = 14;
// Identifier for the survey associated with this model
optional string survey_id = 15;
// Optional banner content (e.g., deprecation or promotion notes)
optional string banner = 16;
// Canonical model identifier as reported by OCA
string model_name = 17;
}
// Aggregated OCA model catalog keyed by model identifier
message OcaCompatibleModelInfo {
// key: canonical model id as reported by OCA (e.g., "openai/gpt-4o-mini")
// value: OcaModelInfo describing that model
map<string, OcaModelInfo> models = 1;
optional string error = 2;
}
// API Provider enumeration
enum ApiProvider {
ANTHROPIC = 0;
@@ -164,6 +209,7 @@ enum ApiProvider {
VERCEL_AI_GATEWAY = 32;
QWEN_CODE = 33;
DIFY = 34;
OCA = 35;
}
// Model info for OpenAI-compatible models
@@ -276,6 +322,9 @@ message ModelsApiConfiguration {
optional string qwen_code_oauth_path = 70;
optional string dify_api_key = 71;
optional string dify_base_url = 72;
optional string oca_base_url = 73;
optional string oca_api_key = 74;
optional string oca_refresh_token = 75;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
@@ -309,6 +358,8 @@ message ModelsApiConfiguration {
optional OpenRouterModelInfo plan_mode_baseten_model_info = 128;
optional string plan_mode_vercel_ai_gateway_model_id = 129;
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130;
optional string plan_mode_oca_model_id = 131;
optional OcaModelInfo plan_mode_oca_model_info = 132;
// Act mode configurations
@@ -343,4 +394,6 @@ message ModelsApiConfiguration {
optional OpenRouterModelInfo act_mode_baseten_model_info = 228;
optional string act_mode_vercel_ai_gateway_model_id = 229;
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230;
optional string act_mode_oca_model_id = 231;
optional OcaModelInfo act_mode_oca_model_info = 232;
}
+37
View File
@@ -0,0 +1,37 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
// Service for account-related operations
service OcaAccountService {
// Handles the user clicking the login link in the UI.
// Generates a secure nonce for state validation, stores it in secrets,
// and opens the authentication URL in the external browser.
rpc ocaAccountLoginClicked(EmptyRequest) returns (String);
// Handles the user clicking the logout button in the UI.
// Clears API keys and user state.
rpc ocaAccountLogoutClicked(EmptyRequest) returns (Empty);
// Subscribe to auth status update events (when authentication state changes)
rpc ocaSubscribeToAuthStatusUpdate(EmptyRequest)
returns (stream OcaAuthState);
}
message OcaAuthState {
optional OcaUserInfo user = 1;
optional string api_key = 2;
}
// User's information
message OcaUserInfo {
string uid = 1;
optional string display_name = 2;
optional string email = 3;
}
+1
View File
@@ -2,6 +2,7 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
+26 -1
View File
@@ -2,6 +2,7 @@ syntax = "proto3";
package cline;
import "cline/common.proto";
import "cline/models.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
@@ -18,8 +19,15 @@ service StateService {
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
rpc updateInfoBannerVersion(Int64Request) returns (Empty);
rpc updateModelBannerVersion(Int64Request) returns (Empty);
rpc getProcessInfo(EmptyRequest) returns (ProcessInfo);
}
message DictationSettings {
bool feature_enabled = 1;
bool dictation_enabled = 2;
string dictation_language = 3;
}
message State {
string state_json = 1;
}
@@ -140,6 +148,9 @@ message UpdateSettingsRequest {
optional BrowserSettingsUpdate browser_settings = 20;
optional string default_terminal_profile = 21;
optional bool yolo_mode_toggled = 22;
optional DictationSettings dictation_settings = 23;
optional int32 auto_condense_threshold = 24;
optional bool multi_root_enabled = 25;
}
// Complete API Configuration message
@@ -210,6 +221,9 @@ message ApiConfiguration {
optional string qwen_code_oauth_path = 63;
optional string dify_api_key = 64;
optional string dify_base_url = 65;
optional string oca_base_url = 66;
optional string oca_api_key = 67;
optional string oca_refresh_token = 68;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
@@ -236,6 +250,8 @@ message ApiConfiguration {
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 121;
optional string plan_mode_vercel_ai_gateway_model_id = 122;
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 123;
optional string plan_mode_oca_model_id = 124;
optional OcaModelInfo plan_mode_oca_model_info = 125;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
@@ -262,6 +278,8 @@ message ApiConfiguration {
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 221;
optional string act_mode_vercel_ai_gateway_model_id = 222;
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 223;
optional string act_mode_oca_model_id = 224;
optional OcaModelInfo act_mode_oca_model_info = 225;
// Extension fields for Bedrock Api Keys
optional string aws_authentication = 301;
@@ -287,3 +305,10 @@ message Viewport {
message UpdateTerminalConnectionTimeoutResponse {
optional int32 timeout_ms = 1;
}
message ProcessInfo {
int32 process_id = 1;
optional string version = 2;
optional int64 uptime_ms = 3;
}

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