Compare commits

...

172 Commits

Author SHA1 Message Date
CandiedUniverse 452733c3fd Release changeset PR (#9528)
* Update package.json and package-lock.json version numbers for patch release

* Patch fix changeset PR

* update cli package.json

* fixup! Patch fix changeset PR

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-24 14:10:39 -08:00
Juan Pablo Flores 7fadcfaa3f feat: add thinking to MiniMax M2.5 and add the M2.5-highspeed model to MiniMax provider (#9394)
* feat: add MiniMax M2.5 model to MiniMax provider

- Add MiniMax-M2.5 to minimaxModels with 192K context, 128K max tokens,
  prompt caching, and reasoning/thinking support
- Update minimaxDefaultModelId to MiniMax-M2.5
- Add minimax/minimax-m2.5 to OpenRouter prompt caching switch

Closes #9391

* fix: add temperature: 1 to MiniMax M2.5 for reasoning support

* docs: update MiniMax provider docs with M2.5 model

* feat: wire up thinking/reasoning support for MiniMax M2.5

- Pass thinkingBudgetTokens from factory to MinimaxHandler
- Use thinking param in API call when reasoning is enabled
- Disable temperature and forced tool_choice when thinking is on
- Add ThinkingBudgetSlider to MiniMaxProvider UI for M2.5

* Add MiniMax-M2.5-highspeed

* Add thinking for highspeed

* Refactor thinking logic

---------

Co-authored-by: BarreiroT <tomasmbarreiroi@gmail.com>
Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
2026-02-24 22:24:41 +01:00
Tomás Barreiro 9989225b69 Parse CLINE_OTEL_EXPORTER_OTLP_HEADERS headers and force the build constant ones (#9534) 2026-02-24 22:07:55 +01:00
Ara 8a73f63189 feat: update recommended model from GPT-5.2 Codex to GPT-5.3 Codex (#9533)
- Update model ID and name from gpt-5.2-codex to gpt-5.3-codex
- Change tag from "HOT" to "NEW" for the updated model
- Add What's New banner entry promoting Codex 5.3 availability
2026-02-24 12:49:20 -08:00
Ara 31e8c85f0a Remove voice mode UI and disable dictation (#9511)
* remove voice mode UI and disable dictation flags

* remove legacy dictation settings path and dead voice recorder

* remove dictation feature stack and state/proto hooks
2026-02-24 12:26:15 -08:00
Bee 5b9916866d fix: remove placeholder tools from final native tool list (#9499)
This commit ensures that internal placeholder tools, specifically `focus_chain`, are filtered out from the final list of native tools exposed to the LLM.

- Added a test case in `PromptRegistry.test.ts` to verify `focus_chain` is excluded from native tools output.
- Updated snapshot files for various models (OpenAI GPT-5, Vertex Gemini 3, etc.) to reflect the removal of the `focus_chain` tool definition.
2026-02-24 12:07:55 -08:00
Tomás Barreiro f001e735f8 Add isLocatedInPath tests (#9526)
* Add isLocatedInPath tests

* Add another test case
2026-02-24 19:22:55 +01:00
Tomás Barreiro 32893ee343 Fix OpenAI Codex by setting Store to false (#9523) 2026-02-24 18:28:32 +01:00
Raushan Singh 0d4e47e5c3 fix: use isLocatedInPath() instead of string.includes() for path containment check (#9519)
Fixes false positives in getReadablePath() when directories share a prefix
(e.g., /home/user/project matching /home/user/project-backup). The existing
isLocatedInPath() function correctly handles path boundaries using path.relative().

Closes #8761

Co-authored-by: Raushan Singh <raushrak@Raushans-MacBook-Air.local>
2026-02-24 18:23:29 +01:00
Max c1a43482e7 sdk lib (#9259)
* sdk lib

* improve cline sdk api surface

- better api design and messages

* fix some types, fix session id retrieval, improve wording

* hide controller from sdk surface completely

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-23 22:30:54 -08:00
Bee ea65383e16 chore: replace baseUrl with explicit relative paths in tsconfig files (#9508)
* chore: replace baseUrl with explicit relative paths in tsconfig files

Remove `baseUrl: "."` from tsconfig configurations and update all path aliases to use explicit relative paths (e.g., `./src/*` instead of `src/*`). This makes path resolution more explicit and avoids potential ambiguity in module resolution across the main project and webview-ui configurations.

* update package-lock.json
2026-02-23 18:00:09 -08:00
Ara b97d1487a7 Release V3.67.0 (#9509)
* Release V3.67.0

Bump version from 3.66.0 to 3.67.0 in package.json and
package-lock.json. Add changelog entry for v3.67.0 covering new
features (subagent skills, AgentConfigLoader, Responses API, websocket
preconnect, CLI /q command), bug fixes (reasoning delta crash, OpenAI
tool ID, auth checks, Gemini 3.1 Pro), and other changes. Update
WhatsNewItems fallback banners to reflect current promotions.

* Fixing stuff
2026-02-23 17:25:30 -08:00
Robin Newhouse 091cf945e4 Move PR skill to .agents/skills (#9505)
* Move PR skill to .agents/skills and add changeset guidance

* Remove changeset guidance from PR skill
2026-02-23 16:31:32 -08:00
Bee df4f551ba7 feat: add support for skills and optional modelId in subagent config (ENG-1564) (#9502)
* refactor: consolidate subagent request usage tracking into state object

Replace scattered per-request token tracking variables with a structured
`SubagentUsageState` interface containing `currentRequest` and
`lastRequest` states. This improves code organization by grouping related
token metrics (input, output, cache write/read, total tokens, cost) into
a cohesive `SubagentRequestUsageState` object, reducing variable sprawl
and making the usage lifecycle (current → last) more explicit.

* feat(subagent): add support for skills and optional modelId in agent config

- Update `AgentBaseConfigSchema` and `AgentConfigFrontmatterSchema` to include an optional `skills` field and make `modelId` optional.
- Implement `parseSkills` and `normalizeSkillName` in `AgentConfigLoader` to handle skill parsing from YAML frontmatter.
- Update `SubagentBuilder` to provide access to configured skills.
- Modify `SubagentRunner` to filter available skills based on the agent's configuration, falling back to all available skills if none are specified.
- Update host retrieval to use `HostRegistryInfo` instead of `HostProvider`.

This allows subagents to be restricted to specific skills and provides more flexibility in model configuration.

* update unit test
2026-02-23 16:23:36 -08:00
Ara 88da4ddf89 feat: fetch featured models from backend with local fallback (#9495)
* feat(cli): fetch featured models from backend with local fallback

- Add async getFeaturedModelsForCline() to fetch models via controller
- Load featured models dynamically in AuthView with useEffect
- Update FeaturedModelPicker to accept featuredModels as optional prop
- Refactor helper functions to accept models parameter for flexibility
- Keep local hardcoded models as fallback when backend fetch fails

* Fixing stuff

* Fixing stuff

* Fixing stuff
2026-02-23 16:13:51 -08:00
CandiedUniverse 93eb607e6d Remove all traces of changeset-converter.yml GitHub Action and npm run changeset (#9506) 2026-02-23 15:53:56 -08:00
Tony Loehr 810b5b78f5 added mcp enterprise configuration details (#9501)
Co-authored-by: Juan Pablo Flores <juan@cline.bot>
2026-02-23 15:51:05 -08:00
Tony Loehr 38ea422f6a Sso video (#9496)
* docs: add CVE scan sample to navigation, fix accordion labels, clarify --yolo flag

* added sso video

* Remove CVE scanner changes from SSO video PR
2026-02-23 15:29:48 -08:00
Robin Newhouse a7a35c0138 ci: add automatic retries for smoke test jobs (#9503) 2026-02-23 14:55:38 -08:00
Bee 01547ba1f4 feat: add AgentConfigLoader for file-based agent configs (ENG-1547) (#9245)
* refactor: centralize tool handler registration and filter by allowed tools

- Create centralized toolHandlersMap for all tool handler instantiation
- Add registerToolHandlers method to register only allowed tools from config
- Filter subagent tools to only include allowed tools from allowedTools config
- Remove scattered tool handler registration logic in favor of single source of truth
- Improve maintainability by consolidating tool handler creation in one place

This refactoring ensures subagents only have access to explicitly allowed tools
and makes the tool registration process more maintainable and consistent.

It also improves separation of concerns by having the coordinator manage all tool handler registration, while the executor focuses on orchestration. The allowedTools parameter enables runtime filtering of available tools for different contexts.

Also update PromptRegistry to load synchronous and simplify variant lookup

- Convert async load() to synchronous, called in constructor as both loadVariants and loadComponents are not async functions
- Remove health check logic and loaded state tracking
- Extract getVariant() method with proper generic fallback
- Add getComponents() accessor and simplify component loading
- Convert variant/component loaders from async to synchronous
- Remove unnecessary await calls throughout the codebase
- Add PromptRegistry tests for variant resolution and components

* fix test

* feat: add AgentConfigLoader for file-based agent configs

Add AgentConfigLoader singleton to manage agent configurations loaded from
YAML files in the agents directory. Supports hot-reloading via file watcher,
validates config schema with Zod, and integrates with extension lifecycle
(StateManager initialization and tearDown disposal).

* add tests

* add missing export

* update tests

* feat(tools): implement dynamic tool registration for subagents

Updates the tool system to support dynamically registered subagents as individual tools.

- Modifies `ClineToolSet` to generate specific tool definitions for configured subagents via `AgentConfigLoader`.
- Updates `parseAssistantMessageV2` to use `getToolUseNames()` instead of a static list, enabling the parser to recognize dynamic tool tags.
- Replaces the generic `USE_SUBAGENTS` tool with specific subagent instances when available in the system prompt context.

* update config path and refine tool descriptions

- Relocate the subagent configuration directory from `~/.cline/data/agents` to `~/Documents/Cline/Agents` to improve user accessibility.
- Update subagent tool descriptions and parameter instructions in the system prompt to be more descriptive and helpful for the model.

* revert unrelated changes

* revert unrelated changes

* update unit test

* fix: await AgentConfigLoader initialization before StateManager completes

Ensure agent configs are fully loaded during StateManager initialization
by awaiting the `ready()` promise. Previously, `AgentConfigLoader` was
instantiated without waiting for the initial load to complete, causing
potential race conditions where configs might not be available when
needed.

- Add `initialLoadPromise` field to track the async initial load
- Expose a `ready()` method to allow callers to await initialization
- Await `AgentConfigLoader.getInstance().ready()` in StateManager

* set previousRequestTotalTokens
2026-02-23 13:26:41 -08:00
Robin Newhouse 7f1632f09f fix: prevent reasoning delta crash on usage-only stream chunks (#9432)
* fix: guard missing delta in reasoning streams

* test: isolate litellm prompt cache call-count assertions
2026-02-23 12:29:34 -08:00
Han Wang 1b0ab3d01b sambanova provider: update models list (#9479)
* Add changeset

* Allow temperature config

* update issues summary

* Update SambaNova docs

* Update list of sambanova models

* Update minimax m2.5

* remove 2 models

* Remove residual
2026-02-23 11:27:36 -08:00
Chaitanya Eranki dce0902596 Oca Messages API implementation for new Claude Models (#9447)
* Made messages api changes

* Made changes

* Added changes

* Made maxTokens point to the right thing

* Removed deprecated max_tokens field

* Reverted the change

* Making an additional change to not cause any issues with chat completions

* reverting changes so we can make them in the backend

* Added changeset

* Fixed changes based on AI comments
2026-02-23 11:20:05 -08:00
Bee 9e7a30bd34 feat: preconnect websocket to reduce response latency (#9458)
Warm up the OpenAI WebSocket connection early in WebSocket mode to avoid handshake latency on the first response.create call. This introduces a responsesWsReadyPromise to track the connection state and prevent duplicate connection attempts while the initial connection is in flight.
2026-02-23 11:17:03 -08:00
Bee 2b1b1d1cf2 fix: restrict OpenAI tool ID transformation to native provider (#9459)
* fix: restrict OpenAI tool ID transformation to native provider

Update `convertToOpenAiMessages` and `transformToolCallId` to only apply tool ID transformations when the provider is explicitly set to `openai-native`. This prevents unintended ID modifications for other providers (like OpenRouter or local LLMs) that use the OpenAI format but may have different tool ID requirements or already provide compatible IDs.

* update tests

* transformToolCallIdForNativeApi
2026-02-23 11:16:55 -08:00
Max fcf3792f63 fix auth check for acp mode (#9491)
- acp code wasn't using the proper 'isAuthConfigured' method for
checking auth status

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-23 09:41:56 -08:00
shey-cline 0e833ade82 Add /q command to quit CLI (#9400)
* init

* changeset

* Apply suggestion from @greptile-apps[bot]

oops

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* add tests

* add /q info to help panel

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-23 07:37:34 -08:00
Ara 4455db5198 Pull Cline's recommended from internal endpoint (#9376)
* feat(cline): fetch recommended models from API endpoint

* Adding 1m

* Adding 1m

* Adding 1m

* fix: harden model tag label handling and tab init

* fix(models): add retry-safe fetch, id canonicalization, and shared filtering

* chore: trigger PR head refresh

* refactor(models): remove canonical alias map for OpenRouter IDs

* refactor(webview): remove redundant cline fetch on mount

* Adding 1m
2026-02-22 22:28:55 -08:00
Bee 03ab2968a6 feat: responses api for openai native provider (#9411)
* fix: openai native provider token usage mapping

- Add `store` parameter support to OpenAI native provider options to allow persisting completions.
- Fix incorrect mapping of `cached_tokens` and `reasoning_tokens` in usage statistics.
- Include `thoughtsTokenCount` in the final usage report to track reasoning model performance and costs.

* feat: add websocket support for OpenAI Responses API

This commit introduces WebSocket support for the OpenAI Native provider's Responses API, providing an alternative to the standard HTTP streaming.

- Implement `createResponseStreamWebsocket` in `OpenAiNativeHandler` with a fallback to HTTP on failure.
- Refactor `OpenAiNativeHandler` to modularize tool mapping and parameter construction for the Responses API.
- Update `OcaHandler` to explicitly disable `previousResponseId` when using the Responses API and add validation for model information.
- Integrate `undici` WebSocket for better compatibility in the extension environment.

* disablePreviousResponseId

* feat: add timestamp to conversation messages for response chaining

Add `ts` field to `ClineStorageMessage` to track when messages were
created. Use this timestamp to enforce a 23-hour validity window when
chaining OpenAI responses via `previousResponseId`, since the API only
retains responses for 24 hours. Also fix non-null assertion operators
in tests to use optional chaining for safer access.

* add OpenAI Responses Websocket Mode ApiFormat support

- Add `OPENAI_RESPONSES_WEBSOCKET_MODE` to the `ApiFormat` enum in proto definitions.
- Update `OpenAiNativeHandler` to use the new API format for determining when to use websocket mode, replacing previous environment-based logic.
- Refactor tool mapping for OpenAI Responses to support strict mode and correctly handle null parameters.
- Ensure the `store` option is disabled when `previous_response_id` is present in websocket mode.
- Bump version to 2.4.1 and update package dependencies.

* use abortController

* add support for websocket mode to openai-codex

* set behind feature flag
2026-02-20 17:17:54 -08:00
Max a0d52d4d59 cli yolo mode should not persist yolo setting to disk ever (#9370)
- added a method to StateManager, setSessionOverride, which overrides
state settings while the statemanager lives in memory

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-20 11:16:29 -08:00
Max 75fbeb4aad fix cline auth with acp flag (#9405)
- was missing a check for "cline:clineAccountId" in the isAuthed method
of acp agent

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-20 11:15:45 -08:00
Bee 70db6bde34 fix: inline focus-chain slider within its feature row (#9444)
* fix: inline focus-chain slider within its feature row

Moves the focus-chain reminder interval `SettingsSlider` from a
standalone element rendered after all experimental feature rows to
being rendered directly beneath the focus-chain `FeatureRow`. The
slider now renders conditionally when `feature.id === "focus-chain"`
and the feature is enabled, improving UI cohesion and making the
relationship between the toggle and its configuration more explicit.

Additionally:
- Relocates focus-chain from `experimentalFeatures` to `agentFeatures`
- Removes the `isExperimental` prop and "Experimental:" label badge
  from `FeatureRow` and related feature toggle definitions
- Simplifies `SettingsSlider` markup by removing the wrapper card
  styling, making it suitable for inline embedding
  - Removes unused line from common.ts

* nestedKey
2026-02-20 10:58:46 -08:00
Saoud Rizwan 02c2601e0e fix(gemini): add 3.1 pro while keeping 3.0 compatibility (#9438) 2026-02-20 09:18:40 -08:00
Juan Pablo Flores 94692b5091 docs: add MCP support documentation for Cline CLI (#9390) 2026-02-20 09:39:02 -06:00
Robin Newhouse a2794c680f fix(evals): restore missing smoke eval npm scripts (#9429) 2026-02-19 14:49:27 -08:00
CandiedUniverse 6d3f8e1d5d fix(release-eng): Pin VSCode nightly build to node version 22 (#9423)
* fix(release-eng): Pin VSCode nightlybuild to node version 22

* fix(release-eng): Pin publish.yml GitHub workflow to node version 22
2026-02-19 12:04:14 -08:00
cryptoque 3e5847890b feat: add dynamic flag to adjust banner cache duration (#9421) 2026-02-19 11:03:50 -08:00
CandiedUniverse 0eab54ab12 fix(release-eng): Fix nightly extension publish failure caused by workspace self-link mismatch (#9420) 2026-02-19 10:40:45 -08:00
github-actions[bot] 7fa0a4924b Changeset version bump (#9413)
* changeset version bump

* Updating CHANGELOG.md format

* Adding 1m

* Adding 1m

* Adding 1m

* Adding 1m

---------

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: Arafatkatze <arafat.da.khan@gmail.com>
2026-02-19 10:36:21 -08:00
Junjie Tang 8680218e0e Update sap-ai-sdk version (#9417) 2026-02-19 06:46:09 -08:00
Dominic Cooney 8787ab35b9 Update package-lock.json. (#9414) 2026-02-18 22:56:49 -08:00
Dominic Cooney 6ed3944f04 Export global, workspace and secrets from VSCode to share with CLI (#9227)
Caveat: Task history, tasks, etc. are not written to the same place by all clients yet. This is just about globalState, workspaceState and secrets.
2026-02-19 15:46:23 +09:00
Bee 1dd8e763d1 fix(chat): make Cmd/Ctrl+A select-all deterministic (#9408)
Handle plain Cmd/Ctrl+A directly in `ChatTextArea` keydown to force
textarea-wide selection via `setSelectionRange`, while preventing default
and propagation. This avoids intermittent failures caused by global shortcut
listener races, and keeps Cmd/Ctrl+Shift+A behavior unchanged.fix(chat): make Cmd/Ctrl+A select-all deterministic

Handle plain Cmd/Ctrl+A directly in `ChatTextArea` keydown to force
textarea-wide selection via `setSelectionRange`, while preventing default
and propagation. This avoids intermittent failures caused by global shortcut
listener races, and keeps Cmd/Ctrl+Shift+A behavior unchanged.
2026-02-18 20:55:57 -08:00
Bee 28e6297769 fix: flaky Cancel behavior by preventing duplicate cancel actions (CLINE-1380) (#9409)
* fix: flaky Cancel behavior by preventing duplicate cancel actions

This PR fixes chat cancel behavior where users sometimes had to click Cancel multiple times, and repeated clicks could accidentally transition into Resume/restart behavior.

* move to finally
2026-02-18 20:55:41 -08:00
Bee 5a2a5d1c0a refactor: replace non-null assertions with checks in PatchParser (#9402)
* refactor: replace non-null assertions with safe null checks in PatchParser

Replace all forbidden non-null assertions (`!`) in PatchParser.ts with
safe alternatives using optional chaining (`?.`) and nullish coalescing
(`?? ""`/`?? 0`). Also refactor the Levenshtein distance matrix from a
2D array to a flat array to eliminate index-based non-null assertions,
improving type safety and code robustness.

No feature behavior changes.

* simplify Levenshtein matrix indexing

Initialize the distance matrix with zeroes and add `at`/`set` helpers for flat-array access in `levenshteinDistance`.

This removes repeated index math and nullish fallbacks, making the algorithm easier to read while keeping bounds-safe access and identical behavior.refactor(patch-parser): simplify Levenshtein matrix indexing

Initialize the distance matrix with zeroes and add `at`/`set` helpers for flat-array access in `levenshteinDistance`.

This removes repeated index math and nullish fallbacks, making the algorithm easier to read while keeping bounds-safe access and identical behavior.
2026-02-18 19:35:48 -08:00
Max 113039a259 CLI 2.0: allow custom inference profile arn for bedrock provider (#9271) 2026-02-18 19:16:32 -08:00
Max 4023c18257 remove default timeout (#9401) 2026-02-18 17:28:50 -08:00
cryptoque 7c95b53892 Add support for DB backed Welcome Banner (#9315)
* feat: add welcome banner support from backend

* make DB banner format conform with existing banners

* add support for welcome banner actions

* remove debugging helper that bypass dismissal, dismissal should work again

* undo changes to make welcome banner always appear during debugging

* remove console logs for debugging

* clean up bannerservice

* clean up welcomesection.tsx

* add new tests for ide type filtering

* add welcome banner own feature flag and conditionally display between hard coded welcome banner and DB backed ones

* turn on welcome banner flag locally by default

* close welcome banners when clicking on actions

* apply bot review suggestion, fix memory leak

* address feedback: use p without span

* split welcome banners into a seperate component to keep whatsnewmodal clean

* get action through api schema instead of extractin it from rules_json

* use only bannerWaitTimeoutRef, remove waitingForBannersRef

* resolve new merge conflict

* linter

* cerebra
2026-02-18 14:52:02 -08:00
Bee 9fd2b99be4 chore: remove autoCondenseThreshold setting and related code (#9396)
- Remove `auto_condense_threshold` from `Settings` and `UpdateSettingsRequest` in `state.proto`.
- Remove `autoCondenseThreshold` from `ApiProviderInfo` interface.
- Update `generate-state-proto.mjs` to remove double field handling and improve integer parsing.
- Add error handling to `ContextManager` when parsing previous request JSON to prevent crashes on malformed data.
2026-02-18 12:43:30 -08:00
github-actions[bot] 7c782abaf4 Changeset version bump (#9364)
* changeset version bump

* Updating CHANGELOG.md format

* update package versions

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-18 12:22:53 -08:00
Ara 3a14a88f4f fix: include root workspace for changesets release (#9393) 2026-02-18 11:40:02 -08:00
Saoud Rizwan 28d60a83a4 fix(models): keep Sonnet 4.5 as default for now (#9389)
* fix(models): keep Sonnet 4.5 as default

* chore(changeset): add release note for Sonnet 4.5 default

* fix(models): remove Sonnet 4.6 from curated model lists

* fix(models): restore Sonnet 4.6 in web recommended list
2026-02-18 10:53:07 -08:00
Saoud Rizwan ae6468b161 fix(models): reinstate minimax m2.5 free promo surfaces (#9387) 2026-02-18 10:28:54 -08:00
Saoud Rizwan af93e31862 feat(models): make sonnet 4.6 default and remove free promo positioning (#9377) 2026-02-18 10:07:20 -08:00
Tomás Barreiro ca154eb8f5 Add MiniMax M2.5 to the MiniMax provider (#9381)
* Add MiniMax M2.5 to the MiniMax provider

* Add changeset

* Update default model
2026-02-18 09:16:23 -08:00
Tomás Barreiro 1d8497c6bf Prevent error messages when displaying featured models in the CLI (#9379)
* Fix the featured models key

* Add changeset
2026-02-18 16:51:29 +01:00
Seb Duerr 5871fd02b1 feat(cerebras): remove deprecated llama-3.3-70b and qwen-3-32b models (#9345)
* feat(cerebras): remove deprecated llama-3.3-70b and qwen-3-32b models

These models have been deprecated from the Cerebras inference platform.

- Remove llama-3.3-70b and qwen-3-32b from cerebrasModels in api.ts
- Update supported models documentation in cerebras.mdx
- Add changeset for the deprecation

* fix: remove stale llama-3.3-70b and qwen-3-32b references from rate limits

Remove dead switch cases in getRateLimits() that referenced deprecated models
no longer present in cerebrasModels.
2026-02-17 23:39:38 -08:00
alex-lum 2d81c310d2 Alex/inf 413 bug no telemetry for versions greater than 20 (#9372) 2026-02-17 18:55:07 -08:00
Robin Newhouse 0c691f72d2 feat(cli): add /skills slash command (#9089)
* feat(cli): add /skills slash command for managing skills

- Add /skills to CLI_ONLY_COMMANDS in slashCommands.ts
- Create SkillsPanelContent component with:
  - Display global and workspace skills with toggle indicators
  - Enter to use skill (inserts @path into input)
  - Space to toggle skill enabled/disabled
  - Selectable marketplace link to skills.sh
  - Keyboard navigation with arrow keys and vim keys
- Wire up panel in ChatView.tsx
- Add comprehensive tests for keyboard interactions

* refactor(cli): use static skill controller imports

* fix(cli): add React import to skills panel test

* fix(cli): suppress required React import lint in skills test

* fix(cli): harden /skills panel interactions

Revert optimistic skill toggle state when persistence fails, and surface a fallback URL when opening the marketplace fails. Also tighten and extend tests to verify exact marketplace URL handling and rollback behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 17:13:51 -08:00
Robin Newhouse a8409137b3 fix: disable click-to-set auto-condense threshold and hardcode default (#9348)
* fix: disable click-to-set auto-condense threshold and hardcode default

Clicking anywhere on the context window progress bar silently set
autoCondenseThreshold to a value based on click position (e.g. 0.05),
persisting in globalState. This caused compaction to fire at ~10K tokens
instead of the intended ~150K, resulting in ~20 context resets per task.

- Comment out click and keyboard handlers on progress bar (keep components
  for future release with proper UX)
- Hardcode threshold to 0.75 default, ignoring corrupted stored values

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: add shouldCompactContextWindow unit tests

Cover threshold math including the accidental low-threshold bug case,
undefined/zero fallbacks, cache token inclusion, and maxAllowedSize cap.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: hardcode autoCondenseThreshold in all remaining callsites

Address Greptile review: SubagentRunner.ts, task/index.ts display
logic, and controller/index.ts webview state all still read the
corrupted value from globalState. Hardcode 0.75 everywhere.

Co-authored-by: Cursor <cursoragent@cursor.com>

* style: remove unnecessary union type on hardcoded threshold

Drop `number | undefined` annotation from the hardcoded 0.75 literal
in SubagentRunner.ts per Greptile review feedback.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: use SETTINGS_DEFAULTS constant, remove commented-out code, clarify test

- Replace hardcoded 0.75 with SETTINGS_DEFAULTS.autoCondenseThreshold
  across all 4 callsites for a single source of truth
- Delete commented-out click/keyboard handlers in ContextWindow.tsx,
  replace with TODO referencing PR #9348
- Make bug-case test self-documenting by deriving token values from
  the threshold calculation

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 16:32:13 -08:00
Saoud Rizwan 34a21b8e26 docs: add security policy (#9365) 2026-02-17 16:00:01 -08:00
ClineXDiego eb9f53edf4 fix: smarter retry for write_to_file missing content parameter (#9276)
* fix: smarter retry for write_to_file missing content parameter (#7998)

Replace generic 'missing parameter' error with progressive guidance when
write_to_file fails due to empty content parameter. This breaks the
infinite retry loop where the model repeatedly attempts the same
write_to_file call that exceeds output token limits.

Changes:
- Add writeToFileMissingContentError() to formatResponse with 3 tiers:
  1st failure: suggestions (use skeleton + replace_in_file)
  2nd failure: strong directive (stop retrying write_to_file)
  3rd+ failure: CRITICAL stop, forces alternative strategies
- Add context window awareness: warns model when >50% context used
- Add getContextUsagePercent() helper to WriteToFileToolHandler
- Add 22 unit tests covering progressive escalation and context awareness

Fixes #7998

* add changeset for write_to_file retry fix

* refactor: simplify write_to_file error handling per review

- Simplify writeToFileMissingContentError to single-tier error following
  existing diffError pattern (no progressive escalation)
- Use shared getLastApiReqTotalTokens() for context window awareness
- Remove private getContextUsagePercent() method from handler
- Add proactive skeleton + replace_in_file guidance to write_to_file
  tool description for all variants
- Simplify tests to match new API (11 tests)

* test: update system prompt snapshots

* chore: revert write_to_file prompt guidance

* feat: restore progressive 3-tier guidance for write_to_file missing content

Restore the progressive escalation that was removed in dd3c12d4e:
- Tier 1 (1st failure): Gentle suggestions (skeleton + replace_in_file)
- Tier 2 (2nd failure): Strong directive, 'Do NOT attempt full write again'
- Tier 3 (3rd+ failure): CRITICAL stop, forces alternative strategies
- Context window warning when >50% full
- Dynamic UI message: 'Retrying...' vs 'multiple times — different approach'
- 21 tests covering all tiers and context awareness

* nit: extract context window warning threshold to named constant

Also replace emoji with plain text in warning message for consistency.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Robin Newhouse <robin@cline.bot>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 19:08:42 -03:00
github-actions[bot] c60f18d907 Adding 1m (#9346)
Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-02-17 11:11:54 -08:00
Saoud Rizwan 36c68a6ab9 fix: remove expired MiniMax free promo surfaces (#9361)
* fix: remove expired MiniMax free promo surfaces

* fix: remove MiniMax M2.5 from recommended models

* chore: update GLM 5 whats-new promo wording
2026-02-17 10:57:19 -08:00
Saoud Rizwan 80dfce0f60 docs: remove stale Claude 5 mention from Auto Compact docs (#9360)
* docs: remove stale Claude 5 wording from auto compact docs

* Remove stale Claude 5 mention from docs
2026-02-17 10:48:23 -08:00
Saoud Rizwan 955ae2f62f feat: add Claude Sonnet 4.6 support and surface it as free (#9356)
* feat: add Sonnet 5 support and make it default across surfaces

* feat: surface Sonnet 5 as free while keeping Sonnet 4.5 defaults

* fix: rename Sonnet 5 support to Sonnet 4.6 across providers and UI

* fix: allow duplicate onboarding model ids across free and frontier

* chore: update Sonnet 4.6 banner to limited-time free messaging

* fix: align Bedrock Sonnet 4.6 model ids with AWS format

* feat: update whats new promo to Sonnet 4.6 free offer

* chore: update Sonnet 4.6 promo copy and timing
2026-02-17 10:43:24 -08:00
Dominic Cooney 8cb0c6d236 Fix e2e tests. (#9350) 2026-02-17 15:06:40 +09:00
github-actions[bot] bb05b2f7b0 changeset version bump (#9316)
Updating CHANGELOG.md format

update changelog

update banner and bump version

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-16 12:46:01 -08:00
alex-lum 1a911f4232 fix(telemetry): refresh OTEL org attributes on identify (#9318)
* fix(telemetry): refresh OTEL user/org attributes on every identify

* fix(telemetry): refresh OTEL user/org attributes on every identify

* test(telemetry): cover OTEL identifyUser org refresh scenarios

* refactor(telemetry): rename member_roles to member_role (singular)

* Apply suggestion from @BarreiroT

simpler commenting

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

* removing verbose comments

/**
 * Helper to build a ClineAccountUserInfo with an active organization.
 */

* removing verbose comments

* removing unnecessary logger

* assert -> chai expect

---------

Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
2026-02-16 12:45:31 -08:00
Ara 1cfd560921 feat(cline): add z-ai/glm-5 to free model recommendations (#9341)
* feat: add z-ai/glm-5 to free models list

Include Z.AI's GLM 5 in the free model whitelist for zero-cost usage
and update the model picker UI to display the free label.

* Adding thinking

* Adding thinking

* Adding thinking
2026-02-16 11:59:09 -08:00
Juan Pablo Flores 203ff8f549 docs: enrich Quick Start guide with more context for new users (#9338)
* docs: enrich Quick Start guide with more context for new users

* docs: add Antigravity to full installation guide
2026-02-16 09:00:50 -08:00
Saoud Rizwan e920f1de02 fix(chat): keep reasoning visible before low-stakes tool groups (#9335)
* fix(chat): keep reasoning visible before low-stakes tool groups

* chore(changeset): add patch note for tool-group reasoning visibility

* fix(chat): keep thinking loader visibility aligned with waiting states

* fix(chat): avoid clipping descenders in thinking label
2026-02-15 17:14:41 -08:00
Saoud Rizwan b266475d0a fix(chat): prevent partial row churn during native tool arg streaming (#9334)
* fix(chat): prevent partial text flicker during native tool streaming

* fix(chat): revert act mode partial dedupe change
2026-02-15 15:40:54 -08:00
Saoud Rizwan 402361c482 fix(chat): restore reasoning traces and polish thinking UX (#9330)
* fix(chat): restore reasoning traces and polish thinking UX

* chore(changeset): add patch note for reasoning trace UX fixes
2026-02-15 01:19:59 -08:00
Renee Huang e884699d24 New Cline Docs (#9280)
* only doc changes

* merge

* fix installtion page redirects

* fix redirect, remove unused parts

* rm irrelevant info

* clean up terminal guides

* docs: add home page and reorganize navigation

* chore: remove 71 unused docs files not referenced in navigation

Remove .mdx files that are no longer referenced in docs.json navigation
and only existed as stale content from previous documentation restructuring.
These files were either completely orphaned or only served as redirect
source pages (Mintlify handles redirects at the routing level without
needing the source file to exist).

Updated docs.json redirects that previously pointed to archive/ pages
to point to current equivalents instead:
  - /archive/understanding-context-management → /model-config/context-windows
  - /archive/prompt-engineering-guide → /customization/cline-rules
  - /archive/telemetry → /enterprise-solutions/monitoring/telemetry

Deleted files by category:

Archive (entire directory removed):
  - archive/prompt-engineering-guide.mdx
  - archive/telemetry.mdx
  - archive/understanding-context-management.mdx

Cline CLI:
  - cline-cli/cli-reference-deprecated.mdx

Enterprise Solutions (16 files):
  - enterprise-solutions/bundled-endpoints.mdx
  - enterprise-solutions/configuration/overview.mdx
  - enterprise-solutions/configuration/choosing-your-deployment.mdx
  - enterprise-solutions/configuration/infrastructure-configuration/overview.mdx
  - enterprise-solutions/configuration/infrastructure-configuration/rules.mdx
  - enterprise-solutions/configuration/infrastructure-configuration/workflows.mdx
  - enterprise-solutions/configuration/infrastructure-configuration/control-other-cline-features/overview.mdx
  - enterprise-solutions/configuration/infrastructure-configuration/mcp/overview.mdx
  - enterprise-solutions/configuration/infrastructure-configuration/mcp/mcp-marketplace.mdx
  - enterprise-solutions/configuration/infrastructure-configuration/mcp/remote-mcp-servers.mdx
  - enterprise-solutions/configuration/infrastructure-configuration/providers/overview.mdx
  - enterprise-solutions/configuration/infrastructure-configuration/providers/custom.mdx
  - enterprise-solutions/configuration/infrastructure-configuration/providers/aws-bedrock/overview.mdx
  - enterprise-solutions/configuration/infrastructure-configuration/providers/google-vertex/overview.mdx
  - enterprise-solutions/configuration/infrastructure-configuration/providers/litellm/overview.mdx
  - enterprise-solutions/monitoring/opentelemetry_override.mdx

Exploring Cline's Tools (entire directory removed):
  - exploring-clines-tools/cline-tools-guide.mdx
  - exploring-clines-tools/new-task-tool.mdx
  - exploring-clines-tools/remote-browser-support.mdx

Features — old pages consolidated into core-workflows/ and customization/ (37 files):
  - features/checkpoints.mdx
  - features/drag-and-drop.mdx
  - features/editing-messages.mdx
  - features/explain-changes.mdx
  - features/skills.mdx
  - features/yolo-mode.mdx
  - features/at-mentions/ (7 files — all consolidated into core-workflows/working-with-files)
  - features/cline-rules/ (2 files — consolidated into customization/cline-rules)
  - features/commands-and-shortcuts/ (5 files — consolidated into core-workflows/using-commands)
  - features/customization/ (2 files)
  - features/hooks/ (3 files — consolidated into customization/hooks)
  - features/slash-commands/ (7 files)
  - features/slash-commands/workflows/ (3 files — consolidated into customization/workflows)
  - features/tasks/ (2 files — consolidated into core-workflows/task-management)

Introduction (entire directory removed):
  - introduction/overview.mdx
  - introduction/welcome.mdx

MCP:
  - mcp/adding-mcp-servers-from-github.mdx
  - mcp/configuring-mcp-servers.mdx

More Info (entire directory removed):
  - more-info/telemetry.mdx

Prompting (entire directory removed):
  - prompting/cline-memory-bank.mdx
  - prompting/prompt-engineering-guide.mdx
  - prompting/understanding-context-management.mdx

Provider Config:
  - provider-config/fireworks-ai.mdx
  - provider-config/ollama.mdx

Getting Started:
  - getting-started/selecting-your-model.mdx

Total: 71 files deleted, 12,344 lines removed.

* docs: update and add documentation pages

* revert unintended formatting changes to src files

* new first project docs

---------

Co-authored-by: Juan Pablo Flores <juan@cline.bot>
2026-02-14 22:02:22 -08:00
Saoud Rizwan f59d950ac6 fix: open Cline sidebar for task deeplinks (#9320)
* fix: open Cline sidebar for task deeplinks

* refactor: share task URI path constant
2026-02-14 01:15:13 -08:00
Ara d2e4f1c7b9 feat(zai): add glm-5 pricing and make it default (#9253) 2026-02-13 20:42:49 -08:00
Ara 49975fd0a3 feat(cli): support Moonshot provider across CLI flows (#9314)
* feat(cli): add moonshot provider support across CLI flows

* Update cli/man/cline.1

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-13 16:10:10 -08:00
shey-cline c0020e10b1 Allow Custom AWS Regions in Bedrock (CLI) (#9103)
* init

* changeset

* prevent regoinIndex = -1 when navigating with arrow keys while filteredRegions.length = 0

* refactor

* content fix
2026-02-13 15:46:36 -08:00
Ara 8dc5e15ee0 chore: bump version to 3.62.0 (#9313) 2026-02-13 15:13:36 -08:00
github-actions[bot] 276cb4c3a4 Changeset version bump (#9312)
* changeset version bump

* v3.62.0 Release Notes

- Banners now display immediately when opening the extension instead of requiring user interaction first
- Resolved 17 security vulnerabilities including high-severity DoS issues in dependencies (body-parser, axios, qs, tar, and others)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-13 14:24:54 -08:00
Saoud Rizwan 8169abbc74 fix(e2e): stabilize banner and code action assertions (#9311) 2026-02-13 14:11:55 -08:00
Ara a47ad46824 fix: rename package from "claude-dev" to "cline" in changesets (#9309)
Update changeset metadata files to use the correct package name
"cline" instead of the legacy "claude-dev" identifier.
2026-02-13 14:08:57 -08:00
Saoud Rizwan 7896e6d896 feat: promote MiniMax M2.5 in top banner and route CTA to free tab (#9307)
* feat: add minimax promo banner and free-tab model routing

* Add changeset for promoting MiniMax M2.5
2026-02-13 13:20:22 -08:00
aikido-autofix[bot] 166ec38d26 fix(security): update dependencies (#9234)
Co-authored-by: aikido-autofix[bot] <119856028+aikido-autofix[bot]@users.noreply.github.com>
2026-02-13 12:42:06 -08:00
Tomás Barreiro e44ea9d772 Post state to webview after fetching the banners (#9306)
* Post state to webview after fetching

* Test post state to webview is called

* refactors
2026-02-13 21:18:32 +01:00
Ara fcca1d4fe3 v3.61.0 Release Notes (#9305) 2026-02-13 10:35:16 -08:00
github-actions[bot] 34f0795217 v3.60.0 Release Notes (#9274)
- Fixes for Minimax model family
- Fixes for Response chaining for OpenAI's Responses API

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-13 09:02:46 -08:00
Bee 0648ed42b2 feat: make response ID chaining configurable (#9285)
Add an optional `usePreviousResponseId` flag to
`convertToOpenAIResponsesInput` and gate previous assistant-response
lookup behind it.

This makes response chaining opt-in instead of always-on, so providers
can control whether to continue from the latest stored OpenAI response
ID.feat(openai): make response ID chaining configurable

Add an optional `usePreviousResponseId` flag to
`convertToOpenAIResponsesInput` and gate previous assistant-response
lookup behind it.

This makes response chaining opt-in instead of always-on, so providers
can control whether to continue from the latest stored OpenAI response
ID.
2026-02-13 08:40:02 -08:00
Saoud Rizwan f32dde2c16 chore: update codex environment config 2026-02-13 04:17:07 -08:00
Saoud Rizwan 605c497eaf chore: update codex environment config 2026-02-13 04:14:22 -08:00
Saoud Rizwan a8322eec44 fix(task): avoid duplicate partial text from reentrant presentation (#9302) 2026-02-13 03:47:56 -08:00
Saoud Rizwan 974a7a748a fix(webview): add spacing before ask followup options 2026-02-13 02:31:14 -08:00
Saoud Rizwan fb2d092301 fix(hooks): preserve streaming tool rows in combineHookSequences (#9301) 2026-02-13 02:10:27 -08:00
Saoud Rizwan 45b3eb4833 fix(chat): stabilize tool-group text and thinking footer behavior (#9300)
* fix(chat): ignore streamed text after tool group starts

* fix(chat): suppress thinking during ask and completion handoff

* fix(chat): prevent thinking footer shimmer remount flicker
2026-02-13 02:03:32 -08:00
Bee a5048189e5 fix: remove focused BannerService test to run full suite (#9299)
The unit test suite currenlt is running the BannerService tests only when it should run the full suite.
Also update package-lock.json that wentout of sync.
2026-02-13 01:08:30 -08:00
Saoud Rizwan 897e842eb4 fix(task): ignore interleaved reasoning UI after text starts (#9298) 2026-02-13 00:25:08 -08:00
Saoud Rizwan 0389d4de07 Revert "fix(task): prevent duplicate streamed text rows after completion (#9235)" (#9297)
This reverts commit b514f18e4f.
2026-02-13 00:20:33 -08:00
Saoud Rizwan d99eec15d8 fix(minimax): emit single reasoning chunk on thinking start (#9290) 2026-02-12 23:46:53 -08:00
Ara 98ed009e69 fix: add missing name fields to free featured models and improve type safety (#9291)
- Add `name` property to minimax, kat-coder-pro, and trinity-large-preview
  models that were previously missing it
- Move type annotation from `as FeaturedModel[]` casts to the variable
  declaration for proper type checking at assignment time
- Add test to verify all featured models include a display name
2026-02-12 23:18:51 -08:00
Saoud Rizwan 8fb7b94297 Revert "Jose/thinking and flicker fix (#9148)" (#9292)
This reverts commit d8397c71b2.
2026-02-12 22:54:07 -08:00
Jose R. Perez d8397c71b2 Jose/thinking and flicker fix (#9148)
* feat: persistant thinking loader at bottom of stream during any cline activity with no visual feedback

* feat: thinking and flicker fix

* refactor: remove multi-layer throttling, use single canonical throttle point

Collapse 4 independent throttle layers (up to ~500ms added latency) into
a single 50ms debounce in subscribeToPartialMessage. Replace index-based
partial message tracking with stable ts-based tracking. Remove webview
queue/timer/flush system in favor of cheap equality dedup.

* fix: Add production-grade improvements to flicker fix

- Fix global mutable state bug in subscribeToPartialMessage.ts
- Add comprehensive test coverage (51 tests passing)
- Rename ThrottledApiHandler → SanitizedApiHandler
- Remove incomplete OpenAI reasoning effort code

* Fix test failures

* PR changes as per Greptile feedback

* Fixes as per feedback during PR review

---------

Co-authored-by: cline-test <132302818+candieduniverse@users.noreply.github.com>
2026-02-12 22:28:46 -08:00
Robin Newhouse 3899469d76 feat(evals): comprehensive LLM evaluation framework with CI (#8909)
* chore(evals): reorganize eval structure with purpose-based naming

- Move evals/diff-edits/ → evals/benchmarks/tool-precision/replace-in-file/
- Move evals/cli/ → evals/legacy/cli/ (preserve for reference)
- Create evals/benchmarks/real-world/ directory
- Create evals/benchmarks/coding-exercises/cases/ directory
- Create evals/analysis/ directory structure

Note: No repositories/exercism/ directory found to move.
Skipping pre-commit hook as this is a reorganization of legacy code.

* chore(evals): remove legacy evaluation code

Remove abandoned evaluation infrastructure:
- evals/benchmarks/tool-precision/ - Dashboard, database, diff implementations
- evals/legacy/cli/ - Old HTTP-based eval harness

This functionality is superseded by the new testing pyramid:
- Tool precision is now covered by contract tests in src/core/
- E2E testing uses the cline-bench framework

* feat(evals): add analysis framework for benchmark results

Add shared infrastructure for analyzing evaluation results:
- TypeScript schemas for Harbor and analysis output formats
- Parsers for Harbor, tool-precision, and exercise results
- Failure classifier with pattern matching (cline-failures.yaml)
- Metrics calculator (pass@k, consistency, latency)
- JSON and Markdown reporters
- CLI with analyze and compare commands
- Unit tests for classifier and metrics

This framework is used by both smoke tests and E2E evaluations
to provide consistent metrics and failure categorization.

* feat(evals): add contract tests for API transforms

Add tests to verify API response transformations preserve data correctly:
- thinking-traces.test.ts: Tests thinking block extraction and formatting
- tool-parsing.test.ts: Tests tool call parsing across providers

These contract tests catch regressions when modifying transform logic,
ensuring API responses are correctly processed regardless of provider.
Run with: npm run test:unit

* feat(evals): add provider smoke tests with pass@k metrics

Add lightweight smoke tests that validate provider integrations work
correctly with real LLM calls:

Scenarios (5 curated tests):
- 01-create-file: Tests write_to_file tool
- 02-edit-file: Tests replace_in_file tool
- 03-read-summarize: Tests read_file tool
- 04-multi-file: Tests multi-file edits
- 05-typescript-function: Tests code generation

Features:
- CLI-based runner using the cline CLI
- Multiple trials per scenario for reliability testing
- pass@k metrics (solution finding) and pass^k (consistency)
- Results storage with logs and latest symlink
- Adaptive metric display based on trial count

Run locally: npm run eval:smoke

* feat(evals): add E2E runner with cline-bench

Add end-to-end testing infrastructure using real-world production bugs:

- cline-bench submodule: 12 curated tasks from actual Cline sessions
  - Complex multi-file refactors
  - Bug fixes requiring deep context understanding
  - Cross-language/framework tasks

- E2E runner (evals/e2e/run-cline-bench.ts):
  - Integrates with Harbor for containerized execution
  - Supports single task or full suite runs
  - Pass/fail metrics with detailed logging

Run: npm run eval:e2e -- --task discord-trivia

Note: E2E tests require Docker and are intended for weekly/release
testing, not per-commit CI (each task takes 20-30 minutes).

* feat(evals): add CI workflow and documentation

CI Workflow (.github/workflows/cline-evals-regression.yml):
- Triggers on push/PR to main (src/core, src/shared, proto, evals paths)
- Builds CLI from source with Go 1.24
- Runs 5 smoke test scenarios in parallel
- Uses Anthropic API with claude-sonnet-4
- Uploads results as artifacts with summary

npm scripts:
- eval:smoke - Run smoke tests locally (builds CLI first)
- eval:smoke:run - Run smoke tests (assumes CLI is built)
- eval:e2e - Run cline-bench E2E tests

Documentation:
- ARCHITECTURE.md: Testing pyramid overview with ASCII diagrams
- EVALS_OVERVIEW.md: High-level introduction for mixed audience
- Updated README.md with current structure and usage

* chore(evals): restore tool-precision as deprecated legacy

Restore the diff edit evaluation framework for @ara's use case.
Marked as DEPRECATED - target removal Q2 2026 when cline-bench
is fully operational for model comparison.

Note: Skipping linter as this is legacy code being preserved as-is.

* feat(evals): add per-scenario model support and apply_patch test

Also honor --model overrides and prune stubs.

* chore(evals): update smoke tests for CLI 2.0

- Remove Go setup from workflow (CLI 2.0 is TypeScript)
- Build CLI via `npm run build` in cli/ directory
- Install CLI via `npm link` to test built code from PR
- Update CLI flags: -y -m model --json (remove -o and -s)
- Provider configured via `cline auth` before tests run

* chore(evals): add auth check and CLI 2.0 flags

- Add configureAuth() that runs cline auth non-interactively
- Require CLINE_API_KEY env var or use existing ~/.cline auth
- Add --config flag to use shared config directory
- Add -t timeout flag to CLI args
- Reduce scenario timeout to 30s for faster iteration
- Remove --json flag (CLI doesn't output errors in json mode)

* feat(evals): add parallel execution and move workspaces to results

- Add --parallel flag to run scenarios concurrently (default limit: 4)
- Move trial workspaces from scenarios/ to results/ directory
- Workspaces now cleaned up with `npm run eval:smoke:clean`
- Keeps scenarios/ clean and version-controllable

* ci: add smoke tests workflow with parallel execution

- Single job runs all 7 scenarios in parallel using test runner's --parallel flag
- Builds CLI in-job (no artifact passing needed)
- Outputs summary.md to GitHub step summary
- Syncs package-lock.json for tiktoken/commander deps

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(evals): increase 01-create-file timeout to 120s

The 30s timeout was too short for reliable execution.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: restore changesets deleted during rebase

These changesets belong to the already-merged CLI fix (#9073)
and should not be deleted by this branch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(evals): remove unused dependencies from package.json

Drop execa, node-fetch, ora, sqlite, uuid, yargs and their types.
These were leftovers from the old CLI-based eval runner. The smoke
tests use Node builtins and the tool-precision benchmark only needs
axios, better-sqlite3, chalk, commander, dotenv, tiktoken.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add TypeScript build info files to .gitignore

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-02-12 22:23:35 -06:00
Robin Newhouse e85332319e feat: add .agents/skills directory support for skill discovery (#9074)
* feat: add .agents/skills directory support for skill discovery

Add compatibility for the standardized .agents/skills directory pattern,
both globally (~/.agents/skills) and locally (.agents/skills in workspace).

* feat: make .agents/skills the default for new skills

New skills are now created in .agents/skills (local) and ~/.agents/skills
(global) by default. These directories also have highest priority in
skill discovery, overriding skills with the same name from other locations.

* docs: update skills documentation for .agents/skills directories

* refactor skills directory helpers
2026-02-12 22:08:30 -06:00
Bee 1a29d428ae refactor: BannerService initialization and cache management (#8969)
* increases banner cache duration to 24 hours so we make one api calls per day per user; implements a circuit breaker that stops retrying after 3 consecutive failures

* add new tests

* Clear banner cache when auth status changes

* revert 5898bc6e0e

* Fixing circuit breaker

* fix: reset circuitBreakerOpenedAt on failed half-open recovery

Previously, circuitBreakerOpenedAt was only set when consecutiveFailures
reached exactly MAX_CONSECUTIVE_FAILURES. This meant that after a failed
half-open recovery attempt, the timestamp wasn't updated, causing the
circuit breaker to immediately enter half-open state again on the next call.

Now circuitBreakerOpenedAt is updated on every failure once the circuit
breaker is tripped, ensuring proper timeout between recovery attempts.

* refactor: BannerService initialization and cache management

- Move BannerService initialization from common.ts to AuthService (which is initialized in controller)
- Re-initialize BannerService after auth state updates to ensure user context
- Add HostRegistryInfo to centralize host/platform information collection
- Improve rate limiting with exponential backoff (5min → 15min → 30min)
- Refactor error handling to better distinguish between rate limits and server errors
- Remove temporary disabled banner fetching comments

This change ensures banners are only fetched when user authentication is
available and implements more robust rate limiting to prevent API hammering.
The banner service now properly tracks user context and respects server
rate limits with progressive backoff delays.

* refactor(banner): simplify banner service initialization and usage

- Remove `getBanners()` wrapper method from Controller class
- Call `BannerService.get().getActiveBanners()` directly in Controller
- Change `BannerService.initialize()` to synchronous, returns instance immediately
- Make banner fetching non-blocking by moving to background
- Remove unused `BannerCardData` import from Controller
- Update tests to handle asynchronous background fetching with timeouts
- Clean up AuthService banner service initialization comment

This change simplifies the banner service API by removing unnecessary abstraction layers and making initialization non-blocking. The service now fetches banners in the background rather than blocking on initialization, improving application startup performance.

* clean up

* apply feedback

* un-skip unit test

* mock

* mock env

* clean up and add debounce fetch

* log fetch time

* revert

* feature flag: remote-banners

* fix loop in authService on auth update

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

* Fix tests

* small fixes

* use .? for banner

* moves initializeDistinctId to StateManager

* initializeDistinctId

* use v2 endpoint

---------

Co-authored-by: Zhongying Qiao <cryptoque@users.noreply.github.com>
Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
Co-authored-by: Tomás Barreiro <BarreiroT@users.noreply.github.com>
Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
Co-authored-by: BarreiroT <tomasmbarreiroi@gmail.com>
2026-02-12 18:28:12 -08:00
Saoud Rizwan 92cf03e42c feat(subagents): simplify research output guidance and command workflow (#9284) 2026-02-12 16:00:02 -08:00
Bee 3ea393a5e9 fix: openai native provider token usage mapping (#9272)
* fix: openai native provider token usage mapping

- Add `store` parameter support to OpenAI native provider options to allow persisting completions.
- Fix incorrect mapping of `cached_tokens` and `reasoning_tokens` in usage statistics.
- Include `thoughtsTokenCount` in the final usage report to track reasoning model performance and costs.

* Update src/core/api/providers/openai-native.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-12 14:26:35 -08:00
Max 9829e7d49e restore yolo mode to what it was before cline cli started (#9205)
Apply suggestions from code review

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-12 11:10:24 -08:00
Max 56de96e5ff fix oca auth (#9145)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-12 10:20:28 -08:00
github-actions[bot] ecde79cf08 v3.59.0 Release Notes (#9263)
- Added Minimax 2.5 Free Promo
- Fixed Response chaining for OpenAI's Responses API

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-12 10:20:19 -08:00
Bee ff36cbcb87 feat: implement response chaining for Responses API (#9270)
* feat: implement response chaining for Responses API

Implement response chaining by tracking and passing previous_response_id
to continue conversations from the last assistant message. This enables
the Responses API to maintain context across multiple turns.

Key changes:
- Search backwards through messages to find last assistant message with ID
- Only send new messages after the chained response
- Track function call metadata (call_id, name, id) across chunks
- Include call_id in tool_call events for proper correlation
- Clean up debug logging and remove commented code
- Remove redundant "Ran out of tokens" log message

This improves conversation continuity and ensures function calls are
properly tracked with their associated IDs throughout the streaming
response lifecycle.

* clean up

* update oca

* codex
2026-02-12 10:01:34 -08:00
shey-cline 5a75f08118 Prevent Parent Container Scrolling In Dropdowns (#9146)
* init

* missed some dropdowns & make scroll behavior work for scrolling nested elements

* add changeset

* add combobox roles
2026-02-12 10:00:40 -08:00
shey-cline 120754c2fe Allow Custom AWS Regions in Bedrock (Extension) (#9104)
* init

* changeset

* addressed comments – add onBlur, aria attributes and redundant useMemo
2026-02-12 09:57:04 -08:00
Saoud Rizwan 5d048d09f8 fix(subagents): retry initial stream bootstrap failures (#9264)
* fix(subagents): retry initial stream bootstrap failures

* fix(subagents): align initial retry classification with main loop

* fix(subagents): compact context on window limit during startup

* fix(subagents): proactively compact context at token thresholds

* feat(subagents): optimize file reads before context truncation
2026-02-12 06:34:43 -08:00
Saoud Rizwan 36580ce086 chore(codex): update environment to use launch script and simplify reinstall
Point the VS Code action at the new run-extension-host.sh script and
drop the git checkout of lock files from the reinstall action.
2026-02-12 05:53:25 -08:00
Saoud Rizwan c584bf4185 feat(dev): add tmux-based extension host launch script
Replaces the inline VS Code launch command with a proper dev script that:
- Builds protos and webview upfront
- Runs esbuild, tsc, and webview watchers in parallel tmux panes
- Waits for dist/extension.js before launching the extension host
- Cleans up all processes and closes the dev window on Ctrl+C
2026-02-12 05:53:18 -08:00
Saoud Rizwan 8133babf41 fix(chat): keep focus chain placeholder visible to prevent layout jump (#9266)
* fix(webview): stabilize focus chain header space and placeholder

* fix(chat): add follow-up bottom scroll to avoid short scroll

* style(chat): refine markdown spacing and tool group summary tone

* fix(chat): retry auto-scroll at 40ms and 70ms

* fix(chat): keep focus chain placeholder visible until checklist exists
2026-02-12 03:50:01 -08:00
Saoud Rizwan 741f524da7 chore(deps): upgrade openai sdk to 6.21.0 for xhigh reasoning (#9267) 2026-02-12 03:48:13 -08:00
Robin Newhouse d3918dd7df fix(task): canonicalize attempt_completion result parameter (#9262) 2026-02-12 00:37:27 -06:00
alex-lum 024bb65443 Add organization attributes to telemetry metrics (#9242) 2026-02-11 16:51:24 -08:00
github-actions[bot] 58ebbdbf80 Changeset version bump (#9252)
* changeset version bump

* Updating CHANGELOG.md format

* changeset version bump

* Updating CHANGELOG.md format

* Eve manually updating the banner and the release version

* Manually update the changelog

* Fix GLM 5 model ID in banner

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: cline-test <132302818+candieduniverse@users.noreply.github.com>
2026-02-11 16:30:14 -08:00
Juan Pablo Flores cee74d2f3a docs: add subagents feature documentation (#9258)
* docs: add subagents feature documentation

Add new documentation page covering the Subagents feature, including
how it works, enabling/configuring, auto-approve behavior, available
tools, and usage guidance. Register the page in docs.json sidebar nav.

* docs: remove hardcoded subagent limit from subagents page

Remove references to 'up to five' subagents, as the limit is no longer
fixed. Updates both the intro paragraph and the How It Works section.
2026-02-11 15:54:57 -08:00
Ara a6f3b9f856 Revert "fix MCP OAuth: add missing scope parameter (#9117)" (#9256)
This reverts commit 401358374f.
2026-02-11 15:07:34 -08:00
Ara 0e524ffc3a feat(zai): add glm-5 pricing and make it default (#9254)
* feat(zai): add glm-5 pricing and make it default

* fix(zai,qwen): fallback model id when apiModelId is invalid
2026-02-11 14:05:50 -08:00
Ara 95ca14fa2a Fixing changeset files (#9251) 2026-02-11 12:38:31 -08:00
Max a6c57a4ce5 print task id in headless modes (#9229)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-11 12:21:02 -08:00
Tomás Barreiro 9470cf19cd Add headers to the Remotely Configured MCP server schema (#9238) 2026-02-11 18:53:44 +01:00
Saoud Rizwan acf34860bb chore: fix codex desktop app configuration 2026-02-11 02:34:33 -08:00
Saoud Rizwan 49cad88aca chore: fix codex desktop app configuration 2026-02-11 02:25:59 -08:00
Saoud Rizwan 3c65cbc7f2 chore: add codex desktop app configuration 2026-02-11 02:22:29 -08:00
Saoud Rizwan 12603d4be1 feat: replace legacy CLI subagents with native use_subagents tool (#9208)
* feat: checkpoint subagent tool workflow and approval UX

* feat: support subagent tool execution without native tool calls

* fix: expose use_subagents when native tool calling is disabled

* fix: stabilize subagent command UX and suppress nested command rows

* chore: tune subagent prompt guidance for context-heavy exploration

* fix: align subagent row spacing with chat row conventions

* fix: keep cancelled subagent state during immediate resume

* feat: implement subagent message rendering for approval prompts and progress updates

* feat: enhance SubagentRunner with tool use ID resolution and fallback handling

* fix: stabilize subagent cline requests with ulid and initial workspace metadata

* refactor: unify subagent chat row rendering

* feat: surface subagent costs in task metrics and status rows

* fix: refine cli subagent tree alignment and wrapping

* fix: refine subagent streaming rows in cli and webview

* fix: ensure unique act mode hint keys in CLI chat

* feat: add subagents settings toggle wiring across webview and cli

* fix(webview): stream subagent stats per prompt while constructing prompts

* fix: remove duplicate subagentsEnabled declaration after rebase

* chore: restore package lockfiles to main

* fix: harden task history usage parsing and clean prompt separators

* chore: refine subagent response formatting guidance

* feat: collapse subagent prompts with show more

* feat: show latest subagent tool call in status rows

* fix: fall back to non-native mode for subagents when native tools are unavailable

* fix: retry empty subagent responses before failing

* fix(subagents): require attempt_completion and dedupe tool result formatting

* feat(subagents): polish prompt guidance and webview status row
2026-02-11 02:17:45 -08:00
Robin Newhouse b514f18e4f fix(task): prevent duplicate streamed text rows after completion (#9235)
* fix(task): prevent duplicate partial text rows after completion

Avoid adding a new partial text message when the latest text row is already completed with the same content. This stops a presenter race from rendering duplicate streamed text lines for MiniMax-style timing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(task): cover duplicate partial text dedupe behavior

Add a Task.say unit test that reproduces the duplicate-partial-after-complete scenario and verifies we skip creating a second text row with identical content.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 23:44:01 -08:00
Saoud Rizwan ce85d7d414 fix(cli): preserve OAuth callback paths in auth redirects (#9237) 2026-02-10 19:28:28 -08:00
Saoud Rizwan 739d75afe3 fix(claude-code): add opus 4.6 1m model option (#9231)
* fix(claude-code): add opus 4.6 1m model option

* fix(claude-code): support opus[1m] alias and align opus alias

* fix(claude-code): add sonnet[1m] model support
2026-02-11 04:19:44 +01:00
Max fc1be2baac add more shortcuts to help output (#9204)
* add more shortcuts to help output

* Apply suggestions from code review

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

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-10 16:14:11 -08:00
Saoud Rizwan 5dcaa8c8cc fix(vertex): add opus 4.6 1m model support on Vertex (#9230)
* fix(vertex): add opus 4.6 1m and global endpoint support

* fix(vertex): enable thinking for opus 4.6 1m in webview
2026-02-10 15:30:12 -08:00
CandiedUniverse 79cf77db0a Finish adding Amazon Bedrock to isNexGenModelProvider() list [CLINE-1291] (#9216)
* Add Bedrock to the list in isNextGenModelProvider()

* feat(bedrock): Remove testing script used to develop isNextGenModelProvider() change against

* refactor: extract shared isParallelToolCallingEnabled into model-utils

Consolidate duplicated parallel tool calling logic from ToolExecutor.ts
and task/index.ts into a single exported function in model-utils.ts.

Both callers now delegate to the shared function, eliminating the need
to maintain identical checks in two places.
2026-02-10 14:39:48 -08:00
Robin Newhouse 4b61799df5 docs: improve PR creation skill to use --body-file flag (#8789)
Replaces inline --body with --body-file approach in the PR creation skill documentation. This avoids shell escaping issues, newline problems, and command-line flakiness when creating PRs with complex markdown content.

Related to #8785
2026-02-10 15:52:05 -06:00
cryptoque 806708e802 feat: enable sync-ed deletion for remote mcp servers from remote config to extension (#9210)
* feat: enable sync-ed deletion for remote mcp servers from remote config to extension

* chore: add tests for syncing remote mcp server adding and removal

* address comments
2026-02-10 10:24:19 -08:00
Max 642ea849e3 fix publish-cli-trusted workflow (#9220)
- parent workflow needs to request permissions for children workflows

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-09 21:53:40 -08:00
Max dff8193de5 make trusted npm publish workflow (#9219)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-09 21:40:04 -08:00
Max a05c1c5e53 store input text on remount (#9124)
- my input was getting cleared when i resized the screen. this fixes
that

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-09 21:06:59 -08:00
Max 9a11976d27 improve cline config command (#9212)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-09 18:15:05 -08:00
Tomás Barreiro b54647ab17 [PF-389] Render remote config options and add test buttons (#9051)
* WIP - Render a Remote Config secttion and add an option to refresh

* Add the different remote config sections and test them

* fixes

* refactor

* Add proper wrapping

* Stack more values

* Properly report errors when prompt uploading fails

* Add a better error message for the otel test button

* clean

* Fix option rendering

* Render less options if they aren't configured
2026-02-10 02:46:40 +01:00
ClineXDiego 6e253dfa9a Fix/vscode web oauth callback (#9173)
* fix: use vscode.env.asExternalUri for web OAuth callbacks

In VS Code Web (Codespaces, code serve-web), OAuth callbacks using
http://127.0.0.1:PORT break because the extension host runs remotely.

Changes:
- getCallbackUrl now accepts a path parameter
- Desktop: uses vscode://extension-id/path directly
- Web (UIKind.Web): uses vscode.env.asExternalUri() for web-reachable URL
- Updated all callers (/auth, /openrouter, /hicap, /requesty, MCP) to
  pass path and use URL+searchParams for proper encoding
- Added regression test asserting web callback URL is not 127.0.0.1
- AuthHandler (localhost HTTP) now only used by CLI/standalone mode

* fix: use URL.searchParams for proper callback URL encoding

Callers were using template literal interpolation to embed callback URLs
into query strings, which breaks when the URL contains special characters
(e.g. from asExternalUri with query params). Use URL+searchParams.set()
which automatically encodes values.

* chore: revert unrelated whitespace change in account.proto

* revert: remove non-essential URL encoding changes in auth callers

Keep only the core fix (getCallbackUrl path parameter + asExternalUri for web).
Revert the URL+searchParams encoding improvement to minimize diff.

* fix: URL-encode callback_url in auth callers, add encoding test

In VS Code Web, callback URLs from asExternalUri can contain their own
query params (?tkn=...&extra=...). String-interpolating them into
callback_url= causes everything after the first & to be parsed as
top-level params, truncating the callback URL.

Use URL + searchParams.set() in openrouter, hicap, and requesty callers.
Replace tautology test with deterministic round-trip encoding assertions.
2026-02-09 16:43:24 -08:00
Ara 7236d02ebb feat(tools): add auto-approval support for attempt_completion commands (#8926)
* feat(tools): add auto-approval support for attempt_completion commands

- Add auto-approval logic for bash commands in AttemptCompletionHandler
- Show commands as 'say' instead of 'ask' when auto-approved
- Display notification prompting user approval when manual approval needed
- Add 30-second timeout notification for long-running auto-approved commands
- Fix Logger import path from @/shared to @shared

* Send to cline provider
2026-02-09 15:57:29 -08:00
Saoud Rizwan 84fef6fe1f chore(ci): remove ai review workflows and publish caching (#9211) 2026-02-09 15:42:44 -08:00
CandiedUniverse 7bdbf0a9a7 feat(bedrock): Support parallel tool calling in Amazon Bedrock [CLINE-1291] (#9150)
* feat(bedrock): Create agent implementation plan for supporting parallel tool calling.

* Add Bedrock tool calling support

* Improve Bedrock tool calling test guidance

* Add Bedrock CLI parallel tool calling test script

* fix: add ALLOW_AWS_DEFAULT_CHAIN support to live integration test script

* chore: add changeset for Bedrock parallel tool calling

* feat(bedrock): enable native parallel tool calling for Bedrock provider

- Add 'bedrock' to isNextGenModelProvider() so native tool calling is enabled
- Add 'bedrock' to getNativeConverter() to use Anthropic-format tool specs (input_schema)
- Fix empty tool description validation error in mapClineToolsToBedrockToolConfig
  (Bedrock requires description length >= 1)
- Update CLI test to use Sonnet 4.5 (Haiku too small for native tool calling)
- Add <invoke> XML detection to CLI test to catch XML fallback

Verified: conversation history shows 3 native tool_use blocks in a single
assistant response with 3 matching tool_result blocks — true parallel
tool calling via Bedrock Converse API.

* docs: mark all phases complete in bedrock parallel tool calling implementation plan

* chore: switch test scripts default model to Haiku 4.5 (cheaper for testing)

* feat: enhance CLI verification suite with 3 test cases (single, parallel, round-trip)

* Remove bedrock parallel tool calling implementation plan doc.

* refactor: simplify to single CLI verification script for bedrock parallel tool calling

Remove the handler-level test script (test-bedrock-tool-calling.ts) and consolidate
into a single focused CLI test that proves parallel tool calling works end-to-end:
- Spawns Cline CLI with Bedrock config
- Asks it to read 3 files
- Verifies ≥2 parallel native tool calls (not XML fallback)
- Task completion proves tool result round-trip works

* refactor(bedrock): improve type safety and code quality for parallel tool calling

- Add typed interfaces (ToolUseStart, ToolUseDelta) for Bedrock stream
  events instead of relying on `as any` casts
- Extend ContentBlockStart and ContentBlockDelta interfaces with toolUse
  fields so stream parsing uses typed property access
- Remove dead `inputBuffer` field from activeToolCalls Map (was tracked
  but never read — tool input deltas are yielded immediately)
- Add JSDoc to mapClineToolsToBedrockToolConfig explaining its purpose
  and return semantics
- Document why createDeepseekMessage intentionally ignores the tools
  parameter (DeepSeek R1 uses InvokeModel, not Converse API)

* refactor(scripts): improve test script readability and resource cleanup

- Add try/finally with cleanupDirs() to remove temp workspace and config
  dirs after each run (previously accumulated in $TMPDIR)
- Extract named constants for CLI_TIMEOUT_SECONDS and HEARTBEAT_INTERVAL_MS
- Add CliResult interface for the runCli return type
- Rename cryptic variables: hb → heartbeatInterval, c → chunk, p/d → filePath/data
- Add JSDoc to parseReadFilePaths and hasXmlFallback
- Add explanatory comments to empty catch blocks
- Log stderr on non-zero exit code for easier debugging
- Extract createTestWorkspace() to separate workspace setup from main flow
- Add section separator comments for visual structure

* test(bedrock): add missing edge-case tests and remove dead describe block

- Add tests for mapClineToolsToBedrockToolConfig edge cases:
  undefined/empty input returns undefined, tools without input_schema
  are silently dropped
- Add test for formatMessagesForConverseAPI with array tool_result
  content (multi-block text responses)
- Add test for tool_result is_error → status:'error' mapping
- Remove empty 'reasoning content handling (deprecated)' describe block

35 tests passing (was 31).

* test(bedrock): add integration-level tests covering E2E script gaps

Add 'native tool calling integration' test suite that validates the
concerns previously only covered by the live E2E CLI script:

- Bedrock + Claude 4 is recognized as native tool calling eligible
  (catches silent regression if Bedrock is removed from
  isNextGenModelProvider or Claude 4 from isNextGenModelFamily)
- Bedrock + Claude 3.x correctly does NOT qualify (pre-4.0 guard)
- Native tool calling disabled when user setting is off
- createAnthropicMessage passes toolConfig to ConverseStreamCommand
  (catches the tool spec not reaching the API)
- Full multi-turn tool call round-trip formatting (tool_use in
  assistant → tool_result in user → reformatted for next API call)

40 tests passing (was 35).

* Remove functional verification script before code review
2026-02-09 15:23:56 -08:00
Max bab336f172 use cline provider for cline pr review workflow. use npx instead of npm install (#9202)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-09 10:21:32 -08:00
Max 967342999f if yolo mode is on, don't ask permission to use mcp tools (#9100)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-09 10:12:30 -08:00
Jose R. Perez d19a8779e7 feat: consolidate ViewHeader and styling (#8989)
* feat: consolidate ViewHeader and styling

* feat: changeset

* fix: added back environment variables for color differentiation

* fix: co-pilot fixes

* feat: github copilot fix
2026-02-09 10:02:16 -08:00
Saoud Rizwan 5c04fa3aa3 fix(cli): flush telemetry on shutdown and include activation metadata (#9195) 2026-02-08 22:00:45 -08:00
Saoud Rizwan 7c31c1d02a fix: restore reasoning behavior parity after #9168 (#9188)
* fix: restore reasoning parity after #9168

* fix: restore webview reasoning support compatibility checks

fix: simplify reasoning support model matching
2026-02-08 19:43:23 -08:00
Saoud Rizwan 740f99400b feat(cli): add max-consecutive-mistakes task flag (#9194) 2026-02-08 18:52:45 -08:00
Igor Tceglevskii 195294f389 feat: bundled endpoints.json (#9113) 2026-02-08 18:31:01 -08:00
Saoud Rizwan 2cc070eee2 fix(api): preserve vercel model id when metadata is missing (#9192) 2026-02-08 18:00:13 -08:00
Saoud Rizwan 627838243e fix(e2e): increase test timeout for Windows CI runners (#9185)
The diff editor e2e test flakes consistently on Windows CI because the
40s test timeout is too tight. The test does signin, message send,
history verification, then a second message send before the diff
assertion -- on slow Windows runners this setup alone can eat most of
the budget. Bumping to 60s gives enough headroom.
2026-02-08 11:27:48 -08:00
Saoud Rizwan f8a1f75664 feat: add output precision and threshold rules to double-check prompt (#9184)
Add Terminal-Bench-proven rules as items 5 and 6 in the double-check
re-verification checklist, so they're enforced at completion
verification time rather than in the system prompt.
2026-02-08 11:09:27 -08:00
Saoud Rizwan 54aeba1fee feat: add double-check completion experimental feature (#9180)
* feat: add double-check completion experimental feature

When enabled, the first attempt_completion call in a task is rejected
with a tool error that instructs the model to re-verify its work
against the original task requirements. The rejection includes the
initial task text for context. The second call proceeds normally.

This is opt-in (default off) and available via:
- Settings > Features > Experimental > Double-Check Completion
- CLI flag: --double-check-completion
- CLI TUI settings panel toggle

Adds completionAttemptCount to TaskState, plumbs the setting through
TaskConfig/ToolExecutor following existing patterns, and includes
9 unit tests.

* chore: add cli:run script for quick CLI testing

* fix: increase task preview to 8000 chars, revert unintended regex change

* fix: preserve existing proto field numbers

The auto-generator renumbered open_ai_headers (175->177) and
openai_codex_oauth_credentials (46->48), and dropped the reserved 146
comment. Restore original field numbers to avoid breaking wire-format
compatibility.

* fix: remove partial completion_result message on double-check rejection

During streaming, handlePartialBlock shows the completion_result in
the chat view. When we reject the first attempt, we need to clean up
that partial message so the user doesn't see a stale completion that
was actually rejected.

* refactor: switch from counter to boolean toggle for double-check

Use a boolean pending flag instead of a counter so that every
attempt_completion gets double-checked, not just the first one in
a task. The flag toggles: reject (set pending), accept (clear pending),
so if the model does more work and tries to complete again later, it
gets double-checked again.
2026-02-08 10:56:10 -08:00
Robin Newhouse a03642ba4a fix(prompt): add output precision and threshold iteration rules (#9178)
* fix(prompt): add output precision and threshold iteration rules

Two concise rules proven effective via Terminal-Bench testing:

1. Output precision: produce exactly what's specified, no extra columns/fields/debug output
2. Threshold iteration: verify results meet numerical criteria before completing

Tested on 6 targeted Terminal-Bench tasks (job 2026-02-07__16-15-00):
- log-summary-date-ranges: FAIL→PASS (output precision rule eliminated extra columns)
- dna-insert: FAIL→PASS (iterate rule helped agent meet Tm threshold)

A third rule (no-cleanup) was tested and deliberately excluded: it failed to
prevent self-sabotage on configure-git-webserver despite STRICTLY FORBIDDEN
language, and caused a side-effect on polyglot-c-py by preventing legitimate
build artifact cleanup. The cleanup behavior is too deeply trained to override
via prompt rules alone.

* test: update prompt snapshots for new rules
2026-02-08 09:37:04 -08:00
Saoud Rizwan b65435fc55 fix(cli): route PostHog networking through shared fetch (#9149)
* fix(cli): route PostHog networking through shared fetch

* remove unnecessary `as RequestInit` casts from PostHog fetch wrappers

PostHogFetchOptions is a structural subset of RequestInit, so the cast
is unnecessary. Also removes a stale comment about shared client support
in PostHogErrorProvider.
2026-02-07 18:26:23 -08:00
Saoud Rizwan 88694d39fa feat(cli): allow --thinking flag to accept custom token budget (#9177)
The --thinking flag now accepts an optional number argument to set a
custom thinking budget instead of always using the 1024 default.

  cline "prompt" --thinking         # 1024 tokens (default)
  cline "prompt" --thinking 8000    # 8000 tokens

Invalid values get a warning and fall back to 1024.
2026-02-07 17:06:33 -08:00
Saoud Rizwan 6c53daa88e feat: move reasoning effort to model config and settings UX (#9168)
* feat: move reasoning effort to model config and update model selection UX

* refactor: dedupe reasoning effort handling and drop lockfile churn

* refactor: default reasoning effort to low

* refactor(cli): sync mode-scoped thinking and reasoning writes

* fix: centralize reasoning effort normalization and avoid implicit openai effort

* fix: restore proto field number for codex credentials and reserve removed fields

- Keep openai_codex_oauth_credentials at field 46 (was incorrectly
  changed to 47)
- Add reserved 146 in Settings for removed openai_reasoning_effort
- Add reserved 15 in UpdateSettingsRequest for removed openai_reasoning_effort
- Remove stale openai_reasoning_effort field from UpdateSettingsRequest

* fix: map medium reasoning effort to LOW for Gemini models

Gemini API only accepts LOW and HIGH thinking levels. MEDIUM exists in
the SDK enum but is rejected at the API level. Map medium to LOW and
update the default fallback accordingly.
2026-02-07 16:58:10 -08:00
Ara 1f3c00c613 feat(task): add support for writing prompt metadata artifacts (#9158)
Introduces a mechanism to save system prompts and task metadata to disk for debugging and analysis purposes.

- Added `writePromptMetadataArtifacts` to the `Task` class.
- Feature is enabled via the `CLINE_WRITE_PROMPT_ARTIFACTS` environment variable.
- Artifacts are saved to `.cline-prompt-artifacts` or a custom path defined by `CLINE_PROMPT_ARTIFACT_DIR`.
- Writes both a JSON manifest (containing task ID, model info, and timestamp) and the raw system prompt for every API request.
2026-02-07 15:21:41 -08:00
Saoud Rizwan 4d455ea015 fix(terminal): tune execute_command timeout strategy for long-running tasks (#9159)
* fix(terminal): tune managed timeout policy for long-running commands

* Reduce default command timeout from 120 to 30 seconds

* Update ExecuteCommandToolHandler.timeout.test.ts

---------

Co-authored-by: Ara <arafat.da.khan@gmail.com>
2026-02-07 14:47:26 -08:00
Saoud Rizwan 3daf24662e fix(prompt): add guidance to use -- for leading-dash positional args (#9161) 2026-02-07 14:18:55 -08:00
Saoud Rizwan 942fcf5762 fix(terminal): surface command exit codes in results (#9156) 2026-02-07 13:31:22 -08:00
ClineXDiego 70a99047ed fix: use vscode.env.asExternalUri for auth callback URLs in VS Code Web (#9144)
* fix: use vscode.env.asExternalUri for auth callback URLs in VS Code Web

The OAuth callback redirect was broken in VS Code Web (code serve-web)
environments because the callback URL used a raw vscode:// URI scheme,
which the OS would route to the local desktop VS Code app instead of
the web instance.

This change wraps both getCallbackUrl() and getIdeRedirectUri() with
vscode.env.asExternalUri() which properly transforms URIs based on the
environment:
- Desktop VS Code: unchanged (vscode://...)
- VS Code Remote SSH: adds remote authority for proper routing
- VS Code Web: transforms to HTTPS URL that routes through the web server

Fixes #5109 (remaining callback redirect issue)
Related: #2152

* fix: use HTTP-based auth callback for VS Code Web mode

In VS Code Web (code serve-web), vscode:// URIs redirect to the desktop
app instead of staying in the browser. This change uses AuthHandler
(local HTTP server) for the auth callback in web mode, matching how
CLI/standalone already handles auth.

- getCallbackUrl: use AuthHandler when UIKind.Web
- getIdeRedirectUri: return empty in web mode to avoid vscode:// redirect

* fix: add fallback for openExternal RPC for JetBrains compatibility

The openExternal host bridge RPC is not implemented in the JetBrains
plugin, causing sign-in to fail silently. This adds a fallback to the
'open' npm package when the host RPC fails with UNIMPLEMENTED.

Fixes #9164, #9137, #9138
2026-02-07 10:25:14 -08:00
Robin Newhouse 844038084c feat: add CLI build workflow for testing from any commit (#9131) 2026-02-07 05:23:22 -08:00
Robin Newhouse 0c6f77ea46 Remove accidentally committed implementation_plan.md (#9160) 2026-02-07 00:11:02 -06:00
Saoud Rizwan 9b70f94174 fix(prompt): require verification before completion (#9154)
* fix(prompt): require verification before completion

* fix(prompt): align gemini verification-first completion guidance
2026-02-06 19:22:40 -08:00
Saoud Rizwan 0a4f939ecb chore(ci): tag bot PR reviews with workflow footer (#9152) 2026-02-06 15:08:38 -08:00
720 changed files with 35366 additions and 30855 deletions
@@ -1,6 +1,6 @@
---
name: create-pull-request
description: Create a GitHub pull request following project conventions. Use when the user asks to create a PR, submit changes for review, or open a pull request. Handles commit analysis, branch management, and PR creation using the gh CLI tool.
description: Create a GitHub pull request following project conventions. Use when the user asks to create a PR, submit changes for review, or open a pull request. Handles commit analysis, branch management, PR template usage, and PR creation using the gh CLI tool.
---
# Create Pull Request
@@ -147,14 +147,29 @@ When filling out the template:
### Create PR with gh CLI
**Use a temporary file for the PR body** to avoid shell escaping issues, newline problems, and other command-line flakiness:
1. Write the PR body to a temporary file:
```
/tmp/pr-body.md
```
2. Create the PR using the file:
```bash
gh pr create --title "PR_TITLE" --body-file /tmp/pr-body.md --base main
```
3. Clean up the temporary file:
```bash
rm /tmp/pr-body.md
```
For draft PRs:
```bash
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main
gh pr create --title "PR_TITLE" --body-file /tmp/pr-body.md --base main --draft
```
Alternatively, create as draft if the user wants review before marking ready:
```bash
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main --draft
```
**Why use a file?** Passing complex markdown with newlines, special characters, and checkboxes directly via `--body` is error-prone. The `--body-file` flag handles all content reliably.
## Post-Creation
-8
View File
@@ -1,8 +0,0 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
Replace the LiteLLM model list with a selector
+5
View File
@@ -0,0 +1,5 @@
---
"cline": minor
---
Adds Messages API support to Oracle Code Assist, adding functionality for Claude models
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
fix(cli): prevent hang when spawned without TTY
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
Add Claude Opus 4.6 model support
-11
View File
@@ -1,11 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
Add /q command to quit CLI
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
Supports rendering markdown table in chat view.
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
Fix CLI crashing in CI environments and with stdin redirection (e.g., `cline "prompt" < /dev/null`). Now checks both stdin and stdout TTY status before using Ink, and only errors on empty stdin when no prompt is provided.
-7
View File
@@ -1,7 +0,0 @@
---
"cline": patch
---
fix: use vscode.env.openExternal for auth in remote environments
Fixes OAuth authentication in VS Code Server and remote environments by routing browser URL opening through VS Code's native openExternal API instead of the npm 'open' package.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Updating script documentation and removing unnecessary continue on error
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix a streaming crash when a chunk has usage data but no `delta` by guarding reasoning field checks in provider handlers. Add regression tests for OpenRouter, Cline, Vercel AI Gateway, and Fireworks handlers to cover usage-only chunks.
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
Fix Bedrock model id
+4
View File
@@ -0,0 +1,4 @@
"claude-dev": patch
---
Add missing smoke evaluation npm scripts so documented commands like `npm run eval:smoke:run` work from the repository root.
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
Add automatic retries (up to 3 attempts) for smoke test CI jobs to reduce flaky failures
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
fix acp auth check so acp mode can be used with more providers
-5
View File
@@ -1,5 +0,0 @@
---
"cline": minor
---
Add Generate API Key on Hicap Provider selection
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Update SambaNova Provider models list and add temperature for models
-26
View File
@@ -1,26 +0,0 @@
changesDir: .changes
unreleasedDir: unreleased
headerPath: header.tpl.md
changelogPath: CHANGELOG.md
versionExt: md
versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
kindFormat: "### {{.Kind}}"
changeFormat: "* {{.Body}}"
kinds:
- label: Added
auto: minor
- label: Changed
auto: major
- label: Deprecated
auto: minor
- label: Removed
auto: major
- label: Fixed
auto: patch
- label: Security
auto: patch
newlines:
afterChangelogHeader: 1
beforeChangelogVersion: 1
endOfVersion: 1
envPrefix: CHANGIE_
+12 -1
View File
@@ -14,7 +14,7 @@ This file is the secret sauce for working effectively in this codebase. It captu
## Miscellaneous
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
- When creating PRs, if the change is user-facing and significant enough to warrant a changelog entry, run `npm run changeset` and create a patch changeset. Never create minor or major version bumps. Skip changesets for trivial fixes, internal refactors, or minor UI tweaks that users wouldn't notice.
- When creating PRs, contributors should not create changelog-entry files. Maintainers handle release versioning and changelog curation during the release process.
- When adding new feature flags, see this PR as a reference https://github.com/cline/cline/pull/7566
- Additional instructions about making requests: @.clinerules/network.md
@@ -147,6 +147,17 @@ Required steps:
Common mistake: Adding only the return value without the `context.globalState.get()` call. This compiles but the value is always `undefined` on load.
Settings plumbing gotcha: if a key is user-toggleable from settings, wire both controller update paths:
- `src/core/controller/state/updateSettings.ts` for webview `updateSetting(...)`
- `src/core/controller/state/updateSettingsCli.ts` for CLI/ACP settings updates
Missing one path causes a toggle to appear to change in one surface while the backend state stays unchanged.
Webview toggle gotcha: settings changes must also round-trip back in state payloads.
- Add the field to `UpdateSettingsRequest` in `proto/cline/state.proto` (for webview update requests), then run `npm run protos`
- Include the key in `Controller.getStateToPostToWebview()` (`src/core/controller/index.ts`)
- Ensure `ExtensionState` and webview defaults include the key (`src/shared/ExtensionMessage.ts`, `webview-ui/src/context/ExtensionStateContext.tsx`)
If this round-trip wiring is missing, the backend value can update but the toggle in webview appears stuck or reverts.
## StateManager Cache vs Direct globalState Access
StateManager uses an in-memory cache populated during `StateManager.initialize(context)` in `common.ts`. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
+64
View File
@@ -0,0 +1,64 @@
# Storage Architecture
Global settings, secrets and workspace state are stored in **file-backed JSON stores** under `~/.cline/data/`. This is the shared storage layer used by VSCode, CLI, and JetBrains.
## Key Abstractions
### `StorageContext` (src/shared/storage/storage-context.ts)
The entry point. Created via `createStorageContext()` and passed to `StateManager.initialize()`. Contains three `ClineFileStorage` instances:
- `globalState``~/.cline/data/globalState.json`
- `secrets``~/.cline/data/secrets.json` (mode 0o600)
- `workspaceState``~/.cline/data/workspaces/<hash>/workspaceState.json`
### `ClineFileStorage` (src/shared/storage/ClineFileStorage.ts)
Synchronous JSON key-value store backed by a single file. Supports `get()`, `set()`, `setBatch()`, `delete()`. Writes are atomic (write-then-rename).
### `StateManager` (src/core/storage/StateManager.ts)
In-memory cache on top of `StorageContext`. All runtime reads hit the cache; writes update cache immediately and debounce-flush to disk.
## ⚠️ Do NOT Use VSCode's ExtensionContext for Storage
**Do not** read from or write to `context.globalState`, `context.workspaceState`, or `context.secrets` for persistent data. These are VSCode-specific and not available on CLI or JetBrains.
Instead, use:
```typescript
// Reading state
StateManager.get().getGlobalStateKey("myKey")
StateManager.get().getSecretKey("mySecretKey")
StateManager.get().getWorkspaceStateKey("myWsKey")
// Writing state
StateManager.get().setGlobalState("myKey", value)
StateManager.get().setSecret("mySecretKey", value)
StateManager.get().setWorkspaceState("myWsKey", value)
```
Remember that your data may be read by a different client than the one that wrote it. For example, a value written by Cline in JetBrains may be read by Cline CLI.
## VSCode Migration (src/hosts/vscode/vscode-to-file-migration.ts)
On VSCode startup, a migration copies data from VSCode's `ExtensionContext` storage into the file-backed stores. This runs in `src/common.ts` before `StateManager.initialize()`.
- **Sentinel**: `__vscodeMigrationVersion` key in global state and workspace state — prevents re-migration.
- **Merge strategy**: File store wins. Existing values are never overwritten.
- **Safe downgrade**: VSCode storage is NOT cleared, so older extension versions still work.
## Adding New Storage Keys
1. Add to `src/shared/storage/state-keys.ts` (see existing patterns)
2. Read/write via `StateManager` (NOT via `context.globalState`)
3. If adding a secret, add to `SecretKeys` array in `state-keys.ts`
## File Layout
```
~/.cline/
data/
globalState.json # Global settings & state
secrets.json # API keys (mode 0o600)
tasks/
taskHistory.json # Task history (separate file)
workspaces/
<hash>/
workspaceState.json # Per-workspace toggles
```
+1 -1
View File
@@ -19,7 +19,7 @@ Review and address all comments on the current branch's PR.
- Inline comments: `gh api repos/{owner}/{repo}/pulls/{pr_number}/comments`
- General comments: `gh pr view {pr_number} --json comments,reviews`
4. Present a summary of all comments with your recommendation for each (apply, skip, or respond). Ignore bot noise (changeset-bot, CI status, etc.).
4. Present a summary of all comments with your recommendation for each (apply, skip, or respond). Ignore bot noise (release automation, CI status, etc.).
5. **Wait for my approval** before proceeding.
-549
View File
@@ -1,549 +0,0 @@
The goal of this workflow is to take a changeset for a release of Cline, an autonomous coding agent extension that plugs right into your IDE, and write the updated announcement component, and the updated changelog.
For reference, here are some examples of how we converted previous changesets to announcement components / changelogs.
- 3.14
<changeset>
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
claude-dev@3.14.0
Minor Changes
77c9863: create clinerules folder if its currently a file and creating new rule
0ffb7dd: disabling shift hint for now & improving tooltip behavior
79b76fd: Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile.
eb6e481: Full support for LaTeX rendering
df37f29: Add support for custom API request timeout. Previously, timeouts were hardcoded to 30 seconds for providers like Ollama or 15 seconds for OpenRouter and Cline. Now users can set a custom timeout value in milliseconds through the settings interface.
e4d26be: allow cursorrules and windsurfrules
c5de50f: Fix Handle @withRetry() SyntaxError when running extension locally issue
61d2f42: enabled pricing calculation for gemini and vertex + more robust caching & cache tracking for gemini & vertex
aed152b: add truncation notice when truncating manually
2fe2405: Migrate Cline Tools Section to new docs
19cc8bc: Add a timeout setting for the terminal connection, allowing users to adjust this if they are having timeout issues
03d4410: Added copy button to code blocks.
c78fe23: addressed race condition in terminal command usage
91e222f: add checkpoints after more messages
14230e7: add newrule slash command
1c7d33a: Add remote config with posthog allowing for disabling new features until they're reading, making for a better developer experience.
4196c14: add cache ui for open router and cline provider
d97424f: showing expanded task by default
5294e78: Refactor to not pass a message for showing the MCP View from the servers modal
70cc437: Fix Windows path issue: Correct handling of import.meta.url to avoid leading slash in pathname
4b697d8: Migrate the addRemoteServer to protobus
Patch Changes
c63d9a1: updated drag and drop text to say "drop" instead of "drag"
459adf0: Add markdown copy to chat
74ec823: Minor UX improvement to drag and drop ux
b0961f4: Remove linear pull request action
e9ce384: searchCommits protobus migration
5802b68: createRuleFile protobus migration
df7f9fc: Add dependsOn to more blocks in the tasks.json
41ae732: Fix for git commit mentions in repos with no git commits
7e78445: Adding args to allow Cursor to open workspaces (for checkpoint testing/development)
bdfda6f: feat(bedrock): Introduce Amazon Nova Premier
65243ad: Introduce UI library for future UI development
4565e06: checkIsImageURL migrated to protobus
5a8e9d8: protobus migration for openImage
deeda6e: Lowering Gemini cache TTL time
db0b022: Adding UI to show openrouter balance next to provider
4650ffa: deleteRuleFile protobus migration
d4bd755: fix cost calculation
</changeset>
<changelog>
## [3.14.0]
- Add UI to show openrouter balance next to provider
- Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile (Thanks @clicube!)
- Add more robust caching & cache tracking for gemini & vertex providers
- Add support for LaTeX rendering
- Add support for custom API request timeout. Timeouts were 15-30s, but can now be configured via settings for OpenRouter/Cline & Ollama (Thanks @WingsDrafterwork!)
- Add truncation notice when truncating manually
- Add a timeout setting for the terminal connection, allowing users to set a time to wait for terminal startup
- Add copy button to code blocks
- Add copy button to markdown blocks (Thanks @weshoke!)
- Add checkpoints to more messages
- Add slash command to create a new rules file (/newrule)
- Add cache ui for open router and cline provider
- Add Amazon Nova Premier model to Bedrock (Thanks @watany!)
- Add support for cursorrules and windsurfrules
- Add support for batch history deletion (Thanks @danix800!)
- Improve Drag & Drop experience
- Create clinerules folder creating new rule if it's needed
- Enable pricing calculation for gemini and vertex providers
- Refactor message handling to not show the MCP View of the server modal
- Migrate the addRemoteServer to protobus (Thanks @DaveFres!)
- Update task header to be expanded by default
- Update Gemini cache TTL time to 15 minutes
- Fix race condition in terminal command usage
- Fix to correctly handle `import.meta.url`, avoiding leading slash in pathname for Windows (Thanks @DaveFres!)
- Fix @withRetry() decoration syntax error when running extension locally (Thanks @DaveFres!)
- Fix for git commit mentions in repos with no git commits
- Fix cost calculation (Thanks @BarreiroT!)
</changelog>
<announcement-component>
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
return (
<div style={containerStyle}>
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
<h3 style={h3TitleStyle}>
🎉{" "}New in v{minorVersion}
</h3>
<ul style={ulStyle}>
<li>
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price tracking for
Gemini models.
</li>
<li>
<b>Copy Buttons:</b> Buttons were added to Markdown and Code blocks that allow you to copy their contents
easily.
</li>
<li>
<b>/newrule command:</b> New slash command to have cline write your .clinerules for you based on your
workflow.
</li>
<li>
<b>Drag and drop improvements:</b> Don't forget to hold shift while dragging files!
</li>
<li>Added more checkpoints across the task, allowing you to restore from more than just file changes.</li>
<li>Added support for rendering LaTeX in message responses. (Try asking Cline to show the quadratic formula)</li>
</ul>
<Accordion isCompact className="pl-0">
<AccordionItem
key="1"
aria-label="Previous Updates"
title="Previous Updates:"
classNames={{
trigger: "bg-transparent border-0 pl-0 pb-0 w-fit",
title: "font-bold text-(--vscode-foreground)",
indicator:
"text-(--vscode-foreground) mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
}}>
<ul style={ulStyle}>
<li>
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between
projects.
</li>
<li>
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files
to plug and play specific rules for the task
</li>
<li>
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a
new task (more coming soon!)
</li>
<li>
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally
restore your project when the message was sent!
</li>
</ul>
</AccordionItem>
</Accordion>
{/*
// Leave this here for an example of how to structure the announcement
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
so I recommend trying them out.
<br />
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink
href={getOpenRouterAuthUrl(vscodeUriScheme)}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Get OpenRouter API Key
</VSCodeButtonLink>
)}
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
<VSCodeButton
onClick={() => {
vscode.postMessage({
type: "apiConfiguration",
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
})
}}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Switch to OpenRouter
</VSCodeButton>
)}
</li>
<li>
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
</li>
<li>
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
</li>
<li>
When Cline runs commands, you can now type directly in the terminal (+ support for Python
environments)
</li>
</ul>*/}
<div style={hrStyle} />
<p style={linkContainerStyle}>
Join us on{" "}
<VSCodeLink style={linkStyle} href="https://x.com/cline">
X,
</VSCodeLink>{" "}
<VSCodeLink style={linkStyle} href="https://discord.gg/cline">
discord,
</VSCodeLink>{" "}
or{" "}
<VSCodeLink style={linkStyle} href="https://www.reddit.com/r/cline/">
r/cline
</VSCodeLink>
for more updates!
</p>
</div>
)
}
</announcement-component>
- 3.13
<changeset>
Minor Changes
2964388: Added copy button to MermaidBlock component
75143a7: Add the ability to fetch from global cline rules files
Patch Changes
a0252e7: convert inline style to tailwind css of file SettingsView.tsx
ab59bd9: Add stream options back to xai provider
7276f50: Icons to indicate an action is occuring outside of the users workspace
0b19ba6: update to NEW model
</changeset>
<changelog>
## [3.13.0]
- Add Cline rules popover under the chat field, allowing you to easily add, enable & disable workspace level or global rule files
- Add new slash command menu letting you type “/“ to do quick actions like creating new tasks
- Add ability to edit past messages, with options to restore your workspace back to that point
- Allow sending a message when selecting an option provided by the question or plan tool
- Add command to jump to Cline's chat input
- Add support for OpenAI o3 & 4o-mini (Thanks @PeterDaveHello and @arafatkatze!)
- Add baseURL option for Google Gemini provider (Thanks @owengo and @olivierhub!)
- Add support for Azure's DeepSeek model. (Thanks @yt3trees!)
- Add ability for models that support it to receive image responses from MCP servers (Thanks @rikaaa0928!)
- Improve search and replace diff editing by making it more flexible with models that fail to follow structured output instructions. (Thanks @chi-cat!)
- Add detection of Ctrl+C termination in terminal, improving output reading issues
- Fix issue where some commands with large output would cause UI to freeze
- Fix token usage tracking issues with vertex provider (Thanks @mzsima!)
- Fix issue with xAI reasoning content not being parsed (Thanks @mrubens!)
</changelog>
<announcement-component>
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
return (
<div style={containerStyle}>
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
<h3 style={h3TitleStyle}>
🎉{" "}New in v{minorVersion}
</h3>
<ul style={ulStyle}>
<li>
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between projects.
</li>
<li>
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files to plug
and play specific rules for the task
</li>
<li>
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a new task
(more coming soon!)
</li>
<li>
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally restore
your project when the message was sent!
</li>
</ul>
<h4 style={{ margin: "5px 0 5px" }}>Previous Updates:</h4>
<ul style={ulStyle}>
<li>
<b>Model Favorites:</b> You can now mark your favorite models when using Cline & OpenRouter providers for
quick access!
</li>
<li>
<b>Faster Diff Editing:</b> Improved animation performance for large files, plus a new indicator in chat
showing the number of edits Cline makes.
</li>
<li>
<b>New Auto-Approve Options:</b> Turn off Cline's ability to read and edit files outside your workspace.
</li>
</ul>
{/*
// Leave this here for an example of how to structure the announcement
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
so I recommend trying them out.
<br />
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink
href={getOpenRouterAuthUrl(vscodeUriScheme)}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Get OpenRouter API Key
</VSCodeButtonLink>
)}
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
<VSCodeButton
onClick={() => {
vscode.postMessage({
type: "apiConfiguration",
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
})
}}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Switch to OpenRouter
</VSCodeButton>
)}
</li>
<li>
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
</li>
<li>
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
</li>
<li>
When Cline runs commands, you can now type directly in the terminal (+ support for Python
environments)
</li>
</ul>*/}
<div style={hrStyle} />
<p style={linkContainerStyle}>
Join us on{" "}
<VSCodeLink style={linkStyle} href="https://x.com/cline">
X,
</VSCodeLink>{" "}
<VSCodeLink style={linkStyle} href="https://discord.gg/cline">
discord,
</VSCodeLink>{" "}
or{" "}
<VSCodeLink style={linkStyle} href="https://www.reddit.com/r/cline/">
r/cline
</VSCodeLink>
for more updates!
</p>
</div>
)
}
</announcement-component>
We have a changeset PR that automatically generated as new unreleased PRs are merged into main, the PR is always called "Changeset version bump" and the author is github-actions.
The Changeset PR description looks something like this:
<changeset-pr-description>
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or [setup this action to publish automatically](https://github.com/changesets/action#with-publishing). If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
# Releases
## claude-dev@3.16.0
### Minor Changes
- c6e8b04: Recent task list is now collapsible, allowing users to hide their recent tasks (e.g. when sharing their screen).
- aabe4ae: Add detection for new users to display special components
- 6c18d51: adds global endpoint for vertex ai users
- 080ed7c: Add Tailwind CSS IntelliSense to the the recommended extensions list
- 5147e28: new workflow feature
### Patch Changes
- c0b3c69: fix eternal loading states when the last message is a checkpoint
- 570ece3: selectImages protos migration
- 8d8452e: askResponse protobus migration
- cd1ff2a: Finishing the migration of Vscode Advanced settings to Settings Webview
</changeset-pr-description>
The changeset pr is ALWAYS on the following branch: `changeset-release/main`.
I have the `gh` command line tool set up and authenticated, so you have everything you need.
The first step is to get the full diff from the changeset PR to look at the changes that were automatically made to the `CHANGELOG.md` file. By default it will automatically add a new section to the changelog.md file with the new version. The problem with the automatically generated section is that it just takes the text that the developers threw into their changeset files for each corresponding PR, and they can be pretty vague and bad. Additionally there's some stuff that is totally irrelevant for the end user, like minor refactoring changes. So I manually typically go in and update this section to be a proper changelog that will show up in our patchnotes. You can look at how the rest of the file is done because those are all good examples of us updating this to use good language for the end user. We usually put new features up top (and the most exciting flagship features at the very top), and then bug fixes/improvements at the bottom. Having some basic organization to the ordering of the bullet points by content is nice. But use common sense.
To handle this process effectively, do the following:
For each of the automatically generated bullet points in the Changelog.md, you should
1. Take the commit hash at the start of the bullet point, and use the `gh` command line tool find the PR that it was associated with.
2. Use the `gh` command to get the PR title/description/discussion to understand the context surrounding the PR.
3. Use the `gh` command line tool to get the full PR diff to fully understand the changes made in the code.
4. Synthesize that knowledge to determine (a) whether or not this change is relevant to end users and (b) what the text & ordering of the line should be.
5. Update the `CHANGELOG.md` accordingly
Do this for every single item in the list from the autogenerated bullet points. We want to be diligent and have a full understanding of every feature so we can make the best changelog ever!
Here are some principles for good changelogs from keepchangelog.com, a handy guide:
<keepachangelog-pinciples-for-good-changelogs>
### Guiding Principles
- Changelogs are for humans, not machines.
- There should be an entry for every single version.
- The same types of changes should be grouped.
- The latest version comes first.
### Bullet points in the changelog should follow these principles:
- Types of changes
- Added for new features.
- Changed for changes in existing functionality.
- Deprecated for soon-to-be removed features.
- Removed for now removed features.
- Fixed for any bug fixes.
- Security in case of vulnerabilities.
</keepachangelog-pinciples-for-good-changelogs>
Lastly, when developers make a PR, they typically make a changeset. And they have 3 options when making the changeset:
1. Patch
2. Minor
3. Major
Sometimes they label something as minor when really it should just be a patch. Or vice versa. Because of this, the automatic version bump may be incorrect. So when starting out this workflow, you should use the <ask_followup_question> tool to confirm with me whether or not this should be a patch bump (show the old version number and what the proposed new version number would be) or a minor bump. Part of the release process is making sure the version in package.json that is automatically changed actually corresponds with what we decided the bump should actually be based on the features. ALL these modifications happen in the `changeset-release/main` branch btw.
<important_note>
Before doing any of this, make sure you check out the `changeset-release/main` and pull the most recent up to date changes. Then perform all this work in that branch.
New announcement banners should ONLY be made for minor version bumps or higher. That's another reason why double checking if the changelog warrants the bump is important.
Also, SUPER important: For any external contributors that aren't part of the cline github organization, we always want to add a (Thanks @username!) at the end of the changelog to attribute them properly. We're an open source project and it's ethical to do this.
</important_note>
Once the changelog looks good, and the version number looks good, we gotta double check that the version number in the changelog has the brackets around it. And as a final step, double check the package.json version number matches the latest number in the changelog. And as the ultimate final step we run `npm run install:all` to make sure the package version number permiates through the lock file.
<detailed_sequence_of_steps>
# Cline Release Process - Detailed Sequence of Steps
## Before Starting
1. First, examine the changeset PR without checking it out:
```bash
gh pr view changeset-release/main
```
2. View the PR diff to see the auto-generated CHANGELOG.md changes:
```bash
gh pr diff changeset-release/main > changeset-diff.txt
cat changeset-diff.txt | grep -A 50 "CHANGELOG.md"
```
## Initial Setup
3. Once you're ready to start, checkout and update the changeset release branch:
```bash
git checkout changeset-release/main
git pull origin changeset-release/main
```
## Analyzing Each Change
4. For each commit hash in the auto-generated changelog entries:
a. Find the PR number associated with a commit hash:
```bash
gh pr list --search "<commit-hash>" --state merged
```
b. Get PR details for better context:
```bash
gh pr view <PR-number>
```
c. Check if the contributor is external to determine if attribution is needed:
```bash
# Extract username from PR
USERNAME=$(gh pr view <PR-number> --json author --jq .author.login)
# Check if user is a member of the Cline organization
# this command is a bit finnicky, but it 100% works.
# if you see a `Error executing command: The command ran successfully, but we couldn't capture its output. Please proceed accordingly.` error, just retry it until you actually get the output
# don't make any assumptions, just retry the command to actually get the output and determine if they're external or not.
# no output means they are an external contributor, otherwise if there is output they are an internal contributor (part of our github org)
gh api "orgs/cline/members" --jq "map(.login)" | grep -i "pashpashpash"
```
d. View the full PR diff to understand code changes:
```bash
gh pr diff <PR-number> > pr-diff-<PR-number>.txt
cat pr-diff-<PR-number>.txt
```
## Updating the Changelog
5. Based on PR analysis, update the CHANGELOG.md with user-friendly descriptions:
- Use the `<replace_in_file>` tool to edit the CHANGELOG.md file
- Group by feature type (Added, Changed, Fixed)
- Put most exciting features at the top
- Move bug fixes and small improvements to the bottom
- Use clear, end-user focused language
- For external contributors, add attribution at the end of the relevant entry: `(Thanks @username!)`
## Version Number Verification
6. Confirm the version bump is appropriate:
- Check package.json to verify the auto-generated version number:
```bash
cat package.json | grep "\"version\""
```
- If the feature set doesn't warrant a minor bump, use the `<replace_in_file>` tool to modify package.json
7. Ensure the version in CHANGELOG.md has brackets around it:
```
## [3.16.0]
```
## Creating the Announcement (for minor/major versions only)
8. If this is a minor version bump, create/update the announcement component:
- Use the `<replace_in_file>` tool to edit the src/views/components/announcement.tsx file
- Update the highlights based on key features
- Move previous version highlights to the "Previous Updates" section
- Use the previous announcement components as reference for structure
## Finalizing the Release
9. Update dependencies with the new version number:
```bash
npm run install:all
```
10. Commit your changes:
```bash
git add CHANGELOG.md package.json package-lock.json src/views/components/announcement.tsx
git commit -m "Update CHANGELOG.md and announcement for version 3.16.0"
```
11. Push your changes to the changeset branch:
```bash
git push origin changeset-release/main
```
12. Check that your changes pushed successfully:
```bash
git status
```
</detailed_sequence_of_steps>
+3 -10
View File
@@ -89,16 +89,9 @@ On the main branch, create a commit that updates:
2. **package.json** - Update the version field to the new version
3. **Delete changesets** for the commits being included in the hotfix. This prevents the changeset bot from including duplicate entries in the next regular release.
3. No changelog-entry file cleanup is needed. Contributors do not create changelog-entry files in this repo.
Find and delete the changeset files associated with the selected commits:
```bash
ls .changeset/
```
Each changeset file in `.changeset/` corresponds to a PR. Read them to identify which ones belong to the commits you're hotfixing, then delete those files.
**Skip running `npm run install:all`** - the automation handles outdated lockfiles.
**Skip running `npm run install:all`** - release automation handles lockfile consistency as needed.
Commit with message format: `v{VERSION} Release Notes (hotfix)`
@@ -107,7 +100,7 @@ In the commit body, mention:
- List the cherry-picked commits that will be included
```bash
git add CHANGELOG.md package.json .changeset/
git add CHANGELOG.md package.json
git commit -m "v3.40.1 Release Notes (hotfix)
Hotfix release including:
-2
View File
@@ -347,8 +347,6 @@ A few notes:
So until the settings page is update, and this is added to settings in a way that's clean and doesn't confuse new users, I don't think we can merge this. Please bear with us.
</request_changes_comment>
<request_changes_comment>
Also, don't forget to add a changeset since this fixes a user-facing bug.
The architectural change is solid - moving the focus logic to the command handlers makes sense. Just don't want to introduce subtle timing issues by removing those timeouts.
</request_changes_comment>
</example_comments_that_i_have_written_before>
+40 -208
View File
@@ -1,232 +1,64 @@
# Release
Prepare and publish a release from the open changeset PR.
Prepare and publish a release directly from `main`.
## Overview
This workflow helps you:
1. Find and checkout the open changeset PR
2. Clean up the changelog (fix version format, wordsmith entries)
3. Push changes back to the PR branch
4. Merge with proper commit message format
5. Tag and push the release (after verifying the commit)
6. Trigger the publish workflow
7. Update GitHub release notes
8. Provide final summary with Slack announcement
1. Select/confirm the target version
2. Curate `CHANGELOG.md` entries manually for end users
3. Ensure `package.json` version matches the changelog
4. Create and push a release commit + tag
5. Trigger publish workflow
6. Update GitHub release notes and share a summary
## Step 1: Find the Changeset PR
## Process
Look for the open changeset PR:
```bash
gh pr list --search "Changeset version bump" --state open --json number,title,headRefName,url
```
If no PR is found, inform the user there's no changeset PR ready. They may need to:
- Merge PRs with changesets to main first
- Manually trigger the Changeset Converter workflow at: https://github.com/cline/cline/actions/workflows/changeset-converter.yml
## Step 2: Gather PR Information
Get the PR details:
```bash
PR_NUMBER=<number from step 1>
gh pr view $PR_NUMBER --json body,files,headRefName
```
Checkout the PR branch:
```bash
git fetch origin changeset-release/main
git checkout changeset-release/main
```
If the branch has diverged from remote, reset to the remote version:
```bash
git reset --hard origin/changeset-release/main
```
## Step 3: Analyze the Changes
Read the current CHANGELOG.md to see what the automation generated:
```bash
head -50 CHANGELOG.md
```
Get the version from package.json:
```bash
cat package.json | grep '"version"'
```
**Present to the user:**
- The version number that will be released
- The raw changelog entries from the changeset PR
- Whether this is a patch, minor, or major release
## Step 4: Clean Up the Changelog
The changelog needs these fixes:
1. **Add brackets to version number**: Change `## 3.44.1` to `## [3.44.1]`
2. **No category headers**: Don't use `### Added`, `### Fixed`, etc. Just a flat list of bullet points.
3. **Order entries from most important to least important**:
- Lead with major new features or significant fixes users care about
- End with minor fixes or internal changes
4. **Write user-friendly descriptions**:
- This is for end users, not developers—explain what changed in plain language
- Remove commit hashes from the beginning of lines (the automation adds these)
- Look at the actual commit diffs (`git show <hash>`) and PRs to understand what changed
- Write colorful descriptions that explain the value and impact, not just technical details
- Consolidate related changes into single entries when appropriate
**Ask the user** to review the proposed changelog changes before applying them. Show them:
- Current (raw) changelog section
- Proposed (cleaned) changelog section
Once approved, apply the changes to CHANGELOG.md.
## Step 5: Commit and Push Changes
After making changelog edits:
```bash
git add CHANGELOG.md
git commit -m "Clean up changelog formatting"
git push origin changeset-release/main
```
## Step 6: Merge the PR
**Ask the user to confirm** they're ready to merge.
Merge the PR with the proper commit message format:
```bash
VERSION=<version from package.json>
gh pr merge $PR_NUMBER --squash --subject "v${VERSION} Release Notes" --body ""
```
**If merge is blocked by branch protection:**
- Users with admin privileges can add the `--admin` flag to bypass
- Users without admin privileges need to get the PR approved through normal review first before merging
## Step 7: Tag the Release
After the merge completes, checkout main and pull:
### 1) Sync and determine version
```bash
git checkout main
git pull origin main
cat package.json | grep '"version"'
```
**IMPORTANT: Verify the latest commit is the release commit before tagging:**
Confirm the release version with the maintainer (patch/minor/major).
### 2) Curate changelog and version
- Edit `CHANGELOG.md` for the target version using human-friendly release notes.
- Ensure version headers use bracket format, e.g. `## [3.66.1]`.
- Update `package.json` version to the same value.
### 3) Commit and tag
```bash
git log -1 --oneline
git add CHANGELOG.md package.json package-lock.json
git commit -m "v<version> Release Notes"
git push origin main
git tag v<version>
git push origin v<version>
```
Confirm the commit message matches `v{VERSION} Release Notes` (e.g., `v3.44.1 Release Notes`). Do NOT blindly tag HEAD without verification.
### 4) Trigger publish workflow
Once verified, tag and push:
Tell the maintainer to run:
https://github.com/cline/cline/actions/workflows/publish.yml
Use `v<version>` as the release tag.
### 5) Update GitHub release notes
After publish completes:
```bash
VERSION=<version>
git tag v${VERSION}
git push origin v${VERSION}
gh release view v<version> --json body --jq '.body'
gh release edit v<version> --notes "<final curated release notes>"
```
## Step 8: Trigger Publish Workflow
### 6) Final summary
**Copy the tag to clipboard** so the user can easily paste it into the GitHub Actions workflow:
```bash
echo -n "v{VERSION}" | pbcopy
```
**Tell the user to trigger the publish workflow:**
1. Go to: https://github.com/cline/cline/actions/workflows/publish.yml
2. Select **"release"** for release-type
3. Paste **`v{VERSION}`** as the tag (already in clipboard)
**Wait for the user** to confirm the publish workflow has completed before proceeding.
## Step 9: Update GitHub Release Notes
Once the user confirms the publish workflow is done, fetch the auto-generated release content:
```bash
VERSION=<version>
gh release view v${VERSION} --json body --jq '.body'
```
The auto-generated release has:
- `## What's Changed` - PR list (we'll replace this with our changelog)
- `## New Contributors` - First-time contributors (keep this if present)
- `**Full Changelog**` - Comparison link (keep this)
Build the new release body:
1. Start with `## What's Changed` header
2. Add our changelog content (from CHANGELOG.md for this version)
3. Keep the `## New Contributors` section if it exists
4. Keep the `**Full Changelog**` link
Update the release:
```bash
gh release edit v${VERSION} --notes "<new body content>"
```
Verify the release was updated:
```bash
gh release view v${VERSION}
```
## Step 10: Final Summary
**Copy a Slack announcement message to clipboard** (include the full changelog, not just highlights):
```bash
echo "VS Code v{VERSION} Released
- Changelog entry 1
- Changelog entry 2
- Changelog entry 3" | pbcopy
```
**Present a final summary:**
- Version released: v{VERSION}
- PR merged: #{PR_NUMBER}
- Tag pushed: v{VERSION}
- Release: https://github.com/cline/cline/releases/tag/v{VERSION}
- Slack message copied to clipboard
**Final reminder:**
Post the Slack message to announce the release
## Handling Edge Cases
### No changesets found
If the changeset PR body shows no changes, inform the user they need to merge PRs with changesets first.
### Merge conflicts
If there are conflicts on the changeset branch, help the user resolve them:
```bash
git fetch origin main
git rebase origin/main
# resolve conflicts
git push origin changeset-release/main --force-with-lease
```
### User wants to add more changes
If the user wants to include additional PRs before releasing:
1. Ask them to merge those PRs to main first
2. The changeset automation will update the PR automatically
3. Re-run this workflow after the PR is updated
Provide:
- Released version/tag
- Link to release page
- Summary of top end-user changes
+49
View File
@@ -0,0 +1,49 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "cline"
[setup]
script = '''
if [ ! -d "node_modules" ]; then
MAIN_WORKTREE="$(git worktree list | head -n1 | awk '{print $1}')"
ln -s "$MAIN_WORKTREE/node_modules" node_modules
ln -s "$MAIN_WORKTREE/webview-ui/node_modules" webview-ui/node_modules
fi
'''
[[actions]]
name = "VS Code"
icon = "run"
command = "chmod +x ./scripts/run-extension-host.sh && ./scripts/run-extension-host.sh production"
[[actions]]
name = "CLI"
icon = "run"
command = '''
npm run cli:build
npm run cli:run
'''
[[actions]]
name = "npm install"
icon = "tool"
command = '''
rm node_modules
rm webview-ui/node_modules
npm run install:all
'''
[[actions]]
name = "pull main"
icon = "tool"
command = '''
git fetch origin main
if ! git merge-base --is-ancestor main origin/main; then
echo "Local main has commits not on origin/main. Aborting..."
exit 1
fi
git update-ref refs/heads/main refs/remotes/origin/main
echo "main updated to $(git rev-parse --short main)"
'''
-1
View File
@@ -60,7 +60,6 @@ We're not looking for exhaustive documentation - just evidence that you've thoug
- [ ] Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
- [ ] Tests are passing (`npm test`) and code is formatted and linted (`npm run format && npm run lint`)
- [ ] I have created a changeset using `npm run changeset` (required for user-facing changes)
- [ ] I have reviewed [contributor guidelines](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)
### Screenshots
@@ -1,79 +0,0 @@
"""
This script updates a specific version's release notes section in CHANGELOG.md with new content
or reformats existing content.
The script:
1. Takes a version number, changelog path, and optionally new content as input from environment variables
2. Finds the section in the changelog for the specified version
3. Either:
a) Replaces the content with new content if provided, or
b) Reformats existing content by:
- Removing the first two lines of the changeset format
- Ensuring version numbers are wrapped in square brackets
4. Writes the updated changelog back to the file
Environment Variables:
CHANGELOG_PATH: Path to the changelog file (defaults to 'CHANGELOG.md')
VERSION: The version number to update/format
PREV_VERSION: The previous version number (used to locate section boundaries)
NEW_CONTENT: Optional new content to insert for this version
"""
#!/usr/bin/env python3
import os
CHANGELOG_PATH = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md")
VERSION = os.environ['VERSION']
PREV_VERSION = os.environ.get("PREV_VERSION", "")
NEW_CONTENT = os.environ.get("NEW_CONTENT", "")
def overwrite_changelog_section(changelog_text: str, new_content: str):
# Find the section for the specified version
version_pattern = f"## {VERSION}\n"
unformmatted_prev_version_pattern = f"## {PREV_VERSION}\n"
prev_version_pattern = f"## [{PREV_VERSION}]\n"
print(f"latest version: {VERSION}")
print(f"prev_version: {PREV_VERSION}")
notes_start_index = changelog_text.find(version_pattern) + len(version_pattern)
notes_end_index = changelog_text.find(prev_version_pattern, notes_start_index) if PREV_VERSION and (prev_version_pattern in changelog_text or unformmatted_prev_version_pattern in changelog_text) else len(changelog_text)
if new_content:
return changelog_text[:notes_start_index] + f"{new_content}\n" + changelog_text[notes_end_index:]
else:
changeset_lines = changelog_text[notes_start_index:notes_end_index].split("\n")
filtered_lines = []
for line in changeset_lines:
# If the previous line is a changeset format
if len(filtered_lines) > 1 and filtered_lines[-1].startswith("### "):
# Remove the last two lines from the filted_lines
filtered_lines.pop()
filtered_lines.pop()
else:
filtered_lines.append(line.strip())
# Prepend a new line to the first line of filtered_lines
if filtered_lines:
filtered_lines[0] = "\n" + filtered_lines[0]
# Print filted_lines wiht a "\n" at the end of each line
for line in filtered_lines:
print(line.strip())
parsed_lines = "\n".join(line for line in filtered_lines)
updated_changelog = changelog_text[:notes_start_index] + parsed_lines + changelog_text[notes_end_index:]
return updated_changelog
with open(CHANGELOG_PATH, 'r') as f:
changelog_content = f.read()
new_changelog = overwrite_changelog_section(changelog_content, NEW_CONTENT)
# print("----------------------------------------------------------------------------------")
# print(new_changelog)
# print("----------------------------------------------------------------------------------")
# Write back to CHANGELOG.md
with open(CHANGELOG_PATH, 'w') as f:
f.write(new_changelog)
print(f"{CHANGELOG_PATH} updated successfully!")
-113
View File
@@ -1,113 +0,0 @@
name: Changeset Converter
run-name: Changeset Conversion
on:
workflow_dispatch:
pull_request:
types: [closed]
env:
REPO_PATH: ${{ github.repository }}
GIT_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'main' }}
NODE_VERSION: 20.18.1
jobs:
# Job 1: Create version bump PR when changesets are merged to main
changeset-pr-version-bump:
if: |
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main' &&
github.actor != 'github-actions'
)
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Check user for team affiliation
id: team_check
if: github.event_name == 'workflow_dispatch'
uses: morfien101/actions-authorized-user@4a3cfbf0bcb3cafe4a71710a278920c5d94bb38b
with:
username: ${{ github.actor }}
org: ${{ github.repository_owner }}
team: "deployer"
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Check if user is authorized
if: github.event_name == 'workflow_dispatch'
run: |
if [ "${{ steps.team_check.outputs.authorized }}" != "true" ]; then
echo "User is not authorized to run this workflow."
exit 1
fi
- name: Git Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ env.GIT_REF }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install Dependencies
run: npm ci
# Check if there are any new changesets to process
- name: Check for changesets
id: check-changesets
run: |
NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
echo "Changesets diff with previous version: $NEW_CHANGESETS"
echo "new_changesets=$NEW_CHANGESETS" >> $GITHUB_OUTPUT
# Create version bump PR using changesets/action if there are new changesets
- name: Create Changeset Pull Request
if: steps.check-changesets.outputs.new_changesets != '0'
uses: changesets/action@v1
with:
commit: "changeset version bump"
title: "Changeset version bump"
version: npm run version-packages # This performs the changeset version bump
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Get current and previous versions to edit changelog entry
- name: Get version
id: get_version
run: |
VERSION=$(git show HEAD:package.json | jq -r '.version')
echo "version=$VERSION" >> $GITHUB_OUTPUT
PREV_VERSION=$(git show origin/main:package.json | jq -r '.version')
echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
echo "version=$VERSION"
echo "prev_version=$PREV_VERSION"
# Update CHANGELOG.md with proper format
- name: Update Changelog Format
env:
VERSION: ${{ steps.get_version.outputs.version }}
PREV_VERSION: ${{ steps.get_version.outputs.prev_version }}
run: python .github/scripts/overwrite_changeset_changelog.py
# Commit and push changelog updates
- name: Push Changelog updates to Pull Request
run: |
git config user.name "github-actions"
git config user.email github-actions@github.com
echo "Running git add and commit..."
git add CHANGELOG.md
git commit -m "Updating CHANGELOG.md format"
git status
echo "--------------------------------------------------------------------------------"
echo "Pushing to remote..."
echo "--------------------------------------------------------------------------------"
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
git push origin $CURRENT_BRANCH
-173
View File
@@ -1,173 +0,0 @@
name: Claude Issue Triage
on:
issues:
types: [opened]
# Manual trigger for backfilling existing issues. Run from terminal:
# gh workflow run claude-issue-triage.yml -f issue_number=1234
# Or batch process:
# gh issue list --state open --limit 10 --json number --jq '.[].number' | while read num; do
# gh workflow run claude-issue-triage.yml -f issue_number=$num
# sleep 60
# done
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to triage'
required: true
type: string
jobs:
claude-issue-triage:
runs-on: ubuntu-latest
timeout-minutes: 120
# SECURITY: These permissions are intentionally restrictive.
# - contents: read -> Claude can read the codebase but CANNOT write/push any code
# - issues: write -> Claude can comment and add labels (the only write access needed)
# - pull-requests: read -> Claude can view PR context but CANNOT create PRs
# This ensures that even if a malicious user attempts prompt injection via issue content,
# Claude cannot modify repository code, create branches, or open PRs.
permissions:
contents: read
issues: write
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Issue Response & Triage
id: triage
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
# Allow all tools - security is enforced by GitHub permissions above (contents: read, issues: write)
claude_args: --model claude-opus-4-5-20251101 --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch"
prompt: |
You're a GitHub issue first responder for the open source Cline repository.
**Issue:** #${{ github.event.issue.number || inputs.issue_number }}
**Title:** ${{ github.event.issue.title || 'See issue details below' }}
**Author:** @${{ github.event.issue.user.login || 'See issue details below' }}
## Your job
Investigate this issue thoroughly, then post a single helpful comment that helps the user and gives maintainers the context they need.
## Investigation
Start by reading the full issue:
gh issue view ${{ github.event.issue.number || inputs.issue_number }}
### Search for duplicates and related issues
Search thoroughly for existing issues that match this one:
gh issue list --search "<keywords from the issue>" --state all --limit 30
gh issue list --search "<error messages>" --state all --limit 20
gh issue list --search "<affected feature/component>" --state all --limit 20
For each relevant issue you find, read it including its comments:
gh issue view <number> --comments
You're looking for:
- **Duplicates**: Issues describing the same problem. Link to them and explain why you think they're duplicates. If closed, check how they were resolved - the solution might apply here.
- **Related issues**: Similar problems or context that could help. Pull useful information from their comments (workarounds others found, debugging steps that helped, maintainer explanations). Link to them and explain the connection.
If there are closed issues with solutions, surface those solutions prominently - this might immediately solve the user's problem.
### Analyze recent changes (ALWAYS DO THIS)
Many issues are regressions from recent releases. **Always** check what changed recently:
gh release list --limit 10
gh pr list --state merged --limit 50 --json number,title,mergedAt,author,body
Look for PRs merged in the last few weeks that might correlate with the issue. If you find a likely connection:
gh pr view <number>
gh pr diff <number>
git log --since="1 month ago" --oneline -- <relevant paths>
git show <commit>
**Always include your findings in your comment:**
- If you find a regression, call it out explicitly: which PR/commit likely caused it, who authored it, what changed, and suggest a fix direction if you can see one.
- If you don't find anything related, still mention it: "I analyzed recent PRs and releases but didn't find any changes that seem related to this issue."
### Search the codebase
Find the relevant code:
- Use grep/find to locate code related to the issue
- Key areas: `src/api/` (providers/models), `src/core/prompts/` (tools/prompts), platform-specific code for VS Code vs JetBrains
### Find documentation
Cline docs are at **https://docs.cline.bot/** and built with Mintlify from the `docs/` directory.
The URL structure maps directly to the file structure:
- `docs/getting-started/selecting-your-model.mdx` → https://docs.cline.bot/getting-started/selecting-your-model
- `docs/troubleshooting.mdx` → https://docs.cline.bot/troubleshooting
- Headings become anchors: `## Which Model` → `#which-model`
Search the `docs/` directory to find relevant documentation, then construct URLs to link users to:
```bash
ls docs/
grep -r "keyword" docs/ --include="*.mdx" -l
```
### Identify subject matter experts
For issues that clearly need engineering attention:
git log --since="6 months ago" --format="%an" -- <relevant paths> | sort | uniq -c | sort -rn | head -5
Cross-reference with GitHub usernames. Include in your response (@mention, do NOT assign):
| SME | Reason |
|-----|--------|
| @username1 | Authored PR #X which modified this area |
| @username2 | Primary contributor to affected file |
## Weak model detection
Many issues are caused by users running small or non-frontier models that don't tool-call reliably. Signs include:
- Model failing to use tools correctly
- Nonsensical or malformed responses
- User is running a small/local model or older model version
If this looks like a weak model issue, kindly suggest they try reproducing with Claude Sonnet and report back if it persists. Link to https://docs.cline.bot/getting-started/selecting-your-model if helpful. Still label and triage normally.
## Your comment
Write a single comment as a helpful community member. Be conversational, not robotic. Include what's relevant:
- **Helpful response** - Answer their question, suggest a fix, provide a workaround. If you found solutions in related closed issues, surface those prominently.
- **Duplicates and related issues** - Link to any you found and explain why they're duplicates/related. Summarize useful context from their comments.
- **Regression analysis** - If this looks like a regression, explain what change likely caused it, link to the PR/commit, and tag the author.
- **Clarifying questions** - If you need more info, ask specific questions. Don't ask for things already provided.
- **SME table** - Include the table above if this needs engineering attention. Don't tag people for questions with obvious answers or weak-model issues.
- **Context for maintainers** - Relevant code paths, what you found. Keep it concise.
- **Docs links** - If there's relevant documentation, link to it naturally in your response as a recommendation (e.g., "For more details, check out [the Ollama setup guide](url)"). Do NOT add a "Sources" section at the end - integrate doc links into your response where they're helpful.
- **Possible Duplicates section** - ALWAYS include a "Possible Duplicates" section at the end of your comment listing issues that might be duplicates so maintainers can quickly close if appropriate. If none found, say "No obvious duplicates found."
## Labels
First, retrieve all available labels and read their descriptions to understand what each is for:
gh label list --json name,description --limit 100
Then apply the appropriate labels based on your analysis. Only use labels from the list above—do not create new labels.
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "label1,label2"
If your regression analysis found a likely culprit (a recent PR/commit that probably caused this issue), add the "Regression" label:
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "Regression"
IMPORTANT: After posting your comment, add the "Bot Responded" label to indicate this issue has received an automated response:
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "Bot Responded"
## Remember
- **This is a one-time automated response** - you will NOT see their reply or respond again. Never say things like "I can help you", "let me know", "once I have that info", or "I can give you more targeted help" - you won't be there to follow up. If you ask clarifying questions, frame them for the maintainers who will follow up, e.g., "If you can share X, that would help the maintainers diagnose this."
- Don't be formulaic. Respond to what the issue actually needs.
- Surface solutions from past issues - often the fastest path to helping.
- Connecting regressions to specific changes is extremely valuable.
- Link issues with #number so they're clickable.
-284
View File
@@ -1,284 +0,0 @@
name: Claude PR Review
on:
pull_request:
types: [opened, ready_for_review]
# Manual trigger for backfilling existing PRs. Run from terminal:
# gh workflow run claude-pr-review.yml -f pr_number=1234
# Or batch process open PRs:
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
# gh workflow run claude-pr-review.yml -f pr_number=$num
# sleep 60
# done
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to review'
required: true
type: string
jobs:
claude-pr-review:
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
if: |
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 120
# SECURITY: These permissions are intentionally restrictive.
# - contents: read -> Claude can read the codebase but CANNOT write/push any code
# - pull-requests: write -> Claude can post reviews and inline suggestions
# - issues: read -> Claude can search for related issues
# NOTE: Even with pull-requests: write, Claude CANNOT merge PRs because branch protection
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
permissions:
contents: read
pull-requests: write
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Print HEAD commit
run: |
echo "HEAD is at: $(git rev-parse HEAD)"
echo "Short: $(git rev-parse --short HEAD)"
git log -1 --format="Commit: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s"
- name: Get PR number
id: pr
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
else
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
fi
- name: Run PR Review
id: review
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
claude_args: --model claude-opus-4-5-20251101 --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch"
prompt: |
You're a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
PR: #${{ steps.pr.outputs.number }}
## Gather context
```bash
# Get full PR details
gh pr view ${{ steps.pr.outputs.number }} --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
# Get the diff
gh pr diff ${{ steps.pr.outputs.number }}
# Check CI status
gh pr checks ${{ steps.pr.outputs.number }}
# Get existing review comments (to understand context and your previous feedback)
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments --jq '.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'
# Get conversation comments
gh pr view ${{ steps.pr.outputs.number }} --comments
```
If this is a re-review (workflow_dispatch event):
Read your previous comments carefully. Understand what you asked for before.
Check if new commits or comments address your previous feedback.
## Check contributing guidelines
Flag (but don't block) if:
- Missing changeset - For user-facing changes, check if there's a `.changeset/` file:
```bash
gh pr diff ${{ steps.pr.outputs.number }} --name-only | grep '.changeset/' || echo "No changeset found"
```
If missing, ask them to run `npm run changeset`
- Missing tests - New features should have tests
## Find related issues and PRs
Search thoroughly for context that might help with the review:
```bash
# Find related issues for context
gh issue list --search "<keywords from the PR>" --state all --limit 30
gh issue list --search "<error messages or feature names>" --state all --limit 20
# Find similar PRs for reference
gh pr list --search "<keywords>" --state all --limit 30
```
For each relevant issue or PR you find, read it including comments:
```bash
gh issue view <number> --comments
gh pr view <number> --comments
```
Look for:
- Open issues this PR might fix that weren't linked in the description
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
- Context from maintainer discussions that could inform your review
## Find subject matter experts
For files changed in this PR, find who knows the code best:
```bash
# Get files changed
gh pr diff ${{ steps.pr.outputs.number }} --name-only
# For each relevant path, find contributors
git log --since="6 months ago" --format="%an" -- <path> | sort | uniq -c | sort -rn | head -5
```
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
| SME | Reason |
|-----|--------|
| @username1 | Authored PR #X which modified this area |
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
| @username3 | Reviewed similar PR #Y with extensive feedback |
## Deep code review
This is the most important part. Don't just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
Step 1: Understand the intent
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
Step 2: Form your own opinion first
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
Step 3: Compare approaches
Now look at their implementation. How does it compare to what you would have done?
- Is their approach better in some ways? Note what they did well.
- Is their approach missing something? Be specific about what and why.
- Are there edge cases they haven't considered?
- Does it follow the patterns established in similar parts of the codebase?
Step 4: Look at the bigger picture
- What other files or systems does this change interact with?
- Could this break anything else?
- Is there additional work needed beyond this PR to complete the feature?
- Does this fit well with the overall architecture?
Step 5: Find reference implementations
Look for similar changes in the codebase:
```bash
git log --oneline --all --grep="<relevant keywords>" | head -20
git log --oneline -- <similar files> | head -20
```
If this is adding a new API provider, look at how other providers are implemented.
If this is adding a new feature, look at how similar features were added.
Note where their implementation aligns with or diverges from established patterns.
Step 6: Standard code review checks
- DRY: Is there duplicated code that could be extracted?
- Error handling: Are errors handled appropriately?
- Security: Any injection risks, credential exposure, unsafe dependencies?
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
- Types: Is TypeScript used correctly? Any unsafe type assertions?
- Naming: Are variables and functions named clearly?
- Comments: Is complex logic explained? Are there outdated comments?
## Inline code suggestions
For specific code improvements, use GitHub's suggestion syntax via `gh api`.
This creates suggestions the author can commit with one click.
Single-line suggestion:
```bash
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews \
-X POST \
-f commit_id="$(gh pr view ${{ steps.pr.outputs.number }} --json headRefOid -q .headRefOid)" \
-f event="COMMENT" \
-f body="" \
-F comments='[
{
"path": "src/example.ts",
"line": 42,
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
}
]'
```
Multi-line suggestion (replacing lines 40-45):
```bash
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews \
-X POST \
-f commit_id="$(gh pr view ${{ steps.pr.outputs.number }} --json headRefOid -q .headRefOid)" \
-f event="COMMENT" \
-f body="" \
-F comments='[
{
"path": "src/example.ts",
"start_line": 40,
"line": 45,
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
}
]'
```
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
## Post your review
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
Start by noting the commit hash you reviewed:
```bash
git rev-parse --short HEAD
```
Include this at the top of your comment: "Reviewed at commit: <short hash>"
Then thank them for their contribution. Be conversational, not robotic.
Include what's relevant:
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author's intent, why they made the changes, how they implemented it, and what files/systems are affected. Don't just summarize - explain.
- Related issues/PRs you found that provide useful context (link to them)
- Your review findings (issues to address, suggestions, etc.)
- Clear next steps for the author
Include a "For Maintainers" section with:
- Anything else useful to help the maintainer resolve this PR
- Related issues/PRs with context on why they're relevant
- Open issues this PR might fix that weren't linked in the description
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
- SME table - who should review this and why
For the SME table:
| SME | Reason |
|-----|--------|
| @username | Primary contributor to affected files |
## Update labels
Add appropriate labels based on your analysis:
```bash
gh label list --json name,description --limit 100
gh pr edit ${{ steps.pr.outputs.number }} --add-label "label1,label2"
```
When done, add the reviewed label:
```bash
gh pr edit ${{ steps.pr.outputs.number }} --add-label "Bot Reviewed"
```
## Remember
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like "let me know if you have questions", "I can help you with", or "feel free to ask" - you won't be there to follow up. Frame any questions for the maintainers who will follow up.
- Be helpful and welcoming - Many contributors are new to the project
- Be specific - Point to exact lines and suggest fixes, don't give vague feedback
- Think deeply - Don't just surface-level review, understand the intent and evaluate the approach
- Use inline suggestions - Make it easy for authors to accept changes
- You're a first-pass reviewer - A human maintainer will do final approval
@@ -0,0 +1,85 @@
name: Smoke Tests
on:
push:
branches: [main]
paths:
- 'src/core/**'
- 'src/shared/**'
- 'proto/**'
- 'evals/**'
- '.github/workflows/cline-evals-regression.yml'
pull_request:
paths:
- 'src/core/**'
- 'src/shared/**'
- 'proto/**'
- 'evals/**'
- '.github/workflows/cline-evals-regression.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: smoke-tests-${{ github.ref }}
cancel-in-progress: true
jobs:
smoke-tests:
name: Smoke Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build and install CLI
run: |
npm run protos
cd cli && npm install && npm run build && npm link
echo "$(npm config get prefix)/bin" >> $GITHUB_PATH
- name: Verify CLI
run: cline --version
- name: Run smoke tests
env:
CLINE_API_KEY: ${{ secrets.CLINE_API_KEY }}
run: |
cline auth -p cline -k "$CLINE_API_KEY" -m "anthropic/claude-sonnet-4.5"
max_attempts=3
for attempt in $(seq 1 $max_attempts); do
echo "::group::Attempt $attempt of $max_attempts"
if npx tsx evals/smoke-tests/run-smoke-tests.ts --trials 1 --parallel; then
echo "::endgroup::"
echo "Smoke tests passed on attempt $attempt"
exit 0
fi
echo "::endgroup::"
if [ $attempt -lt $max_attempts ]; then
echo "::warning::Smoke tests failed on attempt $attempt, retrying..."
sleep 10
fi
done
echo "::error::Smoke tests failed after $max_attempts attempts"
exit 1
- name: Generate summary
if: always()
run: cat evals/smoke-tests/results/latest/summary.md >> $GITHUB_STEP_SUMMARY
- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: smoke-test-results-${{ github.run_id }}
path: evals/smoke-tests/results/latest/
retention-days: 30
-325
View File
@@ -1,325 +0,0 @@
name: Cline PR Code Review
on:
pull_request:
types:
[opened, ready_for_review]
# Manual trigger for backfilling existing PRs. Run from terminal:
# gh workflow run cline-pr-review.yml -f pr_number=1234
# Or batch process open PRs:
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
# gh workflow run cline-pr-review.yml -f pr_number=$num
# sleep 60
# done
workflow_dispatch:
inputs:
pr_number:
description: "PR number to review"
required: true
type: string
concurrency:
group: pr-review-${{ github.event.pull_request.number || inputs.pr_number }}
cancel-in-progress: true
jobs:
cline-pr-review:
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
if: |
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 60
# SECURITY: These permissions are intentionally restrictive.
# - contents: read -> cline can read the codebase but CANNOT write/push any code
# - pull-requests: write -> cline can post reviews and inline suggestions
# - issues: read -> cline can search for related issues
# NOTE: Even with pull-requests: write, cline CANNOT merge PRs because branch protection
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
permissions:
contents: read
pull-requests: write
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Print HEAD commit
run: |
echo "HEAD is at: $(git rev-parse HEAD)"
echo "Short: $(git rev-parse --short HEAD)"
git log -1 --format="Commit: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
- name: Install and Verify Cline CLI
run: |
npm install -g cline
cline version # verify installation
- name: Configure Cline with Anthropic
run: |
npx cline auth --provider anthropic \
--apikey "${{ secrets.ANTHROPIC_API_KEY }}" \
--modelid claude-opus-4-5-20251101
- name: Get PR number
id: pr
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
else
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
fi
- name: Review PR with Cline
env:
PR_NUMBER: ${{ steps.pr.outputs.number }}
GITHUB_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
CLINE_COMMAND_PERMISSIONS: |
{
"allow": [
"gh pr diff *",
"gh pr view *",
"gh pr checks *",
"gh pr list *",
"gh label list *",
"gh issue list *",
"gh issue view *",
"git log *",
"gh pr comment ${{ steps.pr.outputs.number }} *",
"gh pr edit ${{ steps.pr.outputs.number }} *",
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments *",
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews *"
]
}
run: |
npx cline --yolo 'You'\''re a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
PR: #'"${PR_NUMBER}"'
## Gather context
```bash
# Get full PR details
gh pr view '"${PR_NUMBER}"' --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
# Get the diff
gh pr diff '"${PR_NUMBER}"'
# Check CI status
gh pr checks '"${PR_NUMBER}"'
# Get existing review comments (to understand context and your previous feedback)
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/comments --jq '\''.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'\''
# Get conversation comments
gh pr view '"${PR_NUMBER}"' --comments
```
If this is a re-review (workflow_dispatch event):
Read your previous comments carefully. Understand what you asked for before.
Check if new commits or comments address your previous feedback.
## Check contributing guidelines
Flag (but don'\''t block) if:
- Missing changeset - For user-facing changes, check if there'\''s a `.changeset/` file:
```bash
gh pr diff '"${PR_NUMBER}"' --name-only | grep '\''.changeset/'\'' || echo '\''No changeset found'\''
```
If missing, ask them to run `npm run changeset`
- Missing tests - New features should have tests
## Find related issues and PRs
Search thoroughly for context that might help with the review:
```bash
# Find related issues for context
gh issue list --search '\''<keywords from the PR>'\'' --state all --limit 30
gh issue list --search '\''<error messages or feature names>'\'' --state all --limit 20
# Find similar PRs for reference
gh pr list --search '\''<keywords>'\'' --state all --limit 30
```
For each relevant issue or PR you find, read it including comments:
```bash
gh issue view <number> --comments
gh pr view <number> --comments
```
Look for:
- Open issues this PR might fix that weren'\''t linked in the description
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
- Context from maintainer discussions that could inform your review
## Find subject matter experts
For files changed in this PR, find who knows the code best:
```bash
# Get files changed
gh pr diff '"${PR_NUMBER}"' --name-only
# For each relevant path, find contributors
git log --since='\''6 months ago'\'' --format='\''%an'\'' -- <path> | sort | uniq -c | sort -rn | head -5
```
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
| SME | Reason |
|-----|--------|
| @username1 | Authored PR #X which modified this area |
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
| @username3 | Reviewed similar PR #Y with extensive feedback |
## Bash command usage
Don'\''t use operators like `|`, `&&`, or `;` - run each command separately and analyze the output.
When referencing command outputs, quote them properly to avoid formatting issues.
## Deep code review
This is the most important part. Don'\''t just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
Step 1: Understand the intent
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
Step 2: Form your own opinion first
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
Step 3: Compare approaches
Now look at their implementation. How does it compare to what you would have done?
- Is their approach better in some ways? Note what they did well.
- Is their approach missing something? Be specific about what and why.
- Are there edge cases they haven'\''t considered?
- Does it follow the patterns established in similar parts of the codebase?
Step 4: Look at the bigger picture
- What other files or systems does this change interact with?
- Could this break anything else?
- Is there additional work needed beyond this PR to complete the feature?
- Does this fit well with the overall architecture?
Step 5: Find reference implementations
Look for similar changes in the codebase:
```bash
git log --oneline --all --grep='\''<relevant keywords>'\'' | head -20
git log --oneline -- <similar files> | head -20
```
If this is adding a new API provider, look at how other providers are implemented.
If this is adding a new feature, look at how similar features were added.
Note where their implementation aligns with or diverges from established patterns.
Step 6: Standard code review checks
- DRY: Is there duplicated code that could be extracted?
- Error handling: Are errors handled appropriately?
- Security: Any injection risks, credential exposure, unsafe dependencies?
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
- Types: Is TypeScript used correctly? Any unsafe type assertions?
- Naming: Are variables and functions named clearly?
- Comments: Is complex logic explained? Are there outdated comments?
## Inline code suggestions
For specific code improvements, use GitHub'\''s suggestion syntax via `gh api`.
This creates suggestions the author can commit with one click.
Single-line suggestion:
```bash
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
-X POST \
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
-f event='\''COMMENT'\'' \
-f body='\'''\'' \
-F comments='\''[
{
"path": "src/example.ts",
"line": 42,
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
}
]'\''
```
Multi-line suggestion (replacing lines 40-45):
```bash
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
-X POST \
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
-f event='\''COMMENT'\'' \
-f body='\'''\'' \
-F comments='\''[
{
"path": "src/example.ts",
"start_line": 40,
"line": 45,
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
}
]'\''
```
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
## Post your review
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
Start by noting the commit hash you reviewed:
```bash
git rev-parse --short HEAD
```
Include this at the top of your comment: "Reviewed at commit: <short hash>"
Then thank them for their contribution. Be conversational, not robotic.
Include what'\''s relevant:
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author'\''s intent, why they made the changes, how they implemented it, and what files/systems are affected. Don'\''t just summarize - explain.
- Related issues/PRs you found that provide useful context (link to them)
- Your review findings (issues to address, suggestions, etc.)
- Clear next steps for the author
Include a '\''For Maintainers'\'' section with:
- Anything else useful to help the maintainer resolve this PR
- Related issues/PRs with context on why they'\''re relevant
- Open issues this PR might fix that weren'\''t linked in the description
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
- SME table - who should review this and why
For the SME table:
| SME | Reason |
|-----|--------|
| @username | Primary contributor to affected files |
## Update labels
Add appropriate labels based on your analysis:
```bash
gh label list --json name,description --limit 100
gh pr edit '"${PR_NUMBER}"' --add-label '\''label1,label2'\''
```
When done, add the reviewed label:
```bash
gh pr edit '"${PR_NUMBER}"' --add-label '\''Bot Reviewed'\''
```
## Remember
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like '\''let me know if you have questions'\'', '\''I can help you with'\'', or '\''feel free to ask'\'' - you won'\''t be there to follow up. Frame any questions for the maintainers who will follow up.
- Be helpful and welcoming - Many contributors are new to the project
- Be specific - Point to exact lines and suggest fixes, don'\''t give vague feedback
- Think deeply - Don'\''t just surface-level review, understand the intent and evaluate the approach
- Use inline suggestions - Make it easy for authors to accept changes
- You'\''re a first-pass reviewer - A human maintainer will do final approval'
+4 -14
View File
@@ -1,7 +1,7 @@
name: Publish NPM Release
on:
workflow_dispatch:
workflow_call:
inputs:
confirm_publish:
description: 'Type "publish" to confirm you want to publish to NPM'
@@ -10,6 +10,7 @@ on:
permissions:
contents: write # Required for pushing tags
id-token: write # Required for npm trusted publishing (OIDC)
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
@@ -20,7 +21,7 @@ jobs:
publish-npm-release:
needs: test
name: Publish Cline CLI to NPM
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main' && github.event.inputs.confirm_publish == 'publish'
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main' && inputs.confirm_publish == 'publish'
runs-on: ubuntu-latest
steps:
@@ -30,19 +31,10 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Install root dependencies and CLI dependencies
if: steps.check_commits.outputs.skip != 'true'
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
- name: Generate Protos
@@ -81,8 +73,6 @@ jobs:
cat dist-standalone/package.json | grep version
- name: Publish to NPM with latest tag
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'latest'..."
cd dist-standalone
+15 -15
View File
@@ -1,12 +1,17 @@
name: Publish NPM Nightly
on:
schedule:
- cron: "0 12 * * *" # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
workflow_call:
inputs:
force_publish:
description: "Force publish even if there are no commits in the last 24 hours"
required: false
type: boolean
default: false
permissions:
contents: read
id-token: write # Required for npm trusted publishing (OIDC)
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
@@ -27,6 +32,12 @@ jobs:
- name: Check for recent commits
id: check_commits
run: |
if [ "${{ inputs.force_publish }}" = "true" ]; then
echo "force_publish enabled, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
fi
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
echo "No commits in last 24 hours, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
@@ -39,18 +50,9 @@ jobs:
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: "20.x"
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
if: steps.check_commits.outputs.skip != 'true'
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Install root dependencies and CLI dependencies
if: steps.check_commits.outputs.skip != 'true'
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
@@ -118,8 +120,6 @@ jobs:
- name: Publish to NPM with nightly tag
if: steps.check_commits.outputs.skip != 'true'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'nightly'..."
cd dist-standalone
+215
View File
@@ -0,0 +1,215 @@
# Build and Pack CLI
#
# Builds a CLI tarball from any branch/commit and publishes it as a GitHub Release.
# Requires write access to the repository (maintainers/collaborators only).
#
# Security: Split into two jobs to isolate untrusted build code from write tokens.
# The build job runs arbitrary ref code with zero permissions. The release job
# only runs trusted GitHub Actions with write scope.
#
# Usage (helper script, auto-detects current branch):
# ./scripts/build-cli-artifact.sh
# ./scripts/build-cli-artifact.sh feature/my-changes
# ./scripts/build-cli-artifact.sh feature/my-changes 1234 # comments on PR
#
# Usage (gh CLI directly):
# gh workflow run pack-cli.yml -f ref=main
# gh workflow run pack-cli.yml -f ref=abc123 -f pr_number=1234
#
# Install the built CLI (no auth required):
# npm install -g https://github.com/cline/cline/releases/download/cli-build-<sha>/cline-<ver>.tgz
#
# Find releases:
# gh release list --limit 10
name: Build and Pack CLI
permissions:
contents: read
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch, tag, or commit SHA to build (leave empty for default branch)'
required: false
type: string
pr_number:
description: 'PR number to comment on with install instructions (optional)'
required: false
type: number
jobs:
# ── Build job: runs untrusted ref code with ZERO permissions ──
build:
name: Build CLI
runs-on: ubuntu-latest
permissions: {}
outputs:
commit_sha: ${{ steps.commit.outputs.sha }}
tarball: ${{ steps.pack.outputs.tarball }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Get commit SHA
id: commit
run: |
COMMIT_SHA=$(git rev-parse --short HEAD)
echo "sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "Building from commit: $COMMIT_SHA"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Install dependencies
run: npm ci --include=optional
- name: Generate Protos
run: npm run protos
- name: Build standalone package
run: node scripts/package-npm.mjs
- name: Create Tarball
id: pack
run: |
cd dist-standalone
TARBALL=$(npm pack)
echo "tarball=$TARBALL" >> $GITHUB_OUTPUT
echo "Created tarball: $TARBALL"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: cli-tarball
path: dist-standalone/*.tgz
# ── Release job: only trusted Actions code, with write permissions ──
release:
name: Release CLI
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: cli-tarball
path: dist-standalone
- name: Create GitHub Release
id: create_release
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const commit = '${{ needs.build.outputs.commit_sha }}';
const tarball = '${{ needs.build.outputs.tarball }}';
// Delete existing release/tag if re-running for the same commit
const tagName = `cli-build-${commit}`;
try {
const existing = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tagName
});
await github.rest.repos.deleteRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: existing.data.id
});
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `tags/${tagName}`
});
core.info(`Deleted existing release for ${tagName}`);
} catch (e) {
// Release doesn't exist yet, that's fine
}
// Create a release
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `CLI Build (${commit})`,
body: `Automated CLI build from commit ${commit}\n\nInstall with:\n\`\`\`bash\nnpm install -g https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}\n\`\`\``,
draft: false,
prerelease: true
});
// Upload the tarball as a release asset
const tarballPath = path.join('dist-standalone', tarball);
const tarballData = fs.readFileSync(tarballPath);
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.data.id,
name: tarball,
data: tarballData
});
const downloadUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}`;
core.setOutput('release_url', release.data.html_url);
core.setOutput('download_url', downloadUrl);
- name: Comment on PR with download instructions
if: inputs.pr_number != ''
uses: actions/github-script@v7
with:
script: |
const commit = '${{ needs.build.outputs.commit_sha }}';
const releaseUrl = '${{ steps.create_release.outputs.release_url }}';
const downloadUrl = '${{ steps.create_release.outputs.download_url }}';
const prNumber = ${{ inputs.pr_number || 0 }};
if (!prNumber) return;
const comment = `## 📦 CLI Build Ready
A CLI build has been created for commit \`${commit}\`.
### Install Directly from URL (No Authentication Required!)
\`\`\`bash
npm install -g ${downloadUrl}
\`\`\`
### Alternative: Download and Install
\`\`\`bash
curl -L ${downloadUrl} -o cline.tgz
npm install -g ./cline.tgz
\`\`\`
📦 [View Release](${releaseUrl})
`;
await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
- name: Summary
run: |
echo "✅ CLI build complete!"
echo ""
echo "📦 Release: ${{ steps.create_release.outputs.release_url }}"
echo "🔗 Download URL: ${{ steps.create_release.outputs.download_url }}"
echo ""
echo "Install from anywhere (no authentication required):"
echo " npm install -g ${{ steps.create_release.outputs.download_url }}"
@@ -0,0 +1,55 @@
name: Publish CLI (Trusted)
on:
schedule:
- cron: "0 12 * * *" # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
inputs:
publish_target:
description: "Which publish flow to run"
required: true
default: "main"
type: choice
options:
- main
- nightly
confirm_publish:
description: 'Required when publish_target=main. Type "publish" to confirm release publish.'
required: false
type: string
force_nightly_publish:
description: "Force nightly publish even with no commits in last 24h"
required: false
type: boolean
default: false
permissions:
id-token: write # Required for npm trusted publishing (OIDC)
contents: write # Required because npm-main creates/pushes git tags
checks: write # Required by nested reusable test workflow
pull-requests: write # Required by nested reusable test workflow
jobs:
publish-main:
if: |
github.repository == 'cline/cline' && (
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_target == 'main' &&
github.event.inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
)
uses: ./.github/workflows/npm-main.yaml
secrets: inherit
with:
confirm_publish: ${{ github.event.inputs.confirm_publish }}
publish-nightly:
if: |
github.repository == 'cline/cline' && (
github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.publish_target == 'nightly')
)
uses: ./.github/workflows/npm-nightly.yaml
secrets: inherit
with:
force_publish: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_nightly_publish == 'true' }}
+3 -17
View File
@@ -36,23 +36,9 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
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
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
# Keep publish environment aligned with test workflow/tooling lockfile expectations.
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
node-version: 22
- name: Install root dependencies
run: npm ci --include=optional
+1 -19
View File
@@ -42,30 +42,12 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
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
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
node-version: 22
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm install --include=optional
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm install --include=optional
- name: Install Publishing Tools
+3
View File
@@ -48,3 +48,6 @@ test-results
.secrets
*.tsbuildinfo
# Smoke test results (generated)
evals/smoke-tests/results/
+3
View File
@@ -0,0 +1,3 @@
[submodule "evals/cline-bench"]
path = evals/cline-bench
url = https://github.com/cline/cline-bench.git
+2 -1
View File
@@ -16,7 +16,8 @@
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
"--disable-extension",
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
"${workspaceFolder}"
"${workspaceFolder}",
"--disable-extensions"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
-2
View File
@@ -35,11 +35,9 @@ cli/**
eslint-rules/
old_docs/
evals/
.changie.yaml
.codespellrc
.mocharc.json
buf.yaml
.changeset/
.clinerules/
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
+142 -12
View File
@@ -1,5 +1,135 @@
# Changelog
## [3.67.1]
### Added
- Added Cline SDK API interface for programmatic access to Cline features and tools, enabling integration into custom applications.
- Added Codex 5.3 model support
### Fixed
- Fix OpenAI Codex by setting `store` to `false`
- Use `isLocatedInPath()` instead of string matching for path containment checks
## [3.67.0]
### Added
- Add support for skills and optional modelId in subagent configuration
- Add AgentConfigLoader for file-based agent configs
- Add Responses API support for OpenAI native provider
- Preconnect websocket to reduce response latency
- Fetch featured models from backend with local fallback
- Add /q command to quit CLI
- Add MCP enterprise configuration details
- Pull Cline's recommended models from internal endpoint
- Add dynamic flag to adjust banner cache duration
### Fixed
- Fix reasoning delta crash on usage-only stream chunks
- Fix OpenAI tool ID transformation restricted to native provider only
- Fix auth check for ACP mode
- Fix CLI yolo mode to not persist yolo setting to disk
- Fix inline focus-chain slider within its feature row
- Fix Gemini 3.1 Pro compatibility
- Fix Cline auth with ACP flag
### Changed
- Move PR skill to .agents/skills
- SambaNova provider: update models list
- Remove changeset-converter GitHub Action and npm run changeset
## [3.66.0]
### Added
- Gemini-3.1 Pro Preview
## [3.65.0]
### Added
- Add /skills slash command to CLI for viewing and managing installed skills
### Fixed
- Fix aggressive context compaction caused by accidental clicks on the context window progress bar silently setting a very low auto-condense threshold
- Fix infinite retry loop when write_to_file fails with missing content parameter.
- Fixed default claude model
## [3.64.0]
### Added
- Added sonnet 4.6
## [3.63.0]
### Added
- added zai GLM 5 Free promo
### Fixed
- Restore reasoning trace visibility in chat and improve the thinking row UX so reasoning is visible, then collapsible after completion.
## [3.62.0]
### Fixed
- Banners now display immediately when opening the extension instead of requiring user interaction first
- Resolved 17 security vulnerabilities including high-severity DoS issues in dependencies (body-parser, axios, qs, tar, and others)
## [3.61.0]
- UI/UX fixes with minimax model family
## [3.60.0]
- Fixes for Minimax model family
## [3.59.0]
- Added Minimax 2.5 Free Promo
- Fixed Response chaining for OpenAI's Responses API
## [3.58.0]
### Added
- Subagent: replace legacy subagents with the native `use_subagents` tool
- Bundle `endpoints.json` support so packaged distributions can ship required endpoints out-of-the-box
- Amazon Bedrock: support parallel tool calling
- New "double-check completion" experimental feature to verify work before marking tasks complete
- CLI: new task controls/flags including custom `--thinking` token budget and `--max-consecutive-mistakes` for yolo runs
- Remote config: new UI/options (including connection/test buttons) and support for syncing deletion of remotely configured MCP servers
- Vertex / Claude Code: add 1M context model options for Claude Opus 4.6
- ZAI/GLM: add GLM-5
### Fixed
- CLI: handle stdin redirection correctly in CI/headless environments
- CLI: preserve OAuth callback paths during auth redirects
- VS Code Web: generate auth callback URLs via `vscode.env.asExternalUri` (OAuth callback reliability)
- Terminal: surface command exit codes in results and improve long-running `execute_command` timeout behavior
- UI: add loading indicator and fix `api_req_started` rendering
- Task streaming: prevent duplicate streamed text rows after completion
- API: preserve selected Vercel model when model metadata is missing
- Telemetry: route PostHog networking through proxy-aware shared fetch and ensure telemetry flushes on shutdown
- CI: increase Windows E2E test timeout to reduce flakiness
### Changed
- Settings/model UX: move "reasoning effort" into model configuration and expose it in settings
- CLI provider selection: limit provider list to those remotely configured
- UI: consolidate ViewHeader component/styling across views
- Tools: add auto-approval support for `attempt_completion` commands
- Remotely configured MCP server schema now supports custom headers
## [3.57.1]
### Fixed
@@ -11,7 +141,7 @@
### Added
- Cline CLI 2.0 now available. Install with `npm install -g cline`
- Anthopic Opus 4.6
- Anthopic Opus 4.6
- Minimax-2.1 and Kimi-k2.5 now available for free for a limited time promo
- Codex-5.3 through ChatGPT subscription
@@ -31,23 +161,23 @@
### Added
- __CLI authentication:__ Added Vercel AI Gateway and Cline API key provider support for headless CI/automation workflows
- __New model:__ Added Kimi-K2.5 model to Moonshot provider (262K context, image support, prompt caching)
- __Prompt variant:__ Added Trinity Large prompt variant for improved tool-calling support
- __OpenTelemetry:__ Added support for custom headers on metrics and logs endpoints
- __Social links:__ Added community icons (X, Discord, GitHub, Reddit, LinkedIn) to the What's New modal
- **CLI authentication:** Added Vercel AI Gateway and Cline API key provider support for headless CI/automation workflows
- **New model:** Added Kimi-K2.5 model to Moonshot provider (262K context, image support, prompt caching)
- **Prompt variant:** Added Trinity Large prompt variant for improved tool-calling support
- **OpenTelemetry:** Added support for custom headers on metrics and logs endpoints
- **Social links:** Added community icons (X, Discord, GitHub, Reddit, LinkedIn) to the What's New modal
### Fixed
- __LiteLLM:__ Fixed thinking configuration not appearing for reasoning-capable models
- __OpenTelemetry:__ Fixed endpoint path handling (no longer incorrectly appends `/v1/logs` or `/v1/metrics`) and ensured logs are sent regardless of VSCode telemetry settings
- __CLI auth:__ Fixed `cline auth` displaying incorrect provider information after configuration
- **LiteLLM:** Fixed thinking configuration not appearing for reasoning-capable models
- **OpenTelemetry:** Fixed endpoint path handling (no longer incorrectly appends `/v1/logs` or `/v1/metrics`) and ensured logs are sent regardless of VSCode telemetry settings
- **CLI auth:** Fixed `cline auth` displaying incorrect provider information after configuration
### Changed
- __Hooks:__ Hook scripts now run from the workspace repository root instead of filesystem root
- __Default settings:__ Enabled multi-root workspaces, parallel tool calling, and skills by default; disabled strict plan mode by default
- __Settings UI:__ Refreshed feature settings section with collapsible design
- **Hooks:** Hook scripts now run from the workspace repository root instead of filesystem root
- **Default settings:** Enabled multi-root workspaces, parallel tool calling, and skills by default; disabled strict plan mode by default
- **Settings UI:** Refreshed feature settings section with collapsible design
## [3.55.0]
+6 -25
View File
@@ -57,25 +57,11 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
### Creating a Pull Request
1. Before creating a PR, generate a changeset entry:
```bash
npm run changeset
```
This will prompt you for:
- Type of change (major, minor, patch)
- `major` → breaking changes (1.0.0 → 2.0.0)
- `minor` → new features (1.0.0 → 1.1.0)
- `patch` → bug fixes (1.0.0 → 1.0.1)
- Description of your changes
1. Commit your changes.
2. Commit your changes and the generated `.changeset` file
3. Push your branch and create a PR on GitHub. Our CI will:
2. Push your branch and create a PR on GitHub. Our CI will:
- Run tests and checks
- Changesetbot will create a comment showing the version impact
- When merged to main, changesetbot will create a Version Packages PR
- When the Version Packages PR is merged, a new release will be published
4. Testing
3. Testing
- Run `npm run test` to run tests locally.
- Before submitting PR, run `npm run format:fix` to format your code
@@ -192,15 +178,10 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
- Temporary workspaces with test fixtures
- Video recording for failed tests
4. **Version Management with Changesets**
4. **Versioning & Changelog Notes**
- Create a changeset for any user-facing changes using `npm run changeset`
- Choose the appropriate version bump:
- `major` for breaking changes (1.0.0 → 2.0.0)
- `minor` for new features (1.0.0 → 1.1.0)
- `patch` for bug fixes (1.0.0 → 1.0.1)
- Write clear, descriptive changeset messages that explain the impact
- Documentation-only changes don't require changesets
- Contributors do not need to create changelog-entry files as part of PRs.
- Maintainers handle release versioning and changelog curation during the release process.
5. **Commit Guidelines**
+27
View File
@@ -0,0 +1,27 @@
# Security Policy
## Supported Versions
We actively patch only the most recent minor release of Cline. Older versions receive fixes at our discretion.
## Reporting a Vulnerability
We appreciate your efforts to responsibly disclose your findings and will make every effort to acknowledge your contributions.
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/cline/cline/security/advisories/new) tab.
The team will send a response indicating the next steps in handling your report. After the initial reply, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
When reporting, please include:
- A short summary of the issue
- Steps to reproduce or a proof of concept
- Any logs, stack traces, or screenshots that might help us understand the problem
We acknowledge reports within 48 hours and aim to release a fix or mitigation within 30 days. While we work on a resolution, please keep the details private.
## Escalation
If you do not receive an acknowledgement of your report within 5 business days, you may send an email to security@cline.bot.
Thank you for helping us keep Cline users safe.
+130
View File
@@ -0,0 +1,130 @@
# cline
## [2.5.0]
### Added
- Added Cline SDK API interface for programmatic access to Cline features and tools, enabling integration into custom applications.
- Added Codex 5.3 model support
### Fixed
- Fix OpenAI Codex by setting `store` to `false`
- Use `isLocatedInPath()` instead of string matching for path containment checks
## [2.4.3]
### Added
- Add /q command to quit CLI
- Fetch featured models from backend with local fallback
### Fixed
- Fix auth check for ACP mode
- Fix Cline auth with ACP flag
- Fix yolo mode to not persist yolo setting to disk
## [2.4.2]
### Added
- Gemini-3.1 Pro Preview
### Patch Changes
- VSCode uses shared files for global, workspace and secret state.
## [2.4.1]
### Fixed
- Fix infinite retry loop when write_to_file fails with missing content parameter. Provides progressive guidance to the model, escalating from suggestions to hard stops, with context window awareness to break the loop.
## [2.4.0]
### Added
- Adding Anthropic Sonnet 4.6
- Allows users to enter custom aws region when selecting bedrock as a provider in CLI
- Keep reasoning rows visible when low-stakes tool groups start immediately after reasoning.
- Restore reasoning trace visibility in chat and improve the thinking row UX so streamed reasoning is visible, then collapsible after completion.
### Fixed
- Banners now display immediately when opening the extension instead of requiring user interaction first
- Resolved 17 security vulnerabilities including high-severity DoS issues in dependencies (body-parser, axios, qs, tar, and others)
## [2.2.2]
- Allows users to enter custom aws region when selecting bedrock as a provider
- Prevent Parent Container Scrolling In Dropdowns
## [2.2.1]
- Added Minimax 2.5 Free Promo
- Fixed Response chaining for OpenAI's Responses API
## [2.2.0]
### Added
- Subagent: replace legacy subagents with the native `use_subagents` tool
- Bundle `endpoints.json` support so packaged distributions can ship required endpoints out-of-the-box
- Amazon Bedrock: support parallel tool calling
- New "double-check completion" experimental feature to verify work before marking tasks complete
- CLI: new task controls/flags including custom `--thinking` token budget and `--max-consecutive-mistakes` for yolo runs
- Remote config: new UI/options (including connection/test buttons) and support for syncing deletion of remotely configured MCP servers
- Vertex / Claude Code: add 1M context model options for Claude Opus 4.6
- ZAI/GLM: add GLM-5
### Fixed
- CLI: handle stdin redirection correctly in CI/headless environments
- CLI: preserve OAuth callback paths during auth redirects
- VS Code Web: generate auth callback URLs via `vscode.env.asExternalUri` (OAuth callback reliability)
- Terminal: surface command exit codes in results and improve long-running `execute_command` timeout behavior
- UI: add loading indicator and fix `api_req_started` rendering
- Task streaming: prevent duplicate streamed text rows after completion
- API: preserve selected Vercel model when model metadata is missing
- Telemetry: route PostHog networking through proxy-aware shared fetch and ensure telemetry flushes on shutdown
- CI: increase Windows E2E test timeout to reduce flakiness
### Changed
- Settings/model UX: move "reasoning effort" into model configuration and expose it in settings
- CLI provider selection: limit provider list to those remotely configured
- UI: consolidate ViewHeader component/styling across views
- Tools: add auto-approval support for `attempt_completion` commands
- Remotely configured MCP server schema now supports custom headers
## [2.1.0]
### Minor Changes
- 42ce100: Add Generate API Key on Hicap Provider selection
### Patch Changes
- 195294f: Add support for bundled endpoints.json in enterprise distributions. Extensions can now include a pre-configured endpoints.json file that automatically switches Cline to self-hosted mode. Includes packaging scripts for VSIX, NPM, and JetBrains plugins.
- a1f2601: Replace the LiteLLM model list with a selector
- 739d75a: Add Claude Code provider support for Claude Opus 4.6 and Sonnet 4.5 1M variants via both full model names and aliases (`opus[1m]`, `sonnet[1m]`), and align the `opus` alias with Opus 4.6.
- 8440380: Add GitHub Actions workflow to build CLI from any commit for testing
- b1a8db2: fix(cli): prevent hang when spawned without TTY
- 7c87017: Add Claude Opus 4.6 model support
- d116ac5: Supports rendering markdown table in chat view.
- 6d8fb85: Fix CLI crashing in CI environments and with stdin redirection (e.g., `cline "prompt" < /dev/null`). Now checks both stdin and stdout TTY status before using Ink, and only errors on empty stdin when no prompt is provided.
- 70a9904: Fix JetBrains sign-in regression by adding fallback for openExternal RPC
- f440f3a: fix: use vscode.env.openExternal for auth in remote environments
Fixes OAuth authentication in VS Code Server and remote environments by routing browser URL opening through VS Code's native openExternal API instead of the npm 'open' package.
- 70a9904: fix: use vscode.env.asExternalUri for auth callback URLs only in VS Code Web
Fixes OAuth callback redirect in VS Code Web (`code serve-web`, Codespaces) by using `vscode.env.asExternalUri()` to resolve the callback URI. This is gated behind a `vscode.env.uiKind === UIKind.Web` check so regular desktop VS Code continues to use the `vscode://` URI directly. The `getCallbackUrl` API now accepts a `path` parameter so the full callback URI (including route) is resolved correctly, and callers pass their path directly instead of appending after.
- 5308ded: Updating script documentation and removing unnecessary continue on error
- b514f18: Prevent duplicate streamed text rows when a partial text update arrives after the same text was already finalized.
- 26391c9: Fix Bedrock model id
- d19a877: Unify ViewHeader Styles Across All Views
- 5dcaa8c: Add Vertex Claude Opus 4.6 1M model option and global endpoint support, and pass the 1M beta header for Vertex Claude requests.
+1 -2
View File
@@ -45,7 +45,7 @@ cline
### Use any API and Model
Cline supports API providers like ChatGPT, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using a Cline Account, you'll always have access to the newest models as soon as they're available.
Cline supports API providers like ChatGPT, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras, Groq, and Moonshot. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using a Cline Account, you'll always have access to the newest models as soon as they're available.
<!-- Transparent pixel to create line break after floating image -->
@@ -79,4 +79,3 @@ Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), g
## License
[Apache 2.0 © 2026 Cline Bot Inc.](./LICENSE)
+41 -10
View File
@@ -208,8 +208,8 @@ if (production) {
buildEnvVars["process.env.IS_DEV"] = "false"
}
const config: esbuild.BuildOptions = {
entryPoints: [path.join(__dirname, "src", "index.ts")],
// Shared build options
const sharedOptions: Partial<esbuild.BuildOptions> = {
bundle: true,
minify: production,
sourcemap: !production,
@@ -221,7 +221,6 @@ const config: esbuild.BuildOptions = {
sourcesContent: false,
platform: "node",
target: "node20",
outfile: path.join(__dirname, "dist", "cli.mjs"),
// These modules need to load files from the module directory at runtime
external: [
"@grpc/reflection",
@@ -237,6 +236,13 @@ const config: esbuild.BuildOptions = {
"@vscode/ripgrep", // Uses __dirname to locate the binary
],
supported: { "top-level-await": true },
}
// CLI executable configuration
const cliConfig: esbuild.BuildOptions = {
...sharedOptions,
entryPoints: [path.join(__dirname, "src", "index.ts")],
outfile: path.join(__dirname, "dist", "cli.mjs"),
banner: {
js: `#!/usr/bin/env node
// Suppress all Node.js warnings (deprecation, experimental, etc.)
@@ -250,19 +256,44 @@ const __dirname = _dirname(__filename);`,
},
}
// Library configuration for programmatic use
const libConfig: esbuild.BuildOptions = {
...sharedOptions,
entryPoints: [path.join(__dirname, "src", "exports.ts")],
outfile: path.join(__dirname, "dist", "lib.mjs"),
banner: {
js: `// Cline Library - Programmatic API
import { createRequire as _createRequire } from 'module';
import { fileURLToPath as _fileURLToPath } from 'url';
import { dirname as _dirname } from 'path';
const require = _createRequire(import.meta.url);
const __filename = _fileURLToPath(import.meta.url);
const __dirname = _dirname(__filename);`,
},
}
async function main() {
const ctx = await esbuild.context(config)
if (watch) {
// In watch mode, only watch the CLI (primary use case for development)
const ctx = await esbuild.context(cliConfig)
await ctx.watch()
console.log("[cli] Watching for changes...")
} else {
await ctx.rebuild()
await ctx.dispose()
// Build both CLI and library
console.log("[cli esbuild] Building CLI executable...")
const cliCtx = await esbuild.context(cliConfig)
await cliCtx.rebuild()
await cliCtx.dispose()
// Make the output executable
const outfile = path.join(__dirname, "dist", "cli.mjs")
if (fs.existsSync(outfile)) {
fs.chmodSync(outfile, "755")
console.log("[cli esbuild] Building library bundle...")
const libCtx = await esbuild.context(libConfig)
await libCtx.rebuild()
await libCtx.dispose()
// Make the CLI output executable
const cliOutfile = path.join(__dirname, "dist", "cli.mjs")
if (fs.existsSync(cliOutfile)) {
fs.chmodSync(cliOutfile, "755")
}
}
}
+8 -2
View File
@@ -125,13 +125,13 @@ authentication wizard, or use quick setup flags.
Options:
.PP
\f[B]\-p\f[R], \f[B]\-\-provider\f[R] \f[I]id\f[R] : Provider ID for
quick setup (e.g., openai\-native, anthropic, openrouter)
quick setup (e.g., openai\-native, anthropic, openrouter, moonshot)
.PP
\f[B]\-k\f[R], \f[B]\-\-apikey\f[R] \f[I]key\f[R] : API key for the
provider
.PP
\f[B]\-m\f[R], \f[B]\-\-modelid\f[R] \f[I]id\f[R] : Model ID to
configure (e.g., gpt\-4o, claude\-sonnet\-4\-5\-20250929)
configure (e.g., gpt\-4o, claude\-sonnet\-4\-5\-20250929, kimi\-k2.5)
.PP
\f[B]\-b\f[R], \f[B]\-\-baseurl\f[R] \f[I]url\f[R] : Base URL (optional,
for OpenAI\-compatible providers)
@@ -242,6 +242,9 @@ cline \-m claude\-sonnet\-4\-5\-20250929 \(dqRefactor this function\(dq
\f[I]# Quick auth setup with model\f[R]
cline auth \-p anthropic \-k sk\-ant\-xxxxx \-m claude\-sonnet\-4\-5\-20250929
\f[I]# Quick auth setup for Moonshot\f[R]
cline auth \-p moonshot \-k sk\-xxxxx \-m kimi\-k2.5
.EE
.SS Including Images
.IP
@@ -309,6 +312,9 @@ cline auth \-p anthropic \-k sk\-ant\-api\-xxxxx
\f[I]# Quick setup for OpenAI\f[R]
cline auth \-p openai\-native \-k sk\-xxxxx \-m gpt\-4o
\f[I]# Quick setup for Moonshot\f[R]
cline auth \-p moonshot \-k sk\-xxxxx \-m kimi\-k2.5
\f[I]# OpenAI\-compatible provider with custom base URL\f[R]
cline auth \-p openai \-k your\-api\-key \-b https://api.example.com/v1
.EE
+4
View File
@@ -56,6 +56,8 @@ Run a new task with a prompt.
**-y**, **\--yolo** : Enable yolo/yes mode (auto-approve all actions, output in plain mode, exit process automatically when task complete)
**-t**, **\--timeout** *seconds* : Optional timeout in seconds. Only applied when explicitly provided.
**-m**, **\--model** *model* : Model to use for the task
**-i**, **\--images** *paths...* : Image file paths to include with the task
@@ -144,6 +146,8 @@ When running **cline** with just a prompt (no subcommand), these options are ava
**-y**, **\--yolo** : Enable yolo mode (auto-approve all actions). Also forces plain text output mode.
**-t**, **\--timeout** *seconds* : Optional timeout in seconds. Only applied when explicitly provided.
**-m**, **\--model** *model* : Model to use for the task
**-v**, **\--verbose** : Show verbose output
+12 -4
View File
@@ -1,11 +1,18 @@
{
"name": "cline",
"version": "2.0.5",
"version": "2.5.0",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "dist/cli.mjs",
"main": "dist/lib.mjs",
"types": "dist/lib.d.ts",
"bin": {
"cline": "./dist/cli.mjs"
},
"exports": {
".": {
"import": "./dist/lib.mjs",
"types": "./dist/lib.d.ts"
}
},
"os": [
"darwin",
"linux",
@@ -23,8 +30,9 @@
"scripts": {
"package:brew": "npx tsx ./scripts/update-brew-formula.mts",
"package": "npm pack --pack-destination ./dist",
"build": "npm run typecheck && npx tsx esbuild.mts",
"build:production": "npm run typecheck && npx tsx esbuild.mts --production",
"build": "npm run typecheck && npx tsx esbuild.mts && npm run build:types",
"build:production": "npm run typecheck && npx tsx esbuild.mts --production && npm run build:types",
"build:types": "(npx tsc -p tsconfig.lib.json || true) && cp dist/types/cli/src/exports.d.ts dist/lib.d.ts && mkdir -p dist/agent && cp dist/types/cli/src/agent/ClineAgent.d.ts dist/types/cli/src/agent/ClineSessionEmitter.d.ts dist/types/cli/src/agent/public-types.d.ts dist/agent/ && rm -rf dist/types",
"watch": "npx tsx esbuild.mts --watch",
"dev": "IS_DEV=true && npm run link && npm run watch ; npm run unlink",
"clean": "rimraf dist",
+2 -6
View File
@@ -108,11 +108,7 @@ class ACPDiffServiceClient implements DiffServiceClientInterface {
class ACPEnvServiceClient implements EnvServiceClientInterface {
private readonly version: string
constructor(
_clientCapabilities: acp.ClientCapabilities | undefined,
_sessionIdResolver: SessionIdResolver,
version: string = "1.0.0",
) {
constructor(_clientCapabilities: acp.ClientCapabilities | undefined, _sessionIdResolver: SessionIdResolver, version: string) {
this.version = version
}
@@ -402,7 +398,7 @@ export class ACPHostBridgeClientProvider implements HostBridgeClientProvider {
clientCapabilities: acp.ClientCapabilities | undefined,
sessionIdResolver: SessionIdResolver,
cwdResolver: CwdResolver,
version: string = "1.0.0",
version: string,
) {
this.workspaceClient = new ACPWorkspaceServiceClient(clientCapabilities, sessionIdResolver, cwdResolver)
this.envClient = new ACPEnvServiceClient(clientCapabilities, sessionIdResolver, version)
+5 -21
View File
@@ -15,7 +15,7 @@
import type * as acp from "@agentclientprotocol/sdk"
import { Logger } from "@/shared/services/Logger.js"
import { ClineAgent } from "../agent/ClineAgent.js"
import type { AcpAgentOptions, SessionUpdateType } from "../agent/types.js"
import { type AcpAgentOptions, type SessionUpdateType } from "../agent/types.js"
/**
* ACP Agent wrapper that bridges stdio connection to ClineAgent.
@@ -39,37 +39,21 @@ export class AcpAgent implements acp.Agent {
this.clineAgent = new ClineAgent(options)
// Wire up the permission handler to use the connection
this.clineAgent.setPermissionHandler(async (request, resolve) => {
this.clineAgent.setPermissionHandler(async (request) => {
try {
Logger.debug("[AcpAgent] Forwarding permission request to connection")
const response = await this.connection.requestPermission({
sessionId: this.getCurrentSessionId() ?? "",
return await this.connection.requestPermission({
sessionId: request.sessionId,
toolCall: request.toolCall,
options: request.options,
})
resolve(response)
} catch (error) {
Logger.debug("[AcpAgent] Error requesting permission:", error)
resolve({ outcome: "rejected" as unknown as acp.RequestPermissionOutcome })
return { outcome: { outcome: "cancelled" } }
}
})
}
/**
* Get the current active session ID from the ClineAgent.
*/
private getCurrentSessionId(): string | undefined {
// Find the session that's currently processing
for (const [sessionId, session] of this.clineAgent.sessions) {
if (session.controller?.task) {
return sessionId
}
}
// Fall back to the first session if none is actively processing
const firstSession = this.clineAgent.sessions.keys().next()
return firstSession.done ? undefined : firstSession.value
}
/**
* Subscribe to session events and forward them to the connection.
*/
+10 -28
View File
@@ -12,11 +12,7 @@
import type * as acp from "@agentclientprotocol/sdk"
import type { TerminalHandle } from "@agentclientprotocol/sdk"
import {
DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT,
DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT,
PROCESS_HOT_TIMEOUT_NORMAL,
} from "@integrations/terminal/constants"
import { DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT, PROCESS_HOT_TIMEOUT_NORMAL } from "@integrations/terminal/constants"
import type {
ITerminal,
ITerminalManager,
@@ -142,12 +138,12 @@ export interface ManagedTerminal {
* Wraps ACP terminal operations and emits events compatible with ITerminalProcess.
*/
class AcpTerminalProcess extends EventEmitter<TerminalProcessEvents> implements ITerminalProcess {
isHot: boolean = false
waitForShellIntegration: boolean = false
isHot = false
waitForShellIntegration = false
private _unretrievedOutput: string = ""
private _continued: boolean = false
private _completed: boolean = false
private _unretrievedOutput = ""
private _continued = false
private _completed = false
private _hotTimeout: NodeJS.Timeout | null = null
private _exitWaitTimeout: NodeJS.Timeout | null = null
private readonly manager: AcpTerminalManager
@@ -397,7 +393,7 @@ export class AcpTerminalManager implements ITerminalManager {
private readonly numericIdToStringId: Map<number, string> = new Map()
/** Next numeric ID to assign */
private nextNumericId: number = 1
private nextNumericId = 1
/** Active processes indexed by numeric terminal ID */
private readonly processes: Map<number, AcpTerminalProcess> = new Map()
@@ -406,9 +402,8 @@ export class AcpTerminalManager implements ITerminalManager {
private readonly terminalInfos: Map<number, TerminalInfo> = new Map()
// Configuration options for ITerminalManager
private terminalReuseEnabled: boolean = true
private terminalReuseEnabled = true
private terminalOutputLineLimit: number = DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT
private subagentTerminalOutputLineLimit: number = DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT
/**
* Creates a new AcpTerminalManager.
@@ -667,14 +662,6 @@ export class AcpTerminalManager implements ITerminalManager {
this.terminalOutputLineLimit = limit
}
/**
* Set the maximum number of output lines for subagent commands.
* @param limit Maximum number of lines
*/
setSubagentTerminalOutputLineLimit(limit: number): void {
this.subagentTerminalOutputLineLimit = limit
}
/**
* Set the default terminal profile.
* @param profile The profile identifier
@@ -687,15 +674,10 @@ export class AcpTerminalManager implements ITerminalManager {
* Process output lines, potentially truncating if over limit.
* @param outputLines Array of output lines
* @param overrideLimit Optional limit override
* @param isSubagentCommand Whether this is a subagent command
* @returns Processed output string
*/
processOutput(outputLines: string[], overrideLimit?: number, isSubagentCommand?: boolean): string {
const limit = isSubagentCommand
? overrideLimit !== undefined
? overrideLimit
: this.subagentTerminalOutputLineLimit
: this.terminalOutputLineLimit
processOutput(outputLines: string[], overrideLimit?: number): string {
const limit = overrideLimit !== undefined ? overrideLimit : this.terminalOutputLineLimit
if (outputLines.length > limit) {
const halfLimit = Math.floor(limit / 2)
-5
View File
@@ -15,22 +15,18 @@
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
import { Logger } from "@/shared/services/Logger"
import { version as CLI_VERSION } from "../../../package.json"
import { AcpAgent } from "./AcpAgent.js"
import { nodeToWebReadable, nodeToWebWritable } from "./streamUtils.js"
// Re-export classes for programmatic use
export { ClineAgent } from "../agent/ClineAgent.js"
export { ClineSessionEmitter } from "../agent/ClineSessionEmitter.js"
// Re-export types
export type {
AcpAgentOptions,
AcpSessionState,
ClineAcpSession,
ClineAgentOptions,
ClineSessionEvents,
PermissionHandler,
PermissionResolver,
} from "../agent/types.js"
export { AcpAgent } from "./AcpAgent.js"
@@ -99,7 +95,6 @@ export async function runAcpMode(options: AcpModeOptions = {}): Promise<void> {
new AgentSideConnection((conn) => {
agent = new AcpAgent(conn, {
version: CLI_VERSION,
debug: Boolean(options.verbose),
})
return agent
+58 -85
View File
@@ -28,6 +28,8 @@ import {
groqModels,
mistralDefaultModelId,
mistralModels,
moonshotDefaultModelId,
moonshotModels,
openAiCodexDefaultModelId,
openAiNativeDefaultModelId,
openAiNativeModels,
@@ -36,7 +38,6 @@ import {
} from "@shared/api"
import type { ClineAsk, ClineMessage as ClineMessageType } from "@shared/ExtensionMessage"
import { CLI_ONLY_COMMANDS, VSCODE_ONLY_COMMANDS } from "@shared/slashCommands"
import { ProviderToApiKeyMap } from "@shared/storage"
import { getProviderModelIdKey } from "@shared/storage/provider-keys"
import { ClineEndpoint } from "@/config.js"
import { Controller } from "@/core/controller"
@@ -51,18 +52,21 @@ import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { StandaloneTerminalManager } from "@/integrations/terminal/index.js"
import { AuthService } from "@/services/auth/AuthService.js"
import { Logger } from "@/shared/services/Logger.js"
import { secretStorage } from "@/shared/storage/ClineSecretStorage"
import type { Mode } from "@/shared/storage/types"
import { openExternal } from "@/utils/env"
import { version as AGENT_VERSION } from "../../package.json"
import { ACPDiffViewProvider } from "../acp/ACPDiffViewProvider.js"
import { ACPHostBridgeClientProvider } from "../acp/ACPHostBridgeClientProvider.js"
import { AcpTerminalManager } from "../acp/AcpTerminalManager.js"
import { isAuthConfigured } from "../utils/auth"
import { fetchOpenRouterModels, usesOpenRouterModels } from "../utils/openrouter-models"
import { CliContextResult, initializeCliContext } from "../vscode-context.js"
import { ClineSessionEmitter } from "./ClineSessionEmitter.js"
import { translateMessage } from "./messageTranslator.js"
import { handlePermissionResponse } from "./permissionHandler.js"
import type { AcpSessionState, ClineAcpSession, ClineAgentOptions, PermissionHandler } from "./types.js"
import type { ClineAcpSession, ClineAgentOptions, PermissionHandler } from "./public-types.js"
import { AcpSessionStatus } from "./public-types.js"
import { type AcpSessionState } from "./types.js"
// Map providers to their static model lists and defaults (copied from ModelPicker.tsx)
const providerModels: Record<string, { models: Record<string, unknown>; defaultId: string }> = {
@@ -72,6 +76,7 @@ const providerModels: Record<string, { models: Record<string, unknown>; defaultI
bedrock: { models: bedrockModels, defaultId: bedrockDefaultModelId },
deepseek: { models: deepSeekModels, defaultId: deepSeekDefaultModelId },
mistral: { models: mistralModels, defaultId: mistralDefaultModelId },
moonshot: { models: moonshotModels, defaultId: moonshotDefaultModelId },
groq: { models: groqModels, defaultId: groqDefaultModelId },
xai: { models: xaiModels, defaultId: xaiDefaultModelId },
}
@@ -102,7 +107,12 @@ function getModelList(provider: string): string[] {
export class ClineAgent implements acp.Agent {
private readonly options: ClineAgentOptions
private readonly ctx: CliContextResult
readonly sessions: Map<string, ClineAcpSession> = new Map()
/** Map of active sessions by session ID */
public readonly sessions: Map<string, ClineAcpSession> = new Map()
/** WeakMap to associate ClineAcpSession with its Controller without exposing it to consumers */
readonly #sessionControllers = new WeakMap<ClineAcpSession, Controller>()
/** Runtime state for active sessions */
private readonly sessionStates: Map<string, AcpSessionState> = new Map()
@@ -130,7 +140,7 @@ export class ClineAgent implements acp.Agent {
constructor(options: ClineAgentOptions) {
this.options = options
this.ctx = initializeCliContext()
this.ctx = initializeCliContext({ clineDir: options.clineDir })
}
/**
@@ -173,8 +183,8 @@ export class ClineAgent implements acp.Agent {
async initialize(params: acp.InitializeRequest, connection?: acp.AgentSideConnection): Promise<acp.InitializeResponse> {
this.clientCapabilities = params.clientCapabilities
this.initializeHostProvider(this.clientCapabilities, connection)
await ClineEndpoint.initialize()
await StateManager.initialize(this.ctx.extensionContext)
await ClineEndpoint.initialize(this.ctx.EXTENSION_DIR)
await StateManager.initialize(this.ctx.storageContext)
return {
protocolVersion: PROTOCOL_VERSION,
@@ -192,7 +202,7 @@ export class ClineAgent implements acp.Agent {
},
agentInfo: {
name: "cline",
version: this.options.version,
version: AGENT_VERSION,
},
authMethods: [
{
@@ -224,7 +234,7 @@ export class ClineAgent implements acp.Agent {
clientCapabilities,
() => this.currentActiveSessionId,
() => this.sessions.get(this.currentActiveSessionId ?? "")?.cwd ?? process.cwd(),
this.options.version,
AGENT_VERSION,
)
HostProvider.initialize(
@@ -246,8 +256,8 @@ export class ClineAgent implements acp.Agent {
},
hostBridgeClientProvider,
(message: string) => Logger.info(message),
async () => {
return AuthHandler.getInstance().getCallbackUrl()
async (path: string) => {
return AuthHandler.getInstance().getCallbackUrl(path)
},
async () => "", // get binary location not needed in ACP mode
this.ctx.EXTENSION_DIR,
@@ -263,7 +273,7 @@ export class ClineAgent implements acp.Agent {
*/
async newSession(params: acp.NewSessionRequest): Promise<acp.NewSessionResponse> {
// Check if authentication is required
const isAuthenticated = await this.isAuthConfigured()
const isAuthenticated = await isAuthConfigured()
if (!isAuthenticated) {
throw RequestError.authRequired()
}
@@ -287,16 +297,16 @@ export class ClineAgent implements acp.Agent {
mcpServers: params.mcpServers ?? [],
createdAt: Date.now(),
lastActivityAt: Date.now(),
controller,
}
this.#sessionControllers.set(session, controller)
this.sessions.set(sessionId, session)
// Initialize session state
const sessionState: AcpSessionState = {
sessionId,
isProcessing: false,
cancelled: false,
status: AcpSessionStatus.Idle,
pendingToolCalls: new Map(),
}
@@ -433,11 +443,11 @@ export class ClineAgent implements acp.Agent {
*
* The prompt flow:
* 1. Extract content from the ACP prompt (text, images, files)
* 2. Set up state broadcasting (subscribe to controller updates)
* 3. Initialize or continue task with Controller
* 2. Set up internal cline state subsription
* 3. Initialize or continue cline task
* 4. Translate ClineMessages to ACP SessionUpdates
* 5. Handle permission requests for tools/commands
* 6. Return when task completes, is cancelled, or needs user input
* 6. Return when cline task completes, is cancelled, or needs user input
*/
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
const session = this.sessions.get(params.sessionId)
@@ -447,11 +457,11 @@ export class ClineAgent implements acp.Agent {
throw new Error(`Session not found: ${params.sessionId}`)
}
if (sessionState.isProcessing) {
if (sessionState.status === AcpSessionStatus.Processing) {
throw new Error(`Session ${params.sessionId} is already processing a prompt`)
}
const controller = session.controller
const controller = this.#sessionControllers.get(session)
if (!controller) {
throw new Error("Controller not initialized for session. This is a bug in the ACP agent setup.")
}
@@ -462,8 +472,7 @@ export class ClineAgent implements acp.Agent {
})
// Mark session as processing and set as current active session
sessionState.isProcessing = true
sessionState.cancelled = false
sessionState.status = AcpSessionStatus.Processing
session.lastActivityAt = Date.now()
this.currentActiveSessionId = params.sessionId
@@ -584,7 +593,7 @@ export class ClineAgent implements acp.Agent {
Logger.debug("[ClineAgent] Error during cleanup:", error)
}
}
sessionState.isProcessing = false
sessionState.status = AcpSessionStatus.Idle
}
}
@@ -646,7 +655,13 @@ export class ClineAgent implements acp.Agent {
permissionRequest: Omit<acp.RequestPermissionRequest, "sessionId">,
): Promise<void> {
const session = this.sessions.get(sessionId)
const controller = session?.controller
if (!session) {
Logger.debug("[ClineAgent] No session found for permission request")
return
}
const controller = this.#sessionControllers.get(session)
if (!controller?.task) {
Logger.debug("[ClineAgent] No active task for permission request")
@@ -827,7 +842,7 @@ export class ClineAgent implements acp.Agent {
await this.emitSessionUpdate(sessionId, {
sessionUpdate,
content: { type: "text", text: needsNewline ? "\n" + textDelta : textDelta },
content: { type: "text", text: needsNewline ? `\n${textDelta}` : textDelta },
})
}
@@ -880,18 +895,22 @@ export class ClineAgent implements acp.Agent {
*/
async cancel(params: acp.CancelNotification): Promise<void> {
const session = this.sessions.get(params.sessionId)
if (!session) {
Logger.debug("[ClineAgent] cancel called for non-existent session:", params.sessionId)
return
}
const sessionState = this.sessionStates.get(params.sessionId)
Logger.debug("[ClineAgent] cancel called:", {
sessionId: params.sessionId,
isProcessing: sessionState?.isProcessing,
status: sessionState?.status,
})
if (sessionState) {
sessionState.cancelled = true
sessionState.status = AcpSessionStatus.Cancelled
// If we have an active controller task, cancel it
const controller = session?.controller
const controller = this.#sessionControllers.get(session)
if (controller?.task) {
try {
await controller.cancelTask()
@@ -932,7 +951,7 @@ export class ClineAgent implements acp.Agent {
session.lastActivityAt = Date.now()
// Update Controller mode if active
const controller = session.controller
const controller = this.#sessionControllers.get(session)
if (controller) {
controller.stateManager.setGlobalState("mode", session.mode)
@@ -973,7 +992,7 @@ export class ClineAgent implements acp.Agent {
// Get the callback URL first to ensure the server is ready
let callbackUrl: string
try {
callbackUrl = await authHandler.getCallbackUrl()
callbackUrl = await authHandler.getCallbackUrl("/auth")
Logger.debug("[ClineAgent] Callback URL ready:", callbackUrl)
} catch (error) {
Logger.error("[ClineAgent] Failed to get callback URL:", error)
@@ -1004,13 +1023,14 @@ export class ClineAgent implements acp.Agent {
const startTime = Date.now()
while (Date.now() - startTime < AUTH_TIMEOUT_MS) {
const stateManager = StateManager.get()
// Check if auth data has been stored
const authData = await secretStorage.get("cline:clineAccountId")
const authData = stateManager.getSecretKey("cline:clineAccountId")
if (authData) {
Logger.debug("[ClineAgent] Authentication successful")
// Set up the provider configuration for cline
const stateManager = StateManager.get()
stateManager.setGlobalState("actModeApiProvider", "cline")
stateManager.setGlobalState("planModeApiProvider", "cline")
await stateManager.flushPendingState()
@@ -1062,7 +1082,7 @@ export class ClineAgent implements acp.Agent {
* @returns The permission response from the client
*/
protected async requestPermission(
_sessionId: string,
sessionId: string,
toolCall: acp.ToolCallUpdate,
options: acp.PermissionOption[],
): Promise<acp.RequestPermissionResponse> {
@@ -1077,17 +1097,15 @@ export class ClineAgent implements acp.Agent {
return { outcome: "rejected" as unknown as acp.RequestPermissionOutcome }
}
// Use the permission handler callback pattern
return new Promise<acp.RequestPermissionResponse>((resolve) => {
this.permissionHandler!({ toolCall, options }, resolve)
})
return await this.permissionHandler({ sessionId, toolCall, options })
}
async shutdown(): Promise<void> {
for (const [sessionId, session] of this.sessions) {
await session.controller?.task?.abortTask()
await session.controller?.stateManager.flushPendingState()
await session.controller?.dispose()
const controller = this.#sessionControllers.get(session)
await controller?.task?.abortTask()
await controller?.stateManager.flushPendingState()
await controller?.dispose()
this.sessions.delete(sessionId)
this.sessionStates.delete(sessionId)
}
@@ -1143,48 +1161,6 @@ export class ClineAgent implements acp.Agent {
}
}
/**
* Check if the user has authentication configured.
* Returns true if they have either:
* - Cline provider with stored auth data
* - OpenAI Codex provider with OAuth credentials
* - BYO provider with an API key configured
*/
private async isAuthConfigured(): Promise<boolean> {
const stateManager = StateManager.get()
const mode = stateManager.getGlobalSettingsKey("mode") as string
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const currentProvider = (stateManager.getGlobalSettingsKey(providerKey) as string) || "cline"
if (currentProvider === "cline") {
// For Cline provider, check if we have stored auth data
const values = await Promise.all(["clineApiKey", "clineAccountId"].map((key) => secretStorage.get(key)))
return values.some(Boolean)
}
// For OpenAI Codex provider, check OAuth credentials
if (currentProvider === "openai-codex") {
openAiCodexOAuthManager.initialize(this.ctx.extensionContext)
return await openAiCodexOAuthManager.isAuthenticated()
}
// For BYO providers, check if the API key is configured
const keyField = ProviderToApiKeyMap[currentProvider as keyof typeof ProviderToApiKeyMap]
if (!keyField) {
return false
}
const fields = Array.isArray(keyField) ? keyField : [keyField]
for (const field of fields) {
const value = await secretStorage.get(field)
if (value) {
return true
}
}
return false
}
/**
* Handle OpenAI Codex OAuth authentication flow.
*
@@ -1198,9 +1174,6 @@ export class ClineAgent implements acp.Agent {
Logger.debug("[ClineAgent] Starting OpenAI Codex OAuth flow...")
try {
// Initialize the OAuth manager with extension context
openAiCodexOAuthManager.initialize(this.ctx.extensionContext)
// Get the authorization URL and start the callback server
const authUrl = openAiCodexOAuthManager.startAuthorizationFlow()
+1 -1
View File
@@ -8,7 +8,7 @@
*/
import { EventEmitter } from "events"
import type { ClineSessionEvents } from "./types.js"
import type { ClineSessionEvents } from "./public-types.js"
/**
* Type-safe EventEmitter for ClineAgent session events.
+4 -4
View File
@@ -12,6 +12,7 @@ import type { ClineMessage } from "@shared/ExtensionMessage"
import { beforeEach, describe, expect, it } from "vitest"
import { createSessionState, translateMessage, translateMessages } from "./messageTranslator"
import type { AcpSessionState } from "./types"
import { AcpSessionStatus } from "./types"
// =============================================================================
// Test Helpers
@@ -175,8 +176,7 @@ describe("createSessionState", () => {
const state = createSessionState("my-session-123")
expect(state.sessionId).toBe("my-session-123")
expect(state.isProcessing).toBe(false)
expect(state.cancelled).toBe(false)
expect(state.status).toBe(AcpSessionStatus.Idle)
expect(state.pendingToolCalls).toBeInstanceOf(Map)
expect(state.pendingToolCalls.size).toBe(0)
expect(state.currentToolCallId).toBeUndefined()
@@ -187,11 +187,11 @@ describe("createSessionState", () => {
const state2 = createSessionState("session-2")
// Modify state1
state1.isProcessing = true
state1.status = AcpSessionStatus.Processing
state1.pendingToolCalls.set("tool-1", {} as acp.ToolCall)
// state2 should be unaffected
expect(state2.isProcessing).toBe(false)
expect(state2.status).toBe(AcpSessionStatus.Idle)
expect(state2.pendingToolCalls.size).toBe(0)
})
})
+6 -2
View File
@@ -11,6 +11,7 @@
import type * as acp from "@agentclientprotocol/sdk"
import type { ClineMessage, ClineSayBrowserAction, ClineSayTool } from "@shared/ExtensionMessage"
import type { AcpSessionState, TranslatedMessage } from "./types.js"
import { AcpSessionStatus } from "./types.js"
/**
* Maps Cline tool types to ACP ToolKind values.
@@ -312,6 +313,10 @@ function translateSayMessage(
// API request finished - no specific update needed
break
case "subagent_usage":
// Hidden aggregate metrics event used for task-level accounting.
break
case "task":
// Task started - don't echo the user's prompt back to them
// The ACP client already knows what they typed
@@ -1015,8 +1020,7 @@ export function translateMessages(messages: ClineMessage[], sessionState: AcpSes
export function createSessionState(sessionId: string): AcpSessionState {
return {
sessionId,
isProcessing: false,
cancelled: false,
status: AcpSessionStatus.Idle,
pendingToolCalls: new Map(),
}
}
+254
View File
@@ -0,0 +1,254 @@
/**
* Public types for the Cline library API.
*
* This file contains types that are safe to export to library consumers.
* It must NOT import any internal types (Controller, StateManager, etc.)
* to keep the generated declaration files clean.
*
* Internal-only extensions of these types live in ./types.ts.
*/
import type * as acp from "@agentclientprotocol/sdk"
// ============================================================
// Session Update Type Utilities
// ============================================================
/**
* Different types of updates that can be sent during session processing.
*
* These updates provide real-time feedback about the agent's progress.
*
* See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-turn#3-agent-reports-output)
*/
export type SessionUpdateType = acp.SessionUpdate["sessionUpdate"]
/**
* Different types of update payloads that can be sent during session processing.
*
* Each update type has a corresponding payload structure defined in the ACP SessionUpdate union.
*/
export type SessionUpdatePayload<T extends SessionUpdateType> = Omit<
Extract<acp.SessionUpdate, { sessionUpdate: T }>,
"sessionUpdate"
>
// ============================================================
// Permission Handler Callback Types
// ============================================================
/**
* Handler function for permission requests.
* Called when the agent needs permission for a tool call.
* The handler should present the request to the user and call resolve() with their response.
*/
export type PermissionHandler = (request: acp.RequestPermissionRequest) => Promise<acp.RequestPermissionResponse>
// ============================================================
// Session Event Emitter Types
// ============================================================
/**
* Maps ACP SessionUpdate types to their event listener signatures.
* Uses the sessionUpdate discriminator to derive event names and payload types.
*/
export type ClineSessionEvents = {
[K in SessionUpdateType]: (payload: SessionUpdatePayload<K>) => void
} & {
/** Error event for session-level errors (not part of ACP SessionUpdate) */
error: (error: Error) => void
}
// ============================================================
// ClineAgent Options
// ============================================================
/**
* Options for creating a ClineAgent instance.
*/
export interface ClineAgentOptions {
/** Whether debug logging is enabled */
debug?: boolean
/** Cline Config Directory (defaults to ~/.cline) */
clineDir?: string
}
/**
* Options for creating an ACP agent instance.
*/
export interface AcpAgentOptions {
/** Whether debug logging is enabled */
debug?: boolean
}
// ============================================================
// Session Types
// ============================================================
export type SessionID = string
/**
* Extended session data stored by Cline for ACP sessions.
*/
export interface ClineAcpSession {
/** Unique session ID */
sessionId: SessionID
/** Working directory for the session */
cwd: string
/** Current mode (plan/act) */
mode: "plan" | "act"
/** MCP servers passed from the client */
mcpServers: acp.McpServer[]
/** Timestamp when session was created */
createdAt: number
/** Timestamp of last activity */
lastActivityAt: number
/** Whether this session was loaded from history (needs resume on first prompt) */
isLoadedFromHistory?: boolean
/** Model ID override for plan mode (format: "provider/modelId") */
planModeModelId?: string
/** Model ID override for act mode (format: "provider/modelId") */
actModeModelId?: string
}
/**
* Lifecycle status of an ACP session.
*
* Represents the state machine:
* Idle → Processing → Idle (normal completion)
* Idle → Processing → Cancelled (cancellation, then back to Idle on next prompt)
*/
export enum AcpSessionStatus {
/** Session is idle, waiting for a prompt */
Idle = "idle",
/** Session is actively processing a prompt */
Processing = "processing",
/** Session processing was cancelled */
Cancelled = "cancelled",
}
/**
* State tracking for an active ACP session within Cline.
*/
export interface AcpSessionState {
/** Session ID */
sessionId: SessionID
/** Current lifecycle status of the session */
status: AcpSessionStatus
/** Current tool call ID being executed (if any) */
currentToolCallId?: string
/** Accumulated tool calls for permission batching */
pendingToolCalls: Map<string, acp.ToolCall>
}
// ============================================================
// Agent Capabilities
// ============================================================
/**
* Cline-specific agent capabilities extending the ACP base capabilities.
*/
export interface ClineAgentCapabilities {
/** Support for loading sessions from disk */
loadSession: boolean
/** Prompt capabilities for the agent */
promptCapabilities: {
/** Support for image inputs */
image: boolean
/** Support for audio inputs */
audio: boolean
/** Support for embedded context (file resources) */
embeddedContext: boolean
}
/** MCP server passthrough capabilities */
mcpCapabilities: {
/** Support for HTTP MCP servers */
http: boolean
/** Support for SSE MCP servers */
sse: boolean
}
}
/**
* Cline agent info for ACP initialization response.
*/
export interface ClineAgentInfo {
name: "cline"
title: "Cline"
version: string
}
// ============================================================
// Permission Options
// ============================================================
/**
* Permission option as presented to the ACP client.
*/
export interface ClinePermissionOption {
kind: acp.PermissionOptionKind
name: string
optionId: string
}
// ============================================================
// Message Translation
// ============================================================
/**
* Result of translating a Cline message to ACP session update(s).
* A single Cline message may produce multiple ACP updates.
*/
export interface TranslatedMessage {
/** The session updates to send */
updates: acp.SessionUpdate[]
/** Whether this message requires a permission request */
requiresPermission?: boolean
/** Permission request details if required */
permissionRequest?: Omit<acp.RequestPermissionRequest, "sessionId">
/** The toolCallId that was created/used (for tracking across streaming updates) */
toolCallId?: string
}
// ============================================================
// Re-exported ACP Types
// ============================================================
export type {
Agent,
AgentSideConnection,
AudioContent,
CancelNotification,
ClientCapabilities,
ContentBlock,
ImageContent,
InitializeRequest,
InitializeResponse,
LoadSessionRequest,
LoadSessionResponse,
McpServer,
ModelInfo,
NewSessionRequest,
NewSessionResponse,
PermissionOption,
PermissionOptionKind,
PromptRequest,
PromptResponse,
RequestPermissionRequest,
RequestPermissionResponse,
SessionConfigOption,
SessionModelState,
SessionNotification,
SessionUpdate,
SetSessionConfigOptionRequest,
SetSessionConfigOptionResponse,
SetSessionModelRequest,
SetSessionModelResponse,
SetSessionModeRequest,
SetSessionModeResponse,
StopReason,
TextContent,
ToolCall,
ToolCallStatus,
ToolCallUpdate,
ToolKind,
} from "@agentclientprotocol/sdk"
+20 -199
View File
@@ -1,76 +1,13 @@
/**
* Custom types and extensions for ACP integration with Cline CLI.
* Internal types for ACP integration with Cline CLI.
*
* This file extends the base ACP types with Cline-specific functionality.
* This file re-exports all public types from ./public-types.ts and adds
* internal-only Types that reference core modules (Controller, etc.).
*
* Library consumers should never import from this file directly — they
* get the public types via the library entrypoint (exports.ts).
*/
import type * as acp from "@agentclientprotocol/sdk"
import type { Controller } from "@/core/controller"
// ============================================================
// Session Update Type Utilities
// ============================================================
/**
* Extract the sessionUpdate discriminator value from a SessionUpdate variant.
*/
export type SessionUpdateType = acp.SessionUpdate["sessionUpdate"]
/**
* Extract the payload type for a given sessionUpdate discriminator value.
* This removes the `sessionUpdate` discriminator field from the type.
*/
export type SessionUpdatePayload<T extends SessionUpdateType> = Omit<
Extract<acp.SessionUpdate, { sessionUpdate: T }>,
"sessionUpdate"
>
// ============================================================
// Permission Handler Callback Types
// ============================================================
/**
* Callback to resolve a permission request with the user's response.
*/
export type PermissionResolver = (response: acp.RequestPermissionResponse) => void
/**
* Handler function for permission requests.
* Called when the agent needs permission for a tool call.
* The handler should present the request to the user and call resolve() with their response.
*/
export type PermissionHandler = (request: Omit<acp.RequestPermissionRequest, "sessionId">, resolve: PermissionResolver) => void
// ============================================================
// Session Event Emitter Types
// ============================================================
/**
* Maps ACP SessionUpdate types to their event listener signatures.
* Uses the sessionUpdate discriminator to derive event names and payload types.
*/
export type ClineSessionEvents = {
[K in SessionUpdateType]: (payload: SessionUpdatePayload<K>) => void
} & {
/** Error event for session-level errors (not part of ACP SessionUpdate) */
error: (error: Error) => void
}
// ============================================================
// ClineAgent Options (decoupled from connection)
// ============================================================
/**
* Options for creating a ClineAgent instance (decoupled from connection).
*/
export interface ClineAgentOptions {
/** CLI version string */
version: string
/** Whether debug logging is enabled */
debug?: boolean
}
// Re-export common ACP types for convenience
export type {
Agent,
AgentSideConnection,
@@ -114,134 +51,18 @@ export type {
WriteTextFileResponse,
} from "@agentclientprotocol/sdk"
/**
* Cline-specific agent capabilities extending the ACP base capabilities.
*/
export interface ClineAgentCapabilities {
/** Support for loading sessions from disk */
loadSession: boolean
/** Prompt capabilities for the agent */
promptCapabilities: {
/** Support for image inputs */
image: boolean
/** Support for audio inputs */
audio: boolean
/** Support for embedded context (file resources) */
embeddedContext: boolean
}
/** MCP server passthrough capabilities */
mcpCapabilities: {
/** Support for HTTP MCP servers */
http: boolean
/** Support for SSE MCP servers */
sse: boolean
}
}
export type {
AcpAgentOptions,
AcpSessionState,
ClineAgentCapabilities,
ClineAgentInfo,
ClineAgentOptions,
ClinePermissionOption,
ClineSessionEvents,
PermissionHandler,
SessionUpdatePayload,
SessionUpdateType,
TranslatedMessage,
} from "./public-types.js"
/**
* Cline agent info for ACP initialization response.
*/
export interface ClineAgentInfo {
name: "cline"
title: "Cline"
version: string
}
/**
* Extended session data stored by Cline for ACP sessions.
* Maps to Cline's task history structure.
*/
export interface ClineAcpSession {
/** Unique session/task ID */
sessionId: string
/** Working directory for the session */
cwd: string
/** Current mode (plan/act) */
mode: "plan" | "act"
/** MCP servers passed from the client */
mcpServers: acp.McpServer[]
/** Timestamp when session was created */
createdAt: number
/** Timestamp of last activity */
lastActivityAt: number
/** Whether this session was loaded from history (needs resume on first prompt) */
isLoadedFromHistory?: boolean
/** Controller instance for this session (manages task execution) */
controller?: Controller
/** Model ID override for plan mode (format: "provider/modelId") */
planModeModelId?: string
/** Model ID override for act mode (format: "provider/modelId") */
actModeModelId?: string
}
/**
* Permission option as presented to the ACP client.
*/
export interface ClinePermissionOption {
kind: acp.PermissionOptionKind
name: string
optionId: string
}
/**
* Mapping of Cline message types to their ACP session update equivalents.
*/
export type ClineToAcpUpdateMapping = {
/** Text messages from the agent */
text: "agent_message_chunk"
/** Reasoning/thinking from the agent */
reasoning: "agent_thought_chunk"
/** Markdown content from the agent */
markdown: "agent_message_chunk"
/** Tool execution */
tool: "tool_call"
/** Command execution */
command: "tool_call"
/** Command output */
command_output: "tool_call_update"
/** Task completion */
completion_result: "end_turn"
/** Error messages */
error: "tool_call_update" | "error"
}
/**
* Options for creating an ACP agent instance.
*/
export interface AcpAgentOptions {
/** CLI version string */
version: string
/** Whether debug logging is enabled */
debug?: boolean
}
/**
* Result of translating a Cline message to ACP session update(s).
* A single Cline message may produce multiple ACP updates.
*/
export interface TranslatedMessage {
/** The session updates to send */
updates: acp.SessionUpdate[]
/** Whether this message requires a permission request */
requiresPermission?: boolean
/** Permission request details if required */
permissionRequest?: Omit<acp.RequestPermissionRequest, "sessionId">
/** The toolCallId that was created/used (for tracking across streaming updates) */
toolCallId?: string
}
/**
* State tracking for an active ACP session within Cline.
*/
export interface AcpSessionState {
/** Session ID */
sessionId: string
/** Whether the session is currently processing a prompt */
isProcessing: boolean
/** Current tool call ID being executed (if any) */
currentToolCallId?: string
/** Whether the session has been cancelled */
cancelled: boolean
/** Accumulated tool calls for permission batching */
pendingToolCalls: Map<string, acp.ToolCall>
}
export { AcpSessionStatus } from "./public-types.js"
+85 -17
View File
@@ -5,15 +5,17 @@
import { Box, Text, useApp, useInput } from "ink"
import Spinner from "ink-spinner"
// biome-ignore lint/style/useImportType: React is used as a value by JSX (jsx: "react" in tsconfig)
import React, { useCallback, useEffect, useMemo, useState } from "react"
import { refreshOcaModels } from "@/core/controller/models/refreshOcaModels"
import { StateManager } from "@/core/storage/StateManager"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { AuthService } from "@/services/auth/AuthService"
import { liteLlmDefaultModelId, openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
import { openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
import { StringRequest } from "@/shared/proto/cline/common"
import { openExternal } from "@/utils/env"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import { useClineFeaturedModels } from "../hooks/useClineFeaturedModels"
import { useOcaAuth } from "../hooks/useOcaAuth"
import { useScrollableList } from "../hooks/useScrollableList"
import { type DetectedSources, detectImportSources, type ImportSource } from "../utils/import-configs"
@@ -22,6 +24,7 @@ import { applyBedrockConfig, applyProviderConfig } from "../utils/provider-confi
import { useValidProviders } from "../utils/providers"
import { ApiKeyInput } from "./ApiKeyInput"
import { StaticRobotFrame } from "./AsciiMotionCli"
import { BedrockCustomModelFlow } from "./BedrockCustomModelFlow"
import { type BedrockConfig, BedrockSetup } from "./BedrockSetup"
import {
FeaturedModelPicker,
@@ -30,7 +33,8 @@ import {
isBrowseAllSelected,
} from "./FeaturedModelPicker"
import { ImportView } from "./ImportView"
import { getDefaultModelId, hasModelPicker, ModelPicker } from "./ModelPicker"
import { CUSTOM_MODEL_ID, getDefaultModelId, hasModelPicker, ModelPicker } from "./ModelPicker"
import { OcaEmployeeCheck } from "./OcaEmployeeCheck"
import { getProviderLabel } from "./ProviderPicker"
type AuthStep =
@@ -43,11 +47,13 @@ type AuthStep =
| "success"
| "error"
| "cline_auth"
| "oca_employee_check"
| "oca_auth"
| "cline_model"
| "openai_codex_auth"
| "bedrock"
| "import"
| "bedrock_custom"
interface AuthViewProps {
controller: any
@@ -73,7 +79,7 @@ const Select: React.FC<{
const [selectedIndex, setSelectedIndex] = useState(0)
useInput(
(input, key) => {
(_, key) => {
if (key.upArrow) {
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1))
} else if (key.downArrow) {
@@ -139,7 +145,11 @@ const TextInput: React.FC<{
return (
<Box>
<Text color="white">{displayValue || placeholder || ""}</Text>
{!displayValue && placeholder ? (
<Text color="gray">e.g. {placeholder}</Text>
) : (
<Text color="white">{displayValue || ""}</Text>
)}
<Text inverse> </Text>
</Box>
)
@@ -160,10 +170,10 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const [modelId, setModelId] = useState("")
const [baseUrl, setBaseUrl] = useState("")
const [errorMessage, setErrorMessage] = useState("")
const [authStatus, setAuthStatus] = useState<string>("")
const [providerSearch, setProviderSearch] = useState("")
const [providerIndex, setProviderIndex] = useState(0)
const [clineModelIndex, setClineModelIndex] = useState(0)
const featuredModels = useClineFeaturedModels()
const [importSources, setImportSources] = useState<DetectedSources>({ codex: false, opencode: false })
const [importSource, setImportSource] = useState<ImportSource | null>(null)
const [bedrockConfig, setBedrockConfig] = useState<BedrockConfig | null>(null)
@@ -171,11 +181,14 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
// OCA auth hook - enabled when step is oca_auth
const handleOcaAuthSuccess = useCallback(async () => {
await applyProviderConfig({ providerId: "oca", controller })
// Fetch OCA models from the API - this sets actModeOcaModelId/planModeOcaModelId in state
await refreshOcaModels(controller, StringRequest.create({ value: "" }))
const stateManager = StateManager.get()
stateManager.setGlobalState("welcomeViewCompleted", true)
await stateManager.flushPendingState()
setSelectedProvider("oca")
setModelId(liteLlmDefaultModelId)
const actModelId = stateManager.getGlobalSettingsKey("actModeOcaModelId") || ""
setModelId(actModelId)
setStep("success")
}, [controller])
@@ -243,6 +256,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
}, [])
// Reset provider index when search changes
// biome-ignore lint/correctness/useExhaustiveDependencies: we want to reset here
useEffect(() => {
setProviderIndex(0)
}, [providerSearch])
@@ -268,7 +282,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
return
}
if (authState.user && authState.user.email) {
if (authState.user?.email) {
// Auth succeeded - save configuration and transition to model selection
await applyProviderConfig({ providerId: "cline", controller })
setSelectedProvider("cline")
@@ -317,7 +331,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const startClineAuth = useCallback(async () => {
try {
setStep("cline_auth")
setAuthStatus("Starting authentication...")
await AuthService.getInstance(controller).createAuthRequest()
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : String(error))
@@ -327,7 +340,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const startOcaAuth = useCallback(() => {
setStep("oca_auth")
setAuthStatus("Starting authentication...")
initiateOcaAuth()
}, [initiateOcaAuth])
@@ -358,7 +370,8 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
(value: string) => {
setSelectedProvider(value)
if (value === "oca") {
startOcaAuth()
// Show employee check screen before starting auth
setStep("oca_employee_check")
} else if (value === "openai-codex") {
setStep("openai_codex_auth")
startOpenAiCodexAuth()
@@ -385,6 +398,33 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
[selectedProvider],
)
// Save custom Bedrock ARN configuration with base model for capability detection
const saveCustomBedrockConfiguration = useCallback(
async (arn: string, baseModelId: string) => {
try {
if (!bedrockConfig) {
throw new Error("Bedrock configuration is missing")
}
await applyBedrockConfig({
bedrockConfig,
modelId: arn,
customModelBaseId: baseModelId,
controller,
})
const stateManager = StateManager.get()
stateManager.setGlobalState("welcomeViewCompleted", true)
await stateManager.flushPendingState()
setStep("success")
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : String(error))
setStep("error")
}
},
[bedrockConfig, controller],
)
const saveConfiguration = useCallback(
async (model: string, base: string) => {
try {
@@ -419,6 +459,12 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const handleModelIdSubmit = useCallback(
(value: string) => {
// Intercept "Custom" selection for Bedrock — redirect to custom ARN input flow
if (value === CUSTOM_MODEL_ID && selectedProvider === "bedrock") {
setStep("bedrock_custom")
return
}
if (value.trim()) {
setModelId(value)
}
@@ -526,6 +572,9 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
// Go back to cline_model if we came from there (Cline provider)
if (selectedProvider === "cline") {
setStep("cline_model")
} else if (selectedProvider === "bedrock") {
// Bedrock skips the API key step — go back to Bedrock setup
setStep("bedrock")
} else {
setStep("apikey")
}
@@ -534,9 +583,12 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
setBaseUrl("")
setStep("modelid")
break
case "oca_auth":
case "oca_employee_check":
setStep("provider")
break
case "oca_auth":
setStep("oca_employee_check")
break
case "cline_auth":
setStep("menu")
break
@@ -639,7 +691,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
<Box flexDirection="column">
<Text color="white">Model ID</Text>
<Text> </Text>
<Text color="gray">e.g., claude-sonnet-4-20250514, gpt-4o</Text>
<Text color="gray">e.g., claude-sonnet-4-6, gpt-4o</Text>
<Text> </Text>
<TextInput onChange={setModelId} onSubmit={handleModelIdSubmit} placeholder="model-id" value={modelId} />
<Text> </Text>
@@ -675,6 +727,9 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
</Box>
)
case "oca_employee_check":
return <OcaEmployeeCheck isActive={step === "oca_employee_check"} onCancel={goBack} onSignIn={startOcaAuth} />
case "oca_auth":
case "cline_auth":
return (
@@ -714,7 +769,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
<Box flexDirection="column">
<Text color="white">Choose a model</Text>
<Text> </Text>
<FeaturedModelPicker selectedIndex={clineModelIndex} />
<FeaturedModelPicker featuredModels={featuredModels} selectedIndex={clineModelIndex} />
</Box>
)
}
@@ -731,6 +786,18 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
/>
)
case "bedrock_custom":
return (
<BedrockCustomModelFlow
isActive={step === "bedrock_custom"}
onCancel={() => setStep("modelid")}
onComplete={(arn, baseModelId) => {
setStep("saving")
saveCustomBedrockConfiguration(arn, baseModelId)
}}
/>
)
case "import":
if (!importSource) {
return null
@@ -760,6 +827,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const [menuIndex, setMenuIndex] = useState(0)
// Steps that allow going back with escape (apikey handled by ApiKeyInput component)
// OcaEmployeeCheck handles its own escape key, so oca_employee_check is not in this list
const canGoBack = [
"provider",
"modelid",
@@ -803,17 +871,17 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
setProviderSearch((prev) => prev + input)
}
} else if (step === "cline_model") {
const maxIndex = getFeaturedModelMaxIndex()
const maxIndex = getFeaturedModelMaxIndex(featuredModels)
if (key.upArrow) {
setClineModelIndex((prev) => (prev > 0 ? prev - 1 : maxIndex))
} else if (key.downArrow) {
setClineModelIndex((prev) => (prev < maxIndex ? prev + 1 : 0))
} else if (key.return) {
if (isBrowseAllSelected(clineModelIndex)) {
if (isBrowseAllSelected(clineModelIndex, featuredModels)) {
setStep("modelid")
} else {
const selectedModel = getFeaturedModelAtIndex(clineModelIndex)
const selectedModel = getFeaturedModelAtIndex(clineModelIndex, featuredModels)
if (selectedModel) {
handleClineModelSelect(selectedModel.id)
}
@@ -0,0 +1,111 @@
/**
* Bedrock Custom Model Flow component
* Two-step flow: ARN/custom model ID input → base model selection for capability detection.
* Used by both AuthView (onboarding) and SettingsPanelContent (/settings).
*/
import { Box, Text, useInput } from "ink"
// biome-ignore lint/correctness/noUnusedImports: React is needed for JSX at runtime
import React, { useCallback, useState } from "react"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import { getModelList } from "./ModelPicker"
import { SearchableList } from "./SearchableList"
type FlowStep = "arn_input" | "base_model"
interface BedrockCustomModelFlowProps {
/** Whether this component should capture keyboard input */
isActive: boolean
/** Called when the user completes both steps (ARN + base model selection) */
onComplete: (arn: string, baseModelId: string) => void
/** Called when the user presses Escape on the first step (ARN input) */
onCancel: () => void
}
export const BedrockCustomModelFlow: React.FC<BedrockCustomModelFlowProps> = ({ isActive, onComplete, onCancel }) => {
const { isRawModeSupported } = useStdinContext()
const [step, setStep] = useState<FlowStep>("arn_input")
const [customArn, setCustomArn] = useState("")
const handleArnSubmit = useCallback(() => {
if (customArn.trim()) {
setStep("base_model")
}
}, [customArn])
const handleBaseModelCancel = useCallback(() => {
setStep("arn_input")
}, [])
useInput(
(input, key) => {
if (step === "arn_input") {
if (key.escape) {
onCancel()
} else if (key.return) {
handleArnSubmit()
} else if (key.backspace || key.delete) {
setCustomArn((prev) => prev.slice(0, -1))
} else if (input && !key.ctrl && !key.meta) {
setCustomArn((prev) => prev + input)
}
return
}
if (step === "base_model") {
if (key.escape) {
handleBaseModelCancel()
}
// Other input is handled by SearchableList
}
},
{ isActive: isActive && isRawModeSupported },
)
if (step === "arn_input") {
return (
<Box flexDirection="column">
<Text bold color={COLORS.primaryBlue}>
Custom Model ID
</Text>
<Box marginTop={1}>
<Text color="gray">Enter your Application Inference Profile ARN or custom model ID</Text>
</Box>
<Box marginTop={1}>
{customArn ? (
<Text color="white">{customArn}</Text>
) : (
<Text color="gray">e.g. arn:aws:bedrock:region:account:application-inference-profile/...</Text>
)}
<Text inverse> </Text>
</Box>
<Box marginTop={1}>
<Text color="gray">Enter to continue, Esc to go back</Text>
</Box>
</Box>
)
}
// step === "base_model"
return (
<Box flexDirection="column">
<Text bold color={COLORS.primaryBlue}>
Base Inference Model
</Text>
<Text color="gray">Select the base model your inference profile uses (for capability detection)</Text>
<Box marginTop={1}>
<SearchableList
isActive={isActive && step === "base_model"}
items={getModelList("bedrock").map((id) => ({ id, label: id }))}
onSelect={(item) => {
onComplete(customArn, item.id)
}}
/>
</Box>
<Box marginTop={1}>
<Text color="gray">Type to search, arrows to navigate, Enter to select, Esc to go back</Text>
</Box>
</Box>
)
}
+19 -9
View File
@@ -114,8 +114,11 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
// Filtered regions
const filteredRegions = useMemo(() => {
const search = regionSearch.toLowerCase()
return search ? AWS_REGIONS.filter((r) => r.includes(search)) : AWS_REGIONS
const search = regionSearch.toLowerCase().trim()
if (!search) {
return AWS_REGIONS
}
return AWS_REGIONS.filter((r) => r.toLowerCase().includes(search))
}, [regionSearch])
const {
@@ -170,10 +173,18 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
}
}, [step, authMethod, onCancel])
const getSelectedRegion = useCallback(() => {
if (filteredRegions.length > 0 && regionIndex >= 0 && regionIndex < filteredRegions.length) {
return filteredRegions[regionIndex]
}
// If no matches, use the search term as custom region
return regionSearch.trim() || "us-east-1"
}, [filteredRegions, regionIndex, regionSearch])
const finish = useCallback(() => {
const config: BedrockConfig = {
awsAuthentication: authMethod === "default" ? "credentials" : authMethod,
awsRegion: filteredRegions[regionIndex] || "us-east-1",
awsRegion: getSelectedRegion(),
awsUseCrossRegionInference: crossRegion,
}
if (authMethod === "profile") {
@@ -184,7 +195,7 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
if (sessionToken) config.awsSessionToken = sessionToken
}
onComplete(config)
}, [authMethod, profileName, accessKey, secretKey, sessionToken, filteredRegions, regionIndex, crossRegion, onComplete])
}, [authMethod, profileName, accessKey, secretKey, sessionToken, getSelectedRegion, crossRegion, onComplete])
// Handle input for auth_method, region, and options steps
useInput(
@@ -204,11 +215,11 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
} else if (step === "region") {
if (key.escape) {
goBack()
} else if (key.upArrow) {
} else if (key.upArrow && filteredRegions.length > 0) {
setRegionIndex((prev) => (prev > 0 ? prev - 1 : filteredRegions.length - 1))
} else if (key.downArrow) {
} else if (key.downArrow && filteredRegions.length > 0) {
setRegionIndex((prev) => (prev < filteredRegions.length - 1 ? prev + 1 : 0))
} else if (key.return && filteredRegions.length > 0) {
} else if (key.return && (filteredRegions.length > 0 || regionSearch.trim())) {
setStep("options")
} else if (key.backspace || key.delete) {
setRegionSearch((prev) => prev.slice(0, -1))
@@ -330,7 +341,7 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
<Text color="white">AWS Region</Text>
<Text> </Text>
<Box>
<Text color="gray">Search: </Text>
<Text color="gray">Search or enter custom region: </Text>
<Text color="white">{regionSearch}</Text>
<Text inverse> </Text>
</Box>
@@ -350,7 +361,6 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
{showRegionBottom && (
<Text color="gray">... {filteredRegions.length - regionVisibleStart - regionVisibleCount} more below</Text>
)}
{filteredRegions.length === 0 && <Text color="gray">No regions match "{regionSearch}"</Text>}
<Text> </Text>
<Text color="gray">Type to search, arrows to navigate, Enter to select, Esc to go back</Text>
</Box>
+106
View File
@@ -0,0 +1,106 @@
import type { ClineMessage } from "@shared/ExtensionMessage"
import { render } from "ink-testing-library"
import React from "react"
import { describe, expect, it, vi } from "vitest"
import { ChatMessage } from "./ChatMessage"
vi.mock("../hooks/useTerminalSize", () => ({
useTerminalSize: () => ({
columns: 120,
rows: 40,
resizeKey: 0,
}),
}))
describe("ChatMessage subagent rendering", () => {
it("renders subagent approval prompts as a tree", () => {
const message: ClineMessage = {
ts: Date.now(),
type: "ask",
ask: "use_subagents",
text: JSON.stringify({
prompts: [
"Find codebase stats and size",
"Find funny comments and easter eggs",
"Find unusual patterns and history",
],
}),
}
const { lastFrame } = render(React.createElement(ChatMessage, { message, mode: "act" }))
const frame = lastFrame() || ""
expect(frame).toContain("Cline wants to run subagents")
expect(frame).toContain("├─ Find codebase stats and size")
expect(frame).toContain("├─ Find funny comments and easter eggs")
expect(frame).toContain("└─ Find unusual patterns and history")
})
it("renders subagent progress rows with compact token stats and completion checks", () => {
const message: ClineMessage = {
ts: Date.now(),
type: "say",
say: "subagent",
text: JSON.stringify({
status: "running",
total: 3,
completed: 1,
successes: 1,
failures: 0,
toolCalls: 21,
inputTokens: 0,
outputTokens: 0,
contextWindow: 0,
maxContextTokens: 0,
maxContextUsagePercentage: 0,
items: [
{
index: 1,
prompt: "Find codebase stats and size",
status: "completed",
toolCalls: 5,
inputTokens: 0,
outputTokens: 0,
totalCost: 0.034,
contextTokens: 24400,
contextWindow: 200000,
contextUsagePercentage: 12.2,
},
{
index: 2,
prompt: "Find funny comments and easter eggs",
status: "running",
toolCalls: 11,
inputTokens: 0,
outputTokens: 0,
totalCost: 0.056,
contextTokens: 31600,
contextWindow: 200000,
contextUsagePercentage: 15.8,
},
{
index: 3,
prompt: "Find unusual patterns and history",
status: "pending",
toolCalls: 5,
inputTokens: 0,
outputTokens: 0,
totalCost: 0,
contextTokens: 28900,
contextWindow: 200000,
contextUsagePercentage: 14.4,
},
],
}),
}
const { lastFrame } = render(React.createElement(ChatMessage, { isStreaming: true, message, mode: "act" }))
const frame = lastFrame() || ""
expect(frame).toContain("Cline is running subagents")
expect(frame).toContain("✓ Find codebase stats and size")
expect(frame).toContain("5 tool uses · 24.4k tokens · $0.03")
expect(frame).toContain("11 tool uses · 31.6k tokens · $0.06")
expect(frame).toContain("5 tool uses · 28.9k tokens · $0.00")
})
})
+16 -7
View File
@@ -17,6 +17,7 @@ import { useTerminalSize } from "../hooks/useTerminalSize"
import { jsonParseSafe } from "../utils/parser"
import { getToolDescription, isFileEditTool, parseToolFromMessage } from "../utils/tools"
import { DiffView } from "./DiffView"
import { SubagentMessage } from "./SubagentMessage"
/**
* Add "(Tab)" hint after "Act mode" mentions.
@@ -24,7 +25,7 @@ import { DiffView } from "./DiffView"
* Matches just "Act mode" without requiring "to " prefix because markdown
* processing may split "toggle to **Act mode**" into separate text chunks.
*/
function addActModeHint(text: string): React.ReactNode[] {
function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
// Match "Act mode" in various capitalizations, but not if already followed by (Tab)
const actModeRegex = /\bact\s+mode\b(?!\s*\(tab\))/gi
const parts = text.split(actModeRegex)
@@ -41,7 +42,7 @@ function addActModeHint(text: string): React.ReactNode[] {
}
if (matches[i]) {
nodes.push(
<React.Fragment key={`act-mode-${i}`}>
<React.Fragment key={`${keyPrefix}-act-mode-${i}`}>
{matches[i]}
<Text color="gray"> (Tab)</Text>
</React.Fragment>,
@@ -59,6 +60,8 @@ function addActModeHint(text: string): React.ReactNode[] {
*/
function renderInlineMarkdown(text: string): React.ReactNode[] {
const nodes: React.ReactNode[] = []
let hintCallIndex = 0
const addHintedText = (value: string) => addActModeHint(value, `hint-${hintCallIndex++}`)
// Match **bold**, *italic*, or `code` - order matters (** before *)
const regex = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g
let lastIndex = 0
@@ -68,7 +71,7 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
// Add text before match (with Act Mode hint processing)
if (match.index > lastIndex) {
const beforeText = text.slice(lastIndex, match.index)
nodes.push(...addActModeHint(beforeText))
nodes.push(...addHintedText(beforeText))
}
const fullMatch = match[0]
@@ -77,7 +80,7 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
// Bold - also process for Act Mode hints inside bold text
const boldContent = fullMatch.slice(2, -2)
const hintedContent = addActModeHint(boldContent)
const hintedContent = addHintedText(boldContent)
nodes.push(
<Text bold key={key}>
{hintedContent}
@@ -100,10 +103,10 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
// Add remaining text (with Act Mode hint processing)
if (lastIndex < text.length) {
nodes.push(...addActModeHint(text.slice(lastIndex)))
nodes.push(...addHintedText(text.slice(lastIndex)))
}
return nodes.length > 0 ? nodes : addActModeHint(text)
return nodes.length > 0 ? nodes : addHintedText(text)
}
/**
@@ -224,7 +227,7 @@ function truncate(text: string, maxLength: number): string {
/**
* Format tool result for display
*/
function formatToolResult(result: string, maxLines: number = 5): string[] {
function formatToolResult(result: string, maxLines = 5): string[] {
const lines = result.split("\n")
if (lines.length <= maxLines) {
return lines
@@ -446,6 +449,10 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
)
}
if ((type === "ask" && ask === "use_subagents") || say === "use_subagents" || say === "subagent") {
return <SubagentMessage isStreaming={isStreaming} message={message} mode={mode} />
}
// MCP response
if (say === "mcp_server_response" && text) {
const lines = formatToolResult(text, 8)
@@ -800,6 +807,8 @@ export const ChatMessageList: React.FC<ChatMessageListProps> = ({ messages, maxM
const displayMessages = messages.filter((m) => {
// Skip api_req_finished, they're just markers
if (m.say === "api_req_finished") return false
// Skip hidden aggregated usage messages
if (m.say === "subagent_usage") return false
// Skip empty text messages
if (m.say === "text" && !m.text?.trim()) return false
// Skip checkpoint messages
+81 -6
View File
@@ -150,9 +150,28 @@ import { HighlightedInput } from "./HighlightedInput"
import { HistoryPanelContent } from "./HistoryPanelContent"
import { providerModels } from "./ModelPicker"
import { SettingsPanelContent } from "./SettingsPanelContent"
import { SkillsPanelContent } from "./SkillsPanelContent"
import { SlashCommandMenu } from "./SlashCommandMenu"
import { ThinkingIndicator } from "./ThinkingIndicator"
/**
* Persistent input storage that survives React remounts (e.g., during terminal resize).
* Keyed by a stable identifier so each task/session maintains its own input state.
*/
interface PersistedInputState {
text: string
cursorPos: number
pastedTexts: Map<number, string>
pasteCounter: number
}
const inputStateStorage = new Map<string, PersistedInputState>()
function getInputStorageKey(controller: any, taskId?: string): string {
// Use taskId if available, otherwise fall back to controller instance
return taskId || (controller?.task?.taskId ?? "default")
}
interface ChatViewProps {
controller?: any
onExit?: () => void
@@ -351,6 +370,9 @@ export const ChatView: React.FC<ChatViewProps> = ({
insertText: insertTextAtCursor,
} = useTextInput()
// Get storage key for persisting input across remounts
const storageKey = useMemo(() => getInputStorageKey(ctrl, taskId), [ctrl, taskId])
// Refs for text input and cursor position (used by useHomeEndKeys and to avoid stale closures in useInput)
const textInputRef = useRef(textInput)
textInputRef.current = textInput
@@ -367,8 +389,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
const [userScrolled, setUserScrolled] = useState(false)
// Pasted text storage - maps placeholder number to full pasted content
const [pastedTexts, setPastedTexts] = useState<Map<number, string>>(new Map())
const pasteCounterRef = useRef(0)
const [pastedTexts, setPastedTexts] = useState<Map<number, string>>(() => {
return inputStateStorage.get(storageKey)?.pastedTexts ?? new Map()
})
const pasteCounterRef = useRef<number>(inputStateStorage.get(storageKey)?.pasteCounter ?? 0)
// Track paste timing to combine chunks that arrive in rapid succession
const lastPasteTimeRef = useRef<number>(0)
const activePasteNumRef = useRef<number>(0)
@@ -389,6 +413,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
| { type: "settings"; initialMode?: "model-picker" | "featured-models"; initialModelKey?: "actModelId" | "planModelId" }
| { type: "history" }
| { type: "help" }
| { type: "skills" }
| null
>(null)
@@ -402,6 +427,29 @@ export const ChatView: React.FC<ChatViewProps> = ({
// Track when we're exiting to hide UI elements before exit
const [isExiting, setIsExiting] = useState(false)
// Restore input state from storage on mount (after resize remount)
useEffect(() => {
const stored = inputStateStorage.get(storageKey)
if (stored) {
setTextInput(stored.text)
setCursorPos(stored.cursorPos)
setPastedTexts(stored.pastedTexts)
pasteCounterRef.current = stored.pasteCounter
}
}, [storageKey, setTextInput, setCursorPos])
// Persist input state to storage whenever it changes (survives remount)
useEffect(() => {
if (textInput || pastedTexts.size > 0) {
inputStateStorage.set(storageKey, {
text: textInput,
cursorPos,
pastedTexts: new Map(pastedTexts),
pasteCounter: pasteCounterRef.current,
})
}
}, [storageKey, textInput, cursorPos, pastedTexts])
// Task switch handling: when switching tasks via /history, we clear the terminal and
// increment a counter used as the root Box's key. This forces React to remount the tree,
// giving us a fresh Static instance. Mirrors how App.tsx handles resize with resizeKey.
@@ -494,12 +542,14 @@ export const ChatView: React.FC<ChatViewProps> = ({
clearState() // Force clear React state (bypasses empty messages check)
setTextInput("")
setCursorPos(0)
// Clear persisted state
inputStateStorage.delete(storageKey)
// Post the now-empty state
if (ctrl) {
ctrl.postStateToWebview()
}
}, [ctrl, clearState])
}, [ctrl, clearState, storageKey])
const refs = useRef({
searchTimeout: null as NodeJS.Timeout | null,
@@ -760,6 +810,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
setCursorPos(0)
setPastedTexts(new Map()) // Clear stored pastes
pasteCounterRef.current = 0
// Clear persisted state
inputStateStorage.delete(storageKey)
try {
await ctrl.task.handleWebviewAskResponse(responseType, expandedText)
@@ -767,7 +819,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
// Controller may be disposed
}
},
[ctrl, pendingAsk, pastedTexts],
[ctrl, pendingAsk, pastedTexts, storageKey],
)
// Handle cancel/interrupt
@@ -858,6 +910,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
setCursorPos(0)
setPastedTexts(new Map()) // Clear stored pastes
pasteCounterRef.current = 0
// Clear persisted state
inputStateStorage.delete(storageKey)
try {
// Convert image paths to data URLs if needed
@@ -885,7 +939,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
onError?.()
}
},
[ctrl, onError, pastedTexts],
[ctrl, onError, pastedTexts, storageKey],
)
// Auto-submit initial prompt if provided
@@ -1104,13 +1158,21 @@ export const ChatView: React.FC<ChatViewProps> = ({
setSlashMenuDismissed(true)
return
}
if (cmd.name === "skills") {
setActivePanel({ type: "skills" })
setTextInput("")
setCursorPos(0)
setSelectedSlashIndex(0)
setSlashMenuDismissed(true)
return
}
if (cmd.name === "clear") {
clearViewAndResetTask()
setSelectedSlashIndex(0)
setSlashMenuDismissed(true)
return
}
if (cmd.name === "exit") {
if (cmd.name === "exit" || cmd.name === "q") {
handleExit()
return
}
@@ -1493,6 +1555,19 @@ export const ChatView: React.FC<ChatViewProps> = ({
{/* Help panel */}
{activePanel?.type === "help" && <HelpPanelContent onClose={() => setActivePanel(null)} />}
{/* Skills panel */}
{activePanel?.type === "skills" && ctrl && (
<SkillsPanelContent
controller={ctrl}
onClose={() => setActivePanel(null)}
onUseSkill={(skillPath) => {
setActivePanel(null)
setTextInput(`@${skillPath} `)
setCursorPos(skillPath.length + 2)
}}
/>
)}
{/* Slash command menu - below input (takes priority over file menu) */}
{showSlashMenu && !activePanel && (
<Box paddingLeft={1} paddingRight={1}>
+124 -18
View File
@@ -13,6 +13,7 @@ import {
import { Box, Text, useApp, useInput } from "ink"
import React, { useMemo, useState } from "react"
import { useStdinContext } from "../context/StdinContext"
import { fuzzyFilter } from "../utils/fuzzy-search"
import {
BooleanSelect,
buildConfigEntries,
@@ -21,6 +22,8 @@ import {
HookInfo,
HookRow,
MAX_VISIBLE,
ObjectEditorPanel,
ObjectEditorState,
parseValue,
SEPARATOR,
SectionHeader,
@@ -105,6 +108,8 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
const [isEditing, setIsEditing] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(0)
const [editValue, setEditValue] = useState("")
const [searchQuery, setSearchQuery] = useState("")
const [objectEditor, setObjectEditor] = useState<ObjectEditorState | null>(null)
// Build entries for settings tab
const configEntries = useMemo(
@@ -112,6 +117,13 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
[globalState, workspaceState],
)
const filteredConfigEntries = useMemo(() => {
if (!searchQuery.trim()) {
return configEntries
}
return fuzzyFilter(configEntries, searchQuery, (entry) => `${entry.key} ${String(entry.value ?? "")}`)
}, [configEntries, searchQuery])
// Build entries for rules tab
const ruleEntries = useMemo(() => {
const entries: ToggleEntry[] = []
@@ -159,7 +171,7 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
const currentListLength = useMemo(() => {
switch (currentTab) {
case "settings":
return configEntries.length
return filteredConfigEntries.length
case "rules":
return ruleEntries.length
case "workflows":
@@ -171,7 +183,14 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
default:
return 0
}
}, [currentTab, configEntries.length, ruleEntries.length, workflowEntries.length, hookEntries.length, skillEntries.length])
}, [
currentTab,
filteredConfigEntries.length,
ruleEntries.length,
workflowEntries.length,
hookEntries.length,
skillEntries.length,
])
// Get available tabs
const availableTabs = useMemo(() => {
@@ -191,10 +210,11 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
setCurrentTab(newTab)
setSelectedIndex(0)
setIsEditing(false)
setObjectEditor(null)
}
// Settings tab handlers
const selectedConfigEntry = configEntries[selectedIndex]
const selectedConfigEntry = filteredConfigEntries[selectedIndex]
const handleSettingsSave = (value: string | boolean) => {
if (!selectedConfigEntry) {
@@ -210,6 +230,43 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
setIsEditing(false)
}
const getObjectAtPath = (root: Record<string, unknown>, path: string[]): Record<string, unknown> => {
let current: unknown = root
for (const segment of path) {
if (!current || typeof current !== "object") {
return {}
}
current = (current as Record<string, unknown>)[segment]
}
return current && typeof current === "object" ? (current as Record<string, unknown>) : {}
}
const setObjectValueAtPath = (
root: Record<string, unknown>,
path: string[],
key: string,
value: unknown,
): Record<string, unknown> => {
if (path.length === 0) {
return { ...root, [key]: value }
}
const [head, ...rest] = path
const child = root[head]
const childObj = child && typeof child === "object" ? (child as Record<string, unknown>) : {}
return {
...root,
[head]: setObjectValueAtPath(childObj, rest, key, value),
}
}
const persistObjectEditor = (nextObject: Record<string, unknown>, source: "global" | "workspace", key: string) => {
if (source === "global" && onUpdateGlobal) {
onUpdateGlobal(key as GlobalStateAndSettingsKey, nextObject as never)
} else if (source === "workspace" && onUpdateWorkspace) {
onUpdateWorkspace(key as LocalStateKey, nextObject as never)
}
}
const handleSettingsReset = () => {
if (!selectedConfigEntry?.isEditable || selectedConfigEntry.source !== "global") {
return
@@ -240,15 +297,22 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
// Input handling
useInput(
(input, key) => {
if (input.toLowerCase() === "q" || key.escape) {
if (objectEditor) {
return
}
if (key.escape) {
exit()
}
// Tab navigation with Tab key or number keys
if (key.tab || (input >= "1" && input <= "5")) {
const targetIdx = key.tab
? (availableTabs.findIndex((t) => t.key === currentTab) + 1) % availableTabs.length
: parseInt(input) - 1
if (key.leftArrow || key.rightArrow || (input >= "1" && input <= "5")) {
const currentTabIndex = availableTabs.findIndex((t) => t.key === currentTab)
const targetIdx =
input >= "1" && input <= "5"
? Number.parseInt(input) - 1
: key.leftArrow
? (currentTabIndex - 1 + availableTabs.length) % availableTabs.length
: (currentTabIndex + 1) % availableTabs.length
if (targetIdx >= 0 && targetIdx < availableTabs.length) {
handleTabChange(availableTabs[targetIdx].key)
}
@@ -256,21 +320,45 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
}
// List navigation (arrow keys and vim-style j/k)
if (key.upArrow || input === "k") {
if (key.upArrow) {
setSelectedIndex((i) => (i > 0 ? i - 1 : currentListLength - 1))
} else if (key.downArrow || input === "j") {
} else if (key.downArrow) {
setSelectedIndex((i) => (i < currentListLength - 1 ? i + 1 : 0))
}
// Tab-specific actions
if (currentTab === "settings") {
if ((key.return || input === "e") && selectedConfigEntry?.isEditable) {
if ((key.return || key.tab) && selectedConfigEntry?.isEditable) {
if (selectedConfigEntry.type === "boolean") {
handleSettingsSave(!selectedConfigEntry.value)
return
}
if (selectedConfigEntry.type === "object") {
const value =
selectedConfigEntry.value && typeof selectedConfigEntry.value === "object"
? (selectedConfigEntry.value as Record<string, unknown>)
: {}
setObjectEditor({
source: selectedConfigEntry.source,
key: selectedConfigEntry.key,
path: [],
value,
selectedIndex: 0,
isEditingValue: false,
editValue: "",
})
return
}
setEditValue(selectedConfigEntry.value !== undefined ? String(selectedConfigEntry.value) : "")
setIsEditing(true)
} else if (input === "r") {
} else if (key.ctrl && input.toLowerCase() === "r") {
handleSettingsReset()
} else if (key.backspace || key.delete) {
setSearchQuery((prev) => prev.slice(0, -1))
} else if (input && !key.ctrl && !key.meta && !key.escape && !key.upArrow && !key.downArrow) {
setSearchQuery((prev) => prev + input)
}
} else if (key.return || input === " ") {
} else if (key.return || key.tab || input === " ") {
// Toggle for rules/workflows/hooks/skills
handleToggle()
}
@@ -338,13 +426,31 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
)
}
if (objectEditor && currentTab === "settings") {
return (
<ObjectEditorPanel
getObjectAtPath={getObjectAtPath}
onClose={() => setObjectEditor(null)}
onPersist={(nextObject) => persistObjectEditor(nextObject, objectEditor.source, objectEditor.key)}
setObjectValueAtPath={setObjectValueAtPath}
setState={setObjectEditor}
state={objectEditor}
/>
)
}
// Render tab content
const renderTabContent = () => {
switch (currentTab) {
case "settings": {
const visibleEntries = configEntries.slice(startIndex, startIndex + MAX_VISIBLE)
const visibleEntries = filteredConfigEntries.slice(startIndex, startIndex + MAX_VISIBLE)
return (
<React.Fragment>
<Box>
<Text>Search: </Text>
<Text color="white">{searchQuery}</Text>
<Text inverse> </Text>
</Box>
<Box>
<Text>Data directory: </Text>
<Text color="blue" underline>
@@ -507,12 +613,12 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
// Help text based on current tab
const getHelpText = () => {
const base = "↑/↓/j/k Navigate • Tab/1-5 Switch tabs • q/Esc Exit"
const base = "↑/↓ Navigate • ←/→ tabs • 1-5 tabs • Esc Exit"
if (currentTab === "settings") {
return `${base} • Enter/e Edit • r Reset`
return `${base} Type to search • Enter/Tab Edit (booleans toggle) • Backspace clear search • Ctrl+R Reset`
}
const openFolder = onOpenFolder ? " • o Open folder" : ""
return `${base} • Enter/Space Toggle${openFolder}`
return `${base} • Enter/Tab/Space Toggle${openFolder}`
}
return (
+180 -11
View File
@@ -46,16 +46,19 @@ export interface SkillInfo {
enabled: boolean
}
export const EXCLUDED_KEYS = new Set([
"taskHistory",
"primaryRootIndex",
"subagentsEnabled",
"subagentTerminalOutputLineLimit",
"welcomeViewCompleted",
"isNewUser",
])
export interface ObjectEditorState {
source: "global" | "workspace"
key: string
path: string[]
value: Record<string, unknown>
selectedIndex: number
isEditingValue: boolean
editValue: string
}
export const EDITABLE_TYPES: Set<ValueType> = new Set(["string", "number", "boolean"])
export const EXCLUDED_KEYS = new Set(["taskHistory", "primaryRootIndex", "welcomeViewCompleted", "isNewUser"])
export const EDITABLE_TYPES: Set<ValueType> = new Set(["string", "number", "boolean", "object"])
export const MAX_VISIBLE = 12
export const SEPARATOR = "─".repeat(80)
@@ -135,7 +138,7 @@ export function parseValue(input: string, type: ValueType): unknown {
return input.toLowerCase() === "true" || input === "1"
}
if (type === "number") {
const num = parseFloat(input)
const num = Number.parseFloat(input)
return Number.isNaN(num) ? 0 : num
}
if (type === "object") {
@@ -217,7 +220,7 @@ export const TextInput: React.FC<TextInputProps> = ({ label, onChange, onCancel,
</Text>
<Box>
<Text color="white">{value}</Text>
<Text inverse> </Text>
<Text color="cyan">|</Text>
</Box>
<Text color="gray">Type: {type} Enter to save Esc to cancel</Text>
</Box>
@@ -379,3 +382,169 @@ export const SectionHeader: React.FC<{ title: string }> = ({ title }) => (
</Text>
</Box>
)
interface ObjectEditorPanelProps {
state: ObjectEditorState
setState: React.Dispatch<React.SetStateAction<ObjectEditorState | null>>
onClose: () => void
onPersist: (nextObject: Record<string, unknown>) => void
getObjectAtPath: (root: Record<string, unknown>, path: string[]) => Record<string, unknown>
setObjectValueAtPath: (root: Record<string, unknown>, path: string[], key: string, value: unknown) => Record<string, unknown>
}
export const ObjectEditorPanel: React.FC<ObjectEditorPanelProps> = ({
state,
setState,
onClose,
onPersist,
getObjectAtPath,
setObjectValueAtPath,
}) => {
const { isRawModeSupported } = useStdinContext()
const currentNode = getObjectAtPath(state.value, state.path)
const objectEntries = Object.entries(currentNode).sort(([a], [b]) => a.localeCompare(b))
const selectedEntry = objectEntries[state.selectedIndex]
const breadcrumb = [state.key, ...state.path].join(" ")
useInput(
(input, key) => {
if (state.isEditingValue) {
if (key.escape) {
setState((prev) => (prev ? { ...prev, isEditingValue: false, editValue: "" } : prev))
return
}
if (key.return) {
if (!selectedEntry) {
setState((prev) => (prev ? { ...prev, isEditingValue: false, editValue: "" } : prev))
return
}
const [entryKey, entryValue] = selectedEntry
let parsed: unknown = state.editValue
if (typeof entryValue === "boolean") {
parsed = state.editValue.toLowerCase() === "true" || state.editValue === "1"
} else if (typeof entryValue === "number") {
const maybeNum = Number(state.editValue)
parsed = Number.isNaN(maybeNum) ? 0 : maybeNum
}
const nextObject = setObjectValueAtPath(state.value, state.path, entryKey, parsed)
onPersist(nextObject)
setState((prev) => (prev ? { ...prev, value: nextObject, isEditingValue: false, editValue: "" } : prev))
return
}
if (key.backspace || key.delete) {
setState((prev) => (prev ? { ...prev, editValue: prev.editValue.slice(0, -1) } : prev))
return
}
if (input && !key.ctrl && !key.meta) {
setState((prev) => (prev ? { ...prev, editValue: prev.editValue + input } : prev))
}
return
}
if (key.escape) {
if (state.path.length > 0) {
setState((prev) => (prev ? { ...prev, path: prev.path.slice(0, -1), selectedIndex: 0 } : prev))
} else {
onClose()
}
return
}
if (key.upArrow || input === "k") {
setState((prev) =>
prev
? {
...prev,
selectedIndex:
objectEntries.length > 0
? prev.selectedIndex > 0
? prev.selectedIndex - 1
: objectEntries.length - 1
: 0,
}
: prev,
)
return
}
if (key.downArrow || input === "j") {
setState((prev) =>
prev
? {
...prev,
selectedIndex:
objectEntries.length > 0
? prev.selectedIndex < objectEntries.length - 1
? prev.selectedIndex + 1
: 0
: 0,
}
: prev,
)
return
}
if (key.return || key.tab) {
if (!selectedEntry) {
return
}
const [entryKey, entryValue] = selectedEntry
if (typeof entryValue === "boolean") {
const nextObject = setObjectValueAtPath(state.value, state.path, entryKey, !entryValue)
onPersist(nextObject)
setState((prev) => (prev ? { ...prev, value: nextObject } : prev))
return
}
if (entryValue && typeof entryValue === "object" && !Array.isArray(entryValue)) {
setState((prev) => (prev ? { ...prev, path: [...prev.path, entryKey], selectedIndex: 0 } : prev))
return
}
setState((prev) =>
prev
? { ...prev, isEditingValue: true, editValue: entryValue !== undefined ? String(entryValue) : "" }
: prev,
)
}
},
{ isActive: isRawModeSupported },
)
return (
<Box flexDirection="column">
<Text bold color="white">
Edit Nested Object
</Text>
<Text color="gray">{SEPARATOR}</Text>
<Text color="cyan">{breadcrumb}</Text>
{state.isEditingValue ? (
<Box flexDirection="column" marginTop={1}>
<Box>
<Text color="white">{state.editValue}</Text>
<Text color="cyan">|</Text>
</Box>
<Text color="gray">Enter to save Esc to cancel</Text>
</Box>
) : (
<Box flexDirection="column" marginTop={1}>
{objectEntries.length === 0 ? (
<Text color="gray">No nested keys at this level.</Text>
) : (
objectEntries.map(([key, value], idx) => {
const isSelected = idx === state.selectedIndex
const valueText =
value && typeof value === "object" && !Array.isArray(value) ? "{...}" : String(value)
return (
<Text color={isSelected ? "cyan" : undefined} key={key}>
{isSelected ? " " : " "}
<Text color="cyan">{key}</Text>
<Text color="gray">: </Text>
<Text color="white">{valueText}</Text>
</Text>
)
})
)}
<Text color="gray">/ Navigate Enter/Tab Edit or drill in Esc Back/Close</Text>
</Box>
)}
</Box>
)
}
+11 -12
View File
@@ -7,13 +7,14 @@
import { Box, Text } from "ink"
import React from "react"
import { COLORS } from "../constants/colors"
import { type FeaturedModel, getAllFeaturedModels } from "../constants/featured-models"
import type { FeaturedModel } from "../constants/featured-models"
interface FeaturedModelPickerProps {
selectedIndex: number
title?: string
showBrowseAll?: boolean
helpText?: string
featuredModels: FeaturedModel[]
}
export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
@@ -21,8 +22,9 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
title,
showBrowseAll = true,
helpText = "Arrows to navigate, Enter to select",
featuredModels,
}) => {
const featuredModels = getAllFeaturedModels()
const models = featuredModels
return (
<Box flexDirection="column">
@@ -35,11 +37,11 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
</Text>
)}
{featuredModels.map((model, i) => {
{models.map((model, i) => {
const isSelected = i === selectedIndex
return (
<Box flexDirection="column" key={model.id} marginBottom={1}>
<Box flexDirection="column" key={`${model.id}-${model.labels[0] || "default"}`} marginBottom={1}>
<Box>
<Text color={isSelected ? COLORS.primaryBlue : undefined}>{isSelected ? " " : " "}</Text>
<Text bold color={isSelected ? COLORS.primaryBlue : "white"}>
@@ -64,8 +66,8 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
{showBrowseAll && (
<Box>
<Text color={selectedIndex === featuredModels.length ? COLORS.primaryBlue : "white"}>
{selectedIndex === featuredModels.length ? " " : " "}
<Text color={selectedIndex === models.length ? COLORS.primaryBlue : "white"}>
{selectedIndex === models.length ? " " : " "}
Browse all models...
</Text>
</Box>
@@ -81,24 +83,21 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
* Get the maximum valid index for the featured model picker
* (includes "Browse all" option if showBrowseAll is true)
*/
export function getFeaturedModelMaxIndex(showBrowseAll: boolean = true): number {
const featuredModels = getAllFeaturedModels()
export function getFeaturedModelMaxIndex(featuredModels: FeaturedModel[], showBrowseAll = true): number {
return showBrowseAll ? featuredModels.length : featuredModels.length - 1
}
/**
* Check if the selected index is the "Browse all" option
*/
export function isBrowseAllSelected(selectedIndex: number): boolean {
const featuredModels = getAllFeaturedModels()
export function isBrowseAllSelected(selectedIndex: number, featuredModels: FeaturedModel[]): boolean {
return selectedIndex === featuredModels.length
}
/**
* Get the featured model at the given index, or null if "Browse all" is selected
*/
export function getFeaturedModelAtIndex(index: number): FeaturedModel | null {
const featuredModels = getAllFeaturedModels()
export function getFeaturedModelAtIndex(index: number, featuredModels: FeaturedModel[]): FeaturedModel | null {
if (index >= 0 && index < featuredModels.length) {
return featuredModels[index]
}
+28
View File
@@ -43,6 +43,30 @@ export const HelpPanelContent: React.FC<HelpPanelContentProps> = ({ onClose }) =
</Text>
</Box>
<Box flexDirection="column">
<Text bold>Keyboard Shortcuts</Text>
<Text>
{" "}
<Text color="white">Ctrl+U</Text> - Clear entire input (delete to start)
</Text>
<Text>
{" "}
<Text color="white">Ctrl+K</Text> - Delete from cursor to end
</Text>
<Text>
{" "}
<Text color="white">Ctrl+W</Text> - Delete word backwards
</Text>
<Text>
{" "}
<Text color="white">Ctrl+A / Ctrl+E</Text> - Jump to start / end of input
</Text>
<Text>
{" "}
<Text color="white">Alt/Option+/</Text> - Move by word
</Text>
</Box>
<Box flexDirection="column">
<Text bold>Slash Commands</Text>
<Text>
@@ -64,6 +88,10 @@ export const HelpPanelContent: React.FC<HelpPanelContentProps> = ({ onClose }) =
{" "}
<Text color="white">/clear</Text> - Start a fresh task
</Text>
<Text>
{" "}
<Text color="white">/q</Text> - Quit Cline
</Text>
</Box>
<Text>
+34 -6
View File
@@ -6,6 +6,7 @@
import { Box, Text } from "ink"
import Spinner from "ink-spinner"
import React, { useEffect, useMemo, useState } from "react"
import { refreshOcaModels } from "@/core/controller/models/refreshOcaModels"
import { refreshOpenRouterModels } from "@/core/controller/models/refreshOpenRouterModels"
import {
type ApiProvider,
@@ -64,11 +65,15 @@ import {
xaiDefaultModelId,
xaiModels,
} from "@/shared/api"
import { StringRequest } from "@/shared/proto/cline/common"
import { filterOpenRouterModelIds } from "@/shared/utils/model-filters"
import { COLORS } from "../constants/colors"
import { getOpenRouterDefaultModelId, usesOpenRouterModels } from "../utils/openrouter-models"
import { SearchableList, SearchableListItem } from "./SearchableList"
// Special ID used to indicate the user wants to enter a custom model ID / ARN
export const CUSTOM_MODEL_ID = "__custom__"
// Map providers to their static model lists and defaults
export const providerModels: Record<string, { models: Record<string, unknown>; defaultId: string }> = {
anthropic: { models: anthropicModels, defaultId: anthropicDefaultModelId },
@@ -105,7 +110,7 @@ export function hasStaticModels(provider: string): boolean {
}
export function hasModelPicker(provider: string): boolean {
return hasStaticModels(provider) || usesOpenRouterModels(provider)
return hasStaticModels(provider) || usesOpenRouterModels(provider) || provider === "oca"
}
export function getDefaultModelId(provider: string): string {
@@ -132,7 +137,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({ provider, controller,
const [isLoading, setIsLoading] = useState(false)
const [asyncModels, setAsyncModels] = useState<string[]>([])
// Fetch OpenRouter models when needed using shared core function
// Fetch async models (OpenRouter or OCA) when needed
useEffect(() => {
if (usesOpenRouterModels(provider)) {
setIsLoading(true)
@@ -145,22 +150,45 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({ provider, controller,
.finally(() => {
setIsLoading(false)
})
} else if (provider === "oca") {
setIsLoading(true)
refreshOcaModels(controller, StringRequest.create({ value: "" }))
.then((result) => {
if (result.models) {
const modelIds = Object.keys(result.models).sort((a, b) => a.localeCompare(b))
setAsyncModels(modelIds)
}
})
.finally(() => {
setIsLoading(false)
})
}
}, [provider, controller])
const modelList = useMemo(() => {
if (usesOpenRouterModels(provider)) {
if (usesOpenRouterModels(provider) || provider === "oca") {
return asyncModels
}
return getModelList(provider)
}, [provider, asyncModels])
// Providers that support custom model IDs (e.g., Bedrock Application Inference Profiles)
const supportsCustomModel = provider === "bedrock"
const items: SearchableListItem[] = useMemo(() => {
return modelList.map((modelId) => ({
const list = modelList.map((modelId) => ({
id: modelId,
label: modelId,
}))
}, [modelList])
// Add "Custom" option at the end for providers that support it
if (supportsCustomModel) {
list.push({
id: CUSTOM_MODEL_ID,
label: "Custom (ARN / Inference Profile)",
})
}
return list
}, [modelList, supportsCustomModel])
// For providers without a model picker, render nothing
if (!hasModelPicker(provider)) {
@@ -180,7 +208,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({ provider, controller,
}
// If async fetch returned no models, render nothing
if (usesOpenRouterModels(provider) && modelList.length === 0) {
if ((usesOpenRouterModels(provider) || provider === "oca") && modelList.length === 0) {
return null
}
+88
View File
@@ -0,0 +1,88 @@
/**
* OCA (Oracle Cloud Assist) employee check component.
* Shows a checkbox for "I'm an Oracle Employee" and a sign-in button.
* Sets ocaMode in state before triggering the OAuth flow.
*/
import { Box, Text, useInput } from "ink"
// biome-ignore lint/style/useImportType: React is used as a value by JSX (jsx: "react" in tsconfig)
import React, { useCallback, useState } from "react"
import { StateManager } from "@/core/storage/StateManager"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
interface OcaEmployeeCheckProps {
/** Whether this component is active and should handle input */
isActive: boolean
/** Called when user confirms and wants to proceed with sign-in */
onSignIn: () => void
/** Called when user presses Escape to go back */
onCancel: () => void
}
export const OcaEmployeeCheck: React.FC<OcaEmployeeCheckProps> = ({ isActive, onSignIn, onCancel }) => {
const { isRawModeSupported } = useStdinContext()
const [isEmployee, setIsEmployee] = useState(true) // Default to checked (internal), matching extension behavior
const [selectedIndex, setSelectedIndex] = useState(0) // 0 = checkbox, 1 = sign in button
const ITEM_COUNT = 2
const handleSignIn = useCallback(async () => {
// Persist ocaMode to state before starting auth
const stateManager = StateManager.get()
stateManager.setGlobalState("ocaMode", isEmployee ? "internal" : "external")
await stateManager.flushPendingState()
onSignIn()
}, [isEmployee, onSignIn])
useInput(
(_input, key) => {
if (key.escape) {
onCancel()
return
}
if (key.upArrow) {
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : ITEM_COUNT - 1))
} else if (key.downArrow) {
setSelectedIndex((prev) => (prev < ITEM_COUNT - 1 ? prev + 1 : 0))
} else if (key.tab || (key.return && selectedIndex === 0)) {
// Toggle checkbox when Tab is pressed or Enter on checkbox item
if (selectedIndex === 0) {
setIsEmployee((prev) => !prev)
}
} else if (key.return && selectedIndex === 1) {
// Sign in button
handleSignIn()
}
},
{ isActive: isRawModeSupported && isActive },
)
return (
<Box flexDirection="column">
<Text color="white">Oracle Code Assist</Text>
<Text> </Text>
{/* Checkbox: I'm an Oracle Employee */}
<Text>
<Text bold color={selectedIndex === 0 ? COLORS.primaryBlue : undefined}>
{selectedIndex === 0 ? "" : " "}{" "}
</Text>
<Text color={selectedIndex === 0 || isEmployee ? COLORS.primaryBlue : "gray"}>{isEmployee ? "[✓]" : "[ ]"}</Text>
<Text color={selectedIndex === 0 ? COLORS.primaryBlue : "white"}> I'm an Oracle Employee</Text>
{selectedIndex === 0 && <Text color="gray"> (Tab to toggle)</Text>}
</Text>
{/* Sign in button */}
<Text>
<Text bold color={selectedIndex === 1 ? COLORS.primaryBlue : undefined}>
{selectedIndex === 1 ? "" : " "}{" "}
</Text>
<Text color={selectedIndex === 1 ? COLORS.primaryBlue : "white"}>Sign in with Oracle Code Assist</Text>
{selectedIndex === 1 && <Text color="gray"> (Enter)</Text>}
</Text>
<Text> </Text>
<Text color="gray">Please ask your IT administrator to set up Oracle Code Assist as a model provider.</Text>
<Text> </Text>
<Text color="gray">Arrows to navigate, Tab to toggle, Enter to continue, Esc to go back</Text>
</Box>
)
}
+112
View File
@@ -0,0 +1,112 @@
import { render } from "ink-testing-library"
// biome-ignore lint/correctness/noUnusedImports: React must be in scope for JSX in this test file.
import React from "react"
import { beforeEach, describe, expect, it, vi } from "vitest"
// Mock ink's useApp
const mockExit = vi.fn()
vi.mock("ink", async (importOriginal) => {
const actual = await importOriginal<typeof import("ink")>()
return {
...actual,
useApp: () => ({ exit: mockExit }),
}
})
// Mock child_process
vi.mock("child_process", () => ({
execSync: vi.fn().mockReturnValue(""),
exec: vi.fn(),
}))
// Mock dependencies
vi.mock("@/core/controller/slash/getAvailableSlashCommands", () => ({
getAvailableSlashCommands: vi.fn().mockResolvedValue({ commands: [] }),
}))
vi.mock("@/core/storage/StateManager", () => ({
StateManager: {
get: () => ({
getGlobalSettingsKey: vi.fn().mockReturnValue("act"),
getGlobalStateKey: vi.fn().mockReturnValue([]),
getApiConfiguration: vi.fn().mockReturnValue({}),
}),
},
}))
vi.mock("@/services/telemetry", () => ({
telemetryService: {
captureHostEvent: vi.fn(),
},
}))
vi.mock("@shared/services/Session", () => ({
Session: {
get: () => ({
getStats: vi.fn().mockReturnValue({}),
}),
},
}))
vi.mock("../context/TaskContext", () => ({
useTaskContext: () => ({
controller: {},
clearState: vi.fn(),
}),
useTaskState: () => ({
clineMessages: [],
}),
}))
vi.mock("../hooks/useStateSubscriber", () => ({
useIsSpinnerActive: () => ({ isActive: false, startTime: 0 }),
}))
import { ChatView } from "./ChatView"
// Helper to wait for async state updates
const delay = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms))
describe("Quit Command (/q and /exit)", () => {
const mockOnExit = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it("should exit the application when /q is selected from slash menu", async () => {
const { stdin } = render(<ChatView onExit={mockOnExit} />)
await delay()
// Type /q
stdin.write("/q")
await delay()
// Press Enter
stdin.write("\r")
// handleExit has a 150ms timeout
await delay(200)
expect(mockExit).toHaveBeenCalled()
expect(mockOnExit).toHaveBeenCalled()
})
it("should exit the application when /exit is selected from slash menu", async () => {
const { stdin } = render(<ChatView onExit={mockOnExit} />)
await delay()
// Type /exit
stdin.write("/exit")
await delay()
// Press Enter
stdin.write("\r")
// handleExit has a 150ms timeout
await delay(200)
expect(mockExit).toHaveBeenCalled()
expect(mockOnExit).toHaveBeenCalled()
})
})
+246 -25
View File
@@ -7,24 +7,30 @@ import type { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import type { ApiProvider, ModelInfo } from "@shared/api"
import { getProviderModelIdKey, isSettingsKey, ProviderToApiKeyMap } from "@shared/storage"
import { isOpenaiReasoningEffort, OPENAI_REASONING_EFFORT_OPTIONS, type OpenaiReasoningEffort } from "@shared/storage/types"
import type { TelemetrySetting } from "@shared/TelemetrySetting"
import { Box, Text, useInput } from "ink"
import Spinner from "ink-spinner"
import React, { useCallback, useEffect, useMemo, useState } from "react"
import { buildApiHandler } from "@/core/api"
import type { Controller } from "@/core/controller"
import { refreshOcaModels } from "@/core/controller/models/refreshOcaModels"
import { StateManager } from "@/core/storage/StateManager"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { ClineAccountService } from "@/services/account/ClineAccountService"
import { AuthService, ClineAccountOrganization } from "@/services/auth/AuthService"
import { StringRequest } from "@/shared/proto/cline/common"
import { openExternal } from "@/utils/env"
import { supportsReasoningEffortForModel } from "@/utils/model-utils"
import { version as CLI_VERSION } from "../../package.json"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import { useClineFeaturedModels } from "../hooks/useClineFeaturedModels"
import { useOcaAuth } from "../hooks/useOcaAuth"
import { isMouseEscapeSequence } from "../utils/input"
import { applyBedrockConfig, applyProviderConfig } from "../utils/provider-config"
import { ApiKeyInput } from "./ApiKeyInput"
import { BedrockCustomModelFlow } from "./BedrockCustomModelFlow"
import { type BedrockConfig, BedrockSetup } from "./BedrockSetup"
import { Checkbox } from "./Checkbox"
import {
@@ -34,7 +40,8 @@ import {
isBrowseAllSelected,
} from "./FeaturedModelPicker"
import { LanguagePicker } from "./LanguagePicker"
import { hasModelPicker, ModelPicker } from "./ModelPicker"
import { CUSTOM_MODEL_ID, hasModelPicker, ModelPicker } from "./ModelPicker"
import { OcaEmployeeCheck } from "./OcaEmployeeCheck"
import { OrganizationPicker } from "./OrganizationPicker"
import { Panel, PanelTab } from "./Panel"
import { getProviderLabel, ProviderPicker } from "./ProviderPicker"
@@ -51,13 +58,25 @@ type SettingsTab = "api" | "auto-approve" | "features" | "other" | "account"
interface ListItem {
key: string
label: string
type: "checkbox" | "readonly" | "editable" | "separator" | "header" | "spacer" | "action"
type: "checkbox" | "readonly" | "editable" | "separator" | "header" | "spacer" | "action" | "cycle"
value: string | boolean
description?: string
isSubItem?: boolean
parentKey?: string
}
function normalizeReasoningEffort(value: unknown): OpenaiReasoningEffort {
if (isOpenaiReasoningEffort(value)) {
return value
}
return "low"
}
function nextReasoningEffort(current: OpenaiReasoningEffort): OpenaiReasoningEffort {
const idx = OPENAI_REASONING_EFFORT_OPTIONS.indexOf(current)
return OPENAI_REASONING_EFFORT_OPTIONS[(idx + 1) % OPENAI_REASONING_EFFORT_OPTIONS.length]
}
const TABS: PanelTab[] = [
{ key: "api", label: "API" },
{ key: "auto-approve", label: "Auto-approve" },
@@ -68,6 +87,12 @@ const TABS: PanelTab[] = [
// Settings configuration for simple boolean toggles
const FEATURE_SETTINGS = {
subagents: {
stateKey: "subagentsEnabled",
default: false,
label: "Subagents",
description: "Let Cline run focused subagents in parallel to explore the codebase for you",
},
autoCondense: {
stateKey: "useAutoCondense",
default: false,
@@ -98,6 +123,12 @@ const FEATURE_SETTINGS = {
label: "Parallel tool calling",
description: "Allow multiple tools in a single response",
},
doubleCheckCompletion: {
stateKey: "doubleCheckCompletionEnabled",
default: false,
label: "Double-check completion",
description: "Reject first completion attempt and require re-verification",
},
} as const
type FeatureKey = keyof typeof FEATURE_SETTINGS
@@ -131,16 +162,21 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
)
const [isPickingFeaturedModel, setIsPickingFeaturedModel] = useState(initialMode === "featured-models")
const [featuredModelIndex, setFeaturedModelIndex] = useState(0)
const featuredModels = useClineFeaturedModels()
const [isPickingProvider, setIsPickingProvider] = useState(false)
const [isPickingLanguage, setIsPickingLanguage] = useState(false)
const [isEnteringApiKey, setIsEnteringApiKey] = useState(false)
const [isConfiguringBedrock, setIsConfiguringBedrock] = useState(false)
const [isWaitingForCodexAuth, setIsWaitingForCodexAuth] = useState(false)
const [isShowingOcaEmployeeCheck, setIsShowingOcaEmployeeCheck] = useState(false)
const [codexAuthError, setCodexAuthError] = useState<string | null>(null)
const [pendingProvider, setPendingProvider] = useState<string | null>(null)
const [apiKeyValue, setApiKeyValue] = useState("")
const [editValue, setEditValue] = useState("")
// Bedrock custom ARN flow state
const [isBedrockCustomFlow, setIsBedrockCustomFlow] = useState(false)
// Settings state - single object for feature toggles
const [features, setFeatures] = useState<Record<FeatureKey, boolean>>(() => {
const initial: Record<string, boolean> = {}
@@ -165,6 +201,12 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
const [planThinkingEnabled, setPlanThinkingEnabled] = useState<boolean>(
() => (stateManager.getGlobalSettingsKey("planModeThinkingBudgetTokens") ?? 0) > 0,
)
const [actReasoningEffort, setActReasoningEffort] = useState<OpenaiReasoningEffort>(() =>
normalizeReasoningEffort(stateManager.getGlobalSettingsKey("actModeReasoningEffort")),
)
const [planReasoningEffort, setPlanReasoningEffort] = useState<OpenaiReasoningEffort>(() =>
normalizeReasoningEffort(stateManager.getGlobalSettingsKey("planModeReasoningEffort")),
)
// Auto-approve settings (complex nested object)
const [autoApproveSettings, setAutoApproveSettings] = useState<AutoApprovalSettings>(() => {
@@ -203,6 +245,8 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
// OCA auth hook
const handleOcaAuthSuccess = useCallback(async () => {
await applyProviderConfig({ providerId: "oca", controller })
// Fetch OCA models from the API - this sets actModeOcaModelId/planModeOcaModelId in state
await refreshOcaModels(controller!, StringRequest.create({ value: "" }))
setProvider("oca")
refreshModelIds()
}, [controller, refreshModelIds])
@@ -395,9 +439,12 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
// Build items list based on current tab
const items: ListItem[] = useMemo(() => {
// OpenAI Native, Codex, and GPT models don't support thinking budget (they use reasoning effort)
const isGptModel = actModelId?.toLowerCase().includes("gpt") || planModelId?.toLowerCase().includes("gpt")
const showThinkingOption = provider !== "openai-native" && provider !== "openai-codex" && !isGptModel
// Some providers/models expose reasoning effort instead of thinking budget controls.
const providerUsesReasoningEffort = provider === "openai-native" || provider === "openai-codex"
const showActReasoningEffort = supportsReasoningEffortForModel(actModelId || "")
const showPlanReasoningEffort = supportsReasoningEffortForModel(planModelId || "")
const showActThinkingOption = !providerUsesReasoningEffort && !showActReasoningEffort
const showPlanThinkingOption = !providerUsesReasoningEffort && !showPlanReasoningEffort
switch (currentTab) {
case "api":
@@ -421,7 +468,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
type: "editable" as const,
value: actModelId || "not set",
},
...(showThinkingOption
...(showActThinkingOption
? [
{
key: "actThinkingEnabled",
@@ -431,6 +478,16 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
},
]
: []),
...(showActReasoningEffort
? [
{
key: "actReasoningEffort",
label: "Reasoning effort",
type: "cycle" as const,
value: actReasoningEffort,
},
]
: []),
{ key: "planHeader", label: "Plan Mode", type: "header" as const, value: "" },
{
key: "planModelId",
@@ -438,7 +495,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
type: "editable" as const,
value: planModelId || "not set",
},
...(showThinkingOption
...(showPlanThinkingOption
? [
{
key: "planThinkingEnabled",
@@ -448,6 +505,16 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
},
]
: []),
...(showPlanReasoningEffort
? [
{
key: "planReasoningEffort",
label: "Reasoning effort",
type: "cycle" as const,
value: planReasoningEffort,
},
]
: []),
{ key: "spacer1", label: "", type: "spacer" as const, value: "" },
]
: [
@@ -457,7 +524,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
type: "editable" as const,
value: actModelId || "not set",
},
...(showThinkingOption
...(showActThinkingOption
? [
{
key: "actThinkingEnabled",
@@ -467,6 +534,16 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
},
]
: []),
...(showActReasoningEffort
? [
{
key: "actReasoningEffort",
label: "Reasoning effort",
type: "cycle" as const,
value: actReasoningEffort,
},
]
: []),
]),
{
key: "separateModels",
@@ -629,6 +706,8 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
separateModels,
actThinkingEnabled,
planThinkingEnabled,
actReasoningEffort,
planReasoningEffort,
autoApproveSettings,
features,
preferredLanguage,
@@ -662,6 +741,33 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
}
}, [items.length, selectedIndex])
const rebuildTaskApi = useCallback(() => {
if (!controller?.task) {
return
}
const currentMode = stateManager.getGlobalSettingsKey("mode")
const apiConfig = stateManager.getApiConfiguration()
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
}, [controller, stateManager])
const setReasoningEffortForMode = useCallback(
(mode: "act" | "plan", effort: OpenaiReasoningEffort) => {
if (mode === "act") {
setActReasoningEffort(effort)
stateManager.setGlobalState("actModeReasoningEffort", effort)
if (!separateModels) {
setPlanReasoningEffort(effort)
stateManager.setGlobalState("planModeReasoningEffort", effort)
}
} else {
setPlanReasoningEffort(effort)
stateManager.setGlobalState("planModeReasoningEffort", effort)
}
rebuildTaskApi()
},
[separateModels, rebuildTaskApi, stateManager],
)
// Handle toggle/edit for selected item
const handleAction = useCallback(() => {
const item = items[selectedIndex]
@@ -685,6 +791,15 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
return
}
if (item.type === "cycle") {
const targetMode = item.key === "actReasoningEffort" ? "act" : item.key === "planReasoningEffort" ? "plan" : undefined
if (targetMode) {
const currentEffort = targetMode === "act" ? actReasoningEffort : planReasoningEffort
setReasoningEffortForMode(targetMode, nextReasoningEffort(currentEffort))
}
return
}
if (item.type === "editable") {
// For provider field, use the provider picker
if (item.key === "provider") {
@@ -742,7 +857,16 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
const actModel = stateManager.getGlobalSettingsKey(actKey)
if (planKey) stateManager.setGlobalState(planKey, actModel)
}
const actThinkingBudget = stateManager.getGlobalSettingsKey("actModeThinkingBudgetTokens") ?? 0
stateManager.setGlobalState("planModeThinkingBudgetTokens", actThinkingBudget)
setPlanThinkingEnabled(actThinkingBudget > 0)
const actEffort = normalizeReasoningEffort(stateManager.getGlobalSettingsKey("actModeReasoningEffort"))
stateManager.setGlobalState("planModeReasoningEffort", actEffort)
setPlanReasoningEffort(actEffort)
}
rebuildTaskApi()
return
}
@@ -750,23 +874,19 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
if (item.key === "actThinkingEnabled") {
setActThinkingEnabled(newValue)
stateManager.setGlobalState("actModeThinkingBudgetTokens", newValue ? 1024 : 0)
// Rebuild API handler to apply thinking budget change
if (controller?.task) {
const currentMode = stateManager.getGlobalSettingsKey("mode")
const apiConfig = stateManager.getApiConfiguration()
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
if (!separateModels) {
setPlanThinkingEnabled(newValue)
stateManager.setGlobalState("planModeThinkingBudgetTokens", newValue ? 1024 : 0)
}
// Rebuild API handler to apply thinking budget change
rebuildTaskApi()
return
}
if (item.key === "planThinkingEnabled") {
setPlanThinkingEnabled(newValue)
stateManager.setGlobalState("planModeThinkingBudgetTokens", newValue ? 1024 : 0)
// Rebuild API handler to apply thinking budget change
if (controller?.task) {
const currentMode = stateManager.getGlobalSettingsKey("mode")
const apiConfig = stateManager.getApiConfiguration()
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
}
rebuildTaskApi()
return
}
@@ -823,12 +943,63 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
handleClineLogin,
handleClineLogout,
accountOrganizations,
separateModels,
actReasoningEffort,
planReasoningEffort,
rebuildTaskApi,
setReasoningEffortForMode,
])
// Handle completion of the Bedrock custom ARN flow (ARN + base model selected)
const handleBedrockCustomFlowComplete = useCallback(
async (arn: string, baseModelId: string) => {
if (!pickingModelKey) return
const apiConfig = stateManager.getApiConfiguration()
// Build a minimal BedrockConfig from current state for applyBedrockConfig
const bedrockConfig: BedrockConfig = {
awsRegion: apiConfig.awsRegion ?? "us-east-1",
awsAuthentication: apiConfig.awsUseProfile ? "profile" : "credentials",
awsUseCrossRegionInference: Boolean(apiConfig.awsUseCrossRegionInference),
}
await applyBedrockConfig({
bedrockConfig,
modelId: arn,
customModelBaseId: baseModelId,
controller,
})
// Flush pending state to ensure everything is persisted
await stateManager.flushPendingState()
// Rebuild API handler if there's an active task
rebuildTaskApi()
refreshModelIds()
setIsBedrockCustomFlow(false)
setPickingModelKey(null)
// If opened from /models command, close the entire settings panel
if (initialMode) {
onClose()
}
},
[pickingModelKey, stateManager, controller, rebuildTaskApi, refreshModelIds, initialMode, onClose],
)
// Handle model selection from picker
const handleModelSelect = useCallback(
async (modelId: string) => {
if (!pickingModelKey) return
// Intercept "Custom" selection for Bedrock — redirect to custom ARN input flow
if (modelId === CUSTOM_MODEL_ID && provider === "bedrock") {
setIsPickingModel(false)
setIsBedrockCustomFlow(true)
return
}
const apiConfig = stateManager.getApiConfiguration()
const actProvider = apiConfig.actModeApiProvider
const planProvider = apiConfig.planModeApiProvider || actProvider
@@ -889,7 +1060,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
onClose()
}
},
[pickingModelKey, separateModels, stateManager, controller, refreshModelIds, initialMode, onClose],
[pickingModelKey, separateModels, stateManager, controller, provider, refreshModelIds, initialMode, onClose],
)
// Handle language selection from picker
@@ -965,8 +1136,8 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
setProvider("oca")
refreshModelIds()
} else {
// Not logged in - trigger OAuth
startOcaAuth()
// Not logged in - show employee check before auth
setIsShowingOcaEmployeeCheck(true)
}
return
}
@@ -1123,7 +1294,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
// Featured model picker mode (Cline provider)
if (isPickingFeaturedModel) {
const maxIndex = getFeaturedModelMaxIndex()
const maxIndex = getFeaturedModelMaxIndex(featuredModels)
if (key.escape) {
setIsPickingFeaturedModel(false)
@@ -1137,12 +1308,12 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
} else if (key.downArrow) {
setFeaturedModelIndex((prev) => (prev < maxIndex ? prev + 1 : 0))
} else if (key.return) {
if (isBrowseAllSelected(featuredModelIndex)) {
if (isBrowseAllSelected(featuredModelIndex, featuredModels)) {
// Switch to full ModelPicker
setIsPickingFeaturedModel(false)
setIsPickingModel(true)
} else {
const selectedModel = getFeaturedModelAtIndex(featuredModelIndex)
const selectedModel = getFeaturedModelAtIndex(featuredModelIndex, featuredModels)
if (selectedModel && pickingModelKey) {
handleModelSelect(selectedModel.id)
setIsPickingFeaturedModel(false)
@@ -1213,6 +1384,11 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
return
}
// Bedrock custom flow - input handled by BedrockCustomModelFlow component
if (isBedrockCustomFlow) {
return
}
if (isEditing) {
if (key.escape) {
setIsEditing(false)
@@ -1257,7 +1433,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
return
}
},
{ isActive: isRawModeSupported && !isEnteringApiKey && !isConfiguringBedrock },
{ isActive: isRawModeSupported && !isEnteringApiKey && !isConfiguringBedrock && !isShowingOcaEmployeeCheck },
)
// Render content
@@ -1348,6 +1524,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
const label = pickingModelKey === "actModelId" ? "Model ID (Act)" : "Model ID (Plan)"
return (
<FeaturedModelPicker
featuredModels={featuredModels}
helpText="Arrows to navigate, Enter to select, Esc to cancel"
selectedIndex={featuredModelIndex}
title={`Select: ${label}`}
@@ -1433,6 +1610,19 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
)
}
if (isShowingOcaEmployeeCheck) {
return (
<OcaEmployeeCheck
isActive={isShowingOcaEmployeeCheck}
onCancel={() => setIsShowingOcaEmployeeCheck(false)}
onSignIn={() => {
setIsShowingOcaEmployeeCheck(false)
startOcaAuth()
}}
/>
)
}
if (isWaitingForOcaAuth) {
return (
<Box flexDirection="column">
@@ -1452,6 +1642,20 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
)
}
// Bedrock custom model flow (ARN input + base model selection)
if (isBedrockCustomFlow) {
return (
<BedrockCustomModelFlow
isActive={isBedrockCustomFlow}
onCancel={() => {
setIsBedrockCustomFlow(false)
setIsPickingModel(true)
}}
onComplete={handleBedrockCustomFlowComplete}
/>
)
}
// Account tab - loading state
if (currentTab === "account" && isAccountLoading) {
return (
@@ -1569,6 +1773,21 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
)
}
if (item.type === "cycle") {
return (
<Text key={item.key}>
<Text bold color={isSelected ? COLORS.primaryBlue : undefined}>
{isSelected ? "" : " "}{" "}
</Text>
<Text color={isSelected ? COLORS.primaryBlue : "white"}>{item.label}: </Text>
<Text color={COLORS.primaryBlue}>
{typeof item.value === "string" ? item.value : String(item.value)}
</Text>
{isSelected && <Text color="gray"> (Tab to cycle)</Text>}
</Text>
)
}
// Readonly or editable field
return (
<Text key={item.key}>
@@ -1599,7 +1818,9 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
!!codexAuthError ||
isPickingOrganization ||
isWaitingForClineAuth ||
isShowingOcaEmployeeCheck ||
isWaitingForOcaAuth ||
isBedrockCustomFlow ||
isEditing
return (
@@ -0,0 +1,230 @@
/**
* Tests for SkillsPanelContent component
*
* Tests keyboard interactions and callbacks.
* Rendering tests are limited due to ink-testing-library constraints with nested components.
*/
import { render } from "ink-testing-library"
// biome-ignore lint/correctness/noUnusedImports: React must be in scope for JSX in this test file.
import React from "react"
import { beforeEach, describe, expect, it, vi } from "vitest"
// Mock refreshSkills
const mockRefreshSkills = vi.fn()
vi.mock("@/core/controller/file/refreshSkills", () => ({
refreshSkills: () => mockRefreshSkills(),
}))
// Mock toggleSkill
const mockToggleSkill = vi.fn()
vi.mock("@/core/controller/file/toggleSkill", () => ({
toggleSkill: (...args: unknown[]) => mockToggleSkill(...args),
}))
// Mock child_process exec
const mockExec = vi.fn()
vi.mock("node:child_process", () => ({
exec: (...args: unknown[]) => mockExec(...args),
}))
// Mock StdinContext
vi.mock("../context/StdinContext", () => ({
useStdinContext: () => ({ isRawModeSupported: true }),
}))
import { SkillsPanelContent } from "./SkillsPanelContent"
// Helper to wait for async state updates
const delay = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms))
describe("SkillsPanelContent", () => {
const mockController = {} as any
const mockOnClose = vi.fn()
const mockOnUseSkill = vi.fn()
const defaultProps = {
controller: mockController,
onClose: mockOnClose,
onUseSkill: mockOnUseSkill,
}
beforeEach(() => {
vi.clearAllMocks()
mockRefreshSkills.mockResolvedValue({
globalSkills: [],
localSkills: [],
})
})
describe("keyboard interactions", () => {
it("should call onClose when Escape is pressed", async () => {
mockRefreshSkills.mockResolvedValue({
globalSkills: [],
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
stdin.write("\x1B") // Escape
await delay()
expect(mockOnClose).toHaveBeenCalled()
})
it("should call onUseSkill with skill path when Enter is pressed on a skill", async () => {
mockRefreshSkills.mockResolvedValue({
globalSkills: [{ name: "test-skill", description: "Test", path: "/test/path/SKILL.md", enabled: true }],
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
stdin.write("\r") // Enter
await delay()
expect(mockOnUseSkill).toHaveBeenCalledWith("/test/path/SKILL.md")
})
it("should call toggleSkill when Space is pressed on a skill", async () => {
mockRefreshSkills.mockResolvedValue({
globalSkills: [{ name: "test-skill", description: "Test", path: "/test/path/SKILL.md", enabled: true }],
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
stdin.write(" ") // Space
await delay()
expect(mockToggleSkill).toHaveBeenCalledWith(
mockController,
expect.objectContaining({
skillPath: "/test/path/SKILL.md",
isGlobal: true,
enabled: false, // toggled from true to false
}),
)
})
it("should open marketplace URL when Enter is pressed on marketplace item", async () => {
mockRefreshSkills.mockResolvedValue({
globalSkills: [{ name: "skill", description: "desc", path: "/path", enabled: true }],
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
// Navigate down to marketplace (past the one skill)
stdin.write("\x1B[B") // Down arrow
await delay()
stdin.write("\r") // Enter
await delay()
// Should have called exec with open command
expect(mockExec).toHaveBeenCalled()
const execCall = mockExec.mock.calls[0][0]
expect(execCall).toContain("https://skills.sh/")
})
it("should navigate through skills with arrow keys", async () => {
mockRefreshSkills.mockResolvedValue({
globalSkills: [
{ name: "skill-1", description: "First", path: "/path1", enabled: true },
{ name: "skill-2", description: "Second", path: "/path2", enabled: true },
],
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
// Navigate down
stdin.write("\x1B[B") // Down arrow
await delay()
// Press Enter - should use second skill
stdin.write("\r")
await delay()
expect(mockOnUseSkill).toHaveBeenCalledWith("/path2")
})
it("should navigate with vim keys (j/k)", async () => {
mockRefreshSkills.mockResolvedValue({
globalSkills: [
{ name: "skill-1", description: "First", path: "/path1", enabled: true },
{ name: "skill-2", description: "Second", path: "/path2", enabled: true },
],
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
// Navigate down with j
stdin.write("j")
await delay()
// Press Enter - should use second skill
stdin.write("\r")
await delay()
expect(mockOnUseSkill).toHaveBeenCalledWith("/path2")
})
it("should revert optimistic toggle on failure", async () => {
mockRefreshSkills.mockResolvedValue({
globalSkills: [{ name: "test-skill", description: "Test", path: "/test/path/SKILL.md", enabled: true }],
localSkills: [],
})
mockToggleSkill.mockRejectedValueOnce(new Error("toggle failed"))
const { stdin, lastFrame } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
stdin.write(" ") // Space to toggle
await delay(100)
// toggleSkill was called with enabled: false (toggled from true)
expect(mockToggleSkill).toHaveBeenCalledWith(mockController, expect.objectContaining({ enabled: false }))
const frame = lastFrame() || ""
expect(frame).toContain("● test-skill")
expect(frame).not.toContain("○ test-skill")
})
it("should wrap navigation at list boundaries", async () => {
mockRefreshSkills.mockResolvedValue({
globalSkills: [{ name: "only-skill", description: "Only", path: "/only", enabled: true }],
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
// Navigate up from first item (should wrap to last - marketplace)
stdin.write("\x1B[A") // Up arrow
await delay()
stdin.write("\r") // Enter
await delay()
// Should have opened marketplace (wrapped to last item)
expect(mockExec).toHaveBeenCalled()
})
})
describe("skill loading", () => {
it("should call refreshSkills on mount", async () => {
render(<SkillsPanelContent {...defaultProps} />)
await delay()
expect(mockRefreshSkills).toHaveBeenCalled()
})
})
})
+257
View File
@@ -0,0 +1,257 @@
/**
* Skills panel content for inline display in ChatView
* Shows installed skills with toggle and use functionality
*/
import { exec } from "node:child_process"
import os from "node:os"
import { Box, Text, useInput } from "ink"
import React, { useCallback, useEffect, useMemo, useState } from "react"
import type { Controller } from "@/core/controller"
import { refreshSkills } from "@/core/controller/file/refreshSkills"
import { toggleSkill } from "@/core/controller/file/toggleSkill"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import { isMouseEscapeSequence } from "../utils/input"
import { Panel } from "./Panel"
const SKILLS_MARKETPLACE_URL = "https://skills.sh/"
interface SkillInfo {
name: string
description: string
path: string
enabled: boolean
}
interface SkillsPanelContentProps {
controller: Controller
onClose: () => void
onUseSkill: (skillPath: string) => void
}
const MAX_VISIBLE = 8
export const SkillsPanelContent: React.FC<SkillsPanelContentProps> = ({ controller, onClose, onUseSkill }) => {
const { isRawModeSupported } = useStdinContext()
const [globalSkills, setGlobalSkills] = useState<SkillInfo[]>([])
const [localSkills, setLocalSkills] = useState<SkillInfo[]>([])
const [selectedIndex, setSelectedIndex] = useState(0)
const [isLoading, setIsLoading] = useState(true)
// Load skills on mount
useEffect(() => {
const loadSkills = async () => {
try {
const skillsData = await refreshSkills(controller)
setGlobalSkills(skillsData.globalSkills || [])
setLocalSkills(skillsData.localSkills || [])
} catch (_error) {
// Skills loading failed, show empty state
} finally {
setIsLoading(false)
}
}
loadSkills()
}, [controller])
// Build flat list of skills with source info (global first, then local, alphabetical within each)
const skillEntries = useMemo(() => {
const entries: { skill: SkillInfo; isGlobal: boolean }[] = []
globalSkills.forEach((skill) => entries.push({ skill, isGlobal: true }))
localSkills.forEach((skill) => entries.push({ skill, isGlobal: false }))
return entries.sort((a, b) => {
if (a.isGlobal !== b.isGlobal) return a.isGlobal ? -1 : 1
return a.skill.name.localeCompare(b.skill.name)
})
}, [globalSkills, localSkills])
// Handle toggle
const handleToggle = useCallback(async () => {
const entry = skillEntries[selectedIndex]
if (!entry) return
const newEnabled = !entry.skill.enabled
const setter = entry.isGlobal ? setGlobalSkills : setLocalSkills
const update = (enabled: boolean) =>
setter((prev) => prev.map((s) => (s.path === entry.skill.path ? { ...s, enabled } : s)))
// Optimistic update
update(newEnabled)
try {
await toggleSkill(controller, {
metadata: undefined,
skillPath: entry.skill.path,
isGlobal: entry.isGlobal,
enabled: newEnabled,
})
} catch {
// Revert on failure
update(!newEnabled)
}
}, [controller, skillEntries, selectedIndex])
// Handle use skill (insert @ mention)
const handleUse = useCallback(() => {
const entry = skillEntries[selectedIndex]
if (!entry) return
onUseSkill(entry.skill.path)
}, [skillEntries, selectedIndex, onUseSkill])
// Handle opening the marketplace URL
const openMarketplace = useCallback(() => {
const platform = os.platform()
let command: string
if (platform === "darwin") {
command = `open "${SKILLS_MARKETPLACE_URL}"`
} else if (platform === "win32") {
command = `start "${SKILLS_MARKETPLACE_URL}"`
} else {
command = `xdg-open "${SKILLS_MARKETPLACE_URL}"`
}
exec(command, (err) => {
if (err) {
// Fallback: show URL in terminal if browser open fails
console.error(`Visit: ${SKILLS_MARKETPLACE_URL}`)
}
})
}, [])
// Total items = skills + 1 for marketplace link
const totalItems = skillEntries.length + 1
const isMarketplaceSelected = selectedIndex === skillEntries.length
useInput(
(input, key) => {
if (isMouseEscapeSequence(input)) {
return
}
if (key.escape) {
onClose()
return
}
// Navigation
if (key.upArrow || input === "k") {
setSelectedIndex((i) => (i > 0 ? i - 1 : totalItems - 1))
return
}
if (key.downArrow || input === "j") {
setSelectedIndex((i) => (i < totalItems - 1 ? i + 1 : 0))
return
}
// Actions
if (key.return) {
if (isMarketplaceSelected) {
openMarketplace()
} else {
handleUse()
}
return
}
if (input === " " && !isMarketplaceSelected) {
handleToggle()
return
}
},
{ isActive: isRawModeSupported },
)
// Scrolling window (includes marketplace row)
const halfVisible = Math.floor(MAX_VISIBLE / 2)
const startIndex = Math.max(0, Math.min(selectedIndex - halfVisible, totalItems - MAX_VISIBLE))
if (isLoading) {
return (
<Panel label="Skills">
<Text color="gray">Loading skills...</Text>
</Panel>
)
}
// Check if marketplace row is in visible window
const marketplaceIndex = skillEntries.length
const showMarketplace = marketplaceIndex >= startIndex && marketplaceIndex < startIndex + MAX_VISIBLE
return (
<Panel label="Skills">
<Box flexDirection="column" gap={1}>
{skillEntries.length === 0 ? (
<Box flexDirection="column" gap={1}>
<Text color="gray">No skills installed.</Text>
<Text>
Install skills with: <Text color="white">npx skills add owner/repo</Text>
</Text>
</Box>
) : (
<Box flexDirection="column">
{skillEntries
.slice(startIndex, Math.min(startIndex + MAX_VISIBLE, skillEntries.length))
.map((entry, idx) => {
const actualIndex = startIndex + idx
const prevEntry = skillEntries[actualIndex - 1]
const showHeader = actualIndex === 0 || (prevEntry && prevEntry.isGlobal !== entry.isGlobal)
return (
<React.Fragment key={entry.skill.path}>
{showHeader && (
<Box marginTop={actualIndex > 0 ? 1 : 0}>
<Text bold color="gray">
{entry.isGlobal ? "Global Skills:" : "Workspace Skills:"}
</Text>
</Box>
)}
<SkillRow isSelected={actualIndex === selectedIndex} skill={entry.skill} />
</React.Fragment>
)
})}
</Box>
)}
{/* Marketplace link - selectable */}
{showMarketplace && (
<Box marginTop={1}>
<Text color={isMarketplaceSelected ? "cyan" : undefined}>
{isMarketplaceSelected ? " " : " "}
<Text color={COLORS.primaryBlue}>Browse more skills at https://skills.sh/</Text>
</Text>
</Box>
)}
{/* Help text */}
<Box marginTop={1}>
<Text color="gray">
/ Navigate Enter {isMarketplaceSelected ? "Open" : "Use"}
{!isMarketplaceSelected && " • Space Toggle"}
</Text>
</Box>
</Box>
</Panel>
)
}
const SkillRow: React.FC<{ skill: SkillInfo; isSelected: boolean }> = ({ skill, isSelected }) => {
return (
<Box flexDirection="column">
<Box>
<Text color={isSelected ? "cyan" : undefined}>
{isSelected ? " " : " "}
<Text color={skill.enabled ? "green" : "red"}>{skill.enabled ? "●" : "○"}</Text>
<Text> </Text>
<Text bold color="white">
{skill.name}
</Text>
</Text>
</Box>
{skill.description && (
<Box marginLeft={4}>
<Text color="gray">
{skill.description.length > 60 ? skill.description.slice(0, 57) + "..." : skill.description}
</Text>
</Box>
)}
</Box>
)
}
+361
View File
@@ -0,0 +1,361 @@
import type { ClineAskUseSubagents, ClineMessage, ClineSaySubagentStatus } from "@shared/ExtensionMessage"
import { Box, Text } from "ink"
import Spinner from "ink-spinner"
import React from "react"
import { COLORS } from "../constants/colors"
import { useTerminalSize } from "../hooks/useTerminalSize"
import { jsonParseSafe } from "../utils/parser"
interface SubagentMessageProps {
message: ClineMessage
isStreaming?: boolean
mode?: "act" | "plan"
}
const TREE_PREFIX_WIDTH = 5
const MIN_PROMPT_WIDTH = 20
const DotRow: React.FC<{ children: React.ReactNode; color?: string; flashing?: boolean }> = ({
children,
color,
flashing = false,
}) => (
<Box flexDirection="row">
<Box width={2}>
{flashing ? (
<Text color={color}>
<Spinner type="toggle8" />
</Text>
) : (
<Text color={color}></Text>
)}
</Box>
<Box flexGrow={1}>{children}</Box>
</Box>
)
function formatCompactTokens(tokens: number | undefined): string {
const value = Number.isFinite(tokens) ? Math.max(0, tokens || 0) : 0
return new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
})
.format(value)
.toLowerCase()
}
function formatCompactCost(cost: number | undefined): string {
const value = Number.isFinite(cost) ? Math.max(0, cost || 0) : 0
const maximumFractionDigits = value >= 0.01 ? 2 : 4
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits,
}).format(value)
}
function formatSubagentStatsValues(
toolCalls: number | undefined,
contextTokens: number | undefined,
totalCost: number | undefined,
latestToolCall?: string,
) {
const safeToolCalls = Number.isFinite(toolCalls) ? Math.max(0, toolCalls || 0) : 0
const toolUses = safeToolCalls === 1 ? "tool use" : "tool uses"
const tokensUsed = formatCompactTokens(contextTokens || 0)
const formattedCost = formatCompactCost(totalCost || 0)
const stats = `${safeToolCalls} ${toolUses} · ${tokensUsed} tokens · ${formattedCost}`
const latestTool = latestToolCall?.trim()
return latestTool ? `${latestTool} · ${stats}` : stats
}
function wrapPrompt(text: string, width: number): string[] {
if (!text) {
return [""]
}
const normalizedWidth = Math.max(1, width)
const wrappedLines: string[] = []
const paragraphs = text.split("\n")
for (const paragraph of paragraphs) {
const words = paragraph.trim().split(/\s+/).filter(Boolean)
if (words.length === 0) {
wrappedLines.push("")
continue
}
let line = ""
for (const word of words) {
if (!line) {
if (word.length <= normalizedWidth) {
line = word
continue
}
let remaining = word
while (remaining.length > normalizedWidth) {
wrappedLines.push(remaining.slice(0, normalizedWidth))
remaining = remaining.slice(normalizedWidth)
}
line = remaining
continue
}
if (line.length + 1 + word.length <= normalizedWidth) {
line = `${line} ${word}`
continue
}
wrappedLines.push(line)
if (word.length <= normalizedWidth) {
line = word
continue
}
let remaining = word
while (remaining.length > normalizedWidth) {
wrappedLines.push(remaining.slice(0, normalizedWidth))
remaining = remaining.slice(normalizedWidth)
}
line = remaining
}
if (line) {
wrappedLines.push(line)
}
}
return wrappedLines.length > 0 ? wrappedLines : [text]
}
const TreePromptRow: React.FC<{
prefix: React.ReactNode
continuationPrefix: string
prompt: string
promptWidth: number
color?: string
}> = ({ prefix, continuationPrefix, prompt, promptWidth, color }) => {
const lines = wrapPrompt(prompt, promptWidth)
return (
<Box flexDirection="column" width="100%">
{lines.map((line, index) => (
<Box flexDirection="row" key={`${line}-${index}`} width="100%">
<Box flexShrink={0} width={TREE_PREFIX_WIDTH}>
{index === 0 ? prefix : <Text color="gray">{continuationPrefix}</Text>}
</Box>
<Box flexGrow={1}>
<Text color={color}>{line}</Text>
</Box>
</Box>
))}
</Box>
)
}
const TreeStatsRow: React.FC<{ prefix: string; stats: string }> = ({ prefix, stats }) => (
<Box flexDirection="row" width="100%">
<Box flexShrink={0} width={TREE_PREFIX_WIDTH}>
<Text color="gray">{prefix}</Text>
</Box>
<Box flexGrow={1}>
<Text color="gray"> {stats}</Text>
</Box>
</Box>
)
export const SubagentMessage: React.FC<SubagentMessageProps> = ({ message, mode, isStreaming }) => {
const { type, ask, say, text, partial } = message
const toolColor = mode === "plan" ? "yellow" : COLORS.primaryBlue
const { columns } = useTerminalSize()
const promptWidth = Math.max(MIN_PROMPT_WIDTH, columns - 2 - TREE_PREFIX_WIDTH)
if ((type === "ask" && ask === "use_subagents") || say === "use_subagents") {
const parsed = text
? jsonParseSafe<ClineAskUseSubagents>(text, {
prompts: [],
})
: { prompts: [] }
const prompts = (parsed.prompts || []).map((prompt) => prompt?.trim()).filter(Boolean)
if (prompts.length === 0) {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor}>
<Text color={toolColor}>Cline wants to run subagents:</Text>
</DotRow>
</Box>
)
}
const singular = prompts.length === 1
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
<Text color={toolColor}>{singular ? "Cline wants to run a subagent:" : "Cline wants to run subagents:"}</Text>
</DotRow>
<Box flexDirection="column" marginLeft={2} width="100%">
{prompts.map((prompt, index) => {
const isLastPrompt = index === prompts.length - 1
const branch = isLastPrompt ? "└─" : "├─"
const continuationPrefix = isLastPrompt ? " " : "│ "
const shouldShowPromptStats = partial !== true || !isLastPrompt
return (
<Box flexDirection="column" key={`${prompt}-${index}`}>
<TreePromptRow
color={toolColor}
continuationPrefix={continuationPrefix}
prefix={<Text color={toolColor}>{`${branch} `}</Text>}
prompt={prompt}
promptWidth={promptWidth}
/>
{shouldShowPromptStats && (
<TreeStatsRow
prefix={continuationPrefix}
stats={formatSubagentStatsValues(undefined, undefined, undefined)}
/>
)}
</Box>
)
})}
</Box>
</Box>
)
}
if (say === "subagent" && text) {
const parsed = jsonParseSafe<ClineSaySubagentStatus>(text, {
status: "running",
total: 0,
completed: 0,
successes: 0,
failures: 0,
toolCalls: 0,
inputTokens: 0,
outputTokens: 0,
contextWindow: 0,
maxContextTokens: 0,
maxContextUsagePercentage: 0,
items: [],
})
const items = parsed.items || []
if (items.length === 0) {
return null
}
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
<Text color={toolColor}>
{items.length === 1 ? "Cline is running a subagent:" : "Cline is running subagents:"}
</Text>
</DotRow>
<Box flexDirection="column" marginLeft={2} width="100%">
{items.map((entry, index) => {
const isLastEntry = index === items.length - 1
const branch = isLastEntry ? "└─" : "├─"
const continuationPrefix = isLastEntry ? " " : "│ "
const key = `${entry.index}-${index}`
const shouldShowStats = true
if (entry.status === "completed") {
return (
<Box flexDirection="column" key={key}>
<TreePromptRow
color="green"
continuationPrefix={continuationPrefix}
prefix={
<Box flexDirection="row">
<Text color="gray">{`${branch} `}</Text>
<Text color="green"></Text>
</Box>
}
prompt={entry.prompt}
promptWidth={promptWidth}
/>
<TreeStatsRow
prefix={continuationPrefix}
stats={formatSubagentStatsValues(
entry.toolCalls,
entry.contextTokens,
entry.totalCost,
entry.latestToolCall,
)}
/>
</Box>
)
}
if (entry.status === "failed") {
return (
<Box flexDirection="column" key={key}>
<TreePromptRow
color="red"
continuationPrefix={continuationPrefix}
prefix={
<Box flexDirection="row">
<Text color="gray">{`${branch} `}</Text>
<Text color="red"></Text>
</Box>
}
prompt={entry.prompt}
promptWidth={promptWidth}
/>
<TreeStatsRow
prefix={continuationPrefix}
stats={formatSubagentStatsValues(
entry.toolCalls,
entry.contextTokens,
entry.totalCost,
entry.latestToolCall,
)}
/>
</Box>
)
}
return (
<Box flexDirection="column" key={key}>
<TreePromptRow
color={toolColor}
continuationPrefix={continuationPrefix}
prefix={
<Box flexDirection="row">
<Text color="gray">{branch} </Text>
{entry.status === "running" ? (
<Text color={toolColor}>
<Spinner type="dots" />
</Text>
) : (
<Text color={toolColor}></Text>
)}
</Box>
}
prompt={entry.prompt}
promptWidth={promptWidth}
/>
{shouldShowStats && (
<TreeStatsRow
prefix={continuationPrefix}
stats={formatSubagentStatsValues(
entry.toolCalls,
entry.contextTokens,
entry.totalCost,
entry.latestToolCall,
)}
/>
)}
</Box>
)
})}
</Box>
</Box>
)
}
return null
}
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest"
import { getAllFeaturedModels, mapRecommendedModelsToFeaturedModels } from "./featured-models"
describe("featured models", () => {
it("includes display names for all featured models", () => {
const models = getAllFeaturedModels()
for (const model of models) {
expect(model.name).toBeTruthy()
}
})
it("fills free model metadata from fallback when upstream payload is sparse", () => {
const models = mapRecommendedModelsToFeaturedModels({
recommended: [],
free: [{ id: "trinity-large-preview:free", name: "trinity-large-preview:free", description: "", tags: [] }],
})
expect(models.free[0]?.name).toBe("Arcee AI Trinity Large Preview")
expect(models.free[0]?.description).toBe("Arcee AI's advanced large preview model in the Trinity series")
expect(models.free[0]?.labels).toContain("FREE")
})
})
+76 -49
View File
@@ -2,6 +2,7 @@
* Featured models shown in the Cline model picker during onboarding
* These are curated models that work well with Cline
*/
import { CLINE_RECOMMENDED_MODELS_FALLBACK } from "@shared/cline/recommended-models"
export interface FeaturedModel {
id: string
@@ -10,55 +11,81 @@ export interface FeaturedModel {
labels: string[]
}
export const FEATURED_MODELS = {
recommended: [
{
id: "anthropic/claude-opus-4.6",
name: "Claude Opus 4.6",
description: "State-of-the-art for complex coding",
labels: ["BEST"],
},
{
id: "openai/gpt-5.2-codex",
name: "GPT 5.2 Codex",
description: "OpenAI's latest with strong coding abilities",
labels: ["NEW"],
},
{
id: "google/gemini-3-pro-preview",
name: "Gemini 3 Pro",
description: "1M context window for large codebases",
labels: ["TRENDING"],
},
] as FeaturedModel[],
free: [
{
id: "minimax/minimax-m2.1",
name: "MiniMax M2.1",
description: "Exceptional Multi-Programming Language Capabilities",
labels: ["FREE"],
},
{
id: "moonshotai/kimi-k2.5",
name: "Kimi K2.5",
description: "State-of-the-art model topping benchmarks",
labels: ["FREE"],
},
{
id: "kwaipilot/kat-coder-pro",
name: "KAT Coder Pro",
description: "Advanced agentic coding model",
labels: ["FREE"],
},
{
id: "arcee-ai/trinity-large-preview:free",
name: "Trinity Large Preview",
description: "US built open source coding model",
labels: ["FREE"],
},
] as FeaturedModel[],
type RecommendedModelLike = {
id: string
name: string
description: string
tags: string[]
}
export function getAllFeaturedModels(): FeaturedModel[] {
return [...FEATURED_MODELS.recommended, ...FEATURED_MODELS.free]
export interface FeaturedModelsByTier {
recommended: FeaturedModel[]
free: FeaturedModel[]
}
interface RecommendedModelsByTier {
recommended: RecommendedModelLike[]
free: RecommendedModelLike[]
}
function toFeaturedModel(model: RecommendedModelLike): FeaturedModel {
return {
id: model.id,
name: model.name,
description: model.description,
labels: model.tags,
}
}
function getModelIdSuffix(id: string): string {
const lastSlashIndex = id.lastIndexOf("/")
return lastSlashIndex >= 0 ? id.slice(lastSlashIndex + 1) : id
}
function findFallbackFeaturedModelById(models: FeaturedModel[], id: string): FeaturedModel | undefined {
const idSuffix = getModelIdSuffix(id)
return models.find((model) => model.id === id || getModelIdSuffix(model.id) === idSuffix)
}
function mapRecommendedModelToFeaturedModelWithFallback(
model: RecommendedModelLike,
fallbackModels: FeaturedModel[],
defaultLabels: string[] = [],
): FeaturedModel {
const fallbackModel = findFallbackFeaturedModelById(fallbackModels, model.id)
const upstreamNameLooksLikeFallback = model.name === model.id || model.name.trim().length === 0
const name = upstreamNameLooksLikeFallback ? (fallbackModel?.name ?? model.name) : model.name
const description = model.description.trim().length > 0 ? model.description : (fallbackModel?.description ?? "")
const labels = model.tags.length > 0 ? model.tags : (fallbackModel?.labels ?? defaultLabels)
return {
id: model.id,
name,
description,
labels,
}
}
export const FEATURED_MODELS: FeaturedModelsByTier = {
recommended: CLINE_RECOMMENDED_MODELS_FALLBACK.recommended.map(toFeaturedModel),
free: CLINE_RECOMMENDED_MODELS_FALLBACK.free.map(toFeaturedModel),
}
export function getAllFeaturedModels(modelsByTier: FeaturedModelsByTier = FEATURED_MODELS): FeaturedModel[] {
return [...modelsByTier.recommended, ...modelsByTier.free]
}
export function mapRecommendedModelsToFeaturedModels(data: RecommendedModelsByTier): FeaturedModelsByTier {
return {
recommended: data.recommended.map((model) =>
mapRecommendedModelToFeaturedModelWithFallback(model, FEATURED_MODELS.recommended),
),
free: data.free.map((model) => mapRecommendedModelToFeaturedModelWithFallback(model, FEATURED_MODELS.free, ["FREE"])),
}
}
export function withFeaturedModelFallback(modelsByTier: FeaturedModelsByTier): FeaturedModelsByTier {
const recommended = modelsByTier.recommended.length > 0 ? modelsByTier.recommended : FEATURED_MODELS.recommended
const free = modelsByTier.free.length > 0 ? modelsByTier.free : FEATURED_MODELS.free
return { recommended, free }
}
+71
View File
@@ -0,0 +1,71 @@
/**
* Cline Library Exports
*
* This file exports the public API for programmatic use of Cline.
* Use these classes and types to embed Cline into your applications.
*
* @example
* ```typescript
* import { ClineAgent } from "cline"
*
* const agent = new ClineAgent()
* await agent.initialize({ clientCapabilities: {} })
* const session = await agent.newSession({ cwd: process.cwd() })
* ```
* @module cline
*/
export { ClineAgent } from "./agent/ClineAgent.js"
export { ClineSessionEmitter } from "./agent/ClineSessionEmitter.js"
export type {
AcpAgentOptions,
AcpSessionState,
AcpSessionStatus,
Agent,
AgentSideConnection,
AudioContent,
CancelNotification,
ClientCapabilities,
ClineAcpSession,
ClineAgentCapabilities,
ClineAgentInfo,
ClineAgentOptions,
ClinePermissionOption,
ClineSessionEvents,
ContentBlock,
ImageContent,
InitializeRequest,
InitializeResponse,
LoadSessionRequest,
LoadSessionResponse,
McpServer,
ModelInfo,
NewSessionRequest,
NewSessionResponse,
PermissionHandler,
PermissionOption,
PermissionOptionKind,
PromptRequest,
PromptResponse,
RequestPermissionRequest,
RequestPermissionResponse,
SessionConfigOption,
SessionModelState,
SessionNotification,
SessionUpdate,
SessionUpdatePayload,
SessionUpdateType,
SetSessionConfigOptionRequest,
SetSessionConfigOptionResponse,
SetSessionModelRequest,
SetSessionModelResponse,
SetSessionModeRequest,
SetSessionModeResponse,
StopReason,
TextContent,
ToolCall,
ToolCallStatus,
ToolCallUpdate,
ToolKind,
TranslatedMessage,
} from "./agent/public-types.js"
+34
View File
@@ -0,0 +1,34 @@
import { useEffect, useState } from "react"
import { refreshClineRecommendedModels } from "@/core/controller/models/refreshClineRecommendedModels"
import {
type FeaturedModel,
getAllFeaturedModels,
mapRecommendedModelsToFeaturedModels,
withFeaturedModelFallback,
} from "../constants/featured-models"
export function useClineFeaturedModels(): FeaturedModel[] {
const [featuredModels, setFeaturedModels] = useState<FeaturedModel[]>(() => getAllFeaturedModels())
useEffect(() => {
let cancelled = false
void (async () => {
try {
const recommendedModels = await refreshClineRecommendedModels()
const mappedModels = mapRecommendedModelsToFeaturedModels(recommendedModels)
const modelsWithFallback = withFeaturedModelFallback(mappedModels)
if (!cancelled) {
setFeaturedModels(getAllFeaturedModels(modelsWithFallback))
}
} catch {
// Keep local fallback models on error.
}
})()
return () => {
cancelled = true
}
}, [])
return featuredModels
}
+13 -1
View File
@@ -6,6 +6,7 @@
* - Ctrl+A/E: start/end of line
* - Ctrl+W: delete word backwards
* - Ctrl+U: delete to start of line
* - Ctrl+K: delete to end of line
*
* Note: Home/End keys are handled by useHomeEndKeys hook because Ink doesn't
* expose them in useInput (it sets input='' for these keys).
@@ -152,6 +153,14 @@ export function useTextInput(): UseTextInputReturn {
}
}, [])
const deleteToEnd = useCallback(() => {
const pos = cursorRef.current
if (pos < textRef.current.length) {
setTextState((prev) => prev.slice(0, pos))
// Cursor stays at same position (now at end of text)
}
}, [])
// Cursor movement (internal, used by handlers)
const moveToStart = useCallback(() => setCursorPosState(0), [])
const moveToEnd = useCallback(() => setCursorPosState(textRef.current.length), [])
@@ -190,6 +199,9 @@ export function useTextInput(): UseTextInputReturn {
case "u": // Ctrl+U - delete to start
deleteToStart()
return true
case "k": // Ctrl+K - delete to end
deleteToEnd()
return true
case "w": // Ctrl+W - delete word backwards
deleteWordBefore()
return true
@@ -197,7 +209,7 @@ export function useTextInput(): UseTextInputReturn {
return false
}
},
[moveToStart, moveToEnd, deleteToStart, deleteWordBefore],
[moveToStart, moveToEnd, deleteToStart, deleteToEnd, deleteWordBefore],
)
return {
+42 -2
View File
@@ -30,7 +30,9 @@ describe("CLI Commands", () => {
.option("-v, --verbose", "Show verbose output")
.option("-c, --cwd <path>", "Working directory")
.option("--config <path>", "Configuration directory")
.option("--thinking", "Enable extended thinking")
.option("--thinking [tokens]", "Enable extended thinking")
.option("--reasoning-effort <effort>", "Reasoning effort")
.option("--max-consecutive-mistakes <count>", "Maximum consecutive mistakes")
.action(() => {})
program
@@ -67,7 +69,9 @@ describe("CLI Commands", () => {
.option("-v, --verbose", "Verbose output")
.option("-c, --cwd <path>", "Working directory")
.option("--config <path>", "Configuration directory")
.option("--thinking", "Enable extended thinking")
.option("--thinking [tokens]", "Enable extended thinking")
.option("--reasoning-effort <effort>", "Reasoning effort")
.option("--max-consecutive-mistakes <count>", "Maximum consecutive mistakes")
.action(() => {})
})
@@ -146,6 +150,27 @@ describe("CLI Commands", () => {
expect(taskCmd.opts().thinking).toBe(true)
})
it("should parse --thinking with token budget", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const args = ["test prompt", "--thinking", "8000"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().thinking).toBe("8000")
})
it("should parse --reasoning-effort option", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const args = ["test prompt", "--reasoning-effort", "high"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().reasoningEffort).toBe("high")
})
it("should parse --max-consecutive-mistakes option", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const args = ["test prompt", "--max-consecutive-mistakes", "999"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().maxConsecutiveMistakes).toBe("999")
})
it("should parse short flags", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const args = ["test prompt", "-a", "-v", "-m", "gpt-4"]
@@ -281,6 +306,21 @@ describe("CLI Commands", () => {
program.parse(["node", "cli", "--thinking"])
expect(program.opts().thinking).toBe(true)
})
it("should parse --thinking with token budget", () => {
program.parse(["node", "cli", "--thinking", "4096"])
expect(program.opts().thinking).toBe("4096")
})
it("should parse --reasoning-effort option", () => {
program.parse(["node", "cli", "--reasoning-effort", "medium"])
expect(program.opts().reasoningEffort).toBe("medium")
})
it("should parse --max-consecutive-mistakes option", () => {
program.parse(["node", "cli", "--max-consecutive-mistakes", "7"])
expect(program.opts().maxConsecutiveMistakes).toBe("7")
})
})
describe("command structure", () => {
+176 -152
View File
@@ -8,21 +8,20 @@ import { Command } from "commander"
import { render } from "ink"
import React from "react"
import { ClineEndpoint } from "@/config"
import { Controller } from "@/core/controller"
import type { Controller } from "@/core/controller"
import { StateManager } from "@/core/storage/StateManager"
import { AuthHandler } from "@/hosts/external/AuthHandler"
import { HostProvider } from "@/hosts/host-provider"
import { FileEditProvider } from "@/integrations/editor/FileEditProvider"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { StandaloneTerminalManager } from "@/integrations/terminal/standalone/StandaloneTerminalManager"
import { BannerService } from "@/services/banner/BannerService"
import { ErrorService } from "@/services/error/ErrorService"
import { initializeDistinctId } from "@/services/logging/distinctId"
import { telemetryService } from "@/services/telemetry"
import { PostHogClientProvider } from "@/services/telemetry/providers/posthog/PostHogClientProvider"
import { HistoryItem } from "@/shared/HistoryItem"
import { Logger } from "@/shared/services/Logger"
import { Session } from "@/shared/services/Session"
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@/shared/storage"
import { getProviderModelIdKey } from "@/shared/storage"
import { isOpenaiReasoningEffort, OPENAI_REASONING_EFFORT_OPTIONS, type OpenaiReasoningEffort } from "@/shared/storage/types"
import { version as CLI_VERSION } from "../package.json"
import { runAcpMode } from "./acp/index.js"
import { App } from "./components/App"
@@ -30,19 +29,24 @@ import { checkRawModeSupport } from "./context/StdinContext"
import { createCliHostBridgeProvider } from "./controllers"
import { CliCommentReviewController } from "./controllers/CliCommentReviewController"
import { CliWebviewProvider } from "./controllers/CliWebviewProvider"
import { restoreConsole } from "./utils/console"
import { isAuthConfigured } from "./utils/auth"
import { restoreConsole, suppressConsoleUnlessVerbose } from "./utils/console"
import { printInfo, printWarning } from "./utils/display"
import { selectOutputMode } from "./utils/mode-selection"
import { parseImagesFromInput, processImagePaths } from "./utils/parser"
import { CLINE_CLI_DIR, getCliBinaryPath } from "./utils/path"
import { readStdinIfPiped } from "./utils/piped"
import { runPlainTextTask } from "./utils/plain-text-task"
import { applyProviderConfig } from "./utils/provider-config"
import { selectOutputMode } from "./utils/mode-selection"
import { getValidCliProviders, isValidCliProvider } from "./utils/providers"
import { autoUpdateOnStartup, checkForUpdates } from "./utils/update"
import { initializeCliContext } from "./vscode-context"
import { CLI_LOG_FILE, shutdownEvent, window } from "./vscode-shim"
// CLI-only behavior: suppress console output unless verbose mode is enabled.
// Kept explicit here so importing the library bundle does not mutate global console methods.
suppressConsoleUnlessVerbose()
/**
* Common options shared between runTask and resumeTask
*/
@@ -53,13 +57,75 @@ interface TaskOptions {
verbose?: boolean
cwd?: string
config?: string
thinking?: boolean
thinking?: boolean | string
reasoningEffort?: string
maxConsecutiveMistakes?: string
yolo?: boolean
doubleCheckCompletion?: boolean
timeout?: string
json?: boolean
stdinWasPiped?: boolean
}
let telemetryDisposed = false
async function disposeTelemetryServices(): Promise<void> {
if (telemetryDisposed) {
return
}
telemetryDisposed = true
await Promise.allSettled([telemetryService.dispose(), PostHogClientProvider.getInstance().dispose()])
}
async function disposeCliContext(ctx: CliContext): Promise<void> {
await ctx.controller.stateManager.flushPendingState()
await ctx.controller.dispose()
await ErrorService.get().dispose()
await disposeTelemetryServices()
}
function setModeScopedState(currentMode: "act" | "plan", setter: (mode: "act" | "plan") => void): void {
const stateManager = StateManager.get()
setter(currentMode)
const separateModels = stateManager.getGlobalSettingsKey("planActSeparateModelsSetting") ?? false
if (!separateModels) {
const otherMode: "act" | "plan" = currentMode === "act" ? "plan" : "act"
setter(otherMode)
}
}
function normalizeReasoningEffort(value?: string): OpenaiReasoningEffort | undefined {
if (value === undefined) {
return undefined
}
const normalized = value.toLowerCase()
if (isOpenaiReasoningEffort(normalized)) {
return normalized
}
printWarning(
`Invalid --reasoning-effort '${value}'. Using 'medium'. Valid values: ${OPENAI_REASONING_EFFORT_OPTIONS.join(", ")}.`,
)
return "medium"
}
function normalizeMaxConsecutiveMistakes(value?: string): number | undefined {
if (value === undefined) {
return undefined
}
const parsed = Number.parseInt(value, 10)
if (Number.isNaN(parsed) || parsed < 1) {
printWarning(`Invalid --max-consecutive-mistakes value '${value}'. Expected integer >= 1.`)
return undefined
}
return parsed
}
/**
* Apply task-related options (mode, model, thinking, yolo) to StateManager.
* Shared between runTask and resumeTask to avoid duplication.
@@ -86,20 +152,57 @@ function applyTaskOptions(options: TaskOptions): void {
telemetryService.captureHostEvent("model_flag", options.model)
}
// Set thinking budget based on --thinking flag
const thinkingBudget = options.thinking ? 1024 : 0
const currentMode = StateManager.get().getGlobalSettingsKey("mode") || "act"
const thinkingKey = currentMode === "act" ? "actModeThinkingBudgetTokens" : "planModeThinkingBudgetTokens"
StateManager.get().setGlobalState(thinkingKey, thinkingBudget)
// Set thinking budget based on --thinking flag (boolean or number)
let thinkingBudget = 0
if (options.thinking) {
if (typeof options.thinking === "string") {
const parsed = Number.parseInt(options.thinking, 10)
if (Number.isNaN(parsed) || parsed < 0) {
printWarning(`Invalid --thinking value '${options.thinking}'. Using default 1024.`)
thinkingBudget = 1024
} else {
thinkingBudget = parsed
}
} else {
thinkingBudget = 1024
}
}
const currentMode = (StateManager.get().getGlobalSettingsKey("mode") || "act") as "act" | "plan"
setModeScopedState(currentMode, (mode) => {
const thinkingKey = mode === "act" ? "actModeThinkingBudgetTokens" : "planModeThinkingBudgetTokens"
StateManager.get().setGlobalState(thinkingKey, thinkingBudget)
})
if (options.thinking) {
telemetryService.captureHostEvent("thinking_flag", "true")
}
// Set yolo mode based on --yolo flag
const reasoningEffort = normalizeReasoningEffort(options.reasoningEffort)
if (reasoningEffort !== undefined) {
setModeScopedState(currentMode, (mode) => {
const reasoningKey = mode === "act" ? "actModeReasoningEffort" : "planModeReasoningEffort"
StateManager.get().setGlobalState(reasoningKey, reasoningEffort)
})
telemetryService.captureHostEvent("reasoning_effort_flag", reasoningEffort)
}
const maxConsecutiveMistakes = normalizeMaxConsecutiveMistakes(options.maxConsecutiveMistakes)
if (maxConsecutiveMistakes !== undefined) {
StateManager.get().setGlobalState("maxConsecutiveMistakes", maxConsecutiveMistakes)
telemetryService.captureHostEvent("max_consecutive_mistakes_flag", String(maxConsecutiveMistakes))
}
// Set yolo mode as a session-scoped override so AutoApprove picks it up,
// but it is never persisted to disk (setSessionOverride never touches pendingGlobalState).
if (options.yolo) {
StateManager.get().setGlobalState("yoloModeToggled", true)
StateManager.get().setSessionOverride("yoloModeToggled", true)
telemetryService.captureHostEvent("yolo_flag", "true")
}
// Set double-check completion based on flag
if (options.doubleCheckCompletion) {
StateManager.get().setGlobalState("doubleCheckCompletionEnabled", true)
telemetryService.captureHostEvent("double_check_completion_flag", "true")
}
}
/**
@@ -151,9 +254,7 @@ async function runTaskInPlainTextMode(
const hasAuth = await isAuthConfigured()
if (!hasAuth) {
printWarning("Not authenticated. Please run 'cline auth' first to configure your API credentials.")
await ctx.controller.stateManager.flushPendingState()
await ctx.controller.dispose()
await ErrorService.get().dispose()
await disposeCliContext(ctx)
exit(1)
}
@@ -168,13 +269,11 @@ async function runTaskInPlainTextMode(
imageDataUrls: taskConfig.imageDataUrls,
verbose: options.verbose,
jsonOutput: options.json,
timeoutSeconds: options.timeout ? parseInt(options.timeout, 10) : undefined,
timeoutSeconds: options.timeout ? Number.parseInt(options.timeout, 10) : undefined,
})
// Cleanup
await ctx.controller.stateManager.flushPendingState()
await ctx.controller.dispose()
await ErrorService.get().dispose()
await disposeCliContext(ctx)
// Ensure stdout is fully drained before exiting - critical for piping
await drainStdout()
@@ -186,9 +285,7 @@ async function runTaskInPlainTextMode(
*/
function createInkCleanup(ctx: CliContext, onTaskError?: () => boolean): () => Promise<void> {
return async () => {
await ctx.controller.stateManager.flushPendingState()
await ctx.controller.dispose()
await ErrorService.get().dispose()
await disposeCliContext(ctx)
if (onTaskError?.()) {
printWarning("Task ended with errors.")
exit(1)
@@ -248,10 +345,17 @@ function setupSignalHandlers() {
if (task) {
task.abortTask()
}
await activeContext.controller.stateManager.flushPendingState()
await activeContext.controller.dispose()
await disposeCliContext(activeContext)
} else {
// Best-effort flush of restored yolo state when no active context
try {
await StateManager.get().flushPendingState()
} catch {
// StateManager may not be initialized yet
}
await ErrorService.get().dispose()
await disposeTelemetryServices()
}
await ErrorService.get().dispose()
} catch {
// Best effort cleanup
}
@@ -299,13 +403,22 @@ interface InitOptions {
*/
async function initializeCli(options: InitOptions): Promise<CliContext> {
const workspacePath = options.cwd || process.cwd()
const { extensionContext, DATA_DIR, EXTENSION_DIR } = initializeCliContext({
const { extensionContext, storageContext, DATA_DIR, EXTENSION_DIR } = initializeCliContext({
clineDir: options.config,
workspaceDir: workspacePath,
})
await ClineEndpoint.initialize()
await initializeDistinctId(extensionContext)
// Set up output channel and Logger early so ClineEndpoint.initialize logs are captured
const outputChannel = window.createOutputChannel("Cline CLI")
const logToChannel = (message: string) => outputChannel.appendLine(message)
// Configure the shared Logging class early to capture all initialization logs
Logger.subscribe(logToChannel)
await ClineEndpoint.initialize(EXTENSION_DIR)
// Auto-update check (after endpoints initialized, so we can detect bundled configs)
autoUpdateOnStartup(CLI_VERSION)
// Initialize/reset session tracking for this CLI run
Session.reset()
@@ -314,11 +427,9 @@ async function initializeCli(options: InitOptions): Promise<CliContext> {
AuthHandler.getInstance().setEnabled(true)
}
const outputChannel = window.createOutputChannel("Cline CLI")
outputChannel.appendLine(
`Cline CLI initialized. Data dir: ${DATA_DIR}, Extension dir: ${EXTENSION_DIR}, Log dir: ${CLINE_CLI_DIR.log}`,
)
const logToChannel = (message: string) => outputChannel.appendLine(message)
HostProvider.initialize(
() => new CliWebviewProvider(extensionContext as any),
@@ -327,28 +438,20 @@ async function initializeCli(options: InitOptions): Promise<CliContext> {
() => new StandaloneTerminalManager(),
createCliHostBridgeProvider(workspacePath),
logToChannel,
async () => (options.enableAuth ? AuthHandler.getInstance().getCallbackUrl() : ""),
async (path: string) => (options.enableAuth ? AuthHandler.getInstance().getCallbackUrl(path) : ""),
getCliBinaryPath,
EXTENSION_DIR,
DATA_DIR,
)
await StateManager.initialize(extensionContext as any)
await StateManager.initialize(storageContext)
await ErrorService.initialize()
// Initialize OpenAI Codex OAuth manager with extension context for secrets storage
openAiCodexOAuthManager.initialize(extensionContext)
// Configure the shared Logging class to use HostProvider's output channel
Logger.subscribe((msg: string) => HostProvider.get().logToChannel(msg))
const webview = HostProvider.get().createWebviewProvider() as CliWebviewProvider
const controller = webview.controller
BannerService.initialize(webview.controller)
telemetryService.captureExtensionActivated()
telemetryService.captureHostEvent("cline_cli", "initialized")
await telemetryService.captureExtensionActivated()
await telemetryService.captureHostEvent("cline_cli", "initialized")
const ctx = { extensionContext, dataDir: DATA_DIR, extensionDir: EXTENSION_DIR, workspacePath, controller }
activeContext = ctx
@@ -430,8 +533,7 @@ async function runTask(prompt: string, options: TaskOptions & { images?: string[
taskError = true
},
onWelcomeExit: () => {
// User pressed Esc
exit(0)
// User pressed Esc; Ink exits and cleanup handles process exit.
},
}),
createInkCleanup(ctx, () => taskError),
@@ -447,8 +549,8 @@ async function listHistory(options: { config?: string; limit?: number; page?: nu
const taskHistory = StateManager.get().getGlobalStateKey("taskHistory") || []
// Sort by timestamp (newest first) before pagination
const sortedHistory = [...taskHistory].sort((a: any, b: any) => (b.ts || 0) - (a.ts || 0))
const limit = typeof options.limit === "string" ? parseInt(options.limit, 10) : options.limit || 10
const initialPage = typeof options.page === "string" ? parseInt(options.page, 10) : options.page || 1
const limit = typeof options.limit === "string" ? Number.parseInt(options.limit, 10) : options.limit || 10
const initialPage = typeof options.page === "string" ? Number.parseInt(options.page, 10) : options.page || 1
const totalCount = sortedHistory.length
const totalPages = Math.ceil(totalCount / limit)
@@ -456,9 +558,7 @@ async function listHistory(options: { config?: string; limit?: number; page?: nu
if (sortedHistory.length === 0) {
printInfo("No task history found.")
await ctx.controller.stateManager.flushPendingState()
await ctx.controller.dispose()
await ErrorService.get().dispose()
await disposeCliContext(ctx)
exit(0)
}
@@ -472,9 +572,7 @@ async function listHistory(options: { config?: string; limit?: number; page?: nu
isRawModeSupported: checkRawModeSupport(),
}),
async () => {
await ctx.controller.stateManager.flushPendingState()
await ctx.controller.dispose()
await ErrorService.get().dispose()
await disposeCliContext(ctx)
exit(0)
},
)
@@ -503,9 +601,7 @@ async function showConfig(options: { config?: string }) {
isRawModeSupported: checkRawModeSupport(),
}),
async () => {
await ctx.controller.stateManager.flushPendingState()
await ctx.controller.dispose()
await ErrorService.get().dispose()
await disposeCliContext(ctx)
exit(0)
},
)
@@ -581,17 +677,15 @@ async function runAuth(options: {
baseurl: options.baseurl,
})
await ctx.controller.stateManager.flushPendingState()
await ctx.controller.dispose()
await ErrorService.get().dispose()
if (!result.success) {
printWarning(result.error || "Quick setup failed")
telemetryService.captureHostEvent("auth", "error")
await telemetryService.captureHostEvent("auth", "error")
await disposeCliContext(ctx)
exit(1)
}
telemetryService.captureHostEvent("auth", "completed")
await telemetryService.captureHostEvent("auth", "completed")
await disposeCliContext(ctx)
exit(0)
}
@@ -605,7 +699,6 @@ async function runAuth(options: {
isRawModeSupported: checkRawModeSupport(),
onComplete: () => {
telemetryService.captureHostEvent("auth", "completed")
exit(0)
},
onError: () => {
telemetryService.captureHostEvent("auth", "error")
@@ -613,16 +706,10 @@ async function runAuth(options: {
},
}),
async () => {
await ctx.controller.stateManager.flushPendingState()
await ctx.controller.dispose()
await ErrorService.get().dispose()
exit(0)
await disposeCliContext(ctx)
exit(authError ? 1 : 0)
},
)
if (authError) {
process.exit(1)
}
}
// Setup CLI commands
@@ -641,13 +728,16 @@ program
.option("-a, --act", "Run in act mode")
.option("-p, --plan", "Run in plan mode")
.option("-y, --yolo", "Enable yes/yolo mode (auto-approve actions)")
.option("-t, --timeout <seconds>", "Timeout in seconds for yes/yolo mode (default: 600)")
.option("-t, --timeout <seconds>", "Optional timeout in seconds (applies only when provided)")
.option("-m, --model <model>", "Model to use for the task")
.option("-v, --verbose", "Show verbose output")
.option("-c, --cwd <path>", "Working directory for the task")
.option("--config <path>", "Path to Cline configuration directory")
.option("--thinking", "Enable extended thinking (1024 token budget)")
.option("--thinking [tokens]", "Enable extended thinking (default: 1024 tokens)")
.option("--reasoning-effort <effort>", "Reasoning effort: none|low|medium|high|xhigh")
.option("--max-consecutive-mistakes <count>", "Maximum consecutive mistakes before halting in yolo mode")
.option("--json", "Output messages as JSON instead of styled text")
.option("--double-check-completion", "Reject first completion attempt to force re-verification")
.option("-T, --taskId <id>", "Resume an existing task by ID")
.action((prompt, options) => {
if (options.taskId) {
@@ -674,9 +764,9 @@ program
program
.command("auth")
.description("Authenticate a provider and configure what model is used")
.option("-p, --provider <id>", "Provider ID for quick setup (e.g., openai-native, anthropic)")
.option("-p, --provider <id>", "Provider ID for quick setup (e.g., openai-native, anthropic, moonshot)")
.option("-k, --apikey <key>", "API key for the provider")
.option("-m, --modelid <id>", "Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929)")
.option("-m, --modelid <id>", "Model ID to configure (e.g., gpt-4o, claude-sonnet-4-6, kimi-k2.5)")
.option("-b, --baseurl <url>", "Base URL (optional, only for openai provider)")
.option("-v, --verbose", "Show verbose output")
.option("-c, --cwd <path>", "Working directory for the task")
@@ -705,68 +795,6 @@ devCommand
await openExternal(CLI_LOG_FILE)
})
/**
* Check if the user has completed onboarding (has any provider configured).
*
* Uses `welcomeViewCompleted` as the single source of truth, matching the VS Code extension's approach.
* If `welcomeViewCompleted` is undefined (first run), checks if ANY provider has credentials
* and sets the flag accordingly.
*/
async function isAuthConfigured(): Promise<boolean> {
const stateManager = StateManager.get()
// Check welcomeViewCompleted first - this is the single source of truth
const welcomeViewCompleted = stateManager.getGlobalStateKey("welcomeViewCompleted")
if (welcomeViewCompleted !== undefined) {
return welcomeViewCompleted
}
// welcomeViewCompleted is undefined - run migration logic to check if ANY provider has credentials
// This mirrors the extension's migrateWelcomeViewCompleted behavior
const hasAnyAuth = await checkAnyProviderConfigured()
// Set welcomeViewCompleted based on what we found
stateManager.setGlobalState("welcomeViewCompleted", hasAnyAuth)
await stateManager.flushPendingState()
return hasAnyAuth
}
/**
* Check if ANY provider has valid credentials configured.
* Used for migration when welcomeViewCompleted is undefined.
*/
async function checkAnyProviderConfigured(): Promise<boolean> {
const stateManager = StateManager.get()
const config = stateManager.getApiConfiguration() as Record<string, unknown>
// Check Cline account (stored as "cline:clineAccountId" in secrets, loaded into config)
if (config["clineApiKey"] || config["cline:clineAccountId"]) return true
// Check OpenAI Codex OAuth (stored in SECRETS_KEYS, loaded into config)
if (config["openai-codex-oauth-credentials"]) return true
// Check all BYO provider API keys (loaded into config from secrets)
for (const [provider, keyField] of Object.entries(ProviderToApiKeyMap)) {
// Skip cline - already checked above with the correct key
if (provider === "cline") continue
const fields = Array.isArray(keyField) ? keyField : [keyField]
for (const field of fields) {
if (config[field]) return true
}
}
// Check provider-specific settings that indicate configuration
// (for providers that don't require API keys like Bedrock with IAM, Ollama, LM Studio)
if (config.awsRegion) return true
if (config.vertexProjectId) return true
if (config.ollamaBaseUrl) return true
if (config.lmStudioBaseUrl) return true
return false
}
/**
* Validate that a task exists in history
* @returns The task history item if found, null otherwise
@@ -788,9 +816,7 @@ async function resumeTask(taskId: string, options: TaskOptions & { initialPrompt
if (!historyItem) {
printWarning(`Task not found: ${taskId}`)
printInfo("Use 'cline history' to see available tasks.")
await ctx.controller.stateManager.flushPendingState()
await ctx.controller.dispose()
await ErrorService.get().dispose()
await disposeCliContext(ctx)
exit(1)
}
@@ -823,7 +849,7 @@ async function resumeTask(taskId: string, options: TaskOptions & { initialPrompt
taskError = true
},
onWelcomeExit: () => {
exit(0)
// User pressed Esc; Ink exits and cleanup handles process exit.
},
}),
createInkCleanup(ctx, () => taskError),
@@ -850,16 +876,14 @@ async function showWelcome(options: { verbose?: boolean; cwd?: string; config?:
controller: ctx.controller,
isRawModeSupported: checkRawModeSupport(),
onWelcomeExit: () => {
exit(0)
// User pressed Esc; Ink exits and cleanup handles process exit.
},
onError: () => {
hadError = true
},
}),
async () => {
await ctx.controller.stateManager.flushPendingState()
await ctx.controller.dispose()
await ErrorService.get().dispose()
await disposeCliContext(ctx)
exit(hadError ? 1 : 0)
},
)
@@ -871,13 +895,16 @@ program
.option("-a, --act", "Run in act mode")
.option("-p, --plan", "Run in plan mode")
.option("-y, --yolo", "Enable yolo mode (auto-approve actions)")
.option("-t, --timeout <seconds>", "Timeout in seconds for yolo mode (default: 600)")
.option("-t, --timeout <seconds>", "Optional timeout in seconds (applies only when provided)")
.option("-m, --model <model>", "Model to use for the task")
.option("-v, --verbose", "Show verbose output")
.option("-c, --cwd <path>", "Working directory")
.option("--config <path>", "Configuration directory")
.option("--thinking", "Enable extended thinking (1024 token budget)")
.option("--thinking [tokens]", "Enable extended thinking (default: 1024 tokens)")
.option("--reasoning-effort <effort>", "Reasoning effort: none|low|medium|high|xhigh")
.option("--max-consecutive-mistakes <count>", "Maximum consecutive mistakes before halting in yolo mode")
.option("--json", "Output messages as JSON instead of styled text")
.option("--double-check-completion", "Reject first completion attempt to force re-verification")
.option("--acp", "Run in ACP (Agent Client Protocol) mode for editor integration")
.option("-T, --taskId <id>", "Resume an existing task by ID")
.action(async (prompt, options) => {
@@ -947,8 +974,5 @@ program
}
})
// Background auto-update check (non-blocking)
autoUpdateOnStartup(CLI_VERSION)
// Parse and run
program.parse()
+9
View File
@@ -0,0 +1,9 @@
import { describe, expect, it } from "vitest"
describe("library import side effects", () => {
it("importing library exports must not mutate console.log", async () => {
const originalConsoleLog = console.log
await import("./exports")
expect(console.log).toBe(originalConsoleLog)
}, 10000)
})
+64
View File
@@ -0,0 +1,64 @@
import { StateManager } from "@/core/storage/StateManager"
import { ProviderToApiKeyMap } from "@/shared/storage"
/**
* Check if the user has completed onboarding (has any provider configured).
*
* Uses `welcomeViewCompleted` as the single source of truth, matching the VS Code extension's approach.
* If `welcomeViewCompleted` is undefined (first run), checks if ANY provider has credentials
* and sets the flag accordingly.
*/
export async function isAuthConfigured(): Promise<boolean> {
const stateManager = StateManager.get()
// Check welcomeViewCompleted first - this is the single source of truth
const welcomeViewCompleted = stateManager.getGlobalStateKey("welcomeViewCompleted")
if (welcomeViewCompleted !== undefined) {
return welcomeViewCompleted
}
// welcomeViewCompleted is undefined - run migration logic to check if ANY provider has credentials
// This mirrors the extension's migrateWelcomeViewCompleted behavior
const hasAnyAuth = await checkAnyProviderConfigured()
// Set welcomeViewCompleted based on what we found
stateManager.setGlobalState("welcomeViewCompleted", hasAnyAuth)
await stateManager.flushPendingState()
return hasAnyAuth
}
/**
* Check if ANY provider has valid credentials configured.
* Used for migration when welcomeViewCompleted is undefined.
*/
export async function checkAnyProviderConfigured(): Promise<boolean> {
const stateManager = StateManager.get()
const config = stateManager.getApiConfiguration() as Record<string, unknown>
// Check Cline account (stored as "cline:clineAccountId" in secrets, loaded into config)
if (config["clineApiKey"] || config["cline:clineAccountId"]) return true
// Check OpenAI Codex OAuth (stored in SECRETS_KEYS, loaded into config)
if (config["openai-codex-oauth-credentials"]) return true
// Check all BYO provider API keys (loaded into config from secrets)
for (const [provider, keyField] of Object.entries(ProviderToApiKeyMap)) {
// Skip cline - already checked above with the correct key
if (provider === "cline") continue
const fields = Array.isArray(keyField) ? keyField : [keyField]
for (const field of fields) {
if (config[field]) return true
}
}
// Check provider-specific settings that indicate configuration
// (for providers that don't require API keys like Bedrock with IAM, Ollama, LM Studio)
if (config.awsRegion) return true
if (config.vertexProjectId) return true
if (config.ollamaBaseUrl) return true
if (config.lmStudioBaseUrl) return true
return false
}
+12 -4
View File
@@ -12,11 +12,19 @@ export const originalConsoleWarn = console.warn.bind(console)
export const originalConsoleInfo = console.info.bind(console)
export const originalConsoleDebug = console.debug.bind(console)
// Check for verbose flag early (before commander parses)
const isVerbose = process.argv.includes("-v") || process.argv.includes("--verbose")
/**
* Suppress console output unless verbose mode is enabled.
*
* This is intentionally opt-in and should only be called by the CLI entrypoint.
* Library consumers should not have their global console methods mutated as a
* side effect of importing the library bundle.
*/
export function suppressConsoleUnlessVerbose(argv: string[] = process.argv) {
const isVerbose = argv.includes("-v") || argv.includes("--verbose")
if (isVerbose) {
return
}
// Suppress console output unless verbose mode
if (!isVerbose) {
console.log = () => {}
console.warn = () => {}
console.error = () => {}
+28
View File
@@ -0,0 +1,28 @@
import { afterEach, describe, expect, it, vi } from "vitest"
import { emitTaskStartedMessage } from "./task-start-output"
describe("emitTaskStartedMessage", () => {
afterEach(() => {
vi.restoreAllMocks()
})
it("writes structured task_started JSON to stdout in json mode", () => {
const stdoutWriteSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true)
const stderrWriteSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true)
emitTaskStartedMessage("task-123", true)
expect(stdoutWriteSpy).toHaveBeenCalledWith('{"type":"task_started","taskId":"task-123"}\n')
expect(stderrWriteSpy).not.toHaveBeenCalled()
})
it("writes human-readable task started line to stderr in non-json mode", () => {
const stdoutWriteSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true)
const stderrWriteSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true)
emitTaskStartedMessage("task-456", false)
expect(stderrWriteSpy).toHaveBeenCalledWith("Task started: task-456\n")
expect(stdoutWriteSpy).not.toHaveBeenCalled()
})
})
+27 -5
View File
@@ -17,6 +17,7 @@ import type { Controller } from "@/core/controller"
import { getRequestRegistry } from "@/core/controller/grpc-handler"
import { subscribeToState } from "@/core/controller/state/subscribeToState"
import { showTaskWithId } from "@/core/controller/task/showTaskWithId"
import { emitTaskStartedMessage } from "./task-start-output"
export interface PlainTextTaskOptions {
controller: Controller
@@ -25,7 +26,7 @@ export interface PlainTextTaskOptions {
imageDataUrls?: string[]
verbose?: boolean
jsonOutput?: boolean
/** Timeout in seconds (default: 600 = 10 minutes) */
/** Timeout in seconds (only applied when explicitly provided) */
timeoutSeconds?: number
/** Task ID to resume an existing task */
taskId?: string
@@ -52,6 +53,7 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
})
let hasError = false
let hasEmittedTaskStarted = false
// Track which messages have been processed (by timestamp)
const processedMessages = new Map<number, string>()
@@ -62,6 +64,20 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
// results AFTER this time should trigger task completion.
const completionCutoffTs = Date.now()
const emitTaskStarted = () => {
if (hasEmittedTaskStarted) {
return
}
const taskId = controller.task?.taskId
if (!taskId) {
return
}
emitTaskStartedMessage(taskId, Boolean(jsonOutput))
hasEmittedTaskStarted = true
}
// Helper to process a message and track completion state
const processMessage = (message: ClineMessage) => {
const ts = message.ts || 0
@@ -119,6 +135,7 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
if (options.taskId) {
// Load the existing task
await showTaskWithId(controller, StringRequest.create({ value: options.taskId }))
emitTaskStarted()
// If a prompt was provided, send it as a message to the resumed task
if (prompt && controller.task) {
@@ -131,14 +148,19 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
} else if (prompt) {
// Start a new task with the prompt
await controller.initTask(prompt, imageDataUrls)
emitTaskStarted()
} else {
throw new Error("Either taskId or prompt must be provided")
}
// Normal mode: wait for task completion
const timeoutMs = (options.timeoutSeconds ?? 600) * 1000 // default 10 minutes
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs))
await Promise.race([completionPromise, timeoutPromise])
// Wait for task completion, with optional timeout only when explicitly configured
if (options.timeoutSeconds) {
const timeoutMs = options.timeoutSeconds * 1000
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs))
await Promise.race([completionPromise, timeoutPromise])
} else {
await completionPromise
}
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error)
if (jsonOutput) {
+16 -1
View File
@@ -80,15 +80,18 @@ export async function applyProviderConfig(options: ApplyProviderConfigOptions):
export interface ApplyBedrockConfigOptions {
bedrockConfig: BedrockConfig
modelId?: string
customModelBaseId?: string // Base model ID for custom ARN/Inference Profile (for capability detection)
controller?: Controller
}
/**
* Apply Bedrock provider configuration to state
* Handles AWS-specific fields (authentication, region, credentials)
* When customModelBaseId is provided, sets the custom model flags so the system
* knows to use the ARN as the model ID and the base model for capability detection.
*/
export async function applyBedrockConfig(options: ApplyBedrockConfigOptions): Promise<void> {
const { bedrockConfig, modelId, controller } = options
const { bedrockConfig, modelId, customModelBaseId, controller } = options
const stateManager = StateManager.get()
const config: Record<string, unknown> = {
@@ -108,6 +111,18 @@ export async function applyBedrockConfig(options: ApplyBedrockConfigOptions): Pr
if (planModelKey) config[planModelKey] = finalModelId
}
// Handle custom model (Application Inference Profile ARN)
if (customModelBaseId) {
config.actModeAwsBedrockCustomSelected = true
config.planModeAwsBedrockCustomSelected = true
config.actModeAwsBedrockCustomModelBaseId = customModelBaseId
config.planModeAwsBedrockCustomModelBaseId = customModelBaseId
} else {
// Ensure custom flags are cleared when using a standard model
config.actModeAwsBedrockCustomSelected = false
config.planModeAwsBedrockCustomSelected = false
}
// Add optional AWS credentials
if (bedrockConfig.awsProfile !== undefined) config.awsProfile = bedrockConfig.awsProfile
if (bedrockConfig.awsAccessKey) config.awsAccessKey = bedrockConfig.awsAccessKey
+8
View File
@@ -0,0 +1,8 @@
export function emitTaskStartedMessage(taskId: string, jsonOutput: boolean): void {
if (jsonOutput) {
process.stdout.write(JSON.stringify({ type: "task_started", taskId }) + "\n")
return
}
process.stderr.write(`Task started: ${taskId}\n`)
}
+7 -1
View File
@@ -1,6 +1,7 @@
import { spawn } from "node:child_process"
import { realpathSync } from "node:fs"
import { exit } from "node:process"
import { ClineEndpoint } from "@/config"
import { fetch } from "@/shared/net"
import { printInfo, printWarning } from "./display"
@@ -107,7 +108,7 @@ async function getLatestVersion(currentVersion: string): Promise<string | null>
* process to install if a newer version is available.
*
* Supports npm, pnpm, yarn, and bun global installs.
* Skipped for npx, local dev, and unknown installations.
* Skipped for npx, local dev, unknown installations, and bundled enterprise packages.
* Can be disabled with CLINE_NO_AUTO_UPDATE=1 environment variable.
*/
export function autoUpdateOnStartup(currentVersion: string): void {
@@ -121,6 +122,11 @@ export function autoUpdateOnStartup(currentVersion: string): void {
return
}
// Skip if using bundled enterprise config (single source of truth)
if (ClineEndpoint.isBundledConfig()) {
return
}
const { updateCommand } = getInstallationInfo(currentVersion)
if (!updateCommand) {
return
+62 -93
View File
@@ -1,17 +1,20 @@
/**
* VSCode context stub for CLI mode
* Provides mock implementations of VSCode extension context
* Provides mock implementations of VSCode extension context.
*/
import { mkdirSync } from "node:fs"
import { fileURLToPath } from "node:url"
import os from "os"
import path from "path"
import { ExtensionRegistryInfo } from "@/registry"
import { ClineExtensionContext } from "@/shared/cline"
import { ClineFileStorage } from "@/shared/storage"
import type { ClineMemento } from "@/shared/storage/ClineStorage"
import { createStorageContext, type StorageContext } from "@/shared/storage/storage-context"
import { EnvironmentVariableCollection, ExtensionKind, ExtensionMode, readJson, URI } from "./vscode-shim"
const SETTINGS_SUBFOLDER = "data"
// ES module equivalent of __dirname
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
/**
* CLI-specific state overrides.
@@ -30,33 +33,43 @@ const CLI_STATE_OVERRIDES: Record<string, any> = {
}
/**
* File-based Memento store with optional key overrides.
* Implements VSCode's Memento interface using SyncJsonFileStorage.
* Memento adapter that wraps a ClineFileStorage with optional key overrides.
* Used for globalState where CLI needs to inject hardcoded overrides.
*/
class MementoStore extends ClineFileStorage {
private overrides: Record<string, any>
class MementoAdapter implements ClineMemento {
constructor(
private readonly store: ClineMemento,
private readonly overrides: Record<string, any> = {},
) {}
constructor(filePath: string, overrides: Record<string, any> = {}) {
super(filePath, "MementoStore")
this.overrides = overrides
}
// VSCode Memento interface - override base class get() with overload support
override get<T>(key: string): T | undefined
override get<T>(key: string, defaultValue: T): T
override get<T>(key: string, defaultValue?: T): T | undefined {
get<T>(key: string): T | undefined
get<T>(key: string, defaultValue: T): T
get<T>(key: string, defaultValue?: T): T | undefined {
if (key in this.overrides) {
return this.overrides[key] as T
}
const value = super.get<T>(key)
const value = this.store.get<T>(key)
return value !== undefined ? value : defaultValue
}
override async update(key: string, value: any): Promise<void> {
if (key in this.overrides) {
return
update(key: string, value: any): Thenable<void> {
return this.setBatch({ [key]: value })
}
keys(): readonly string[] {
return this.store.keys()
}
setBatch(entries: Record<string, any>): Thenable<void> {
// Filter out overridden keys and delegate to underlying store
const filteredEntries: Record<string, any> = {}
for (const [key, value] of Object.entries(entries)) {
if (!(key in this.overrides)) {
filteredEntries[key] = value
}
}
this.set(key, value)
this.store.setBatch(filteredEntries)
return Promise.resolve()
}
setKeysForSync(_keys: readonly string[]): void {
@@ -64,84 +77,48 @@ class MementoStore extends ClineFileStorage {
}
}
/**
* File-based secret storage implementing VSCode's SecretStorage interface.
* Uses sync storage internally but exposes async API for VSCode compatibility.
*/
class SecretStore {
private storage: ClineFileStorage<string>
private onDidChangeEmitter = {
event: () => ({ dispose: () => {} }),
fire: (_e: any) => {},
dispose: () => {},
}
onDidChange = this.onDidChangeEmitter.event
constructor(filePath: string) {
this.storage = new ClineFileStorage<string>(filePath, "SecretStore")
}
get(key: string): Promise<string | undefined> {
return Promise.resolve(this.storage.get(key))
}
store(key: string, value: string): Promise<void> {
this.storage.set(key, value)
return Promise.resolve()
}
delete(key: string): Promise<void> {
this.storage.delete(key)
return Promise.resolve()
}
}
export interface CliContextConfig {
clineDir?: string
/** The workspace directory being worked in (for hashing into storage path) */
/** The workspace directory being worked in (used to compute workspace storage hash) */
workspaceDir?: string
}
/**
* Create a short hash of a string for use in directory names
*/
function hashString(str: string): string {
let hash = 0
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i)
hash = (hash << 5) - hash + char
hash = hash & hash // Convert to 32bit integer
}
return Math.abs(hash).toString(16).substring(0, 8)
}
export interface CliContextResult {
extensionContext: ClineExtensionContext
storageContext: StorageContext
DATA_DIR: string
EXTENSION_DIR: string
WORKSPACE_STORAGE_DIR: string
}
/**
* Initialize the VSCode-like context for CLI mode
* Initialize the VSCode-like context for CLI mode.
*
* Creates a shared StorageContext (the single source of truth for all storage)
* and wraps it in a ClineExtensionContext shell for legacy APIs that still
* expect the VSCode ExtensionContext shape.
*/
export function initializeCliContext(config: CliContextConfig = {}): CliContextResult {
const CLINE_DIR = config.clineDir || process.env.CLINE_DIR || path.join(os.homedir(), ".cline")
const DATA_DIR = path.join(CLINE_DIR, SETTINGS_SUBFOLDER)
// Workspace storage should always be under ~/.cline/data/workspaces/<hash>/
// where hash is derived from the workspace path to keep workspaces isolated
const workspacePath = config.workspaceDir || process.cwd()
const workspaceHash = hashString(workspacePath)
const WORKSPACE_STORAGE_DIR = process.env.WORKSPACE_STORAGE_DIR || path.join(DATA_DIR, "workspaces", workspaceHash)
// Create the shared StorageContext — this owns all ClineFileStorage instances.
// CLI, JetBrains, and VSCode all share this same file-backed implementation.
let storageContext = createStorageContext({
clineDir: CLINE_DIR,
workspacePath: config.workspaceDir || process.cwd(),
workspaceStorageDir: process.env.WORKSPACE_STORAGE_DIR || undefined,
})
storageContext = {
...storageContext,
// Storage — delegates to storageContext stores (with CLI overrides for globalState)
globalState: new MementoAdapter(storageContext.globalState, CLI_STATE_OVERRIDES),
}
// Ensure directories exist
mkdirSync(DATA_DIR, { recursive: true })
mkdirSync(WORKSPACE_STORAGE_DIR, { recursive: true })
const DATA_DIR = storageContext.dataDir
const WORKSPACE_STORAGE_DIR = storageContext.workspaceStoragePath
// For CLI, extension dir is the root of the project (parent of cli)
const EXTENSION_DIR = path.resolve(__dirname, "..", "..")
// For CLI, extension dir is the package root (one level up from dist/)
const EXTENSION_DIR = path.resolve(__dirname, "..")
const EXTENSION_MODE = process.env.IS_DEV === "true" ? ExtensionMode.Development : ExtensionMode.Production
const extension: ClineExtensionContext["extension"] = {
@@ -155,38 +132,30 @@ export function initializeCliContext(config: CliContextConfig = {}): CliContextR
extensionKind: ExtensionKind.UI,
}
// Build the ClineExtensionContext shell. All storage delegates to storageContext —
// there are NO separate ClineFileStorage instances here.
const extensionContext: ClineExtensionContext = {
extension: extension,
extensionMode: EXTENSION_MODE,
// Set up KV stores (globalState has CLI-specific overrides)
globalState: new MementoStore(path.join(DATA_DIR, "globalState.json"), CLI_STATE_OVERRIDES),
secrets: new SecretStore(path.join(DATA_DIR, "secrets.json")),
// Set up URIs
// URIs / paths
storageUri: URI.file(WORKSPACE_STORAGE_DIR),
storagePath: WORKSPACE_STORAGE_DIR,
globalStorageUri: URI.file(DATA_DIR),
globalStoragePath: DATA_DIR,
// Logs
logUri: URI.file(DATA_DIR),
logPath: DATA_DIR,
extensionUri: URI.file(EXTENSION_DIR),
extensionPath: EXTENSION_DIR,
asAbsolutePath: (relPath: string) => path.join(EXTENSION_DIR, relPath),
subscriptions: [],
environmentVariableCollection: new EnvironmentVariableCollection() as any,
// Workspace state
workspaceState: new MementoStore(path.join(WORKSPACE_STORAGE_DIR, "workspaceState.json")),
}
return {
extensionContext,
storageContext,
DATA_DIR,
EXTENSION_DIR,
WORKSPACE_STORAGE_DIR,
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"declarationMap": false,
"noCheck": true,
"noResolve": true,
"outDir": "dist/types"
},
"include": [
"src/exports.ts",
"src/agent/public-types.ts",
"src/agent/ClineAgent.ts",
"src/agent/ClineSessionEmitter.ts",
"src/agent/types.ts",
"src/agent/messageTranslator.ts",
"src/agent/permissionHandler.ts"
]
}
+1
View File
@@ -13,6 +13,7 @@ export default defineConfig({
},
resolve: {
alias: {
vscode: path.resolve(__dirname, "src/vscode-shim.ts"),
// Match tsconfig paths - baseUrl is parent directory
"@": path.resolve(__dirname, "../src"),
"@api": path.resolve(__dirname, "../src/core/api"),
+4 -4
View File
@@ -202,15 +202,15 @@ If Cline can't access files or run commands:
Learn about Cline CLI's core capabilities and use cases.
</Card>
<Card title="Three Core Flows" icon="route" href="/cline-cli/three-core-flows">
Master interactive mode, headless automation, and multi-instance workflows.
<Card title="Headless Mode" icon="robot" href="/cline-cli/three-core-flows">
Run Cline autonomously in scripts, CI/CD pipelines, and automated workflows.
</Card>
<Card title="Skills" icon="graduation-cap" href="/features/skills">
<Card title="Skills" icon="graduation-cap" href="/customization/skills">
Understand how Cline's Skills work across all editors via ACP.
</Card>
<Card title="Hooks" icon="link" href="/features/hooks/index">
<Card title="Hooks" icon="link" href="/customization/hooks">
Learn how to enforce policies with Hooks in any editor.
</Card>
</Columns>

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